@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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Install command — registers aide plugin and MCP server in OpenCode config.
2
+ * Install command — registers aide plugin and MCP server for OpenCode or Codex CLI.
3
3
  *
4
4
  * On reinstall, detects and upgrades stale MCP command configurations
5
5
  * (e.g. old `aide-wrapper` commands) to the current format.
@@ -15,51 +15,37 @@ import {
15
15
  PLUGIN_NAME,
16
16
  MCP_SERVER_NAME,
17
17
  } from "./config.js";
18
+ import { installCodex, isCodexConfigured } from "./codex-config.js";
18
19
 
19
20
  export interface InstallFlags {
20
21
  project?: boolean;
21
22
  noMcp?: boolean;
23
+ platform?: "opencode" | "codex";
22
24
  }
23
25
 
24
- /**
25
- * Check if an existing MCP config has the current expected command format.
26
- * Returns false if the command is missing, empty, or uses an outdated format.
27
- */
28
26
  function isMcpCommandCurrent(config: ReturnType<typeof readConfig>): boolean {
29
27
  const mcpConfig = config.mcp?.[MCP_SERVER_NAME];
30
- if (!mcpConfig?.command || mcpConfig.command.length === 0) {
31
- return false;
32
- }
33
-
28
+ if (!mcpConfig?.command || mcpConfig.command.length === 0) return false;
34
29
  const cmd = mcpConfig.command;
35
-
36
- // Current format: ["bunx", "-y", "@jmylchreest/aide-plugin", "mcp"]
37
- if (
30
+ return (
38
31
  cmd.length === 4 &&
39
32
  cmd[0] === "bunx" &&
40
33
  cmd[1] === "-y" &&
41
34
  cmd[2] === PLUGIN_NAME &&
42
35
  cmd[3] === "mcp"
43
- ) {
44
- return true;
45
- }
46
-
47
- return false;
36
+ );
48
37
  }
49
38
 
50
- export async function install(flags: InstallFlags): Promise<void> {
39
+ async function installOpenCode(flags: InstallFlags): Promise<void> {
51
40
  const configPath = flags.project
52
41
  ? getProjectConfigPath()
53
42
  : getGlobalConfigPath();
54
43
 
55
44
  const scope = flags.project ? "project" : "global";
56
- console.log(`Installing aide plugin (${scope})...\n`);
45
+ console.log(`Installing aide plugin for OpenCode (${scope})...\n`);
57
46
 
58
- // Read existing config
59
47
  const existing = readConfig(configPath);
60
48
  const before = isAideConfigured(existing);
61
-
62
- // Check if MCP command needs updating (stale format from older versions)
63
49
  const mcpNeedsUpdate =
64
50
  !flags.noMcp && before.mcp && !isMcpCommandCurrent(existing);
65
51
 
@@ -71,16 +57,13 @@ export async function install(flags: InstallFlags): Promise<void> {
71
57
  return;
72
58
  }
73
59
 
74
- // If MCP config is stale, remove it so addAideToConfig will write the current version
75
60
  if (mcpNeedsUpdate && existing.mcp) {
76
61
  delete existing.mcp[MCP_SERVER_NAME];
77
62
  }
78
63
 
79
- // Apply changes
80
64
  const updated = addAideToConfig(existing, { noMcp: flags.noMcp });
81
65
  writeConfig(configPath, updated);
82
66
 
83
- // Report what was done
84
67
  const after = isAideConfigured(updated);
85
68
  console.log(`Updated: ${configPath}\n`);
86
69
 
@@ -110,3 +93,47 @@ export async function install(flags: InstallFlags): Promise<void> {
110
93
  );
111
94
  }
112
95
  }
