specmem-hardwicksoftware 3.7.32 → 3.7.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,6 +26,9 @@ import {
26
26
  handleRequest,
27
27
  log,
28
28
  togglePause,
29
+ setPaused,
30
+ startReaper,
31
+ hasLiveProjects,
29
32
  PROXY_PORT,
30
33
  PORT_FILE,
31
34
  PID_FILE,
@@ -37,9 +40,9 @@ import {
37
40
  // Precondition checks
38
41
  // ============================================================================
39
42
 
40
- if (existsSync(DISABLED_FILE)) {
41
- process.exit(0);
42
- }
43
+ // If disabled, start in paused (passthrough) mode instead of exiting.
44
+ // The proxy must always be in the request path for seamless on/off toggling.
45
+ const startPaused = existsSync(DISABLED_FILE);
43
46
 
44
47
  // ============================================================================
45
48
  // Port collision check
@@ -73,6 +76,9 @@ const KYS_IDLE_THRESHOLD = 120000; // 2 minutes without Claude → exit
73
76
  const KYS_GRACE_PERIOD = 60000; // 60s grace after startup
74
77
 
75
78
  function isClaudeAlive() {
79
+ // Check registry first — much cheaper than pgrep
80
+ if (hasLiveProjects()) return true;
81
+ // Fallback to pgrep for cases where MCP didn't register (old versions)
76
82
  try {
77
83
  const result = execSync(
78
84
  'pgrep -f "claude" 2>/dev/null',
@@ -122,7 +128,15 @@ async function main() {
122
128
  server.listen(PROXY_PORT, '127.0.0.1', () => {
123
129
  // Write port file on successful bind — MCP and bashrc use this to find us
124
130
  writeFileSync(PORT_FILE, String(PROXY_PORT), { mode: 0o644 });
125
- log('info', `Daemon started: PID=${process.pid} port=${PROXY_PORT}`);
131
+ // If disabled flag existed at startup, start in passthrough mode
132
+ if (startPaused) {
133
+ setPaused(true);
134
+ log('info', `Daemon started in PASSTHROUGH mode (disabled flag): PID=${process.pid} port=${PROXY_PORT}`);
135
+ } else {
136
+ log('info', `Daemon started: PID=${process.pid} port=${PROXY_PORT}`);
137
+ }
138
+ // Start the stale project reaper
139
+ startReaper();
126
140
  });
127
141
 
128
142
  // --- Signal handlers ---
@@ -2072,6 +2072,14 @@ export class SpecMemServer {
2072
2072
  catch (err) {
2073
2073
  logger.warn({ error: err }, 'Error closing MCP server');
2074
2074
  }
2075
+ // Step 6.5: Deregister from compaction proxy daemon
2076
+ try {
2077
+ stopCompactionProxy();
2078
+ logger.debug('Compaction proxy deregistered');
2079
+ }
2080
+ catch (err) {
2081
+ logger.debug({ error: err }, 'Could not deregister from compaction proxy');
2082
+ }
2075
2083
  // FIX 6.10 + CRIT-2: unhandledRejection/uncaughtException use once() so auto-remove.
2076
2084
  // Only remove SIGINT/SIGTERM/SIGHUP which may still be registered.
2077
2085
  if (this._processHandlers) {
@@ -30,6 +30,7 @@ export class WatcherManager {
30
30
  syncInProgress = false;
31
31
  syncTimeout = null;
32
32
  lastLowScoreResyncAt = 0;
33
+ lastLowScoreResyncScore = null; // track score at last resync to detect drops
33
34
  constructor(config) {
34
35
  // Create handler first - it's the core component
35
36
  this.handler = new AutoUpdateTheMemories(config.handler);
@@ -120,21 +121,34 @@ export class WatcherManager {
120
121
  logger.debug('periodic sync: indexing pending, skipping resync');
121
122
  return;
122
123
  }
123
- // Low-score trigger: if score drops below threshold, force resync w/ debounce
124
+ // Low-score trigger: only force resync if score DROPPED by >=10% since last resync
125
+ // This prevents infinite resync loops when score is stuck below threshold
124
126
  const LOW_SCORE_THRESHOLD = parseFloat(process.env['SPECMEM_LOW_SCORE_THRESHOLD'] || '0.85');
127
+ const LOW_SCORE_DROP_THRESHOLD = parseFloat(process.env['SPECMEM_LOW_SCORE_DROP_THRESHOLD'] || '0.10');
125
128
  const LOW_SCORE_DEBOUNCE_MS = parseInt(process.env['SPECMEM_LOW_SCORE_DEBOUNCE_MS'] || String(15 * 60 * 1000), 10);
126
129
  if (report.syncScore < LOW_SCORE_THRESHOLD) {
130
+ // First time seeing low score — always resync
131
+ // After that, only resync if score dropped by >=10% from the post-resync score
132
+ const scoreDrop = this.lastLowScoreResyncScore !== null
133
+ ? this.lastLowScoreResyncScore - report.syncScore
134
+ : Infinity; // first time = always trigger
135
+ if (scoreDrop < LOW_SCORE_DROP_THRESHOLD) {
136
+ logger.warn({ syncScore: report.syncScore, lastResyncScore: this.lastLowScoreResyncScore, scoreDrop }, 'sync score below threshold but has not dropped by >=10% since last resync — skipping');
137
+ return;
138
+ }
127
139
  const now = Date.now();
128
140
  const debounceRemaining = LOW_SCORE_DEBOUNCE_MS - (now - this.lastLowScoreResyncAt);
129
141
  if (debounceRemaining > 0) {
130
142
  logger.warn({ syncScore: report.syncScore, debounceRemainingSec: Math.round(debounceRemaining / 1000) }, 'sync score below threshold but debounce active — skipping forced resync');
131
143
  } else {
132
144
  this.lastLowScoreResyncAt = now;
133
- logger.warn({ syncScore: report.syncScore, threshold: LOW_SCORE_THRESHOLD }, 'sync score below threshold — forcing resync');
145
+ logger.warn({ syncScore: report.syncScore, threshold: LOW_SCORE_THRESHOLD, scoreDrop }, 'sync score below threshold with >=10% drop — forcing resync');
134
146
  const resyncResult = await this.syncChecker.resyncEverythingFrFr();
135
147
  logger.info({ filesAdded: resyncResult.filesAdded, filesUpdated: resyncResult.filesUpdated, errors: resyncResult.errors.length }, 'low-score forced resync complete');
136
148
  const postReport = await this.syncChecker.checkSync();
137
149
  await this.writeSyncScore(postReport.syncScore);
150
+ // Track the post-resync score so we can detect future drops
151
+ this.lastLowScoreResyncScore = postReport.syncScore;
138
152
  }
139
153
  return;
140
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specmem-hardwicksoftware",
3
- "version": "3.7.32",
3
+ "version": "3.7.33",
4
4
  "type": "module",
5
5
  "description": "Your Claude Code sessions don't have to start from scratch anymore — SpecMem gives your AI real memory. It won't forget your conversations, your code, or your architecture decisions between sessions. That's the whole point. Semantic code indexing that actually works: TypeScript, JavaScript, Python, Go, Rust, Java, Kotlin, C, C++, HTML and more. It doesn't just track functions — it gets classes, methods, fields, constants, enums, macros, imports, structs, the whole codebase graph. There's chat memory too, powered by pgvector embeddings. You've also got token compression, team coordination, multi-agent comms, and file watching built in. 74+ MCP tools. Runs on PostgreSQL + Docker. It's kind of a big deal. justcalljon.pro",
6
6
  "main": "dist/index.js",
@@ -1014,7 +1014,10 @@ function configureMCP() {
1014
1014
  console.log(`${C.dim}Backed up to ${backupPath}${C.reset}`);
1015
1015
  }
1016
1016
 
1017
- fs.writeFileSync(CLAUDE_JSON, JSON.stringify(claudeJson, null, 2));
1017
+ // ATOMIC WRITE: write to temp then rename to prevent corruption
1018
+ const tmpClaudePath = CLAUDE_JSON + '.tmp.' + process.pid;
1019
+ fs.writeFileSync(tmpClaudePath, JSON.stringify(claudeJson, null, 2));
1020
+ fs.renameSync(tmpClaudePath, CLAUDE_JSON);
1018
1021
  console.log(`${C.green}✓${C.reset} Configured MCP server: specmem for project ${PROJECT_PATH}`);
1019
1022
 
1020
1023
  // ACK verification - re-read and verify
@@ -1292,53 +1292,50 @@ fi
1292
1292
  }
1293
1293
 
1294
1294
  // ── Step 3b: Add ANTHROPIC_BASE_URL proxy routing to bashrc ─────────────
1295
- // Routes Claude Code API calls through the SpecMem compaction proxy for
1296
- // automatic token compression (40-80% savings). Reads port from the port
1297
- // file written by the proxy on startup. Only activates if proxy has run.
1295
+ // Always routes Claude Code API calls through the compaction proxy.
1296
+ // Proxy runs in passthrough mode when disabled seamless on/off toggling.
1298
1297
  // Does NOT touch ANTHROPIC_API_KEY — only sets the base URL.
1299
1298
  const proxyMarker = '# specmem-proxy-env';
1299
+ const defaultProxyPort = process.env.COMPACTION_PROXY_PORT || '4080';
1300
1300
  let hasProxyFix = false;
1301
1301
  try {
1302
1302
  if (fs.existsSync(bashrcPath)) {
1303
1303
  const currentBashrc = fs.readFileSync(bashrcPath, 'utf8');
1304
1304
  hasProxyFix = currentBashrc.includes(proxyMarker);
1305
+ // If old conditional version exists, replace it with the unconditional one
1306
+ if (hasProxyFix && currentBashrc.includes('.compaction-proxy-port')) {
1307
+ const cleaned = currentBashrc.replace(/\n?# specmem-proxy-env\n(?:# [^\n]*\n)*if \[ -f "\$HOME\/\.claude\/\.compaction-proxy-port" \];[\s\S]*?fi\n?/g, '');
1308
+ fs.writeFileSync(bashrcPath, cleaned);
1309
+ hasProxyFix = false; // Will re-add the new version below
1310
+ initLog('Replaced old conditional proxy bashrc block with unconditional version');
1311
+ }
1305
1312
  }
1306
1313
  } catch (e) { /* no .bashrc */ }
1307
1314
 
1308
1315
  if (!hasProxyFix) {
1309
1316
  const proxyFix = `
1310
1317
  ${proxyMarker}
1311
- # SpecMem: Route Claude Code API calls through compaction proxy for token savings
1312
- # Reads port from proxy port file — only activates if proxy has been started
1313
- if [ -f "$HOME/.claude/.compaction-proxy-port" ]; then
1314
- _specmem_proxy_port="$(cat "$HOME/.claude/.compaction-proxy-port" 2>/dev/null)"
1315
- if [ -n "$_specmem_proxy_port" ]; then
1316
- export ANTHROPIC_BASE_URL="http://127.0.0.1:$_specmem_proxy_port"
1317
- fi
1318
- unset _specmem_proxy_port
1319
- fi
1318
+ # SpecMem: Always route API calls through compaction proxy (passthrough when disabled)
1319
+ export ANTHROPIC_BASE_URL="http://127.0.0.1:\${COMPACTION_PROXY_PORT:-${defaultProxyPort}}"
1320
1320
  `;
1321
1321
  try {
1322
1322
  fs.appendFileSync(bashrcPath, proxyFix);
1323
1323
  result.proxyEnvFixed = true;
1324
1324
  result.fixed = true;
1325
- initLog('Added ANTHROPIC_BASE_URL proxy routing to .bashrc');
1325
+ initLog('Added unconditional ANTHROPIC_BASE_URL proxy routing to .bashrc');
1326
1326
  } catch (e) {
1327
1327
  initLog('Failed to write .bashrc proxy fix', e);
1328
1328
  }
1329
1329
  }
1330
1330
 
1331
- // Also set for current session if port file exists
1332
- try {
1333
- const portFile = path.join(homeDir, '.claude', '.compaction-proxy-port');
1334
- if (fs.existsSync(portFile)) {
1335
- const port = fs.readFileSync(portFile, 'utf8').trim();
1336
- if (port && !process.env.ANTHROPIC_BASE_URL) {
1337
- process.env.ANTHROPIC_BASE_URL = `http://127.0.0.1:${port}`;
1338
- initLog(`Set ANTHROPIC_BASE_URL=http://127.0.0.1:${port} for current session`);
1339
- }
1331
+ // Always set for current session proxy is always in the path
1332
+ {
1333
+ const proxyUrl = `http://127.0.0.1:${defaultProxyPort}`;
1334
+ if (process.env.ANTHROPIC_BASE_URL !== proxyUrl) {
1335
+ process.env.ANTHROPIC_BASE_URL = proxyUrl;
1336
+ initLog(`Set ANTHROPIC_BASE_URL=${proxyUrl} for current session`);
1340
1337
  }
1341
- } catch (e) { /* non-fatal */ }
1338
+ }
1342
1339
 
1343
1340
  // ── Step 4: Set TERM for current session ────────────────────────────────
1344
1341
  if (result.needed && !term.includes('256color')) {
@@ -7266,16 +7263,9 @@ async function launchScreenSessions(projectPath, ui) {
7266
7263
  // Uses screen hardcopy to tmpfs on-demand instead of continuous logging
7267
7264
  // -h 5000 sets scrollback buffer to 5000 lines for hardcopy capture
7268
7265
  const claudeBin = getClaudeBinary();
7269
- // FIX: Set ANTHROPIC_BASE_URL for compaction proxy routing.
7270
- // bash -c is non-interactive so .bashrc isn't sourced — read port file directly.
7271
- const portFile = path.join(os.homedir(), '.claude', '.compaction-proxy-port');
7272
- let proxyEnv = '';
7273
- try {
7274
- if (fs.existsSync(portFile)) {
7275
- const port = fs.readFileSync(portFile, 'utf8').trim();
7276
- if (port) proxyEnv = `ANTHROPIC_BASE_URL="http://127.0.0.1:${port}" `;
7277
- }
7278
- } catch (e) { /* non-fatal */ }
7266
+ // Always set ANTHROPIC_BASE_URL proxy runs in passthrough mode when disabled
7267
+ const proxyPort = process.env.COMPACTION_PROXY_PORT || '4080';
7268
+ const proxyEnv = `ANTHROPIC_BASE_URL="http://127.0.0.1:${proxyPort}" `;
7279
7269
  execSync(`screen -h 5000 -dmS ${claudeSession} bash -c 'cd "${projectPath}" && ${proxyEnv}SPECMEM_DASHBOARD=1 "${claudeBin}" 2>&1; exec bash'`, { stdio: 'ignore' });
7280
7270
  await sleep(300);
7281
7271
 
@@ -8843,7 +8833,10 @@ CREATE INDEX IF NOT EXISTS idx_embedding_queue_project ON embedding_queue (proje
8843
8833
  };
8844
8834
  claudeJson.projects[projectPath].hasTrustDialogAccepted = true;
8845
8835
 
8846
- fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
8836
+ // ATOMIC WRITE: write to temp then rename to prevent corruption
8837
+ const tmpPath1 = claudeJsonPath + '.tmp.' + process.pid;
8838
+ fs.writeFileSync(tmpPath1, JSON.stringify(claudeJson, null, 2));
8839
+ fs.renameSync(tmpPath1, claudeJsonPath);
8847
8840
  initLog('[SETUP] ✓ MCP server configured in ~/.claude.json for ' + projectPath);
8848
8841
  } catch (e) {
8849
8842
  initLog('[SETUP] ⚠ Failed to configure MCP in .claude.json: ' + e.message);
@@ -10555,7 +10548,10 @@ priority=999
10555
10548
  };
10556
10549
  claudeJson.projects[projectPath].hasTrustDialogAccepted = true;
10557
10550
 
10558
- fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
10551
+ // ATOMIC WRITE: write to temp then rename to prevent corruption
10552
+ const tmpPath2 = claudeJsonPath + '.tmp.' + process.pid;
10553
+ fs.writeFileSync(tmpPath2, JSON.stringify(claudeJson, null, 2));
10554
+ fs.renameSync(tmpPath2, claudeJsonPath);
10559
10555
  initLog('[CONTAINER] ✓ MCP server configured in ~/.claude.json for ' + projectPath);
10560
10556
  ui.setStatus('MCP configured');
10561
10557
 
@@ -119,7 +119,10 @@ function cleanClaudeConfigs() {
119
119
  if (changed) {
120
120
  const backup = claudeJsonPath + '.pre-uninstall.' + Date.now();
121
121
  fs.copyFileSync(claudeJsonPath, backup);
122
- fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
122
+ // ATOMIC WRITE: write to temp then rename to prevent corruption
123
+ const tmpPath = claudeJsonPath + '.tmp.' + process.pid;
124
+ fs.writeFileSync(tmpPath, JSON.stringify(claudeJson, null, 2));
125
+ fs.renameSync(tmpPath, claudeJsonPath);
123
126
  console.log(` ${c.green}✓${c.reset} Removed SpecMem MCP server from .claude.json`);
124
127
  console.log(` ${c.dim} Backup: ${backup}${c.reset}`);
125
128
  }
@@ -35,12 +35,12 @@
35
35
  },
36
36
  "resources": {
37
37
  "cpuMin": 10,
38
- "cpuMax": 45,
38
+ "cpuMax": 30,
39
39
  "cpuCoreMin": 1,
40
40
  "cpuCoreMax": 4,
41
41
  "ramMinMb": 4000,
42
42
  "ramMaxMb": 15000,
43
- "updatedAt": "2026-02-24T19:22:11.693Z"
43
+ "updatedAt": "2026-02-24T20:01:48.721Z"
44
44
  },
45
45
  "resourcePool": {
46
46
  "embedding": {
@@ -95,7 +95,7 @@
95
95
  },
96
96
  "heavyOps": {
97
97
  "enabled": true,
98
- "enabledAt": "2026-02-24T11:57:35.508Z",
98
+ "enabledAt": "2026-02-26T14:18:50.747Z",
99
99
  "originalBatchSize": 32,
100
100
  "batchSizeMultiplier": 2,
101
101
  "throttleReduction": 0.2
@@ -1,6 +1,6 @@
1
1
  ; ============================================
2
2
  ; SPECMEM BRAIN CONTAINER - DYNAMIC SUPERVISORD CONFIG
3
- ; Generated by specmem-init at 2026-02-24T19:19:59.260Z
3
+ ; Generated by specmem-init at 2026-02-26T14:42:59.603Z
4
4
  ; Thread counts from model-config.json resourcePool
5
5
  ; ============================================
6
6
 
@@ -1,82 +1,118 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 240" width="700" height="240">
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" width="800" height="420">
2
2
  <defs>
3
- <linearGradient id="termBg" x1="0%" y1="0%" x2="100%" y2="100%">
3
+ <linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
4
4
  <stop offset="0%" style="stop-color:#0d1117"/>
5
5
  <stop offset="100%" style="stop-color:#161b22"/>
6
6
  </linearGradient>
7
- <linearGradient id="accentGradient" x1="0%" y1="0%" x2="100%" y2="0%">
7
+ <linearGradient id="cardGrad" x1="0%" y1="0%" x2="100%" y2="100%">
8
+ <stop offset="0%" style="stop-color:#1c2128;stop-opacity:0.9"/>
9
+ <stop offset="100%" style="stop-color:#21262d;stop-opacity:0.85"/>
10
+ </linearGradient>
11
+ <linearGradient id="titleGrad" x1="0%" y1="0%" x2="100%" y2="0%">
8
12
  <stop offset="0%" style="stop-color:#00bfff"/>
9
- <stop offset="100%" style="stop-color:#a855f7"/>
13
+ <stop offset="50%" style="stop-color:#a855f7"/>
14
+ <stop offset="100%" style="stop-color:#00bfff"/>
10
15
  </linearGradient>
11
- <linearGradient id="stepGradient" x1="0%" y1="0%" x2="100%" y2="100%">
16
+ <linearGradient id="cyanPurple" x1="0%" y1="0%" x2="100%" y2="0%">
12
17
  <stop offset="0%" style="stop-color:#00bfff"/>
13
18
  <stop offset="100%" style="stop-color:#a855f7"/>
14
19
  </linearGradient>
15
- <filter id="checkGlow" x="-50%" y="-50%" width="200%" height="200%">
16
- <feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blur"/>
17
- <feFlood flood-color="#22c55e" flood-opacity="0.6"/>
18
- <feComposite in2="blur" operator="in"/>
19
- <feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
20
- </filter>
21
- <filter id="stepGlow" x="-50%" y="-50%" width="200%" height="200%">
22
- <feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur"/>
23
- <feFlood flood-color="#00bfff" flood-opacity="0.3"/>
24
- <feComposite in2="blur" operator="in"/>
25
- <feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
26
- </filter>
20
+ <linearGradient id="greenGrad" x1="0%" y1="0%" x2="100%" y2="0%">
21
+ <stop offset="0%" style="stop-color:#22c55e"/>
22
+ <stop offset="100%" style="stop-color:#4ade80"/>
23
+ </linearGradient>
27
24
  </defs>
28
25
 
29
- <rect width="700" height="240" rx="12" ry="12" fill="url(#termBg)"/>
30
- <rect width="700" height="240" rx="12" ry="12" fill="none" stroke="url(#accentGradient)" stroke-width="1" stroke-opacity="0.3"/>
26
+ <!-- Background -->
27
+ <rect width="800" height="420" rx="16" ry="16" fill="url(#bgGrad)"/>
28
+ <rect width="800" height="420" rx="16" ry="16" fill="none" stroke="url(#cyanPurple)" stroke-width="1" stroke-opacity="0.25"/>
31
29
 
32
- <text x="350" y="30" text-anchor="middle" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" font-size="15" font-weight="700" fill="#f5f5f5">Quick Install</text>
33
-
34
- <!-- Connecting line -->
35
- <line x1="50" y1="78" x2="50" y2="168" stroke="#2d333b" stroke-width="2" stroke-dasharray="4,4"/>
30
+ <!-- Title -->
31
+ <text x="400" y="38" text-anchor="middle" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" font-size="22" font-weight="700" fill="url(#titleGrad)">Quick Install</text>
32
+ <text x="400" y="60" text-anchor="middle" font-family="system-ui, sans-serif" font-size="12" fill="#8b949e">3 steps. No root required. Works on Debian, Ubuntu, Mint, Kali.</text>
36
33
 
37
34
  <!-- Step 1 -->
38
- <g transform="translate(30, 48)">
39
- <circle cx="20" cy="20" r="13" fill="none" stroke="url(#stepGradient)" stroke-width="2" filter="url(#stepGlow)"/>
40
- <text x="20" y="25" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="700" fill="#00bfff">1</text>
41
- <rect x="44" y="3" width="618" height="34" rx="6" ry="6" fill="#1a1f26"/>
42
- <text x="57" y="25" font-family="'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace" font-size="13" fill="#6e7681">$</text>
43
- <text x="72" y="25" font-family="'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace" font-size="13">
35
+ <g transform="translate(30, 78)">
36
+ <rect width="740" height="72" rx="12" fill="url(#cardGrad)" stroke="#30363d" stroke-width="1"/>
37
+ <rect width="740" height="3" rx="1.5" fill="url(#cyanPurple)"/>
38
+ <!-- Step badge -->
39
+ <circle cx="36" cy="42" r="16" fill="none" stroke="#00bfff" stroke-width="1.5" stroke-opacity="0.6"/>
40
+ <text x="36" y="47" text-anchor="middle" font-family="system-ui, sans-serif" font-size="14" font-weight="700" fill="#00bfff">1</text>
41
+ <!-- Command -->
42
+ <rect x="64" y="20" width="660" height="32" rx="6" fill="#0d1117" stroke="#21262d" stroke-width="1"/>
43
+ <text x="78" y="40" font-family="'SF Mono', 'Fira Code', Consolas, monospace" font-size="13" fill="#6e7681">$</text>
44
+ <text x="94" y="40" font-family="'SF Mono', 'Fira Code', Consolas, monospace" font-size="13">
44
45
  <tspan fill="#ff7b72">npm</tspan>
45
- <tspan fill="#f5f5f5"> install </tspan>
46
+ <tspan fill="#e6edf3"> install </tspan>
46
47
  <tspan fill="#ffa657">-g</tspan>
47
- <tspan fill="#f5f5f5"> specmem-hardwicksoftware</tspan>
48
+ <tspan fill="#e6edf3"> specmem-hardwicksoftware</tspan>
48
49
  </text>
50
+ <text x="64" y="65" font-family="system-ui, sans-serif" font-size="10" fill="#6e7681">Install globally from npm</text>
49
51
  </g>
50
52
 
53
+ <!-- Connector line -->
54
+ <line x1="66" y1="150" x2="66" y2="168" stroke="#30363d" stroke-width="2" stroke-dasharray="3,3"/>
55
+
51
56
  <!-- Step 2 -->
52
- <g transform="translate(30, 96)">
53
- <circle cx="20" cy="20" r="13" fill="none" stroke="url(#stepGradient)" stroke-width="2" filter="url(#stepGlow)"/>
54
- <text x="20" y="25" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="700" fill="#00bfff">2</text>
55
- <rect x="44" y="3" width="618" height="34" rx="6" ry="6" fill="#1a1f26"/>
56
- <text x="57" y="25" font-family="'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace" font-size="13" fill="#6e7681">$</text>
57
- <text x="72" y="25" font-family="'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace" font-size="13">
57
+ <g transform="translate(30, 168)">
58
+ <rect width="740" height="72" rx="12" fill="url(#cardGrad)" stroke="#30363d" stroke-width="1"/>
59
+ <rect width="740" height="3" rx="1.5" fill="url(#cyanPurple)"/>
60
+ <circle cx="36" cy="42" r="16" fill="none" stroke="#a855f7" stroke-width="1.5" stroke-opacity="0.6"/>
61
+ <text x="36" y="47" text-anchor="middle" font-family="system-ui, sans-serif" font-size="14" font-weight="700" fill="#a855f7">2</text>
62
+ <rect x="64" y="20" width="660" height="32" rx="6" fill="#0d1117" stroke="#21262d" stroke-width="1"/>
63
+ <text x="78" y="40" font-family="'SF Mono', 'Fira Code', Consolas, monospace" font-size="13" fill="#6e7681">$</text>
64
+ <text x="94" y="40" font-family="'SF Mono', 'Fira Code', Consolas, monospace" font-size="13">
58
65
  <tspan fill="#ff7b72">cd</tspan>
59
- <tspan fill="#f5f5f5"> /your/project/directory</tspan>
66
+ <tspan fill="#e6edf3"> /your/project/directory</tspan>
60
67
  </text>
68
+ <text x="64" y="65" font-family="system-ui, sans-serif" font-size="10" fill="#6e7681">Navigate to your project root</text>
61
69
  </g>
62
70
 
71
+ <!-- Connector line -->
72
+ <line x1="66" y1="240" x2="66" y2="258" stroke="#30363d" stroke-width="2" stroke-dasharray="3,3"/>
73
+
63
74
  <!-- Step 3 -->
64
- <g transform="translate(30, 144)">
65
- <circle cx="20" cy="20" r="13" fill="none" stroke="url(#stepGradient)" stroke-width="2" filter="url(#stepGlow)"/>
66
- <text x="20" y="25" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="700" fill="#00bfff">3</text>
67
- <rect x="44" y="3" width="618" height="34" rx="6" ry="6" fill="#1a1f26"/>
68
- <text x="57" y="25" font-family="'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace" font-size="13" fill="#6e7681">$</text>
69
- <text x="72" y="25" font-family="'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace" font-size="13">
70
- <tspan fill="#ff7b72">specmem</tspan>
71
- <tspan fill="#f5f5f5"> init</tspan>
75
+ <g transform="translate(30, 258)">
76
+ <rect width="740" height="72" rx="12" fill="url(#cardGrad)" stroke="#22c55e" stroke-width="1.5"/>
77
+ <rect width="740" height="3" rx="1.5" fill="url(#greenGrad)"/>
78
+ <circle cx="36" cy="42" r="16" fill="none" stroke="#22c55e" stroke-width="1.5" stroke-opacity="0.6"/>
79
+ <text x="36" y="47" text-anchor="middle" font-family="system-ui, sans-serif" font-size="14" font-weight="700" fill="#4ade80">3</text>
80
+ <rect x="64" y="20" width="660" height="32" rx="6" fill="#0d1117" stroke="#21262d" stroke-width="1"/>
81
+ <text x="78" y="40" font-family="'SF Mono', 'Fira Code', Consolas, monospace" font-size="13" fill="#6e7681">$</text>
82
+ <text x="94" y="40" font-family="'SF Mono', 'Fira Code', Consolas, monospace" font-size="13">
83
+ <tspan fill="#4ade80">specmem</tspan>
84
+ <tspan fill="#e6edf3"> init</tspan>
72
85
  </text>
86
+ <text x="64" y="65" font-family="system-ui, sans-serif" font-size="10" fill="#6e7681">One-time setup — models, DB, hooks, everything</text>
73
87
  </g>
74
88
 
75
- <!-- Done -->
76
- <g transform="translate(30, 192)">
77
- <circle cx="20" cy="16" r="13" fill="#22c55e" fill-opacity="0.2" stroke="#22c55e" stroke-width="2" filter="url(#checkGlow)"/>
78
- <path d="M13 16 L18 21 L28 11" fill="none" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
79
- <text x="50" y="21" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#22c55e">Done.</text>
80
- <text x="98" y="21" font-family="system-ui, sans-serif" font-size="12" fill="#8b949e">Claude now remembers your entire codebase across every session.</text>
81
- </g>
89
+ <!-- Done bar -->
90
+ <rect x="30" y="350" width="740" height="52" rx="12" fill="#22c55e" fill-opacity="0.08" stroke="#22c55e" stroke-width="1" stroke-opacity="0.3"/>
91
+ <!-- Checkmark -->
92
+ <circle cx="66" cy="376" r="12" fill="none" stroke="#22c55e" stroke-width="2"/>
93
+ <path d="M60 376 L64 381 L74 370" fill="none" stroke="#4ade80" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
94
+ <text x="90" y="372" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#4ade80">Done.</text>
95
+ <text x="90" y="389" font-family="system-ui, sans-serif" font-size="11" fill="#6e7681">Claude now has full semantic memory of your codebase — persistent across every session.</text>
96
+
97
+ <!-- OS logos (right side of done bar) -->
98
+ <!-- Ubuntu circle pattern -->
99
+ <circle cx="600" cy="376" r="10" fill="none" stroke="#E95420" stroke-width="1.5"/>
100
+ <circle cx="600" cy="376" r="3" fill="#E95420"/>
101
+ <circle cx="600" cy="366" r="2" fill="#E95420"/>
102
+ <circle cx="591" cy="381" r="2" fill="#E95420"/>
103
+ <circle cx="609" cy="381" r="2" fill="#E95420"/>
104
+
105
+ <!-- Debian swirl (simplified) -->
106
+ <circle cx="630" cy="376" r="10" fill="none" stroke="#A80030" stroke-width="1.5"/>
107
+ <path d="M630 368 C636 368 638 374 634 379 C630 384 624 382 622 378" fill="none" stroke="#D70751" stroke-width="2" stroke-linecap="round"/>
108
+ <circle cx="630" cy="376" r="2" fill="#D70751"/>
109
+
110
+ <!-- Mint LM badge -->
111
+ <rect x="647" y="366" width="20" height="20" rx="5" fill="none" stroke="#87CF3E" stroke-width="1.5"/>
112
+ <text x="657" y="380" text-anchor="middle" font-family="system-ui, sans-serif" font-size="8" font-weight="700" fill="#87CF3E">LM</text>
113
+
114
+ <!-- Kali dragon shape (simplified) -->
115
+ <path d="M688 366 L696 374 L694 380 L700 388 L688 382 L676 388 L682 380 L680 374 Z" fill="none" stroke="#557C94" stroke-width="1.5" stroke-linejoin="round"/>
116
+
117
+ <text x="710" y="374" font-family="system-ui, sans-serif" font-size="9" fill="#484f58">+ more</text>
82
118
  </svg>