specmem-hardwicksoftware 3.7.36 → 3.7.38

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.
@@ -1317,11 +1317,26 @@ fi
1317
1317
  if (fs.existsSync(portFile)) try { fs.unlinkSync(portFile); } catch {}
1318
1318
  } catch {}
1319
1319
  // Spawn fresh proxy daemon
1320
+ // Capture original ANTHROPIC_BASE_URL BEFORE we override it to localhost proxy.
1321
+ // If it's a custom API (MiniMax, etc.), pass it as COMPACTION_PROXY_UPSTREAM so the
1322
+ // proxy knows where to forward. For native Claude (no custom URL), leave unset so
1323
+ // proxy defaults to api.anthropic.com with original OAuth headers intact.
1324
+ const originalBaseUrl = process.env.ANTHROPIC_BASE_URL || '';
1325
+ const isCustomApi = originalBaseUrl && !originalBaseUrl.includes('127.0.0.1') && !originalBaseUrl.includes('localhost');
1326
+ const daemonEnv = { ...process.env, SPECMEM_DAEMON: '1' };
1327
+ if (isCustomApi) {
1328
+ daemonEnv.COMPACTION_PROXY_UPSTREAM = originalBaseUrl;
1329
+ initLog(`Custom API detected: ${originalBaseUrl} → COMPACTION_PROXY_UPSTREAM`);
1330
+ // Pass through API key and model for custom upstream
1331
+ if (process.env.ANTHROPIC_AUTH_TOKEN) daemonEnv.COMPACTION_PROXY_API_KEY = process.env.ANTHROPIC_AUTH_TOKEN;
1332
+ else if (process.env.ANTHROPIC_API_KEY) daemonEnv.COMPACTION_PROXY_API_KEY = process.env.ANTHROPIC_API_KEY;
1333
+ if (process.env.ANTHROPIC_MODEL) daemonEnv.COMPACTION_PROXY_MODEL = process.env.ANTHROPIC_MODEL;
1334
+ }
1320
1335
  try {
1321
1336
  const daemonPath = require.resolve('specmem-hardwicksoftware/dist/mcp/compactionProxyDaemon.js');
1322
1337
  const { spawn } = require('child_process');
1323
1338
  const child = spawn(process.execPath, [daemonPath], {
1324
- detached: true, stdio: 'ignore', env: { ...process.env, SPECMEM_DAEMON: '1' }
1339
+ detached: true, stdio: 'ignore', env: daemonEnv
1325
1340
  });
1326
1341
  child.unref();
1327
1342
  // Wait for daemon to write port file (up to 5s)
@@ -1331,9 +1346,19 @@ fi
1331
1346
  const p = fs.readFileSync(portWaitFile, 'utf8').trim();
1332
1347
  if (p && parseInt(p) > 0) {
1333
1348
  proxyPort = p;
1334
- // Verify the daemon process is still alive (signal 0 = existence check)
1335
- try { process.kill(child.pid, 0); proxyAlive = true; } catch {}
1336
- initLog(`Proxy daemon port file appeared (port ${p}, PID ${child.pid}, alive=${proxyAlive})`);
1349
+ // Verify daemon is alive AND actually listening (not just port file exists)
1350
+ try { process.kill(child.pid, 0); } catch { break; }
1351
+ // TCP liveness check — port file can outlive a crashed daemon
1352
+ try {
1353
+ const { execSync: _tcpCheck } = require('child_process');
1354
+ _tcpCheck(`bash -c 'echo > /dev/tcp/127.0.0.1/${p}'`, { stdio: 'ignore', timeout: 2000 });
1355
+ proxyAlive = true;
1356
+ } catch {
1357
+ // Proxy wrote port file but isn't listening yet — wait and retry
1358
+ initLog(`Port file exists but proxy not listening on port ${p}, retrying...`);
1359
+ continue;
1360
+ }
1361
+ initLog(`Proxy daemon confirmed listening (port ${p}, PID ${child.pid})`);
1337
1362
  break;
1338
1363
  }
1339
1364
  }
