@evo-dev/evodev 0.0.1-alpha → 0.0.1-alpha.1
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/dist/.claude-plugin/marketplace.json +2 -2
- package/dist/assets/agents/review/code-reviewer/examples.md +1 -1
- package/dist/assets/agents/review/code-reviewer/prompt.md +1 -1
- package/dist/assets/agents/review/code-reviewer/verification.md +1 -1
- package/dist/assets/skills/coding/knowledge-distillation/SKILL.md +248 -0
- package/dist/assets/skills/coding/knowledge-distillation/manifest.json +10 -0
- package/dist/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +122 -0
- package/dist/assets/workflows/rd-bug-fix/WORKFLOW.json +1 -1
- package/dist/assets/workflows/rd-code-review/WORKFLOW.json +1 -1
- package/dist/assets/workflows/rd-docs-update/WORKFLOW.json +1 -1
- package/dist/assets/workflows/rd-feature-implementation/WORKFLOW.json +1 -1
- package/dist/assets/workflows/rd-refactor/WORKFLOW.json +1 -1
- package/dist/assets/workflows/rd-release-readiness/WORKFLOW.json +1 -1
- package/dist/assets/workflows/rd-security-boundary-review/WORKFLOW.json +2 -2
- package/dist/assets/workflows/rd-test-generation/WORKFLOW.json +1 -1
- package/dist/index.js +13114 -6432
- package/dist/plugins/evodev/.claude-plugin/plugin.json +2 -2
- package/dist/plugins/evodev/.codex-plugin/plugin.json +8 -6
- package/dist/plugins/evodev/.mcp.json +6 -0
- package/dist/plugins/evodev/hooks/codex-hooks.json +10 -10
- package/dist/plugins/evodev/hooks/codex.ts +393 -34
- package/dist/plugins/evodev/hooks/hooks.json +18 -18
- package/dist/plugins/evodev/hooks/hooks.ts +268 -10
- package/dist/plugins/evodev/hooks/index.ts +15 -0
- package/dist/plugins/evodev/hooks/paths.ts +44 -1
- package/dist/plugins/evodev/hooks/plugin.ts +160 -9
- package/dist/plugins/evodev/hooks/runtime.ts +146 -31
- package/dist/plugins/evodev/hooks/transform-agent.ts +30 -0
- package/dist/plugins/evodev/hooks/workspace-core.ts +181 -0
- package/dist/plugins/evodev/package.json +3 -3
- package/dist/plugins/evodev/skills/engineering-discipline/SKILL.md +63 -0
- package/dist/plugins/evodev/skills/engineering-discipline/anti-patterns.md +21 -0
- package/dist/plugins/evodev/skills/engineering-discipline/examples.md +19 -0
- package/dist/plugins/evodev/skills/engineering-discipline/verification.md +11 -0
- package/dist/plugins/evodev/skills/knowledge-distillation/SKILL.md +248 -0
- package/dist/plugins/evodev/skills/knowledge-distillation/references/knowledge-distillation-methods.md +122 -0
- package/package.json +3 -6
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "EvoDev
|
|
2
|
+
"description": "EvoDev observes Claude Code lifecycle events through evodev hook runtime for local trace and advisory workflow suggestions.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "
|
|
9
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
10
10
|
"timeout": 30
|
|
11
11
|
}
|
|
12
12
|
]
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"hooks": [
|
|
18
18
|
{
|
|
19
19
|
"type": "command",
|
|
20
|
-
"command": "
|
|
20
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
21
21
|
"timeout": 30
|
|
22
22
|
}
|
|
23
23
|
]
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"hooks": [
|
|
30
30
|
{
|
|
31
31
|
"type": "command",
|
|
32
|
-
"command": "
|
|
32
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
33
33
|
"timeout": 30
|
|
34
34
|
}
|
|
35
35
|
]
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"hooks": [
|
|
42
42
|
{
|
|
43
43
|
"type": "command",
|
|
44
|
-
"command": "
|
|
44
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
45
45
|
"timeout": 30
|
|
46
46
|
}
|
|
47
47
|
]
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"hooks": [
|
|
54
54
|
{
|
|
55
55
|
"type": "command",
|
|
56
|
-
"command": "
|
|
56
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
57
57
|
"timeout": 30
|
|
58
58
|
}
|
|
59
59
|
]
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"hooks": [
|
|
66
66
|
{
|
|
67
67
|
"type": "command",
|
|
68
|
-
"command": "
|
|
68
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
69
69
|
"timeout": 30
|
|
70
70
|
}
|
|
71
71
|
]
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"hooks": [
|
|
77
77
|
{
|
|
78
78
|
"type": "command",
|
|
79
|
-
"command": "
|
|
79
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
80
80
|
"timeout": 30
|
|
81
81
|
}
|
|
82
82
|
]
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"hooks": [
|
|
89
89
|
{
|
|
90
90
|
"type": "command",
|
|
91
|
-
"command": "
|
|
91
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
92
92
|
"timeout": 30
|
|
93
93
|
}
|
|
94
94
|
]
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"hooks": [
|
|
100
100
|
{
|
|
101
101
|
"type": "command",
|
|
102
|
-
"command": "
|
|
102
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
103
103
|
"timeout": 30
|
|
104
104
|
}
|
|
105
105
|
]
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"hooks": [
|
|
111
111
|
{
|
|
112
112
|
"type": "command",
|
|
113
|
-
"command": "
|
|
113
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
114
114
|
"timeout": 30
|
|
115
115
|
}
|
|
116
116
|
]
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"hooks": [
|
|
122
122
|
{
|
|
123
123
|
"type": "command",
|
|
124
|
-
"command": "
|
|
124
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
125
125
|
"timeout": 30
|
|
126
126
|
}
|
|
127
127
|
]
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"hooks": [
|
|
133
133
|
{
|
|
134
134
|
"type": "command",
|
|
135
|
-
"command": "
|
|
135
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
136
136
|
"timeout": 30
|
|
137
137
|
}
|
|
138
138
|
]
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
"hooks": [
|
|
144
144
|
{
|
|
145
145
|
"type": "command",
|
|
146
|
-
"command": "
|
|
146
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
147
147
|
"timeout": 30
|
|
148
148
|
}
|
|
149
149
|
]
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
"hooks": [
|
|
155
155
|
{
|
|
156
156
|
"type": "command",
|
|
157
|
-
"command": "
|
|
157
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
158
158
|
"timeout": 30
|
|
159
159
|
}
|
|
160
160
|
]
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"hooks": [
|
|
166
166
|
{
|
|
167
167
|
"type": "command",
|
|
168
|
-
"command": "
|
|
168
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
169
169
|
"timeout": 30
|
|
170
170
|
}
|
|
171
171
|
]
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
"hooks": [
|
|
177
177
|
{
|
|
178
178
|
"type": "command",
|
|
179
|
-
"command": "
|
|
179
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
180
180
|
"timeout": 30
|
|
181
181
|
}
|
|
182
182
|
]
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
"hooks": [
|
|
188
188
|
{
|
|
189
189
|
"type": "command",
|
|
190
|
-
"command": "
|
|
190
|
+
"command": "bun run \"${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts\" hook runtime --target claude",
|
|
191
191
|
"timeout": 5
|
|
192
192
|
}
|
|
193
193
|
]
|
|
@@ -1,16 +1,28 @@
|
|
|
1
|
+
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
1
3
|
import {
|
|
2
4
|
type CanonicalHookEventType,
|
|
3
5
|
type HookEventV1,
|
|
4
6
|
type HookInputEventType,
|
|
5
7
|
normalizeHookEvent,
|
|
6
8
|
} from "@evo-dev/core";
|
|
7
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type ClaudePaths,
|
|
11
|
+
type CodexPaths,
|
|
12
|
+
resolveClaudePaths,
|
|
13
|
+
resolveCodexPaths,
|
|
14
|
+
} from "./paths.ts";
|
|
8
15
|
|
|
9
16
|
export interface ClaudeHookInstallDryRunInput {
|
|
10
17
|
homeDir: string;
|
|
11
18
|
paths?: ClaudePaths;
|
|
12
19
|
}
|
|
13
20
|
|
|
21
|
+
export interface CodexHookInstallDryRunInput {
|
|
22
|
+
homeDir: string;
|
|
23
|
+
paths?: CodexPaths;
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
export interface ClaudeHookInstallDryRunPlan {
|
|
15
27
|
target: "claude";
|
|
16
28
|
settingsPath: string;
|
|
@@ -20,7 +32,27 @@ export interface ClaudeHookInstallDryRunPlan {
|
|
|
20
32
|
reason: string;
|
|
21
33
|
}>;
|
|
22
34
|
warnings: string[];
|
|
23
|
-
|
|
35
|
+
advisories: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CodexHookInstallDryRunPlan {
|
|
39
|
+
target: "codex";
|
|
40
|
+
configPath: string;
|
|
41
|
+
plannedWrites: Array<{
|
|
42
|
+
action: "merge-with-backup" | "create";
|
|
43
|
+
targetPath: string;
|
|
44
|
+
reason: string;
|
|
45
|
+
}>;
|
|
46
|
+
warnings: string[];
|
|
47
|
+
advisories: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface HookInstallResult {
|
|
51
|
+
target: "claude" | "codex";
|
|
52
|
+
targetPath: string;
|
|
53
|
+
backupPath: string | null;
|
|
54
|
+
changed: boolean;
|
|
55
|
+
warnings: string[];
|
|
24
56
|
}
|
|
25
57
|
|
|
26
58
|
export interface ClaudeHookCommand {
|
|
@@ -44,9 +76,10 @@ export interface CodexHookConfig {
|
|
|
44
76
|
}
|
|
45
77
|
|
|
46
78
|
export const CLAUDE_HOOK_RUNTIME_COMMAND =
|
|
47
|
-
'
|
|
79
|
+
'bun run "${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts" hook runtime --target claude';
|
|
48
80
|
export const CODEX_HOOK_RUNTIME_COMMAND =
|
|
49
|
-
'
|
|
81
|
+
'bun run "${PLUGIN_ROOT}/hooks/runtime.ts" hook runtime --target codex';
|
|
82
|
+
const CODEX_FEATURE_FLAGS = ["plugins", "plugin_hooks", "hooks"] as const;
|
|
50
83
|
const CLAUDE_TOOL_EVENTS = new Set<CanonicalHookEventType>([
|
|
51
84
|
"PreToolUse",
|
|
52
85
|
"PermissionRequest",
|
|
@@ -107,17 +140,73 @@ export function planClaudeHookInstallDryRun(
|
|
|
107
140
|
action: "merge-with-backup",
|
|
108
141
|
targetPath: paths.settingsPath,
|
|
109
142
|
reason:
|
|
110
|
-
"
|
|
143
|
+
"would add or replace EvoDev-managed user-level hook entries without touching user-owned hooks",
|
|
111
144
|
},
|
|
112
145
|
],
|
|
113
|
-
warnings: [
|
|
114
|
-
|
|
115
|
-
|
|
146
|
+
warnings: ["Project .claude, .codex, CLAUDE.md, and AGENTS.md are not targeted."],
|
|
147
|
+
advisories: [],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function planCodexHookInstallDryRun(
|
|
152
|
+
input: CodexHookInstallDryRunInput,
|
|
153
|
+
): CodexHookInstallDryRunPlan {
|
|
154
|
+
const paths = input.paths ?? resolveCodexPaths({ homeDir: input.homeDir });
|
|
155
|
+
return {
|
|
156
|
+
target: "codex",
|
|
157
|
+
configPath: paths.configPath,
|
|
158
|
+
plannedWrites: [
|
|
159
|
+
{
|
|
160
|
+
action: "merge-with-backup",
|
|
161
|
+
targetPath: paths.configPath,
|
|
162
|
+
reason:
|
|
163
|
+
"would enable missing Codex plugin hook feature flags while preserving explicit user-owned false values",
|
|
164
|
+
},
|
|
116
165
|
],
|
|
117
|
-
|
|
166
|
+
warnings: ["Codex hook definitions are supplied by the EvoDev Codex plugin manifest."],
|
|
167
|
+
advisories: [],
|
|
118
168
|
};
|
|
119
169
|
}
|
|
120
170
|
|
|
171
|
+
export async function installClaudeHooks(input: {
|
|
172
|
+
homeDir: string;
|
|
173
|
+
paths?: ClaudePaths;
|
|
174
|
+
now?: string;
|
|
175
|
+
}): Promise<HookInstallResult> {
|
|
176
|
+
const paths = input.paths ?? resolveClaudePaths({ homeDir: input.homeDir });
|
|
177
|
+
const existing = await readTextIfExists(paths.settingsPath);
|
|
178
|
+
const next = mergeClaudeSettingsHooks(existing, createClaudeHookConfig().hooks);
|
|
179
|
+
return writeMergedConfig({
|
|
180
|
+
target: "claude",
|
|
181
|
+
path: paths.settingsPath,
|
|
182
|
+
existing,
|
|
183
|
+
next,
|
|
184
|
+
now: input.now,
|
|
185
|
+
warnings: [],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function installCodexHooks(input: {
|
|
190
|
+
homeDir: string;
|
|
191
|
+
paths?: CodexPaths;
|
|
192
|
+
now?: string;
|
|
193
|
+
}): Promise<HookInstallResult> {
|
|
194
|
+
const paths = input.paths ?? resolveCodexPaths({ homeDir: input.homeDir });
|
|
195
|
+
const existing = await readTextIfExists(paths.configPath);
|
|
196
|
+
const merged = mergeCodexFeatureFlags(existing);
|
|
197
|
+
return writeMergedConfig({
|
|
198
|
+
target: "codex",
|
|
199
|
+
path: paths.configPath,
|
|
200
|
+
existing,
|
|
201
|
+
next: merged.content,
|
|
202
|
+
now: input.now,
|
|
203
|
+
warnings: [
|
|
204
|
+
"Codex hook definitions are loaded from the installed EvoDev Codex plugin.",
|
|
205
|
+
...merged.warnings,
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
121
210
|
export function normalizeClaudeHookPayload(input: {
|
|
122
211
|
type: HookInputEventType;
|
|
123
212
|
payload: Record<string, unknown>;
|
|
@@ -194,7 +283,7 @@ export function createClaudeHookConfig(
|
|
|
194
283
|
}
|
|
195
284
|
return {
|
|
196
285
|
description:
|
|
197
|
-
"EvoDev
|
|
286
|
+
"EvoDev observes Claude Code lifecycle events through evodev hook runtime for local trace and advisory workflow suggestions.",
|
|
198
287
|
hooks,
|
|
199
288
|
};
|
|
200
289
|
}
|
|
@@ -215,3 +304,172 @@ export function createCodexHookConfig(
|
|
|
215
304
|
}
|
|
216
305
|
return { hooks };
|
|
217
306
|
}
|
|
307
|
+
|
|
308
|
+
async function readTextIfExists(path: string): Promise<string | null> {
|
|
309
|
+
try {
|
|
310
|
+
return await readFile(path, "utf8");
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if (isNotFoundError(error)) return null;
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function writeMergedConfig(input: {
|
|
318
|
+
target: "claude" | "codex";
|
|
319
|
+
path: string;
|
|
320
|
+
existing: string | null;
|
|
321
|
+
next: string;
|
|
322
|
+
now?: string;
|
|
323
|
+
warnings: string[];
|
|
324
|
+
}): Promise<HookInstallResult> {
|
|
325
|
+
if (input.existing === input.next) {
|
|
326
|
+
return {
|
|
327
|
+
target: input.target,
|
|
328
|
+
targetPath: input.path,
|
|
329
|
+
backupPath: null,
|
|
330
|
+
changed: false,
|
|
331
|
+
warnings: input.warnings,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
await mkdir(dirname(input.path), { recursive: true });
|
|
336
|
+
let backupPath: string | null = null;
|
|
337
|
+
if (input.existing !== null && (await fileExists(input.path))) {
|
|
338
|
+
backupPath = `${input.path}.evodev-${backupTimestamp(input.now)}.bak`;
|
|
339
|
+
await copyFile(input.path, backupPath);
|
|
340
|
+
}
|
|
341
|
+
await writeFile(input.path, input.next, "utf8");
|
|
342
|
+
return {
|
|
343
|
+
target: input.target,
|
|
344
|
+
targetPath: input.path,
|
|
345
|
+
backupPath,
|
|
346
|
+
changed: true,
|
|
347
|
+
warnings: input.warnings,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function mergeClaudeSettingsHooks(
|
|
352
|
+
existing: string | null,
|
|
353
|
+
evodevHooks: ClaudeHookConfig["hooks"],
|
|
354
|
+
): string {
|
|
355
|
+
const settings = parseJsonConfig(existing, "Claude settings");
|
|
356
|
+
const currentHooks = isRecord(settings.hooks) ? settings.hooks : {};
|
|
357
|
+
const nextHooks: Record<string, unknown> = { ...currentHooks };
|
|
358
|
+
|
|
359
|
+
for (const [eventName, groups] of Object.entries(evodevHooks)) {
|
|
360
|
+
const existingGroups = Array.isArray(currentHooks[eventName]) ? currentHooks[eventName] : [];
|
|
361
|
+
nextHooks[eventName] = [
|
|
362
|
+
...existingGroups.filter((group) => !isEvoDevHookGroup(group, "claude")),
|
|
363
|
+
...(groups ?? []),
|
|
364
|
+
];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return `${JSON.stringify({ ...settings, hooks: nextHooks }, null, 2)}\n`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function parseJsonConfig(existing: string | null, label: string): Record<string, unknown> {
|
|
371
|
+
if (existing === null || existing.trim() === "") return {};
|
|
372
|
+
const parsed = JSON.parse(existing) as unknown;
|
|
373
|
+
if (!isRecord(parsed)) throw new Error(`${label} must be a JSON object.`);
|
|
374
|
+
return parsed;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function isEvoDevHookGroup(group: unknown, target: "claude" | "codex"): boolean {
|
|
378
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) return false;
|
|
379
|
+
return group.hooks.some((hook) => {
|
|
380
|
+
if (!isRecord(hook) || typeof hook.command !== "string") return false;
|
|
381
|
+
return hook.command.includes(`hook runtime --target ${target}`);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function mergeCodexFeatureFlags(existing: string | null): { content: string; warnings: string[] } {
|
|
386
|
+
const source = existing ?? "";
|
|
387
|
+
const lines = source === "" ? [] : source.replace(/\n$/, "").split("\n");
|
|
388
|
+
const section = findTomlSection(lines, "features");
|
|
389
|
+
if (section === null) {
|
|
390
|
+
const prefix = lines.length === 0 ? [] : [...lines, ""];
|
|
391
|
+
return {
|
|
392
|
+
content: [
|
|
393
|
+
...prefix,
|
|
394
|
+
"[features]",
|
|
395
|
+
...CODEX_FEATURE_FLAGS.map((flag) => `${flag} = true`),
|
|
396
|
+
"",
|
|
397
|
+
].join("\n"),
|
|
398
|
+
warnings: [],
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const nextLines = [...lines];
|
|
403
|
+
const seen = new Set<string>();
|
|
404
|
+
const preservedFalseFlags: string[] = [];
|
|
405
|
+
for (let index = section.start + 1; index < section.end; index += 1) {
|
|
406
|
+
const match = nextLines[index]?.match(/^(\s*)([A-Za-z0-9_-]+)(\s*=\s*)(.*)$/);
|
|
407
|
+
if (match === null) continue;
|
|
408
|
+
const key = match[2];
|
|
409
|
+
if ((CODEX_FEATURE_FLAGS as readonly string[]).includes(key)) {
|
|
410
|
+
seen.add(key);
|
|
411
|
+
if (isTomlBooleanFalse(match[4])) {
|
|
412
|
+
preservedFalseFlags.push(key);
|
|
413
|
+
} else {
|
|
414
|
+
nextLines[index] = `${match[1]}${key}${match[3]}true`;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const missing = CODEX_FEATURE_FLAGS.filter((flag) => !seen.has(flag));
|
|
420
|
+
nextLines.splice(section.end, 0, ...missing.map((flag) => `${flag} = true`));
|
|
421
|
+
return {
|
|
422
|
+
content: `${nextLines.join("\n")}\n`,
|
|
423
|
+
warnings:
|
|
424
|
+
preservedFalseFlags.length === 0
|
|
425
|
+
? []
|
|
426
|
+
: [
|
|
427
|
+
`Explicitly disabled Codex feature flags were preserved: ${preservedFalseFlags.join(", ")}.`,
|
|
428
|
+
],
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function isTomlBooleanFalse(value: string): boolean {
|
|
433
|
+
return /^false(?:\s*(?:#.*)?)?$/i.test(value.trim());
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function findTomlSection(lines: string[], name: string): { start: number; end: number } | null {
|
|
437
|
+
const header = `[${name}]`;
|
|
438
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
439
|
+
if (start < 0) return null;
|
|
440
|
+
let end = lines.length;
|
|
441
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
442
|
+
if (/^\s*\[[^\]]+\]\s*$/.test(lines[index] ?? "")) {
|
|
443
|
+
end = index;
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { start, end };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function backupTimestamp(value?: string): string {
|
|
451
|
+
const source = value ?? new Date().toISOString();
|
|
452
|
+
return source.replace(/[^0-9A-Za-z]/g, "").slice(0, 20) || "now";
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
456
|
+
try {
|
|
457
|
+
return (await stat(path)).isFile();
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (isNotFoundError(error)) return false;
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function isNotFoundError(error: unknown): boolean {
|
|
465
|
+
return (
|
|
466
|
+
typeof error === "object" &&
|
|
467
|
+
error !== null &&
|
|
468
|
+
"code" in error &&
|
|
469
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
474
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
475
|
+
}
|
|
@@ -2,15 +2,21 @@ export {
|
|
|
2
2
|
type CodexCommandResult,
|
|
3
3
|
type CodexCommandRunner,
|
|
4
4
|
type CodexPluginOptions,
|
|
5
|
+
type CodexSyncFileSystem,
|
|
5
6
|
codexPlugin,
|
|
6
7
|
createCodexPlugin,
|
|
7
8
|
} from "./codex.ts";
|
|
8
9
|
|
|
9
10
|
export { type ClaudeCommandResult, type ClaudeCommandRunner, detectClaudeCode } from "./detect.ts";
|
|
10
11
|
export {
|
|
12
|
+
type CodexPaths,
|
|
13
|
+
type ResolveCodexPathsOptions,
|
|
11
14
|
type ClaudePaths,
|
|
12
15
|
type ResolveClaudePathsOptions,
|
|
16
|
+
assertSafeCodexAssetName,
|
|
13
17
|
assertSafeClaudeAssetName,
|
|
18
|
+
resolveCodexAgentTargetPath,
|
|
19
|
+
resolveCodexPaths,
|
|
14
20
|
resolveClaudeAgentTargetPath,
|
|
15
21
|
resolveClaudePaths,
|
|
16
22
|
resolveClaudeSkillTargetPath,
|
|
@@ -23,13 +29,19 @@ export {
|
|
|
23
29
|
type ClaudeHookInstallDryRunInput,
|
|
24
30
|
type ClaudeHookInstallDryRunPlan,
|
|
25
31
|
type ClaudeHookMatcherGroup,
|
|
32
|
+
type CodexHookInstallDryRunInput,
|
|
33
|
+
type CodexHookInstallDryRunPlan,
|
|
26
34
|
type CodexHookConfig,
|
|
35
|
+
type HookInstallResult,
|
|
27
36
|
createCodexHookConfig,
|
|
28
37
|
createClaudeHookConfig,
|
|
38
|
+
installClaudeHooks,
|
|
39
|
+
installCodexHooks,
|
|
29
40
|
normalizeCodexHookPayload,
|
|
30
41
|
normalizeCodexRuntimeHookPayload,
|
|
31
42
|
normalizeClaudeHookPayload,
|
|
32
43
|
normalizeClaudeRuntimeHookPayload,
|
|
44
|
+
planCodexHookInstallDryRun,
|
|
33
45
|
planClaudeHookInstallDryRun,
|
|
34
46
|
} from "./hooks.ts";
|
|
35
47
|
export {
|
|
@@ -43,6 +55,9 @@ export {
|
|
|
43
55
|
export {
|
|
44
56
|
type ClaudeAgentTransformInput,
|
|
45
57
|
type ClaudeAgentTransformResult,
|
|
58
|
+
type CodexAgentTransformInput,
|
|
59
|
+
type CodexAgentTransformResult,
|
|
60
|
+
transformCodexAgent,
|
|
46
61
|
transformClaudeAgent,
|
|
47
62
|
} from "./transform-agent.ts";
|
|
48
63
|
export {
|
|
@@ -6,11 +6,24 @@ export interface ClaudePaths {
|
|
|
6
6
|
agentsDir: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface CodexPaths {
|
|
10
|
+
homeDir: string;
|
|
11
|
+
codexDir: string;
|
|
12
|
+
configPath: string;
|
|
13
|
+
hooksPath: string;
|
|
14
|
+
agentsDir: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
export interface ResolveClaudePathsOptions {
|
|
10
18
|
homeDir: string;
|
|
11
19
|
claudeDir?: string;
|
|
12
20
|
}
|
|
13
21
|
|
|
22
|
+
export interface ResolveCodexPathsOptions {
|
|
23
|
+
homeDir: string;
|
|
24
|
+
codexDir?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
export function resolveClaudePaths(options: ResolveClaudePathsOptions): ClaudePaths {
|
|
15
28
|
const homeDir = stripTrailingSlash(options.homeDir);
|
|
16
29
|
const claudeDir = stripTrailingSlash(options.claudeDir ?? `${homeDir}/.claude`);
|
|
@@ -24,6 +37,19 @@ export function resolveClaudePaths(options: ResolveClaudePathsOptions): ClaudePa
|
|
|
24
37
|
};
|
|
25
38
|
}
|
|
26
39
|
|
|
40
|
+
export function resolveCodexPaths(options: ResolveCodexPathsOptions): CodexPaths {
|
|
41
|
+
const homeDir = stripTrailingSlash(options.homeDir);
|
|
42
|
+
const codexDir = stripTrailingSlash(options.codexDir ?? `${homeDir}/.codex`);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
homeDir,
|
|
46
|
+
codexDir,
|
|
47
|
+
configPath: `${codexDir}/config.toml`,
|
|
48
|
+
hooksPath: `${codexDir}/hooks.json`,
|
|
49
|
+
agentsDir: `${codexDir}/agents`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
27
53
|
export function resolveClaudeSkillTargetPath(skillName: string, paths: ClaudePaths): string {
|
|
28
54
|
const safeSkillName = assertSafeClaudeAssetName(skillName, "skillName");
|
|
29
55
|
return `${paths.skillsDir}/${safeSkillName}/SKILL.md`;
|
|
@@ -34,10 +60,27 @@ export function resolveClaudeAgentTargetPath(agentName: string, paths: ClaudePat
|
|
|
34
60
|
return `${paths.agentsDir}/${safeAgentName}.md`;
|
|
35
61
|
}
|
|
36
62
|
|
|
63
|
+
export function resolveCodexAgentTargetPath(agentName: string, paths: CodexPaths): string {
|
|
64
|
+
const safeAgentName = assertSafeCodexAssetName(agentName, "agentName");
|
|
65
|
+
return `${paths.agentsDir}/${safeAgentName}.toml`;
|
|
66
|
+
}
|
|
67
|
+
|
|
37
68
|
export function assertSafeClaudeAssetName(name: string, label = "name"): string {
|
|
69
|
+
return assertSafeCodeAgentAssetName(name, "Claude", label);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function assertSafeCodexAssetName(name: string, label = "name"): string {
|
|
73
|
+
return assertSafeCodeAgentAssetName(name, "Codex", label);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertSafeCodeAgentAssetName(
|
|
77
|
+
name: string,
|
|
78
|
+
target: "Claude" | "Codex",
|
|
79
|
+
label: string,
|
|
80
|
+
): string {
|
|
38
81
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || name === "." || name === "..") {
|
|
39
82
|
throw new Error(
|
|
40
|
-
`Invalid
|
|
83
|
+
`Invalid ${target} asset ${label}: must be a single safe path segment using letters, numbers, dot, underscore, or hyphen`,
|
|
41
84
|
);
|
|
42
85
|
}
|
|
43
86
|
|