@phnx-labs/agents-cli 1.19.2 → 1.20.0

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 (103) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +69 -9
  3. package/dist/browser.js +0 -0
  4. package/dist/commands/browser.js +88 -16
  5. package/dist/commands/cli.d.ts +14 -0
  6. package/dist/commands/cli.js +244 -0
  7. package/dist/commands/commands.js +3 -3
  8. package/dist/commands/computer.js +18 -1
  9. package/dist/commands/doctor.d.ts +1 -1
  10. package/dist/commands/doctor.js +2 -2
  11. package/dist/commands/exec.js +3 -3
  12. package/dist/commands/factory.d.ts +3 -14
  13. package/dist/commands/factory.js +3 -3
  14. package/dist/commands/hooks.js +3 -3
  15. package/dist/commands/plugins.js +11 -4
  16. package/dist/commands/profiles.js +1 -1
  17. package/dist/commands/prune.js +39 -160
  18. package/dist/commands/pull.js +56 -3
  19. package/dist/commands/routines.js +106 -13
  20. package/dist/commands/secrets.js +5 -7
  21. package/dist/commands/sessions.d.ts +28 -0
  22. package/dist/commands/sessions.js +98 -33
  23. package/dist/commands/setup.d.ts +1 -0
  24. package/dist/commands/setup.js +37 -28
  25. package/dist/commands/skills.js +3 -3
  26. package/dist/commands/teams.js +13 -0
  27. package/dist/commands/versions.d.ts +4 -3
  28. package/dist/commands/versions.js +131 -127
  29. package/dist/commands/view.js +12 -12
  30. package/dist/computer.js +0 -0
  31. package/dist/index.js +34 -6
  32. package/dist/lib/acp/harnesses.js +8 -0
  33. package/dist/lib/agents.js +110 -23
  34. package/dist/lib/browser/cdp.d.ts +8 -1
  35. package/dist/lib/browser/cdp.js +40 -3
  36. package/dist/lib/browser/chrome.d.ts +13 -0
  37. package/dist/lib/browser/chrome.js +42 -3
  38. package/dist/lib/browser/domain-skills.d.ts +51 -0
  39. package/dist/lib/browser/domain-skills.js +157 -0
  40. package/dist/lib/browser/drivers/local.js +45 -4
  41. package/dist/lib/browser/drivers/ssh.js +1 -1
  42. package/dist/lib/browser/ipc.d.ts +8 -1
  43. package/dist/lib/browser/ipc.js +37 -28
  44. package/dist/lib/browser/profiles.d.ts +13 -0
  45. package/dist/lib/browser/profiles.js +41 -1
  46. package/dist/lib/browser/service.d.ts +3 -0
  47. package/dist/lib/browser/service.js +21 -5
  48. package/dist/lib/browser/types.d.ts +7 -0
  49. package/dist/lib/cli-resources.d.ts +109 -0
  50. package/dist/lib/cli-resources.js +255 -0
  51. package/dist/lib/cloud/rush.js +5 -5
  52. package/dist/lib/command-skills.js +0 -2
  53. package/dist/lib/computer-rpc.d.ts +3 -0
  54. package/dist/lib/computer-rpc.js +53 -0
  55. package/dist/lib/daemon.js +20 -0
  56. package/dist/lib/exec.d.ts +3 -2
  57. package/dist/lib/exec.js +44 -9
  58. package/dist/lib/hooks.js +182 -0
  59. package/dist/lib/mcp.js +6 -0
  60. package/dist/lib/migrate.js +1 -1
  61. package/dist/lib/overdue.d.ts +26 -0
  62. package/dist/lib/overdue.js +101 -0
  63. package/dist/lib/permissions.js +5 -1
  64. package/dist/lib/plugin-marketplace.js +1 -1
  65. package/dist/lib/profiles-presets.js +37 -0
  66. package/dist/lib/resources/mcp.js +37 -0
  67. package/dist/lib/resources.d.ts +1 -1
  68. package/dist/lib/rotate.js +10 -4
  69. package/dist/lib/routines-format.d.ts +35 -0
  70. package/dist/lib/routines-format.js +173 -0
  71. package/dist/lib/routines.d.ts +7 -1
  72. package/dist/lib/routines.js +32 -12
  73. package/dist/lib/runner.js +19 -5
  74. package/dist/lib/scheduler.js +8 -1
  75. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  76. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  77. package/dist/lib/secrets/bundles.d.ts +22 -1
  78. package/dist/lib/secrets/bundles.js +234 -36
  79. package/dist/lib/secrets/index.d.ts +6 -11
  80. package/dist/lib/secrets/index.js +107 -87
  81. package/dist/lib/session/active.d.ts +8 -0
  82. package/dist/lib/session/active.js +3 -2
  83. package/dist/lib/session/db.d.ts +0 -4
  84. package/dist/lib/session/db.js +0 -26
  85. package/dist/lib/session/parse.d.ts +1 -0
  86. package/dist/lib/session/parse.js +44 -0
  87. package/dist/lib/session/types.d.ts +1 -1
  88. package/dist/lib/session/types.js +1 -1
  89. package/dist/lib/shims.d.ts +1 -1
  90. package/dist/lib/shims.js +66 -4
  91. package/dist/lib/state.d.ts +0 -1
  92. package/dist/lib/state.js +2 -15
  93. package/dist/lib/teams/agents.js +1 -1
  94. package/dist/lib/teams/parsers.d.ts +1 -1
  95. package/dist/lib/teams/parsers.js +153 -3
  96. package/dist/lib/teams/summarizer.js +18 -2
  97. package/dist/lib/teams/worktree.js +14 -3
  98. package/dist/lib/types.d.ts +6 -3
  99. package/dist/lib/types.js +6 -3
  100. package/dist/lib/versions.d.ts +10 -2
  101. package/dist/lib/versions.js +227 -35
  102. package/package.json +7 -7
  103. package/npm-shrinkwrap.json +0 -3162
