Skip to content

Commit 860bc6b

Browse files
author
StackMemory Bot (CLI)
committed
feat(desires): add doctor check and sm_desire_paths MCP tool
- Doctor: reports tool failure count in last 7d, warns on unknown tools - MCP tool: sm_desire_paths with summary/list modes, category filter, days param - Reads from ~/.stackmemory/desire-paths/*.jsonl (written by Phase 3 hook)
1 parent ba4e2d0 commit 860bc6b

3 files changed

Lines changed: 238 additions & 2 deletions

File tree

src/cli/commands/setup.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66

77
import { Command } from 'commander';
88
import chalk from 'chalk';
9-
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'fs';
9+
import {
10+
existsSync,
11+
readFileSync,
12+
readdirSync,
13+
writeFileSync,
14+
mkdirSync,
15+
rmSync,
16+
} from 'fs';
1017
import { join } from 'path';
1118
import { homedir } from 'os';
1219
import { execSync } from 'child_process';
@@ -593,7 +600,66 @@ export function createDoctorCommand(): Command {
593600
}
594601
}
595602

596-
// 11. Check file permissions
603+
// 11. Check desire paths (unmet agent needs in last 7d)
604+
{
605+
const desireDir = join(homedir(), '.stackmemory', 'desire-paths');
606+
if (existsSync(desireDir)) {
607+
try {
608+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
609+
const files = readdirSync(desireDir).filter(
610+
(f) => f.startsWith('desire-') && f.endsWith('.jsonl')
611+
);
612+
let totalFailures = 0;
613+
let unknownTools = 0;
614+
for (const file of files) {
615+
const lines = readFileSync(join(desireDir, file), 'utf-8')
616+
.split('\n')
617+
.filter(Boolean);
618+
for (const line of lines) {
619+
try {
620+
const entry = JSON.parse(line);
621+
if (new Date(entry.ts).getTime() < cutoff) continue;
622+
totalFailures++;
623+
if (entry.category === 'unknown_tool') unknownTools++;
624+
} catch {
625+
// skip malformed
626+
}
627+
}
628+
}
629+
if (totalFailures > 0) {
630+
results.push({
631+
name: 'Desire Paths',
632+
status: unknownTools > 0 ? 'warn' : 'ok',
633+
message: `${totalFailures} tool failures in last 7d (${unknownTools} unknown tools)`,
634+
fix:
635+
unknownTools > 0
636+
? 'Run: stackmemory desires summary'
637+
: undefined,
638+
});
639+
} else {
640+
results.push({
641+
name: 'Desire Paths',
642+
status: 'ok',
643+
message: 'No tool failures in last 7d',
644+
});
645+
}
646+
} catch {
647+
results.push({
648+
name: 'Desire Paths',
649+
status: 'ok',
650+
message: 'Desire path logging active (no data yet)',
651+
});
652+
}
653+
} else {
654+
results.push({
655+
name: 'Desire Paths',
656+
status: 'ok',
657+
message: 'Desire path logging not yet active',
658+
});
659+
}
660+
}
661+
662+
// 12. Check file permissions
597663
const homeStackmemory = join(homedir(), '.stackmemory');
598664
if (existsSync(homeStackmemory)) {
599665
try {

src/integrations/mcp/server.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ import {
2020
} from './schemas.js';
2121
import {
2222
readFileSync,
23+
readdirSync,
2324
existsSync,
2425
mkdirSync,
2526
writeFileSync,
2627
appendFileSync,
2728
} from 'fs';
29+
import { homedir } from 'os';
2830
import { compactPlan } from '../../orchestrators/multimodal/utils.js';
2931
import { filterPending } from './pending-utils.js';
3032
import { join, dirname } from 'path';
@@ -1797,6 +1799,10 @@ class LocalStackMemoryMCP {
17971799
result = await this.handleSmDigest(args);
17981800
break;
17991801

1802+
case 'sm_desire_paths':
1803+
result = this.handleDesirePaths(args);
1804+
break;
1805+
18001806
default:
18011807
throw new Error(`Unknown tool: ${name}`);
18021808
}
@@ -3589,6 +3595,128 @@ ${typeBreakdown}`,
35893595
}
35903596
}
35913597

3598+
/** Handle sm_desire_paths tool — read and aggregate desire path logs */
3599+
private handleDesirePaths(args: Record<string, unknown>) {
3600+
const mode = String(args.mode || 'summary');
3601+
const days = Number(args.days || 7);
3602+
const limit = Number(args.limit || 20);
3603+
const categoryFilter = args.category ? String(args.category) : undefined;
3604+
3605+
const desireDir = join(homedir(), '.stackmemory', 'desire-paths');
3606+
3607+
if (!existsSync(desireDir)) {
3608+
return {
3609+
content: [{ type: 'text', text: 'No desire path data found.' }],
3610+
};
3611+
}
3612+
3613+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
3614+
const files = readdirSync(desireDir).filter(
3615+
(f) => f.startsWith('desire-') && f.endsWith('.jsonl')
3616+
);
3617+
3618+
interface DesireEntry {
3619+
ts: string;
3620+
tool: string;
3621+
error: string;
3622+
category: string;
3623+
cwd?: string;
3624+
source?: string;
3625+
}
3626+
3627+
const entries: DesireEntry[] = [];
3628+
for (const file of files) {
3629+
const lines = readFileSync(join(desireDir, file), 'utf-8')
3630+
.split('\n')
3631+
.filter(Boolean);
3632+
for (const line of lines) {
3633+
try {
3634+
const entry = JSON.parse(line) as DesireEntry;
3635+
if (new Date(entry.ts).getTime() < cutoff) continue;
3636+
if (categoryFilter && entry.category !== categoryFilter) continue;
3637+
entries.push(entry);
3638+
} catch {
3639+
// skip malformed
3640+
}
3641+
}
3642+
}
3643+
3644+
if (entries.length === 0) {
3645+
return {
3646+
content: [
3647+
{
3648+
type: 'text',
3649+
text: `No desire path data in the last ${days} day(s).`,
3650+
},
3651+
],
3652+
};
3653+
}
3654+
3655+
if (mode === 'list') {
3656+
const recent = entries
3657+
.sort((a, b) => b.ts.localeCompare(a.ts))
3658+
.slice(0, limit);
3659+
3660+
const lines = recent.map(
3661+
(e) =>
3662+
`${e.ts.slice(0, 19)} ${e.tool} [${e.category}]\n ${e.error.slice(0, 120)}`
3663+
);
3664+
3665+
return {
3666+
content: [
3667+
{
3668+
type: 'text',
3669+
text: `Recent desire paths (${recent.length}/${entries.length}):\n\n${lines.join('\n\n')}`,
3670+
},
3671+
],
3672+
};
3673+
}
3674+
3675+
// Summary mode
3676+
const byTool = new Map<
3677+
string,
3678+
{ count: number; category: string; lastSeen: string }
3679+
>();
3680+
for (const e of entries) {
3681+
const existing = byTool.get(e.tool);
3682+
if (!existing || e.ts > existing.lastSeen) {
3683+
byTool.set(e.tool, {
3684+
count: (existing?.count || 0) + 1,
3685+
category: e.category,
3686+
lastSeen: e.ts,
3687+
});
3688+
} else {
3689+
existing.count++;
3690+
}
3691+
}
3692+
3693+
const sorted = [...byTool.entries()].sort(
3694+
(a, b) => b[1].count - a[1].count
3695+
);
3696+
3697+
const byCat = new Map<string, number>();
3698+
for (const e of entries) {
3699+
byCat.set(e.category, (byCat.get(e.category) || 0) + 1);
3700+
}
3701+
3702+
const toolLines = sorted.map(
3703+
([tool, data]) =>
3704+
`${tool}: ${data.count}x [${data.category}] (last: ${data.lastSeen.slice(0, 10)})`
3705+
);
3706+
const catLines = [...byCat.entries()]
3707+
.sort((a, b) => b[1] - a[1])
3708+
.map(([cat, count]) => ` ${cat}: ${count}`);
3709+
3710+
return {
3711+
content: [
3712+
{
3713+
type: 'text',
3714+
text: `Desire Path Summary (last ${days}d, ${entries.length} total failures)\n\nBy Tool:\n${toolLines.join('\n')}\n\nBy Category:\n${catLines.join('\n')}`,
3715+
},
3716+
],
3717+
};
3718+
}
3719+
35923720
private async handleSmDigest(args: any) {
35933721
const period = String(args.period || 'today') as DigestPeriod;
35943722
if (!['today', 'yesterday', 'week'].includes(period)) {

src/integrations/mcp/tool-definitions.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export class MCPToolDefinitions {
3535
...this.getTeamTools(),
3636
...this.getCordTools(),
3737
...this.getDigestTools(),
38+
...this.getDesirePathTools(),
3839
];
3940
}
4041

@@ -1231,6 +1232,45 @@ export class MCPToolDefinitions {
12311232
];
12321233
}
12331234