96
+
97
+ async function installForCodex(flags: InstallFlags): Promise<void> {
98
+ const scope = flags.project ? "project" : "user";
99
+ console.log(`Installing aide for Codex CLI (${scope})...\n`);
100
+
101
+ const before = isCodexConfigured(scope);
102
+ if (before.mcp && before.hooks) {
103
+ console.log("aide is already configured for Codex CLI");
104
+ console.log(" mcp: registered in config.toml");
105
+ console.log(" hooks: registered in hooks.json");
106
+ console.log("\nNothing to do.");
107
+ return;
108
+ }
109
+
110
+ const result = installCodex(scope);
111
+
112
+ if (result.configWritten) {
113
+ console.log(" + Added aide MCP server to config.toml");
114
+ } else if (before.mcp) {
115
+ console.log(" = MCP server already registered in config.toml");
116
+ }
117
+
118
+ if (result.hooksWritten) {
119
+ console.log(" + Generated hooks.json with aide hooks");
120
+ } else if (before.hooks) {
121
+ console.log(" = Hooks already registered in hooks.json");
122
+ }
123
+
124
+ console.log("\nInstallation complete. Start Codex CLI to use aide.");
125
+
126
+ if (!flags.project) {
127
+ console.log(
128
+ "\nThe plugin is installed globally and will apply to all Codex CLI sessions.",
129
+ );
130
+ }
131
+ }
132
+
133
+ export async function install(flags: InstallFlags): Promise<void> {
134
+ if (flags.platform === "codex") {
135
+ await installForCodex(flags);
136
+ } else {
137
+ await installOpenCode(flags);
138
+ }
139
+ }
package/src/cli/status.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Status command — shows current aide installation status for OpenCode.
2
+ * Status command — shows current aide installation status for OpenCode or Codex CLI.
3
3
  */
4
4
 
5
5
  import { existsSync } from "fs";
@@ -9,38 +9,71 @@ import {
9
9
  isAideConfigured,
10
10
  readConfig,
11
11
  } from "./config.js";
12
+ import {
13
+ isCodexConfigured,
14
+ getCodexConfigTomlPath,
15
+ getCodexHooksJsonPath,
16
+ } from "./codex-config.js";
17
+
18
+ export interface StatusFlags {
19
+ platform?: "opencode" | "codex";
20
+ }
12
21
 
