diff --git a/README.md b/README.md
index 717b4938..37b75433 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
-
+
@@ -73,6 +73,7 @@ Restart Claude Code. Context from previous sessions will automatically appear in
- ๐ฅ๏ธ **Web Viewer UI** - Real-time memory stream at http://localhost:37777
- ๐ค **Automatic Operation** - No manual intervention required
- ๐ **Citations** - Reference past decisions with `claude-mem://` URIs
+- ๐งช **Beta Channel** - Try experimental features like Endless Mode via version switching
---
@@ -92,6 +93,7 @@ npx mintlify dev
- **[Installation Guide](https://docs.claude-mem.ai/installation)** - Quick start & advanced installation
- **[Usage Guide](https://docs.claude-mem.ai/usage/getting-started)** - How Claude-Mem works automatically
- **[Search Tools](https://docs.claude-mem.ai/usage/search-tools)** - Query your project history with natural language
+- **[Beta Features](https://docs.claude-mem.ai/beta-features)** - Try experimental features like Endless Mode
### Best Practices
@@ -189,6 +191,44 @@ See [Search Tools Guide](https://docs.claude-mem.ai/usage/search-tools) for deta
---
+## Beta Features & Endless Mode
+
+Claude-Mem offers a **beta channel** with experimental features. Switch between stable and beta versions directly from the web viewer UI.
+
+### How to Try Beta
+
+1. Open http://localhost:37777
+2. Click Settings (gear icon)
+3. In **Version Channel**, click "Try Beta (Endless Mode)"
+4. Wait for the worker to restart
+
+Your memory data is preserved when switching versions.
+
+### Endless Mode (Beta)
+
+The flagship beta feature is **Endless Mode** - a biomimetic memory architecture that dramatically extends session length:
+
+**The Problem**: Standard Claude Code sessions hit context limits after ~50 tool uses. Each tool adds 1-10k+ tokens, and Claude re-synthesizes all previous outputs on every response (O(Nยฒ) complexity).
+
+**The Solution**: Endless Mode compresses tool outputs into ~500-token observations and transforms the transcript in real-time:
+
+```
+Working Memory (Context): Compressed observations (~500 tokens each)
+Archive Memory (Disk): Full tool outputs preserved for recall
+```
+
+**Expected Results**:
+- ~95% token reduction in context window
+- ~20x more tool uses before context exhaustion
+- Linear O(N) scaling instead of quadratic O(Nยฒ)
+- Full transcripts preserved for perfect recall
+
+**Caveats**: Adds latency (60-90s per tool for observation generation), still experimental.
+
+See [Beta Features Documentation](https://docs.claude-mem.ai/beta-features) for details.
+
+---
+
## What's New in v6.0.0
**๐ Major Session Management & Transcript Processing Improvements:**
diff --git a/docs/public/beta-features.mdx b/docs/public/beta-features.mdx
new file mode 100644
index 00000000..a925e690
--- /dev/null
+++ b/docs/public/beta-features.mdx
@@ -0,0 +1,144 @@
+---
+title: "Beta Features"
+description: "Try experimental features like Endless Mode before they're released"
+---
+
+# Beta Features
+
+Claude-Mem offers a beta channel for users who want to try experimental features before they're released to the stable channel.
+
+## Version Channel Switching
+
+You can switch between stable and beta versions directly from the web viewer UI at http://localhost:37777.
+
+### How to Access
+
+1. Open the Claude-Mem viewer at http://localhost:37777
+2. Click the **Settings** gear icon in the top-right
+3. Find the **Version Channel** section
+4. Click **Try Beta (Endless Mode)** to switch to beta, or **Switch to Stable** to return
+
+### What Happens When You Switch
+
+When switching versions:
+
+1. **Local changes are discarded** - Any modifications in the plugin directory are reset
+2. **Git fetch and checkout** - The installed plugin switches to the target branch
+3. **Dependencies reinstall** - `npm install` runs to ensure correct dependencies
+4. **Worker restarts automatically** - The background service restarts with the new version
+
+**Your memory data is always preserved.** The database at `~/.claude-mem/claude-mem.db` is not affected by version switching. All your observations, sessions, and summaries remain intact.
+
+### Version Indicators
+
+The Version Channel section shows your current status:
+
+- **Stable** (green badge) - You're running the production release
+- **Beta** (orange badge) - You're running the beta with experimental features
+
+You'll also see the exact branch name (e.g., `main` for stable, `beta/7.0` for beta).
+
+## Endless Mode (Beta)
+
+The flagship experimental feature in beta is **Endless Mode** - a biomimetic memory architecture that dramatically extends how long Claude can maintain context in a session.
+
+### The Problem Endless Mode Solves
+
+In standard Claude Code sessions:
+
+- Tool outputs (file reads, bash output, search results) accumulate in the context window
+- Each tool can add 1-10k+ tokens to the context
+- After ~50 tool uses, the context window fills up (~200k tokens)
+- You're forced to start a new session, losing conversational continuity
+
+Worse, Claude **re-synthesizes all previous tool outputs** on every response. This is O(Nยฒ) complexity - quadratically growing both in tokens and compute.
+
+### How Endless Mode Works
+
+Endless Mode applies a biomimetic memory architecture inspired by how human memory works:
+
+**Two-Tier Memory System:**
+
+```
+Working Memory (Context Window):
+ โ Compressed observations only (~500 tokens each)
+ โ Fast, efficient, manageable
+
+Archive Memory (Transcript File):
+ โ Full tool outputs preserved on disk
+ โ Perfect recall, searchable
+```
+
+**The Key Innovation**: After each tool use, Endless Mode:
+1. Waits for the worker to generate a compressed observation (blocking)
+2. Transforms the transcript file on disk
+3. Replaces the full tool output with the compressed observation
+4. Claude resumes with the compressed context
+
+This transforms O(Nยฒ) scaling into O(N) - linear instead of quadratic.
+
+### Expected Results
+
+Based on analysis of real sessions:
+
+- **Token savings**: ~95% reduction in context window usage
+- **Efficiency gain**: ~20x more tool uses before context exhaustion
+- **Quality preservation**: Observations cache the synthesis result, so no information is lost
+
+### Caveats
+
+Endless Mode is experimental:
+
+- **Adds latency** - Blocking hooks wait for observation generation (60-90s per tool use)
+- **Requires working database** - Observations must save successfully for transformation
+- **New architecture** - Less battle-tested than standard mode
+
+### When to Use Beta
+
+Consider switching to beta if you:
+
+- Frequently hit context window limits
+- Work on long, complex sessions with many tool uses
+- Want to help test and provide feedback on new features
+- Are comfortable with experimental software
+
+### When to Stay on Stable
+
+Stay on stable if you:
+
+- Need maximum reliability for critical work
+- Prefer battle-tested, production-ready features
+- Don't frequently hit context limits
+- Want the smoothest, fastest experience
+
+## Checking for Updates
+
+While on beta (or stable), you can check for updates:
+
+1. Open Settings in the viewer
+2. In the Version Channel section, click **Check for Updates**
+3. The plugin will pull the latest changes and restart
+
+## Switching Back
+
+If you encounter issues on beta:
+
+1. Open Settings in the viewer
+2. Click **Switch to Stable**
+3. Wait for the worker to restart
+
+Your memory data is preserved, and you'll be back on the stable release.
+
+## Providing Feedback
+
+If you encounter bugs or have feedback about beta features:
+
+- Open an issue at [GitHub Issues](https://github.com/thedotmack/claude-mem/issues)
+- Include your branch (`beta/7.0` etc.) in the report
+- Describe what you expected vs. what happened
+
+## Next Steps
+
+- [Configuration](configuration) - Customize other Claude-Mem settings
+- [Troubleshooting](troubleshooting) - Common issues and solutions
+- [Architecture Overview](architecture/overview) - Understand how Claude-Mem works
diff --git a/docs/public/configuration.mdx b/docs/public/configuration.mdx
index 805abf58..0605ce70 100644
--- a/docs/public/configuration.mdx
+++ b/docs/public/configuration.mdx
@@ -151,6 +151,26 @@ Search operations are now provided via:
- **HTTP API**: 10 endpoints on worker service port 37777
- **Progressive Disclosure**: Full instructions loaded on-demand only when needed
+## Version Channel
+
+Claude-Mem supports switching between stable and beta versions via the web viewer UI.
+
+### Accessing Version Channel
+
+1. Open the viewer at http://localhost:37777
+2. Click the Settings gear icon
+3. Find the **Version Channel** section
+
+### Switching Versions
+
+- **Try Beta**: Click "Try Beta (Endless Mode)" to switch to the beta branch with experimental features
+- **Switch to Stable**: Click "Switch to Stable" to return to the production release
+- **Check for Updates**: Pull the latest changes for your current branch
+
+**Your memory data is preserved** when switching versions. Only the plugin code changes.
+
+See [Beta Features](beta-features) for details on what's available in beta.
+
## PM2 Configuration
Worker service is managed by PM2 via `ecosystem.config.cjs`:
diff --git a/docs/public/docs.json b/docs/public/docs.json
index 464f5b0d..d398690a 100644
--- a/docs/public/docs.json
+++ b/docs/public/docs.json
@@ -36,7 +36,8 @@
"introduction",
"installation",
"usage/getting-started",
- "usage/search-tools"
+ "usage/search-tools",
+ "beta-features"
]
},
{
diff --git a/scripts/analyze-transformations-smart.js b/scripts/analyze-transformations-smart.js
new file mode 100644
index 00000000..782388a3
--- /dev/null
+++ b/scripts/analyze-transformations-smart.js
@@ -0,0 +1,410 @@
+#!/usr/bin/env node
+
+import fs from 'fs';
+import Database from 'better-sqlite3';
+import readline from 'readline';
+import path from 'path';
+import { homedir } from 'os';
+import { globSync } from 'glob';
+
+// =============================================================================
+// TOOL REPLACEMENT DECISION TABLE
+// =============================================================================
+//
+// KEY INSIGHT: Observations are the SEMANTIC SYNTHESIS of tool results.
+// They contain what Claude LEARNED, which is what future Claude needs.
+//
+// Tool | Replace OUTPUT? | Reason
+// ------------------|-----------------|----------------------------------------
+// Read | โ
YES | Observation = what was learned from file
+// Bash | โ
YES | Observation = what command revealed
+// Grep | โ
YES | Observation = what search found
+// Task | โ
YES | Observation = what agent discovered
+// WebFetch | โ
YES | Observation = what page contained
+// Glob | โ ๏ธ MAYBE | File lists are often small already
+// WebSearch | โ ๏ธ MAYBE | Results are moderate size
+// Edit | โ NO | OUTPUT is tiny ("success"), INPUT is ground truth
+// Write | โ NO | OUTPUT is tiny, INPUT is the file content
+// NotebookEdit | โ NO | OUTPUT is tiny, INPUT is the code
+// TodoWrite | โ NO | Both tiny
+// AskUserQuestion | โ NO | Both small, user input matters
+// mcp__* | โ ๏ธ MAYBE | Varies by tool
+//
+// NEVER REPLACE INPUT - it contains the action (diff, command, query, path)
+// ONLY REPLACE OUTPUT - swap raw results for semantic synthesis (observation)
+//
+// REPLACEMENT FORMAT:
+// Original output gets replaced with:
+// "[Strategically Omitted by Claude-Mem to save tokens]
+//
+// [Observation: Title here]
+// Facts: ...
+// Concepts: ..."
+// =============================================================================
+
+// Configuration
+const DB_PATH = path.join(homedir(), '.claude-mem', 'claude-mem.db');
+const MAX_TRANSCRIPTS = parseInt(process.env.MAX_TRANSCRIPTS || '500', 10);
+
+// Find transcript files (most recent first)
+const TRANSCRIPT_DIR = path.join(homedir(), '.claude/projects/-Users-alexnewman-Scripts-claude-mem');
+const allTranscriptFiles = globSync(path.join(TRANSCRIPT_DIR, '*.jsonl'));
+
+// Sort by modification time (most recent first), take MAX_TRANSCRIPTS
+const transcriptFiles = allTranscriptFiles
+ .map(f => ({ path: f, mtime: fs.statSync(f).mtime }))
+ .sort((a, b) => b.mtime - a.mtime)
+ .slice(0, MAX_TRANSCRIPTS)
+ .map(f => f.path);
+
+console.log(`Config: MAX_TRANSCRIPTS=${MAX_TRANSCRIPTS}`);
+console.log(`Using ${transcriptFiles.length} most recent transcript files (of ${allTranscriptFiles.length} total)\n`);
+
+// Map to store original content from transcript (both inputs and outputs)
+const originalContent = new Map();
+
+// Track contaminated (already transformed) transcripts
+let skippedTranscripts = 0;
+
+// Marker for already-transformed content (endless mode replacement format)
+const TRANSFORMATION_MARKER = '**Key Facts:**';
+
+// Auto-discover agent transcripts linked to main session
+async function discoverAgentFiles(mainTranscriptPath) {
+ console.log('Discovering linked agent transcripts...');
+
+ const agentIds = new Set();
+ const fileStream = fs.createReadStream(mainTranscriptPath);
+ const rl = readline.createInterface({
+ input: fileStream,
+ crlfDelay: Infinity
+ });
+
+ for await (const line of rl) {
+ if (!line.includes('agentId')) continue;
+
+ try {
+ const obj = JSON.parse(line);
+
+ // Check for agentId in toolUseResult
+ if (obj.toolUseResult?.agentId) {
+ agentIds.add(obj.toolUseResult.agentId);
+ }
+ } catch (e) {
+ // Skip malformed lines
+ }
+ }
+
+ // Build agent file paths
+ const directory = path.dirname(mainTranscriptPath);
+ const agentFiles = Array.from(agentIds).map(id =>
+ path.join(directory, `agent-${id}.jsonl`)
+ ).filter(filePath => fs.existsSync(filePath));
+
+ console.log(` โ Found ${agentIds.size} agent IDs`);
+ console.log(` โ ${agentFiles.length} agent files exist on disk\n`);
+
+ return agentFiles;
+}
+
+// Parse transcript to get BOTH tool_use (inputs) and tool_result (outputs) content
+// Returns true if transcript is clean, false if contaminated (already transformed)
+async function loadOriginalContentFromFile(filePath, fileLabel) {
+ const fileStream = fs.createReadStream(filePath);
+ const rl = readline.createInterface({
+ input: fileStream,
+ crlfDelay: Infinity
+ });
+
+ let count = 0;
+ let isContaminated = false;
+ const toolUseIdsFromThisFile = new Set();
+
+ for await (const line of rl) {
+ if (!line.includes('toolu_')) continue;
+
+ try {
+ const obj = JSON.parse(line);
+
+ if (obj.message?.content) {
+ for (const item of obj.message.content) {
+ // Capture tool_use (inputs)
+ if (item.type === 'tool_use' && item.id) {
+ const existing = originalContent.get(item.id) || { input: '', output: '', name: '' };
+ existing.input = JSON.stringify(item.input || {});
+ existing.name = item.name;
+ originalContent.set(item.id, existing);
+ toolUseIdsFromThisFile.add(item.id);
+ count++;
+ }
+
+ // Capture tool_result (outputs)
+ if (item.type === 'tool_result' && item.tool_use_id) {
+ const content = typeof item.content === 'string' ? item.content : JSON.stringify(item.content);
+
+ // Check for transformation marker - if found, transcript is contaminated
+ if (content.includes(TRANSFORMATION_MARKER)) {
+ isContaminated = true;
+ }
+
+ const existing = originalContent.get(item.tool_use_id) || { input: '', output: '', name: '' };
+ existing.output = content;
+ originalContent.set(item.tool_use_id, existing);
+ toolUseIdsFromThisFile.add(item.tool_use_id);
+ }
+ }
+ }
+ } catch (e) {
+ // Skip malformed lines
+ }
+ }
+
+ // If contaminated, remove all data from this file and report
+ if (isContaminated) {
+ for (const id of toolUseIdsFromThisFile) {
+ originalContent.delete(id);
+ }
+ console.log(` โ ๏ธ Skipped ${fileLabel} (already transformed)`);
+ return false;
+ }
+
+ if (count > 0) {
+ console.log(` โ Found ${count} tool uses in ${fileLabel}`);
+ }
+ return true;
+}
+
+async function loadOriginalContent() {
+ console.log('Loading original content from transcripts...');
+ console.log(` โ Scanning ${transcriptFiles.length} transcript files...\n`);
+
+ let cleanTranscripts = 0;
+
+ // Load from all transcript files
+ for (const transcriptFile of transcriptFiles) {
+ const filename = path.basename(transcriptFile);
+ const isClean = await loadOriginalContentFromFile(transcriptFile, filename);
+ if (isClean) {
+ cleanTranscripts++;
+ } else {
+ skippedTranscripts++;
+ }
+ }
+
+ // Also check for any agent files not already included
+ for (const transcriptFile of transcriptFiles) {
+ if (transcriptFile.includes('agent-')) continue; // Already an agent file
+ const agentFiles = await discoverAgentFiles(transcriptFile);
+ for (const agentFile of agentFiles) {
+ if (transcriptFiles.includes(agentFile)) continue; // Already processed
+ const filename = path.basename(agentFile);
+ const isClean = await loadOriginalContentFromFile(agentFile, `agent transcript (${filename})`);
+ if (!isClean) {
+ skippedTranscripts++;
+ }
+ }
+ }
+
+ console.log(`\nTotal: Loaded original content for ${originalContent.size} tool uses (inputs + outputs)`);
+ if (skippedTranscripts > 0) {
+ console.log(`โ ๏ธ Skipped ${skippedTranscripts} transcripts (already transformed with endless mode)`);
+ }
+ console.log();
+}
+
+// Strip __N suffix from tool_use_id to get base ID
+function getBaseToolUseId(id) {
+ return id ? id.replace(/__\d+$/, '') : id;
+}
+
+// Query observations from database using tool_use_ids found in transcripts
+// Handles suffixed IDs like toolu_abc__1, toolu_abc__2 matching transcript's toolu_abc
+function queryObservations() {
+ // Get tool_use_ids from the loaded transcript content
+ const toolUseIds = Array.from(originalContent.keys());
+
+ if (toolUseIds.length === 0) {
+ console.log('No tool use IDs found in transcripts\n');
+ return [];
+ }
+
+ console.log(`Querying observations for ${toolUseIds.length} tool use IDs from transcripts...`);
+
+ const db = new Database(DB_PATH, { readonly: true });
+
+ // Build LIKE clauses to match both exact IDs and suffixed variants (toolu_abc, toolu_abc__1, etc)
+ const likeConditions = toolUseIds.map(() => 'tool_use_id LIKE ?').join(' OR ');
+ const likeParams = toolUseIds.map(id => `${id}%`);
+
+ const query = `
+ SELECT
+ id,
+ tool_use_id,
+ type,
+ narrative,
+ title,
+ facts,
+ concepts,
+ LENGTH(COALESCE(facts,'')) as facts_len,
+ LENGTH(COALESCE(title,'')) + LENGTH(COALESCE(facts,'')) as title_facts_len,
+ LENGTH(COALESCE(title,'')) + LENGTH(COALESCE(facts,'')) + LENGTH(COALESCE(concepts,'')) as compact_len,
+ LENGTH(COALESCE(narrative,'')) as narrative_len,
+ LENGTH(COALESCE(title,'')) + LENGTH(COALESCE(narrative,'')) + LENGTH(COALESCE(facts,'')) + LENGTH(COALESCE(concepts,'')) as full_obs_len
+ FROM observations
+ WHERE ${likeConditions}
+ ORDER BY created_at DESC
+ `;
+
+ const observations = db.prepare(query).all(...likeParams);
+ db.close();
+
+ console.log(`Found ${observations.length} observations matching tool use IDs (including suffixed variants)\n`);
+
+ return observations;
+}
+
+// Tools eligible for OUTPUT replacement (observation = semantic synthesis of result)
+const REPLACEABLE_TOOLS = new Set(['Read', 'Bash', 'Grep', 'Task', 'WebFetch', 'Glob', 'WebSearch']);
+
+// Analyze OUTPUT-only replacement for eligible tools
+function analyzeTransformations(observations) {
+ console.log('='.repeat(110));
+ console.log('OUTPUT REPLACEMENT ANALYSIS (Eligible Tools Only)');
+ console.log('='.repeat(110));
+ console.log();
+ console.log('Eligible tools:', Array.from(REPLACEABLE_TOOLS).join(', '));
+ console.log();
+
+ // Group observations by BASE tool_use_id (strip __N suffix)
+ // This groups toolu_abc, toolu_abc__1, toolu_abc__2 together
+ const obsByToolId = new Map();
+ observations.forEach(obs => {
+ const baseId = getBaseToolUseId(obs.tool_use_id);
+ if (!obsByToolId.has(baseId)) {
+ obsByToolId.set(baseId, []);
+ }
+ obsByToolId.get(baseId).push(obs);
+ });
+
+ // Define strategies to test
+ const strategies = [
+ { name: 'facts_only', field: 'facts_len', desc: 'Facts only (~400 chars)' },
+ { name: 'title_facts', field: 'title_facts_len', desc: 'Title + Facts (~450 chars)' },
+ { name: 'compact', field: 'compact_len', desc: 'Title + Facts + Concepts (~500 chars)' },
+ { name: 'narrative', field: 'narrative_len', desc: 'Narrative only (~700 chars)' },
+ { name: 'full', field: 'full_obs_len', desc: 'Full observation (~1200 chars)' }
+ ];
+
+ // Track results per strategy
+ const results = {};
+ strategies.forEach(s => {
+ results[s.name] = {
+ transforms: 0,
+ noTransform: 0,
+ saved: 0,
+ totalOriginal: 0
+ };
+ });
+
+ // Track stats
+ let eligible = 0;
+ let ineligible = 0;
+ let noTranscript = 0;
+ const toolCounts = {};
+
+ // Analyze each tool use
+ obsByToolId.forEach((obsArray, toolUseId) => {
+ const original = originalContent.get(toolUseId);
+ const toolName = original?.name || 'unknown';
+ const outputLen = original?.output?.length || 0;
+
+ // Skip if no transcript data
+ if (!original || outputLen === 0) {
+ noTranscript++;
+ return;
+ }
+
+ // Skip if tool not eligible for replacement
+ if (!REPLACEABLE_TOOLS.has(toolName)) {
+ ineligible++;
+ return;
+ }
+
+ eligible++;
+ toolCounts[toolName] = (toolCounts[toolName] || 0) + 1;
+
+ // Sum lengths across ALL observations for this tool use (handles multiple obs per tool_use_id)
+ // Test each strategy - OUTPUT replacement only
+ strategies.forEach(strategy => {
+ const obsLen = obsArray.reduce((sum, obs) => sum + (obs[strategy.field] || 0), 0);
+ const r = results[strategy.name];
+
+ r.totalOriginal += outputLen;
+
+ if (obsLen > 0 && obsLen < outputLen) {
+ r.transforms++;
+ r.saved += (outputLen - obsLen);
+ } else {
+ r.noTransform++;
+ }
+ });
+ });
+
+ // Print results
+ console.log('TOOL BREAKDOWN:');
+ Object.entries(toolCounts).sort((a, b) => b[1] - a[1]).forEach(([tool, count]) => {
+ console.log(` ${tool}: ${count}`);
+ });
+ console.log();
+ console.log('-'.repeat(100));
+ console.log(`Eligible tool uses: ${eligible}`);
+ console.log(`Ineligible (Edit/Write/etc): ${ineligible}`);
+ console.log(`No transcript data: ${noTranscript}`);
+ console.log('-'.repeat(100));
+ console.log();
+ console.log('Strategy Transforms No Transform Chars Saved Original Size Savings %');
+ console.log('-'.repeat(100));
+
+ strategies.forEach(strategy => {
+ const r = results[strategy.name];
+ const pct = r.totalOriginal > 0 ? ((r.saved / r.totalOriginal) * 100).toFixed(1) : '0.0';
+ console.log(
+ `${strategy.desc.padEnd(35)} ${String(r.transforms).padStart(10)} ${String(r.noTransform).padStart(12)} ${String(r.saved.toLocaleString()).padStart(13)} ${String(r.totalOriginal.toLocaleString()).padStart(15)} ${pct.padStart(8)}%`
+ );
+ });
+
+ console.log('-'.repeat(100));
+ console.log();
+
+ // Find best strategy
+ let bestStrategy = null;
+ let bestSavings = 0;
+ strategies.forEach(strategy => {
+ if (results[strategy.name].saved > bestSavings) {
+ bestSavings = results[strategy.name].saved;
+ bestStrategy = strategy;
+ }
+ });
+
+ if (bestStrategy) {
+ const r = results[bestStrategy.name];
+ const pct = ((r.saved / r.totalOriginal) * 100).toFixed(1);
+ console.log(`BEST STRATEGY: ${bestStrategy.desc}`);
+ console.log(` - Transforms ${r.transforms} of ${eligible} eligible tool uses (${((r.transforms/eligible)*100).toFixed(1)}%)`);
+ console.log(` - Saves ${r.saved.toLocaleString()} of ${r.totalOriginal.toLocaleString()} chars (${pct}% reduction)`);
+ }
+
+ console.log();
+}
+
+// Main execution
+async function main() {
+ await loadOriginalContent();
+ const observations = queryObservations();
+ analyzeTransformations(observations);
+}
+
+main().catch(error => {
+ console.error('Fatal error:', error);
+ process.exit(1);
+});
diff --git a/scripts/sync-marketplace.cjs b/scripts/sync-marketplace.cjs
new file mode 100644
index 00000000..d0f8c9b6
--- /dev/null
+++ b/scripts/sync-marketplace.cjs
@@ -0,0 +1,64 @@
+#!/usr/bin/env node
+/**
+ * Protected sync-marketplace script
+ *
+ * Prevents accidental rsync overwrite when installed plugin is on beta branch.
+ * If on beta, the user should use the UI to update instead.
+ */
+
+const { execSync } = require('child_process');
+const { existsSync } = require('fs');
+const path = require('path');
+const os = require('os');
+
+const INSTALLED_PATH = path.join(os.homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
+
+function getCurrentBranch() {
+ try {
+ if (!existsSync(path.join(INSTALLED_PATH, '.git'))) {
+ return null;
+ }
+ return execSync('git rev-parse --abbrev-ref HEAD', {
+ cwd: INSTALLED_PATH,
+ encoding: 'utf-8',
+ stdio: ['pipe', 'pipe', 'pipe']
+ }).trim();
+ } catch {
+ return null;
+ }
+}
+
+const branch = getCurrentBranch();
+
+if (branch && branch !== 'main') {
+ console.log('');
+ console.log('\x1b[33m%s\x1b[0m', `WARNING: Installed plugin is on beta branch: ${branch}`);
+ console.log('\x1b[33m%s\x1b[0m', 'Running rsync would overwrite beta code.');
+ console.log('');
+ console.log('Options:');
+ console.log(' 1. Use UI at http://localhost:37777 to update beta');
+ console.log(' 2. Switch to stable in UI first, then run sync');
+ console.log(' 3. Force rsync: npm run sync-marketplace:force');
+ console.log('');
+ process.exit(1);
+}
+
+// Normal rsync for main branch or fresh install
+console.log('Syncing to marketplace...');
+try {
+ execSync(
+ 'rsync -av --delete --exclude=.git ./ ~/.claude/plugins/marketplaces/thedotmack/',
+ { stdio: 'inherit' }
+ );
+
+ console.log('Running npm install in marketplace...');
+ execSync(
+ 'cd ~/.claude/plugins/marketplaces/thedotmack/ && npm install',
+ { stdio: 'inherit' }
+ );
+
+ console.log('\x1b[32m%s\x1b[0m', 'Sync complete!');
+} catch (error) {
+ console.error('\x1b[31m%s\x1b[0m', 'Sync failed:', error.message);
+ process.exit(1);
+}