@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.
- 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 +17 -6
- package/dist/apply.d.ts.map +1 -1
- package/dist/apply.js +85 -47
- 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 +628 -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
|
// =============================================================================
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
455
|
-
const
|
|
456
|
-
console.
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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
|
-
//
|
|
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
|