@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.
- 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 +208 -8
- 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,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();
|