@@ -11,11 +11,13 @@ import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import * as yaml from 'yaml';
13
13
  import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemonPid, readDaemonLog, } from '../lib/daemon.js';
14
+ import { humanizeCron, humanizeNextRun, formatRepoLink } from '../lib/routines-format.js';
14
15
  import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, getLatestRun, getRunDir, getJobPath, parseAtTime, } from '../lib/routines.js';
15
16
  import { getRoutinesDir } from '../lib/state.js';
16
17
  import { safeJoin } from '../lib/paths.js';
17
- import { executeJob } from '../lib/runner.js';
18
+ import { executeJob, executeJobDetached } from '../lib/runner.js';
18
19
  import { JobScheduler } from '../lib/scheduler.js';
20
+ import { detectOverdueJobs } from '../lib/overdue.js';
19
21
  import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
20
22
  import { setHelpSections } from '../lib/help.js';
21
23
  /** Start or reload the background scheduler so newly-added jobs fire on time. */
@@ -60,7 +62,7 @@ async function pickJob(message, filter, alternatives = []) {
60
62
  message,
61
63
  choices: jobs.map((job) => ({
62
64
  value: job.name,
63
- name: `${job.name} ${chalk.gray(`(${job.agent}, ${job.schedule})`)}`,
65
+ name: `${job.name} ${chalk.gray(`(${job.workflow ? `wf:${job.workflow}` : job.agent}, ${job.schedule})`)}`,
64
66
  })),
65
67
  });
66
68
  }
@@ -120,18 +122,57 @@ export function registerRoutinesCommands(program) {
120
122
  }
121
123
  const scheduler = new JobScheduler(async () => { });
122
124
  scheduler.loadAll();
125
+ // Build a quick lookup: which jobs are currently overdue?
126
+ const overdueSet = new Set();
127
+ try {
128
+ for (const j of detectOverdueJobs())
129
+ overdueSet.add(j.name);
130
+ }
131
+ catch {
132
+ // Best-effort indicator; never block the list on detection errors.
133
+ }
123
134
  console.log(chalk.bold('Scheduled Jobs\n'));
124
- const header = ` ${'Name'.padEnd(24)} ${'Agent'.padEnd(10)} ${'Schedule'.padEnd(20)} ${'Enabled'.padEnd(10)} ${'Next Run'.padEnd(24)} ${'Last Status'}`;
135
+ // OSC 8 hyperlink helper renders as a clickable link in supporting terminals.
136
+ // In terminals that do not support OSC 8 the escape sequences are ignored and
137
+ // the label is displayed as plain text.
138
+ const link = (label, url) => url ? `\x1b]8;;${url}\x07${label}\x1b]8;;\x07` : label;
139
+ const now = new Date();
140
+ const NAME_W = 24;
141
+ const AGENT_W = 10;
142
+ const REPO_W = 24;
143
+ const SCHED_W = 22;
144
+ const ENABLED_W = 10;
145
+ const NEXT_W = 22;
146
+ const header = ` ${'Name'.padEnd(NAME_W)} ${'Agent'.padEnd(AGENT_W)} ${'Repo'.padEnd(REPO_W)} ${'Schedule'.padEnd(SCHED_W)} ${'Enabled'.padEnd(ENABLED_W)} ${'Next Run'.padEnd(NEXT_W)} Last Status`;
125
147
  console.log(chalk.gray(header));
