@jmylchreest/aide-plugin 0.0.56 → 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.
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent Cleanup Hook (Stop)
4
+ *
5
+ * Cleans up agent-specific state when an agent stops.
6
+ * This prevents stale state from polluting future agents with the same ID.
7
+ *
8
+ * Runs after persistence hook to clean up when agent is allowed to stop.
9
+ */
10
+
11
+ import { existsSync, appendFileSync } from "fs";
12
+ import { join } from "path";
13
+ import { readStdin } from "../lib/hook-utils.js";
14
+ import { findAideBinary } from "../core/aide-client.js";
15
+ import { cleanupAgent } from "../core/cleanup.js";
16
+ import { debug } from "../lib/logger.js";
17
+
18
+ const SOURCE = "agent-cleanup";
19
+
20
+ interface HookInput {
21
+ hook_event_name: string;
22
+ session_id: string;
23
+ cwd: string;
24
+ agent_id?: string;
25
+ transcript_path?: string;
26
+ permission_mode?: string;
27
+ }
28
+
29
+ async function main(): Promise<void> {
30
+ try {
31
+ const input = await readStdin();
32
+ if (!input.trim()) {
33
+ console.log(JSON.stringify({ continue: true }));
34
+ return;
35
+ }
36
+
37
+ const data: HookInput = JSON.parse(input);
38
+ const cwd = data.cwd || process.cwd();
39
+ const agentId = data.agent_id || data.session_id;
40
+
41
+ // Clean up agent-specific state — delegates to core
42
+ if (agentId) {
43
+ const binary = findAideBinary({
44
+ cwd,
45
+ pluginRoot:
46
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
47
+ });
48
+ if (binary) {
49
+ const cleared = cleanupAgent(binary, cwd, agentId);
50
+ if (cleared) {
51
+ const logDir = join(cwd, ".aide", "_logs");
52
+ if (existsSync(logDir)) {
53
+ const logPath = join(logDir, "agent-cleanup.log");
54
+ const timestamp = new Date().toISOString();
55
+ appendFileSync(
56
+ logPath,
57
+ `${timestamp} Cleaned up state for agent: ${agentId}\n`,
58
+ );
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ // Always continue - cleanup is best-effort
65
+ console.log(JSON.stringify({ continue: true }));
66
+ } catch (error) {
67
+ debug(SOURCE, `Hook error: ${error}`);
68
+ console.log(JSON.stringify({ continue: true }));
69
+ }
70
+ }
71
+
72
+ process.on("uncaughtException", (err) => {
73
+ debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
74
+ try {
75
+ console.log(JSON.stringify({ continue: true }));
76
+ } catch {
77
+ console.log('{"continue":true}');
78
+ }
79
+ process.exit(0);
80
+ });
81
+ process.on("unhandledRejection", (reason) => {
82
+ debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
83
+ try {
84
+ console.log(JSON.stringify({ continue: true }));
85
+ } catch {
86
+ console.log('{"continue":true}');
87
+ }
88
+ process.exit(0);
89
+ });
90
+
91
+ main();
@@ -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();