@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jmylchreest/aide-plugin",
3
- "version": "0.0.56",
4
- "description": "aide plugin for OpenCode — multi-agent orchestration, memory, skills, and persistence",
3
+ "version": "0.0.58",
4
+ "description": "aide plugin for OpenCode and Codex CLI — multi-agent orchestration, memory, skills, and persistence",
5
5
  "type": "module",
6
6
  "main": "./src/opencode/index.ts",
7
7
  "exports": {
@@ -14,6 +14,7 @@
14
14
  "src/core",
15
15
  "src/cli",
16
16
  "src/lib",
17
+ "src/hooks",
17
18
  "skills",
18
19
  "bin",
19
20
  "README.md"
@@ -31,6 +32,7 @@
31
32
  "keywords": [
32
33
  "aide",
33
34
  "opencode",
35
+ "codex",
34
36
  "ai",
35
37
  "agents",
36
38
  "orchestration",
@@ -50,6 +52,7 @@
50
52
  },
51
53
  "dependencies": {
52
54
  "cross-spawn": "^7.0.6",
55
+ "smol-toml": "^1.3.1",
53
56
  "which": "^6.0.1"
54
57
  }
55
58
  }
@@ -0,0 +1,428 @@
1
+ /**
2
+ * Codex CLI configuration generator.
3
+ *
4
+ * Generates .codex/config.toml (MCP server) and .codex/hooks.json
5
+ * for integrating aide with Codex CLI.
6
+ */
7
+
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
9
+ import { dirname, join, resolve } from "path";
10
+ import { fileURLToPath } from "url";
11
+ import { homedir } from "os";
12
+ import * as TOML from "smol-toml";
13
+ import whichSync from "which";
14
+
15
+ const MCP_SERVER_NAME = "aide";
16
+ const AIDE_PLUGIN_BIN_NAME = "aide-plugin";
17
+
18
+ /** Check if a hook command belongs to aide (matches both global install and local dev paths). */
19
+ function isAideHookCommand(command?: string): boolean {
20
+ if (!command) return false;
21
+ return command.includes(AIDE_PLUGIN_BIN_NAME) || command.includes("index.ts hook");
22
+ }
23
+
24
+ /**
25
+ * Resolve the command prefix for hook/mcp commands.
26
+ *
27
+ * If `aide-plugin` is on PATH (global npm install), use it directly.
28
+ * Otherwise fall back to `bun <path-to-cli>` for local dev.
29
+ */
30
+ function resolvePluginCommand(): { bin: string; hookPrefix: string; mcpCommand: string; mcpArgs: string[]; wrapperEnv?: Record<string, string> } {
31
+ try {
32
+ const resolved = whichSync.sync(AIDE_PLUGIN_BIN_NAME, { nothrow: true });
33
+ if (resolved) {
34
+ return {
35
+ bin: AIDE_PLUGIN_BIN_NAME,
36
+ hookPrefix: `${AIDE_PLUGIN_BIN_NAME} hook`,
37
+ mcpCommand: AIDE_PLUGIN_BIN_NAME,
38
+ mcpArgs: ["mcp"],
39
+ };
40
+ }
41
+ } catch { /* not on PATH */ }
42
+
43
+ // Fallback: use bun with full path to the wrapper/CLI
44
+ const thisDir = dirname(fileURLToPath(import.meta.url));
45
+ const pluginRoot = resolve(thisDir, "..", "..");
46
+ const cliPath = join(pluginRoot, "src", "cli", "index.ts");
47
+ const wrapperPath = join(pluginRoot, "bin", "aide-wrapper.ts");
48
+
49
+ return {
50
+ bin: `bun ${cliPath}`,
51
+ hookPrefix: `bun ${cliPath} hook`,
52
+ mcpCommand: "bun",
53
+ mcpArgs: [wrapperPath, "mcp"],
54
+ wrapperEnv: { AIDE_PLUGIN_ROOT: pluginRoot },
55
+ };
56
+ }
57
+
58
+ // =============================================================================
59
+ // Config paths
60
+ // =============================================================================
61
+
62
+ export function getCodexGlobalConfigDir(): string {
63
+ return join(homedir(), ".codex");
64
+ }
65
+
66
+ export function getCodexProjectConfigDir(cwd?: string): string {
67
+ return join(cwd || process.cwd(), ".codex");
68
+ }
69
+
70
+ export function getCodexConfigTomlPath(scope: "user" | "project"): string {
71
+ const dir =
72
+ scope === "user"
73
+ ? getCodexGlobalConfigDir()
74
+ : getCodexProjectConfigDir();
75
+ return join(dir, "config.toml");
76
+ }
77
+
78
+ export function getCodexHooksJsonPath(scope: "user" | "project"): string {
79
+ const dir =
80
+ scope === "user"
81
+ ? getCodexGlobalConfigDir()
82
+ : getCodexProjectConfigDir();
83
+ return join(dir, "hooks.json");
84
+ }
85
+
86
+ // =============================================================================
87
+ // TOML config read/write
88
+ // =============================================================================
89
+
90
+ interface CodexTomlConfig {
91
+ mcp_servers?: Record<string, Record<string, unknown>>;
92
+ [key: string]: unknown;
93
+ }
94
+
95
+ function readCodexToml(path: string): CodexTomlConfig {
96
+ if (!existsSync(path)) return {};
97
+ try {
98
+ return TOML.parse(readFileSync(path, "utf-8")) as CodexTomlConfig;
99
+ } catch {
100
+ return {};
101
+ }
102
+ }
103
+
104
+ function writeCodexToml(path: string, config: CodexTomlConfig): void {
105
+ const dir = dirname(path);
106
+ mkdirSync(dir, { recursive: true });
107
+ writeFileSync(
108
+ path,
109
+ TOML.stringify(config as Record<string, unknown>) + "\n",
110
+ );
111
+ }
112
+
113
+ // =============================================================================
114
+ // hooks.json generation
115
+ // =============================================================================
116
+
117
+ interface CodexHookEntry {
118
+ type: string;
119
+ command: string;
120
+ timeout?: number;
121
+ statusMessage?: string;
122
+ }
123
+
124
+ interface CodexHookMatcher {
125
+ matcher: string;
126
+ hooks: CodexHookEntry[];
127
+ }
128
+
129
+ interface CodexHooksJson {
130
+ hooks: Record<string, CodexHookMatcher[]>;
131
+ }
132
+
133
+ function generateHooksJson(hookPrefix: string): CodexHooksJson {
134
+ return {
135
+ hooks: {
136
+ SessionStart: [
137
+ {
138
+ matcher: "*",
139
+ hooks: [
140
+ {
141
+ type: "command",
142
+ command: `${hookPrefix} session-start`,
143
+ timeout: 60,
144
+ statusMessage: "Initializing aide session",
145
+ },
146
+ ],
147
+ },
148
+ ],
149
+ UserPromptSubmit: [
150
+ {
151
+ matcher: "*",
152
+ hooks: [
153
+ {
154
+ type: "command",
155
+ command: `${hookPrefix} skill-injector`,
156
+ timeout: 5,
157
+ statusMessage: "Matching aide skills",
158
+ },
159
+ ],
160
+ },
161
+ ],
162
+ PreToolUse: [
163
+ {
164
+ matcher: "*",
165
+ hooks: [
166
+ {
167
+ type: "command",
168
+ command: `${hookPrefix} tool-tracker`,
169
+ timeout: 2,
170
+ },
171
+ {
172
+ type: "command",
173
+ command: `${hookPrefix} write-guard`,
174
+ timeout: 3,
175
+ },
176
+ {
177
+ type: "command",
178
+ command: `${hookPrefix} pre-tool-enforcer`,
179
+ timeout: 3,
180
+ },
181
+ {
182
+ type: "command",
183
+ command: `${hookPrefix} context-guard`,
184
+ timeout: 2,
185
+ },
186
+ ],
187
+ },
188
+ ],
189
+ PostToolUse: [
190
+ {
191
+ matcher: "*",
192
+ hooks: [
193
+ {
194
+ type: "command",
195
+ command: `${hookPrefix} comment-checker`,
196
+ timeout: 3,
197
+ },
198
+ {
199
+ type: "command",
200
+ command: `${hookPrefix} context-pruning`,
201
+ timeout: 3,
202
+ },
203
+ ],
204
+ },
205
+ ],
206
+ Stop: [
207
+ {
208
+ matcher: "*",
209
+ hooks: [
210
+ {
211
+ type: "command",
212
+ command: `${hookPrefix} persistence`,
213
+ timeout: 5,
214
+ },
215
+ {
216
+ type: "command",
217
+ command: `${hookPrefix} session-summary`,
218
+ timeout: 10,
219
+ },
220
+ {
221
+ type: "command",
222
+ command: `${hookPrefix} agent-cleanup`,
223
+ timeout: 5,
224
+ },
225
+ {
226
+ type: "command",
227
+ command: `${hookPrefix} session-end`,
228
+ timeout: 10,
229
+ },
230
+ ],
231
+ },
232
+ ],
233
+ },
234
+ };
235
+ }
236
+
237
+ // =============================================================================
238
+ // Install / uninstall
239
+ // =============================================================================
240
+
241
+ export function installCodex(scope: "user" | "project"): {
242
+ configWritten: boolean;
243
+ hooksWritten: boolean;
244
+ } {
245
+ const configPath = getCodexConfigTomlPath(scope);
246
+ const hooksPath = getCodexHooksJsonPath(scope);
247
+ let configWritten = false;
248
+ let hooksWritten = false;
249
+
250
+ const config = readCodexToml(configPath);
251
+ const resolved = resolvePluginCommand();
252
+ let configChanged = false;
253
+
254
+ // Enable hooks feature flag (required for Codex to process hooks.json)
255
+ const features = (config.features || {}) as Record<string, unknown>;
256
+ if (!features.codex_hooks) {
257
+ features.codex_hooks = true;
258
+ config.features = features;
259
+ configChanged = true;
260
+ }
261
+
262
+ // Add aide MCP server
263
+ const mcpServers = (config.mcp_servers || {}) as Record<
264
+ string,
265
+ Record<string, unknown>
266
+ >;
267
+
268
+ if (!mcpServers[MCP_SERVER_NAME]) {
269
+ mcpServers[MCP_SERVER_NAME] = {
270
+ command: resolved.mcpCommand,
271
+ args: resolved.mcpArgs,
272
+ env: {
273
+ AIDE_CODE_WATCH: "1",
274
+ AIDE_CODE_WATCH_DELAY: "30s",
275
+ ...(resolved.wrapperEnv || {}),
276
+ },
277
+ };
278
+ config.mcp_servers = mcpServers;
279
+ configChanged = true;
280
+ }
281
+
282
+ if (configChanged) {
283
+ writeCodexToml(configPath, config);
284
+ configWritten = true;
285
+ }
286
+
287
+ let existingHooks: CodexHooksJson | null = null;
288
+ if (existsSync(hooksPath)) {
289
+ try {
290
+ existingHooks = JSON.parse(
291
+ readFileSync(hooksPath, "utf-8"),
292
+ ) as CodexHooksJson;
293
+ } catch {
294
+ // Overwrite corrupt file
295
+ }
296
+ }
297
+
298
+ const hasAideHook = (event: string) =>
299
+ existingHooks?.hooks?.[event]?.some((m) =>
300
+ m.hooks?.some((h) => isAideHookCommand(h.command)),
301
+ ) ?? false;
302
+ const hasAideHooks = hasAideHook("SessionStart") && hasAideHook("Stop");
303
+
304
+ if (!hasAideHooks) {
305
+ const dir = dirname(hooksPath);
306
+ mkdirSync(dir, { recursive: true });
307
+
308
+ if (existingHooks?.hooks) {
309
+ // Merge: add aide hooks to existing hooks.json
310
+ const aideHooks = generateHooksJson(resolved.hookPrefix).hooks;
311
+ for (const [event, matchers] of Object.entries(aideHooks)) {
312
+ if (!existingHooks.hooks[event]) {
313
+ existingHooks.hooks[event] = matchers;
314
+ } else {
315
+ // Append aide matchers to existing event
316
+ existingHooks.hooks[event].push(...matchers);
317
+ }
318
+ }
319
+ writeFileSync(
320
+ hooksPath,
321
+ JSON.stringify(existingHooks, null, 2) + "\n",
322
+ );
323
+ } else {
324
+ writeFileSync(
325
+ hooksPath,
326
+ JSON.stringify(generateHooksJson(resolved.hookPrefix), null, 2) + "\n",
327
+ );
328
+ }
329
+ hooksWritten = true;
330
+ }
331
+
332
+ return { configWritten, hooksWritten };
333
+ }
334
+
335
+ export function uninstallCodex(scope: "user" | "project"): {
336
+ configRemoved: boolean;
337
+ hooksRemoved: boolean;
338
+ } {
339
+ const configPath = getCodexConfigTomlPath(scope);
340
+ const hooksPath = getCodexHooksJsonPath(scope);
341
+ let configRemoved = false;
342
+ let hooksRemoved = false;
343
+
344
+ // Remove aide MCP server from config.toml
345
+ if (existsSync(configPath)) {
346
+ const config = readCodexToml(configPath);
347
+ const mcpServers = (config.mcp_servers || {}) as Record<
348
+ string,
349
+ Record<string, unknown>
350
+ >;
351
+
352
+ if (mcpServers[MCP_SERVER_NAME]) {
353
+ delete mcpServers[MCP_SERVER_NAME];
354
+ if (Object.keys(mcpServers).length === 0) {
355
+ delete config.mcp_servers;
356
+ } else {
357
+ config.mcp_servers = mcpServers;
358
+ }
359
+ writeCodexToml(configPath, config);
360
+ configRemoved = true;
361
+ }
362
+ }
363
+
364
+ // Remove aide hooks from hooks.json
365
+ if (existsSync(hooksPath)) {
366
+ try {
367
+ const hooks = JSON.parse(
368
+ readFileSync(hooksPath, "utf-8"),
369
+ ) as CodexHooksJson;
370
+
371
+ if (hooks.hooks) {
372
+ let changed = false;
373
+ for (const [event, matchers] of Object.entries(hooks.hooks)) {
374
+ const filtered = matchers.filter(
375
+ (m) => !m.hooks?.some((h) => isAideHookCommand(h.command)),
376
+ );
377
+ if (filtered.length !== matchers.length) {
378
+ hooks.hooks[event] = filtered;
379
+ changed = true;
380
+ }
381
+ if (hooks.hooks[event].length === 0) {
382
+ delete hooks.hooks[event];
383
+ }
384
+ }
385
+ if (changed) {
386
+ writeFileSync(hooksPath, JSON.stringify(hooks, null, 2) + "\n");
387
+ hooksRemoved = true;
388
+ }
389
+ }
390
+ } catch {
391
+ // Leave corrupt file alone
392
+ }
393
+ }
394
+
395
+ return { configRemoved, hooksRemoved };
396
+ }
397
+
398
+ export function isCodexConfigured(scope: "user" | "project"): {
399
+ mcp: boolean;
400
+ hooks: boolean;
401
+ } {
402
+ const configPath = getCodexConfigTomlPath(scope);
403
+ const hooksPath = getCodexHooksJsonPath(scope);
404
+
405
+ let mcp = false;
406
+ if (existsSync(configPath)) {
407
+ const config = readCodexToml(configPath);
408
+ const mcpServers = (config.mcp_servers || {}) as Record<string, unknown>;
409
+ mcp = MCP_SERVER_NAME in mcpServers;
410
+ }
411
+
412
+ let hooks = false;
413
+ if (existsSync(hooksPath)) {
414
+ try {
415
+ const hooksJson = JSON.parse(
416
+ readFileSync(hooksPath, "utf-8"),
417
+ ) as CodexHooksJson;
418
+ hooks =
419
+ hooksJson.hooks?.SessionStart?.some((m) =>
420
+ m.hooks?.some((h) => isAideHookCommand(h.command)),
421
+ ) ?? false;
422
+ } catch {
423
+ // Corrupt file
424
+ }
425
+ }
426
+
427
+ return { mcp, hooks };
428
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Hook dispatcher for Codex CLI.
3
+ *
4
+ * Codex hooks.json calls `aide-plugin hook <name>` which dispatches to
5
+ * the appropriate hook script in src/hooks/. Input is normalized from
6
+ * stdin and passed through to the hook script.
7
+ *
8
+ * This avoids duplicating hook scripts — the same scripts work for both
9
+ * Claude Code (via plugin.json) and Codex CLI (via hooks.json).
10
+ */
11
+
12
+ import { execFileSync } from "child_process";
13
+ import { existsSync } from "fs";
14
+ import { dirname, join, resolve } from "path";
15
+ import { fileURLToPath } from "url";
16
+ import { readStdin, normalizeHookInput } from "../lib/hook-utils.js";
17
+
18
+ /** Maps hook CLI names to their script files in src/hooks/. */
19
+ const HOOK_MAP: Record<string, string> = {
20
+ "session-start": "session-start.ts",
21
+ "skill-injector": "skill-injector.ts",
22
+ "tool-tracker": "tool-tracker.ts",
23
+ "write-guard": "write-guard.ts",
24
+ "pre-tool-enforcer": "pre-tool-enforcer.ts",
25
+ "context-guard": "context-guard.ts",
26
+ "hud-updater": "hud-updater.ts",
27
+ "comment-checker": "comment-checker.ts",
28
+ "context-pruning": "context-pruning.ts",
29
+ "persistence": "persistence.ts",
30
+ "session-summary": "session-summary.ts",
31
+ "agent-cleanup": "agent-cleanup.ts",
32
+ "session-end": "session-end.ts",
33
+ };
34
+
35
+ export function listHooks(): string[] {
36
+ return Object.keys(HOOK_MAP);
37
+ }
38
+
39
+ /**
40
+ * Dispatch a hook by name.
41
+ *
42
+ * Reads stdin, normalizes field names for cross-platform compatibility,
43
+ * then spawns the hook script with the normalized input.
44
+ */
45
+ export async function dispatchHook(hookName: string): Promise<void> {
46
+ const scriptFile = HOOK_MAP[hookName];
47
+ if (!scriptFile) {
48
+ console.error(
49
+ `Unknown hook: ${hookName}\nAvailable hooks: ${Object.keys(HOOK_MAP).join(", ")}`,
50
+ );
51
+ process.exit(1);
52
+ }
53
+
54
+ const thisDir = dirname(fileURLToPath(import.meta.url));
55
+ const pluginRoot = resolve(thisDir, "..", "..");
56
+ const scriptPath = join(pluginRoot, "src", "hooks", scriptFile);
57
+
58
+ if (!existsSync(scriptPath)) {
59
+ console.error(`Hook script not found: ${scriptPath}`);
60
+ process.exit(1);
61
+ }
62
+
63
+ const env = {
64
+ ...process.env,
65
+ AIDE_PLUGIN_ROOT: pluginRoot,
66
+ AIDE_PLATFORM: "codex",
67
+ };
68
+
69
+ const rawInput = await readStdin();
70
+ const normalizedInput = normalizeHookInput(rawInput);
71
+
72
+ try {
73
+ execFileSync(process.execPath, [scriptPath], {
74
+ input: normalizedInput,
75
+ stdio: ["pipe", "inherit", "inherit"],
76
+ env,
77
+ timeout: 120_000,
78
+ });
79
+ } catch (err: unknown) {
80
+ if (err && typeof err === "object" && "status" in err) {
81
+ process.exit((err as { status: number }).status ?? 1);
82
+ }
83
+ throw err;
84
+ }
85
+ }
package/src/cli/index.ts CHANGED
@@ -1,45 +1,71 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * aide CLI — install/uninstall the aide plugin for OpenCode.
3
+ * aide CLI — install/uninstall the aide plugin for OpenCode and Codex CLI.
4
4
  *
5
5
  * Usage:
6
- * bunx @jmylchreest/aide-plugin install # Install globally for OpenCode
7
- * bunx @jmylchreest/aide-plugin uninstall # Remove from OpenCode config
8
- * bunx @jmylchreest/aide-plugin status # Show current installation status
9
- * bunx @jmylchreest/aide-plugin mcp # Start MCP server (used by OpenCode)
6
+ * aide-plugin install [--platform codex|opencode] # Install for detected platform
7
+ * aide-plugin uninstall [--platform codex|opencode] # Remove from platform config
8
+ * aide-plugin status [--platform codex|opencode] # Show installation status
9
+ * aide-plugin mcp # Start MCP server
10
+ * aide-plugin hook <name> # Dispatch hook (Codex CLI)
10
11
  */
11
12
 
12
13
  import { install } from "./install.js";
13
14
  import { uninstall } from "./uninstall.js";
14
15
  import { status } from "./status.js";
15
16
  import { mcp } from "./mcp.js";
17
+ import { dispatchHook, listHooks } from "./hook.js";
16
18
 
17
19
  const args = process.argv.slice(2);
18
20
  const command = args[0];
19
21
 
22
+ type Platform = "opencode" | "codex";
23
+
24
+ function detectPlatform(): Platform {
25
+ const idx = args.indexOf("--platform");
26
+ if (idx !== -1 && args[idx + 1]) {
27
+ const val = args[idx + 1] as string;
28
+ if (val === "codex" || val === "opencode") return val;
29
+ console.error(`Unknown platform: ${val}. Use "opencode" or "codex".`);
30
+ process.exit(1);
31
+ }
32
+
33
+ if (process.env.CODEX_HOME || process.env.CODEX_SANDBOX_TYPE) return "codex";
34
+
35
+ return "opencode";
36
+ }
37
+
20
38
  function printUsage(): void {
21
- console.log(`aide - AI Development Environment plugin for OpenCode
39
+ console.log(`aide - AI Development Environment plugin for OpenCode and Codex CLI
22
40
 
23
41
  Usage:
24
- aide-plugin install Install aide plugin globally for OpenCode
25
- aide-plugin uninstall Remove aide plugin from OpenCode config
42
+ aide-plugin install Install aide plugin (auto-detects platform)
43
+ aide-plugin uninstall Remove aide plugin from platform config
26
44
  aide-plugin status Show current installation status
27
45
  aide-plugin mcp Start MCP server (delegates to aide-wrapper)
46
+ aide-plugin hook <name> Dispatch a hook by name (used by Codex hooks.json)
28
47
  aide-plugin --help Show this help message
29
48
 
30
49
  Options:
31
- --project Apply to project-level opencode.json instead of global
32
- --no-mcp Skip MCP server registration (plugin only)
50
+ --platform codex|opencode Target platform (auto-detected if omitted)
51
+ --project Apply to project-level config instead of global
52
+ --no-mcp Skip MCP server registration (plugin only)
53
+
54
+ Available hooks:
55
+ ${listHooks().join(", ")}
33
56
 
34
57
  Examples:
35
58
  bunx @jmylchreest/aide-plugin install
36
- aide-plugin install --project`);
59
+ aide-plugin install --platform codex
60
+ aide-plugin install --project
61
+ aide-plugin hook session-start`);
37
62
  }
38
63
 
39
64
  async function main(): Promise<void> {
40
65
  const flags = {
41
66
  project: args.includes("--project"),
42
67
  noMcp: args.includes("--no-mcp"),
68
+ platform: detectPlatform(),
43
69
  };
44
70
 
45
71
  switch (command) {
@@ -50,11 +76,22 @@ async function main(): Promise<void> {
50
76
  await uninstall(flags);
51
77
  break;
52
78
  case "status":
53
- await status();
79
+ await status(flags);
54
80
  break;
55
81
  case "mcp":
56
82
  await mcp(args.slice(1));
57
83
  break;
84
+ case "hook": {
85
+ const hookName = args[1];
86
+ if (!hookName) {
87
+ console.error(
88
+ `Missing hook name.\nAvailable hooks: ${listHooks().join(", ")}`,
89
+ );
90
+ process.exit(1);
91
+ }
92
+ await dispatchHook(hookName);
93
+ break;
94
+ }
58
95
  case "--help":
59
96
  case "-h":
60
97
  case "help":