@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,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Comment Checker Hook (PostToolUse)
|
|
4
|
+
*
|
|
5
|
+
* Detects excessive or obvious comments in code written by AI agents.
|
|
6
|
+
* Injects a warning via additionalContext to nudge the agent toward
|
|
7
|
+
* cleaner code without blocking the tool call.
|
|
8
|
+
*
|
|
9
|
+
* Core logic is in src/core/comment-checker.ts for cross-platform reuse.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readStdin } from "../lib/hook-utils.js";
|
|
13
|
+
import { debug } from "../lib/logger.js";
|
|
14
|
+
import {
|
|
15
|
+
checkComments,
|
|
16
|
+
getCheckableFilePath,
|
|
17
|
+
getContentToCheck,
|
|
18
|
+
} from "../core/comment-checker.js";
|
|
19
|
+
|
|
20
|
+
const SOURCE = "comment-checker";
|
|
21
|
+
|
|
22
|
+
interface HookInput {
|
|
23
|
+
hook_event_name: string;
|
|
24
|
+
session_id: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
tool_name?: string;
|
|
27
|
+
agent_id?: string;
|
|
28
|
+
tool_input?: Record<string, unknown>;
|
|
29
|
+
tool_result?: {
|
|
30
|
+
success: boolean;
|
|
31
|
+
duration?: number;
|
|
32
|
+
};
|
|
33
|
+
transcript_path?: string;
|
|
34
|
+
permission_mode?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface HookOutput {
|
|
38
|
+
continue: true;
|
|
39
|
+
hookSpecificOutput?: {
|
|
40
|
+
hookEventName: string;
|
|
41
|
+
additionalContext?: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function main(): Promise<void> {
|
|
46
|
+
try {
|
|
47
|
+
const input = await readStdin();
|
|
48
|
+
if (!input.trim()) {
|
|
49
|
+
console.log(JSON.stringify({ continue: true }));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data: HookInput = JSON.parse(input);
|
|
54
|
+
const toolName = data.tool_name || "";
|
|
55
|
+
const toolInput = data.tool_input || {};
|
|
56
|
+
|
|
57
|
+
// Only check Write/Edit/MultiEdit tool calls
|
|
58
|
+
const filePath = getCheckableFilePath(toolName, toolInput);
|
|
59
|
+
if (!filePath) {
|
|
60
|
+
console.log(JSON.stringify({ continue: true }));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get the content to analyze
|
|
65
|
+
const contentResult = getContentToCheck(toolName, toolInput);
|
|
66
|
+
if (!contentResult) {
|
|
67
|
+
console.log(JSON.stringify({ continue: true }));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const [content, isNewContent] = contentResult;
|
|
72
|
+
const result = checkComments(filePath, content, isNewContent);
|
|
73
|
+
|
|
74
|
+
if (result.hasExcessiveComments) {
|
|
75
|
+
debug(
|
|
76
|
+
SOURCE,
|
|
77
|
+
`Detected ${result.suspiciousCount} suspicious comments in ${filePath}`,
|
|
78
|
+
);
|
|
79
|
+
const output: HookOutput = {
|
|
80
|
+
continue: true,
|
|
81
|
+
hookSpecificOutput: {
|
|
82
|
+
hookEventName: "PostToolUse",
|
|
83
|
+
additionalContext: result.warning,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
console.log(JSON.stringify(output));
|
|
87
|
+
} else {
|
|
88
|
+
console.log(JSON.stringify({ continue: true }));
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
debug(SOURCE, `Hook error: ${error}`);
|
|
92
|
+
console.log(JSON.stringify({ continue: true }));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
process.on("uncaughtException", (err) => {
|
|
97
|
+
debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
|
|
98
|
+
try {
|
|
99
|
+
console.log(JSON.stringify({ continue: true }));
|
|
100
|
+
} catch {
|
|
101
|
+
console.log('{"continue":true}');
|
|
102
|
+
}
|
|
103
|
+
process.exit(0);
|
|
104
|
+
});
|
|
105
|
+
process.on("unhandledRejection", (reason) => {
|
|
106
|
+
debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
|
|
107
|
+
try {
|
|
108
|
+
console.log(JSON.stringify({ continue: true }));
|
|
109
|
+
} catch {
|
|
110
|
+
console.log('{"continue":true}');
|
|
111
|
+
}
|
|
112
|
+
process.exit(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
main();
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Context Guard Hook (PreToolUse)
|
|
4
|
+
*
|
|
5
|
+
* Monitors Read tool calls and advises agents to use code_outline
|
|
6
|
+
* before reading large files. Also tracks code_outline/code_symbols
|
|
7
|
+
* calls so it knows which files have been outlined.
|
|
8
|
+
*
|
|
9
|
+
* This is a soft warning — it never blocks, only injects advisory context.
|
|
10
|
+
*
|
|
11
|
+
* Core logic is in src/core/context-guard.ts for cross-platform reuse.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readStdin } from "../lib/hook-utils.js";
|
|
15
|
+
import { debug } from "../lib/logger.js";
|
|
16
|
+
import { checkContextGuard, checkSmartReadHint } from "../core/context-guard.js";
|
|
17
|
+
import { findAideBinary } from "../core/aide-client.js";
|
|
18
|
+
|
|
19
|
+
const SOURCE = "context-guard";
|
|
20
|
+
|
|
21
|
+
interface HookInput {
|
|
22
|
+
hook_event_name: string;
|
|
23
|
+
session_id: string;
|
|
24
|
+
cwd: string;
|
|
25
|
+
tool_name?: string;
|
|
26
|
+
agent_name?: string;
|
|
27
|
+
agent_id?: string;
|
|
28
|
+
tool_input?: Record<string, unknown>;
|
|
29
|
+
transcript_path?: string;
|
|
30
|
+
permission_mode?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface HookOutput {
|
|
34
|
+
continue: boolean;
|
|
35
|
+
message?: string;
|
|
36
|
+
hookSpecificOutput?: {
|
|
37
|
+
hookEventName: string;
|
|
38
|
+
additionalContext?: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main(): Promise<void> {
|
|
43
|
+
try {
|
|
44
|
+
const input = await readStdin();
|
|
45
|
+
if (!input.trim()) {
|
|
46
|
+
console.log(JSON.stringify({ continue: true }));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data: HookInput = JSON.parse(input);
|
|
51
|
+
const toolName = data.tool_name || "";
|
|
52
|
+
const toolInput = data.tool_input || {};
|
|
53
|
+
const cwd = data.cwd || process.cwd();
|
|
54
|
+
const sessionId = data.session_id || "unknown";
|
|
55
|
+
|
|
56
|
+
const result = checkContextGuard(toolName, toolInput, cwd, sessionId);
|
|
57
|
+
|
|
58
|
+
if (result.shouldAdvise && result.advisory) {
|
|
59
|
+
debug(SOURCE, `Advising on large file read`);
|
|
60
|
+
const output: HookOutput = {
|
|
61
|
+
continue: true,
|
|
62
|
+
hookSpecificOutput: {
|
|
63
|
+
hookEventName: "PreToolUse",
|
|
64
|
+
additionalContext: result.advisory,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
console.log(JSON.stringify(output));
|
|
68
|
+
} else {
|
|
69
|
+
// Smart read hint: suggest code index for re-reads of unchanged files
|
|
70
|
+
const binary = findAideBinary({
|
|
71
|
+
cwd,
|
|
72
|
+
pluginRoot:
|
|
73
|
+
process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
|
|
74
|
+
});
|
|
75
|
+
const hintResult = checkSmartReadHint(toolName, toolInput, cwd, binary);
|
|
76
|
+
if (hintResult.shouldHint && hintResult.hint) {
|
|
77
|
+
debug(SOURCE, `Smart read hint triggered`);
|
|
78
|
+
const output: HookOutput = {
|
|
79
|
+
continue: true,
|
|
80
|
+
hookSpecificOutput: {
|
|
81
|
+
hookEventName: "PreToolUse",
|
|
82
|
+
additionalContext: hintResult.hint,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
console.log(JSON.stringify(output));
|
|
86
|
+
} else {
|
|
87
|
+
console.log(JSON.stringify({ continue: true }));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
debug(SOURCE, `Hook error: ${error}`);
|
|
92
|
+
console.log(JSON.stringify({ continue: true }));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
process.on("uncaughtException", (err) => {
|
|
97
|
+
debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
|
|
98
|
+
try {
|
|
99
|
+
console.log(JSON.stringify({ continue: true }));
|
|
100
|
+
} catch {
|
|
101
|
+
console.log('{"continue":true}');
|
|
102
|
+
}
|
|
103
|
+
process.exit(0);
|
|
104
|
+
});
|
|
105
|
+
process.on("unhandledRejection", (reason) => {
|
|
106
|
+
debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
|
|
107
|
+
try {
|
|
108
|
+
console.log(JSON.stringify({ continue: true }));
|
|
109
|
+
} catch {
|
|
110
|
+
console.log('{"continue":true}');
|
|
111
|
+
}
|
|
112
|
+
process.exit(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
main();
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Context Pruning Hook (PostToolUse)
|
|
4
|
+
*
|
|
5
|
+
* Reduces context usage by deduplicating repeated tool outputs,
|
|
6
|
+
* annotating superseded reads, and purging large error outputs.
|
|
7
|
+
*
|
|
8
|
+
* For MCP tools: uses `updatedMCPToolOutput` to replace the output.
|
|
9
|
+
* For built-in tools: uses `additionalContext` to add dedup notes.
|
|
10
|
+
*
|
|
11
|
+
* Tracker state is persisted to a temp file per session so it survives
|
|
12
|
+
* across separate hook process invocations.
|
|
13
|
+
*
|
|
14
|
+
* Core logic is in src/core/context-pruning/ for cross-platform reuse.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readStdin } from "../lib/hook-utils.js";
|
|
18
|
+
import { debug } from "../lib/logger.js";
|
|
19
|
+
import { ContextPruningTracker } from "../core/context-pruning/index.js";
|
|
20
|
+
import type { ToolRecord } from "../core/context-pruning/types.js";
|
|
21
|
+
import { tmpdir } from "os";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
24
|
+
|
|
25
|
+
const SOURCE = "context-pruning";
|
|
26
|
+
|
|
27
|
+
interface HookInput {
|
|
28
|
+
hook_event_name: string;
|
|
29
|
+
session_id: string;
|
|
30
|
+
cwd: string;
|
|
31
|
+
tool_name?: string;
|
|
32
|
+
tool_input?: Record<string, unknown>;
|
|
33
|
+
tool_output?: string;
|
|
34
|
+
mcp_server_name?: string;
|
|
35
|
+
transcript_path?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface HookOutput {
|
|
39
|
+
continue: boolean;
|
|
40
|
+
hookSpecificOutput?: {
|
|
41
|
+
hookEventName: string;
|
|
42
|
+
additionalContext?: string;
|
|
43
|
+
updatedMCPToolOutput?: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Path to the persisted tracker history for a session. */
|
|
48
|
+
function historyPath(sessionId: string): string {
|
|
49
|
+
return join(tmpdir(), `aide-context-pruning-${sessionId}.json`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Load tracker history from disk. */
|
|
53
|
+
function loadHistory(sessionId: string): {
|
|
54
|
+
history: ToolRecord[];
|
|
55
|
+
hasExplainedPruning: boolean;
|
|
56
|
+
} {
|
|
57
|
+
const path = historyPath(sessionId);
|
|
58
|
+
try {
|
|
59
|
+
if (existsSync(path)) {
|
|
60
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
61
|
+
return {
|
|
62
|
+
history: data.history || [],
|
|
63
|
+
hasExplainedPruning: data.hasExplainedPruning || false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Corrupt file — start fresh
|
|
68
|
+
}
|
|
69
|
+
return { history: [], hasExplainedPruning: false };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Save tracker history to disk. */
|
|
73
|
+
function saveHistory(
|
|
74
|
+
sessionId: string,
|
|
75
|
+
history: ToolRecord[],
|
|
76
|
+
hasExplainedPruning: boolean,
|
|
77
|
+
): void {
|
|
78
|
+
const path = historyPath(sessionId);
|
|
79
|
+
try {
|
|
80
|
+
// Keep only last 200 entries to prevent unbounded growth
|
|
81
|
+
const trimmed = history.length > 200 ? history.slice(-200) : history;
|
|
82
|
+
writeFileSync(
|
|
83
|
+
path,
|
|
84
|
+
JSON.stringify({ history: trimmed, hasExplainedPruning }),
|
|
85
|
+
"utf-8",
|
|
86
|
+
);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
debug(SOURCE, `Failed to save history: ${err}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Check if a tool is an MCP tool (aide or other MCP server). */
|
|
93
|
+
function isMCPTool(toolName: string, mcpServerName?: string): boolean {
|
|
94
|
+
if (mcpServerName) return true;
|
|
95
|
+
// Convention: MCP tools often have mcp__ prefix or are aide tools
|
|
96
|
+
if (toolName.startsWith("mcp__")) return true;
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function main(): Promise<void> {
|
|
101
|
+
try {
|
|
102
|
+
const input = await readStdin();
|
|
103
|
+
if (!input.trim()) {
|
|
104
|
+
console.log(JSON.stringify({ continue: true }));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const data: HookInput = JSON.parse(input);
|
|
109
|
+
const toolName = data.tool_name || "";
|
|
110
|
+
const toolInput = data.tool_input || {};
|
|
111
|
+
const toolOutput = data.tool_output || "";
|
|
112
|
+
const cwd = data.cwd || process.cwd();
|
|
113
|
+
const sessionId = data.session_id || "unknown";
|
|
114
|
+
|
|
115
|
+
// Skip if no tool output to prune
|
|
116
|
+
if (!toolOutput || toolOutput.length < 50) {
|
|
117
|
+
console.log(JSON.stringify({ continue: true }));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Create tracker with loaded history
|
|
122
|
+
const tracker = new ContextPruningTracker(cwd);
|
|
123
|
+
const { history: priorHistory, hasExplainedPruning } =
|
|
124
|
+
loadHistory(sessionId);
|
|
125
|
+
tracker.loadHistory(priorHistory);
|
|
126
|
+
|
|
127
|
+
// Use a synthetic callId since CC doesn't provide one
|
|
128
|
+
const callId = `cc-${sessionId}-${Date.now()}`;
|
|
129
|
+
|
|
130
|
+
// Process through pruning strategies
|
|
131
|
+
const result = tracker.process(callId, toolName, toolInput, toolOutput);
|
|
132
|
+
|
|
133
|
+
// Track whether we've explained pruning tags to the model
|
|
134
|
+
let explained = hasExplainedPruning;
|
|
135
|
+
|
|
136
|
+
// Save updated history
|
|
137
|
+
saveHistory(sessionId, tracker.getHistory(), explained);
|
|
138
|
+
|
|
139
|
+
if (result.modified) {
|
|
140
|
+
debug(
|
|
141
|
+
SOURCE,
|
|
142
|
+
`Pruned [${result.strategy}]: saved ${result.bytesSaved} bytes for ${toolName}`,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const output: HookOutput = {
|
|
146
|
+
continue: true,
|
|
147
|
+
hookSpecificOutput: {
|
|
148
|
+
hookEventName: "PostToolUse",
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (isMCPTool(toolName, data.mcp_server_name)) {
|
|
153
|
+
// For MCP tools, replace the output entirely
|
|
154
|
+
output.hookSpecificOutput!.updatedMCPToolOutput = result.output;
|
|
155
|
+
} else {
|
|
156
|
+
// For built-in tools, add context note about the dedup
|
|
157
|
+
output.hookSpecificOutput!.additionalContext = result.output.includes(
|
|
158
|
+
"[aide:dedup]",
|
|
159
|
+
)
|
|
160
|
+
? `Note: This tool output is identical to a previous call. The full content was already provided earlier.`
|
|
161
|
+
: result.output.includes("[aide:purge]")
|
|
162
|
+
? `Note: Error output was truncated. Re-run the command to see full output.`
|
|
163
|
+
: undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// On first prune, inject explanation of pruning tags via additionalContext
|
|
167
|
+
if (!explained) {
|
|
168
|
+
const pruningNotes = [
|
|
169
|
+
"<aide-context-pruning>",
|
|
170
|
+
"Tool outputs may contain these tags from aide's context optimization:",
|
|
171
|
+
"- [aide:dedup] — This output is identical to a previous call. Refer to the earlier result.",
|
|
172
|
+
"- [aide:supersede] — A prior Read of this file is now stale after a Write/Edit.",
|
|
173
|
+
"- [aide:purge] — Large error output was trimmed. Re-run the command for full output.",
|
|
174
|
+
"</aide-context-pruning>",
|
|
175
|
+
].join("\n");
|
|
176
|
+
|
|
177
|
+
const existing = output.hookSpecificOutput!.additionalContext || "";
|
|
178
|
+
output.hookSpecificOutput!.additionalContext = existing
|
|
179
|
+
? `${existing}\n\n${pruningNotes}`
|
|
180
|
+
: pruningNotes;
|
|
181
|
+
|
|
182
|
+
explained = true;
|
|
183
|
+
// Persist the flag
|
|
184
|
+
saveHistory(sessionId, tracker.getHistory(), explained);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log(JSON.stringify(output));
|
|
188
|
+
} else {
|
|
189
|
+
console.log(JSON.stringify({ continue: true }));
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
debug(SOURCE, `Hook error: ${error}`);
|
|
193
|
+
console.log(JSON.stringify({ continue: true }));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
process.on("uncaughtException", (err) => {
|
|
198
|
+
debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
|
|
199
|
+
try {
|
|
200
|
+
console.log(JSON.stringify({ continue: true }));
|
|
201
|
+
} catch {
|
|
202
|
+
console.log('{"continue":true}');
|
|
203
|
+
}
|
|
204
|
+
process.exit(0);
|
|
205
|
+
});
|
|
206
|
+
process.on("unhandledRejection", (reason) => {
|
|
207
|
+
debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
|
|
208
|
+
try {
|
|
209
|
+
console.log(JSON.stringify({ continue: true }));
|
|
210
|
+
} catch {
|
|
211
|
+
console.log('{"continue":true}');
|
|
212
|
+
}
|
|
213
|
+
process.exit(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
main();
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* HUD Updater Hook (PostToolUse)
|
|
4
|
+
*
|
|
5
|
+
* Updates the terminal status line with current aide state.
|
|
6
|
+
* Shows: mode, model tier, active agents, context usage
|
|
7
|
+
*
|
|
8
|
+
* Output is written to .aide/state/hud.txt for the terminal to display.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { statSync } from "fs";
|
|
12
|
+
import { resolve, isAbsolute } from "path";
|
|
13
|
+
import { Logger, debug } from "../lib/logger.js";
|
|
14
|
+
import { readStdin } from "../lib/hook-utils.js";
|
|
15
|
+
|
|
16
|
+
const SOURCE = "hud-updater";
|
|
17
|
+
import { findAideBinary } from "../core/aide-client.js";
|
|
18
|
+
import { updateToolStats } from "../core/tool-tracking.js";
|
|
19
|
+
import { storePartialMemory } from "../core/partial-memory.js";
|
|
20
|
+
import { recordFileRead, recordTokenEvent, estimateTokensFromSize } from "../core/read-tracking.js"; // estimateTokensFromSize used for read events
|
|
21
|
+
import {
|
|
22
|
+
getAgentStates,
|
|
23
|
+
loadHudConfig,
|
|
24
|
+
getSessionState,
|
|
25
|
+
formatHud,
|
|
26
|
+
writeHudOutput,
|
|
27
|
+
} from "../lib/hud.js";
|
|
28
|
+
|
|
29
|
+
interface HookInput {
|
|
30
|
+
hook_event_name: string;
|
|
31
|
+
session_id: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
tool_name?: string;
|
|
34
|
+
agent_id?: string;
|
|
35
|
+
tool_input?: {
|
|
36
|
+
file_path?: string;
|
|
37
|
+
command?: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
};
|
|
41
|
+
tool_result?: {
|
|
42
|
+
success: boolean;
|
|
43
|
+
duration?: number;
|
|
44
|
+
};
|
|
45
|
+
transcript_path?: string;
|
|
46
|
+
permission_mode?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function main(): Promise<void> {
|
|
50
|
+
let log: Logger | null = null;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const input = await readStdin();
|
|
54
|
+
if (!input.trim()) {
|
|
55
|
+
console.log(JSON.stringify({ continue: true }));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const data: HookInput = JSON.parse(input);
|
|
60
|
+
const cwd = data.cwd || process.cwd();
|
|
61
|
+
const toolName = data.tool_name || "";
|
|
62
|
+
const agentId = data.agent_id || data.session_id;
|
|
63
|
+
const sessionId = data.session_id;
|
|
64
|
+
|
|
65
|
+
// Initialize logger
|
|
66
|
+
log = new Logger("hud-updater", cwd);
|
|
67
|
+
log.start("total");
|
|
68
|
+
log.debug(
|
|
69
|
+
`Processing PostToolUse for tool: ${toolName}, agent: ${agentId}, session: ${sessionId}`,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Update session state (per-agent tracking) — delegates to core
|
|
73
|
+
if (toolName) {
|
|
74
|
+
log.start("updateSessionState");
|
|
75
|
+
const binary = findAideBinary({
|
|
76
|
+
cwd,
|
|
77
|
+
pluginRoot:
|
|
78
|
+
process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
|
|
79
|
+
});
|
|
80
|
+
if (binary) {
|
|
81
|
+
updateToolStats(binary, cwd, toolName, agentId);
|
|
82
|
+
|
|
83
|
+
// Write a partial memory for significant tool uses
|
|
84
|
+
storePartialMemory(binary, cwd, {
|
|
85
|
+
toolName,
|
|
86
|
+
sessionId,
|
|
87
|
+
filePath: data.tool_input?.file_path,
|
|
88
|
+
command: data.tool_input?.command,
|
|
89
|
+
description: data.tool_input?.description,
|
|
90
|
+
success: data.tool_result?.success,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Record file reads for smart-read-hint feature
|
|
94
|
+
if (
|
|
95
|
+
toolName === "Read" &&
|
|
96
|
+
data.tool_result?.success &&
|
|
97
|
+
data.tool_input?.file_path
|
|
98
|
+
) {
|
|
99
|
+
const fp = data.tool_input.file_path as string;
|
|
100
|
+
recordFileRead(binary, cwd, fp);
|
|
101
|
+
|
|
102
|
+
// Record token event for the read (estimate from file size)
|
|
103
|
+
try {
|
|
104
|
+
const abs = isAbsolute(fp) ? fp : resolve(cwd, fp);
|
|
105
|
+
const stat = statSync(abs);
|
|
106
|
+
const tokens = estimateTokensFromSize(stat.size);
|
|
107
|
+
recordTokenEvent(binary, cwd, "read", "Read", fp, tokens);
|
|
108
|
+
} catch {
|
|
109
|
+
// stat failed — skip token recording
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
}
|
|
114
|
+
log.end("updateSessionState");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Load config and get state
|
|
118
|
+
log.start("loadHudConfig");
|
|
119
|
+
const config = loadHudConfig(cwd);
|
|
120
|
+
log.end("loadHudConfig");
|
|
121
|
+
|
|
122
|
+
log.start("getSessionState");
|
|
123
|
+
const state = getSessionState(cwd);
|
|
124
|
+
log.end("getSessionState", state);
|
|
125
|
+
|
|
126
|
+
log.start("getAgentStates");
|
|
127
|
+
const allAgents = getAgentStates(cwd);
|
|
128
|
+
// Filter to ONLY show agents from the current session
|
|
129
|
+
const agents = sessionId
|
|
130
|
+
? allAgents.filter((a) => a.session === sessionId)
|
|
131
|
+
: [];
|
|
132
|
+
log.end("getAgentStates", {
|
|
133
|
+
total: allAgents.length,
|
|
134
|
+
filtered: agents.length,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Format and write HUD (includes per-agent lines)
|
|
138
|
+
log.start("formatHud");
|
|
139
|
+
const hudOutput = formatHud(config, state, agents, cwd);
|
|
140
|
+
log.end("formatHud");
|
|
141
|
+
log.debug(`HUD output: ${hudOutput}`);
|
|
142
|
+
|
|
143
|
+
log.start("writeHudOutput");
|
|
144
|
+
writeHudOutput(cwd, hudOutput);
|
|
145
|
+
log.end("writeHudOutput");
|
|
146
|
+
|
|
147
|
+
log.end("total");
|
|
148
|
+
log.flush();
|
|
149
|
+
|
|
150
|
+
// Always continue
|
|
151
|
+
console.log(JSON.stringify({ continue: true }));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (log) {
|
|
154
|
+
log.error("HUD update failed", error);
|
|
155
|
+
log.flush();
|
|
156
|
+
}
|
|
157
|
+
console.log(JSON.stringify({ continue: true }));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
process.on("uncaughtException", (err) => {
|
|
162
|
+
debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
|
|
163
|
+
try {
|
|
164
|
+
console.log(JSON.stringify({ continue: true }));
|
|
165
|
+
} catch {
|
|
166
|
+
console.log('{"continue":true}');
|
|
167
|
+
}
|
|
168
|
+
process.exit(0);
|
|
169
|
+
});
|
|
170
|
+
process.on("unhandledRejection", (reason) => {
|
|
171
|
+
debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
|
|
172
|
+
try {
|
|
173
|
+
console.log(JSON.stringify({ continue: true }));
|
|
174
|
+
} catch {
|
|
175
|
+
console.log('{"continue":true}');
|
|
176
|
+
}
|
|
177
|
+
process.exit(0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
main();
|