126
- console.log(chalk.gray(' ' + '-'.repeat(110)));
148
+ console.log(chalk.gray(' ' + '-'.repeat(NAME_W + AGENT_W + REPO_W + SCHED_W + ENABLED_W + NEXT_W + 20)));
127
149
  for (const job of jobs) {
128
150
  const nextRun = scheduler.getNextRun(job.name);
129
- const nextStr = nextRun ? nextRun.toLocaleString() : '-';
151
+ const nextStr = humanizeNextRun(nextRun ?? null, now, job.timezone);
152
+ const schedStr = humanizeCron(job.schedule, job.timezone);
130
153
  const latestRun = getLatestRun(job.name);
131
154
  const lastStatus = latestRun?.status || '-';
155
+ const repoInfo = formatRepoLink(job.repo);
156
+ const repoCell = link(repoInfo.display, repoInfo.href);
157
+ // Pad based on the display string, not the raw cell (which may include escape codes).
158
+ const repoPadding = Math.max(0, REPO_W - repoInfo.display.length);
132
159
  const enabledStr = job.enabled ? chalk.green('yes') : chalk.gray('no');
133
- const statusColor = lastStatus === 'completed' ? chalk.green : lastStatus === 'failed' ? chalk.red : lastStatus === 'timeout' ? chalk.yellow : chalk.gray;
134
- console.log(` ${chalk.cyan(job.name.padEnd(24))} ${job.agent.padEnd(10)} ${job.schedule.padEnd(20)} ${enabledStr.padEnd(10 + 10)} ${chalk.gray(nextStr.padEnd(24))} ${statusColor(lastStatus)}`);
160
+ // chalk adds escape codes; pad the raw word and let chalk wrap it.
161
+ const enabledWord = job.enabled ? 'yes' : 'no';
162
+ const enabledPad = Math.max(0, ENABLED_W - enabledWord.length);
163
+ const statusColor = lastStatus === 'completed' ? chalk.green
164
+ : lastStatus === 'failed' ? chalk.red
165
+ : lastStatus === 'timeout' ? chalk.yellow
166
+ : chalk.gray;
167
+ const overdueTag = overdueSet.has(job.name) ? chalk.yellow(' (overdue)') : '';
168
+ const agentLabelPadded = job.workflow
169
+ ? chalk.magenta(`wf:${job.workflow}`.padEnd(10))
170
+ : (job.agent || '').padEnd(10);
171
+ console.log(` ${chalk.cyan(job.name.padEnd(NAME_W))} ${agentLabelPadded} ${repoCell}${' '.repeat(repoPadding)} ${schedStr.padEnd(SCHED_W)} ${enabledStr}${' '.repeat(enabledPad)} ${chalk.gray(nextStr.padEnd(NEXT_W))} ${statusColor(lastStatus)}${overdueTag}`);
172
+ }
173
+ if (overdueSet.size > 0) {
174
+ console.log();
175
+ console.log(chalk.yellow(` ${overdueSet.size} routine(s) overdue — catch up with: agents routines catchup`));
135
176
  }
136
177
  scheduler.stopAll();
137
178
  console.log();
