@jmylchreest/aide-plugin 0.0.57 → 0.0.58
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/package.json +5 -2
- package/src/cli/codex-config.ts +428 -0
- package/src/cli/hook.ts +85 -0
- package/src/cli/index.ts +49 -12
- package/src/cli/install.ts +52 -25
- package/src/cli/status.ts +50 -17
- package/src/cli/uninstall.ts +29 -8
- package/src/core/mcp-sync.ts +124 -11
- package/src/core/types.ts +2 -2
- package/src/hooks/agent-cleanup.ts +91 -0
- package/src/hooks/comment-checker.ts +115 -0
- package/src/hooks/context-guard.ts +115 -0
- package/src/hooks/context-pruning.ts +216 -0
- package/src/hooks/hud-updater.ts +180 -0
- package/src/hooks/permission-handler.ts +173 -0
- package/src/hooks/persistence.ts +93 -0
- package/src/hooks/pre-compact.ts +127 -0
- package/src/hooks/pre-tool-enforcer.ts +120 -0
- package/src/hooks/session-end.ts +148 -0
- package/src/hooks/session-start.ts +488 -0
- package/src/hooks/session-summary.ts +147 -0
- package/src/hooks/skill-injector.ts +235 -0
- package/src/hooks/subagent-tracker.ts +525 -0
- package/src/hooks/task-completed.ts +445 -0
- package/src/hooks/tool-tracker.ts +89 -0
- package/src/hooks/write-guard.ts +95 -0
- package/src/lib/hook-utils.ts +53 -1
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Subagent Tracker Hook (SubagentStart, SubagentStop)
|
|
4
|
+
*
|
|
5
|
+
* Tracks spawned subagents for HUD display and coordination.
|
|
6
|
+
* Registers agents in aide-memory with their type, model, and task.
|
|
7
|
+
* Also injects memory context for subagents (global preferences, decisions).
|
|
8
|
+
*
|
|
9
|
+
* SubagentStart data from Claude Code:
|
|
10
|
+
* - agent_id, agent_type, session_id, prompt
|
|
11
|
+
* - model, cwd, permission_mode
|
|
12
|
+
*
|
|
13
|
+
* SubagentStop data from Claude Code:
|
|
14
|
+
* - agent_id, agent_type, output, success
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { execFileSync } from "child_process";
|
|
18
|
+
import { basename } from "path";
|
|
19
|
+
import { Logger } from "../lib/logger.js";
|
|
20
|
+
import { readStdin, setMemoryState } from "../lib/hook-utils.js";
|
|
21
|
+
import { findAideBinary } from "../core/aide-client.js";
|
|
22
|
+
import { refreshHud } from "../lib/hud.js";
|
|
23
|
+
import {
|
|
24
|
+
getWorktreeForAgent,
|
|
25
|
+
markWorktreeComplete,
|
|
26
|
+
discoverWorktrees,
|
|
27
|
+
Worktree,
|
|
28
|
+
} from "../lib/worktree.js";
|
|
29
|
+
|
|
30
|
+
// Global logger instance
|
|
31
|
+
let log: Logger | null = null;
|
|
32
|
+
|
|
33
|
+
// Claude Code hook input format (uses hook_event_name, not event)
|
|
34
|
+
interface SubagentStartInput {
|
|
35
|
+
hook_event_name: "SubagentStart";
|
|
36
|
+
agent_id: string;
|
|
37
|
+
agent_type: string;
|
|
38
|
+
session_id: string;
|
|
39
|
+
transcript_path?: string;
|
|
40
|
+
cwd: string;
|
|
41
|
+
permission_mode?: string;
|
|
42
|
+
// Note: prompt and model are NOT provided by Claude Code
|
|
43
|
+
// We'll need to get these from PreToolUse if needed
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface SubagentStopInput {
|
|
47
|
+
hook_event_name: "SubagentStop";
|
|
48
|
+
agent_id: string;
|
|
49
|
+
agent_type: string;
|
|
50
|
+
session_id: string;
|
|
51
|
+
transcript_path?: string;
|
|
52
|
+
agent_transcript_path?: string;
|
|
53
|
+
stop_hook_active?: boolean;
|
|
54
|
+
cwd: string;
|
|
55
|
+
permission_mode?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type HookInput = SubagentStartInput | SubagentStopInput;
|
|
59
|
+
|
|
60
|
+
interface HookOutput {
|
|
61
|
+
continue: boolean;
|
|
62
|
+
hookSpecificOutput?: {
|
|
63
|
+
hookEventName: string;
|
|
64
|
+
additionalContext?: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get project name from git remote or directory name
|
|
70
|
+
*/
|
|
71
|
+
function getProjectName(cwd: string): string {
|
|
72
|
+
try {
|
|
73
|
+
// Try git remote first
|
|
74
|
+
const remoteUrl = execFileSync(
|
|
75
|
+
"git",
|
|
76
|
+
["config", "--get", "remote.origin.url"],
|
|
77
|
+
{
|
|
78
|
+
cwd,
|
|
79
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
80
|
+
timeout: 2000,
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
.toString()
|
|
84
|
+
.trim();
|
|
85
|
+
|
|
86
|
+
// Extract repo name from URL
|
|
87
|
+
const match = remoteUrl.match(/[/:]([^/]+?)(?:\.git)?$/);
|
|
88
|
+
if (match) return match[1];
|
|
89
|
+
} catch (err) {
|
|
90
|
+
log?.debug(
|
|
91
|
+
`getProjectName: git remote failed (not a git repo or no remote): ${err}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Fallback to directory name
|
|
96
|
+
return basename(cwd) || "unknown";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Fetch essential memories for subagent context injection
|
|
101
|
+
*
|
|
102
|
+
* Subagents get:
|
|
103
|
+
* - Global preferences (scope:global)
|
|
104
|
+
* - Project memories (project:<name>)
|
|
105
|
+
* - Project decisions
|
|
106
|
+
*
|
|
107
|
+
* This ensures subagents respect user preferences, project context,
|
|
108
|
+
* and architectural decisions.
|
|
109
|
+
*/
|
|
110
|
+
function fetchSubagentMemories(cwd: string): {
|
|
111
|
+
global: string[];
|
|
112
|
+
project: string[];
|
|
113
|
+
decisions: string[];
|
|
114
|
+
} {
|
|
115
|
+
const result = {
|
|
116
|
+
global: [] as string[],
|
|
117
|
+
project: [] as string[],
|
|
118
|
+
decisions: [] as string[],
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Check for disable flag
|
|
122
|
+
if (process.env.AIDE_MEMORY_INJECT === "0") {
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const binary = findAideBinary({
|
|
127
|
+
cwd,
|
|
128
|
+
pluginRoot: process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
|
|
129
|
+
});
|
|
130
|
+
if (!binary) {
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const projectName = getProjectName(cwd);
|
|
135
|
+
|
|
136
|
+
// Fetch global memories (scope:global)
|
|
137
|
+
try {
|
|
138
|
+
const globalOutput = execFileSync(
|
|
139
|
+
binary,
|
|
140
|
+
[
|
|
141
|
+
"memory",
|
|
142
|
+
"list",
|
|
143
|
+
"--category=global",
|
|
144
|
+
"--tags=scope:global",
|
|
145
|
+
"--format=json",
|
|
146
|
+
],
|
|
147
|
+
{ cwd, stdio: ["pipe", "pipe", "pipe"], timeout: 3000 },
|
|
148
|
+
)
|
|
149
|
+
.toString()
|
|
150
|
+
.trim();
|
|
151
|
+
|
|
152
|
+
if (globalOutput && globalOutput !== "[]") {
|
|
153
|
+
const memories = JSON.parse(globalOutput);
|
|
154
|
+
result.global = memories.map((m: { content: string }) => m.content);
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
log?.debug(
|
|
158
|
+
`fetchSubagentMemories: global memory fetch failed (optional): ${err}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Fetch project memories (project:<name>)
|
|
163
|
+
try {
|
|
164
|
+
const projectOutput = execFileSync(
|
|
165
|
+
binary,
|
|
166
|
+
["memory", "list", `--tags=project:${projectName}`, "--format=json"],
|
|
167
|
+
{ cwd, stdio: ["pipe", "pipe", "pipe"], timeout: 3000 },
|
|
168
|
+
)
|
|
169
|
+
.toString()
|
|
170
|
+
.trim();
|
|
171
|
+
|
|
172
|
+
if (projectOutput && projectOutput !== "[]") {
|
|
173
|
+
const memories = JSON.parse(projectOutput);
|
|
174
|
+
result.project = memories.map((m: { content: string }) => m.content);
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
log?.debug(
|
|
178
|
+
`fetchSubagentMemories: project memory fetch failed (optional): ${err}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Fetch project decisions
|
|
183
|
+
try {
|
|
184
|
+
const decisionsOutput = execFileSync(
|
|
185
|
+
binary,
|
|
186
|
+
["decision", "list", "--format=json"],
|
|
187
|
+
{ cwd, stdio: ["pipe", "pipe", "pipe"], timeout: 3000 },
|
|
188
|
+
)
|
|
189
|
+
.toString()
|
|
190
|
+
.trim();
|
|
191
|
+
|
|
192
|
+
if (decisionsOutput && decisionsOutput !== "[]") {
|
|
193
|
+
const decisions = JSON.parse(decisionsOutput);
|
|
194
|
+
result.decisions = decisions.map(
|
|
195
|
+
(d: { topic: string; value: string }) => `**${d.topic}**: ${d.value}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
} catch (err) {
|
|
199
|
+
log?.debug(
|
|
200
|
+
`fetchSubagentMemories: decision fetch failed (optional): ${err}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Build context for subagent injection
|
|
209
|
+
*/
|
|
210
|
+
function buildSubagentContext(
|
|
211
|
+
memories: {
|
|
212
|
+
global: string[];
|
|
213
|
+
project: string[];
|
|
214
|
+
decisions: string[];
|
|
215
|
+
},
|
|
216
|
+
worktree?: Worktree,
|
|
217
|
+
): string {
|
|
218
|
+
const lines: string[] = [];
|
|
219
|
+
|
|
220
|
+
lines.push("<aide-subagent-context>");
|
|
221
|
+
|
|
222
|
+
// Inject worktree information if this is a swarm agent
|
|
223
|
+
if (worktree) {
|
|
224
|
+
lines.push("");
|
|
225
|
+
lines.push("## Swarm Worktree");
|
|
226
|
+
lines.push("");
|
|
227
|
+
lines.push(`You are working in an isolated git worktree for swarm mode.`);
|
|
228
|
+
lines.push(`- **Worktree Path**: ${worktree.path}`);
|
|
229
|
+
lines.push(`- **Branch**: ${worktree.branch}`);
|
|
230
|
+
lines.push(`- **Story ID**: ${worktree.taskId || "unknown"}`);
|
|
231
|
+
lines.push("");
|
|
232
|
+
lines.push(
|
|
233
|
+
`**IMPORTANT**: All file operations should be performed in: ${worktree.path}`,
|
|
234
|
+
);
|
|
235
|
+
lines.push(
|
|
236
|
+
`Commit your changes to the ${worktree.branch} branch when complete.`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (memories.global.length > 0) {
|
|
241
|
+
lines.push("");
|
|
242
|
+
lines.push("## User Preferences");
|
|
243
|
+
lines.push("");
|
|
244
|
+
for (const mem of memories.global) {
|
|
245
|
+
lines.push(`- ${mem}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (memories.project.length > 0) {
|
|
250
|
+
lines.push("");
|
|
251
|
+
lines.push("## Project Context");
|
|
252
|
+
lines.push("");
|
|
253
|
+
for (const mem of memories.project) {
|
|
254
|
+
lines.push(`- ${mem}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (memories.decisions.length > 0) {
|
|
259
|
+
lines.push("");
|
|
260
|
+
lines.push("## Project Decisions");
|
|
261
|
+
lines.push("");
|
|
262
|
+
for (const decision of memories.decisions) {
|
|
263
|
+
lines.push(`- ${decision}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Always inject messaging protocol for agent coordination
|
|
268
|
+
lines.push("");
|
|
269
|
+
lines.push("## Agent Communication");
|
|
270
|
+
lines.push("");
|
|
271
|
+
lines.push("Use aide MCP messaging tools to coordinate with other agents:");
|
|
272
|
+
lines.push("");
|
|
273
|
+
lines.push("**Send messages** via `mcp__plugin_aide_aide__message_send`:");
|
|
274
|
+
lines.push("- `from`: Your agent ID (required)");
|
|
275
|
+
lines.push("- `to`: Target agent ID (omit to broadcast)");
|
|
276
|
+
lines.push("- `content`: Message text");
|
|
277
|
+
lines.push(
|
|
278
|
+
"- `type`: One of `status`, `request`, `response`, `blocker`, `completion`, `handoff`",
|
|
279
|
+
);
|
|
280
|
+
lines.push("");
|
|
281
|
+
lines.push("**Check messages** via `mcp__plugin_aide_aide__message_list`:");
|
|
282
|
+
lines.push("- `agent_id`: Your agent ID");
|
|
283
|
+
lines.push("");
|
|
284
|
+
lines.push("**Acknowledge** via `mcp__plugin_aide_aide__message_ack`:");
|
|
285
|
+
lines.push("- `message_id`: ID from message_list");
|
|
286
|
+
lines.push("- `agent_id`: Your agent ID");
|
|
287
|
+
lines.push("");
|
|
288
|
+
lines.push("**Protocol:**");
|
|
289
|
+
lines.push("- Send `status` message at each SDLC stage transition");
|
|
290
|
+
lines.push("- Send `blocker` when stuck and need help from another agent");
|
|
291
|
+
lines.push("- Send `completion` when your story/task is done");
|
|
292
|
+
lines.push("- Check messages at the start of each SDLC stage");
|
|
293
|
+
|
|
294
|
+
lines.push("");
|
|
295
|
+
lines.push("</aide-subagent-context>");
|
|
296
|
+
return lines.join("\n");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Set state in aide-memory for an agent (wrapper with logging)
|
|
301
|
+
*/
|
|
302
|
+
function setAgentState(
|
|
303
|
+
cwd: string,
|
|
304
|
+
agentId: string,
|
|
305
|
+
key: string,
|
|
306
|
+
value: string,
|
|
307
|
+
): boolean {
|
|
308
|
+
const truncatedValue = value.replace(/\n/g, " ").slice(0, 500);
|
|
309
|
+
log?.debug(
|
|
310
|
+
`setAgentState: setting ${key}="${truncatedValue}" for agent ${agentId}`,
|
|
311
|
+
);
|
|
312
|
+
const result = setMemoryState(cwd, key, truncatedValue, agentId);
|
|
313
|
+
if (!result) {
|
|
314
|
+
log?.warn(`setAgentState: failed to set ${key} for agent ${agentId}`);
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Handle SubagentStart event
|
|
321
|
+
* Returns context to inject into the subagent
|
|
322
|
+
*/
|
|
323
|
+
async function processSubagentStart(
|
|
324
|
+
data: SubagentStartInput,
|
|
325
|
+
): Promise<string | undefined> {
|
|
326
|
+
const { agent_id, agent_type, session_id, cwd } = data;
|
|
327
|
+
|
|
328
|
+
log?.info(
|
|
329
|
+
`SubagentStart: agent_id=${agent_id}, type=${agent_type}, session=${session_id}`,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// Claude Code doesn't provide prompt/model in SubagentStart
|
|
333
|
+
// Use agent_type directly as the type
|
|
334
|
+
const type = agent_type;
|
|
335
|
+
|
|
336
|
+
log?.debug(`SubagentStart: registering type=${type}`);
|
|
337
|
+
|
|
338
|
+
// Register agent in aide-memory
|
|
339
|
+
// Note: modelTier is NOT stored - model instructions are injected into context instead
|
|
340
|
+
log?.start("registerAgent");
|
|
341
|
+
setAgentState(cwd, agent_id, "status", "running");
|
|
342
|
+
setAgentState(cwd, agent_id, "type", type);
|
|
343
|
+
setAgentState(cwd, agent_id, "startedAt", new Date().toISOString());
|
|
344
|
+
setAgentState(cwd, agent_id, "session", session_id); // Track which session owns this agent
|
|
345
|
+
log?.end("registerAgent");
|
|
346
|
+
|
|
347
|
+
// Refresh HUD to show the new running agent
|
|
348
|
+
log?.start("refreshHud");
|
|
349
|
+
refreshHud(cwd, session_id);
|
|
350
|
+
log?.end("refreshHud");
|
|
351
|
+
|
|
352
|
+
// Auto-discover any worktrees created by the orchestrator via git commands
|
|
353
|
+
// This ensures we track worktrees even if they weren't created via our library
|
|
354
|
+
log?.start("discoverWorktrees");
|
|
355
|
+
const discovered = discoverWorktrees(cwd);
|
|
356
|
+
if (discovered.length > 0) {
|
|
357
|
+
log?.info(`Auto-discovered ${discovered.length} worktrees`);
|
|
358
|
+
}
|
|
359
|
+
log?.end("discoverWorktrees", { discovered: discovered.length });
|
|
360
|
+
|
|
361
|
+
// Check if this agent has an associated worktree (swarm mode)
|
|
362
|
+
// Match by agent_id or by pattern in worktree name
|
|
363
|
+
log?.start("checkWorktree");
|
|
364
|
+
let worktree = getWorktreeForAgent(cwd, agent_id);
|
|
365
|
+
|
|
366
|
+
// If no direct match, try to match by agent_id pattern in worktree name
|
|
367
|
+
// This handles cases where worktree was created before agent_id was known
|
|
368
|
+
if (!worktree) {
|
|
369
|
+
const { loadWorktreeState } = await import("../lib/worktree.js");
|
|
370
|
+
const state = loadWorktreeState(cwd);
|
|
371
|
+
// Look for worktree with matching name pattern (e.g., "story-auth" matches "agent-auth")
|
|
372
|
+
const agentPattern = agent_id.replace(/^agent-/, "");
|
|
373
|
+
worktree = state.active.find(
|
|
374
|
+
(w) => w.name.includes(agentPattern) && !w.agentId,
|
|
375
|
+
);
|
|
376
|
+
if (worktree) {
|
|
377
|
+
// Assign this agent to the worktree
|
|
378
|
+
worktree.agentId = agent_id;
|
|
379
|
+
const { saveWorktreeState } = await import("../lib/worktree.js");
|
|
380
|
+
saveWorktreeState(cwd, state);
|
|
381
|
+
log?.info(`Assigned worktree ${worktree.name} to agent ${agent_id}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (worktree) {
|
|
386
|
+
log?.info(
|
|
387
|
+
`Found worktree for agent ${agent_id}: ${worktree.path} (branch: ${worktree.branch})`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
log?.end("checkWorktree", { hasWorktree: !!worktree });
|
|
391
|
+
|
|
392
|
+
// Fetch memories for subagent context injection
|
|
393
|
+
log?.start("fetchMemories");
|
|
394
|
+
const memories = fetchSubagentMemories(cwd);
|
|
395
|
+
log?.end("fetchMemories", {
|
|
396
|
+
globalCount: memories.global.length,
|
|
397
|
+
projectCount: memories.project.length,
|
|
398
|
+
decisionCount: memories.decisions.length,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Always build and inject context (messaging section is unconditional)
|
|
402
|
+
const context = buildSubagentContext(memories, worktree);
|
|
403
|
+
log?.info(
|
|
404
|
+
`Injecting context for subagent: ${memories.global.length} preferences, ${memories.project.length} project, ${memories.decisions.length} decisions, worktree=${!!worktree}`,
|
|
405
|
+
);
|
|
406
|
+
return context;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Handle SubagentStop event
|
|
411
|
+
*/
|
|
412
|
+
async function processSubagentStop(data: SubagentStopInput): Promise<void> {
|
|
413
|
+
const { agent_id, session_id, cwd, stop_hook_active } = data;
|
|
414
|
+
|
|
415
|
+
log?.info(
|
|
416
|
+
`SubagentStop: agent_id=${agent_id}, session=${session_id}, stop_hook_active=${stop_hook_active}`,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// Mark as completed (Claude Code doesn't provide success/failure status)
|
|
420
|
+
log?.start("updateAgentStatus");
|
|
421
|
+
setAgentState(cwd, agent_id, "status", "completed");
|
|
422
|
+
setAgentState(cwd, agent_id, "endedAt", new Date().toISOString());
|
|
423
|
+
log?.end("updateAgentStatus");
|
|
424
|
+
|
|
425
|
+
// Mark worktree as agent-complete if this agent had one (swarm mode)
|
|
426
|
+
// The worktree stays for merge review - cleanup happens after worktree-resolve
|
|
427
|
+
log?.start("checkWorktreeComplete");
|
|
428
|
+
const worktreeMarked = markWorktreeComplete(cwd, agent_id);
|
|
429
|
+
if (worktreeMarked) {
|
|
430
|
+
log?.info(
|
|
431
|
+
`Marked worktree as agent-complete for ${agent_id} - ready for merge review`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
log?.end("checkWorktreeComplete", { worktreeMarked });
|
|
435
|
+
|
|
436
|
+
// Refresh HUD to remove the completed agent
|
|
437
|
+
log?.start("refreshHud");
|
|
438
|
+
refreshHud(cwd, session_id);
|
|
439
|
+
log?.end("refreshHud");
|
|
440
|
+
|
|
441
|
+
log?.debug(`SubagentStop: agent ${agent_id} marked as completed`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function main(): Promise<void> {
|
|
445
|
+
try {
|
|
446
|
+
const input = await readStdin();
|
|
447
|
+
if (!input.trim()) {
|
|
448
|
+
console.log(JSON.stringify({ continue: true }));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const data: HookInput = JSON.parse(input);
|
|
453
|
+
const cwd = data.cwd || process.cwd();
|
|
454
|
+
|
|
455
|
+
// Initialize logger
|
|
456
|
+
log = new Logger("subagent-tracker", cwd);
|
|
457
|
+
log.start("total");
|
|
458
|
+
log.info(`Received event: ${data.hook_event_name}`);
|
|
459
|
+
log.debug(`Full input: ${JSON.stringify(data, null, 2)}`);
|
|
460
|
+
|
|
461
|
+
let additionalContext: string | undefined;
|
|
462
|
+
|
|
463
|
+
// Dispatch based on event type from input (Claude Code uses hook_event_name)
|
|
464
|
+
if (data.hook_event_name === "SubagentStart") {
|
|
465
|
+
additionalContext = await processSubagentStart(
|
|
466
|
+
data as SubagentStartInput,
|
|
467
|
+
);
|
|
468
|
+
} else if (data.hook_event_name === "SubagentStop") {
|
|
469
|
+
await processSubagentStop(data as SubagentStopInput);
|
|
470
|
+
} else {
|
|
471
|
+
// TypeScript narrows to never here, but handle unexpected events gracefully
|
|
472
|
+
log.warn(
|
|
473
|
+
`Unknown event type: ${(data as { hook_event_name?: string }).hook_event_name || "undefined"}`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
log.end("total");
|
|
478
|
+
log.flush();
|
|
479
|
+
|
|
480
|
+
// Output with optional context injection for subagents
|
|
481
|
+
const output: HookOutput = { continue: true };
|
|
482
|
+
if (additionalContext) {
|
|
483
|
+
output.hookSpecificOutput = {
|
|
484
|
+
hookEventName: "SubagentStart",
|
|
485
|
+
additionalContext,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
console.log(JSON.stringify(output));
|
|
490
|
+
} catch (error) {
|
|
491
|
+
// Log error but don't block
|
|
492
|
+
if (log) {
|
|
493
|
+
log.error("Subagent tracker failed", error);
|
|
494
|
+
log.flush();
|
|
495
|
+
}
|
|
496
|
+
console.log(JSON.stringify({ continue: true }));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
process.on("uncaughtException", (err) => {
|
|
501
|
+
if (log) {
|
|
502
|
+
log.error(`UNCAUGHT EXCEPTION: ${err}`);
|
|
503
|
+
log.flush();
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
console.log(JSON.stringify({ continue: true }));
|
|
507
|
+
} catch {
|
|
508
|
+
console.log('{"continue":true}');
|
|
509
|
+
}
|
|
510
|
+
process.exit(0);
|
|
511
|
+
});
|
|
512
|
+
process.on("unhandledRejection", (reason) => {
|
|
513
|
+
if (log) {
|
|
514
|
+
log.error(`UNHANDLED REJECTION: ${reason}`);
|
|
515
|
+
log.flush();
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
console.log(JSON.stringify({ continue: true }));
|
|
519
|
+
} catch {
|
|
520
|
+
console.log('{"continue":true}');
|
|
521
|
+
}
|
|
522
|
+
process.exit(0);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
main();
|