@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,488 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Session Start Hook (SessionStart)
|
|
4
|
+
*
|
|
5
|
+
* Initializes aide state and configuration on session start.
|
|
6
|
+
* - Creates .aide directories if needed
|
|
7
|
+
* - Loads config files
|
|
8
|
+
* - Initializes HUD state
|
|
9
|
+
* - Runs `aide session init` (single binary call for state reset, cleanup, memory fetch)
|
|
10
|
+
*
|
|
11
|
+
* Debug logging: Set AIDE_DEBUG=1 to enable startup tracing
|
|
12
|
+
* Logs written to: .aide/_logs/startup.log
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
existsSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
copyFileSync,
|
|
21
|
+
chmodSync,
|
|
22
|
+
} from "fs";
|
|
23
|
+
import { join, dirname } from "path";
|
|
24
|
+
import { fileURLToPath } from "url";
|
|
25
|
+
import { homedir } from "os";
|
|
26
|
+
import { Logger, debug, setDebugCwd } from "../lib/logger.js";
|
|
27
|
+
import { readStdin, detectPlatform } from "../lib/hook-utils.js";
|
|
28
|
+
import { findAideBinary, ensureAideBinary } from "../lib/aide-downloader.js";
|
|
29
|
+
import {
|
|
30
|
+
ensureDirectories as coreEnsureDirectories,
|
|
31
|
+
loadConfig as coreLoadConfig,
|
|
32
|
+
initializeSession as coreInitializeSession,
|
|
33
|
+
cleanupStaleStateFiles as coreCleanupStaleStateFiles,
|
|
34
|
+
resetHudState as coreResetHudState,
|
|
35
|
+
getProjectName,
|
|
36
|
+
runSessionInit as coreRunSessionInit,
|
|
37
|
+
buildWelcomeContext as coreBuildWelcomeContext,
|
|
38
|
+
formatTimeAgo,
|
|
39
|
+
} from "../core/session-init.js";
|
|
40
|
+
import { syncMcpServers } from "../core/mcp-sync.js";
|
|
41
|
+
import type {
|
|
42
|
+
AideConfig,
|
|
43
|
+
SessionState,
|
|
44
|
+
SessionInitResult,
|
|
45
|
+
MemoryInjection,
|
|
46
|
+
StartupNotices,
|
|
47
|
+
} from "../core/types.js";
|
|
48
|
+
|
|
49
|
+
const SOURCE = "session-start";
|
|
50
|
+
debug(SOURCE, `Hook started (AIDE_DEBUG=${process.env.AIDE_DEBUG || "unset"})`);
|
|
51
|
+
|
|
52
|
+
interface HookInput {
|
|
53
|
+
hook_event_name: string;
|
|
54
|
+
session_id: string;
|
|
55
|
+
cwd: string;
|
|
56
|
+
transcript_path?: string;
|
|
57
|
+
permission_mode?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface HookOutput {
|
|
61
|
+
continue: boolean;
|
|
62
|
+
hookSpecificOutput?: {
|
|
63
|
+
hookEventName: string;
|
|
64
|
+
additionalContext?: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface BinaryCheckResult {
|
|
69
|
+
binary: string | null;
|
|
70
|
+
error: string | null;
|
|
71
|
+
warning: string | null;
|
|
72
|
+
downloaded: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check for aide binary with logging, version checking, and auto-download
|
|
77
|
+
*/
|
|
78
|
+
async function checkAideBinary(
|
|
79
|
+
cwd: string,
|
|
80
|
+
log: Logger,
|
|
81
|
+
): Promise<BinaryCheckResult> {
|
|
82
|
+
log.start("checkAideBinary");
|
|
83
|
+
|
|
84
|
+
const result = await ensureAideBinary(cwd);
|
|
85
|
+
|
|
86
|
+
if (result.binary) {
|
|
87
|
+
if (result.downloaded) {
|
|
88
|
+
log.info(`aide binary downloaded successfully to ${result.binary}`);
|
|
89
|
+
}
|
|
90
|
+
if (result.warning) {
|
|
91
|
+
log.info("aide update available");
|
|
92
|
+
}
|
|
93
|
+
log.end("checkAideBinary", {
|
|
94
|
+
found: true,
|
|
95
|
+
path: result.binary,
|
|
96
|
+
downloaded: result.downloaded,
|
|
97
|
+
hasWarning: !!result.warning,
|
|
98
|
+
});
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
log.warn("aide binary not found and download failed");
|
|
103
|
+
log.end("checkAideBinary", { found: false });
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Reset HUD state file for clean session start — delegates to core
|
|
110
|
+
*/
|
|
111
|
+
function resetHudState(cwd: string, log: Logger): void {
|
|
112
|
+
log.start("resetHudState");
|
|
113
|
+
try {
|
|
114
|
+
coreResetHudState(cwd);
|
|
115
|
+
log.end("resetHudState", { success: true });
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log.warn("Failed to reset HUD state", err);
|
|
118
|
+
log.end("resetHudState", { success: false, error: String(err) });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get the plugin root directory
|
|
124
|
+
*/
|
|
125
|
+
function getPluginRoot(): string | null {
|
|
126
|
+
// Check AIDE_PLUGIN_ROOT or CLAUDE_PLUGIN_ROOT env var
|
|
127
|
+
const envRoot =
|
|
128
|
+
process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT;
|
|
129
|
+
if (envRoot && existsSync(join(envRoot, "package.json"))) {
|
|
130
|
+
return envRoot;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Calculate from this script's location: dist/hooks/session-start.js -> ../../
|
|
134
|
+
try {
|
|
135
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
136
|
+
const pluginRoot = join(dirname(scriptPath), "..", "..");
|
|
137
|
+
if (existsSync(join(pluginRoot, "package.json"))) {
|
|
138
|
+
return pluginRoot;
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// import.meta.url not available
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Install the HUD wrapper script to ~/.claude/bin/
|
|
149
|
+
*
|
|
150
|
+
* This installs a thin wrapper that delegates to the real HUD script in the plugin.
|
|
151
|
+
* The wrapper allows plugin updates to take effect without reinstalling.
|
|
152
|
+
*/
|
|
153
|
+
function installHudWrapper(log: Logger): void {
|
|
154
|
+
log.start("installHudWrapper");
|
|
155
|
+
|
|
156
|
+
const claudeBinDir = join(homedir(), ".claude", "bin");
|
|
157
|
+
const wrapperDest = join(claudeBinDir, "aide-hud.ts");
|
|
158
|
+
|
|
159
|
+
// Check if wrapper already exists
|
|
160
|
+
if (existsSync(wrapperDest)) {
|
|
161
|
+
log.debug("HUD wrapper already installed");
|
|
162
|
+
log.end("installHudWrapper", { skipped: true, reason: "exists" });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Find our wrapper source
|
|
167
|
+
const pluginRoot = getPluginRoot();
|
|
168
|
+
if (!pluginRoot) {
|
|
169
|
+
log.warn("Could not determine plugin root, skipping HUD wrapper install");
|
|
170
|
+
log.end("installHudWrapper", { skipped: true, reason: "no-plugin-root" });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const wrapperSrc = join(pluginRoot, "scripts", "aide-hud-wrapper.ts");
|
|
175
|
+
if (!existsSync(wrapperSrc)) {
|
|
176
|
+
log.warn(`HUD wrapper source not found: ${wrapperSrc}`);
|
|
177
|
+
log.end("installHudWrapper", { skipped: true, reason: "no-source" });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
// Create ~/.claude/bin if needed
|
|
183
|
+
if (!existsSync(claudeBinDir)) {
|
|
184
|
+
mkdirSync(claudeBinDir, { recursive: true });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Copy wrapper script
|
|
188
|
+
copyFileSync(wrapperSrc, wrapperDest);
|
|
189
|
+
try { chmodSync(wrapperDest, 0o755); } catch { /* no-op on Windows */ }
|
|
190
|
+
|
|
191
|
+
log.info(`Installed HUD wrapper to ${wrapperDest}`);
|
|
192
|
+
log.end("installHudWrapper", { success: true, path: wrapperDest });
|
|
193
|
+
} catch (err) {
|
|
194
|
+
log.warn("Failed to install HUD wrapper", err);
|
|
195
|
+
log.end("installHudWrapper", { success: false, error: String(err) });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Ensure all .aide directories exist — delegates to core
|
|
201
|
+
*/
|
|
202
|
+
function ensureDirectories(cwd: string, log: Logger): void {
|
|
203
|
+
log.start("ensureDirectories");
|
|
204
|
+
const { created, existed } = coreEnsureDirectories(cwd);
|
|
205
|
+
log.end("ensureDirectories", { created, existed });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Load or create config file — delegates to core
|
|
210
|
+
*/
|
|
211
|
+
function loadConfig(cwd: string, log: Logger): AideConfig {
|
|
212
|
+
log.start("loadConfig");
|
|
213
|
+
const config = coreLoadConfig(cwd);
|
|
214
|
+
log.end("loadConfig");
|
|
215
|
+
return config;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Clean up stale state files — delegates to core
|
|
220
|
+
*/
|
|
221
|
+
function cleanupStaleStateFiles(cwd: string, log: Logger): void {
|
|
222
|
+
log.start("cleanupStaleStateFiles");
|
|
223
|
+
const { scanned, deleted } = coreCleanupStaleStateFiles(cwd);
|
|
224
|
+
log.end("cleanupStaleStateFiles", { scanned, deleted });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Initialize session state — delegates to core
|
|
229
|
+
*/
|
|
230
|
+
function initializeSession(
|
|
231
|
+
sessionId: string,
|
|
232
|
+
cwd: string,
|
|
233
|
+
log: Logger,
|
|
234
|
+
): SessionState {
|
|
235
|
+
log.start("initializeSession");
|
|
236
|
+
const state = coreInitializeSession(sessionId, cwd);
|
|
237
|
+
log.end("initializeSession", { sessionId: sessionId.slice(0, 8) });
|
|
238
|
+
return state;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// getProjectName, runSessionInit, formatTimeAgo, buildWelcomeContext
|
|
242
|
+
// are now imported from ../core/session-init.js above.
|
|
243
|
+
// The runSessionInit wrapper below adds logging around the core function.
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Run session init with logging — wraps core function
|
|
247
|
+
*/
|
|
248
|
+
function runSessionInit(
|
|
249
|
+
cwd: string,
|
|
250
|
+
projectName: string,
|
|
251
|
+
sessionLimit: number,
|
|
252
|
+
log: Logger,
|
|
253
|
+
config?: AideConfig,
|
|
254
|
+
): MemoryInjection {
|
|
255
|
+
log.start("sessionInit");
|
|
256
|
+
|
|
257
|
+
const binary = findAideBinary(cwd);
|
|
258
|
+
if (!binary) {
|
|
259
|
+
log.debug("aide binary not found, skipping session init");
|
|
260
|
+
log.end("sessionInit", { skipped: true, reason: "no-binary" });
|
|
261
|
+
return {
|
|
262
|
+
static: { global: [], project: [], decisions: [] },
|
|
263
|
+
dynamic: { sessions: [] },
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const result = coreRunSessionInit(
|
|
268
|
+
binary,
|
|
269
|
+
cwd,
|
|
270
|
+
projectName,
|
|
271
|
+
sessionLimit,
|
|
272
|
+
config,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
log.end("sessionInit", {
|
|
276
|
+
globalCount: result.static.global.length,
|
|
277
|
+
projectCount: result.static.project.length,
|
|
278
|
+
decisionCount: result.static.decisions.length,
|
|
279
|
+
sessionCount: result.dynamic.sessions.length,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Build welcome context — wraps core function
|
|
287
|
+
*/
|
|
288
|
+
function buildWelcomeContext(
|
|
289
|
+
state: SessionState,
|
|
290
|
+
memories: MemoryInjection,
|
|
291
|
+
notices: StartupNotices = {},
|
|
292
|
+
): string {
|
|
293
|
+
return coreBuildWelcomeContext(state, memories, notices);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Debug helper - writes to debug.log (not stderr)
|
|
297
|
+
function debugLog(msg: string): void {
|
|
298
|
+
debug(SOURCE, msg);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Ensure we always output valid JSON, even on catastrophic errors
|
|
302
|
+
function outputContinue(): void {
|
|
303
|
+
try {
|
|
304
|
+
console.log(JSON.stringify({ continue: true }));
|
|
305
|
+
} catch {
|
|
306
|
+
// Last resort - raw JSON string
|
|
307
|
+
console.log('{"continue":true}');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Global error handlers to prevent hook crashes without JSON output
|
|
312
|
+
process.on("uncaughtException", (err) => {
|
|
313
|
+
debugLog(`UNCAUGHT EXCEPTION: ${err}`);
|
|
314
|
+
outputContinue();
|
|
315
|
+
process.exit(0);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
process.on("unhandledRejection", (reason) => {
|
|
319
|
+
debugLog(`UNHANDLED REJECTION: ${reason}`);
|
|
320
|
+
outputContinue();
|
|
321
|
+
process.exit(0);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
async function main(): Promise<void> {
|
|
325
|
+
let log: Logger | null = null;
|
|
326
|
+
const hookStart = Date.now();
|
|
327
|
+
|
|
328
|
+
debugLog(`Hook started at ${new Date().toISOString()}`);
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
debugLog("Reading stdin...");
|
|
332
|
+
const input = await readStdin();
|
|
333
|
+
debugLog(`Stdin read complete (${Date.now() - hookStart}ms)`);
|
|
334
|
+
|
|
335
|
+
if (!input.trim()) {
|
|
336
|
+
debugLog("Empty input, exiting");
|
|
337
|
+
console.log(JSON.stringify({ continue: true }));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const data: HookInput = JSON.parse(input);
|
|
342
|
+
const cwd = data.cwd || process.cwd();
|
|
343
|
+
const sessionId = data.session_id || "unknown";
|
|
344
|
+
|
|
345
|
+
// Switch debug logging to project-local logs
|
|
346
|
+
setDebugCwd(cwd);
|
|
347
|
+
|
|
348
|
+
debugLog(`Parsed input: cwd=${cwd}, sessionId=${sessionId.slice(0, 8)}`);
|
|
349
|
+
|
|
350
|
+
// Initialize logger
|
|
351
|
+
log = new Logger("session-start", cwd);
|
|
352
|
+
log.info(`Session starting: ${sessionId.slice(0, 8)}`);
|
|
353
|
+
log.start("total");
|
|
354
|
+
|
|
355
|
+
debugLog(`Logger initialized, enabled=${log.isEnabled()}`);
|
|
356
|
+
|
|
357
|
+
// Initialize directories (FS only, fast)
|
|
358
|
+
debugLog("ensureDirectories starting...");
|
|
359
|
+
ensureDirectories(cwd, log);
|
|
360
|
+
debugLog(`ensureDirectories complete (${Date.now() - hookStart}ms)`);
|
|
361
|
+
|
|
362
|
+
// Install HUD wrapper script if not present (FS only, fast)
|
|
363
|
+
debugLog("installHudWrapper starting...");
|
|
364
|
+
installHudWrapper(log);
|
|
365
|
+
debugLog(`installHudWrapper complete (${Date.now() - hookStart}ms)`);
|
|
366
|
+
|
|
367
|
+
// Sync MCP server configs across assistants (FS only, fast)
|
|
368
|
+
debugLog("mcpSync starting...");
|
|
369
|
+
log.start("mcpSync");
|
|
370
|
+
try {
|
|
371
|
+
const mcpResult = syncMcpServers(detectPlatform(), cwd);
|
|
372
|
+
const totalImported =
|
|
373
|
+
mcpResult.user.imported + mcpResult.project.imported;
|
|
374
|
+
const totalWritten =
|
|
375
|
+
mcpResult.user.serversWritten + mcpResult.project.serversWritten;
|
|
376
|
+
const totalSkipped = mcpResult.user.skipped + mcpResult.project.skipped;
|
|
377
|
+
log.end("mcpSync", {
|
|
378
|
+
userServers: mcpResult.user.serversWritten,
|
|
379
|
+
projectServers: mcpResult.project.serversWritten,
|
|
380
|
+
imported: totalImported,
|
|
381
|
+
skipped: totalSkipped,
|
|
382
|
+
});
|
|
383
|
+
if (totalImported > 0) {
|
|
384
|
+
debugLog(
|
|
385
|
+
`mcp-sync: imported ${totalImported} server(s), ${totalWritten} total`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
} catch (err) {
|
|
389
|
+
log.warn("MCP sync failed (non-fatal)", err);
|
|
390
|
+
log.end("mcpSync", { success: false, error: String(err) });
|
|
391
|
+
}
|
|
392
|
+
debugLog(`mcpSync complete (${Date.now() - hookStart}ms)`);
|
|
393
|
+
|
|
394
|
+
// Check that aide binary is available (auto-downloads if missing/outdated)
|
|
395
|
+
debugLog("checkAideBinary starting...");
|
|
396
|
+
const {
|
|
397
|
+
error: binaryError,
|
|
398
|
+
warning: binaryWarning,
|
|
399
|
+
downloaded: binaryDownloaded,
|
|
400
|
+
} = await checkAideBinary(cwd, log);
|
|
401
|
+
if (binaryDownloaded) {
|
|
402
|
+
debugLog(`aide binary was downloaded`);
|
|
403
|
+
}
|
|
404
|
+
debugLog(`checkAideBinary complete (${Date.now() - hookStart}ms)`);
|
|
405
|
+
|
|
406
|
+
// Reset HUD state for clean session start (FS only, fast)
|
|
407
|
+
debugLog("resetHudState starting...");
|
|
408
|
+
resetHudState(cwd, log);
|
|
409
|
+
debugLog(`resetHudState complete (${Date.now() - hookStart}ms)`);
|
|
410
|
+
|
|
411
|
+
// Load config (FS only, fast)
|
|
412
|
+
debugLog("loadConfig starting...");
|
|
413
|
+
const config = loadConfig(cwd, log);
|
|
414
|
+
debugLog(`loadConfig complete (${Date.now() - hookStart}ms)`);
|
|
415
|
+
|
|
416
|
+
// Cleanup stale state files on disk (FS only, fast)
|
|
417
|
+
debugLog("cleanupStaleStateFiles starting...");
|
|
418
|
+
cleanupStaleStateFiles(cwd, log);
|
|
419
|
+
debugLog(`cleanupStaleStateFiles complete (${Date.now() - hookStart}ms)`);
|
|
420
|
+
|
|
421
|
+
// Initialize session state file (FS only, fast)
|
|
422
|
+
debugLog("initializeSession starting...");
|
|
423
|
+
const state = initializeSession(sessionId, cwd, log);
|
|
424
|
+
debugLog(`initializeSession complete (${Date.now() - hookStart}ms)`);
|
|
425
|
+
|
|
426
|
+
// Single aide binary call: reset state + cleanup agents + fetch all memories
|
|
427
|
+
// Replaces 7 separate binary spawns (~35-50s) with 1 (~5s)
|
|
428
|
+
const projectName = getProjectName(cwd);
|
|
429
|
+
debugLog("sessionInit starting...");
|
|
430
|
+
const memories = runSessionInit(cwd, projectName, 2, log, config);
|
|
431
|
+
debugLog(`sessionInit complete (${Date.now() - hookStart}ms)`);
|
|
432
|
+
|
|
433
|
+
// Build startup notices
|
|
434
|
+
const notices: StartupNotices = {
|
|
435
|
+
error: binaryError,
|
|
436
|
+
warning: binaryWarning,
|
|
437
|
+
info: [],
|
|
438
|
+
};
|
|
439
|
+
if (binaryDownloaded) {
|
|
440
|
+
notices.info!.push("aide binary downloaded");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Log notices via debug (avoids stderr which Claude Code interprets as error)
|
|
444
|
+
if (notices.error) {
|
|
445
|
+
debugLog(`NOTICE ERROR: ${notices.error}`);
|
|
446
|
+
}
|
|
447
|
+
if (notices.warning) {
|
|
448
|
+
debugLog(`NOTICE WARNING: ${notices.warning}`);
|
|
449
|
+
}
|
|
450
|
+
for (const info of notices.info || []) {
|
|
451
|
+
debugLog(`NOTICE INFO: ${info}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Build welcome context with injected memories
|
|
455
|
+
debugLog("buildWelcomeContext starting...");
|
|
456
|
+
log.start("buildWelcomeContext");
|
|
457
|
+
const context = buildWelcomeContext(state, memories, notices);
|
|
458
|
+
log.end("buildWelcomeContext");
|
|
459
|
+
debugLog(`buildWelcomeContext complete (${Date.now() - hookStart}ms)`);
|
|
460
|
+
|
|
461
|
+
log.end("total");
|
|
462
|
+
log.info("Session start complete");
|
|
463
|
+
debugLog(`Flushing logs to ${log.getLogFile()}...`);
|
|
464
|
+
log.flush();
|
|
465
|
+
debugLog(`Hook complete (${Date.now() - hookStart}ms total)`);
|
|
466
|
+
|
|
467
|
+
const output: HookOutput = {
|
|
468
|
+
continue: true,
|
|
469
|
+
hookSpecificOutput: {
|
|
470
|
+
hookEventName: "SessionStart",
|
|
471
|
+
additionalContext: context,
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
console.log(JSON.stringify(output));
|
|
476
|
+
} catch (error) {
|
|
477
|
+
debugLog(`ERROR: ${error}`);
|
|
478
|
+
// Log error if logger is available
|
|
479
|
+
if (log) {
|
|
480
|
+
log.error("Session start failed", error);
|
|
481
|
+
log.flush();
|
|
482
|
+
}
|
|
483
|
+
// On error, allow continuation without context
|
|
484
|
+
console.log(JSON.stringify({ continue: true }));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
main();
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Session Summary Hook (Stop)
|
|
4
|
+
*
|
|
5
|
+
* Captures a session summary from the transcript when a session ends.
|
|
6
|
+
* Includes files modified, tools used, user tasks, and git commits.
|
|
7
|
+
*
|
|
8
|
+
* Storage:
|
|
9
|
+
* - Uses `aide memory add` with category=session
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { debug, setDebugCwd } from "../lib/logger.js";
|
|
13
|
+
import { readStdin } from "../lib/hook-utils.js";
|
|
14
|
+
import { findAideBinary } from "../core/aide-client.js";
|
|
15
|
+
import {
|
|
16
|
+
buildSessionSummary,
|
|
17
|
+
getSessionCommits,
|
|
18
|
+
storeSessionSummary,
|
|
19
|
+
} from "../core/session-summary-logic.js";
|
|
20
|
+
import {
|
|
21
|
+
gatherPartials,
|
|
22
|
+
buildSummaryFromPartials,
|
|
23
|
+
cleanupPartials,
|
|
24
|
+
} from "../core/partial-memory.js";
|
|
25
|
+
|
|
26
|
+
const SOURCE = "session-summary";
|
|
27
|
+
|
|
28
|
+
interface HookInput {
|
|
29
|
+
hook_event_name: string;
|
|
30
|
+
session_id: string;
|
|
31
|
+
cwd: string;
|
|
32
|
+
transcript_path?: string;
|
|
33
|
+
stop_hook_active?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate and store a final session summary.
|
|
38
|
+
*
|
|
39
|
+
* Uses partials (if available) to enrich the transcript-based summary.
|
|
40
|
+
* After storing the final summary, cleans up all partials for this session
|
|
41
|
+
* by tagging them as "forget".
|
|
42
|
+
*/
|
|
43
|
+
function captureSessionSummary(
|
|
44
|
+
cwd: string,
|
|
45
|
+
sessionId: string,
|
|
46
|
+
transcriptPath: string,
|
|
47
|
+
): boolean {
|
|
48
|
+
const binary = findAideBinary({
|
|
49
|
+
cwd,
|
|
50
|
+
pluginRoot: process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
|
|
51
|
+
});
|
|
52
|
+
if (!binary) {
|
|
53
|
+
debug(SOURCE, "aide binary not found, cannot capture session summary");
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Try to build the richest possible summary:
|
|
58
|
+
// 1. Gather partials from this session
|
|
59
|
+
// 2. Build from transcript (Claude Code has this)
|
|
60
|
+
// 3. Merge partials data with transcript summary
|
|
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(SOURCE, `Built summary from ${partials.length} partials`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Fall back to transcript-based summary if partials didn't produce enough
|
|
72
|
+
if (!summary) {
|
|
73
|
+
summary = buildSessionSummary(transcriptPath, cwd);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!summary) {
|
|
77
|
+
debug(SOURCE, "No summary generated (insufficient activity or transcript)");
|
|
78
|
+
// Still clean up partials even if no summary was generated
|
|
79
|
+
if (partials.length > 0) {
|
|
80
|
+
cleanupPartials(binary, cwd, sessionId);
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const stored = storeSessionSummary(binary, cwd, sessionId, summary);
|
|
86
|
+
if (stored) {
|
|
87
|
+
debug(SOURCE, `Stored final session summary for ${sessionId.slice(0, 8)}`);
|
|
88
|
+
// Clean up partials now that the final summary is stored
|
|
89
|
+
const cleaned = cleanupPartials(binary, cwd, sessionId);
|
|
90
|
+
debug(SOURCE, `Cleaned up ${cleaned} partials after final summary`);
|
|
91
|
+
} else {
|
|
92
|
+
debug(SOURCE, "Failed to store session summary");
|
|
93
|
+
}
|
|
94
|
+
return stored;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main(): Promise<void> {
|
|
98
|
+
try {
|
|
99
|
+
const input = await readStdin();
|
|
100
|
+
if (!input.trim()) {
|
|
101
|
+
console.log(JSON.stringify({ continue: true }));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const data: HookInput = JSON.parse(input);
|
|
106
|
+
const cwd = data.cwd || process.cwd();
|
|
107
|
+
const sessionId = data.session_id || "unknown";
|
|
108
|
+
|
|
109
|
+
setDebugCwd(cwd);
|
|
110
|
+
debug(SOURCE, `Hook triggered: ${data.hook_event_name}`);
|
|
111
|
+
|
|
112
|
+
// For Stop hook, capture session summary
|
|
113
|
+
if (data.hook_event_name === "Stop" && data.transcript_path) {
|
|
114
|
+
// Don't capture if stop hook is already active (avoid recursion)
|
|
115
|
+
if (!data.stop_hook_active) {
|
|
116
|
+
debug(SOURCE, "Stop hook - capturing session summary");
|
|
117
|
+
captureSessionSummary(cwd, sessionId, data.transcript_path);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(JSON.stringify({ continue: true }));
|
|
122
|
+
} catch (err) {
|
|
123
|
+
debug(SOURCE, `Error: ${err}`);
|
|
124
|
+
console.log(JSON.stringify({ continue: true }));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process.on("uncaughtException", (err) => {
|
|
129
|
+
debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
|
|
130
|
+
try {
|
|
131
|
+
console.log(JSON.stringify({ continue: true }));
|
|
132
|
+
} catch {
|
|
133
|
+
console.log('{"continue":true}');
|
|
134
|
+
}
|
|
135
|
+
process.exit(0);
|
|
136
|
+
});
|
|
137
|
+
process.on("unhandledRejection", (reason) => {
|
|
138
|
+
debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
|
|
139
|
+
try {
|
|
140
|
+
console.log(JSON.stringify({ continue: true }));
|
|
141
|
+
} catch {
|
|
142
|
+
console.log('{"continue":true}');
|
|
143
|
+
}
|
|
144
|
+
process.exit(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
main();
|