@agent-loom/loom 1.0.2 → 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.
- package/README.md +69 -0
- package/dist/acp/client.d.ts +182 -0
- package/dist/acp/client.d.ts.map +1 -0
- package/dist/acp/client.js +432 -0
- package/dist/acp/client.js.map +1 -0
- package/dist/acp/index.d.ts +5 -0
- package/dist/acp/index.d.ts.map +1 -0
- package/dist/acp/index.js +3 -0
- package/dist/acp/index.js.map +1 -0
- package/dist/acp/run.d.ts +41 -0
- package/dist/acp/run.d.ts.map +1 -0
- package/dist/acp/run.js +32 -0
- package/dist/acp/run.js.map +1 -0
- package/dist/apply.d.ts +15 -6
- package/dist/apply.d.ts.map +1 -1
- package/dist/apply.js +78 -49
- package/dist/apply.js.map +1 -1
- package/dist/chat/chat.d.ts +108 -0
- package/dist/chat/chat.d.ts.map +1 -0
- package/dist/chat/chat.js +221 -0
- package/dist/chat/chat.js.map +1 -0
- package/dist/chat/discovery.d.ts +30 -0
- package/dist/chat/discovery.d.ts.map +1 -0
- package/dist/chat/discovery.js +68 -0
- package/dist/chat/discovery.js.map +1 -0
- package/dist/chat/frontmatter.d.ts +12 -0
- package/dist/chat/frontmatter.d.ts.map +1 -0
- package/dist/chat/frontmatter.js +11 -0
- package/dist/chat/frontmatter.js.map +1 -0
- package/dist/chat/index.d.ts +16 -0
- package/dist/chat/index.d.ts.map +1 -0
- package/dist/chat/index.js +11 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/registry.d.ts +73 -0
- package/dist/chat/registry.d.ts.map +1 -0
- package/dist/chat/registry.js +118 -0
- package/dist/chat/registry.js.map +1 -0
- package/dist/chat/resolve-agent.d.ts +39 -0
- package/dist/chat/resolve-agent.d.ts.map +1 -0
- package/dist/chat/resolve-agent.js +36 -0
- package/dist/chat/resolve-agent.js.map +1 -0
- package/dist/chat/suggest.d.ts +20 -0
- package/dist/chat/suggest.d.ts.map +1 -0
- package/dist/chat/suggest.js +55 -0
- package/dist/chat/suggest.js.map +1 -0
- package/dist/cli.js +627 -75
- package/dist/cli.js.map +1 -1
- package/dist/clone.d.ts +21 -3
- package/dist/clone.d.ts.map +1 -1
- package/dist/clone.js +240 -12
- package/dist/clone.js.map +1 -1
- package/dist/copilot/mcp.d.ts +48 -0
- package/dist/copilot/mcp.d.ts.map +1 -0
- package/dist/copilot/mcp.js +146 -0
- package/dist/copilot/mcp.js.map +1 -0
- package/dist/copilot/resolve.d.ts +33 -0
- package/dist/copilot/resolve.d.ts.map +1 -0
- package/dist/copilot/resolve.js +96 -0
- package/dist/copilot/resolve.js.map +1 -0
- package/dist/copilot/spawn.d.ts +51 -0
- package/dist/copilot/spawn.d.ts.map +1 -0
- package/dist/copilot/spawn.js +132 -0
- package/dist/copilot/spawn.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/launch/index.d.ts +10 -0
- package/dist/launch/index.d.ts.map +1 -0
- package/dist/launch/index.js +9 -0
- package/dist/launch/index.js.map +1 -0
- package/dist/launch/stage.d.ts +62 -0
- package/dist/launch/stage.d.ts.map +1 -0
- package/dist/launch/stage.js +108 -0
- package/dist/launch/stage.js.map +1 -0
- package/dist/manifest.d.ts +165 -18
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +980 -225
- package/dist/manifest.js.map +1 -1
- package/dist/renderers/claude.d.ts +5 -0
- package/dist/renderers/claude.d.ts.map +1 -1
- package/dist/renderers/claude.js +17 -3
- package/dist/renderers/claude.js.map +1 -1
- package/dist/renderers/copilot.d.ts +1 -1
- package/dist/renderers/copilot.d.ts.map +1 -1
- package/dist/renderers/copilot.js +205 -22
- package/dist/renderers/copilot.js.map +1 -1
- package/dist/repo-clone.js +17 -11
- package/dist/repo-clone.js.map +1 -1
- package/dist/resolve-template.d.ts +12 -4
- package/dist/resolve-template.d.ts.map +1 -1
- package/dist/resolve-template.js +39 -8
- package/dist/resolve-template.js.map +1 -1
- package/dist/run/index.d.ts +4 -0
- package/dist/run/index.d.ts.map +1 -0
- package/dist/run/index.js +2 -0
- package/dist/run/index.js.map +1 -0
- package/dist/run/run.d.ts +143 -0
- package/dist/run/run.d.ts.map +1 -0
- package/dist/run/run.js +406 -0
- package/dist/run/run.js.map +1 -0
- package/dist/search-registry.d.ts +10 -3
- package/dist/search-registry.d.ts.map +1 -1
- package/dist/search-registry.js +16 -16
- package/dist/search-registry.js.map +1 -1
- package/dist/sessions/index.d.ts +16 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +15 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/store.d.ts +56 -0
- package/dist/sessions/store.d.ts.map +1 -0
- package/dist/sessions/store.js +220 -0
- package/dist/sessions/store.js.map +1 -0
- package/dist/sessions/types.d.ts +62 -0
- package/dist/sessions/types.d.ts.map +1 -0
- package/dist/sessions/types.js +5 -0
- package/dist/sessions/types.js.map +1 -0
- package/dist/skill-fetcher.d.ts.map +1 -1
- package/dist/skill-fetcher.js +5 -6
- package/dist/skill-fetcher.js.map +1 -1
- package/dist/types.d.ts +123 -41
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -1
- package/dist/util/binary-cache.d.ts +53 -0
- package/dist/util/binary-cache.d.ts.map +1 -0
- package/dist/util/binary-cache.js +211 -0
- package/dist/util/binary-cache.js.map +1 -0
- package/dist/util/frontmatter.d.ts +53 -0
- package/dist/util/frontmatter.d.ts.map +1 -0
- package/dist/util/frontmatter.js +85 -0
- package/dist/util/frontmatter.js.map +1 -0
- package/dist/util/loom-home.d.ts +19 -0
- package/dist/util/loom-home.d.ts.map +1 -0
- package/dist/util/loom-home.js +37 -0
- package/dist/util/loom-home.js.map +1 -0
- package/dist/util/workspace-folder.d.ts +29 -0
- package/dist/util/workspace-folder.d.ts.map +1 -0
- package/dist/util/workspace-folder.js +43 -0
- package/dist/util/workspace-folder.js.map +1 -0
- package/dist/validate.d.ts +7 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +90 -17
- package/dist/validate.js.map +1 -1
- 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
|
// =============================================================================
|
|
@@ -376,7 +384,7 @@ ws
|
|
|
376
384
|
}
|
|
377
385
|
addTag(entry, tag);
|
|
378
386
|
await saveWorkspaces(data);
|
|
379
|
-
console.log(chalk.green(
|
|
387
|
+
console.log(chalk.green(`Tag "${tag}" added.`));
|
|
380
388
|
});
|
|
381
389
|
ws
|
|
382
390
|
.command('note <path> <note>')
|
|
@@ -390,7 +398,7 @@ ws
|
|
|
390
398
|
}
|
|
391
399
|
setNote(entry, note);
|
|
392
400
|
await saveWorkspaces(data);
|
|
393
|
-
console.log(chalk.green(
|
|
401
|
+
console.log(chalk.green('Note updated.'));
|
|
394
402
|
});
|
|
395
403
|
ws
|
|
396
404
|
.command('remove <path>')
|
|
@@ -398,98 +406,642 @@ ws
|
|
|
398
406
|
.action(async (path) => {
|
|
399
407
|
const removed = await removeWorkspace(path);
|
|
400
408
|
if (removed) {
|
|
401
|
-
console.log(chalk.green(
|
|
409
|
+
console.log(chalk.green('Workspace entry removed.'));
|
|
402
410
|
}
|
|
403
411
|
else {
|
|
404
412
|
console.error(chalk.red(`Workspace "${path}" not found.`));
|
|
405
413
|
process.exit(1);
|
|
406
414
|
}
|
|
407
415
|
});
|
|
408
|
-
|
|
409
|
-
// validate
|
|
410
|
-
// =============================================================================
|
|
411
|
-
program
|
|
412
|
-
.command('validate [template-dir]')
|
|
413
|
-
.description('Validate manifest.yaml schema and references')
|
|
414
|
-
.option('-r, --registry-root <dir>', 'Registry root directory (for resolving shared/ refs)')
|
|
415
|
-
.option('--all', 'Validate all templates in registry (requires --registry-root)')
|
|
416
|
-
.action(async (templateDir, opts) => {
|
|
416
|
+
async function runChatCli(agentId, passthrough, opts) {
|
|
417
417
|
try {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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;
|
|
437
491
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
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".`);
|
|
443
496
|
}
|
|
497
|
+
cloneModeOpt = opts.cloneMode;
|
|
444
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);
|
|
445
540
|
}
|
|
446
|
-
console.log(`\nSummary: ${validCount}/${templateResults.length} templates valid`);
|
|
447
|
-
if (hasError)
|
|
448
|
-
process.exit(1);
|
|
449
541
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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);
|
|
454
705
|
}
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
console.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
console.log(chalk.green(` ✓ ${s.instructions} instructions resolved`));
|
|
463
|
-
if (s.skills > 0)
|
|
464
|
-
console.log(chalk.green(` ✓ ${s.skills} skills`));
|
|
465
|
-
if (s.agents > 0)
|
|
466
|
-
console.log(chalk.green(` ✓ ${s.agents} agents resolved`));
|
|
467
|
-
if (s.mcp > 0)
|
|
468
|
-
console.log(chalk.green(` ✓ ${s.mcp} MCP servers resolved`));
|
|
469
|
-
if (s.repos > 0)
|
|
470
|
-
console.log(chalk.green(` ✓ ${s.repos} repos resolved`));
|
|
471
|
-
if (s.prompts > 0)
|
|
472
|
-
console.log(chalk.green(` ✓ ${s.prompts} prompts resolved`));
|
|
473
|
-
if (s.prerequisites > 0)
|
|
474
|
-
console.log(chalk.green(` ✓ ${s.prerequisites} prerequisites resolved`));
|
|
475
|
-
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)}`);
|
|
476
713
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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 ? ', ...' : ''))}`);
|
|
483
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;
|
|
484
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;
|
|
485
765
|
}
|
|
486
|
-
|
|
487
|
-
|
|
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}`));
|
|
488
934
|
process.exit(1);
|
|
489
935
|
}
|
|
936
|
+
updateSessionMeta(id, { note });
|
|
937
|
+
console.error(chalk.green(`Note set on ${id}`));
|
|
490
938
|
});
|
|
491
939
|
// =============================================================================
|
|
492
|
-
//
|
|
940
|
+
// completion — shell tab-completion scripts
|
|
493
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
|
+
});
|
|
494
1046
|
program.parse();
|
|
495
1047
|
//# sourceMappingURL=cli.js.map
|