@jmylchreest/aide-plugin 0.0.57 → 0.0.59

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.
@@ -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();