Skip to content

Commit 75e48c8

Browse files
author
StackMemory Bot (CLI)
committed
fix(mcp,doctor): improve desire path coverage, daemon auto-start, doctor fixes
- MCP: add is_error: true to 11 soft-fail null-handler return objects (cord 5, team 3, provider 3) so desire path hook catches them - Doctor: fix stale hooks check (was using deprecated hooks.json key, now reads settings.json correctly) - Doctor: add daemon health check with PID/uptime/service count - Doctor: expand --fix to auto-apply hooks install and daemon start - Hook: add daemon-auto-start.js — checks PID file on PostToolUse, spawns daemon if not running (throttled to once per 30min per session) - Tests: update hook counts 10→11, optional 5→6
1 parent 860bc6b commit 75e48c8

6 files changed

Lines changed: 239 additions & 39 deletions

File tree

scripts/install-claude-hooks-auto.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ try {
117117
commandPrefix: 'node',
118118
required: false,
119119
},
120+
{
121+
scriptName: 'daemon-auto-start.js',
122+
eventType: 'PostToolUse',
123+
timeout: 2,
124+
commandPrefix: 'node',
125+
required: false,
126+
},
120127
];
121128

122129
const DEAD_HOOKS = ['sms-response-handler.js'];

src/cli/commands/setup.ts

Lines changed: 84 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { execSync } from 'child_process';
2222
const CLAUDE_DIR = join(homedir(), '.claude');
2323
const CLAUDE_CONFIG_FILE = join(CLAUDE_DIR, 'config.json');
2424
const MCP_CONFIG_FILE = join(CLAUDE_DIR, 'stackmemory-mcp.json');
25-
const HOOKS_JSON = join(CLAUDE_DIR, 'hooks.json');
2625

2726
interface DiagnosticResult {
2827
name: string;
@@ -264,39 +263,41 @@ export function createDoctorCommand(): Command {
264263
});
265264
}
266265

