@agent-loom/loom 1.0.1 → 1.0.3

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.
Files changed (145) hide show
  1. package/README.md +69 -0
  2. package/dist/acp/client.d.ts +182 -0
  3. package/dist/acp/client.d.ts.map +1 -0
  4. package/dist/acp/client.js +432 -0
  5. package/dist/acp/client.js.map +1 -0
  6. package/dist/acp/index.d.ts +5 -0
  7. package/dist/acp/index.d.ts.map +1 -0
  8. package/dist/acp/index.js +3 -0
  9. package/dist/acp/index.js.map +1 -0
  10. package/dist/acp/run.d.ts +41 -0
  11. package/dist/acp/run.d.ts.map +1 -0
  12. package/dist/acp/run.js +32 -0
  13. package/dist/acp/run.js.map +1 -0
  14. package/dist/apply.d.ts +17 -6
  15. package/dist/apply.d.ts.map +1 -1
  16. package/dist/apply.js +85 -47
  17. package/dist/apply.js.map +1 -1
  18. package/dist/chat/chat.d.ts +108 -0
  19. package/dist/chat/chat.d.ts.map +1 -0
  20. package/dist/chat/chat.js +221 -0
  21. package/dist/chat/chat.js.map +1 -0
  22. package/dist/chat/discovery.d.ts +30 -0
  23. package/dist/chat/discovery.d.ts.map +1 -0
  24. package/dist/chat/discovery.js +68 -0
  25. package/dist/chat/discovery.js.map +1 -0
  26. package/dist/chat/frontmatter.d.ts +12 -0
  27. package/dist/chat/frontmatter.d.ts.map +1 -0
  28. package/dist/chat/frontmatter.js +11 -0
  29. package/dist/chat/frontmatter.js.map +1 -0
  30. package/dist/chat/index.d.ts +16 -0
  31. package/dist/chat/index.d.ts.map +1 -0
  32. package/dist/chat/index.js +11 -0
  33. package/dist/chat/index.js.map +1 -0
  34. package/dist/chat/registry.d.ts +73 -0
  35. package/dist/chat/registry.d.ts.map +1 -0
  36. package/dist/chat/registry.js +118 -0
  37. package/dist/chat/registry.js.map +1 -0
  38. package/dist/chat/resolve-agent.d.ts +39 -0
  39. package/dist/chat/resolve-agent.d.ts.map +1 -0
  40. package/dist/chat/resolve-agent.js +36 -0
  41. package/dist/chat/resolve-agent.js.map +1 -0
  42. package/dist/chat/suggest.d.ts +20 -0
  43. package/dist/chat/suggest.d.ts.map +1 -0
  44. package/dist/chat/suggest.js +55 -0
  45. package/dist/chat/suggest.js.map +1 -0
  46. package/dist/cli.js +628 -75
  47. package/dist/cli.js.map +1 -1
  48. package/dist/clone.d.ts +21 -3
  49. package/dist/clone.d.ts.map +1 -1
  50. package/dist/clone.js +240 -12
  51. package/dist/clone.js.map +1 -1
  52. package/dist/copilot/mcp.d.ts +48 -0
  53. package/dist/copilot/mcp.d.ts.map +1 -0
  54. package/dist/copilot/mcp.js +146 -0
  55. package/dist/copilot/mcp.js.map +1 -0
  56. package/dist/copilot/resolve.d.ts +33 -0
  57. package/dist/copilot/resolve.d.ts.map +1 -0
  58. package/dist/copilot/resolve.js +96 -0
  59. package/dist/copilot/resolve.js.map +1 -0
  60. package/dist/copilot/spawn.d.ts +51 -0
  61. package/dist/copilot/spawn.d.ts.map +1 -0
  62. package/dist/copilot/spawn.js +132 -0
  63. package/dist/copilot/spawn.js.map +1 -0
  64. package/dist/index.d.ts +19 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +15 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/launch/index.d.ts +10 -0
  69. package/dist/launch/index.d.ts.map +1 -0
  70. package/dist/launch/index.js +9 -0
  71. package/dist/launch/index.js.map +1 -0
  72. package/dist/launch/stage.d.ts +62 -0
  73. package/dist/launch/stage.d.ts.map +1 -0
  74. package/dist/launch/stage.js +108 -0
  75. package/dist/launch/stage.js.map +1 -0
  76. package/dist/manifest.d.ts +165 -18
  77. package/dist/manifest.d.ts.map +1 -1
  78. package/dist/manifest.js +980 -225
  79. package/dist/manifest.js.map +1 -1
  80. package/dist/renderers/claude.d.ts +5 -0
  81. package/dist/renderers/claude.d.ts.map +1 -1
  82. package/dist/renderers/claude.js +17 -3
  83. package/dist/renderers/claude.js.map +1 -1
  84. package/dist/renderers/copilot.d.ts +1 -1
  85. package/dist/renderers/copilot.d.ts.map +1 -1
  86. package/dist/renderers/copilot.js +205 -22
  87. package/dist/renderers/copilot.js.map +1 -1
  88. package/dist/repo-clone.js +17 -11
  89. package/dist/repo-clone.js.map +1 -1
  90. package/dist/resolve-template.d.ts +12 -4
  91. package/dist/resolve-template.d.ts.map +1 -1
  92. package/dist/resolve-template.js +39 -8
  93. package/dist/resolve-template.js.map +1 -1
  94. package/dist/run/index.d.ts +4 -0
  95. package/dist/run/index.d.ts.map +1 -0
  96. package/dist/run/index.js +2 -0
  97. package/dist/run/index.js.map +1 -0
  98. package/dist/run/run.d.ts +143 -0
  99. package/dist/run/run.d.ts.map +1 -0
  100. package/dist/run/run.js +406 -0
  101. package/dist/run/run.js.map +1 -0
  102. package/dist/search-registry.d.ts +10 -3
  103. package/dist/search-registry.d.ts.map +1 -1
  104. package/dist/search-registry.js +16 -16
  105. package/dist/search-registry.js.map +1 -1
  106. package/dist/sessions/index.d.ts +16 -0
  107. package/dist/sessions/index.d.ts.map +1 -0
  108. package/dist/sessions/index.js +15 -0
  109. package/dist/sessions/index.js.map +1 -0
  110. package/dist/sessions/store.d.ts +56 -0
  111. package/dist/sessions/store.d.ts.map +1 -0
  112. package/dist/sessions/store.js +220 -0
  113. package/dist/sessions/store.js.map +1 -0
  114. package/dist/sessions/types.d.ts +62 -0
  115. package/dist/sessions/types.d.ts.map +1 -0
  116. package/dist/sessions/types.js +5 -0
  117. package/dist/sessions/types.js.map +1 -0
  118. package/dist/skill-fetcher.d.ts.map +1 -1
  119. package/dist/skill-fetcher.js +5 -6
  120. package/dist/skill-fetcher.js.map +1 -1
  121. package/dist/types.d.ts +123 -41
  122. package/dist/types.d.ts.map +1 -1
  123. package/dist/types.js +12 -0
  124. package/dist/types.js.map +1 -1
  125. package/dist/util/binary-cache.d.ts +53 -0
  126. package/dist/util/binary-cache.d.ts.map +1 -0
  127. package/dist/util/binary-cache.js +211 -0
  128. package/dist/util/binary-cache.js.map +1 -0
  129. package/dist/util/frontmatter.d.ts +53 -0
  130. package/dist/util/frontmatter.d.ts.map +1 -0
  131. package/dist/util/frontmatter.js +85 -0
  132. package/dist/util/frontmatter.js.map +1 -0
  133. package/dist/util/loom-home.d.ts +19 -0
  134. package/dist/util/loom-home.d.ts.map +1 -0
  135. package/dist/util/loom-home.js +37 -0
  136. package/dist/util/loom-home.js.map +1 -0
  137. package/dist/util/workspace-folder.d.ts +29 -0
  138. package/dist/util/workspace-folder.d.ts.map +1 -0
  139. package/dist/util/workspace-folder.js +43 -0
  140. package/dist/util/workspace-folder.js.map +1 -0
  141. package/dist/validate.d.ts +7 -1
  142. package/dist/validate.d.ts.map +1 -1
  143. package/dist/validate.js +90 -17
  144. package/dist/validate.js.map +1 -1
  145. package/package.json +31 -2
