@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.
- package/CHANGELOG.md +67 -0
- package/README.md +69 -9
- package/dist/browser.js +0 -0
- package/dist/commands/browser.js +88 -16
- package/dist/commands/cli.d.ts +14 -0
- package/dist/commands/cli.js +244 -0
- package/dist/commands/commands.js +3 -3
- package/dist/commands/computer.js +18 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/exec.js +3 -3
- package/dist/commands/factory.d.ts +3 -14
- package/dist/commands/factory.js +3 -3
- package/dist/commands/hooks.js +3 -3
- package/dist/commands/plugins.js +11 -4
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/prune.js +39 -160
- package/dist/commands/pull.js +56 -3
- package/dist/commands/routines.js +106 -13
- package/dist/commands/secrets.js +5 -7
- package/dist/commands/sessions.d.ts +28 -0
- package/dist/commands/sessions.js +98 -33
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +37 -28
- package/dist/commands/skills.js +3 -3
- package/dist/commands/teams.js +13 -0
- package/dist/commands/versions.d.ts +4 -3
- package/dist/commands/versions.js +131 -127
- package/dist/commands/view.js +12 -12
- package/dist/computer.js +0 -0
- package/dist/index.js +34 -6
- package/dist/lib/acp/harnesses.js +8 -0
- package/dist/lib/agents.js +110 -23
- package/dist/lib/browser/cdp.d.ts +8 -1
- package/dist/lib/browser/cdp.js +40 -3
- package/dist/lib/browser/chrome.d.ts +13 -0
- package/dist/lib/browser/chrome.js +42 -3
- package/dist/lib/browser/domain-skills.d.ts +51 -0
- package/dist/lib/browser/domain-skills.js +157 -0
- package/dist/lib/browser/drivers/local.js +45 -4
- package/dist/lib/browser/drivers/ssh.js +1 -1
- package/dist/lib/browser/ipc.d.ts +8 -1
- package/dist/lib/browser/ipc.js +37 -28
- package/dist/lib/browser/profiles.d.ts +13 -0
- package/dist/lib/browser/profiles.js +41 -1
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +21 -5
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/cli-resources.d.ts +109 -0
- package/dist/lib/cli-resources.js +255 -0
- package/dist/lib/cloud/rush.js +5 -5
- package/dist/lib/command-skills.js +0 -2
- package/dist/lib/computer-rpc.d.ts +3 -0
- package/dist/lib/computer-rpc.js +53 -0
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/exec.d.ts +3 -2
- package/dist/lib/exec.js +44 -9
- package/dist/lib/hooks.js +182 -0
- package/dist/lib/mcp.js +6 -0
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/overdue.d.ts +26 -0
- package/dist/lib/overdue.js +101 -0
- package/dist/lib/permissions.js +5 -1
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/profiles-presets.js +37 -0
- package/dist/lib/resources/mcp.js +37 -0
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/rotate.js +10 -4
- package/dist/lib/routines-format.d.ts +35 -0
- package/dist/lib/routines-format.js +173 -0
- package/dist/lib/routines.d.ts +7 -1
- package/dist/lib/routines.js +32 -12
- package/dist/lib/runner.js +19 -5
- package/dist/lib/scheduler.js +8 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.d.ts +22 -1
- package/dist/lib/secrets/bundles.js +234 -36
- package/dist/lib/secrets/index.d.ts +6 -11
- package/dist/lib/secrets/index.js +107 -87
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +3 -2
- package/dist/lib/session/db.d.ts +0 -4
- package/dist/lib/session/db.js +0 -26
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +44 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +66 -4
- package/dist/lib/state.d.ts +0 -1
- package/dist/lib/state.js +2 -15
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +153 -3
- package/dist/lib/teams/summarizer.js +18 -2
- package/dist/lib/teams/worktree.js +14 -3
- package/dist/lib/types.d.ts +6 -3
- package/dist/lib/types.js +6 -3
- package/dist/lib/versions.d.ts +10 -2
- package/dist/lib/versions.js +227 -35
- package/package.json +7 -7
- 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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
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.,
|
|
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('
|
|
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: '
|
|
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
|
-
|
|
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.')
|
package/dist/commands/secrets.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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 {
|
|
943
|
-
const bundle = readBundle(bundleName);
|
|
941
|
+
const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
|
|
944
942
|
const [cmd, ...args] = commandParts;
|
|
945
|
-
const secretEnv =
|
|
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
|
-
/**
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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(`(${
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -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.
|
package/dist/commands/setup.js
CHANGED
|
@@ -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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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/)
|
package/dist/commands/skills.js
CHANGED
|
@@ -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
|
package/dist/commands/teams.js
CHANGED
|
@@ -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
|
|
5
|
-
* `agents list`. Handles npm-based installation,
|
|
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;
|