delegation-layer 0.1.3 → 0.1.4

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 CHANGED
@@ -36,17 +36,26 @@ already use instead of replacing them.
36
36
 
37
37
  ### Install the Agent Skill
38
38
 
39
- From the project where you want the skill available, run:
39
+ The standard Agent Skills installer works across popular coding agents. From
40
+ the project where you want the skill available, run:
40
41
 
41
42
  ```sh
42
- npx --yes delegation-layer install
43
+ npx --yes skills add hishamkaram/delegation-layer --skill agent-integration
43
44
  ```
44
45
 
45
- The installer detects supported Agent Skills harnesses, shows what it found,
46
- and asks whether to install for the current project or your user account. It
47
- offers the shared `.agents/skills` location when no specific harness is found.
46
+ The `--skill agent-integration` selection matters because this repository also
47
+ contains maintainer skills. The installer detects supported agents and lets you
48
+ choose the project or global scope. Add `--global` to install for all projects,
49
+ or `--agent codex` (and similar agent names) to choose a specific harness.
50
+
51
+ If you want the Delegation Layer guided installer instead, run:
52
+
53
+ ```sh
54
+ npx --yes delegation-layer install
55
+ ```
48
56
 
49
- For scripts or a choice without prompts:
57
+ It provides an interactive scope and harness picker, with global installation
58
+ selected by default. For scripts or a choice without prompts:
50
59
 