package/dist/cli.js CHANGED
@@ -18,15 +18,23 @@ import { applyTemplate } from './apply.js';
18
18
  import { cloneRegistry, resolveTemplatePath } from './clone.js';
19
19
  import { resolveTemplateSource } from './resolve-template.js';
20
20
  import { createCraftFetcher } from './skill-fetcher.js';
21
- import { validateTemplate, validateRegistry } from './validate.js';
22
21
  import { loadWorkspaces, filterWorkspaces, recordWorkspace, removeWorkspace, addTag, setNote, saveWorkspaces, } from './workspaces.js';
22
+ import { chat, listWorkspaceAgents, findWorkspaceRoot, suggestClosest, resolveAgentInWorkspace, resolveAgentFromRegistry, listRegistryAgentIds, listRegistryAgents, } from './chat/index.js';
23
+ import { stageRegistryAgent } from './launch/index.js';
24
+ import { listSessions, readSession, updateSessionMeta, } from './sessions/index.js';
23
25
  const require = createRequire(import.meta.url);
24
26
  const { version } = require('../package.json');
25
27
  const program = new Command();
26
28
  program
27
29
  .name('loom')
28
30
  .description('Template engine for coding agent workspaces')
29
- .version(version);
31
+ .version(version)
32
+ // Route options to the subcommand that owns them. Without this, options
33
+ // declared on both the verbless `loom <agent>` form and on a subcommand
34
+ // (e.g. `--clone-repos`, `--registry`) get consumed by the program-level
35
+ // verbless declaration before subcommand parsing runs, leaving the
36
+ // subcommand's option at its default. Bug repro and rationale: #186.
37
+ .enablePositionalOptions();
30
38
  // =============================================================================
31
39
  // registry
32
40
  // =============================================================================
@@ -270,6 +278,7 @@ program
270
278
  promptFn: cloneReposMode ? promptFn : undefined,
271
279
  linkFn: cloneReposMode ? linkFn : undefined,
272
280
  agents: agentArg ? agentArg.split(',').map((s) => s.trim()) : undefined,
281
+ onProgress: (msg) => console.log(chalk.dim(` ${msg}`)),
273
282
  });
274
283
  console.log(chalk.green(`✓ Template "${result.template}" applied for ${result.target}.`));
275
284
  if (result.localizedSkills.length > 0) {
@@ -375,7 +384,7 @@ ws
375
384
  }
376
385
  addTag(entry, tag);
377
386
  await saveWorkspaces(data);
378
- console.log(chalk.green(`✓ Tag "${tag}" added.`));
387
+ console.log(chalk.green(`Tag "${tag}" added.`));
379
388
  });
380
389
  ws
381
390
  .command('note <path> <note>')
@@ -389,7 +398,7 @@ ws
389
398
  }
390
399
  setNote(entry, note);
391
400
  await saveWorkspaces(data);
392
- console.log(chalk.green(`✓ Note updated.`));
401
+ console.log(chalk.green('Note updated.'));
393
402
  });
394
403
  ws
395
404
  .command('remove <path>')
@@ -397,98 +406,642 @@ ws
397
406
  .action(async (path) => {
398
407
  const removed = await removeWorkspace(path);
399
408
  if (removed) {
400
- console.log(chalk.green(`✓ Workspace entry removed.`));
409
+ console.log(chalk.green('Workspace entry removed.'));
401
410
  }
402
411
  else {
403
412
  console.error(chalk.red(`Workspace "${path}" not found.`));
404
413
  process.exit(1);
405
414
  }
406
415
  });