@@ -1364,8 +1389,8 @@ fi
1364
1389
  if (fs.existsSync(bashrcPath)) {
1365
1390
  const bashrc = fs.readFileSync(bashrcPath, 'utf8');
1366
1391
  hasProxyFix = bashrc.includes(proxyMarker);
1367
- // Detect old unconditional version (no port file check) — needs upgrade to conditional
1368
- if (hasProxyFix && !bashrc.includes('.compaction-proxy-port')) {
1392
+ // Detect old version (no port file check OR no COMPACTION_PROXY_UPSTREAM) — needs upgrade
1393
+ if (hasProxyFix && (!bashrc.includes('.compaction-proxy-port') || !bashrc.includes('COMPACTION_PROXY_UPSTREAM'))) {
1369
1394
  needsUpgrade = true;
1370
1395
  }
1371
1396
  }
@@ -1389,7 +1414,12 @@ fi
1389
1414
  const proxyFix = `
1390
1415
  ${proxyMarker}
1391
1416
  # SpecMem: Route API calls through compaction proxy when it's running
1417
+ # Captures original ANTHROPIC_BASE_URL as COMPACTION_PROXY_UPSTREAM before overriding,
1418
+ # so the proxy knows where to forward (custom API like MiniMax, or default Anthropic).
1392
1419
  if [ -f "\$HOME/.claude/.compaction-proxy-port" ]; then
1420
+ if [ -n "\$ANTHROPIC_BASE_URL" ] && ! echo "\$ANTHROPIC_BASE_URL" | grep -q '127.0.0.1\\|localhost'; then
1421
+ export COMPACTION_PROXY_UPSTREAM="\$ANTHROPIC_BASE_URL"
1422
+ fi
1393
1423
  export ANTHROPIC_BASE_URL="http://127.0.0.1:\$(cat "\$HOME/.claude/.compaction-proxy-port")"
1394
1424
  fi
1395
1425
  `;
@@ -4060,7 +4090,7 @@ async function coldStartEmbeddingDocker(projectPath, modelConfig, ui, codebaseRe
4060
4090
  try {
4061
4091
  const parsed = JSON.parse(line);
4062
4092
  // Skip "processing" heartbeat - model is still loading
4063
- if (parsed.status === 'processing') {
4093
+ if (parsed.status === 'working') {
4064
4094
  ui.setSubStatus(`⏳ Processing... (${elapsed}s)`);
4065
4095
  continue;
4066
4096
  }
@@ -4492,7 +4522,7 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
4492
4522
  try {
4493
4523
  const parsed = JSON.parse(completeLine);
4494
4524
  // skip heartbeat/processing status - keep waiting
4495
- if (parsed.status === 'processing') {
4525
+ if (parsed.status === 'working') {
4496
4526
  continue;
4497
4527
  }
4498
4528
  // got actual response - check if embedding was returned
@@ -5088,7 +5118,7 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
5088
5118
  const parsed = JSON.parse(completeLine);
5089
5119
 
5090
5120
  // FIX: Track heartbeats to detect server backpressure/overload
5091
- if (parsed.status === 'processing') {
5121
+ if (parsed.status === 'working') {
5092
5122
  heartbeatCount++;
5093
5123
  if (heartbeatCount > HEARTBEAT_LIMIT) {
5094
5124
  settled = true;
@@ -5276,7 +5306,7 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
5276
5306
 
5277
5307
  // Skip heartbeat/processing status - keep waiting for actual embedding
5278
5308
  // FIX: Track heartbeats to detect server overload
5279
- if (parsed.status === 'processing') {
5309
+ if (parsed.status === 'working') {
5280
5310
  heartbeatCount++;
5281
5311
  if (heartbeatCount > 20) { // Single embed shouldn't take this long
5282
5312
  settled = true;
@@ -5495,7 +5525,7 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
5495
5525
  try {
5496
5526
  const resp = JSON.parse(line);
5497
5527
  if (resp.error) { clearTimeout(timeout); settled = true; client.end(); reject(new Error(resp.error)); return; }
5498
- if (resp.status === 'processing') continue;
5528
+ if (resp.status === 'working') continue;
5499
5529
  if (resp.total_processed !== undefined || resp.processed !== undefined) {
5500
5530
  clearTimeout(timeout); settled = true; client.end(); resolve(resp); return;
5501
5531
  }
@@ -6745,7 +6775,7 @@ async function extractSessions(projectPath, ui, embeddingResult = null) {
6745
6775
  data = data.slice(newlineIdx + 1);
6746
6776
  try {
6747
6777
  const parsed = JSON.parse(completeLine);
6748
- if (parsed.status === 'processing') continue;
6778
+ if (parsed.status === 'working') continue;
6749
6779
  client.destroy();
6750
6780
  if (parsed.error) {
6751
6781
  reject(new Error('Batch embedding error: ' + parsed.error));
@@ -7353,6 +7383,10 @@ async function launchScreenSessions(projectPath, ui) {
7353
7383
  if (fs.existsSync(proxyPortFile)) {
7354
7384
  const pPort = fs.readFileSync(proxyPortFile, 'utf8').trim();
7355
7385
  if (pPort && parseInt(pPort) > 0) {
7386
+ // ANTHROPIC_BASE_URL tells Claude to route through the proxy.
7387
+ // The proxy daemon already has COMPACTION_PROXY_UPSTREAM from its spawn env —
7388
+ // do NOT set it here, because process.env.ANTHROPIC_BASE_URL is already
7389
+ // overwritten to localhost by this point (would create a loop).
7356
7390
  proxyEnv = `ANTHROPIC_BASE_URL="http://127.0.0.1:${pPort}" `;
7357
7391
  }
7358
7392
  }
@@ -7612,6 +7646,37 @@ function syncHooksAndSettings() {
7612
7646
  const hookEventCount = Object.keys(mergedSettings.hooks || {}).length;
7613
7647
  initLog('[HOOKS-SYNC] settings.json merged: ' + hookEventCount + ' hook events, ' + hookCount + ' hook files');
7614
7648
 
7649
+ // ALSO sync to project-level .claude/settings.json if it exists
7650
+ const projectPath = process.cwd();
7651
+ const projectClaudeDir = path.join(projectPath, '.claude');
7652
+ const projectSettingsPath = path.join(projectClaudeDir, 'settings.json');
7653
+
7654
+ if (fs.existsSync(projectSettingsPath)) {
7655
+ try {
7656
+ const projectSettings = JSON.parse(fs.readFileSync(projectSettingsPath, 'utf8'));
7657
+
7658
+ // Merge hooks from src into project settings
7659
+ const srcSettings = JSON.parse(fs.readFileSync(srcSettingsPath, 'utf8'));
7660
+ const mergedProjectHooks = { ...projectSettings.hooks, ...srcSettings.hooks };
7661
+
7662
+ // Preserve existing non-hook settings (env, mcpServers, etc)
7663
+ const finalProjectSettings = {
7664
+ ...projectSettings,
7665
+ hooks: mergedProjectHooks
7666
+ };
7667
+
7668
+ // Backup project settings
7669
+ const projBackup = projectSettingsPath + '.backup.' + Date.now();
7670
+ fs.copyFileSync(projectSettingsPath, projBackup);
7671
+
7672
+ fs.writeFileSync(projectSettingsPath, JSON.stringify(finalProjectSettings, null, 2));
7673
+ const projHookEvents = Object.keys(finalProjectSettings.hooks || {}).length;
7674
+ initLog('[HOOKS-SYNC] Project-level settings.json updated: ' + projHookEvents + ' hook events');
7675
+ } catch (e) {
7676
+ initLog('[HOOKS-SYNC] Project settings merge failed: ' + e.message);
7677
+ }
7678
+ }
7679
+
7615
7680
  // Verify
7616
7681
  const verifySettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8'));
7617
7682
  const preToolHooks = verifySettings.hooks?.PreToolUse || [];
@@ -8824,6 +8889,28 @@ CREATE INDEX IF NOT EXISTS idx_embedding_queue_project ON embedding_queue (proje
8824
8889
  const permCount = (mergedSettings.permissions?.allow || []).length;
8825
8890
  initLog('[SETUP] settings.json deep-merged: ' + hookEventCount + ' hook events, ' + permCount + ' permissions (user hooks preserved)');
8826
8891
 
8892
+ // ALSO sync hooks to project-level .claude/settings.json if it exists
8893
+ const projectClaudeDir = path.join(projectPath, '.claude');
8894
+ const projectSettingsPath = path.join(projectClaudeDir, 'settings.json');
8895
+
8896
+ if (fs.existsSync(projectSettingsPath)) {
8897
+ try {
8898
+ const projectSettings = JSON.parse(fs.readFileSync(projectSettingsPath, 'utf8'));
8899
+ const mergedProjectHooks = { ...projectSettings.hooks, ...srcSettings.hooks };
8900
+ const finalProjectSettings = { ...projectSettings, hooks: mergedProjectHooks };
8901
+
8902
+ // Backup first
8903
+ const projBackup = projectSettingsPath + '.backup.' + Date.now();
8904
+ fs.copyFileSync(projectSettingsPath, projBackup);
8905
+
8906
+ fs.writeFileSync(projectSettingsPath, JSON.stringify(finalProjectSettings, null, 2));
8907
+ const projHookEvents = Object.keys(finalProjectSettings.hooks || {}).length;
8908
+ initLog('[SETUP] Project-level settings.json updated: ' + projHookEvents + ' hook events');
8909
+ } catch (e) {
8910
+ initLog('[SETUP] Project settings merge failed: ' + e.message);
8911
+ }
8912
+ }
8913
+
8827
8914
  // VERIFICATION: Confirm hooks are properly configured
8828
8915
  const verifySettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8'));
8829
8916
  const hookEvents = Object.keys(verifySettings.hooks || {});
@@ -1,6 +1,6 @@
1
1
  ; ============================================
2
2
  ; SPECMEM BRAIN CONTAINER - DYNAMIC SUPERVISORD CONFIG
3
- ; Generated by specmem-init at 2026-03-02T00:36:22.804Z
3
+ ; Generated by specmem-init at 2026-03-06T01:46:58.539Z
4
4
  ; Thread counts from model-config.json resourcePool
5
5
  ; ============================================
6
6
 
@@ -1,179 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Agent Chooser Hook - Interactive Agent Deployment
4
- * ==================================================
5
- *
6
- * PreToolUse hook that INTERCEPTS Agent tool calls and:
7
- * 1. BLOCKS automatic deployment
8
- * 2. Injects instructions for to ask user for preferences
9
- * 3. User chooses agent type, model, settings PER DEPLOYMENT
10
- *
11
- * This gives users FULL CONTROL over every agent that gets deployed!
12
- *
13
- * Hook Event: PreToolUse
14
- * Matcher: Agent
15
- *
16
- * To disable interactive mode, set SPECMEM_AGENT_AUTO=1
17
- */
18
-
19
- const fs = require('fs');
20
- const path = require('path');
21
-
22
- // Check if auto mode is enabled (skip interactive)
23
- const AUTO_MODE = process.env.SPECMEM_AGENT_AUTO === '1' || process.env.SPECMEM_AGENT_AUTO === 'true';
24
-
25
- // Check if chooser is disabled
26
- const CHOOSER_DISABLED = process.env.SPECMEM_NO_CHOOSER === '1' || process.env.SPECMEM_NO_CHOOSER === 'true';
27
-
28
- // Marker to prevent re-processing
29
- const CHOOSER_MARKER = '[AGENT_CHOOSER_CONFIRMED]';
30
-
31
- /**
32
- * Load user config
33
- */
34
- function loadConfig() {
35
- const configPaths = [
36
- path.join(process.cwd(), '.specmem', 'agent-config.json'),
37
- path.join(process.env.HOME || '', '.specmem', 'agent-config.json')
38
- ];
39
-
40
- for (const p of configPaths) {
41
- try {
42
- if (fs.existsSync(p)) {
43
- return JSON.parse(fs.readFileSync(p, 'utf8'));
44
- }
45
- } catch (e) {}
46
- }
47
-
48
- return {
49
- defaults: { model: 'sonnet', background: true },
50
- agents: {}
51
- };
52
- }
53
-
54
- /**
55
- * Build the chooser context that instructs to ask the user
56
- */
57
- function buildChooserContext(description, currentType, currentModel, config) {
58
- const agentTypes = [
59
- { name: 'general-purpose', desc: 'Full toolset for complex multi-step tasks' },
60
- { name: 'Explore', desc: 'Fast codebase exploration and search' },
61
- { name: 'feature-dev:code-explorer', desc: 'Deep code analysis with tracing' },
62
- { name: 'feature-dev:code-architect', desc: 'Architecture design blueprints' },
63
- { name: 'feature-dev:code-reviewer', desc: 'Code review with confidence filtering' },
64
- { name: 'Bash', desc: 'Shell/git/CLI command specialist' },
65
- { name: 'Plan', desc: 'Architecture and implementation planning' }
66
- ];
67
-
68
- const models = [
69
- { name: 'haiku', desc: 'Fastest, cheapest - simple tasks' },
70
- { name: 'sonnet', desc: 'Balanced speed and quality (default)' },
71
- { name: 'opus', desc: 'Deepest thinking - complex analysis' }
72
- ];
73
-
74
- return `
75
- [AGENT-CHOOSER]
76
- 🎛️ AGENT DEPLOYMENT PAUSED - Confirm settings
77
-
78
- Mission: "${description}"
79
- Agent: ${currentType || 'general-purpose'} | Model: ${currentModel || config.defaults?.model || 'sonnet'}
80
-
81
- Call AskUserQuestion NOW:
82
- {
83
- "questions": [
84
- {
85
- "question": "Agent for: ${description.slice(0, 40)}...?",
86
- "header": "Agent",
87
- "options": [
88
- {"label": "${currentType || 'general-purpose'}", "description": "Current"},
89
- {"label": "Explore", "description": "Fast search"},
90
- {"label": "feature-dev:code-explorer", "description": "Deep analysis"},
91
- {"label": "Plan", "description": "Architecture"}
92
- ],
93
- "multiSelect": false
94
- },
95
- {
96
- "question": "Model?",
97
- "header": "Model",
98
- "options": [
99
- {"label": "sonnet", "description": "Balanced (recommended)"},
100
- {"label": "opus", "description": "Deepest thinking"},
101
- {"label": "haiku", "description": "Fastest"}
102
- ],
103
- "multiSelect": false
104
- }
105
- ]
106
- }
107
-
108
- After response: Re-deploy Agent with choices + "${CHOOSER_MARKER}" in prompt.
109
- [/AGENT-CHOOSER]
110
- `;
111
- }
112
-
113
- async function main() {
114
- let input = '';
115
- process.stdin.setEncoding('utf8');
116
- for await (const chunk of process.stdin) {
117
- input += chunk;
118
- }
119
-
120
- try {
121
- const data = JSON.parse(input);
122
- const toolName = data.tool_name || '';
123
- const toolInput = data.tool_input || {};
124
-
125
- // Only intercept Agent tool
126
- if (toolName !== 'Agent') {
127
- process.exit(0);
128
- }
129
-
130
- // Skip if chooser is disabled
131
- if (CHOOSER_DISABLED) {
132
- process.exit(0); // Let other hooks handle it
133
- }
134
-
135
- // Skip if auto mode is enabled
136
- if (AUTO_MODE) {
137
- process.exit(0); // Let other hooks handle it
138
- }
139
-
140
- const prompt = toolInput.prompt || '';
141
- const description = toolInput.description || 'agent task';
142
- const agentType = toolInput.subagent_type || '';
143
- const model = toolInput.model || '';
144
-
145
- // Skip if already confirmed by user (has our marker)
146
- if (prompt.includes(CHOOSER_MARKER)) {
147
- // User confirmed - let it proceed (other hooks will process)
148
- process.exit(0);
149
- }
150
-
151
- // Load config for defaults
152
- const config = loadConfig();
153
-
154
- // Build the chooser context
155
- const chooserContext = buildChooserContext(description, agentType, model, config);
156
-
157
- // BLOCK the deployment and inject instructions for to ask user
158
- console.log(JSON.stringify({
159
- continue: false, // Block the tool call
160
- stopReason: `Agent deployment requires user confirmation. Use AskUserQuestion to let user choose settings.`,
161
- hookSpecificOutput: {
162
- hookEventName: 'PreToolUse',
163
- permissionDecision: 'deny',
164
- permissionDecisionReason: `🛑 Agent chooser: Awaiting user confirmation for "${description.slice(0, 40)}"`,
165
- additionalContext: chooserContext
166
- }
167
- }));
168
-
169
- } catch (e) {
170
- // LOW-44 FIX: Log errors before exit
171
- console.error('[agent-chooser-hook] Error:', e.message || e);
172
- process.exit(0);
173
- }
174
- }
175
-
176
- main().catch((e) => {
177
- console.error('[agent-chooser-hook] Unhandled error:', e.message || e);
178
- process.exit(0);
179
- });