@@ -141,16 +182,17 @@ export function registerRoutinesCommands(program) {
141
182
  .description('Create a new routine from a YAML file or inline flags. Starts the scheduler automatically if it is not already running.')
142
183
  .option('-s, --schedule <cron>', 'Cron schedule in standard format (5 fields: minute hour day month weekday)')
143
184
  .option('-a, --agent <agent>', 'Which agent runs this routine: claude, codex, gemini, cursor, or opencode')
185
+ .option('--workflow <name>', 'Run an installed workflow (~/.agents/workflows/<name>) via `agents run`. Mutually exclusive with --agent.')
144
186
  .option('-p, --prompt <prompt>', 'Task instruction for the agent')
145
187
  .option('-m, --mode <mode>', 'Execution mode: plan (read-only) or edit (can write files)', 'plan')
146
188
  .option('-e, --effort <effort>', 'Reasoning effort: low | medium | high | xhigh | max | auto', 'auto')
147
- .option('-t, --timeout <timeout>', 'Kill the agent if it runs longer than this (e.g., 30m, 2h)', '30m')
189
+ .option('-t, --timeout <timeout>', 'Kill the agent if it runs longer than this (e.g., 10m, 2h, 3d, 1w; max 1w)', '10m')
148
190
  .option('--timezone <tz>', 'Interpret schedule in this timezone (e.g., America/Los_Angeles)')
149
191
  .option('--at <time>', 'One-shot mode: run once at this time (e.g., "14:30" or "2026-02-24 09:00"), then disable')
150
192
  .option('--disabled', 'Create the routine but keep it paused (enable later with resume)')
151
193
  .action(async (nameOrPath, options) => {
152
194
  // Check if inline mode (has flags) or file mode
153
- const hasInlineFlags = options.schedule || options.agent || options.prompt || options.at;
195
+ const hasInlineFlags = options.schedule || options.agent || options.workflow || options.prompt || options.at;
154
196
  if (hasInlineFlags) {
155
197
  // Inline mode: create job from flags
156
198
  if (!nameOrPath) {
@@ -158,6 +200,11 @@ export function registerRoutinesCommands(program) {
158
200
  console.log(chalk.gray('Usage: agents routines add <name> --schedule "..." --agent <agent> --prompt "..."'));
159
201
  process.exit(1);
160
202
  }
203
+ // Validate mutually exclusive --agent / --workflow
204
+ if (options.agent && options.workflow) {
205
+ console.log(chalk.red('--agent and --workflow are mutually exclusive; specify exactly one'));
206
+ process.exit(1);
207
+ }
161
208
  let schedule = options.schedule;
162
209
  let runOnce = false;
163
210
  // Handle --at for one-shot jobs
@@ -175,8 +222,8 @@ export function registerRoutinesCommands(program) {
175
222
  console.log(chalk.red('Schedule is required (use --schedule or --at)'));
176
223
  process.exit(1);
177
224
  }
178
- if (!options.agent) {
179
- console.log(chalk.red('Agent is required (use --agent)'));
225
+ if (!options.agent && !options.workflow) {
226
+ console.log(chalk.red('An agent or workflow is required (use --agent or --workflow)'));
180
227
  process.exit(1);
181
228
  }
182
229
  if (!options.prompt) {
@@ -187,6 +234,7 @@ export function registerRoutinesCommands(program) {
187
234
  name: nameOrPath,
188
235
  schedule,
189
236
  agent: options.agent,
237
+ ...(options.workflow ? { workflow: options.workflow } : {}),
190
238
  mode: options.mode,
191
239
  effort: options.effort,
192
240
  timeout: options.timeout,
@@ -245,7 +293,7 @@ export function registerRoutinesCommands(program) {
245
293
  const config = {
246
294
  mode: 'plan',
247
295
  effort: 'auto',
248
- timeout: '30m',
296
+ timeout: '10m',
249
297
  enabled: true,
250
298
  ...parsed,
251
299
  };
@@ -387,7 +435,8 @@ export function registerRoutinesCommands(program) {
387
435
  console.log(chalk.red(`Job '${name}' not found`));
388
436
  process.exit(1);
389
437
  }
390
- console.log(chalk.bold(`Running job '${name}' (agent: ${job.agent}, mode: ${job.mode})\n`));
438
+ const runLabel = job.workflow ? `workflow: ${job.workflow}` : `agent: ${job.agent}`;
439
+ console.log(chalk.bold(`Running job '${name}' (${runLabel}, mode: ${job.mode})\n`));
391
440
  const spinner = ora('Executing...').start();
392
441
  try {
393
442
  const result = await executeJob(job);
@@ -413,6 +462,50 @@ export function registerRoutinesCommands(program) {
413
462
  process.exit(1);
414
463
  }
415
464
  });
465
+ routinesCmd
466
+ .command('catchup')
467
+ .description('Run any routines that missed their last scheduled fire (e.g. because your laptop was off). Detached — runs in the background under the scheduler.')
468
+ .option('--dry-run', 'List overdue routines without running them')
469
+ .action(async (options) => {
470
+ const overdue = detectOverdueJobs();
471
+ if (overdue.length === 0) {
472
+ console.log(chalk.gray('No overdue routines.'));
473
+ return;
474
+ }
475
+ console.log(chalk.bold(`${overdue.length} overdue routine(s):\n`));
476
+ for (const job of overdue) {
477
+ const last = job.lastRanAt ? job.lastRanAt.toLocaleString() : 'never';
478
+ console.log(` ${chalk.cyan(job.name)} — missed ${chalk.gray(job.expectedAt.toLocaleString())}, last ran ${chalk.gray(last)}`);
479
+ }
480
+ if (options.dryRun) {
481
+ console.log(chalk.gray('\n(dry run — no jobs triggered)'));
482
+ return;
483
+ }
484
+ // Need the daemon alive so spawned jobs are monitored and meta.json is
485
+ // finalized. Start it if it isn't already running.
486
+ if (!isDaemonRunning()) {
487
+ const started = startDaemon();
488
+ if (started.pid) {
489
+ console.log(chalk.gray(`\nStarted scheduler (PID: ${started.pid}) so catchup runs are monitored.`));
490
+ }
491
+ }
492
+ console.log(chalk.bold('\nTriggering catchup runs...'));
493
+ for (const job of overdue) {
494
+ const config = readJob(job.name);
495
+ if (!config) {
496
+ console.log(` ${job.name} → ${chalk.red('config not found')}`);
497
+ continue;
498
+ }
499
+ try {
500
+ const meta = await executeJobDetached(config);
501
+ console.log(` ${job.name} → ${chalk.green('started')} (run: ${meta.runId}, PID: ${meta.pid ?? 'n/a'})`);
502
+ }
503
+ catch (err) {
504
+ console.log(` ${job.name} → ${chalk.red('failed to start')}: ${err.message}`);
505
+ }
506
+ }
507
+ console.log(chalk.gray('\nTrack progress with: agents routines runs <name>'));
508
+ });
416
509
  routinesCmd
417
510
  .command('logs [name]')
418
511
  .description('Read stdout from the most recent execution. Use --run to see a specific past run.')
@@ -873,13 +873,12 @@ Examples:
873
873
  .option('--force', 'Overwrite existing 1Password items (used with --to-1password)')
874
874
  .action(async (bundleName, opts) => {
875
875
  try {
876
- const { resolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
876
+ const { readAndResolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
877
877
  const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
878
- const bundle = readBundle(resolvedBundleName);
879
878
  if (opts.to1password) {
880
879
  assertOpAvailable();
881
880
  const vault = await resolveVault(opts.vault);
882
- const env = resolveBundleEnv(bundle, { caller: `1Password vault ${vault}` });
881
+ const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `1Password vault ${vault}` });
883
882
  let created = 0;
884
883
  let overwritten = 0;
885
884
  let skipped = 0;
@@ -913,7 +912,7 @@ Examples:
913
912
  console.error(chalk.red('export to a TTY requires --plaintext (prevents shoulder-surfing).'));
914
913
  process.exit(1);
915
914
  }
916
- const env = resolveBundleEnv(bundle, { caller: `export to shell` });
915
+ const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `export to shell` });
917
916
  const prefix = bundleToEnvPrefix(resolvedBundleName);
918
917
  for (const [k, v] of Object.entries(env)) {
919
918
  const exportKey = isReservedEnvName(k) ? `${prefix}_${k}` : k;
@@ -939,10 +938,9 @@ Examples:
939
938
  console.error(chalk.red('Usage: agents secrets exec <bundle> -- <command...>'));
940
939
  process.exit(1);
941
940
  }
942
- const { resolveBundleEnv } = await import('../lib/secrets/bundles.js');
943
- const bundle = readBundle(bundleName);
941
+ const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
944
942
  const [cmd, ...args] = commandParts;
945
- const secretEnv = resolveBundleEnv(bundle, { caller: `command ${cmd}` });
943
+ const { env: secretEnv } = readAndResolveBundleEnv(bundleName, { caller: `command ${cmd}` });
946
944
  const { spawn } = await import('child_process');
947
945
  const proc = spawn(cmd, args, {
948
946
  stdio: 'inherit',
@@ -1,5 +1,33 @@
1
1
  import type { Command } from 'commander';
2
2
  import type { SessionMeta } from '../lib/session/types.js';
3
+ import { type ActiveSession } from '../lib/session/active.js';
4
+ /** Grouped + sorted view of active sessions for the --active renderer. */
5
+ export interface ActiveSessionsLayout {
6
+ workspaces: Array<{
7
+ /** Internal grouping key — `__cloud__`, `__unknown__`, or the cwd. */
8
+ key: string;
9
+ /** Sessions in this workspace, both windowed and flat (preserves total count). */
10
+ total: number;
11
+ /** Terminals grouped by IDE window (sorted by oldest startedAtMs). */
12
+ windows: Array<{
13
+ windowId: string;
14
+ sessions: ActiveSession[];
15
+ }>;
16
+ /** Everything else in this workspace: cloud, teams, headless, terminals without a windowId. */
17
+ flat: ActiveSession[];
18
+ }>;
19
+ }
20
+ /**
21
+ * Group sessions by workspace, then split each workspace into IDE-window
22
+ * sub-groups + a flat bucket. Pure function — no I/O — so the renderer's
23
+ * grouping rules can be tested without mocking the session scanner.
24
+ *
25
+ * Sort order:
26
+ * - workspaces: by session count descending, then key ascending
27
+ * - windows within a workspace: by oldest startedAtMs ascending
28
+ * - sessions within a window/flat bucket: input order preserved
29
+ */
30
+ export declare function groupActiveSessions(sessions: ActiveSession[]): ActiveSessionsLayout;
3
31
  /**
4
32
  * Build the shell command that resumes a picked session.
5
33
  *
@@ -181,18 +181,45 @@ function buildSessionDescription(s) {
181
181
  return s.topic;
182
182
  return '';
183
183
  }
184
- /** Render the unified active-session view. */
185
- async function renderActiveSessions(asJson) {
186
- const sessions = await getActiveSessions();
187
- if (asJson) {
188
- process.stdout.write(JSON.stringify(sessions, null, 2) + '\n');
189
- return;
190
- }
191
- if (sessions.length === 0) {
192
- console.log(chalk.gray('No active agent sessions.'));
193
- return;
194
- }
195
- // Group sessions by workspace (cwd), with cloud/undefined grouped separately
184
+ /**
185
+ * Render a single agent-session row inside an already-printed group header.
186
+ * Indent is the leading whitespace (2 spaces for flat groups, 4 inside a
187
+ * window sub-group).
188
+ */
189
+ function printActiveRow(s, indent) {
190
+ const kindCol = colorAgent(s.kind)(padRight(truncate(s.kind, 8), 9));
191
+ const hostCol = chalk.gray(padRight(truncate(s.host ?? '-', 8), 9));
192
+ const statusCol = statusColor(s.status)(padRight(truncate(s.status, 7), 8));
193
+ const pidCol = chalk.yellow(padRight(s.pid ? String(s.pid) : '-', 7));
194
+ const desc = buildSessionDescription(s);
195
+ console.log(indent +
196
+ pidCol +
197
+ kindCol +
198
+ hostCol +
199
+ statusCol +
200
+ chalk.white(truncate(desc || '-', 50)));
201
+ }
202
+ /**
203
+ * Short label for an IDE window. The slice key in live-terminals.json is
204
+ * `${vscode.env.sessionId}-${ext-host pid}`; the trailing pid is the cheap
205
+ * stable disambiguator. We surface it as `ext-pid` so two windows on the
206
+ * same repo are visibly different.
207
+ */
208
+ function shortWindowLabel(windowId) {
209
+ const m = windowId.match(/-(\d+)$/);
210
+ return m ? `ext-pid ${m[1]}` : `win ${windowId.slice(0, 8)}`;
211
+ }
212
+ /**
213
+ * Group sessions by workspace, then split each workspace into IDE-window
214
+ * sub-groups + a flat bucket. Pure function — no I/O — so the renderer's
215
+ * grouping rules can be tested without mocking the session scanner.
216
+ *
217
+ * Sort order:
218
+ * - workspaces: by session count descending, then key ascending
219
+ * - windows within a workspace: by oldest startedAtMs ascending
220
+ * - sessions within a window/flat bucket: input order preserved
221
+ */
222
+ export function groupActiveSessions(sessions) {
196
223
  const byWorkspace = new Map();
197
224
  for (const s of sessions) {
198
225
  const key = s.cwd ?? (s.context === 'cloud' ? '__cloud__' : '__unknown__');
@@ -200,7 +227,6 @@ async function renderActiveSessions(asJson) {
200
227
  list.push(s);
201
228
  byWorkspace.set(key, list);
202
229
  }
203
- // Sort workspaces: most sessions first, then alphabetically
204
230
  const sortedKeys = Array.from(byWorkspace.keys()).sort((a, b) => {
205
231
  const aCount = byWorkspace.get(a).length;
206
232
  const bCount = byWorkspace.get(b).length;
@@ -208,33 +234,71 @@ async function renderActiveSessions(asJson) {
208
234
  return bCount - aCount;
209
235
  return a.localeCompare(b);
210
236
  });
211
- let first = true;
212
- for (const key of sortedKeys) {
237
+ const workspaces = sortedKeys.map((key) => {
213
238
  const group = byWorkspace.get(key);
239
+ const windowedSessions = [];
240
+ const flat = [];
241
+ for (const s of group) {
242
+ if (s.context === 'terminal' && s.windowId)
243
+ windowedSessions.push(s);
244
+ else
245
+ flat.push(s);
246
+ }
247
+ const byWindow = new Map();
248
+ for (const s of windowedSessions) {
249
+ const list = byWindow.get(s.windowId) || [];
250
+ list.push(s);
251
+ byWindow.set(s.windowId, list);
252
+ }
253
+ const windowKeys = Array.from(byWindow.keys()).sort((a, b) => {
254
+ const aStart = Math.min(...byWindow.get(a).map(s => s.startedAtMs ?? Infinity));
255
+ const bStart = Math.min(...byWindow.get(b).map(s => s.startedAtMs ?? Infinity));
256
+ return aStart - bStart;
257
+ });
258
+ return {
259
+ key,
260
+ total: group.length,
261
+ windows: windowKeys.map((wid) => ({ windowId: wid, sessions: byWindow.get(wid) })),
262
+ flat,
263
+ };
264
+ });
265
+ return { workspaces };
266
+ }
267
+ /** Render the unified active-session view. */
268
+ async function renderActiveSessions(asJson) {
269
+ const sessions = await getActiveSessions();
270
+ if (asJson) {
271
+ process.stdout.write(JSON.stringify(sessions, null, 2) + '\n');
272
+ return;
273
+ }
274
+ if (sessions.length === 0) {
275
+ console.log(chalk.gray('No active agent sessions.'));
276
+ return;
277
+ }
278
+ const layout = groupActiveSessions(sessions);
279
+ let first = true;
280
+ for (const ws of layout.workspaces) {
214
281
  if (!first)
215
282
  console.log();
216
283
  first = false;
217
284
  // Print workspace header
218
- const header = key === '__cloud__'
285
+ const header = ws.key === '__cloud__'
219
286
  ? chalk.magenta.bold('cloud')
220
- : key === '__unknown__'
287
+ : ws.key === '__unknown__'
221
288
  ? chalk.gray.bold('unknown')
222
- : chalk.cyan.bold(shortCwd(key));
223
- console.log(`${header} ${chalk.gray(`(${group.length})`)}`);
224
- // Print each session in this workspace
225
- for (const s of group) {
226
- const kindCol = colorAgent(s.kind)(padRight(truncate(s.kind, 8), 9));
227
- const hostCol = chalk.gray(padRight(truncate(s.host ?? '-', 8), 9));
228
- const statusCol = statusColor(s.status)(padRight(truncate(s.status, 7), 8));
229
- const pidCol = chalk.yellow(padRight(s.pid ? String(s.pid) : '-', 7));
230
- const desc = buildSessionDescription(s);
231
- console.log(' ' +
232
- pidCol +
233
- kindCol +
234
- hostCol +
235
- statusCol +
236
- chalk.white(truncate(desc || '-', 50)));
289
+ : chalk.cyan.bold(shortCwd(ws.key));
290
+ console.log(`${header} ${chalk.gray(`(${ws.total})`)}`);
291
+ for (const win of ws.windows) {
292
+ // Host is per-process, but every terminal in the same IDE window shares
293
+ // an ancestor — take the first non-empty host as the window's label.
294
+ const host = win.sessions.find((s) => s.host)?.host ?? 'terminal';
295
+ const winHeader = `${chalk.gray(host)} ${chalk.gray('·')} ${chalk.gray(shortWindowLabel(win.windowId))} ${chalk.gray(`(${win.sessions.length})`)}`;
296
+ console.log(' ' + winHeader);
297
+ for (const s of win.sessions)
298
+ printActiveRow(s, ' ');
237
299
  }
300
+ for (const s of ws.flat)
301
+ printActiveRow(s, ' ');
238
302
  }
239
303
  const runningCount = sessions.filter(s => s.status === 'running').length;
240
304
  const idleCount = sessions.filter(s => s.status === 'idle').length;
@@ -673,7 +737,8 @@ export function buildResumeCommand(session) {
673
737
  case 'openclaw':
674
738
  case 'rush':
675
739
  case 'hermes':
676
- // Rush and Hermes sessions are captured artifacts, not resumable locally.
740
+ case 'grok':
741
+ // Grok (and some others) sessions are captured artifacts, not resumable the same way.
677
742
  return null;
678
743
  }
679
744
  }
@@ -9,6 +9,7 @@ import type { Command } from 'commander';
9
9
  export declare function runSetup(program: Command, options?: {
10
10
  force?: boolean;
11
11
  suppressFooter?: boolean;
12
+ systemRepo?: boolean;
12
13
  }): Promise<void>;
13
14
  /**
14
15
  * Ensure the system repo exists before running a command that needs it.
@@ -68,38 +68,46 @@ export async function runSetup(program, options = {}) {
68
68
  for (const install of unmanaged) {
69
69
  sessionCounts[install.agentId] = countSessionFiles(install.agentId);
70
70
  }
71
+ const systemRepo = process.env.AGENTS_SYSTEM_REPO || DEFAULT_SYSTEM_REPO;
71
72
  console.log(chalk.bold('\nWelcome to agents-cli.'));
72
- console.log(chalk.gray(`Cloning the system repo from ${systemRepoSlug(DEFAULT_SYSTEM_REPO)} into ~/.agents-system/.\n`));
73
- ensureAgentsDir();
74
- const spinner = ora('Cloning system repo...').start();
75
- if (isGitRepo(agentsDir)) {
76
- // --force on an existing repo: pull instead of re-clone
77
- const result = await pullRepo(agentsDir);
78
- if (!result.success) {
79
- spinner.fail(`Pull failed: ${result.error}`);
80
- console.log(chalk.gray('Fix the issue and re-run: agents setup --force'));
81
- process.exit(1);
82
- }
83
- spinner.succeed(`Updated to ${result.commit}`);
73
+ if (options.systemRepo === false) {
74
+ ensureAgentsDir();
75
+ console.log(chalk.gray('Skipping system repo clone (--no-system-repo).'));
76
+ console.log(chalk.gray(`Populate ~/.agents-system/ yourself before running other commands that depend on it.\n`));
84
77
  }
85
78
  else {
86
- // Check git is available
87
- try {
88
- const { execSync } = await import('child_process');
89
- execSync('which git', { stdio: 'ignore' });
90
- }
91
- catch {
92
- spinner.fail('git is not installed');
93
- console.log(chalk.gray('Install git first: https://git-scm.com/downloads'));
94
- process.exit(1);
79
+ console.log(chalk.gray(`Cloning the system repo from ${systemRepoSlug(systemRepo)} into ~/.agents-system/.\n`));
80
+ ensureAgentsDir();
81
+ const spinner = ora('Cloning system repo...').start();
82
+ if (isGitRepo(agentsDir)) {
83
+ // --force on an existing repo: pull instead of re-clone
84
+ const result = await pullRepo(agentsDir);
85
+ if (!result.success) {
86
+ spinner.fail(`Pull failed: ${result.error}`);
87
+ console.log(chalk.gray('Fix the issue and re-run: agents setup --force'));
88
+ process.exit(1);
89
+ }
90
+ spinner.succeed(`Updated to ${result.commit}`);
95
91
  }
96
- const result = await cloneIntoExisting(DEFAULT_SYSTEM_REPO, agentsDir);
97
- if (!result.success) {
98
- spinner.fail(`Clone failed: ${result.error}`);
99
- console.log(chalk.gray('Fix the issue and re-run: agents setup --force'));
100
- process.exit(1);
92
+ else {
93
+ // Check git is available
94
+ try {
95
+ const { execSync } = await import('child_process');
96
+ execSync('which git', { stdio: 'ignore' });
97
+ }
98
+ catch {
99
+ spinner.fail('git is not installed');
100
+ console.log(chalk.gray('Install git first: https://git-scm.com/downloads'));
101
+ process.exit(1);
102
+ }
103
+ const result = await cloneIntoExisting(systemRepo, agentsDir);
104
+ if (!result.success) {
105
+ spinner.fail(`Clone failed: ${result.error}`);
106
+ console.log(chalk.gray('Fix the issue and re-run: agents setup --force'));
107
+ process.exit(1);
108
+ }
109
+ spinner.succeed(`Cloned ${systemRepoSlug(systemRepo)} (${result.commit})`);
101
110
  }
102
- spinner.succeed(`Cloned ${systemRepoSlug(DEFAULT_SYSTEM_REPO)} (${result.commit})`);
103
111
  }
104
112
  // Offer to import existing unmanaged installations
105
113
  if (unmanaged.length > 0 && isInteractiveTerminal()) {
@@ -195,7 +203,8 @@ export function registerSetupCommand(program) {
195
203
  const setupCmd = program
196
204
  .command('setup')
197
205
  .description('First-time setup. Clones a config repo and installs agent CLIs.')
198
- .option('-f, --force', 'Re-run setup even if ~/.agents-system/ already exists (use with caution)');
206
+ .option('-f, --force', 'Re-run setup even if ~/.agents-system/ already exists (use with caution)')
207
+ .option('--no-system-repo', 'Skip cloning the system repo (you must populate ~/.agents-system/ yourself)');
199
208
  setHelpSections(setupCmd, {
200
209
  examples: `
201
210
  # First-time setup (clones the system repo into ~/.agents-system/)
@@ -394,17 +394,17 @@ Examples:
394
394
  .action(() => {
395
395
  console.error(chalk.red('"agents skills sync" is gone.'));
396
396
  console.error(chalk.gray('Sync runs automatically when you launch the agent.'));
397
- console.error(chalk.gray('To remove orphans, use: agents prune skills'));
397
+ console.error(chalk.gray('To remove orphans, use: agents prune cleanup skills'));
398
398
  process.exit(1);
399
399
  });
400
- // `skills prune` moved to the top-level `agents prune` command.
400
+ // `skills prune` moved to the top-level `agents prune cleanup` command.
401
401
  skillsCmd
402
402
  .command('prune', { hidden: true })
403
403
  .allowUnknownOption()
404
404
  .allowExcessArguments()
405
405
  .action(() => {
406
406
  console.error(chalk.red('"agents skills prune" moved.'));
407
- console.error(chalk.gray('Use: agents prune skills (or `agents prune` for everything)'));
407
+ console.error(chalk.gray('Use: agents prune cleanup skills (or `agents prune cleanup` for everything)'));
408
408
  process.exit(1);
409
409
  });
410
410
  skillsCmd
@@ -21,6 +21,8 @@ const AGENT_NAMES = {
21
21
  gemini: 'Gemini',
22
22
  cursor: 'Cursor',
23
23
  opencode: 'OpenCode',
24
+ grok: 'Grok',
25
+ antigravity: 'Antigravity',
24
26
  };
25
27
  const VALID_AGENTS = Object.keys(AGENT_NAMES);
26
28
  const VALID_MODES = ['plan', 'edit', 'full'];
@@ -756,6 +758,11 @@ export function registerTeamsCommands(program) {
756
758
  die(`Invalid teammate name '${opts.name}'. Use letters, numbers, '-', or '_'.`);
757
759
  }
758
760
  }
761
+ if (opts.worktree !== undefined) {
762
+ if (!opts.worktree || !/^[A-Za-z0-9_-]+$/.test(opts.worktree)) {
763
+ die(`Invalid worktree name '${opts.worktree}'. Use letters, numbers, '-', or '_'.`);
764
+ }
765
+ }
759
766
  const after = opts.after
760
767
  ? opts.after.split(',').map((s) => s.trim()).filter(Boolean)
761
768
  : [];
@@ -899,6 +906,12 @@ export function registerTeamsCommands(program) {
899
906
  console.log();
900
907
  if (staged) {
901
908
  console.log(chalk.gray(`Start the ready teammates: agents teams start ${team}`));
909
+ if (after.length > 0) {
910
+ process.stderr.write(chalk.yellow(`\nWarning: this teammate has --after dependencies and will NEVER start on its own.\n` +
911
+ ` A supervisor watch process is required to launch it when its deps complete.\n` +
912
+ ` Run this in another terminal:\n` +
913
+ ` agents teams start ${team} --watch\n`));
914
+ }
902
915
  }
903
916
  else {
904
917
  console.log(chalk.gray(`Check in later: agents teams status ${team}`));
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Version management commands for installing, switching, and removing agent CLIs.
3
3
  *
4
- * Implements `agents add`, `agents remove`, `agents use`, and the deprecated
5
- * `agents list`. Handles npm-based installation, shim creation, config symlink
4
+ * Implements `agents add`, `agents prune`, `agents remove` (alias),
5
+ * `agents use`, and the deprecated `agents list`. Handles npm-based installation,
6
+ * shim creation, config symlink
6
7
  * switching, resource sync prompts, and project-level version pinning.
7
8
  */
8
9
  import type { Command } from 'commander';
9
- /** Register `agents add`, `agents remove`, `agents use`, and `agents list` (deprecated). */
10
+ /** Register `agents add`, `agents prune`, `agents remove`, `agents use`, and `agents list` (deprecated). */
10
11
  export declare function registerVersionsCommands(program: Command): void;