@stackmemoryai/stackmemory 1.2.8 → 1.2.9
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 +2 -2
- package/dist/src/cli/commands/daemon.js +306 -2
- package/dist/src/cli/commands/desires.js +117 -0
- package/dist/src/cli/commands/setup.js +369 -39
- package/dist/src/cli/commands/team.js +168 -0
- package/dist/src/cli/index.js +4 -0
- package/dist/src/integrations/mcp/server.js +157 -12
- package/dist/src/integrations/mcp/tool-definitions.js +41 -1
- package/dist/src/utils/hook-installer.js +35 -0
- package/package.json +2 -2
- package/scripts/install-claude-hooks-auto.js +35 -0
- package/templates/claude-hooks/daemon-auto-start.js +125 -0
- package/templates/claude-hooks/desire-path-trace.js +118 -0
- package/templates/claude-hooks/team-subagent-stop.js +77 -0
- package/templates/claude-hooks/team-task-complete.js +83 -0
- package/templates/claude-hooks/team-teammate-idle.js +96 -0
|
@@ -13,7 +13,15 @@ import {
|
|
|
13
13
|
AddAnchorSchema,
|
|
14
14
|
CreateTaskSchema
|
|
15
15
|
} from "./schemas.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
readFileSync,
|
|
18
|
+
readdirSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
appendFileSync
|
|
23
|
+
} from "fs";
|
|
24
|
+
import { homedir } from "os";
|
|
17
25
|
import { compactPlan } from "../../orchestrators/multimodal/utils.js";
|
|
18
26
|
import { filterPending } from "./pending-utils.js";
|
|
19
27
|
import { join, dirname } from "path";
|
|
@@ -1444,7 +1452,8 @@ ${summary}...`, 0.8);
|
|
|
1444
1452
|
result = {
|
|
1445
1453
|
content: [
|
|
1446
1454
|
{ type: "text", text: "Cord handlers not initialized." }
|
|
1447
|
-
]
|
|
1455
|
+
],
|
|
1456
|
+
is_error: true
|
|
1448
1457
|
};
|
|
1449
1458
|
} else {
|
|
1450
1459
|
result = await this.cordHandlers.handleCordSpawn(args);
|
|
@@ -1455,7 +1464,8 @@ ${summary}...`, 0.8);
|
|
|
1455
1464
|
result = {
|
|
1456
1465
|
content: [
|
|
1457
1466
|
{ type: "text", text: "Cord handlers not initialized." }
|
|
1458
|
-
]
|
|
1467
|
+
],
|
|
1468
|
+
is_error: true
|
|
1459
1469
|
};
|
|
1460
1470
|
} else {
|
|
1461
1471
|
result = await this.cordHandlers.handleCordFork(args);
|
|
@@ -1466,7 +1476,8 @@ ${summary}...`, 0.8);
|
|
|
1466
1476
|
result = {
|
|
1467
1477
|
content: [
|
|
1468
1478
|
{ type: "text", text: "Cord handlers not initialized." }
|
|
1469
|
-
]
|
|
1479
|
+
],
|
|
1480
|
+
is_error: true
|
|
1470
1481
|
};
|
|
1471
1482
|
} else {
|
|
1472
1483
|
result = await this.cordHandlers.handleCordComplete(args);
|
|
@@ -1477,7 +1488,8 @@ ${summary}...`, 0.8);
|
|
|
1477
1488
|
result = {
|
|
1478
1489
|
content: [
|
|
1479
1490
|
{ type: "text", text: "Cord handlers not initialized." }
|
|
1480
|
-
]
|
|
1491
|
+
],
|
|
1492
|
+
is_error: true
|
|
1481
1493
|
};
|
|
1482
1494
|
} else {
|
|
1483
1495
|
result = await this.cordHandlers.handleCordAsk(args);
|
|
@@ -1488,7 +1500,8 @@ ${summary}...`, 0.8);
|
|
|
1488
1500
|
result = {
|
|
1489
1501
|
content: [
|
|
1490
1502
|
{ type: "text", text: "Cord handlers not initialized." }
|
|
1491
|
-
]
|
|
1503
|
+
],
|
|
1504
|
+
is_error: true
|
|
1492
1505
|
};
|
|
1493
1506
|
} else {
|
|
1494
1507
|
result = await this.cordHandlers.handleCordTree(args);
|
|
@@ -1500,7 +1513,8 @@ ${summary}...`, 0.8);
|
|
|
1500
1513
|
result = {
|
|
1501
1514
|
content: [
|
|
1502
1515
|
{ type: "text", text: "Team handlers not initialized." }
|
|
1503
|
-
]
|
|
1516
|
+
],
|
|
1517
|
+
is_error: true
|
|
1504
1518
|
};
|
|
1505
1519
|
} else {
|
|
1506
1520
|
result = await this.teamHandlers.handleTeamContextGet(args);
|
|
@@ -1511,7 +1525,8 @@ ${summary}...`, 0.8);
|
|
|
1511
1525
|
result = {
|
|
1512
1526
|
content: [
|
|
1513
1527
|
{ type: "text", text: "Team handlers not initialized." }
|
|
1514
|
-
]
|
|
1528
|
+
],
|
|
1529
|
+
is_error: true
|
|
1515
1530
|
};
|
|
1516
1531
|
} else {
|
|
1517
1532
|
result = await this.teamHandlers.handleTeamContextShare(args);
|
|
@@ -1522,7 +1537,8 @@ ${summary}...`, 0.8);
|
|
|
1522
1537
|
result = {
|
|
1523
1538
|
content: [
|
|
1524
1539
|
{ type: "text", text: "Team handlers not initialized." }
|
|
1525
|
-
]
|
|
1540
|
+
],
|
|
1541
|
+
is_error: true
|
|
1526
1542
|
};
|
|
1527
1543
|
} else {
|
|
1528
1544
|
result = await this.teamHandlers.handleTeamSearch(args);
|
|
@@ -1537,7 +1553,8 @@ ${summary}...`, 0.8);
|
|
|
1537
1553
|
type: "text",
|
|
1538
1554
|
text: "Multi-provider routing is disabled."
|
|
1539
1555
|
}
|
|
1540
|
-
]
|
|
1556
|
+
],
|
|
1557
|
+
is_error: true
|
|
1541
1558
|
};
|
|
1542
1559
|
} else {
|
|
1543
1560
|
result = await this.providerHandlers.handleDelegateToModel(
|
|
@@ -1553,7 +1570,8 @@ ${summary}...`, 0.8);
|
|
|
1553
1570
|
type: "text",
|
|
1554
1571
|
text: "Multi-provider routing is disabled."
|
|
1555
1572
|
}
|
|
1556
|
-
]
|
|
1573
|
+
],
|
|
1574
|
+
is_error: true
|
|
1557
1575
|
};
|
|
1558
1576
|
} else {
|
|
1559
1577
|
result = await this.providerHandlers.handleBatchSubmit(
|
|
@@ -1569,7 +1587,8 @@ ${summary}...`, 0.8);
|
|
|
1569
1587
|
type: "text",
|
|
1570
1588
|
text: "Multi-provider routing is disabled."
|
|
1571
1589
|
}
|
|
1572
|
-
]
|
|
1590
|
+
],
|
|
1591
|
+
is_error: true
|
|
1573
1592
|
};
|
|
1574
1593
|
} else {
|
|
1575
1594
|
result = await this.providerHandlers.handleBatchCheck(
|
|
@@ -1580,12 +1599,16 @@ ${summary}...`, 0.8);
|
|
|
1580
1599
|
case "sm_digest":
|
|
1581
1600
|
result = await this.handleSmDigest(args);
|
|
1582
1601
|
break;
|
|
1602
|
+
case "sm_desire_paths":
|
|
1603
|
+
result = this.handleDesirePaths(args);
|
|
1604
|
+
break;
|
|
1583
1605
|
default:
|
|
1584
1606
|
throw new Error(`Unknown tool: ${name}`);
|
|
1585
1607
|
}
|
|
1586
1608
|
} catch (err) {
|
|
1587
1609
|
error = err instanceof Error ? err : new Error(String(err));
|
|
1588
1610
|
toolCall.error = error.message;
|
|
1611
|
+
this.logDesirePath(name, args, error.message);
|
|
1589
1612
|
throw err;
|
|
1590
1613
|
} finally {
|
|
1591
1614
|
const endTime = Date.now();
|
|
@@ -3052,6 +3075,128 @@ ${typeBreakdown}`
|
|
|
3052
3075
|
matchedText: editResult.match.matchedText.length > 200 ? editResult.match.matchedText.slice(0, 200) + "..." : editResult.match.matchedText
|
|
3053
3076
|
};
|
|
3054
3077
|
}
|
|
3078
|
+
/** Log failed tool calls to desire path JSONL for product prioritization */
|
|
3079
|
+
logDesirePath(tool, args, errorMsg) {
|
|
3080
|
+
try {
|
|
3081
|
+
const home = process.env["HOME"] || "/tmp";
|
|
3082
|
+
const dir = join(home, ".stackmemory", "desire-paths");
|
|
3083
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
3084
|
+
const category = /unknown tool/i.test(errorMsg) ? "unknown_tool" : /invalid param|missing.*param/i.test(errorMsg) ? "invalid_params" : "handler_error";
|
|
3085
|
+
const sanitized = {};
|
|
3086
|
+
for (const [k, v] of Object.entries(args)) {
|
|
3087
|
+
sanitized[k] = typeof v === "string" && v.length > 200 ? v.slice(0, 200) + "...[truncated]" : v;
|
|
3088
|
+
}
|
|
3089
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3090
|
+
const entry = {
|
|
3091
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3092
|
+
tool,
|
|
3093
|
+
input: sanitized,
|
|
3094
|
+
error: errorMsg.slice(0, 500),
|
|
3095
|
+
category,
|
|
3096
|
+
source: "mcp-server"
|
|
3097
|
+
};
|
|
3098
|
+
appendFileSync(
|
|
3099
|
+
join(dir, `desire-server-${date}.jsonl`),
|
|
3100
|
+
JSON.stringify(entry) + "\n"
|
|
3101
|
+
);
|
|
3102
|
+
} catch {
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
/** Handle sm_desire_paths tool — read and aggregate desire path logs */
|
|
3106
|
+
handleDesirePaths(args) {
|
|
3107
|
+
const mode = String(args.mode || "summary");
|
|
3108
|
+
const days = Number(args.days || 7);
|
|
3109
|
+
const limit = Number(args.limit || 20);
|
|
3110
|
+
const categoryFilter = args.category ? String(args.category) : void 0;
|
|
3111
|
+
const desireDir = join(homedir(), ".stackmemory", "desire-paths");
|
|
3112
|
+
if (!existsSync(desireDir)) {
|
|
3113
|
+
return {
|
|
3114
|
+
content: [{ type: "text", text: "No desire path data found." }]
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
3118
|
+
const files = readdirSync(desireDir).filter(
|
|
3119
|
+
(f) => f.startsWith("desire-") && f.endsWith(".jsonl")
|
|
3120
|
+
);
|
|
3121
|
+
const entries = [];
|
|
3122
|
+
for (const file of files) {
|
|
3123
|
+
const lines = readFileSync(join(desireDir, file), "utf-8").split("\n").filter(Boolean);
|
|
3124
|
+
for (const line of lines) {
|
|
3125
|
+
try {
|
|
3126
|
+
const entry = JSON.parse(line);
|
|
3127
|
+
if (new Date(entry.ts).getTime() < cutoff) continue;
|
|
3128
|
+
if (categoryFilter && entry.category !== categoryFilter) continue;
|
|
3129
|
+
entries.push(entry);
|
|
3130
|
+
} catch {
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
if (entries.length === 0) {
|
|
3135
|
+
return {
|
|
3136
|
+
content: [
|
|
3137
|
+
{
|
|
3138
|
+
type: "text",
|
|
3139
|
+
text: `No desire path data in the last ${days} day(s).`
|
|
3140
|
+
}
|
|
3141
|
+
]
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
if (mode === "list") {
|
|
3145
|
+
const recent = entries.sort((a, b) => b.ts.localeCompare(a.ts)).slice(0, limit);
|
|
3146
|
+
const lines = recent.map(
|
|
3147
|
+
(e) => `${e.ts.slice(0, 19)} ${e.tool} [${e.category}]
|
|
3148
|
+
${e.error.slice(0, 120)}`
|
|
3149
|
+
);
|
|
3150
|
+
return {
|
|
3151
|
+
content: [
|
|
3152
|
+
{
|
|
3153
|
+
type: "text",
|
|
3154
|
+
text: `Recent desire paths (${recent.length}/${entries.length}):
|
|
3155
|
+
|
|
3156
|
+
${lines.join("\n\n")}`
|
|
3157
|
+
}
|
|
3158
|
+
]
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
3162
|
+
for (const e of entries) {
|
|
3163
|
+
const existing = byTool.get(e.tool);
|
|
3164
|
+
if (!existing || e.ts > existing.lastSeen) {
|
|
3165
|
+
byTool.set(e.tool, {
|
|
3166
|
+
count: (existing?.count || 0) + 1,
|
|
3167
|
+
category: e.category,
|
|
3168
|
+
lastSeen: e.ts
|
|
3169
|
+
});
|
|
3170
|
+
} else {
|
|
3171
|
+
existing.count++;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
const sorted = [...byTool.entries()].sort(
|
|
3175
|
+
(a, b) => b[1].count - a[1].count
|
|
3176
|
+
);
|
|
3177
|
+
const byCat = /* @__PURE__ */ new Map();
|
|
3178
|
+
for (const e of entries) {
|
|
3179
|
+
byCat.set(e.category, (byCat.get(e.category) || 0) + 1);
|
|
3180
|
+
}
|
|
3181
|
+
const toolLines = sorted.map(
|
|
3182
|
+
([tool, data]) => `${tool}: ${data.count}x [${data.category}] (last: ${data.lastSeen.slice(0, 10)})`
|
|
3183
|
+
);
|
|
3184
|
+
const catLines = [...byCat.entries()].sort((a, b) => b[1] - a[1]).map(([cat, count]) => ` ${cat}: ${count}`);
|
|
3185
|
+
return {
|
|
3186
|
+
content: [
|
|
3187
|
+
{
|
|
3188
|
+
type: "text",
|
|
3189
|
+
text: `Desire Path Summary (last ${days}d, ${entries.length} total failures)
|
|
3190
|
+
|
|
3191
|
+
By Tool:
|
|
3192
|
+
${toolLines.join("\n")}
|
|
3193
|
+
|
|
3194
|
+
By Category:
|
|
3195
|
+
${catLines.join("\n")}`
|
|
3196
|
+
}
|
|
3197
|
+
]
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3055
3200
|
async handleSmDigest(args) {
|
|
3056
3201
|
const period = String(args.period || "today");
|
|
3057
3202
|
if (!["today", "yesterday", "week"].includes(period)) {
|
|
@@ -23,7 +23,8 @@ class MCPToolDefinitions {
|
|
|
23
23
|
...this.getProviderTools(),
|
|
24
24
|
...this.getTeamTools(),
|
|
25
25
|
...this.getCordTools(),
|
|
26
|
-
...this.getDigestTools()
|
|
26
|
+
...this.getDigestTools(),
|
|
27
|
+
...this.getDesirePathTools()
|
|
27
28
|
];
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
@@ -1175,6 +1176,43 @@ class MCPToolDefinitions {
|
|
|
1175
1176
|
}
|
|
1176
1177
|
];
|
|
1177
1178
|
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Desire path analysis tools
|
|
1181
|
+
*/
|
|
1182
|
+
getDesirePathTools() {
|
|
1183
|
+
return [
|
|
1184
|
+
{
|
|
1185
|
+
name: "sm_desire_paths",
|
|
1186
|
+
description: 'Analyze failed tool calls (desire paths) \u2014 what agents want but cannot get. Use mode "summary" for aggregated counts or "list" for recent failures.',
|
|
1187
|
+
inputSchema: {
|
|
1188
|
+
type: "object",
|
|
1189
|
+
properties: {
|
|
1190
|
+
mode: {
|
|
1191
|
+
type: "string",
|
|
1192
|
+
enum: ["summary", "list"],
|
|
1193
|
+
description: "Output mode: summary (aggregated) or list (recent)",
|
|
1194
|
+
default: "summary"
|
|
1195
|
+
},
|
|
1196
|
+
limit: {
|
|
1197
|
+
type: "number",
|
|
1198
|
+
default: 20,
|
|
1199
|
+
description: "Max entries to return (list mode only)"
|
|
1200
|
+
},
|
|
1201
|
+
category: {
|
|
1202
|
+
type: "string",
|
|
1203
|
+
enum: ["unknown_tool", "handler_error", "invalid_params"],
|
|
1204
|
+
description: "Filter by failure category"
|
|
1205
|
+
},
|
|
1206
|
+
days: {
|
|
1207
|
+
type: "number",
|
|
1208
|
+
default: 7,
|
|
1209
|
+
description: "Look back N days"
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
];
|
|
1215
|
+
}
|
|
1178
1216
|
/**
|
|
1179
1217
|
* Multi-agent team collaboration tools
|
|
1180
1218
|
*/
|
|
@@ -1426,6 +1464,8 @@ class MCPToolDefinitions {
|
|
|
1426
1464
|
return this.getCordTools();
|
|
1427
1465
|
case "digest":
|
|
1428
1466
|
return this.getDigestTools();
|
|
1467
|
+
case "desire_paths":
|
|
1468
|
+
return this.getDesirePathTools();
|
|
1429
1469
|
default:
|
|
1430
1470
|
return [];
|
|
1431
1471
|
}
|
|
@@ -47,6 +47,41 @@ const CANONICAL_HOOKS = [
|
|
|
47
47
|
timeout: 2,
|
|
48
48
|
commandPrefix: "node",
|
|
49
49
|
required: false
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
scriptName: "team-subagent-stop.js",
|
|
53
|
+
eventType: "SubagentStop",
|
|
54
|
+
timeout: 5,
|
|
55
|
+
commandPrefix: "node",
|
|
56
|
+
required: false
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
scriptName: "team-task-complete.js",
|
|
60
|
+
eventType: "TaskCompleted",
|
|
61
|
+
timeout: 5,
|
|
62
|
+
commandPrefix: "node",
|
|
63
|
+
required: false
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
scriptName: "team-teammate-idle.js",
|
|
67
|
+
eventType: "TeammateIdle",
|
|
68
|
+
timeout: 3,
|
|
69
|
+
commandPrefix: "node",
|
|
70
|
+
required: false
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
scriptName: "desire-path-trace.js",
|
|
74
|
+
eventType: "PostToolUse",
|
|
75
|
+
timeout: 2,
|
|
76
|
+
commandPrefix: "node",
|
|
77
|
+
required: false
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
scriptName: "daemon-auto-start.js",
|
|
81
|
+
eventType: "PostToolUse",
|
|
82
|
+
timeout: 2,
|
|
83
|
+
commandPrefix: "node",
|
|
84
|
+
required: false
|
|
50
85
|
}
|
|
51
86
|
];
|
|
52
87
|
const DEAD_HOOKS = ["sms-response-handler.js"];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "Project-scoped memory for AI coding tools. Durable context across sessions with
|
|
3
|
+
"version": "1.2.9",
|
|
4
|
+
"description": "Project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, Claude/Codex/OpenCode wrappers, Linear sync, automatic hooks, and log analysis.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|
|
7
7
|
"npm": ">=10.0.0"
|
|
@@ -89,6 +89,41 @@ try {
|
|
|
89
89
|
commandPrefix: 'node',
|
|
90
90
|
required: true,
|
|
91
91
|
},
|
|
92
|
+
{
|
|
93
|
+
scriptName: 'team-subagent-stop.js',
|
|
94
|
+
eventType: 'SubagentStop',
|
|
95
|
+
timeout: 5,
|
|
96
|
+
commandPrefix: 'node',
|
|
97
|
+
required: false,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
scriptName: 'team-task-complete.js',
|
|
101
|
+
eventType: 'TaskCompleted',
|
|
102
|
+
timeout: 5,
|
|
103
|
+
commandPrefix: 'node',
|
|
104
|
+
required: false,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
scriptName: 'team-teammate-idle.js',
|
|
108
|
+
eventType: 'TeammateIdle',
|
|
109
|
+
timeout: 3,
|
|
110
|
+
commandPrefix: 'node',
|
|
111
|
+
required: false,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
scriptName: 'desire-path-trace.js',
|
|
115
|
+
eventType: 'PostToolUse',
|
|
116
|
+
timeout: 2,
|
|
117
|
+
commandPrefix: 'node',
|
|
118
|
+
required: false,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
scriptName: 'daemon-auto-start.js',
|
|
122
|
+
eventType: 'PostToolUse',
|
|
123
|
+
timeout: 2,
|
|
124
|
+
commandPrefix: 'node',
|
|
125
|
+
required: false,
|
|
126
|
+
},
|
|
92
127
|
];
|
|
93
128
|
|
|
94
129
|
const DEAD_HOOKS = ['sms-response-handler.js'];
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Daemon Auto-Start Hook (PostToolUse)
|
|
5
|
+
*
|
|
6
|
+
* Checks if the unified daemon is running on each tool call.
|
|
7
|
+
* If not, spawns it in the background. Runs at most once per session
|
|
8
|
+
* (tracks via env var to avoid repeated checks).
|
|
9
|
+
*
|
|
10
|
+
* Must complete in <50ms — PID file check + optional spawn.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { spawn } = require('child_process');
|
|
16
|
+
|
|
17
|
+
const HOME = process.env.HOME || '/tmp';
|
|
18
|
+
const DAEMON_DIR = path.join(HOME, '.stackmemory', 'daemon');
|
|
19
|
+
const PID_FILE = path.join(DAEMON_DIR, 'daemon.pid');
|
|
20
|
+
|
|
21
|
+
// Track whether we already checked this session (avoid repeated spawns)
|
|
22
|
+
const STATE_KEY = 'STACKMEMORY_DAEMON_CHECKED';
|
|
23
|
+
const SESSION_ID = process.env.CLAUDE_INSTANCE_ID || String(process.ppid);
|
|
24
|
+
const STATE_FILE = path.join(
|
|
25
|
+
HOME,
|
|
26
|
+
'.stackmemory',
|
|
27
|
+
`daemon-check-${SESSION_ID}`
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
function isDaemonRunning() {
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(PID_FILE)) return false;
|
|
33
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim(), 10);
|
|
34
|
+
if (isNaN(pid)) return false;
|
|
35
|
+
process.kill(pid, 0); // signal 0 = check existence
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function findDaemonScript() {
|
|
43
|
+
// Check common locations for the daemon script
|
|
44
|
+
const candidates = [
|
|
45
|
+
// Global npm install
|
|
46
|
+
path.join(
|
|
47
|
+
__dirname,
|
|
48
|
+
'..',
|
|
49
|
+
'..',
|
|
50
|
+
'node_modules',
|
|
51
|
+
'@stackmemoryai',
|
|
52
|
+
'stackmemory',
|
|
53
|
+
'dist',
|
|
54
|
+
'daemon',
|
|
55
|
+
'unified-daemon.js'
|
|
56
|
+
),
|
|
57
|
+
// Homebrew global
|
|
58
|
+
path.join(
|
|
59
|
+
'/opt/homebrew/lib/node_modules/@stackmemoryai/stackmemory/dist/daemon/unified-daemon.js'
|
|
60
|
+
),
|
|
61
|
+
// ~/.stackmemory/bin (installed by postinstall)
|
|
62
|
+
path.join(HOME, '.stackmemory', 'bin', 'session-daemon.js'),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function alreadyChecked() {
|
|
72
|
+
try {
|
|
73
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
74
|
+
const age = Date.now() - fs.statSync(STATE_FILE).mtimeMs;
|
|
75
|
+
// Re-check every 30 minutes
|
|
76
|
+
return age < 30 * 60 * 1000;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function markChecked() {
|
|
85
|
+
try {
|
|
86
|
+
const dir = path.dirname(STATE_FILE);
|
|
87
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
88
|
+
fs.writeFileSync(STATE_FILE, String(Date.now()));
|
|
89
|
+
} catch {
|
|
90
|
+
// best effort
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function main() {
|
|
95
|
+
try {
|
|
96
|
+
// Skip if recently checked
|
|
97
|
+
if (alreadyChecked()) return;
|
|
98
|
+
markChecked();
|
|
99
|
+
|
|
100
|
+
// Already running? Done.
|
|
101
|
+
if (isDaemonRunning()) return;
|
|
102
|
+
|
|
103
|
+
// Try to spawn daemon
|
|
104
|
+
const script = findDaemonScript();
|
|
105
|
+
if (!script) return;
|
|
106
|
+
|
|
107
|
+
const child = spawn('node', [script], {
|
|
108
|
+
detached: true,
|
|
109
|
+
stdio: 'ignore',
|
|
110
|
+
env: { ...process.env },
|
|
111
|
+
});
|
|
112
|
+
child.unref();
|
|
113
|
+
} catch {
|
|
114
|
+
// Silent fail -- never block the agent
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Read stdin (required by hook protocol) then run
|
|
119
|
+
let input = '';
|
|
120
|
+
process.stdin.on('data', (chunk) => {
|
|
121
|
+
input += chunk;
|
|
122
|
+
});
|
|
123
|
+
process.stdin.on('end', () => {
|
|
124
|
+
main();
|
|
125
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Desire Path Trace Hook (PostToolUse)
|
|
5
|
+
*
|
|
6
|
+
* Detects failed tool calls and logs them to ~/.stackmemory/desire-paths/
|
|
7
|
+
* as JSONL for later analysis. Tracks what agents *want* but can't get:
|
|
8
|
+
* unknown tools, invalid params, handler errors.
|
|
9
|
+
*
|
|
10
|
+
* Must complete in <50ms -- pure file I/O only.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const HOME = process.env.HOME || '/tmp';
|
|
17
|
+
const DESIRE_DIR = path.join(HOME, '.stackmemory', 'desire-paths');
|
|
18
|
+
|
|
19
|
+
function ensureDir(dir) {
|
|
20
|
+
if (!fs.existsSync(dir)) {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function categorize(errorText) {
|
|
26
|
+
if (!errorText) return 'handler_error';
|
|
27
|
+
if (/unknown tool/i.test(errorText)) return 'unknown_tool';
|
|
28
|
+
if (
|
|
29
|
+
/invalid param|missing.*param|required.*param|unexpected.*param/i.test(
|
|
30
|
+
errorText
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
return 'invalid_params';
|
|
34
|
+
return 'handler_error';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isFailure(input) {
|
|
38
|
+
// Check is_error flag
|
|
39
|
+
if (input.tool_response && input.tool_response.is_error === true) return true;
|
|
40
|
+
|
|
41
|
+
// Check for error text in response
|
|
42
|
+
const response =
|
|
43
|
+
typeof input.tool_response === 'string'
|
|
44
|
+
? input.tool_response
|
|
45
|
+
: JSON.stringify(input.tool_response || '');
|
|
46
|
+
|
|
47
|
+
if (/Unknown tool:/i.test(response)) return true;
|
|
48
|
+
if (/^Error:/m.test(response)) return true;
|
|
49
|
+
if (/McpError|MCP error/i.test(response)) return true;
|
|
50
|
+
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function extractError(input) {
|
|
55
|
+
const response = input.tool_response;
|
|
56
|
+
if (!response) return 'unknown error';
|
|
57
|
+
|
|
58
|
+
if (typeof response === 'string') return response.slice(0, 500);
|
|
59
|
+
if (response.error) return String(response.error).slice(0, 500);
|
|
60
|
+
if (response.content && Array.isArray(response.content)) {
|
|
61
|
+
const textBlock = response.content.find((b) => b.type === 'text');
|
|
62
|
+
if (textBlock) return String(textBlock.text).slice(0, 500);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return JSON.stringify(response).slice(0, 500);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function truncateInput(toolInput) {
|
|
69
|
+
if (!toolInput) return {};
|
|
70
|
+
const result = {};
|
|
71
|
+
for (const [key, value] of Object.entries(toolInput)) {
|
|
72
|
+
if (typeof value === 'string' && value.length > 200) {
|
|
73
|
+
result[key] = value.slice(0, 200) + '...[truncated]';
|
|
74
|
+
} else {
|
|
75
|
+
result[key] = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function readInput() {
|
|
82
|
+
let input = '';
|
|
83
|
+
for await (const chunk of process.stdin) {
|
|
84
|
+
input += chunk;
|
|
85
|
+
}
|
|
86
|
+
return JSON.parse(input);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function main() {
|
|
90
|
+
try {
|
|
91
|
+
const input = await readInput();
|
|
92
|
+
|
|
93
|
+
if (!isFailure(input)) return;
|
|
94
|
+
|
|
95
|
+
const errorText = extractError(input);
|
|
96
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
97
|
+
const entry = {
|
|
98
|
+
ts: new Date().toISOString(),
|
|
99
|
+
sid:
|
|
100
|
+
input.session_id ||
|
|
101
|
+
process.env.CLAUDE_INSTANCE_ID ||
|
|
102
|
+
String(process.ppid),
|
|
103
|
+
tool: input.tool_name || 'unknown',
|
|
104
|
+
input: truncateInput(input.tool_input),
|
|
105
|
+
error: errorText,
|
|
106
|
+
category: categorize(errorText),
|
|
107
|
+
cwd: input.cwd || process.cwd(),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
ensureDir(DESIRE_DIR);
|
|
111
|
+
const filePath = path.join(DESIRE_DIR, `desire-${date}.jsonl`);
|
|
112
|
+
fs.appendFileSync(filePath, JSON.stringify(entry) + '\n');
|
|
113
|
+
} catch {
|
|
114
|
+
// Silent fail -- never block the agent
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
main();
|