@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,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();
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Permission Handler Hook (PermissionRequest)
4
+ *
5
+ * OPT-IN: This hook is NOT registered in plugin.json by default.
6
+ * To enable, add a PermissionRequest entry to .claude-plugin/plugin.json.
7
+ * Not available in OpenCode (no equivalent event).
8
+ *
9
+ * Validates Bash commands before permission prompts are shown.
10
+ * Can auto-approve safe commands or block dangerous ones.
11
+ *
12
+ * PermissionRequest data from Claude Code:
13
+ * - tool_name: "Bash"
14
+ * - tool_input: { command: "...", ... }
15
+ * - cwd, session_id
16
+ *
17
+ * Returns:
18
+ * - { allow: true } to auto-approve
19
+ * - { allow: false, reason: "..." } to block
20
+ * - { continue: true } to show normal permission prompt
21
+ */
22
+
23
+ import { readStdin, findAideBinary, runAide } from "../lib/hook-utils.js";
24
+ import { sanitizeForLog } from "../core/aide-client.js";
25
+ import { debug } from "../lib/logger.js";
26
+
27
+ const SOURCE = "permission-handler";
28
+
29
+ interface PermissionRequestInput {
30
+ event: "PermissionRequest";
31
+ tool_name: string;
32
+ tool_input: {
33
+ command?: string;
34
+ [key: string]: unknown;
35
+ };
36
+ cwd: string;
37
+ session_id: string;
38
+ }
39
+
40
+ interface PermissionResponse {
41
+ allow?: boolean;
42
+ reason?: string;
43
+ continue?: boolean;
44
+ }
45
+
46
+ // Commands that are always safe to auto-approve
47
+ const SAFE_COMMANDS = [
48
+ /^ls\b/,
49
+ /^pwd$/,
50
+ /^echo\b/,
51
+ /^cat\b/,
52
+ /^head\b/,
53
+ /^tail\b/,
54
+ /^wc\b/,
55
+ /^grep\b/,
56
+ /^find\b/,
57
+ /^which\b/,
58
+ /^git\s+(status|log|diff|branch|show)\b/,
59
+ /^git\s+stash\s+list\b/,
60
+ /^npm\s+(list|ls|outdated|view)\b/,
61
+ /^yarn\s+(list|info)\b/,
62
+ /^pnpm\s+(list|outdated)\b/,
63
+ /^node\s+--version$/,
64
+ /^npm\s+--version$/,
65
+ /^python\s+--version$/,
66
+ /^go\s+version$/,
67
+ /^cargo\s+--version$/,
68
+ ];
69
+
70
+ // Commands that should be blocked without prompting
71
+ const BLOCKED_COMMANDS = [
72
+ /rm\s+-rf\s+[/~]/, // rm -rf / or ~
73
+ /rm\s+-rf\s+\*/, // rm -rf *
74
+ /dd\s+.*of=\/dev\//, // dd to device
75
+ /mkfs\./, // format filesystem
76
+ /:\(\)\{:\|:&\};:/, // fork bomb
77
+ />\s*\/dev\/sd[a-z]/, // write to disk device
78
+ /curl.*\|\s*(ba)?sh/, // curl pipe to shell
79
+ /wget.*\|\s*(ba)?sh/, // wget pipe to shell
80
+ ];
81
+
82
+ /**
83
+ * Log permission decision to aide-memory
84
+ */
85
+ function logPermission(cwd: string, command: string, decision: string): void {
86
+ if (!findAideBinary(cwd)) return;
87
+
88
+ const safeCommand = sanitizeForLog(command).slice(0, 200);
89
+ runAide(cwd, [
90
+ "message",
91
+ "send",
92
+ `${decision}: ${safeCommand}`,
93
+ "--from=system",
94
+ "--type=permission",
95
+ ]);
96
+ }
97
+
98
+ /**
99
+ * Check if command matches any patterns
100
+ */
101
+ function matchesPatterns(command: string, patterns: RegExp[]): boolean {
102
+ return patterns.some((pattern) => pattern.test(command));
103
+ }
104
+
105
+ async function main(): Promise<void> {
106
+ try {
107
+ const input = await readStdin();
108
+ if (!input.trim()) {
109
+ console.log(JSON.stringify({ continue: true }));
110
+ return;
111
+ }
112
+
113
+ const data: PermissionRequestInput = JSON.parse(input);
114
+
115
+ // Only handle Bash permissions
116
+ if (data.tool_name !== "Bash" || !data.tool_input?.command) {
117
+ console.log(JSON.stringify({ continue: true }));
118
+ return;
119
+ }
120
+
121
+ const command = data.tool_input.command;
122
+ const cwd = data.cwd || process.cwd();
123
+
124
+ // Check for blocked commands first
125
+ if (matchesPatterns(command, BLOCKED_COMMANDS)) {
126
+ logPermission(cwd, command, "BLOCKED");
127
+ const response: PermissionResponse = {
128
+ allow: false,
129
+ reason:
130
+ "This command has been blocked for safety. It matches a dangerous pattern.",
131
+ };
132
+ console.log(JSON.stringify(response));
133
+ return;
134
+ }
135
+
136
+ // Check for safe commands to auto-approve
137
+ if (matchesPatterns(command, SAFE_COMMANDS)) {
138
+ logPermission(cwd, command, "AUTO-APPROVED");
139
+ const response: PermissionResponse = {
140
+ allow: true,
141
+ };
142
+ console.log(JSON.stringify(response));
143
+ return;
144
+ }
145
+
146
+ // Default: show normal permission prompt
147
+ console.log(JSON.stringify({ continue: true }));
148
+ } catch (error) {
149
+ debug(SOURCE, `Hook error: ${error}`);
150
+ console.log(JSON.stringify({ continue: true }));
151
+ }
152
+ }
153
+
154
+ process.on("uncaughtException", (err) => {
155
+ debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
156
+ try {
157
+ console.log(JSON.stringify({ continue: true }));
158
+ } catch {
159
+ console.log('{"continue":true}');
160
+ }
161
+ process.exit(0);
162
+ });
163
+ process.on("unhandledRejection", (reason) => {
164
+ debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
165
+ try {
166
+ console.log(JSON.stringify({ continue: true }));
167
+ } catch {
168
+ console.log('{"continue":true}');
169
+ }
170
+ process.exit(0);
171
+ });
172
+
173
+ main();
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Persistence Hook (Stop)
4
+ *
5
+ * Prevents Claude from stopping when work is incomplete.
6
+ * Checks for active modes (autopilot) via aide-memory state.
7
+ */
8
+
9
+ import { readStdin } from "../lib/hook-utils.js";
10
+ import { findAideBinary } from "../core/aide-client.js";
11
+ import { checkPersistence } from "../core/persistence-logic.js";
12
+ import { debug } from "../lib/logger.js";
13
+
14
+ const SOURCE = "persistence";
15
+
16
+ interface HookInput {
17
+ hook_event_name: string;
18
+ session_id: string;
19
+ cwd: string;
20
+ stop_hook_active?: boolean;
21
+ transcript_path?: string;
22
+ permission_mode?: string;
23
+ }
24
+
25
+ interface HookOutput {
26
+ decision?: "block";
27
+ reason?: string;
28
+ }
29
+
30
+ async function main(): Promise<void> {
31
+ try {
32
+ const input = await readStdin();
33
+ if (!input.trim()) {
34
+ console.log(JSON.stringify({}));
35
+ return;
36
+ }
37
+
38
+ const data: HookInput = JSON.parse(input);
39
+ const cwd = data.cwd || process.cwd();
40
+
41
+ if (data.stop_hook_active) {
42
+ console.log(JSON.stringify({}));
43
+ return;
44
+ }
45
+
46
+ const binary = findAideBinary({
47
+ cwd,
48
+ pluginRoot:
49
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
50
+ });
51
+ if (!binary) {
52
+ console.log(JSON.stringify({}));
53
+ return;
54
+ }
55
+
56
+ const result = checkPersistence(binary, cwd, data.session_id);
57
+ if (!result) {
58
+ console.log(JSON.stringify({}));
59
+ return;
60
+ }
61
+
62
+ const output: HookOutput = {
63
+ decision: "block",
64
+ reason: result.reason,
65
+ };
66
+
67
+ console.log(JSON.stringify(output));
68
+ } catch (err) {
69
+ debug(SOURCE, `Hook error: ${err}`);
70
+ console.log(JSON.stringify({}));
71
+ }
72
+ }
73
+
74
+ process.on("uncaughtException", (err) => {
75
+ debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
76
+ try {
77
+ console.log(JSON.stringify({}));
78
+ } catch {
79
+ console.log("{}");
80
+ }
81
+ process.exit(0);
82
+ });
83
+ process.on("unhandledRejection", (reason) => {
84
+ debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
85
+ try {
86
+ console.log(JSON.stringify({}));
87
+ } catch {
88
+ console.log("{}");
89
+ }
90
+ process.exit(0);
91
+ });
92
+
93
+ main();
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pre-Compact Hook (PreCompact)
4
+ *
5
+ * Called before Claude Code compacts/summarizes the conversation.
6
+ * Preserves important context in aide-memory before summarization.
7
+ *
8
+ * PreCompact data from Claude Code:
9
+ * - session_id, cwd
10
+ * - summary_prompt (the prompt used for summarization)
11
+ */
12
+
13
+ import { readStdin } from "../lib/hook-utils.js";
14
+ import { findAideBinary } from "../core/aide-client.js";
15
+ import { saveStateSnapshot as coreSaveStateSnapshot } from "../core/pre-compact-logic.js";
16
+ import {
17
+ buildSessionSummaryFromState,
18
+ getSessionCommits,
19
+ storeSessionSummary,
20
+ } from "../core/session-summary-logic.js";
21
+ import {
22
+ gatherPartials,
23
+ buildSummaryFromPartials,
24
+ } from "../core/partial-memory.js";
25
+ import { debug } from "../lib/logger.js";
26
+
27
+ const SOURCE = "pre-compact";
28
+
29
+ interface PreCompactInput {
30
+ event: "PreCompact";
31
+ session_id: string;
32
+ cwd: string;
33
+ summary_prompt?: string;
34
+ }
35
+
36
+ async function main(): Promise<void> {
37
+ try {
38
+ const input = await readStdin();
39
+ if (!input.trim()) {
40
+ console.log(JSON.stringify({ continue: true }));
41
+ return;
42
+ }
43
+
44
+ const data: PreCompactInput = JSON.parse(input);
45
+ const cwd = data.cwd || process.cwd();
46
+ const sessionId = data.session_id || "unknown";
47
+
48
+ // Save state snapshot before compaction — delegates to core
49
+ const binary = findAideBinary({
50
+ cwd,
51
+ pluginRoot:
52
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
53
+ });
54
+ if (binary) {
55
+ coreSaveStateSnapshot(binary, cwd, sessionId);
56
+
57
+ // Persist a session summary as a memory before context is compacted.
58
+ // This ensures the work-so-far is recoverable after compaction.
59
+ // Uses partials (if available) for a richer summary, falling back to git-only.
60
+ try {
61
+ const partials = gatherPartials(binary, cwd, sessionId);
62
+ let summary: string | null = null;
63
+
64
+ if (partials.length > 0) {
65
+ // Build from partials + git data
66
+ const commits = getSessionCommits(cwd);
67
+ summary = buildSummaryFromPartials(partials, commits, []);
68
+ debug(
69
+ SOURCE,
70
+ `Built pre-compact summary from ${partials.length} partials`,
71
+ );
72
+ }
73
+
74
+ // Fall back to state-only summary if no partials
75
+ if (!summary) {
76
+ summary = buildSessionSummaryFromState(cwd);
77
+ }
78
+
79
+ if (summary) {
80
+ // Tag as partial so the session-end summary supersedes it
81
+ const tags = `partial,session-summary,session:${sessionId}`;
82
+ (await import("child_process")).execFileSync(
83
+ binary,
84
+ ["memory", "add", "--category=session", `--tags=${tags}`, summary],
85
+ { cwd, stdio: "pipe", timeout: 5000 },
86
+ );
87
+ debug(
88
+ SOURCE,
89
+ `Saved pre-compaction partial session summary for ${sessionId.slice(0, 8)}`,
90
+ );
91
+ }
92
+ } catch (err) {
93
+ debug(
94
+ SOURCE,
95
+ `Failed to save pre-compaction summary (non-fatal): ${err}`,
96
+ );
97
+ }
98
+ }
99
+
100
+ // Always allow compaction to continue
101
+ console.log(JSON.stringify({ continue: true }));
102
+ } catch (error) {
103
+ debug(SOURCE, `Hook error: ${error}`);
104
+ console.log(JSON.stringify({ continue: true }));
105
+ }
106
+ }
107
+
108
+ process.on("uncaughtException", (err) => {
109
+ debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
110
+ try {
111
+ console.log(JSON.stringify({ continue: true }));
112
+ } catch {
113
+ console.log('{"continue":true}');
114
+ }
115
+ process.exit(0);
116
+ });
117
+ process.on("unhandledRejection", (reason) => {
118
+ debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
119
+ try {
120
+ console.log(JSON.stringify({ continue: true }));
121
+ } catch {
122
+ console.log('{"continue":true}');
123
+ }
124
+ process.exit(0);
125
+ });
126
+
127
+ main();
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pre-Tool Enforcer Hook (PreToolUse)
4
+ *
5
+ * Enforces tool access rules:
6
+ * - Read-only agents cannot use write tools
7
+ * - Injects contextual reminders
8
+ * - Tracks active state
9
+ *
10
+ * Core logic is in src/core/tool-enforcement.ts for cross-platform reuse.
11
+ */
12
+
13
+ import { readStdin } from "../lib/hook-utils.js";
14
+ import { debug } from "../lib/logger.js";
15
+ import { evaluateToolUse } from "../core/tool-enforcement.js";
16
+ import { findAideBinary, getState } from "../core/aide-client.js";
17
+
18
+ const SOURCE = "pre-tool-enforcer";
19
+
20
+ interface HookInput {
21
+ hook_event_name: string;
22
+ session_id: string;
23
+ cwd: string;
24
+ tool_name?: string;
25
+ agent_name?: string;
26
+ agent_id?: string;
27
+ transcript_path?: string;
28
+ permission_mode?: string;
29
+ }
30
+
31
+ interface HookOutput {
32
+ continue: boolean;
33
+ message?: string;
34
+ hookSpecificOutput?: {
35
+ hookEventName: string;
36
+ additionalContext?: string;
37
+ };
38
+ }
39
+
40
+ async function main(): Promise<void> {
41
+ try {
42
+ const input = await readStdin();
43
+ if (!input.trim()) {
44
+ console.log(JSON.stringify({ continue: true }));
45
+ return;
46
+ }
47
+
48
+ const data: HookInput = JSON.parse(input);
49
+ const toolName = data.tool_name || "";
50
+ const agentName = data.agent_name || "";
51
+ const cwd = data.cwd || process.cwd();
52
+
53
+ // Resolve active mode from aide binary (source of truth: BBolt store)
54
+ let activeMode: string | null = null;
55
+ try {
56
+ const binary = findAideBinary({
57
+ cwd,
58
+ pluginRoot:
59
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
60
+ });
61
+ if (binary) {
62
+ activeMode = getState(binary, cwd, "mode");
63
+ }
64
+ } catch (err) {
65
+ debug(SOURCE, `Failed to resolve active mode (non-fatal): ${err}`);
66
+ }
67
+
68
+ const result = evaluateToolUse(
69
+ toolName,
70
+ agentName || undefined,
71
+ activeMode,
72
+ );
73
+
74
+ if (!result.allowed) {
75
+ const output: HookOutput = {
76
+ continue: false,
77
+ message: result.denyMessage,
78
+ };
79
+ console.log(JSON.stringify(output));
80
+ return;
81
+ }
82
+
83
+ if (result.reminder) {
84
+ const output: HookOutput = {
85
+ continue: true,
86
+ hookSpecificOutput: {
87
+ hookEventName: "PreToolUse",
88
+ additionalContext: result.reminder,
89
+ },
90
+ };
91
+ console.log(JSON.stringify(output));
92
+ } else {
93
+ console.log(JSON.stringify({ continue: true }));
94
+ }
95
+ } catch (error) {
96
+ debug(SOURCE, `Hook error: ${error}`);
97
+ console.log(JSON.stringify({ continue: true }));
98
+ }
99
+ }
100
+
101
+ process.on("uncaughtException", (err) => {
102
+ debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
103
+ try {
104
+ console.log(JSON.stringify({ continue: true }));
105
+ } catch {
106
+ console.log('{"continue":true}');
107
+ }
108
+ process.exit(0);
109
+ });
110
+ process.on("unhandledRejection", (reason) => {
111
+ debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
112
+ try {
113
+ console.log(JSON.stringify({ continue: true }));
114
+ } catch {
115
+ console.log('{"continue":true}');
116
+ }
117
+ process.exit(0);
118
+ });
119
+
120
+ main();