@evo-dev/evodev 0.0.1-alpha
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/.agents/plugins/marketplace.json +20 -0
- package/dist/.claude-plugin/marketplace.json +22 -0
- package/dist/assets/agents/review/code-reviewer/examples.md +19 -0
- package/dist/assets/agents/review/code-reviewer/manifest.json +10 -0
- package/dist/assets/agents/review/code-reviewer/prompt.md +59 -0
- package/dist/assets/agents/review/code-reviewer/verification.md +11 -0
- package/dist/assets/skills/coding/engineering-discipline/SKILL.md +63 -0
- package/dist/assets/skills/coding/engineering-discipline/anti-patterns.md +21 -0
- package/dist/assets/skills/coding/engineering-discipline/examples.md +19 -0
- package/dist/assets/skills/coding/engineering-discipline/manifest.json +10 -0
- package/dist/assets/skills/coding/engineering-discipline/verification.md +11 -0
- package/dist/assets/workflows/rd-bug-fix/WORKFLOW.json +45 -0
- package/dist/assets/workflows/rd-code-review/WORKFLOW.json +45 -0
- package/dist/assets/workflows/rd-docs-update/WORKFLOW.json +45 -0
- package/dist/assets/workflows/rd-feature-implementation/WORKFLOW.json +45 -0
- package/dist/assets/workflows/rd-refactor/WORKFLOW.json +45 -0
- package/dist/assets/workflows/rd-release-readiness/WORKFLOW.json +49 -0
- package/dist/assets/workflows/rd-security-boundary-review/WORKFLOW.json +45 -0
- package/dist/assets/workflows/rd-test-generation/WORKFLOW.json +45 -0
- package/dist/evodev +11 -0
- package/dist/index.js +11283 -0
- package/dist/plugins/evodev/.claude-plugin/plugin.json +9 -0
- package/dist/plugins/evodev/.codex-plugin/plugin.json +19 -0
- package/dist/plugins/evodev/hooks/codex-hooks.json +121 -0
- package/dist/plugins/evodev/hooks/codex.ts +257 -0
- package/dist/plugins/evodev/hooks/command-runner.ts +30 -0
- package/dist/plugins/evodev/hooks/detect.ts +101 -0
- package/dist/plugins/evodev/hooks/hooks.json +197 -0
- package/dist/plugins/evodev/hooks/hooks.ts +217 -0
- package/dist/plugins/evodev/hooks/index.ts +52 -0
- package/dist/plugins/evodev/hooks/paths.ts +53 -0
- package/dist/plugins/evodev/hooks/plugin.ts +513 -0
- package/dist/plugins/evodev/hooks/runtime.ts +113 -0
- package/dist/plugins/evodev/hooks/transform-agent.ts +30 -0
- package/dist/plugins/evodev/hooks/transform-skill.ts +18 -0
- package/dist/plugins/evodev/package.json +16 -0
- package/package.json +29 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import type {
|
|
5
|
+
CodeAgentCapabilities,
|
|
6
|
+
CodeAgentPlugin,
|
|
7
|
+
DoctorCheck,
|
|
8
|
+
DoctorContext,
|
|
9
|
+
PluginDetectionResult,
|
|
10
|
+
PluginInstallInput,
|
|
11
|
+
PluginInstallResult,
|
|
12
|
+
SyncAgentsInput,
|
|
13
|
+
SyncResult,
|
|
14
|
+
SyncSkillsInput,
|
|
15
|
+
} from "@evo-dev/core/plugins";
|
|
16
|
+
import { runNodeCommand } from "./command-runner.ts";
|
|
17
|
+
import { type ClaudeCommandRunner, detectClaudeCode } from "./detect.ts";
|
|
18
|
+
import {
|
|
19
|
+
type ClaudePaths,
|
|
20
|
+
resolveClaudeAgentTargetPath,
|
|
21
|
+
resolveClaudePaths,
|
|
22
|
+
resolveClaudeSkillTargetPath,
|
|
23
|
+
} from "./paths.ts";
|
|
24
|
+
import { transformClaudeAgent } from "./transform-agent.ts";
|
|
25
|
+
import { transformClaudeSkill } from "./transform-skill.ts";
|
|
26
|
+
|
|
27
|
+
export interface ClaudePathAccessResult {
|
|
28
|
+
exists: boolean;
|
|
29
|
+
writable: boolean;
|
|
30
|
+
checkedPath: string;
|
|
31
|
+
message?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ClaudePathAccess {
|
|
35
|
+
checkWritableDirectory(path: string): Promise<ClaudePathAccessResult>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ClaudeSyncFileSystem {
|
|
39
|
+
readFile(path: string): Promise<string>;
|
|
40
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
41
|
+
fileExists(path: string): Promise<boolean>;
|
|
42
|
+
ensureDir(path: string): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ClaudePluginOptions {
|
|
46
|
+
commandRunner?: ClaudeCommandRunner;
|
|
47
|
+
pathAccess?: ClaudePathAccess;
|
|
48
|
+
syncFileSystem?: ClaudeSyncFileSystem;
|
|
49
|
+
paths?: ClaudePaths;
|
|
50
|
+
resolvePaths?: (context: DoctorContext) => ClaudePaths;
|
|
51
|
+
pluginSelector?: string;
|
|
52
|
+
pluginMarketplaceSource?: string;
|
|
53
|
+
pluginScope?: "user";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createClaudePlugin(options: ClaudePluginOptions = {}): CodeAgentPlugin {
|
|
57
|
+
const syncFileSystem = options.syncFileSystem ?? nodeSyncFileSystem;
|
|
58
|
+
const getSyncPaths = () => options.paths ?? resolveClaudePathsFromHome();
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
id: "claude",
|
|
62
|
+
name: "Claude Code",
|
|
63
|
+
async detect(): Promise<PluginDetectionResult> {
|
|
64
|
+
return detectClaudeCode({ commandRunner: options.commandRunner });
|
|
65
|
+
},
|
|
66
|
+
async getCapabilities(): Promise<CodeAgentCapabilities> {
|
|
67
|
+
return {
|
|
68
|
+
skills: { supported: true, format: "claude-skill" },
|
|
69
|
+
agents: { supported: true, format: "claude-agent" },
|
|
70
|
+
hooks: {
|
|
71
|
+
supported: true,
|
|
72
|
+
events: [
|
|
73
|
+
"SessionStart",
|
|
74
|
+
"UserPromptSubmit",
|
|
75
|
+
"UserPromptExpansion",
|
|
76
|
+
"PreToolUse",
|
|
77
|
+
"PermissionRequest",
|
|
78
|
+
"PostToolUse",
|
|
79
|
+
"PostToolUseFailure",
|
|
80
|
+
"PostToolBatch",
|
|
81
|
+
"PermissionDenied",
|
|
82
|
+
"SubagentStart",
|
|
83
|
+
"Stop",
|
|
84
|
+
"StopFailure",
|
|
85
|
+
"TeammateIdle",
|
|
86
|
+
"SubagentStop",
|
|
87
|
+
"TaskCreated",
|
|
88
|
+
"TaskCompleted",
|
|
89
|
+
"PreCompact",
|
|
90
|
+
"PostCompact",
|
|
91
|
+
"SessionEnd",
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
async doctor(context: DoctorContext): Promise<DoctorCheck[]> {
|
|
97
|
+
const detection = await detectClaudeCode({ commandRunner: options.commandRunner });
|
|
98
|
+
const paths =
|
|
99
|
+
options.resolvePaths?.(context) ?? resolveClaudePaths({ homeDir: context.homeDir });
|
|
100
|
+
const pathAccess = options.pathAccess ?? nodePathAccess;
|
|
101
|
+
const [claudeDir, skillsDir, agentsDir] = await Promise.all([
|
|
102
|
+
pathAccess.checkWritableDirectory(paths.claudeDir),
|
|
103
|
+
pathAccess.checkWritableDirectory(paths.skillsDir),
|
|
104
|
+
pathAccess.checkWritableDirectory(paths.agentsDir),
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
return [
|
|
108
|
+
detectionToDoctorCheck(detection),
|
|
109
|
+
pathAccessToDoctorCheck(
|
|
110
|
+
"claude.paths.home",
|
|
111
|
+
"Claude user directory",
|
|
112
|
+
paths.claudeDir,
|
|
113
|
+
claudeDir,
|
|
114
|
+
),
|
|
115
|
+
pathAccessToDoctorCheck(
|
|
116
|
+
"claude.paths.skills",
|
|
117
|
+
"Claude skills directory",
|
|
118
|
+
paths.skillsDir,
|
|
119
|
+
skillsDir,
|
|
120
|
+
),
|
|
121
|
+
pathAccessToDoctorCheck(
|
|
122
|
+
"claude.paths.agents",
|
|
123
|
+
"Claude agents directory",
|
|
124
|
+
paths.agentsDir,
|
|
125
|
+
agentsDir,
|
|
126
|
+
),
|
|
127
|
+
];
|
|
128
|
+
},
|
|
129
|
+
async syncSkills(input: SyncSkillsInput): Promise<SyncResult> {
|
|
130
|
+
return syncClaudeSkills(input, getSyncPaths(), syncFileSystem);
|
|
131
|
+
},
|
|
132
|
+
async syncAgents(input: SyncAgentsInput): Promise<SyncResult> {
|
|
133
|
+
return syncClaudeAgents(input, getSyncPaths(), syncFileSystem);
|
|
134
|
+
},
|
|
135
|
+
async installPlugin(input: PluginInstallInput): Promise<PluginInstallResult> {
|
|
136
|
+
return installClaudeCodePlugin(input, options);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const nodeSyncFileSystem: ClaudeSyncFileSystem = {
|
|
142
|
+
async readFile(path: string): Promise<string> {
|
|
143
|
+
return readFile(path, "utf8");
|
|
144
|
+
},
|
|
145
|
+
async writeFile(path: string, content: string): Promise<void> {
|
|
146
|
+
await writeFile(path, content, { encoding: "utf8", flag: "wx" });
|
|
147
|
+
},
|
|
148
|
+
async fileExists(path: string): Promise<boolean> {
|
|
149
|
+
try {
|
|
150
|
+
const fileStat = await stat(path);
|
|
151
|
+
return fileStat.isFile();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (isNotFoundError(error)) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
async ensureDir(path: string): Promise<void> {
|
|
161
|
+
await mkdir(path, { recursive: true });
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const nodePathAccess: ClaudePathAccess = {
|
|
166
|
+
async checkWritableDirectory(path: string): Promise<ClaudePathAccessResult> {
|
|
167
|
+
return checkWritableDirectoryWithoutCreating(path);
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const claudePlugin = createClaudePlugin();
|
|
172
|
+
|
|
173
|
+
async function installClaudeCodePlugin(
|
|
174
|
+
input: PluginInstallInput,
|
|
175
|
+
options: Pick<
|
|
176
|
+
ClaudePluginOptions,
|
|
177
|
+
"commandRunner" | "pluginSelector" | "pluginMarketplaceSource" | "pluginScope"
|
|
178
|
+
>,
|
|
179
|
+
): Promise<PluginInstallResult> {
|
|
180
|
+
const command = "claude";
|
|
181
|
+
const selector = options.pluginSelector ?? "evodev@evo-dev";
|
|
182
|
+
const scope = options.pluginScope ?? "user";
|
|
183
|
+
const args = ["plugin", "install", selector, "--scope", scope];
|
|
184
|
+
const runner = options.commandRunner ?? new NodeClaudeCommandRunner();
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
if (options.pluginMarketplaceSource !== undefined) {
|
|
188
|
+
const marketplaceResult = await runner.run(command, [
|
|
189
|
+
"plugin",
|
|
190
|
+
"marketplace",
|
|
191
|
+
"add",
|
|
192
|
+
options.pluginMarketplaceSource,
|
|
193
|
+
"--scope",
|
|
194
|
+
scope,
|
|
195
|
+
]);
|
|
196
|
+
if (marketplaceResult.exitCode !== 0) {
|
|
197
|
+
return createPluginInstallResult(
|
|
198
|
+
input.targetPlugin,
|
|
199
|
+
"failed",
|
|
200
|
+
command,
|
|
201
|
+
args,
|
|
202
|
+
marketplaceResult.stderr ??
|
|
203
|
+
marketplaceResult.stdout ??
|
|
204
|
+
`Claude Code marketplace add exited with code ${marketplaceResult.exitCode}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const result = await runner.run(command, args);
|
|
210
|
+
if (result.exitCode === 0) {
|
|
211
|
+
if (isAlreadyInstalledMessage(result.stdout ?? result.stderr)) {
|
|
212
|
+
return createPluginInstallResult(
|
|
213
|
+
input.targetPlugin,
|
|
214
|
+
"already-installed",
|
|
215
|
+
command,
|
|
216
|
+
args,
|
|
217
|
+
result.stdout,
|
|
218
|
+
[
|
|
219
|
+
"Claude Code reported the plugin is already installed; if local plugin files changed without a version bump, Claude may keep the existing cached copy. Bump the plugin version or uninstall and reinstall to refresh the cache.",
|
|
220
|
+
],
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return createPluginInstallResult(
|
|
225
|
+
input.targetPlugin,
|
|
226
|
+
"installed",
|
|
227
|
+
command,
|
|
228
|
+
args,
|
|
229
|
+
result.stdout,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return createPluginInstallResult(
|
|
234
|
+
input.targetPlugin,
|
|
235
|
+
"failed",
|
|
236
|
+
command,
|
|
237
|
+
args,
|
|
238
|
+
result.stderr ??
|
|
239
|
+
result.stdout ??
|
|
240
|
+
`Claude Code plugin install exited with code ${result.exitCode}`,
|
|
241
|
+
);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return createPluginInstallResult(
|
|
244
|
+
input.targetPlugin,
|
|
245
|
+
"failed",
|
|
246
|
+
command,
|
|
247
|
+
args,
|
|
248
|
+
describeError(error),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isAlreadyInstalledMessage(message: string | undefined): boolean {
|
|
254
|
+
return message !== undefined && /already\s+installed/i.test(message);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
class NodeClaudeCommandRunner implements ClaudeCommandRunner {
|
|
258
|
+
async run(
|
|
259
|
+
command: string,
|
|
260
|
+
args: string[],
|
|
261
|
+
): Promise<{ exitCode: number; stdout?: string; stderr?: string }> {
|
|
262
|
+
return runNodeCommand(command, args);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function checkWritableDirectoryWithoutCreating(
|
|
267
|
+
path: string,
|
|
268
|
+
): Promise<ClaudePathAccessResult> {
|
|
269
|
+
const existingPath = await findNearestExistingPath(path);
|
|
270
|
+
|
|
271
|
+
if (!existingPath) {
|
|
272
|
+
return {
|
|
273
|
+
exists: false,
|
|
274
|
+
writable: false,
|
|
275
|
+
checkedPath: path,
|
|
276
|
+
message: "No existing parent directory found.",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const existingStat = await stat(existingPath);
|
|
281
|
+
if (!existingStat.isDirectory()) {
|
|
282
|
+
return {
|
|
283
|
+
exists: existingPath === path,
|
|
284
|
+
writable: false,
|
|
285
|
+
checkedPath: existingPath,
|
|
286
|
+
message: "Existing path is not a directory.",
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
await access(existingPath, constants.W_OK);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return {
|
|
294
|
+
exists: existingPath === path,
|
|
295
|
+
writable: false,
|
|
296
|
+
checkedPath: existingPath,
|
|
297
|
+
message: describeError(error),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
exists: existingPath === path,
|
|
303
|
+
writable: true,
|
|
304
|
+
checkedPath: existingPath,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function findNearestExistingPath(path: string): Promise<string | null> {
|
|
309
|
+
let current = path;
|
|
310
|
+
|
|
311
|
+
while (true) {
|
|
312
|
+
try {
|
|
313
|
+
await stat(current);
|
|
314
|
+
return current;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (!isNotFoundError(error) && !isNotDirectoryError(error)) {
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const parent = dirname(current);
|
|
322
|
+
if (parent === current) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
current = parent;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function detectionToDoctorCheck(detection: PluginDetectionResult): DoctorCheck {
|
|
330
|
+
if (detection.status === "installed") {
|
|
331
|
+
return { id: "claude.available", status: "pass", message: detection.message };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (detection.status === "missing") {
|
|
335
|
+
return { id: "claude.available", status: "warn", message: detection.message };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { id: "claude.available", status: "warn", message: detection.message };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function pathAccessToDoctorCheck(
|
|
342
|
+
id: string,
|
|
343
|
+
label: string,
|
|
344
|
+
targetPath: string,
|
|
345
|
+
result: ClaudePathAccessResult,
|
|
346
|
+
): DoctorCheck {
|
|
347
|
+
if (result.exists && result.writable) {
|
|
348
|
+
return {
|
|
349
|
+
id,
|
|
350
|
+
status: "pass",
|
|
351
|
+
message: `${label} is writable: ${targetPath}`,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!result.exists && result.writable) {
|
|
356
|
+
return {
|
|
357
|
+
id,
|
|
358
|
+
status: "pass",
|
|
359
|
+
message: `${label} can be created under writable parent: ${result.checkedPath}`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
id,
|
|
365
|
+
status: "fail",
|
|
366
|
+
message: `${label} is not writable: ${targetPath}${result.message ? ` (${result.message})` : ""}`,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function syncClaudeSkills(
|
|
371
|
+
input: SyncSkillsInput,
|
|
372
|
+
paths: ClaudePaths,
|
|
373
|
+
fileSystem: ClaudeSyncFileSystem,
|
|
374
|
+
): Promise<SyncResult> {
|
|
375
|
+
const result = createEmptySyncResult(input.targetPlugin);
|
|
376
|
+
|
|
377
|
+
for (const asset of input.skills) {
|
|
378
|
+
if (!asset.manifest.targets.includes("claude")) {
|
|
379
|
+
result.skipped.push(asset.registryKey);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const source = await fileSystem.readFile(asset.entryPath);
|
|
385
|
+
const transformed = transformClaudeSkill({ asset, source });
|
|
386
|
+
const targetPath = resolveClaudeSkillTargetPath(transformed.name, paths);
|
|
387
|
+
|
|
388
|
+
if (await fileSystem.fileExists(targetPath)) {
|
|
389
|
+
result.skipped.push(asset.registryKey);
|
|
390
|
+
result.warnings.push(`Skipped existing Claude skill target: ${targetPath}`);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (!input.dryRun) {
|
|
395
|
+
await fileSystem.ensureDir(dirname(targetPath));
|
|
396
|
+
await fileSystem.writeFile(targetPath, ensureTrailingNewline(transformed.content));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
result.syncedSkills.push(asset.registryKey);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
result.errors.push(
|
|
402
|
+
`Failed to sync Claude skill ${asset.registryKey}: ${describeError(error)}`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function syncClaudeAgents(
|
|
411
|
+
input: SyncAgentsInput,
|
|
412
|
+
paths: ClaudePaths,
|
|
413
|
+
fileSystem: ClaudeSyncFileSystem,
|
|
414
|
+
): Promise<SyncResult> {
|
|
415
|
+
const result = createEmptySyncResult(input.targetPlugin);
|
|
416
|
+
|
|
417
|
+
for (const asset of input.agents) {
|
|
418
|
+
if (!asset.manifest.targets.includes("claude")) {
|
|
419
|
+
result.skipped.push(asset.registryKey);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
const source = await fileSystem.readFile(asset.entryPath);
|
|
425
|
+
const transformed = transformClaudeAgent({ asset, source });
|
|
426
|
+
const targetPath = resolveClaudeAgentTargetPath(transformed.name, paths);
|
|
427
|
+
|
|
428
|
+
if (await fileSystem.fileExists(targetPath)) {
|
|
429
|
+
result.skipped.push(asset.registryKey);
|
|
430
|
+
result.warnings.push(`Skipped existing Claude agent target: ${targetPath}`);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (!input.dryRun) {
|
|
435
|
+
await fileSystem.ensureDir(dirname(targetPath));
|
|
436
|
+
await fileSystem.writeFile(targetPath, ensureTrailingNewline(transformed.content));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
result.syncedAgents.push(asset.registryKey);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
result.errors.push(
|
|
442
|
+
`Failed to sync Claude agent ${asset.registryKey}: ${describeError(error)}`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function createEmptySyncResult(targetPlugin: string): SyncResult {
|
|
451
|
+
return {
|
|
452
|
+
targetPlugin,
|
|
453
|
+
syncedSkills: [],
|
|
454
|
+
syncedAgents: [],
|
|
455
|
+
skipped: [],
|
|
456
|
+
warnings: [],
|
|
457
|
+
errors: [],
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function createPluginInstallResult(
|
|
462
|
+
targetPlugin: string,
|
|
463
|
+
status: PluginInstallResult["status"],
|
|
464
|
+
command: string,
|
|
465
|
+
args: string[],
|
|
466
|
+
message?: string,
|
|
467
|
+
warnings: string[] = [],
|
|
468
|
+
): PluginInstallResult {
|
|
469
|
+
const normalizedMessage = message?.trim() || `${command} ${args.join(" ")}`;
|
|
470
|
+
return {
|
|
471
|
+
targetPlugin,
|
|
472
|
+
status,
|
|
473
|
+
command,
|
|
474
|
+
args,
|
|
475
|
+
message: normalizedMessage,
|
|
476
|
+
warnings,
|
|
477
|
+
errors: status === "failed" ? [normalizedMessage] : [],
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function resolveClaudePathsFromHome(): ClaudePaths {
|
|
482
|
+
const home = process.env.HOME;
|
|
483
|
+
|
|
484
|
+
if (home === undefined || home.trim() === "") {
|
|
485
|
+
throw new Error("Cannot resolve Claude paths: HOME is not set");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return resolveClaudePaths({ homeDir: home });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function ensureTrailingNewline(content: string): string {
|
|
492
|
+
return content.endsWith("\n") ? content : `${content}\n`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function isNotFoundError(error: unknown): boolean {
|
|
496
|
+
return (
|
|
497
|
+
error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT"
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function isNotDirectoryError(error: unknown): boolean {
|
|
502
|
+
return (
|
|
503
|
+
error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOTDIR"
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function describeError(error: unknown): string {
|
|
508
|
+
if (error instanceof Error) {
|
|
509
|
+
return error.message;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return String(error);
|
|
513
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createCoreConfigStore,
|
|
5
|
+
createDefaultSettings,
|
|
6
|
+
formatHookRuntimeOutput,
|
|
7
|
+
handleHookRuntime,
|
|
8
|
+
} from "@evo-dev/core";
|
|
9
|
+
import { normalizeClaudeRuntimeHookPayload, normalizeCodexRuntimeHookPayload } from "./hooks.ts";
|
|
10
|
+
|
|
11
|
+
export interface HookRuntimeCliOptions {
|
|
12
|
+
argv?: string[];
|
|
13
|
+
homeDir?: string;
|
|
14
|
+
stdin?: string | (() => Promise<string>);
|
|
15
|
+
write?: (message: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runHookRuntimeCli(options: HookRuntimeCliOptions = {}): Promise<number> {
|
|
19
|
+
const argv = options.argv ?? process.argv.slice(2);
|
|
20
|
+
const write = options.write ?? process.stdout.write.bind(process.stdout);
|
|
21
|
+
|
|
22
|
+
if (argv[0] !== "hook" || argv[1] !== "runtime") {
|
|
23
|
+
throw new Error(`Unknown plugin command: ${argv.join(" ")}`.trim());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const flags = parseHookRuntimeFlags(argv.slice(2));
|
|
27
|
+
if (flags.target !== "claude" && flags.target !== "codex") {
|
|
28
|
+
throw new Error(`Unsupported hook target: ${flags.target}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const homeDir = options.homeDir ?? process.env.HOME ?? "~";
|
|
32
|
+
const payload = JSON.parse(await readHookRuntimeStdin(options.stdin)) as Record<string, unknown>;
|
|
33
|
+
const settings = await readHookSettings(homeDir);
|
|
34
|
+
const event =
|
|
35
|
+
flags.target === "codex"
|
|
36
|
+
? normalizeCodexRuntimeHookPayload({
|
|
37
|
+
payload,
|
|
38
|
+
receivedAt: new Date().toISOString(),
|
|
39
|
+
})
|
|
40
|
+
: normalizeClaudeRuntimeHookPayload({
|
|
41
|
+
payload,
|
|
42
|
+
receivedAt: new Date().toISOString(),
|
|
43
|
+
});
|
|
44
|
+
const result = await handleHookRuntime({
|
|
45
|
+
target: flags.target,
|
|
46
|
+
homeDir,
|
|
47
|
+
settings: settings.hooks,
|
|
48
|
+
event,
|
|
49
|
+
rawPayload: payload,
|
|
50
|
+
receivedAt: event.time.receivedAt,
|
|
51
|
+
});
|
|
52
|
+
const formatted = formatHookRuntimeOutput(result);
|
|
53
|
+
if (formatted.length > 0) write(formatted);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseHookRuntimeFlags(argv: string[]): { target: string } {
|
|
58
|
+
let target: string | undefined;
|
|
59
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
60
|
+
const arg = argv[index];
|
|
61
|
+
if (arg === "--target") {
|
|
62
|
+
const value = argv[index + 1];
|
|
63
|
+
if (value === undefined || value.startsWith("--")) {
|
|
64
|
+
throw new Error("Missing value for --target");
|
|
65
|
+
}
|
|
66
|
+
target = value;
|
|
67
|
+
index += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Unknown hook runtime option: ${arg}`);
|
|
71
|
+
}
|
|
72
|
+
if (target === undefined) throw new Error("Missing required --target <target>");
|
|
73
|
+
return { target };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function readHookRuntimeStdin(stdin: HookRuntimeCliOptions["stdin"]): Promise<string> {
|
|
77
|
+
if (typeof stdin === "string") return stdin;
|
|
78
|
+
if (typeof stdin === "function") return stdin();
|
|
79
|
+
|
|
80
|
+
const chunks: Buffer[] = [];
|
|
81
|
+
for await (const chunk of process.stdin) {
|
|
82
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
83
|
+
}
|
|
84
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function readHookSettings(homeDir: string) {
|
|
88
|
+
try {
|
|
89
|
+
return await createCoreConfigStore(homeDir).readSettings();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (isNotFoundError(error)) {
|
|
92
|
+
return createDefaultSettings();
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isNotFoundError(error: unknown): boolean {
|
|
99
|
+
return (
|
|
100
|
+
error instanceof Error &&
|
|
101
|
+
(("code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") ||
|
|
102
|
+
error.message.includes("ENOENT"))
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (import.meta.main) {
|
|
107
|
+
try {
|
|
108
|
+
process.exitCode = await runHookRuntimeCli();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AgentManifest, ScannedAsset } from "@evo-dev/core/assets";
|
|
2
|
+
|
|
3
|
+
export interface ClaudeAgentTransformInput {
|
|
4
|
+
asset: ScannedAsset<AgentManifest>;
|
|
5
|
+
source: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ClaudeAgentTransformResult {
|
|
9
|
+
name: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function transformClaudeAgent(input: ClaudeAgentTransformInput): ClaudeAgentTransformResult {
|
|
14
|
+
const { manifest } = input.asset;
|
|
15
|
+
const frontmatter = [
|
|
16
|
+
"---",
|
|
17
|
+
`name: ${manifest.id}`,
|
|
18
|
+
`description: ${escapeFrontmatterValue(manifest.description)}`,
|
|
19
|
+
"---",
|
|
20
|
+
].join("\n");
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
name: manifest.id,
|
|
24
|
+
content: `${frontmatter}\n\n${input.source}`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function escapeFrontmatterValue(value: string): string {
|
|
29
|
+
return value.replaceAll("\n", " ");
|
|
30
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ScannedAsset, SkillManifest } from "@evo-dev/core/assets";
|
|
2
|
+
|
|
3
|
+
export interface ClaudeSkillTransformInput {
|
|
4
|
+
asset: ScannedAsset<SkillManifest>;
|
|
5
|
+
source: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ClaudeSkillTransformResult {
|
|
9
|
+
name: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function transformClaudeSkill(input: ClaudeSkillTransformInput): ClaudeSkillTransformResult {
|
|
14
|
+
return {
|
|
15
|
+
name: input.asset.manifest.id,
|
|
16
|
+
content: input.source,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@evo-dev/plugin",
|
|
3
|
+
"version": "0.0.1-alpha",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./hooks/index.ts"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@evo-dev/core": "0.0.1-alpha"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"bun": ">=1.1.0"
|
|
13
|
+
},
|
|
14
|
+
"files": ["hooks", ".claude-plugin", ".codex-plugin", "hooks", "package.json"],
|
|
15
|
+
"license": "MIT"
|
|
16
|
+
}
|