13
- export async function status(): Promise<void> {
14
- console.log("aide plugin status\n");
22
+ function showOpenCodeStatus(): void {
23
+ console.log("aide plugin status (OpenCode)\n");
15
24
 
16
25
  const globalPath = getGlobalConfigPath();
17
26
  const projectPath = getProjectConfigPath();
18
27
 
19
- // Global config
20
28
  console.log(`Global config: ${globalPath}`);
21
29
  if (existsSync(globalPath)) {
22
- const globalConfig = readConfig(globalPath);
23
- const globalStatus = isAideConfigured(globalConfig);
24
- console.log(
25
- ` plugin: ${globalStatus.plugin ? "registered" : "not found"}`,
26
- );
27
- console.log(` mcp: ${globalStatus.mcp ? "registered" : "not found"}`);
30
+ const s = isAideConfigured(readConfig(globalPath));
31
+ console.log(` plugin: ${s.plugin ? "registered" : "not found"}`);
32
+ console.log(` mcp: ${s.mcp ? "registered" : "not found"}`);
28
33
  } else {
29
34
  console.log(" (file does not exist)");
30
35
  }
31
36
 
32
37
  console.log();
33
38
 
34
- // Project config
35
39
  console.log(`Project config: ${projectPath}`);
36
40
  if (existsSync(projectPath)) {
37
- const projectConfig = readConfig(projectPath);
38
- const projectStatus = isAideConfigured(projectConfig);
39
- console.log(
40
- ` plugin: ${projectStatus.plugin ? "registered" : "not found"}`,
41
- );
42
- console.log(` mcp: ${projectStatus.mcp ? "registered" : "not found"}`);
41
+ const s = isAideConfigured(readConfig(projectPath));
42
+ console.log(` plugin: ${s.plugin ? "registered" : "not found"}`);
43
+ console.log(` mcp: ${s.mcp ? "registered" : "not found"}`);
43
44
  } else {
44
45
  console.log(" (file does not exist)");
45
46
  }
46
47
  }
48
+
49
+ function showCodexStatus(): void {
50
+ console.log("aide plugin status (Codex CLI)\n");
51
+
52
+ const userConfig = getCodexConfigTomlPath("user");
53
+ const userHooks = getCodexHooksJsonPath("user");
54
+ const userStatus = isCodexConfigured("user");
55
+
56
+ console.log(`User config: ${userConfig}`);
57
+ console.log(`User hooks: ${userHooks}`);
58
+ console.log(` mcp: ${userStatus.mcp ? "registered" : "not found"}`);
59
+ console.log(` hooks: ${userStatus.hooks ? "registered" : "not found"}`);
60
+
61
+ console.log();
62
+
63
+ const projectConfig = getCodexConfigTomlPath("project");
64
+ const projectHooks = getCodexHooksJsonPath("project");
65
+ const projectStatus = isCodexConfigured("project");
66
+
67
+ console.log(`Project config: ${projectConfig}`);
68
+ console.log(`Project hooks: ${projectHooks}`);
69
+ console.log(` mcp: ${projectStatus.mcp ? "registered" : "not found"}`);
70
+ console.log(` hooks: ${projectStatus.hooks ? "registered" : "not found"}`);
71
+ }
72
+
73
+ export async function status(flags?: StatusFlags): Promise<void> {
74
+ if (flags?.platform === "codex") {
75
+ showCodexStatus();
76
+ } else {
77
+ showOpenCodeStatus();
78
+ }
79
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Uninstall command — removes aide plugin and MCP server from OpenCode config.
2
+ * Uninstall command — removes aide plugin and MCP server from OpenCode or Codex CLI config.
3
3
  */
4
4
 
5
5
  import {
@@ -10,18 +10,20 @@ import {
10
10
  removeAideFromConfig,
11
11
  writeConfig,
12
12
  } from "./config.js";
13
+ import { uninstallCodex, isCodexConfigured } from "./codex-config.js";
13
14
 
14
15
  export interface UninstallFlags {
15
16
  project?: boolean;
17
+ platform?: "opencode" | "codex";
16
18
  }
17
19
 
18
- export async function uninstall(flags: UninstallFlags): Promise<void> {
20
+ async function uninstallOpenCode(flags: UninstallFlags): Promise<void> {
19
21
  const configPath = flags.project
20
22
  ? getProjectConfigPath()
21
23
  : getGlobalConfigPath();
22
24
 
23
25
  const scope = flags.project ? "project" : "global";
24
- console.log(`Uninstalling aide plugin (${scope})...\n`);
26
+ console.log(`Uninstalling aide plugin from OpenCode (${scope})...\n`);
25
27
 
26
28
  const existing = readConfig(configPath);
27
29
  const before = isAideConfigured(existing);
@@ -36,13 +38,32 @@ export async function uninstall(flags: UninstallFlags): Promise<void> {
36
38
  writeConfig(configPath, updated);
37
39
 
38
40
  console.log(`Updated: ${configPath}\n`);
41
+ if (before.plugin) console.log(` - Removed aide plugin from plugin array`);
42
+ if (before.mcp) console.log(` - Removed aide MCP server`);
43
+ console.log("\nUninstallation complete.");
44
+ }
39
45
 
40
- if (before.plugin) {
41
- console.log(` - Removed aide plugin from plugin array`);
42
- }
43
- if (before.mcp) {
44
- console.log(` - Removed aide MCP server`);
46
+ async function uninstallFromCodex(flags: UninstallFlags): Promise<void> {
47
+ const scope = flags.project ? "project" : "user";
48
+ console.log(`Uninstalling aide from Codex CLI (${scope})...\n`);
49
+
50
+ const before = isCodexConfigured(scope);
51
+ if (!before.mcp && !before.hooks) {
52
+ console.log("aide is not configured for Codex CLI.");
53
+ console.log("\nNothing to do.");
54
+ return;
45
55
  }
46
56
 
57
+ const result = uninstallCodex(scope);
58
+ if (result.configRemoved) console.log(" - Removed aide MCP server from config.toml");
59
+ if (result.hooksRemoved) console.log(" - Removed aide hooks from hooks.json");
47
60
  console.log("\nUninstallation complete.");
48
61
  }
62
+
63
+ export async function uninstall(flags: UninstallFlags): Promise<void> {
64
+ if (flags.platform === "codex") {
65
+ await uninstallFromCodex(flags);
66
+ } else {
67
+ await uninstallOpenCode(flags);
68
+ }
69
+ }
@@ -8,6 +8,7 @@
8
8
  * Supports:
9
9
  * - Claude Code: ~/.claude.json (user), .mcp.json (project) (reads legacy ~/.mcp.json)
10
10
  * - OpenCode: ~/.config/opencode/opencode.json (user), ./opencode.json (project)
11
+ * - Codex CLI: ~/.codex/config.toml (user), .codex/config.toml (project)
11
12
  * - Aide canonical: ~/.aide/config/mcp.json (user), .aide/config/mcp.json (project)
12
13
  *
13
14
  * Uses a journal to detect intentional deletions from the aide canonical file,
@@ -21,17 +22,17 @@ import {
21
22
  writeFileSync,
22
23
  mkdirSync,
23
24
  statSync,
24
- unlinkSync,
25
25
  } from "fs";
26
26
  import { join, dirname } from "path";
27
27
  import { homedir } from "os";
28
+ import * as TOML from "smol-toml";
28
29
 
29
30
  // =============================================================================
30
31
  // Types
31
32
  // =============================================================================
32
33
 
33
34
  /** Platform identifier for the current assistant. */
34
- export type McpPlatform = "claude-code" | "opencode";
35
+ export type McpPlatform = "claude-code" | "opencode" | "codex";
35
36
 
36
37
  /**
37
38
  * Discover MCP server names managed by installed Claude Code plugins.
@@ -204,6 +205,11 @@ function getAssistantReadPaths(
204
205
  ? [join(homedir(), ".config", "opencode", "opencode.json")]
205
206
  : [join(cwd, "opencode.json")];
206
207
  }
208
+ if (platform === "codex") {
209
+ return scope === "user"
210
+ ? [join(homedir(), ".codex", "config.toml")]
211
+ : [join(cwd, ".codex", "config.toml")];
212
+ }
207
213
  return [];
208
214
  }
209
215
 
@@ -222,6 +228,11 @@ function getAssistantWritePaths(
222
228
  ? [join(homedir(), ".config", "opencode", "opencode.json")]
223
229
  : [join(cwd, "opencode.json")];
224
230
  }
231
+ if (platform === "codex") {
232
+ return scope === "user"
233
+ ? [join(homedir(), ".codex", "config.toml")]
234
+ : [join(cwd, ".codex", "config.toml")];
235
+ }
225
236
  return [];
226
237
  }
227
238
 
@@ -369,6 +380,59 @@ function readOpenCodeConfig(path: string): Record<string, CanonicalMcpServer> {
369
380
  }
370
381
  }
371
382
 
383
+ /**
384
+ * Read Codex CLI MCP config from config.toml.
385
+ *
386
+ * Format:
387
+ * ```toml
388
+ * [mcp_servers.name]
389
+ * command = "npx"
390
+ * args = ["package-name"]
391
+ *
392
+ * [mcp_servers.name.env]
393
+ * KEY = "value"
394
+ * ```
395
+ */
396
+ function readCodexConfig(path: string): Record<string, CanonicalMcpServer> {
397
+ if (!existsSync(path)) return {};
398
+
399
+ try {
400
+ const raw = TOML.parse(readFileSync(path, "utf-8"));
401
+ const servers: Record<string, CanonicalMcpServer> = {};
402
+
403
+ const mcpServers = raw.mcp_servers as
404
+ | Record<string, Record<string, unknown>>
405
+ | undefined;
406
+ if (
407
+ typeof mcpServers !== "object" ||
408
+ mcpServers === null ||
409
+ Array.isArray(mcpServers)
410
+ )
411
+ return {};
412
+
413
+ for (const [name, def] of Object.entries(mcpServers)) {
414
+ const hasUrl = typeof def.url === "string";
415
+ const isRemote = hasUrl;
416
+
417
+ servers[name] = {
418
+ name,
419
+ type: isRemote ? "remote" : "local",
420
+ transport: isRemote ? "http" : undefined,
421
+ command: def.command as string | undefined,
422
+ args: def.args as string[] | undefined,
423
+ url: def.url as string | undefined,
424
+ env: (def.env as Record<string, string>) || undefined,
425
+ headers: def.headers as Record<string, string> | undefined,
426
+ enabled: true,
427
+ };
428
+ }
429
+
430
+ return servers;
431
+ } catch {
432
+ return {};
433
+ }
434
+ }
435
+
372
436
  /**
373
437
  * Read MCP servers from a specific assistant's config file.
374
438
  */
@@ -378,6 +442,7 @@ function readAssistantConfig(
378
442
  ): Record<string, CanonicalMcpServer> {
379
443
  if (platform === "claude-code") return readClaudeConfig(path);
380
444
  if (platform === "opencode") return readOpenCodeConfig(path);
445
+ if (platform === "codex") return readCodexConfig(path);
381
446
  return {};
382
447
  }
383
448
 
@@ -511,6 +576,59 @@ function writeOpenCodeConfig(
511
576
  writeFileSync(path, JSON.stringify(existing, null, 2) + "\n");
512
577
  }
513
578
 
579
+ /**
580
+ * Write canonical servers to a Codex CLI config.toml file.
581
+ *
582
+ * Preserves all non-mcp_servers content in the file.
583
+ */
584
+ function writeCodexConfig(
585
+ path: string,
586
+ servers: Record<string, CanonicalMcpServer>,
587
+ ): void {
588
+ const dir = dirname(path);
589
+ if (!existsSync(dir)) {
590
+ mkdirSync(dir, { recursive: true });
591
+ }
592
+
593
+ // Read existing file to preserve non-mcp_servers keys
594
+ let existing: Record<string, unknown> = {};
595
+ if (existsSync(path)) {
596
+ try {
597
+ existing = TOML.parse(readFileSync(path, "utf-8")) as Record<
598
+ string,
599
+ unknown
600
+ >;
601
+ } catch {
602
+ // Start fresh
603
+ }
604
+ }
605
+
606
+ const mcpServers: Record<string, Record<string, unknown>> = {};
607
+ for (const [name, server] of Object.entries(servers)) {
608
+ const entry: Record<string, unknown> = {};
609
+
610
+ if (server.type === "remote") {
611
+ if (server.url) entry.url = server.url;
612
+ if (server.headers) entry.headers = server.headers;
613
+ } else {
614
+ if (server.command) entry.command = server.command;
615
+ if (server.args) entry.args = server.args;
616
+ }
617
+
618
+ if (server.env && Object.keys(server.env).length > 0) {
619
+ entry.env = server.env;
620
+ }
621
+
622
+ mcpServers[name] = entry;
623
+ }
624
+
625
+ existing.mcp_servers = mcpServers;
626
+ writeFileSync(
627
+ path,
628
+ TOML.stringify(existing as Record<string, unknown>) + "\n",
629
+ );
630
+ }
631
+
514
632
  /**
515
633
  * Write canonical servers to the current assistant's config file.
516
634
  */
@@ -521,6 +639,7 @@ function writeAssistantConfig(
521
639
  ): void {
522
640
  if (platform === "claude-code") writeClaudeConfig(path, servers);
523
641
  else if (platform === "opencode") writeOpenCodeConfig(path, servers);
642
+ else if (platform === "codex") writeCodexConfig(path, servers);
524
643
  }
525
644
 
526
645
  // =============================================================================
@@ -644,7 +763,7 @@ interface McpSource {
644
763
  * Gather all MCP config source files for a given scope.
645
764
  */
646
765
  function gatherSources(scope: McpScope, cwd: string): McpSource[] {
647
- const platforms: McpPlatform[] = ["claude-code", "opencode"];
766
+ const platforms: McpPlatform[] = ["claude-code", "opencode", "codex"];
648
767
  const sources: McpSource[] = [];
649
768
 
650
769
  const aidePath =
@@ -687,6 +806,13 @@ function updateJournalAndResolve(
687
806
  const now = new Date().toISOString();
688
807
  const activePaths = new Set(sources.map((s) => s.path));
689
808
 
809
+ // Capture servers already in the journal before we process state
810
+ // transitions. The bootstrap phase should only backfill absence for
811
+ // these pre-existing servers — not for servers discovered for the
812
+ // first time in this run — to avoid treating "never existed in
813
+ // another platform's config" as "intentionally removed".
814
+ const preExistingServers = new Set(Object.keys(journal.servers));
815
+
690
816
  // Detect state transitions per server per source
691
817
  for (const source of sources) {
692
818
  const currentNames = new Set(Object.keys(source.servers));
@@ -722,12 +848,14 @@ function updateJournalAndResolve(
722
848
  }
723
849
  }
724
850
 
725
- // Bootstrap: for servers found in some sources but absent from others,
726
- // record "not present" entries for the missing sources. Without this,
727
- // a server deleted from one file before the v2 journal existed would
728
- // have no removal event to counterbalance presence in other files.
729
- const allServerNames = new Set(Object.keys(journal.servers));
730
- for (const serverName of allServerNames) {
851
+ // Bootstrap: for servers that were already tracked in the journal
852
+ // before this run, record "not present" entries for sources that
853
+ // have never been tracked for that server. Without this, a server
854
+ // deleted from one file before the v2 journal existed would have
855
+ // no removal event to counterbalance presence in other files.
856
+ // We only bootstrap pre-existing servers to avoid treating "never
857
+ // existed in another platform's config" as an intentional removal.
858
+ for (const serverName of preExistingServers) {
731
859
  const sourceMap = journal.servers[serverName];
732
860
  for (const source of sources) {
733
861
  if (!sourceMap[source.path]) {
@@ -882,13 +1010,7 @@ function syncScope(
882
1010
  sortedStringify(existingAssistant) !== sortedStringify(assistantServers);
883
1011
 
884
1012
  if (assistantChanged) {
885
- if (Object.keys(assistantServers).length === 0 && existsSync(p)) {
886
- // No servers to write — remove the file rather than leaving
887
- // an empty mcpServers block that the assistant still loads.
888
- try { unlinkSync(p); } catch { /* ignore */ }
889
- } else if (Object.keys(assistantServers).length > 0) {
890
- writeAssistantConfig(platform, p, assistantServers);
891
- }
1013
+ writeAssistantConfig(platform, p, assistantServers);
892
1014
  result.modified = true;
893
1015
  }
894
1016
  }
@@ -911,7 +1033,7 @@ function syncScope(
911
1033
  * handles intentional deletions via a journal, and writes the result to
912
1034
  * both the aide canonical config and the current assistant's config files.
913
1035
  *
914
- * @param platform - The current assistant platform ("claude-code" or "opencode")
1036
+ * @param platform - The current assistant platform ("claude-code", "opencode", or "codex")
915
1037
  * @param cwd - The project working directory
916
1038
  * @returns Combined sync results for both user and project scopes
917
1039
  */
package/src/core/types.ts CHANGED
@@ -111,7 +111,7 @@ export interface Skill {
111
111
  path: string;
112
112
  triggers: string[];
113
113
  description?: string;
114
- /** Optional platform restriction. If set, only matched on listed platforms ("opencode", "claude-code"). */
114
+ /** Optional platform restriction. If set, only matched on listed platforms ("opencode", "claude-code", "codex"). */
115
115
  platforms?: string[];
116
116
  /** Optional binary requirement. If set, skill is only matched when all listed binaries exist on PATH. */
117
117
  requires_binary?: string[];
@@ -156,7 +156,7 @@ export const MAX_PERSISTENCE_ITERATIONS = 20;
156
156
  * Identifies which host platform aide is running in.
157
157
  * Used for platform-specific behavior like binary discovery or context injection.
158
158
  */
159
- export type AidePlatform = "claude-code" | "opencode" | "unknown";
159
+ export type AidePlatform = "claude-code" | "opencode" | "codex" | "unknown";
160
160
 
161
161
  /**
162
162
  * Options for finding the aide binary.
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent Cleanup Hook (Stop)
4
+ *
5
+ * Cleans up agent-specific state when an agent stops.
6
+ * This prevents stale state from polluting future agents with the same ID.
7
+ *
8
+ * Runs after persistence hook to clean up when agent is allowed to stop.
9
+ */
10
+
11
+ import { existsSync, appendFileSync } from "fs";
12
+ import { join } from "path";
13
+ import { readStdin } from "../lib/hook-utils.js";
14
+ import { findAideBinary } from "../core/aide-client.js";
15
+ import { cleanupAgent } from "../core/cleanup.js";
16
+ import { debug } from "../lib/logger.js";
17
+
18
+ const SOURCE = "agent-cleanup";
19
+
20
+ interface HookInput {
21
+ hook_event_name: string;
22
+ session_id: string;
23
+ cwd: string;
24
+ agent_id?: string;
25
+ transcript_path?: string;
26
+ permission_mode?: string;
27
+ }
28
+
29
+ async function main(): Promise<void> {
30
+ try {
31
+ const input = await readStdin();
32
+ if (!input.trim()) {
33
+ console.log(JSON.stringify({ continue: true }));
34
+ return;
35
+ }
36
+
37
+ const data: HookInput = JSON.parse(input);
38
+ const cwd = data.cwd || process.cwd();
39
+ const agentId = data.agent_id || data.session_id;
40
+
41
+ // Clean up agent-specific state — delegates to core
42
+ if (agentId) {
43
+ const binary = findAideBinary({
44
+ cwd,
45
+ pluginRoot:
46
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
47
+ });
48
+ if (binary) {
49
+ const cleared = cleanupAgent(binary, cwd, agentId);
50
+ if (cleared) {
51
+ const logDir = join(cwd, ".aide", "_logs");
52
+ if (existsSync(logDir)) {
53
+ const logPath = join(logDir, "agent-cleanup.log");
54
+ const timestamp = new Date().toISOString();
55
+ appendFileSync(
56
+ logPath,
57
+ `${timestamp} Cleaned up state for agent: ${agentId}\n`,
58
+ );
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ // Always continue - cleanup is best-effort
65
+ console.log(JSON.stringify({ continue: true }));
66
+ } catch (error) {
67
+ debug(SOURCE, `Hook error: ${error}`);
68
+ console.log(JSON.stringify({ continue: true }));
69
+ }
70
+ }
71
+
72
+ process.on("uncaughtException", (err) => {
73
+ debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
74
+ try {
75
+ console.log(JSON.stringify({ continue: true }));
76
+ } catch {
77
+ console.log('{"continue":true}');
78
+ }
79
+ process.exit(0);
80
+ });
81
+ process.on("unhandledRejection", (reason) => {
82
+ debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
83
+ try {
84
+ console.log(JSON.stringify({ continue: true }));
85
+ } catch {
86
+ console.log('{"continue":true}');
87
+ }
88
+ process.exit(0);
89
+ });
90
+
91
+ main();