delegation-layer 0.1.27 → 0.1.29
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/README.md +58 -14
- package/docs/assets/delegation-layer-hero.jpg +0 -0
- package/package.json +3 -2
- package/plugins/delegation-layer/.claude-plugin/plugin.json +1 -1
- package/plugins/delegation-layer/.codex-plugin/plugin.json +1 -1
- package/plugins/delegation-layer/skills/agent-integration/SKILL.md +27 -8
- package/plugins/delegation-layer/skills/agent-integration/examples/json-results.md +6 -7
- package/plugins/delegation-layer/skills/agent-integration/examples/managed-workspace.md +68 -0
- package/plugins/delegation-layer/skills/agent-integration/references/collaboration.md +22 -6
- package/plugins/delegation-layer/skills/agent-integration/references/command-reference.md +56 -18
- package/plugins/delegation-layer/skills/agent-integration/references/continuation.md +36 -0
- package/plugins/delegation-layer/skills/agent-integration/references/result-handling.md +46 -2
- package/plugins/delegation-layer/skills/agent-integration/references/workspaces.md +197 -0
- package/scripts/install-agent-integration.mjs +15 -829
- package/scripts/install-cli.mjs +26 -66
- package/scripts/installer/application.mjs +250 -0
- package/scripts/installer/arguments.mjs +123 -0
- package/scripts/installer/catalog.mjs +95 -0
- package/scripts/installer/detection.mjs +41 -0
- package/scripts/installer/filesystem.mjs +97 -0
- package/scripts/installer/paths.mjs +14 -0
- package/scripts/installer/picker-state.mjs +46 -0
- package/scripts/installer/routing-rules.mjs +99 -0
- package/scripts/installer/routing-store.mjs +151 -0
- package/scripts/installer/selection.mjs +34 -0
- package/scripts/installer/skill-store.mjs +33 -0
- package/scripts/installer/terminal.mjs +104 -0
- package/skills/agent-integration/SKILL.md +27 -8
- package/skills/agent-integration/examples/json-results.md +6 -7
- package/skills/agent-integration/examples/managed-workspace.md +68 -0
- package/skills/agent-integration/references/collaboration.md +22 -6
- package/skills/agent-integration/references/command-reference.md +56 -18
- package/skills/agent-integration/references/continuation.md +36 -0
- package/skills/agent-integration/references/result-handling.md +46 -2
- package/skills/agent-integration/references/workspaces.md +197 -0
|
@@ -1,835 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { copyFile, lstat, mkdir, open, readdir, readFile, rename, unlink } from "node:fs/promises";
|
|
5
|
-
import { emitKeypressEvents } from "node:readline";
|
|
6
|
-
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
7
4
|
import { homedir } from "node:os";
|
|
8
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { runInstaller } from "./installer/application.mjs";
|
|
7
|
+
import { main as installCliMain } from "./install-cli.mjs";
|
|
9
8
|
|
|
10
9
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
name: "codex",
|
|
24
|
-
label: "Codex",
|
|
25
|
-
projectPath: join(".agents", "skills"),
|
|
26
|
-
globalPath: join(".agents", "skills"),
|
|
27
|
-
projectMarkers: [".agents", ".codex"],
|
|
28
|
-
globalMarkers: [".agents", ".codex"],
|
|
29
|
-
commands: ["codex"],
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: "claude",
|
|
33
|
-
label: "Claude Code",
|
|
34
|
-
projectPath: join(".claude", "skills"),
|
|
35
|
-
globalPath: join(".claude", "skills"),
|
|
36
|
-
projectMarkers: [".claude"],
|
|
37
|
-
globalMarkers: [".claude"],
|
|
38
|
-
commands: ["claude"],
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: "cursor",
|
|
42
|
-
label: "Cursor",
|
|
43
|
-
projectPath: join(".cursor", "skills"),
|
|
44
|
-
globalPath: join(".cursor", "skills"),
|
|
45
|
-
projectMarkers: [".cursor"],
|
|
46
|
-
globalMarkers: [".cursor"],
|
|
47
|
-
commands: ["cursor"],
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
name: "gemini",
|
|
51
|
-
label: "Gemini CLI",
|
|
52
|
-
projectPath: join(".gemini", "skills"),
|
|
53
|
-
globalPath: join(".gemini", "skills"),
|
|
54
|
-
projectMarkers: [".gemini"],
|
|
55
|
-
globalMarkers: [".gemini"],
|
|
56
|
-
commands: ["gemini"],
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: "opencode",
|
|
60
|
-
label: "OpenCode",
|
|
61
|
-
projectPath: join(".opencode", "skills"),
|
|
62
|
-
globalPath: join(".config", "opencode", "skills"),
|
|
63
|
-
projectMarkers: [".opencode"],
|
|
64
|
-
globalMarkers: [join(".config", "opencode")],
|
|
65
|
-
commands: ["opencode"],
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: "pi",
|
|
69
|
-
label: "Pi",
|
|
70
|
-
projectPath: join(".pi", "skills"),
|
|
71
|
-
globalPath: join(".pi", "agent", "skills"),
|
|
72
|
-
projectMarkers: [".pi"],
|
|
73
|
-
globalMarkers: [join(".pi", "agent")],
|
|
74
|
-
commands: ["pi"],
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: "hermes",
|
|
78
|
-
label: "Hermes",
|
|
79
|
-
projectPath: join(".hermes", "skills"),
|
|
80
|
-
globalPath: join(".hermes", "skills"),
|
|
81
|
-
projectMarkers: [".hermes"],
|
|
82
|
-
globalMarkers: [".hermes"],
|
|
83
|
-
commands: ["hermes"],
|
|
84
|
-
},
|
|
85
|
-
];
|
|
86
|
-
const harnessByName = new Map(harnesses.map((harness) => [harness.name, harness]));
|
|
87
|
-
|
|
88
|
-
function usage(message, exitCode = 2) {
|
|
89
|
-
if (message) {
|
|
90
|
-
console.error(`Error: ${message}`);
|
|
91
|
-
}
|
|
92
|
-
console.error(
|
|
93
|
-
"Usage:\n delegation-layer install [--scope project|global] [--harness NAME[,NAME...]] [--yes] [--force] [--routing default|skill-only]\n delegation-layer routing status|disable [--scope project|global] [--harness NAME[,NAME...]]\n delegation-layer --target ABSOLUTE_SKILL_DIR [--force]\n delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]",
|
|
94
|
-
);
|
|
95
|
-
process.exitCode = exitCode;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function help() {
|
|
99
|
-
console.log(
|
|
100
|
-
"Usage:\n delegation-layer install [--scope project|global] [--harness NAME[,NAME...]] [--yes] [--force] [--routing default|skill-only]\n delegation-layer routing status|disable [--scope project|global] [--harness NAME[,NAME...]]\n delegation-layer --target ABSOLUTE_SKILL_DIR [--force]\n delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]\n\nThe install command detects supported agent harnesses and installs the skill for a project or user.\nGlobal scope is the default; use --scope project for a project-local install.\nUse --harness universal when no specific harness is detected.",
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function requiredValue(argumentsList, index, option) {
|
|
105
|
-
const value = argumentsList[index + 1];
|
|
106
|
-
if (!value || value.startsWith("--")) {
|
|
107
|
-
usage(`${option} requires a value`);
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
return value;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function parseLegacyArguments(argumentsList) {
|
|
114
|
-
let target;
|
|
115
|
-
let force = false;
|
|
116
|
-
|
|
117
|
-
for (let index = 0; index < argumentsList.length; index += 1) {
|
|
118
|
-
const argument = argumentsList[index];
|
|
119
|
-
if (argument === "--force") {
|
|
120
|
-
force = true;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (argument === "--target") {
|
|
124
|
-
target = requiredValue(argumentsList, index, "--target");
|
|
125
|
-
if (!target) {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
index += 1;
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (argument === "--help" || argument === "-h") {
|
|
132
|
-
help();
|
|
133
|
-
process.exit(0);
|
|
134
|
-
}
|
|
135
|
-
usage(`unknown argument ${argument}`);
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (!target) {
|
|
140
|
-
usage("--target is required");
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
if (!isAbsolute(target)) {
|
|
144
|
-
usage("--target must be an absolute path");
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
return { force, target: resolve(target) };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function parseInstallArguments(argumentsList) {
|
|
151
|
-
let force = false;
|
|
152
|
-
let harnessNames;
|
|
153
|
-
let scope;
|
|
154
|
-
let yes = false;
|
|
155
|
-
let routing;
|
|
156
|
-
|
|
157
|
-
for (let index = 0; index < argumentsList.length; index += 1) {
|
|
158
|
-
const argument = argumentsList[index];
|
|
159
|
-
if (argument === "--force") {
|
|
160
|
-
force = true;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (argument === "--yes" || argument === "-y") {
|
|
164
|
-
yes = true;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (argument === "--routing") {
|
|
168
|
-
routing = requiredValue(argumentsList, index, "--routing");
|
|
169
|
-
if (!routing) return null;
|
|
170
|
-
index += 1;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (argument === "--scope") {
|
|
174
|
-
scope = requiredValue(argumentsList, index, "--scope");
|
|
175
|
-
if (!scope) {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
index += 1;
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
if (argument === "--harness") {
|
|
182
|
-
const value = requiredValue(argumentsList, index, "--harness");
|
|
183
|
-
if (!value) {
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
harnessNames = value.split(",").map((name) => name.trim().toLowerCase());
|
|
187
|
-
index += 1;
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
if (argument === "--help" || argument === "-h") {
|
|
191
|
-
help();
|
|
192
|
-
process.exit(0);
|
|
193
|
-
}
|
|
194
|
-
usage(`unknown argument ${argument}`);
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (scope && scope !== "project" && scope !== "global") {
|
|
199
|
-
usage("--scope must be project or global");
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
if (harnessNames?.length === 0 || harnessNames?.some((name) => !name)) {
|
|
203
|
-
usage("--harness requires at least one harness name");
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
if (harnessNames?.some((name) => !harnessByName.has(name))) {
|
|
207
|
-
const unknown = harnessNames.find((name) => !harnessByName.has(name));
|
|
208
|
-
usage(`unknown harness ${unknown}; choose from ${harnesses.map((harness) => harness.name).join(", ")}`);
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
if (routing && !["default", "skill-only"].includes(routing)) {
|
|
212
|
-
usage("--routing must be default or skill-only");
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
return { force, harnessNames, scope, yes, routing };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function existingPath(path) {
|
|
219
|
-
try {
|
|
220
|
-
return await lstat(path);
|
|
221
|
-
} catch (error) {
|
|
222
|
-
if (error.code === "ENOENT") {
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
throw error;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async function assertNoSymlinkComponents(path) {
|
|
230
|
-
let current = resolve(path);
|
|
231
|
-
while (true) {
|
|
232
|
-
const stat = await existingPath(current);
|
|
233
|
-
if (stat?.isSymbolicLink()) {
|
|
234
|
-
throw new Error(`skill destination must not contain symlink components: ${current}`);
|
|
235
|
-
}
|
|
236
|
-
const parent = dirname(current);
|
|
237
|
-
if (parent === current) {
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
current = parent;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async function skillFiles(directory, relative = "") {
|
|
245
|
-
const entries = await readdir(join(directory, relative), { withFileTypes: true });
|
|
246
|
-
const files = [];
|
|
247
|
-
for (const entry of entries) {
|
|
248
|
-
const child = join(relative, entry.name);
|
|
249
|
-
const path = join(directory, child);
|
|
250
|
-
const stat = await lstat(path);
|
|
251
|
-
if (stat.isSymbolicLink()) {
|
|
252
|
-
throw new Error(`skill source or destination must not contain symlinks: ${path}`);
|
|
253
|
-
}
|
|
254
|
-
if (stat.isDirectory()) {
|
|
255
|
-
files.push(...(await skillFiles(directory, child)));
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
if (!stat.isFile()) {
|
|
259
|
-
throw new Error(`skill entry must be a regular file: ${path}`);
|
|
260
|
-
}
|
|
261
|
-
files.push(child);
|
|
262
|
-
}
|
|
263
|
-
return files.sort();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
async function skillTreeMatches(target, files) {
|
|
267
|
-
for (const relative of files) {
|
|
268
|
-
const destination = join(target, relative);
|
|
269
|
-
await assertNoSymlinkComponents(destination);
|
|
270
|
-
const stat = await existingPath(destination);
|
|
271
|
-
if (!stat || !stat.isFile() || stat.isSymbolicLink()) {
|
|
272
|
-
return false;
|
|
273
|
-
}
|
|
274
|
-
const [source, existing] = await Promise.all([
|
|
275
|
-
readFile(join(sourceDirectory, relative)),
|
|
276
|
-
readFile(destination),
|
|
277
|
-
]);
|
|
278
|
-
if (!source.equals(existing)) {
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
return true;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async function skillTreeHasExistingFiles(target, files) {
|
|
286
|
-
for (const relative of files) {
|
|
287
|
-
const destination = join(target, relative);
|
|
288
|
-
await assertNoSymlinkComponents(destination);
|
|
289
|
-
if (await existingPath(destination)) {
|
|
290
|
-
return true;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async function copySkillTree(target, files, force) {
|
|
297
|
-
for (const relative of files) {
|
|
298
|
-
const destination = join(target, relative);
|
|
299
|
-
const parent = dirname(destination);
|
|
300
|
-
await assertNoSymlinkComponents(destination);
|
|
301
|
-
await mkdir(parent, { recursive: true });
|
|
302
|
-
const stat = await existingPath(destination);
|
|
303
|
-
if (stat?.isSymbolicLink()) {
|
|
304
|
-
throw new Error(`existing skill file must not be a symlink: ${destination}`);
|
|
305
|
-
}
|
|
306
|
-
if (stat && !stat.isFile()) {
|
|
307
|
-
throw new Error(`existing skill entry is not a regular file: ${destination}`);
|
|
308
|
-
}
|
|
309
|
-
if (stat && !force) {
|
|
310
|
-
throw new Error(`skill files already exist; use --force to replace them: ${target}`);
|
|
311
|
-
}
|
|
312
|
-
await copyFile(join(sourceDirectory, relative), destination);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function executableOnPath(command) {
|
|
317
|
-
const pathEntries = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
318
|
-
const candidates = process.platform === "win32" ? [command, `${command}.exe`, `${command}.cmd`] : [command];
|
|
319
|
-
return pathEntries.some((pathEntry) =>
|
|
320
|
-
candidates.some((candidate) => {
|
|
321
|
-
try {
|
|
322
|
-
accessSync(join(pathEntry, candidate), constants.X_OK);
|
|
323
|
-
return true;
|
|
324
|
-
} catch {
|
|
325
|
-
return false;
|
|
326
|
-
}
|
|
327
|
-
}),
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async function detectHarnesses({ cwd, home }) {
|
|
332
|
-
return Promise.all(
|
|
333
|
-
harnesses.map(async (harness) => {
|
|
334
|
-
const reasons = [];
|
|
335
|
-
for (const marker of harness.projectMarkers) {
|
|
336
|
-
const markerPath = resolve(cwd, marker);
|
|
337
|
-
if ((await existingPath(markerPath))?.isDirectory()) {
|
|
338
|
-
reasons.push(`project ${marker}`);
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
for (const marker of harness.globalMarkers) {
|
|
343
|
-
const markerPath = resolve(home, marker);
|
|
344
|
-
if ((await existingPath(markerPath))?.isDirectory()) {
|
|
345
|
-
reasons.push(`global ${marker}`);
|
|
346
|
-
break;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
for (const command of harness.commands) {
|
|
350
|
-
if (executableOnPath(command)) {
|
|
351
|
-
reasons.push(`${command} on PATH`);
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
return { harness, reasons };
|
|
356
|
-
}),
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function defaultHarnessNames(detections) {
|
|
361
|
-
const detected = detections
|
|
362
|
-
.filter(({ harness, reasons }) => harness.name !== "universal" && reasons.length > 0)
|
|
363
|
-
.map(({ harness }) => harness.name);
|
|
364
|
-
if (detected.length > 0) {
|
|
365
|
-
return detected;
|
|
366
|
-
}
|
|
367
|
-
return ["universal"];
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function formatDetected(reasons) {
|
|
371
|
-
return reasons.length > 0 ? ` (detected: ${reasons.join(", ")})` : "";
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function printDetections(detections) {
|
|
375
|
-
console.log("Detected harnesses:");
|
|
376
|
-
detections.forEach(({ harness, reasons }, index) => {
|
|
377
|
-
console.log(` ${index + 1}. ${harness.label}${formatDetected(reasons)}`);
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function renderPicker({ error, index, multiple, options, selected, title }) {
|
|
382
|
-
const lines = [title, ""];
|
|
383
|
-
options.forEach((option, optionIndex) => {
|
|
384
|
-
const current = optionIndex === index;
|
|
385
|
-
const marker = multiple ? (selected.has(option.value) ? "[x]" : "[ ]") : "";
|
|
386
|
-
const cursor = current ? "❯" : " ";
|
|
387
|
-
const color = current ? "\u001b[36m" : "";
|
|
388
|
-
const reset = current ? "\u001b[0m" : "";
|
|
389
|
-
lines.push(`${color}${cursor} ${marker ? `${marker} ` : ""}${option.label}${reset}`);
|
|
390
|
-
if (option.description) {
|
|
391
|
-
lines.push(` ${option.description}`);
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
lines.push("");
|
|
395
|
-
lines.push(multiple ? "↑↓ move Space select Enter continue a all n none q cancel" : "↑↓ move Enter select q cancel");
|
|
396
|
-
if (error) {
|
|
397
|
-
lines.push(`\u001b[31m${error}\u001b[0m`);
|
|
398
|
-
}
|
|
399
|
-
process.stdout.write(`\u001b[2J\u001b[H${lines.join("\n")}\n`);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function pickOptions({ initialValues = [], multiple = false, options, title }) {
|
|
403
|
-
const input = process.stdin;
|
|
404
|
-
const output = process.stdout;
|
|
405
|
-
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") {
|
|
406
|
-
return null;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
let index = 0;
|
|
410
|
-
const selected = new Set(initialValues);
|
|
411
|
-
|
|
412
|
-
return new Promise((resolve, reject) => {
|
|
413
|
-
let settled = false;
|
|
414
|
-
let error;
|
|
415
|
-
|
|
416
|
-
const cleanup = () => {
|
|
417
|
-
input.off("keypress", onKeypress);
|
|
418
|
-
try {
|
|
419
|
-
input.setRawMode(false);
|
|
420
|
-
} catch {
|
|
421
|
-
// The terminal may already be closed while handling cancellation.
|
|
422
|
-
}
|
|
423
|
-
input.pause();
|
|
424
|
-
output.write("\u001b[?25h\u001b[0m");
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
const finish = (value) => {
|
|
428
|
-
if (settled) {
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
settled = true;
|
|
432
|
-
cleanup();
|
|
433
|
-
resolve(value);
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
const onKeypress = (character, key = {}) => {
|
|
437
|
-
if ((key.ctrl && key.name === "c") || key.name === "escape" || key.name === "q") {
|
|
438
|
-
finish(null);
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
if (key.name === "up" || key.name === "k") {
|
|
442
|
-
index = (index + options.length - 1) % options.length;
|
|
443
|
-
error = undefined;
|
|
444
|
-
renderPicker({ error, index, multiple, options, selected, title });
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
if (key.name === "down" || key.name === "j") {
|
|
448
|
-
index = (index + 1) % options.length;
|
|
449
|
-
error = undefined;
|
|
450
|
-
renderPicker({ error, index, multiple, options, selected, title });
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
if (multiple && (key.name === "space" || character === " ")) {
|
|
454
|
-
const value = options[index].value;
|
|
455
|
-
if (selected.has(value)) {
|
|
456
|
-
selected.delete(value);
|
|
457
|
-
} else {
|
|
458
|
-
selected.add(value);
|
|
459
|
-
}
|
|
460
|
-
error = undefined;
|
|
461
|
-
renderPicker({ error, index, multiple, options, selected, title });
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
if (multiple && key.name === "a") {
|
|
465
|
-
options.forEach((option) => selected.add(option.value));
|
|
466
|
-
error = undefined;
|
|
467
|
-
renderPicker({ error, index, multiple, options, selected, title });
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
if (multiple && key.name === "n") {
|
|
471
|
-
selected.clear();
|
|
472
|
-
error = undefined;
|
|
473
|
-
renderPicker({ error, index, multiple, options, selected, title });
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
if (key.name === "return" || key.name === "enter") {
|
|
477
|
-
if (multiple && selected.size === 0) {
|
|
478
|
-
error = "Select at least one harness, or press q to cancel.";
|
|
479
|
-
renderPicker({ error, index, multiple, options, selected, title });
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
finish(multiple ? options.filter((option) => selected.has(option.value)).map((option) => option.value) : options[index].value);
|
|
483
|
-
}
|
|
484
|
-
};
|
|
485
|
-
|
|
486
|
-
try {
|
|
487
|
-
emitKeypressEvents(input);
|
|
488
|
-
input.setRawMode(true);
|
|
489
|
-
input.resume();
|
|
490
|
-
input.on("keypress", onKeypress);
|
|
491
|
-
output.write("\u001b[?25l");
|
|
492
|
-
renderPicker({ error, index, multiple, options, selected, title });
|
|
493
|
-
} catch (pickerError) {
|
|
494
|
-
cleanup();
|
|
495
|
-
reject(pickerError);
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
async function chooseInstallOptions({ force, harnessNames, scope, yes, routing }) {
|
|
501
|
-
const cwd = process.cwd();
|
|
502
|
-
const home = resolve(process.env.DELEGATION_LAYER_HOME ?? homedir());
|
|
503
|
-
const detections = await detectHarnesses({ cwd, home });
|
|
504
|
-
const defaults = defaultHarnessNames(detections);
|
|
505
|
-
const canPrompt = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
506
|
-
const needsPrompt = !yes && !harnessNames && !scope && canPrompt;
|
|
507
|
-
|
|
508
|
-
if (!harnessNames && !needsPrompt) {
|
|
509
|
-
printDetections(detections);
|
|
510
|
-
}
|
|
511
|
-
if (!canPrompt && !yes && !harnessNames && !scope) {
|
|
512
|
-
console.log("No interactive terminal detected; using global scope and detected harnesses.");
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (needsPrompt) {
|
|
516
|
-
scope = await pickOptions({
|
|
517
|
-
initialValues: ["global"],
|
|
518
|
-
options: [
|
|
519
|
-
{
|
|
520
|
-
value: "global",
|
|
521
|
-
label: "Global",
|
|
522
|
-
description: `available to your agents from ${home}`,
|
|
523
|
-
},
|
|
524
|
-
{
|
|
525
|
-
value: "project",
|
|
526
|
-
label: "Project",
|
|
527
|
-
description: `available to this project from ${cwd}`,
|
|
528
|
-
},
|
|
529
|
-
],
|
|
530
|
-
title: "Where should the agent skill be installed?",
|
|
531
|
-
});
|
|
532
|
-
if (!scope) {
|
|
533
|
-
return null;
|
|
534
|
-
}
|
|
535
|
-
harnessNames = await pickOptions({
|
|
536
|
-
initialValues: defaults,
|
|
537
|
-
multiple: true,
|
|
538
|
-
options: detections.map(({ harness, reasons }) => ({
|
|
539
|
-
value: harness.name,
|
|
540
|
-
label: `${harness.label}${formatDetected(reasons)}`,
|
|
541
|
-
description: targetFor(harness, scope, cwd, home),
|
|
542
|
-
})),
|
|
543
|
-
title: "Which harnesses should use the skill?",
|
|
544
|
-
});
|
|
545
|
-
if (!harnessNames) {
|
|
546
|
-
return null;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (needsPrompt && !routing) {
|
|
551
|
-
routing = await pickOptions({
|
|
552
|
-
initialValues: ["default"],
|
|
553
|
-
options: [
|
|
554
|
-
{ value: "default", label: "Prefer Delegate", description: "Install collaboration context hooks for Claude and Codex; native hook trust still applies" },
|
|
555
|
-
{ value: "skill-only", label: "Skill only", description: "Install guidance without adding hooks; keep existing routing settings" },
|
|
556
|
-
],
|
|
557
|
-
title: "How should external collaboration be routed?",
|
|
558
|
-
});
|
|
559
|
-
if (!routing) return null;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
return {
|
|
563
|
-
routing: routing ?? "skill-only",
|
|
564
|
-
cwd,
|
|
565
|
-
force,
|
|
566
|
-
harnessNames: harnessNames ?? defaults,
|
|
567
|
-
home,
|
|
568
|
-
scope: scope ?? "global",
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function targetFor(harness, scope, cwd, home) {
|
|
573
|
-
if (scope === "global" && harness.name === "claude" && process.env.CLAUDE_CONFIG_DIR) {
|
|
574
|
-
return join(resolve(process.env.CLAUDE_CONFIG_DIR), "skills", "agent-integration");
|
|
575
|
-
}
|
|
576
|
-
const root = scope === "global" ? home : cwd;
|
|
577
|
-
const relativePath = scope === "global" ? harness.globalPath : harness.projectPath;
|
|
578
|
-
return join(resolve(root, relativePath), "agent-integration");
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
async function install({ force, target }) {
|
|
582
|
-
await assertNoSymlinkComponents(target);
|
|
583
|
-
const targetStat = await existingPath(target);
|
|
584
|
-
if (targetStat?.isSymbolicLink()) {
|
|
585
|
-
throw new Error(`target directory must not be a symlink: ${target}`);
|
|
586
|
-
}
|
|
587
|
-
if (targetStat && !targetStat.isDirectory()) {
|
|
588
|
-
throw new Error(`target must be a directory: ${target}`);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const files = await skillFiles(sourceDirectory);
|
|
592
|
-
await mkdir(target, { recursive: true });
|
|
593
|
-
|
|
594
|
-
if (!force && (await skillTreeMatches(target, files))) {
|
|
595
|
-
console.log(`Skill already installed: ${join(target, "SKILL.md")}`);
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
if (!force && (await skillTreeHasExistingFiles(target, files))) {
|
|
599
|
-
throw new Error(`skill files already exist; use --force to replace them: ${target}`);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
await copySkillTree(target, files, force);
|
|
603
|
-
console.log(`Installed agent integration skill: ${join(target, "SKILL.md")}`);
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
function shellQuote(value) {
|
|
607
|
-
return "'" + value.replaceAll("'", "'\\''") + "'";
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function routingPath(name, { scope, cwd, home }) {
|
|
611
|
-
if (name === "claude") {
|
|
612
|
-
return scope === "global"
|
|
613
|
-
? join(resolve(process.env.CLAUDE_CONFIG_DIR || join(home, ".claude")), "settings.json")
|
|
614
|
-
: join(cwd, ".claude", "settings.local.json");
|
|
615
|
-
}
|
|
616
|
-
if (name === "codex") {
|
|
617
|
-
return join(scope === "global" ? resolve(process.env.CODEX_HOME || join(home, ".codex")) : join(cwd, ".codex"), "hooks.json");
|
|
618
|
-
}
|
|
619
|
-
return null;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function jsonObject(value) {
|
|
623
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function validateHookConfig(config, path) {
|
|
627
|
-
if (!jsonObject(config) || (config.hooks !== undefined && !jsonObject(config.hooks))) {
|
|
628
|
-
throw new Error(`expected a JSON object with an optional hooks object: ${path}`);
|
|
629
|
-
}
|
|
630
|
-
for (const groups of Object.values(config.hooks ?? {})) {
|
|
631
|
-
if (!Array.isArray(groups) || groups.some((group) => !jsonObject(group)
|
|
632
|
-
|| !Array.isArray(group.hooks) || group.hooks.some((hook) => !jsonObject(hook) || typeof hook.type !== "string"))) {
|
|
633
|
-
throw new Error(`invalid hook configuration; no routing changes written: ${path}`);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
async function prepareRouting(selected, action) {
|
|
639
|
-
const plans = [];
|
|
640
|
-
for (const name of new Set(selected.harnessNames)) {
|
|
641
|
-
const path = routingPath(name, selected);
|
|
642
|
-
if (!path || process.platform === "win32") {
|
|
643
|
-
plans.push({ name, status: "unsupported-host" });
|
|
644
|
-
continue;
|
|
645
|
-
}
|
|
646
|
-
await assertNoSymlinkComponents(path);
|
|
647
|
-
const stat = await existingPath(path);
|
|
648
|
-
if (stat && !stat.isFile()) throw new Error(`routing configuration must be a regular file: ${path}`);
|
|
649
|
-
const before = stat ? await readFile(path, "utf8") : null;
|
|
650
|
-
let config;
|
|
651
|
-
try {
|
|
652
|
-
config = before === null ? {} : JSON.parse(before);
|
|
653
|
-
} catch {
|
|
654
|
-
throw new Error(`invalid JSON; routing configuration left unchanged: ${path}`);
|
|
655
|
-
}
|
|
656
|
-
validateHookConfig(config, path);
|
|
657
|
-
const target = targetFor(harnessByName.get(name), selected.scope, selected.cwd, selected.home);
|
|
658
|
-
const script = join(target, "scripts", "delegate-routing-context.sh");
|
|
659
|
-
const hooks = config.hooks ?? {};
|
|
660
|
-
let present = 0;
|
|
661
|
-
let changed = false;
|
|
662
|
-
for (const event of ["SessionStart", "SubagentStart"]) {
|
|
663
|
-
const command = `/bin/sh ${shellQuote(script)} ${event}`;
|
|
664
|
-
const groups = hooks[event] ?? [];
|
|
665
|
-
const matches = groups.flatMap((group) => group.hooks).filter((hook) => hook.type === "command" && hook.command === command);
|
|
666
|
-
if (matches.length) present += 1;
|
|
667
|
-
if (action === "disable") {
|
|
668
|
-
if (!matches.length) continue;
|
|
669
|
-
hooks[event] = groups.flatMap((group) => {
|
|
670
|
-
const remaining = group.hooks.filter((hook) => hook.type !== "command" || hook.command !== command);
|
|
671
|
-
if (remaining.length === group.hooks.length) return [group];
|
|
672
|
-
return remaining.length ? [{ ...group, hooks: remaining }] : [];
|
|
673
|
-
});
|
|
674
|
-
if (!hooks[event].length) delete hooks[event];
|
|
675
|
-
changed = true;
|
|
676
|
-
} else if (action === "enable" && !matches.length) {
|
|
677
|
-
hooks[event] = [...groups, { hooks: [{ type: "command", command }] }];
|
|
678
|
-
changed = true;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
if (changed) config.hooks = hooks;
|
|
682
|
-
let assetsPresent = true;
|
|
683
|
-
if (action === "status" && present) {
|
|
684
|
-
for (const relative of ["SKILL.md", "scripts/delegate-routing-context.sh", "references/routing-rule.json"]) {
|
|
685
|
-
const asset = await existingPath(join(target, relative));
|
|
686
|
-
if (!asset?.isFile() || asset.isSymbolicLink()) assetsPresent = false;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
plans.push({ name, path, target, before, after: changed ? JSON.stringify(config, null, 2) + "\n" : before,
|
|
690
|
-
mode: stat ? stat.mode & 0o777 : 0o600,
|
|
691
|
-
status: action === "disable" ? "disabled" : action === "enable" || (present === 2 && assetsPresent) ? "configured" : present ? "incomplete" : "not-configured" });
|
|
692
|
-
}
|
|
693
|
-
return plans;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
async function replaceRoutingConfig(plan, before, after) {
|
|
697
|
-
await assertNoSymlinkComponents(plan.path);
|
|
698
|
-
await mkdir(dirname(plan.path), { recursive: true });
|
|
699
|
-
const lockPath = plan.path + ".delegation-layer.lock";
|
|
700
|
-
let lock;
|
|
701
|
-
try {
|
|
702
|
-
lock = await open(lockPath, "wx", 0o600);
|
|
703
|
-
} catch (error) {
|
|
704
|
-
if (error.code === "EEXIST") throw new Error(`routing configuration is locked: ${lockPath}; verify no other installer is running before removing a stale lock`);
|
|
705
|
-
throw error;
|
|
706
|
-
}
|
|
707
|
-
const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
708
|
-
const temporary = plan.path + `.delegation-layer-stage-${suffix}`;
|
|
709
|
-
try {
|
|
710
|
-
const stat = await existingPath(plan.path);
|
|
711
|
-
if (stat && (!stat.isFile() || stat.isSymbolicLink())) throw new Error(`routing configuration changed during installation: ${plan.path}`);
|
|
712
|
-
const current = stat ? await readFile(plan.path, "utf8") : null;
|
|
713
|
-
if (current !== before) throw new Error(`routing configuration changed during installation; retry: ${plan.path}`);
|
|
714
|
-
if (before !== null) {
|
|
715
|
-
const backup = await open(plan.path + `.delegation-layer-backup-${suffix}`, "wx", 0o600);
|
|
716
|
-
try { await backup.writeFile(before); await backup.sync(); } finally { await backup.close(); }
|
|
717
|
-
}
|
|
718
|
-
if (after === null) {
|
|
719
|
-
await unlink(plan.path);
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
const staged = await open(temporary, "wx", plan.mode);
|
|
723
|
-
try { await staged.writeFile(after); await staged.sync(); } finally { await staged.close(); }
|
|
724
|
-
await rename(temporary, plan.path);
|
|
725
|
-
} finally {
|
|
726
|
-
try {
|
|
727
|
-
try { await unlink(temporary); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
728
|
-
} finally {
|
|
729
|
-
try { await lock.close(); } finally { await unlink(lockPath); }
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
async function applyRouting(plans) {
|
|
735
|
-
const applied = [];
|
|
736
|
-
try {
|
|
737
|
-
for (const plan of plans) {
|
|
738
|
-
if (!plan.path || plan.before === plan.after) continue;
|
|
739
|
-
await replaceRoutingConfig(plan, plan.before, plan.after);
|
|
740
|
-
applied.push(plan);
|
|
741
|
-
}
|
|
742
|
-
} catch (error) {
|
|
743
|
-
const failures = [];
|
|
744
|
-
for (const plan of applied.reverse()) {
|
|
745
|
-
try { await replaceRoutingConfig(plan, plan.after, plan.before); }
|
|
746
|
-
catch (rollbackError) { failures.push(rollbackError.message); }
|
|
747
|
-
}
|
|
748
|
-
throw new Error(`${error.message}${failures.length ? `; rollback needs attention: ${failures.join("; ")}` : "; earlier routing changes rolled back"}`);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function printRouting(plan, action) {
|
|
753
|
-
if (!plan.path) {
|
|
754
|
-
console.log(`${plan.name}: unsupported-host for automatic routing; skill-only guidance remains available.`);
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
console.log(`${plan.name}: ${plan.status} (${plan.path}).`);
|
|
758
|
-
if (plan.status === "configured") {
|
|
759
|
-
console.log("Activation not verified: start or resume a session and check the native hook output. Installation is not proof of model compliance.");
|
|
760
|
-
if (plan.name === "codex") console.log("Codex: review and trust the hooks using /hooks. New or changed definitions are skipped until trusted.");
|
|
761
|
-
}
|
|
762
|
-
if (action === "disable") console.log("Only this installer's matching hooks were removed; plugin hooks are managed by the host plugin manager.");
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
async function validateSourceTree() {
|
|
766
|
-
const files = await skillFiles(sourceDirectory);
|
|
767
|
-
if (!files.includes("SKILL.md")) {
|
|
768
|
-
throw new Error(`skill package is missing SKILL.md: ${sourceDirectory}`);
|
|
769
|
-
}
|
|
770
|
-
return files;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
await validateSourceTree();
|
|
774
|
-
|
|
775
|
-
const argumentsList = process.argv.slice(2);
|
|
776
|
-
if (argumentsList[0] === "install-cli") {
|
|
777
|
-
try {
|
|
778
|
-
const { main } = await import("./install-cli.mjs");
|
|
779
|
-
await main(argumentsList.slice(1));
|
|
780
|
-
} catch (error) {
|
|
781
|
-
console.error(`Error: ${error.message}`);
|
|
782
|
-
process.exitCode = 1;
|
|
783
|
-
}
|
|
784
|
-
} else if (argumentsList[0] === "routing") {
|
|
785
|
-
const action = argumentsList[1];
|
|
786
|
-
const options = parseInstallArguments(argumentsList.slice(2));
|
|
787
|
-
if (!["status", "disable"].includes(action)) {
|
|
788
|
-
usage("routing requires status or disable");
|
|
789
|
-
} else if (options) {
|
|
790
|
-
try {
|
|
791
|
-
if (options.routing || options.force) throw new Error("routing status/disable do not accept --routing or --force");
|
|
792
|
-
const selected = await chooseInstallOptions({ ...options, yes: true, harnessNames: options.harnessNames ?? ["claude", "codex"] });
|
|
793
|
-
const plans = await prepareRouting(selected, action);
|
|
794
|
-
if (action === "disable") await applyRouting(plans);
|
|
795
|
-
for (const plan of plans) printRouting(plan, action);
|
|
796
|
-
} catch (error) {
|
|
797
|
-
console.error(`Error: ${error.message}`);
|
|
798
|
-
process.exitCode = 1;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
} else if (argumentsList[0] === "install") {
|
|
802
|
-
const options = parseInstallArguments(argumentsList.slice(1));
|
|
803
|
-
if (options) {
|
|
804
|
-
try {
|
|
805
|
-
const selected = await chooseInstallOptions(options);
|
|
806
|
-
if (!selected) {
|
|
807
|
-
console.log("Installation cancelled.");
|
|
808
|
-
} else {
|
|
809
|
-
const routingPlans = selected.routing === "default" ? await prepareRouting(selected, "enable") : [];
|
|
810
|
-
const targets = selected.harnessNames.map((name) => targetFor(harnessByName.get(name), selected.scope, selected.cwd, selected.home));
|
|
811
|
-
const uniqueTargets = [...new Set(targets)];
|
|
812
|
-
console.log(`Installing for ${selected.scope}: ${uniqueTargets.join(", ")}`);
|
|
813
|
-
for (const target of uniqueTargets) {
|
|
814
|
-
await install({ force: selected.force, target });
|
|
815
|
-
}
|
|
816
|
-
await applyRouting(routingPlans);
|
|
817
|
-
for (const plan of routingPlans) printRouting(plan, "enable");
|
|
818
|
-
if (selected.routing === "skill-only") console.log("Skill-only installation; existing routing settings were not changed.");
|
|
819
|
-
}
|
|
820
|
-
} catch (error) {
|
|
821
|
-
console.error(`Error: ${error.message}`);
|
|
822
|
-
process.exitCode = 1;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
} else {
|
|
826
|
-
const options = parseLegacyArguments(argumentsList);
|
|
827
|
-
if (options) {
|
|
828
|
-
try {
|
|
829
|
-
await install(options);
|
|
830
|
-
} catch (error) {
|
|
831
|
-
console.error(`Error: ${error.message}`);
|
|
832
|
-
process.exitCode = 1;
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
}
|
|
10
|
+
process.exitCode = await runInstaller({
|
|
11
|
+
argumentsList: process.argv.slice(2),
|
|
12
|
+
packageRoot,
|
|
13
|
+
cwd: process.cwd(),
|
|
14
|
+
resolveHome: () => resolve(process.env.DELEGATION_LAYER_HOME ?? homedir()),
|
|
15
|
+
environment: process.env,
|
|
16
|
+
platform: process.platform,
|
|
17
|
+
input: process.stdin,
|
|
18
|
+
output: process.stdout,
|
|
19
|
+
errorOutput: process.stderr,
|
|
20
|
+
installCliMain,
|
|
21
|
+
});
|