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.
- package/README.md +1 -1
- package/bin/specmem-autoclaude.cjs +12 -1
- package/bin/specmem-cli.cjs +1077 -11
- package/bin/specmem-console.cjs +51 -12
- package/bootstrap.cjs +10 -2
- package/claude-hooks/agent-loading-hook.js +10 -3
- package/claude-hooks/refusal-detector-hook.cjs +53 -0
- package/claude-hooks/settings.json +37 -1
- package/claude-hooks/smart-search-interceptor.js +1 -1
- package/claude-hooks/team-comms-enforcer.cjs +64 -0
- package/claude-hooks/use-code-pointers.cjs +1 -1
- package/dist/cli/deploy-to-claude.js +9 -2
- package/dist/index.js +49 -12
- package/dist/init/claudeConfigInjector.js +25 -6
- package/dist/installer/autoInstall.js +7 -1
- package/dist/mcp/compactionProxy.js +218 -6
- package/dist/mcp/embeddingServerManager.js +90 -16
- package/dist/tools/goofy/findCodePointers.js +17 -0
- package/dist/tools/goofy/findWhatISaid.js +19 -0
- package/embedding-sandbox/frankenstein-embeddings.py +4 -3
- package/package.json +1 -1
- package/scripts/deploy-hooks.cjs +10 -2
- package/scripts/fast-batch-embedder.cjs +2 -2
- package/scripts/force-retry.cjs +34 -0
- package/scripts/global-postinstall.cjs +95 -2
- package/scripts/poetic-abliteration.cjs +379 -0
- package/scripts/refusal-enforcer.cjs +88 -0
- package/scripts/specmem-init.cjs +99 -12
- package/specmem/supervisord.conf +1 -1
- package/claude-hooks/agent-chooser-hook.js +0 -179
package/scripts/specmem-init.cjs
CHANGED
|
@@ -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:
|
|
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
|
|
1335
|
-
try { process.kill(child.pid, 0);
|
|
1336
|
-
|
|
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
|
|
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 === '
|
|
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 === '
|
|
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 === '
|
|
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 === '
|
|
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 === '
|
|
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 === '
|
|
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 || {});
|
package/specmem/supervisord.conf
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
; ============================================
|
|
2
2
|
; SPECMEM BRAIN CONTAINER - DYNAMIC SUPERVISORD CONFIG
|
|
3
|
-
; Generated by specmem-init at 2026-03-
|
|
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
|
-
});
|