@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.
- 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 +139 -17
- 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/aide-downloader.ts +58 -21
- package/src/lib/hook-utils.ts +53 -1
|
@@ -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();
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Session End Hook (SessionEnd)
|
|
4
|
+
*
|
|
5
|
+
* Delegates cleanup to `aide session end` — single binary invocation.
|
|
6
|
+
*
|
|
7
|
+
* When invoked from Codex CLI's Stop hook (which has no separate SessionEnd),
|
|
8
|
+
* checks whether autopilot mode is active and skips cleanup if so — the
|
|
9
|
+
* session is continuing, not ending.
|
|
10
|
+
*
|
|
11
|
+
* CONSTRAINTS:
|
|
12
|
+
* - No ES module imports (hoisted resolution adds ~3s)
|
|
13
|
+
* - Output {"continue": true} before require() resolves
|
|
14
|
+
* - Cleanup via detached spawn to avoid blocking exit
|
|
15
|
+
* - MUST complete well within Claude Code's ~1.5s hook timeout
|
|
16
|
+
*
|
|
17
|
+
* STDIN:
|
|
18
|
+
* Claude Code pipes JSON via stdin and closes the pipe. The documented
|
|
19
|
+
* payload includes session_id, but in practice Claude Code often sends
|
|
20
|
+
* just `{}`. Bun's `for await` async iterator on stdin hangs even when
|
|
21
|
+
* the pipe is closed — use synchronous readFileSync(0) instead.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const T0 = performance.now();
|
|
25
|
+
|
|
26
|
+
// Output continue IMMEDIATELY — before require(), before anything.
|
|
27
|
+
console.log(JSON.stringify({ continue: true }));
|
|
28
|
+
|
|
29
|
+
const { spawn, execFileSync } = require("child_process") as typeof import("child_process");
|
|
30
|
+
const { existsSync, realpathSync, appendFileSync, mkdirSync, readFileSync } = require("fs") as typeof import("fs");
|
|
31
|
+
const { join } = require("path") as typeof import("path");
|
|
32
|
+
const whichSync = (require("which") as typeof import("which")).sync;
|
|
33
|
+
|
|
34
|
+
const SESSION_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
35
|
+
|
|
36
|
+
/** Elapsed ms since T0. */
|
|
37
|
+
function ms(): string {
|
|
38
|
+
return `+${(performance.now() - T0).toFixed(0)}ms`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Always log to .aide/_logs/session-end.log (NOT gated on AIDE_DEBUG).
|
|
43
|
+
* This hook has historically been invisible when it fails — always log.
|
|
44
|
+
*/
|
|
45
|
+
function log(cwd: string, msg: string): void {
|
|
46
|
+
try {
|
|
47
|
+
const logDir = join(cwd, ".aide", "_logs");
|
|
48
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
49
|
+
const line = `[${new Date().toISOString()}] [session-end] ${ms()} ${msg}\n`;
|
|
50
|
+
appendFileSync(join(logDir, "session-end.log"), line);
|
|
51
|
+
} catch { /* best effort */ }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Find aide binary — inline, no external module imports. */
|
|
55
|
+
function findBinary(cwd?: string): string | null {
|
|
56
|
+
const pluginRoot = process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT;
|
|
57
|
+
let resolvedRoot = pluginRoot;
|
|
58
|
+
if (resolvedRoot) {
|
|
59
|
+
try { resolvedRoot = realpathSync(resolvedRoot); } catch { /* symlink may not resolve */ }
|
|
60
|
+
const p = join(resolvedRoot, "bin", "aide");
|
|
61
|
+
if (existsSync(p)) return p;
|
|
62
|
+
}
|
|
63
|
+
if (cwd) {
|
|
64
|
+
const p = join(cwd, ".aide", "bin", "aide");
|
|
65
|
+
if (existsSync(p)) return p;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return whichSync("aide", { nothrow: true });
|
|
69
|
+
} catch { return null; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function main(): void {
|
|
73
|
+
const cwd = process.cwd();
|
|
74
|
+
log(cwd, "started");
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Read stdin synchronously — avoids bun's broken async iterator.
|
|
78
|
+
// Claude Code closes the pipe so this returns immediately.
|
|
79
|
+
let input = "";
|
|
80
|
+
try {
|
|
81
|
+
input = readFileSync(0, "utf-8");
|
|
82
|
+
} catch {
|
|
83
|
+
log(cwd, "stdin not readable (not a pipe?)");
|
|
84
|
+
}
|
|
85
|
+
log(cwd, `stdin (${input.length} bytes): ${input.trim().slice(0, 200)}`);
|
|
86
|
+
|
|
87
|
+
// Parse session_id from stdin JSON
|
|
88
|
+
let sessionId = "";
|
|
89
|
+
if (input.trim()) {
|
|
90
|
+
try {
|
|
91
|
+
const data = JSON.parse(input);
|
|
92
|
+
sessionId = data.session_id || "";
|
|
93
|
+
} catch {
|
|
94
|
+
log(cwd, "stdin is not valid JSON");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!sessionId) {
|
|
99
|
+
log(cwd, "no session_id in payload, skipping cleanup");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!SESSION_ID_RE.test(sessionId)) {
|
|
104
|
+
log(cwd, `invalid session_id: ${sessionId}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const binary = findBinary(cwd);
|
|
109
|
+
log(cwd, `binary: ${binary}`);
|
|
110
|
+
if (!binary) {
|
|
111
|
+
log(cwd, "no binary found, skipping cleanup");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Mode guard: skip cleanup if autopilot is active (session is continuing,
|
|
116
|
+
// not ending). This matters when Codex CLI invokes session-end from Stop
|
|
117
|
+
// hook since there's no separate SessionEnd event.
|
|
118
|
+
try {
|
|
119
|
+
const mode = execFileSync(binary, ["state", "get", "mode"], {
|
|
120
|
+
cwd, timeout: 500, encoding: "utf-8",
|
|
121
|
+
}).trim();
|
|
122
|
+
if (mode === "autopilot") {
|
|
123
|
+
log(cwd, "autopilot mode active, skipping cleanup (session continuing)");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Binary may not support 'state get' or state may not exist — proceed
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
log(cwd, `spawning cleanup: session end --session=${sessionId}`);
|
|
131
|
+
const child = spawn(binary, ["session", "end", `--session=${sessionId}`], {
|
|
132
|
+
cwd, detached: true, stdio: "ignore",
|
|
133
|
+
});
|
|
134
|
+
child.unref();
|
|
135
|
+
log(cwd, "cleanup spawned (detached)");
|
|
136
|
+
} catch (error) {
|
|
137
|
+
log(cwd, `error: ${error}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
log(cwd, `done ${ms()}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// On SIGINT/SIGTERM, exit cleanly (continue already output).
|
|
144
|
+
process.on("SIGINT", () => process.exit(0));
|
|
145
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
146
|
+
|
|
147
|
+
main();
|
|
148
|
+
process.exit(0);
|