1235+
/**
1236+
* Desire path analysis tools
1237+
*/
1238+
private getDesirePathTools(): MCPToolDefinition[] {
1239+
return [
1240+
{
1241+
name: 'sm_desire_paths',
1242+
description:
1243+
'Analyze failed tool calls (desire paths) — what agents want but cannot get. Use mode "summary" for aggregated counts or "list" for recent failures.',
1244+
inputSchema: {
1245+
type: 'object',
1246+
properties: {
1247+
mode: {
1248+
type: 'string',
1249+
enum: ['summary', 'list'],
1250+
description: 'Output mode: summary (aggregated) or list (recent)',
1251+
default: 'summary',
1252+
},
1253+
limit: {
1254+
type: 'number',
1255+
default: 20,
1256+
description: 'Max entries to return (list mode only)',
1257+
},
1258+
category: {
1259+
type: 'string',
1260+
enum: ['unknown_tool', 'handler_error', 'invalid_params'],
1261+
description: 'Filter by failure category',
1262+
},
1263+
days: {
1264+
type: 'number',
1265+
default: 7,
1266+
description: 'Look back N days',
1267+
},
1268+
},
1269+
},
1270+
},
1271+
];
1272+
}
1273+
12341274
/**
12351275
* Multi-agent team collaboration tools
12361276
*/
@@ -1513,6 +1553,8 @@ export class MCPToolDefinitions {
15131553
return this.getCordTools();
15141554
case 'digest':
15151555
return this.getDigestTools();
1556+
case 'desire_paths':
1557+
return this.getDesirePathTools();
15161558
default:
15171559
return [];
15181560
}

0 commit comments

Comments
 (0)