407
- // =============================================================================
408
- // validate
409
- // =============================================================================
410
- program
411
- .command('validate [template-dir]')
412
- .description('Validate manifest.yaml schema and references')
413
- .option('-r, --registry-root <dir>', 'Registry root directory (for resolving shared/ refs)')
414
- .option('--all', 'Validate all templates in registry (requires --registry-root)')
415
- .action(async (templateDir, opts) => {
416
+ async function runChatCli(agentId, passthrough, opts) {
416
417
  try {
417
- if (opts.all) {
418
- const registryRoot = opts.registryRoot ?? '.';
419
- const results = await validateRegistry(registryRoot);
420
- let hasError = false;
421
- const templateResults = results.filter((r) => r.templateId !== '__registry__');
422
- const registryResults = results.filter((r) => r.templateId === '__registry__');
423
- // Print registry-level errors first
424
- for (const r of registryResults) {
425
- for (const e of r.errors) {
426
- console.log(chalk.red(` ✗ ${e.message}`));
427
- hasError = true;
428
- }
429
- }
430
- // Print per-template results
431
- const validCount = templateResults.filter((r) => r.errors.length === 0).length;
432
- console.log(chalk.blue(`\nValidating registry (${templateResults.length} templates)`));
433
- for (const r of templateResults) {
434
- if (r.errors.length === 0) {
435
- console.log(chalk.green(` ✓ ${r.templateId} ......... OK`));
418
+ const cwd = opts.cwd ?? process.cwd();
419
+ const runtime = (opts.runtime ?? 'copilot');
420
+ // Prefer `-- <args...>` passthrough. Fall back to deprecated
421
+ // --runtime-args <csv> for anyone who has the old form scripted.
422
+ let extraArgs;
423
+ if (passthrough && passthrough.length > 0) {
424
+ extraArgs = passthrough;
425
+ }
426
+ else if (opts.runtimeArgs) {
427
+ extraArgs = opts.runtimeArgs.split(',');
428
+ }
429
+ // Bolt-on flags from the new verb-less form. These get appended AFTER
430
+ // any explicit passthrough so users can still override them with `-- ...`.
431
+ const boltOn = [];
432
+ if (opts.addDir)
433
+ boltOn.push('--add-dir', opts.addDir);
434
+ if (opts.model)
435
+ boltOn.push('--model', opts.model);
436
+ if (boltOn.length > 0) {
437
+ extraArgs = [...(extraArgs ?? []), ...boltOn];
438
+ }
439
+ // No agent given -> show what's available in the resolved workspace.
440
+ if (!agentId) {
441
+ printAgentList(opts.workspace ?? cwd);
442
+ console.error();
443
+ console.error(chalk.dim('Usage: loom <agent>'));
444
+ return;
445
+ }
446
+ if (runtime !== 'copilot' && runtime !== 'claude') {
447
+ console.error(chalk.red(`Unknown runtime: ${runtime}. Use copilot or claude.`));
448
+ process.exit(2);
449
+ }
450
+ const launchCwd = opts.workspace ?? cwd;
451
+ const interactive = opts.interactive !== false;
452
+ // Step 1: try in-workspace resolution (walk-up unless --workspace is set).
453
+ const local = resolveAgentInWorkspace({
454
+ agentId,
455
+ cwd: launchCwd,
456
+ strictCwd: Boolean(opts.workspace),
457
+ });
458
+ if (local) {
459
+ const result = await chat({
460
+ agentId,
461
+ cwd: launchCwd,
462
+ resolved: local,
463
+ runtime,
464
+ prompt: opts.prompt,
465
+ interactive,
466
+ extraArgs,
467
+ sessionName: opts.sessionName,
468
+ recordSession: opts.session !== false,
469
+ });
470
+ process.exit(result.exitCode ?? 0);
471
+ }
472
+ // Step 2: fall back to configured registries (unless --no-registry-fallback).
473
+ if (opts.registryFallback !== false) {
474
+ const hit = await resolveAgentFromRegistry({
475
+ agentId,
476
+ registryName: typeof opts.registry === 'string' ? opts.registry : undefined,
477
+ workspaceDir: launchCwd,
478
+ });
479
+ if (hit) {
480
+ console.error(chalk.blue(` Staging ${chalk.bold(hit.agentId)} from ${hit.registryName}/${hit.templateId} ...`));
481
+ // Resolve --clone-repos. Commander parses `--clone-repos [mode]`
482
+ // as one of: undefined (flag absent), true (flag present with no
483
+ // value), or a string. Default is 'auto' since staged launches
484
+ // need repos to do real work.
485
+ let cloneReposMode = 'auto';
486
+ if (typeof opts.cloneRepos === 'string') {
487
+ if (!['auto', 'all', 'none'].includes(opts.cloneRepos)) {
488
+ throw new Error(`Invalid --clone-repos "${opts.cloneRepos}". Must be "auto", "all", or "none".`);
489
+ }
490
+ cloneReposMode = opts.cloneRepos;
436
491
  }
437
- else {
438
- hasError = true;
439
- console.log(chalk.red(` ✗ ${r.templateId} ......... ${r.errors.length} error(s)`));
440
- for (const e of r.errors) {
441
- console.log(chalk.red(` → ${e.message}`));
492
+ let cloneModeOpt;
493
+ if (typeof opts.cloneMode === 'string') {
494
+ if (!['full', 'partial'].includes(opts.cloneMode)) {
495
+ throw new Error(`Invalid --clone-mode "${opts.cloneMode}". Must be "full" or "partial".`);
442
496
  }
497
+ cloneModeOpt = opts.cloneMode;
443
498
  }
499
+ // Interactive prompt for large/ADO repos (mirrors `loom apply`).
500
+ const stagingPromptFn = async (message, _choices) => {
501
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
502
+ return new Promise((resolveP) => {
503
+ rl.question(chalk.yellow(` ${message}\n [clone / skip / enter path to existing clone]: `), (answer) => {
504
+ rl.close();
505
+ resolveP(answer.trim() || 'skip');
506
+ });
507
+ });
508
+ };
509
+ const stagingLinkFn = async (target, linkPath) => {
510
+ if (platform() === 'win32') {
511
+ execSync(`cmd /c mklink /J "${linkPath}" "${target}"`, { stdio: 'pipe' });
512
+ }
513
+ else {
514
+ const { symlink } = await import('node:fs/promises');
515
+ await symlink(target, linkPath, 'junction');
516
+ }
517
+ };
518
+ const staged = await stageRegistryAgent({
519
+ hit,
520
+ sessionName: opts.sessionName,
521
+ runtime,
522
+ onProgress: (m) => console.error(chalk.dim(` ${m}`)),
523
+ cloneRepos: cloneReposMode,
524
+ cloneMode: cloneModeOpt,
525
+ promptFn: cloneReposMode !== 'none' ? stagingPromptFn : undefined,
526
+ linkFn: cloneReposMode !== 'none' ? stagingLinkFn : undefined,
527
+ });
528
+ console.error(chalk.green(` Staged at ${staged.session.dir}`));
529
+ const result = await chat({
530
+ agentId,
531
+ cwd: staged.agent.workspaceDir,
532
+ resolved: staged.agent,
533
+ session: staged.session,
534
+ runtime,
535
+ prompt: opts.prompt,
536
+ interactive,
537
+ extraArgs,
538
+ });
539
+ process.exit(result.exitCode ?? 0);
444
540
  }
445
- console.log(`\nSummary: ${validCount}/${templateResults.length} templates valid`);
446
- if (hasError)
447
- process.exit(1);
448
541
  }
449
- else {
450
- if (!templateDir) {
451
- console.error(chalk.red('Please provide a template directory or use --all'));
452
- process.exit(1);
542
+ // Step 3: neither found -- friendly error with suggestions from both layers.
543
+ await printNotFoundHelp(agentId, launchCwd, opts.registryFallback !== false);
544
+ process.exit(1);
545
+ }
546
+ catch (err) {
547
+ const msg = err.message ?? String(err);
548
+ console.error(chalk.red(msg));
549
+ process.exit(1);
550
+ }
551
+ }
552
+ /**
553
+ * Attach the chat option set to a Commander command.
554
+ */
555
+ function applyChatOptions(cmd) {
556
+ return cmd
557
+ .option('-p, --prompt <text>', 'Prefilled / one-shot prompt (use --no-interactive for headless single-shot)')
558
+ .option('--no-interactive', 'Run non-interactively (single-shot, exits after the prompt)')
559
+ .option('--add-dir <path>', 'Additional directory passed to the runtime via --add-dir')
560
+ .option('--model <name>', 'Model override (forwarded to the runtime via --model)')
561
+ .option('-r, --runtime <runtime>', 'Runtime: copilot or claude', 'copilot')
562
+ .option('-w, --workspace <dir>', 'Override workspace (skips walk-up discovery)')
563
+ .option('--cwd <dir>', 'Working directory (defaults to current)')
564
+ .option('--runtime-args <args>', '(deprecated) comma-separated runtime args; prefer `-- <args...>`')
565
+ .option('--session-name <name>', 'Friendly label stored on the session record')
566
+ .option('--no-session', 'Do not record a session under ~/.loom/sessions/')
567
+ .option('--no-registry-fallback', 'Disable the registry fallback (fail fast if agent not in workspace)')
568
+ .option('--registry <name>', 'Only search the named registry when falling back')
569
+ .option('--clone-repos [mode]', 'When staging from a registry, clone manifest repos: auto (default), all, or none')
570
+ .option('--clone-mode <strategy>', 'Clone strategy: full (default) or partial (blobless)');
571
+ }
572
+ // Verb-less default: `loom <agent> [-- ...]`. When no agent is given (and
573
+ // no subcommand matched), this falls through to the agent-list helper.
574
+ applyChatOptions(program
575
+ .argument('[agent]', 'Agent name to launch interactively (verb-less form)')
576
+ .argument('[passthrough...]', 'Args forwarded to the runtime (after --)')
577
+ .description('Launch a single agent interactively (verb-less form). Falls back to configured registries if the agent is not in the current workspace.')
578
+ .addHelpText('after', `
579
+ Examples:
580
+ $ loom triage Launch 'triage' interactively
581
+ $ loom triage -p "investigate X" Launch with a prefilled prompt
582
+ $ loom triage --model opus Use a specific model
583
+ $ loom triage -- --some-runtime-flag Pass --some-runtime-flag through
584
+ $ loom -w C:\\path\\to\\ws triage Use explicit workspace (no walk-up)
585
+ $ loom List available agents (no agent given)
586
+
587
+ For headless / one-shot execution see \`loom run\` (or import from
588
+ '@agent-loom/loom/run' as an SDK).
589
+ `)).action(async (agentId, passthrough, opts) => {
590
+ await runChatCli(agentId, passthrough, opts);
591
+ });
592
+ // =============================================================================
593
+ // run -- headless single-agent execution (SDK-equivalent CLI surface)
594
+ // =============================================================================
595
+ program
596
+ .command('run <template> <agent>')
597
+ .description('Run an agent headlessly and collect artifacts (SESSION, REPORT, PROGRESS)')
598
+ .requiredOption('--prompt <text>', 'Prompt text or @filepath')
599
+ .requiredOption('--work-dir <path>', 'Working directory for the run (required, no temp default)')
600
+ .option('--model <name>', 'Model override')
601
+ .option('--timeout-ms <n>', 'Timeout in milliseconds', (v) => parseInt(v, 10))
602
+ .action(async (template, agentId, opts) => {
603
+ const { run: runFn } = await import('./run/index.js');
604
+ try {
605
+ const result = await runFn({
606
+ template,
607
+ agent: agentId,
608
+ workDir: opts.workDir,
609
+ prompt: opts.prompt,
610
+ model: opts.model,
611
+ // Library handles the default (DEFAULT_RUN_TIMEOUT_MS); only override
612
+ // when the user supplied --timeout-ms explicitly.
613
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
614
+ });
615
+ if (result.artifactPaths.session)
616
+ console.log(`SESSION: ${result.artifactPaths.session}`);
617
+ if (result.artifactPaths.report)
618
+ console.log(`REPORT: ${result.artifactPaths.report}`);
619
+ if (result.artifactPaths.progress)
620
+ console.log(`PROGRESS: ${result.artifactPaths.progress}`);
621
+ if (result.timedOut)
622
+ console.error(chalk.red('Run timed out.'));
623
+ process.exit(result.exitCode);
624
+ }
625
+ catch (err) {
626
+ console.error(chalk.red(err.message));
627
+ process.exit(1);
628
+ }
629
+ });
630
+ /** Helper: aggregate workspace + registry agent ids for the not-found message. */
631
+ async function printNotFoundHelp(agentId, cwd, registryEnabled) {
632
+ console.error(chalk.red(`Agent "${agentId}" not found.`));
633
+ const workspace = findWorkspaceRoot(cwd);
634
+ const localIds = workspace ? listWorkspaceAgents(workspace).map((a) => a.agentId) : [];
635
+ const registryIds = registryEnabled
636
+ ? await listRegistryAgentIds({ workspaceDir: cwd }).catch(() => [])
637
+ : [];
638
+ const pool = Array.from(new Set([...localIds, ...registryIds]));
639
+ const suggestions = suggestClosest(agentId, pool);
640
+ if (suggestions.length > 0) {
641
+ console.error();
642
+ console.error(chalk.yellow(`Did you mean: ${suggestions.join(', ')}?`));
643
+ }
644
+ if (workspace && localIds.length > 0) {
645
+ console.error();
646
+ console.error(chalk.dim(`In workspace ${workspace}:`));
647
+ for (const a of localIds)
648
+ console.error(chalk.dim(` ${a}`));
649
+ }
650
+ if (registryEnabled && registryIds.length > 0) {
651
+ console.error();
652
+ console.error(chalk.dim(`In configured registries:`));
653
+ for (const a of registryIds)
654
+ console.error(chalk.dim(` ${a}`));
655
+ }
656
+ if (pool.length === 0) {
657
+ console.error();
658
+ console.error(chalk.dim('No agents found locally and no registries configured.'));
659
+ console.error(chalk.dim('Add a registry with: loom registry add <name> <url>'));
660
+ }
661
+ }
662
+ // =============================================================================
663
+ // agents — list agents in the current workspace
664
+ // =============================================================================
665
+ program
666
+ .command('agents [scope]')
667
+ .description('List agents in the current workspace (use --all for every registry-installable agent). Scope drills: <registry> or <registry>/<template>')
668
+ .option('--cwd <dir>', 'Working directory (defaults to current)')
669
+ .option('--names-only', 'Output just agent ids, one per line (for scripts / tab completion)')
670
+ .option('--all', 'List every agent available across configured registries')
671
+ .action(async (scope, opts) => {
672
+ const cwd = opts.cwd ?? process.cwd();
673
+ if (opts.all) {
674
+ const parts = (scope ?? '').split('/').map((s) => s.trim()).filter(Boolean);
675
+ const registryFilter = parts[0];
676
+ const templateFilter = parts[1];
677
+ const listings = await listRegistryAgents({
678
+ workspaceDir: cwd,
679
+ registryName: registryFilter,
680
+ });
681
+ if (opts.namesOnly) {
682
+ const filtered = templateFilter
683
+ ? listings.filter((l) => l.templateId === templateFilter)
684
+ : listings;
685
+ for (const l of filtered)
686
+ process.stdout.write(l.agentId + '\n');
687
+ return;
688
+ }
689
+ if (listings.length === 0) {
690
+ console.error(chalk.yellow(registryFilter
691
+ ? `No agents found in registry "${registryFilter}".`
692
+ : 'No agents found in configured registries.'));
693
+ console.error(chalk.dim(' Add one: loom registry add <name> <url>'));
694
+ return;
695
+ }
696
+ // Level 1: no scope → registry summary (skipped when only one registry)
697
+ const allRegistryNames = Array.from(new Set(listings.map((l) => l.registryName)));
698
+ if (!registryFilter && allRegistryNames.length > 1) {
699
+ const byReg = new Map();
700
+ for (const l of listings) {
701
+ const e = byReg.get(l.registryName) ?? { teams: new Set(), agents: 0 };
702
+ e.teams.add(l.templateId);
703
+ e.agents++;
704
+ byReg.set(l.registryName, e);
453
705
  }
454
- const registryRoot = opts.registryRoot ?? templateDir.replace(/[/\\]templates[/\\].*$/, '');
455
- const result = await validateTemplate(templateDir, registryRoot);
456
- console.log(chalk.blue(`\nValidating template: ${result.templateId}`));
457
- if (result.errors.length === 0) {
458
- console.log(chalk.green(' ✓ manifest.yaml valid'));
459
- const s = result.summary;
460
- if (s.instructions > 0)
461
- console.log(chalk.green(` ✓ ${s.instructions} instructions resolved`));
462
- if (s.skills > 0)
463
- console.log(chalk.green(` ✓ ${s.skills} skills`));
464
- if (s.agents > 0)
465
- console.log(chalk.green(` ✓ ${s.agents} agents resolved`));
466
- if (s.mcp > 0)
467
- console.log(chalk.green(` ✓ ${s.mcp} MCP servers resolved`));
468
- if (s.repos > 0)
469
- console.log(chalk.green(` ✓ ${s.repos} repos resolved`));
470
- if (s.prompts > 0)
471
- console.log(chalk.green(` ✓ ${s.prompts} prompts resolved`));
472
- if (s.prerequisites > 0)
473
- console.log(chalk.green(` ✓ ${s.prerequisites} prerequisites resolved`));
474
- console.log(chalk.green('\n0 errors'));
706
+ const rows = Array.from(byReg.entries()).sort((a, b) => a[0].localeCompare(b[0]));
707
+ const regCol = Math.max(8, ...rows.map((r) => r[0].length));
708
+ console.error(chalk.bold.dim(` ${'REGISTRY'.padEnd(regCol)} TEAMS AGENTS`));
709
+ console.error(chalk.dim(' ' + '-'.repeat(regCol + 18)));
710
+ for (const [name, e] of rows) {
711
+ console.error(` ${chalk.bold(name.padEnd(regCol))} ` +
712
+ `${String(e.teams.size).padStart(5)} ${String(e.agents).padStart(6)}`);
475
713
  }
476
- else {
477
- for (const e of result.errors) {
478
- console.log(chalk.red(` ✗ [${e.level}] ${e.field}: ${e.message}`));
479
- }
480
- console.log(chalk.red(`\n${result.errors.length} error(s)`));
481
- process.exit(1);
714
+ console.error(chalk.dim(`\n Drill down: loom agents --all <registry>\n` +
715
+ ` e.g. loom agents --all ${rows[0][0]}`));
716
+ return;
717
+ }
718
+ // Level 2: registry scope (or single-registry auto-collapse) → team summary
719
+ const effectiveRegistry = registryFilter ?? allRegistryNames[0];
720
+ if (!templateFilter) {
721
+ const byTeam = new Map();
722
+ for (const l of listings) {
723
+ if (l.registryName !== effectiveRegistry)
724
+ continue;
725
+ const e = byTeam.get(l.templateId) ?? { agents: 0, sample: [] };
726
+ e.agents++;
727
+ if (e.sample.length < 3)
728
+ e.sample.push(l.agentId);
729
+ byTeam.set(l.templateId, e);
730
+ }
731
+ const rows = Array.from(byTeam.entries()).sort((a, b) => a[0].localeCompare(b[0]));
732
+ const teamCol = Math.max(4, ...rows.map((r) => r[0].length));
733
+ console.error(chalk.bold(effectiveRegistry) + chalk.dim(allRegistryNames.length > 1 ? '' : ' (only registry)'));
734
+ console.error();
735
+ console.error(chalk.bold.dim(` ${'TEAM'.padEnd(teamCol)} AGENTS SAMPLE`));
736
+ console.error(chalk.dim(' ' + '-'.repeat(teamCol + 40)));
737
+ for (const [name, e] of rows) {
738
+ console.error(` ${chalk.bold(name.padEnd(teamCol))} ` +
739
+ `${String(e.agents).padStart(6)} ` +
740
+ `${chalk.dim(e.sample.join(', ') + (e.agents > e.sample.length ? ', ...' : ''))}`);
482
741
  }
742
+ console.error(chalk.dim(`\n Drill down: loom agents --all ${effectiveRegistry}/<team>\n` +
743
+ ` e.g. loom agents --all ${effectiveRegistry}/${rows[0][0]}`));
744
+ return;
745
+ }
746
+ // Level 3: registry/team scope → agent list
747
+ const filtered = listings.filter((l) => l.registryName === effectiveRegistry && l.templateId === templateFilter);
748
+ if (filtered.length === 0) {
749
+ console.error(chalk.yellow(`No team "${templateFilter}" in registry "${effectiveRegistry}".`));
750
+ console.error(chalk.dim(` List teams: loom agents --all ${effectiveRegistry}`));
751
+ return;
483
752
  }
753
+ const idCol = Math.max(5, ...filtered.map((l) => l.agentId.length));
754
+ console.error(chalk.bold(`${effectiveRegistry} / ${templateFilter}`));
755
+ console.error();
756
+ console.error(chalk.bold.dim(` ${'AGENT'.padEnd(idCol)} DESCRIPTION`));
757
+ console.error(chalk.dim(' ' + '-'.repeat(idCol + 60)));
758
+ for (const l of filtered) {
759
+ const desc = l.description ?? chalk.dim('(no description)');
760
+ console.error(` ${chalk.bold(l.agentId.padEnd(idCol))} ${chalk.dim(desc)}`);
761
+ }
762
+ console.error(chalk.dim(`\n ${filtered.length} ${filtered.length === 1 ? 'agent' : 'agents'}. ` +
763
+ `Launch one with: loom <agent>`));
764
+ return;
484
765
  }
485
- catch (err) {
486
- console.error(chalk.red(`Error: ${err.message}`));
766
+ if (opts.namesOnly) {
767
+ const workspace = findWorkspaceRoot(cwd);
768
+ if (!workspace)
769
+ return;
770
+ for (const a of listWorkspaceAgents(workspace)) {
771
+ process.stdout.write(a.agentId + '\n');
772
+ }
773
+ return;
774
+ }
775
+ printAgentList(cwd);
776
+ });
777
+ function printAgentList(cwd) {
778
+ const workspace = findWorkspaceRoot(cwd);
779
+ if (!workspace) {
780
+ console.error(chalk.yellow('No workspace found.'));
781
+ console.error(chalk.dim(` Walked up from: ${cwd}\n` +
782
+ ` Looking for an ancestor containing .github/agents/`));
783
+ return;
784
+ }
785
+ const agents = listWorkspaceAgents(workspace);
786
+ console.error(chalk.dim(`workspace: ${workspace}`));
787
+ console.error();
788
+ if (agents.length === 0) {
789
+ console.error(chalk.yellow('No agents found in .github/agents/'));
790
+ console.error(chalk.dim(' See what you could install: loom agents --all'));
791
+ return;
792
+ }
793
+ const idCol = Math.max(5, ...agents.map((a) => a.agentId.length));
794
+ console.error(chalk.bold.dim(` ${'AGENT'.padEnd(idCol)} DESCRIPTION`));
795
+ console.error(chalk.dim(' ' + '-'.repeat(idCol + 2 + 60)));
796
+ for (const a of agents) {
797
+ const name = a.metadata.name ?? '';
798
+ const desc = a.metadata.description ?? chalk.dim('(no description)');
799
+ const nameCell = name && name !== a.agentId ? chalk.dim(` ${name}`) : '';
800
+ console.error(` ${chalk.bold(a.agentId.padEnd(idCol))}${nameCell}`);
801
+ if (a.metadata.description)
802
+ console.error(` ${chalk.dim(desc)}`);
803
+ }
804
+ console.error(chalk.dim(`\n ${agents.length} ${agents.length === 1 ? 'agent' : 'agents'}. ` +
805
+ `Launch one with: loom <agent>`));
806
+ }
807
+ // =============================================================================
808
+ // sessions — inspect & manage session records
809
+ // =============================================================================
810
+ const sessions = program
811
+ .command('sessions')
812
+ .description('Inspect launch sessions (~/.loom/sessions/)');
813
+ sessions
814
+ .command('list')
815
+ .description('List recent sessions, newest first')
816
+ .option('-n, --limit <n>', 'Max sessions to show', '20')
817
+ .option('--status <s>', 'Filter by status (running|exited|errored)')
818
+ .option('--agent <id>', 'Filter by agent id')
819
+ .option('--ids-only', 'Output just session ids, one per line (for scripts / tab completion)')
820
+ .action((opts) => {
821
+ const limit = Math.max(1, parseInt(opts.limit, 10) || 20);
822
+ let all = listSessions();
823
+ if (opts.status)
824
+ all = all.filter((s) => s.meta.status === opts.status);
825
+ if (opts.agent)
826
+ all = all.filter((s) => s.meta.agentId === opts.agent);
827
+ if (opts.idsOnly) {
828
+ for (const s of all.slice(0, limit))
829
+ process.stdout.write(s.meta.id + '\n');
830
+ return;
831
+ }
832
+ if (all.length === 0) {
833
+ console.error(chalk.dim('No sessions found.'));
834
+ return;
835
+ }
836
+ const shown = all.slice(0, limit);
837
+ // Column widths sized to content; keep status at a fixed width for color padding.
838
+ const idW = Math.max(2, ...shown.map((s) => s.meta.id.length));
839
+ const agentW = Math.max(5, ...shown.map((s) => s.meta.agentId.length));
840
+ const whenW = 19; // YYYY-MM-DD HH:MM:SS
841
+ const statusW = 7; // 'running' is longest
842
+ const header = ` ${'ID'.padEnd(idW)} ${'STATUS'.padEnd(statusW)} ${'STARTED'.padEnd(whenW)} ${'AGENT'.padEnd(agentW)} EXIT NAME`;
843
+ console.log(chalk.bold.dim(header));
844
+ console.log(chalk.dim(' ' + '-'.repeat(header.length - 2)));
845
+ for (const s of shown) {
846
+ const status = s.meta.status.padEnd(statusW);
847
+ const colored = s.meta.status === 'running' ? chalk.cyan(status) :
848
+ s.meta.status === 'exited' && s.meta.exitCode === 0 ? chalk.green(status) :
849
+ chalk.red(status);
850
+ const when = s.meta.startedAt.slice(0, 19).replace('T', ' ');
851
+ const exit = s.meta.exitCode == null ? ' - ' : String(s.meta.exitCode).padEnd(4);
852
+ const name = s.meta.name ? chalk.dim(`"${s.meta.name}"`) : '';
853
+ console.log(` ${s.meta.id.padEnd(idW)} ${colored} ${chalk.dim(when)} ${chalk.bold(s.meta.agentId.padEnd(agentW))} ${exit} ${name}`);
854
+ }
855
+ const total = all.length;
856
+ const hidden = total - shown.length;
857
+ const footer = hidden > 0
858
+ ? chalk.dim(`\n Showing ${shown.length} of ${total}. Use -n <N> to show more.`)
859
+ : chalk.dim(`\n ${total} ${total === 1 ? 'session' : 'sessions'}.`);
860
+ console.error(footer);
861
+ });
862
+ sessions
863
+ .command('show <id>')
864
+ .description('Show full metadata for a session')
865
+ .action((id) => {
866
+ const s = readSession(id);
867
+ if (!s) {
868
+ console.error(chalk.red(`Session not found: ${id}`));
869
+ process.exit(1);
870
+ }
871
+ const m = s.meta;
872
+ const statusColored = m.status === 'running' ? chalk.cyan(m.status) :
873
+ m.status === 'exited' && m.exitCode === 0 ? chalk.green(m.status) :
874
+ chalk.red(m.status);
875
+ console.log(chalk.bold(m.id));
876
+ console.log(chalk.dim(s.dir));
877
+ console.log();
878
+ const row = (label, value) => console.log(` ${chalk.dim(label.padEnd(10))} ${value}`);
879
+ if (m.name)
880
+ row('name', m.name);
881
+ row('agent', m.agentName && m.agentName !== m.agentId
882
+ ? `${m.agentId} ${chalk.dim(`(${m.agentName})`)}` : m.agentId);
883
+ row('runtime', m.runtime);
884
+ row('origin', m.origin);
885
+ row('workspace', m.workspaceDir);
886
+ row('status', m.exitCode != null ? `${statusColored} ${chalk.dim(`exit=${m.exitCode}`)}` : statusColored);
887
+ row('started', m.startedAt);
888
+ if (m.endedAt)
889
+ row('ended', m.endedAt);
890
+ if (m.durationMs != null)
891
+ row('duration', humanizeDuration(m.durationMs));
892
+ if (m.tags && m.tags.length > 0)
893
+ row('tags', m.tags.join(', '));
894
+ if (m.note)
895
+ row('note', m.note);
896
+ console.log();
897
+ console.log(chalk.dim(` Tag: loom sessions tag ${m.id} <tag>`));
898
+ console.log(chalk.dim(` Note: loom sessions note ${m.id} "<text>"`));
899
+ });
900
+ function humanizeDuration(ms) {
901
+ if (ms < 1000)
902
+ return `${ms}ms`;
903
+ const s = Math.floor(ms / 1000);
904
+ if (s < 60)
905
+ return `${s}s`;
906
+ const m = Math.floor(s / 60);
907
+ const rs = s % 60;
908
+ if (m < 60)
909
+ return rs > 0 ? `${m}m ${rs}s` : `${m}m`;
910
+ const h = Math.floor(m / 60);
911
+ const rm = m % 60;
912
+ return rm > 0 ? `${h}h ${rm}m` : `${h}h`;
913
+ }
914
+ sessions
915
+ .command('tag <id> <tag>')
916
+ .description('Add a tag to a session')
917
+ .action((id, tag) => {
918
+ const s = readSession(id);
919
+ if (!s) {
920
+ console.error(chalk.red(`Session not found: ${id}`));
921
+ process.exit(1);
922
+ }
923
+ const tags = Array.from(new Set([...(s.meta.tags ?? []), tag]));
924
+ updateSessionMeta(id, { tags });
925
+ console.error(chalk.green(`Tagged ${id} with "${tag}"`));
926
+ });
927
+ sessions
928
+ .command('note <id> <note>')
929
+ .description('Set a note on a session')
930
+ .action((id, note) => {
931
+ const s = readSession(id);
932
+ if (!s) {
933
+ console.error(chalk.red(`Session not found: ${id}`));
487
934
  process.exit(1);
488
935
  }
936
+ updateSessionMeta(id, { note });
937
+ console.error(chalk.green(`Note set on ${id}`));
489
938
  });
490
939
  // =============================================================================
491
- // Run
940
+ // completion — shell tab-completion scripts
492
941
  // =============================================================================
942
+ const POWERSHELL_COMPLETER = [
943
+ '# loom PowerShell tab completion',
944
+ '# Install: Add to your $PROFILE, or: loom completion powershell | Out-String | Invoke-Expression',
945
+ 'Register-ArgumentCompleter -Native -CommandName loom -ScriptBlock {',
946
+ ' param($wordToComplete, $commandAst, $cursorPosition)',
947
+ ' $tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }',
948
+ ' $sub = if ($tokens.Count -ge 2) { $tokens[1] } else { \'\' }',
949
+ '',
950
+ ' $subcommands = @(',
951
+ ' \'run\',\'agents\',\'sessions\',\'apply\',\'search\',\'registry\',\'workspaces\',',
952
+ ' \'completion\',\'validate\',\'scaffold\'',
953
+ ' )',
954
+ '',
955
+ ' if ($tokens.Count -le 1 -or ($tokens.Count -eq 2 -and $wordToComplete)) {',
956
+ ' $subcommands |',
957
+ ' Where-Object { $_ -like "$wordToComplete*" } |',
958
+ ' ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, \'ParameterValue\', $_) }',
959
+ ' return',
960
+ ' }',
961
+ '',
962
+ ' switch ($sub) {',
963
+ ' \'run\' {',
964
+ ' if (-not $wordToComplete.StartsWith(\'-\')) {',
965
+ ' try {',
966
+ ' (& loom agents --names-only 2>$null) -split [Environment]::NewLine |',
967
+ ' Where-Object { $_ -and $_ -like "$wordToComplete*" } |',
968
+ ' ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, \'ParameterValue\', $_) }',
969
+ ' } catch {}',
970
+ ' }',
971
+ ' }',
972
+ ' \'sessions\' {',
973
+ ' $sessionSub = if ($tokens.Count -ge 3) { $tokens[2] } else { \'\' }',
974
+ ' if (@(\'show\',\'tag\',\'note\') -contains $sessionSub -and -not $wordToComplete.StartsWith(\'-\')) {',
975
+ ' try {',
976
+ ' (& loom sessions list --ids-only -n 50 2>$null) -split [Environment]::NewLine |',
977
+ ' Where-Object { $_ -and $_ -like "$wordToComplete*" } |',
978
+ ' ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, \'ParameterValue\', $_) }',
979
+ ' } catch {}',
980
+ ' } elseif ($tokens.Count -le 2 -or ($tokens.Count -eq 3 -and $wordToComplete)) {',
981
+ ' @(\'list\',\'show\',\'tag\',\'note\') |',
982
+ ' Where-Object { $_ -like "$wordToComplete*" } |',
983
+ ' ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, \'ParameterValue\', $_) }',
984
+ ' }',
985
+ ' }',
986
+ ' }',
987
+ '}',
988
+ '',
989
+ ].join('\n');
990
+ const BASH_COMPLETER = [
991
+ '# loom bash tab completion',
992
+ '# Install: loom completion bash > /etc/bash_completion.d/loom (or source in .bashrc)',
993
+ '_loom_completion() {',
994
+ ' local cur prev words cword',
995
+ ' COMPREPLY=()',
996
+ ' cur="${COMP_WORDS[COMP_CWORD]}"',
997
+ ' prev="${COMP_WORDS[COMP_CWORD-1]}"',
998
+ ' local sub="${COMP_WORDS[1]:-}"',
999
+ ' local subcommands="run agents sessions apply search registry workspaces completion validate scaffold"',
1000
+ '',
1001
+ ' if [ "$COMP_CWORD" -eq 1 ]; then',
1002
+ ' COMPREPLY=( $(compgen -W "$subcommands" -- "$cur") )',
1003
+ ' return 0',
1004
+ ' fi',
1005
+ '',
1006
+ ' case "$sub" in',
1007
+ ' run)',
1008
+ ' if [[ "$cur" != -* && "$prev" != -* ]]; then',
1009
+ ' local agents',
1010
+ ' agents=$(loom agents --names-only 2>/dev/null)',
1011
+ ' COMPREPLY=( $(compgen -W "$agents" -- "$cur") )',
1012
+ ' fi',
1013
+ ' ;;',
1014
+ ' sessions)',
1015
+ ' local ssub="${COMP_WORDS[2]:-}"',
1016
+ ' if [[ "$ssub" =~ ^(show|tag|note)$ && "$cur" != -* ]]; then',
1017
+ ' local ids',
1018
+ ' ids=$(loom sessions list --ids-only -n 50 2>/dev/null)',
1019
+ ' COMPREPLY=( $(compgen -W "$ids" -- "$cur") )',
1020
+ ' elif [ "$COMP_CWORD" -eq 2 ]; then',
1021
+ ' COMPREPLY=( $(compgen -W "list show tag note" -- "$cur") )',
1022
+ ' fi',
1023
+ ' ;;',
1024
+ ' esac',
1025
+ '}',
1026
+ 'complete -F _loom_completion loom',
1027
+ '',
1028
+ ].join('\n');
1029
+ program
1030
+ .command('completion <shell>')
1031
+ .description('Emit tab-completion script for powershell or bash')
1032
+ .action((shell) => {
1033
+ const s = shell.toLowerCase();
1034
+ if (s === 'powershell' || s === 'pwsh') {
1035
+ process.stdout.write(POWERSHELL_COMPLETER);
1036
+ }
1037
+ else if (s === 'bash') {
1038
+ process.stdout.write(BASH_COMPLETER);
1039
+ }
1040
+ else {
1041
+ console.error(chalk.red(`Unsupported shell: ${shell}`));
1042
+ console.error(chalk.dim('Supported: powershell, bash'));
1043
+ process.exit(1);
1044
+ }
1045
+ });
493
1046
  program.parse();
494
1047
  //# sourceMappingURL=cli.js.map