@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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jmylchreest/aide-plugin",
|
|
3
|
-
"version": "0.0.
|
|
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
|
+
}
|
package/src/cli/hook.ts
ADDED
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
25
|
-
aide-plugin uninstall Remove aide plugin from
|
|
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
|
-
--
|
|
32
|
-
--
|
|
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 --
|
|
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":
|