267-
// 4. Check Claude hooks
268-
if (existsSync(HOOKS_JSON)) {
269-
try {
270-
const hooks = JSON.parse(readFileSync(HOOKS_JSON, 'utf8'));
271-
const hasTraceHook = !!hooks['tool-use-approval'];
272-
if (hasTraceHook) {
273-
results.push({
274-
name: 'Claude Hooks',
275-
status: 'ok',
276-
message: 'Tool tracing hook installed',
277-
});
278-
} else {
279-
results.push({
280-
name: 'Claude Hooks',
281-
status: 'warn',
282-
message: 'Hooks file exists but tracing not configured',
283-
fix: 'Run: stackmemory hooks install',
284-
});
266+
// 4. Check Claude hooks (settings.json)
267+
{
268+
const settingsFile = join(homedir(), '.claude', 'settings.json');
269+
let hookCount = 0;
270+
if (existsSync(settingsFile)) {
271+
try {
272+
const settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
273+
if (settings.hooks) {
274+
for (const groups of Object.values(settings.hooks) as Array<
275+
Array<{ hooks: Array<{ command: string }> }>
276+
>) {
277+
for (const group of groups) {
278+
hookCount += group.hooks.length;
279+
}
280+
}
281+
}
282+
} catch {
283+
// Cannot parse settings.json
285284
}
286-
} catch {
285+
}
286+
287+
if (hookCount > 0) {
288+
results.push({
289+
name: 'Claude Hooks',
290+
status: 'ok',
291+
message: `${hookCount} hooks registered in settings.json`,
292+
});
293+
} else {
287294
results.push({
288295
name: 'Claude Hooks',
289296
status: 'warn',
290-
message: 'Could not read hooks.json',
297+
message: 'No hooks registered in settings.json',
298+
fix: 'Run: stackmemory hooks install',
291299
});
292300
}
293-
} else {
294-
results.push({
295-
name: 'Claude Hooks',
296-
status: 'warn',
297-
message: 'Claude hooks not installed (optional)',
298-
fix: 'Run: stackmemory hooks install',
299-
});
300301
}
301302

302303
// 5. Check MCP tool definitions
@@ -659,7 +660,44 @@ export function createDoctorCommand(): Command {
659660
}
660661
}
661662

662-
// 12. Check file permissions
663+
// 12. Check daemon health
664+
{
665+
try {
666+
const { readDaemonStatus } =
667+
await import('../../daemon/daemon-config.js');
668+
const status = readDaemonStatus();
669+
if (status.running) {
670+
const uptime = status.startedAt
671+
? Math.round((Date.now() - status.startedAt) / 1000 / 60)
672+
: 0;
673+
const svcCount = Object.values(status.services || {}).filter(
674+
(s: { enabled?: boolean }) => s.enabled
675+
).length;
676+
results.push({
677+
name: 'Background Daemon',
678+
status: 'ok',
679+
message: `Running (PID: ${status.pid}, ${svcCount} services, ${uptime}min uptime)`,
680+
});
681+
} else {
682+
results.push({
683+
name: 'Background Daemon',
684+
status: 'warn',
685+
message:
686+
'Daemon not running — context auto-save and maintenance disabled',
687+
fix: 'Run: stackmemory daemon start',
688+
});
689+
}
690+
} catch {
691+
results.push({
692+
name: 'Background Daemon',
693+
status: 'warn',
694+
message: 'Could not check daemon status',
695+
fix: 'Run: stackmemory daemon start',
696+
});
697+
}
698+
}
699+
700+
// 13. Check file permissions
663701
const homeStackmemory = join(homedir(), '.stackmemory');
664702
if (existsSync(homeStackmemory)) {
665703
try {
@@ -701,13 +739,23 @@ export function createDoctorCommand(): Command {
701739
console.log(chalk.cyan(` Fix: ${result.fix}`));
702740

703741
if (options.fix && result.status !== 'ok') {
704-
// Auto-fix logic for specific issues
705-
if (result.fix.includes('stackmemory setup-mcp')) {
706-
console.log(chalk.gray(' Attempting auto-fix...'));
707-
try {
708-
execSync('stackmemory setup-mcp', { stdio: 'inherit' });
709-
} catch {
710-
console.log(chalk.red(' Auto-fix failed'));
742+
const fixCmds = [
743+
'stackmemory setup-mcp',
744+
'stackmemory hooks install',
745+
'stackmemory daemon start',
746+
];
747+
for (const cmd of fixCmds) {
748+
if (result.fix.includes(cmd)) {
749+
console.log(chalk.gray(` Attempting: ${cmd}...`));
750+
try {
751+
execSync(cmd, {
752+
stdio: 'inherit',
753+
timeout: 15000,
754+
});
755+
} catch {
756+
console.log(chalk.red(' Auto-fix failed'));
757+
}
758+
break;
711759
}
712760
}
713761
}

src/integrations/mcp/server.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1652,6 +1652,7 @@ class LocalStackMemoryMCP {
16521652
content: [
16531653
{ type: 'text', text: 'Cord handlers not initialized.' },
16541654
],
1655+
is_error: true,
16551656
};
16561657
} else {
16571658
result = await this.cordHandlers.handleCordSpawn(args);
@@ -1664,6 +1665,7 @@ class LocalStackMemoryMCP {
16641665
content: [
16651666
{ type: 'text', text: 'Cord handlers not initialized.' },
16661667
],
1668+
is_error: true,
16671669
};
16681670
} else {
16691671
result = await this.cordHandlers.handleCordFork(args);
@@ -1676,6 +1678,7 @@ class LocalStackMemoryMCP {
16761678
content: [
16771679
{ type: 'text', text: 'Cord handlers not initialized.' },
16781680
],
1681+
is_error: true,
16791682
};
16801683
} else {
16811684
result = await this.cordHandlers.handleCordComplete(args);
@@ -1688,6 +1691,7 @@ class LocalStackMemoryMCP {
16881691
content: [
16891692
{ type: 'text', text: 'Cord handlers not initialized.' },
16901693
],
1694+
is_error: true,
16911695
};
16921696
} else {
16931697
result = await this.cordHandlers.handleCordAsk(args);
@@ -1700,6 +1704,7 @@ class LocalStackMemoryMCP {
17001704
content: [
17011705
{ type: 'text', text: 'Cord handlers not initialized.' },
17021706
],
1707+
is_error: true,
17031708
};
17041709
} else {
17051710
result = await this.cordHandlers.handleCordTree(args);
@@ -1713,6 +1718,7 @@ class LocalStackMemoryMCP {
17131718
content: [
17141719
{ type: 'text', text: 'Team handlers not initialized.' },
17151720
],
1721+
is_error: true,
17161722
};
17171723
} else {
17181724
result = await this.teamHandlers.handleTeamContextGet(args);
@@ -1725,6 +1731,7 @@ class LocalStackMemoryMCP {
17251731
content: [
17261732
{ type: 'text', text: 'Team handlers not initialized.' },
17271733
],
1734+
is_error: true,
17281735
};
17291736
} else {
17301737
result = await this.teamHandlers.handleTeamContextShare(args);
@@ -1737,6 +1744,7 @@ class LocalStackMemoryMCP {
17371744
content: [
17381745
{ type: 'text', text: 'Team handlers not initialized.' },
17391746
],
1747+
is_error: true,
17401748
};
17411749
} else {
17421750
result = await this.teamHandlers.handleTeamSearch(args);
@@ -1753,6 +1761,7 @@ class LocalStackMemoryMCP {
17531761
text: 'Multi-provider routing is disabled.',
17541762
},
17551763
],
1764+
is_error: true,
17561765
};
17571766
} else {
17581767
result = await this.providerHandlers.handleDelegateToModel(
@@ -1770,6 +1779,7 @@ class LocalStackMemoryMCP {
17701779
text: 'Multi-provider routing is disabled.',
17711780
},
17721781
],
1782+
is_error: true,
17731783
};
17741784
} else {
17751785
result = await this.providerHandlers.handleBatchSubmit(
@@ -1787,6 +1797,7 @@ class LocalStackMemoryMCP {
17871797
text: 'Multi-provider routing is disabled.',
17881798
},
17891799
],
1800+
is_error: true,
17901801
};
17911802
} else {
17921803
result = await this.providerHandlers.handleBatchCheck(

src/utils/__tests__/hook-installer.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ const HOOKS_DIR = path.join(os.homedir(), '.claude', 'hooks');
1818

1919
describe('hook-installer', () => {
2020
describe('CANONICAL_HOOKS', () => {
21-
it('defines all 10 hooks', () => {
22-
expect(CANONICAL_HOOKS).toHaveLength(10);
21+
it('defines all 11 hooks', () => {
22+
expect(CANONICAL_HOOKS).toHaveLength(11);
2323
const names = CANONICAL_HOOKS.map((h) => h.scriptName);
2424
expect(names).toContain('session-rescue.sh');
2525
expect(names).toContain('stop-checkpoint.js');
@@ -31,19 +31,21 @@ describe('hook-installer', () => {
3131
expect(names).toContain('team-task-complete.js');
3232
expect(names).toContain('team-teammate-idle.js');
3333
expect(names).toContain('desire-path-trace.js');
34+
expect(names).toContain('daemon-auto-start.js');
3435
});
3536

3637
it('core hooks are required, optional hooks are not', () => {
3738
const required = CANONICAL_HOOKS.filter((h) => h.required);
3839
const optional = CANONICAL_HOOKS.filter((h) => !h.required);
3940
expect(required).toHaveLength(5);
40-
expect(optional).toHaveLength(5);
41+
expect(optional).toHaveLength(6);
4142
const optionalNames = optional.map((h) => h.scriptName);
4243
expect(optionalNames).toContain('theory-capture.js');
4344
expect(optionalNames).toContain('team-subagent-stop.js');
4445
expect(optionalNames).toContain('team-task-complete.js');
4546
expect(optionalNames).toContain('team-teammate-idle.js');
4647
expect(optionalNames).toContain('desire-path-trace.js');
48+
expect(optionalNames).toContain('daemon-auto-start.js');
4749
});
4850

4951
it('js hooks have node commandPrefix', () => {

src/utils/hook-installer.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ export const CANONICAL_HOOKS: HookEntry[] = [
9898
commandPrefix: 'node',
9999
required: false,
100100
},
101+
{
102+
scriptName: 'daemon-auto-start.js',
103+
eventType: 'PostToolUse',
104+
timeout: 2,
105+
commandPrefix: 'node',
106+
required: false,
107+
},
101108
];
102109

103110
/** Script names that should be removed from settings (dead/deprecated hooks) */

0 commit comments

Comments
 (0)