@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.
@@ -13,7 +13,15 @@ import {
13
13
  AddAnchorSchema,
14
14
  CreateTaskSchema
15
15
  } from "./schemas.js";
16
- import { readFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
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.8",
4
- "description": "Project-scoped memory for AI coding tools. Durable context across sessions with 55 MCP tools, FTS5 search, Claude/Codex/OpenCode wrappers, Linear sync, automatic hooks, and log analysis.",
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();