51
60
  ```sh
52
61
  npx --yes delegation-layer install \
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delegation-layer",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Multi-provider AI agent delegation CLI and Agent Skill for running bounded coding tasks across provider CLIs.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { accessSync, constants } from "node:fs";
4
4
  import { copyFile, lstat, mkdir, readFile } from "node:fs/promises";
5
- import { createInterface } from "node:readline/promises";
5
+ import { emitKeypressEvents } from "node:readline";
6
6
  import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import { fileURLToPath } from "node:url";
@@ -97,7 +97,7 @@ function usage(message, exitCode = 2) {
97
97
 
98
98
  function help() {
99
99
  console.log(
100
- "Usage:\n delegation-layer install [--scope project|global] [--harness NAME[,NAME...]] [--yes] [--force]\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.\nUse --harness universal when no specific harness is detected.",
100
+ "Usage:\n delegation-layer install [--scope project|global] [--harness NAME[,NAME...]] [--yes] [--force]\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
101
  );
102
102
  }
103
103
 
@@ -280,25 +280,123 @@ function printDetections(detections) {
280
280
  });
281
281
  }
282
282
 
283
- function parseHarnessSelection(value, detections) {
284
- const tokens = value
285
- .split(",")
286
- .map((token) => token.trim().toLowerCase())
287
- .filter(Boolean);
288
- const names = tokens.map((token) => {
289
- if (/^\d+$/.test(token)) {
290
- const detection = detections[Number(token) - 1];
291
- if (!detection) {
292
- throw new Error(`harness choice ${token} is out of range`);
293
- }
294
- return detection.harness.name;
283
+ function renderPicker({ error, index, multiple, options, selected, title }) {
284
+ const lines = [title, ""];
285
+ options.forEach((option, optionIndex) => {
286
+ const current = optionIndex === index;
287
+ const marker = multiple ? (selected.has(option.value) ? "[x]" : "[ ]") : "";
288
+ const cursor = current ? "❯" : " ";
289
+ const color = current ? "\u001b[36m" : "";
290
+ const reset = current ? "\u001b[0m" : "";
291
+ lines.push(`${color}${cursor} ${marker ? `${marker} ` : ""}${option.label}${reset}`);
292
+ if (option.description) {
293
+ lines.push(` ${option.description}`);
295
294
  }
296
- if (!harnessByName.has(token)) {
297
- throw new Error(`unknown harness ${token}`);
295
+ });
296
+ lines.push("");
297
+ lines.push(multiple ? "↑↓ move Space select Enter continue a all n none q cancel" : "↑↓ move Enter select q cancel");
298
+ if (error) {
299
+ lines.push(`\u001b[31m${error}\u001b[0m`);
300
+ }
301
+ process.stdout.write(`\u001b[2J\u001b[H${lines.join("\n")}\n`);
302
+ }
303
+
304
+ function pickOptions({ initialValues = [], multiple = false, options, title }) {
305
+ const input = process.stdin;
306
+ const output = process.stdout;
307
+ if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") {
308
+ return null;
309
+ }
310
+
311
+ let index = 0;
312
+ const selected = new Set(initialValues);
313
+
314
+ return new Promise((resolve, reject) => {
315
+ let settled = false;
316
+ let error;
317
+
318
+ const cleanup = () => {
319
+ input.off("keypress", onKeypress);
320
+ try {
321
+ input.setRawMode(false);
322
+ } catch {
323
+ // The terminal may already be closed while handling cancellation.
324
+ }
325
+ input.pause();
326
+ output.write("\u001b[?25h\u001b[0m");
327
+ };
328
+
329
+ const finish = (value) => {
330
+ if (settled) {
331
+ return;
332
+ }
333
+ settled = true;
334
+ cleanup();
335
+ resolve(value);
336
+ };
337
+
338
+ const onKeypress = (character, key = {}) => {
339
+ if ((key.ctrl && key.name === "c") || key.name === "escape" || key.name === "q") {
340
+ finish(null);
341
+ return;
342
+ }
343
+ if (key.name === "up" || key.name === "k") {
344
+ index = (index + options.length - 1) % options.length;
345
+ error = undefined;
346
+ renderPicker({ error, index, multiple, options, selected, title });
347
+ return;
348
+ }
349
+ if (key.name === "down" || key.name === "j") {
350
+ index = (index + 1) % options.length;
351
+ error = undefined;
352
+ renderPicker({ error, index, multiple, options, selected, title });
353
+ return;
354
+ }
355
+ if (multiple && (key.name === "space" || character === " ")) {
356
+ const value = options[index].value;
357
+ if (selected.has(value)) {
358
+ selected.delete(value);
359
+ } else {
360
+ selected.add(value);
361
+ }
362
+ error = undefined;
363
+ renderPicker({ error, index, multiple, options, selected, title });
364
+ return;
365
+ }
366
+ if (multiple && key.name === "a") {
367
+ options.forEach((option) => selected.add(option.value));
368
+ error = undefined;
369
+ renderPicker({ error, index, multiple, options, selected, title });
370
+ return;
371
+ }
372
+ if (multiple && key.name === "n") {
373
+ selected.clear();
374
+ error = undefined;
375
+ renderPicker({ error, index, multiple, options, selected, title });
376
+ return;
377
+ }
378
+ if (key.name === "return" || key.name === "enter") {
379
+ if (multiple && selected.size === 0) {
380
+ error = "Select at least one harness, or press q to cancel.";
381
+ renderPicker({ error, index, multiple, options, selected, title });
382
+ return;
383
+ }
384
+ finish(multiple ? options.filter((option) => selected.has(option.value)).map((option) => option.value) : options[index].value);
385
+ }
386
+ };
387
+
388
+ try {
389
+ emitKeypressEvents(input);
390
+ input.setRawMode(true);
391
+ input.resume();
392
+ input.on("keypress", onKeypress);
393
+ output.write("\u001b[?25l");
394
+ renderPicker({ error, index, multiple, options, selected, title });
395
+ } catch (pickerError) {
396
+ cleanup();
397
+ reject(pickerError);
298
398
  }
299
- return token;
300
399
  });
301
- return [...new Set(names)];
302
400
  }
303
401
 
304
402
  async function chooseInstallOptions({ force, harnessNames, scope, yes }) {
@@ -309,28 +407,45 @@ async function chooseInstallOptions({ force, harnessNames, scope, yes }) {
309
407
  const canPrompt = Boolean(process.stdin.isTTY && process.stdout.isTTY);
310
408
  const needsPrompt = !yes && !harnessNames && !scope && canPrompt;
311
409
 
312
- if (!harnessNames) {
410
+ if (!harnessNames && !needsPrompt) {
313
411
  printDetections(detections);
314
412
  }
315
413
  if (!canPrompt && !yes && !harnessNames && !scope) {
316
- console.log("No interactive terminal detected; using project scope and detected harnesses.");
414
+ console.log("No interactive terminal detected; using global scope and detected harnesses.");
317
415
  }
318
416
 
319
417
  if (needsPrompt) {
320
- const readline = createInterface({ input: process.stdin, output: process.stdout });
321
- try {
322
- const selectedScope = (await readline.question("Install for [project/global] (project): ")).trim().toLowerCase();
323
- scope = selectedScope || "project";
324
- if (scope !== "project" && scope !== "global") {
325
- throw new Error("scope must be project or global");
326
- }
327
- const defaultSelection = defaults
328
- .map((name) => detections.findIndex(({ harness }) => harness.name === name) + 1)
329
- .join(",");
330
- const selection = await readline.question(`Choose harnesses [${defaultSelection}]: `);
331
- harnessNames = selection.trim() ? parseHarnessSelection(selection, detections) : defaults;
332
- } finally {
333
- readline.close();
418
+ scope = await pickOptions({
419
+ initialValues: ["global"],
420
+ options: [
421
+ {
422
+ value: "global",
423
+ label: "Global",
424
+ description: `available to your agents from ${home}`,
425
+ },
426
+ {
427
+ value: "project",
428
+ label: "Project",
429
+ description: `available to this project from ${cwd}`,
430
+ },
431
+ ],
432
+ title: "Where should the agent skill be installed?",
433
+ });
434
+ if (!scope) {
435
+ return null;
436
+ }
437
+ harnessNames = await pickOptions({
438
+ initialValues: defaults,
439
+ multiple: true,
440
+ options: detections.map(({ harness, reasons }) => ({
441
+ value: harness.name,
442
+ label: `${harness.label}${formatDetected(reasons)}`,
443
+ description: targetFor(harness, scope, cwd, home),
444
+ })),
445
+ title: "Which harnesses should use the skill?",
446
+ });
447
+ if (!harnessNames) {
448
+ return null;
334
449
  }
335
450
  }
336
451
 
@@ -339,7 +454,7 @@ async function chooseInstallOptions({ force, harnessNames, scope, yes }) {
339
454
  force,
340
455
  harnessNames: harnessNames ?? defaults,
341
456
  home,
342
- scope: scope ?? "project",
457
+ scope: scope ?? "global",
343
458
  };
344
459
  }
345
460
 
@@ -399,11 +514,15 @@ if (argumentsList[0] === "install-cli") {
399
514
  if (options) {
400
515
  try {
401
516
  const selected = await chooseInstallOptions(options);
402
- const targets = selected.harnessNames.map((name) => targetFor(harnessByName.get(name), selected.scope, selected.cwd, selected.home));
403
- const uniqueTargets = [...new Set(targets)];
404
- console.log(`Installing for ${selected.scope}: ${uniqueTargets.join(", ")}`);
405
- for (const target of uniqueTargets) {
406
- await install({ force: selected.force, target });
517
+ if (!selected) {
518
+ console.log("Installation cancelled.");
519
+ } else {
520
+ const targets = selected.harnessNames.map((name) => targetFor(harnessByName.get(name), selected.scope, selected.cwd, selected.home));
521
+ const uniqueTargets = [...new Set(targets)];
522
+ console.log(`Installing for ${selected.scope}: ${uniqueTargets.join(", ")}`);
523
+ for (const target of uniqueTargets) {
524
+ await install({ force: selected.force, target });
525
+ }
407
526
  }
408
527
  } catch (error) {
409
528
  console.error(`Error: ${error.message}`);