@phnx-labs/agents-cli 1.19.1 → 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 +70 -10
- 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/mcp.js +29 -0
- 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 +6 -8
- package/dist/commands/sessions.d.ts +36 -7
- package/dist/commands/sessions.js +130 -53
- 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 +147 -124
- package/dist/commands/view.js +12 -12
- package/dist/index.js +34 -6
- package/dist/lib/acp/harnesses.js +8 -0
- package/dist/lib/agents.js +162 -9
- 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 +62 -6
- 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/registry.d.ts +18 -0
- package/dist/lib/registry.js +44 -0
- package/dist/lib/resources/mcp.js +43 -1
- package/dist/lib/resources/types.d.ts +1 -1
- 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/{AgentsKeychain.app → Agents CLI.app}/Contents/CodeResources +0 -0
- package/dist/lib/secrets/{AgentsKeychain.app/Contents/Info.plist → Agents CLI.app/Contents/Info.plist } +4 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.d.ts +33 -2
- package/dist/lib/secrets/bundles.js +249 -26
- package/dist/lib/secrets/index.d.ts +10 -1
- package/dist/lib/secrets/index.js +143 -48
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +3 -2
- package/dist/lib/session/db.d.ts +10 -4
- package/dist/lib/session/db.js +16 -16
- 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 +6 -2
- package/dist/lib/shims.js +88 -10
- 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 +7 -4
- 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 +9 -9
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/npm-shrinkwrap.json +0 -3162
- /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/_CodeSignature/CodeResources +0 -0
- /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/embedded.provisionprofile +0 -0
package/dist/commands/pull.js
CHANGED
|
@@ -14,10 +14,11 @@ import { isGitRepo, pullRepo, isSystemRepoOrigin, } from '../lib/git.js';
|
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import { installVersion, listInstalledVersions, getGlobalDefault, setGlobalDefault, getVersionHomePath, syncResourcesToVersion, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, promptNewResourceSelection, promptResourceSelection, resolveConfiguredAgentTargets, } from '../lib/versions.js';
|
|
17
|
+
import { listCliStatus, installCli, describeMethod, selectInstallMethod, } from '../lib/cli-resources.js';
|
|
17
18
|
import { ensureShimCurrent, isShimsInPath, addShimsToPath, getPathSetupInstructions, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/shims.js';
|
|
18
19
|
import { parseHookManifest, registerHooksToSettings } from '../lib/hooks.js';
|
|
19
20
|
import { setHelpSections } from '../lib/help.js';
|
|
20
|
-
import { select } from '@inquirer/prompts';
|
|
21
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
21
22
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
22
23
|
/**
|
|
23
24
|
* Old repo layout stored promptcuts under claude/promptcuts.yaml (agent-scoped).
|
|
@@ -244,10 +245,10 @@ export function registerPullCommand(program) {
|
|
|
244
245
|
if (userSelection)
|
|
245
246
|
selection = userSelection;
|
|
246
247
|
}
|
|
247
|
-
else if (hasNewResources(newResources, agentId)) {
|
|
248
|
+
else if (hasNewResources(newResources, agentId, defaultVer)) {
|
|
248
249
|
// Has synced before, but NEW items available
|
|
249
250
|
console.log(chalk.cyan(`\n${agentLabel(agentId)}@${defaultVer}:`));
|
|
250
|
-
const userSelection = await promptNewResourceSelection(agentId, newResources);
|
|
251
|
+
const userSelection = await promptNewResourceSelection(agentId, newResources, defaultVer);
|
|
251
252
|
if (userSelection)
|
|
252
253
|
selection = userSelection;
|
|
253
254
|
}
|
|
@@ -366,6 +367,58 @@ export function registerPullCommand(program) {
|
|
|
366
367
|
console.log(chalk.green(`Set ${agentLabel(agent.id)}@${version} as default`));
|
|
367
368
|
}
|
|
368
369
|
}
|
|
370
|
+
// Report (and optionally install) any declared CLIs that are missing
|
|
371
|
+
// from the host. Skipped under -y so non-interactive pulls don't trigger
|
|
372
|
+
// package-manager prompts.
|
|
373
|
+
try {
|
|
374
|
+
const { statuses, errors } = listCliStatus(process.cwd());
|
|
375
|
+
for (const err of errors) {
|
|
376
|
+
console.log(chalk.yellow(` CLI manifest parse error: ${err.file}: ${err.reason}`));
|
|
377
|
+
}
|
|
378
|
+
const missing = statuses.filter((s) => !s.installed);
|
|
379
|
+
if (missing.length > 0) {
|
|
380
|
+
console.log(chalk.bold('\nDeclared CLIs missing from this host:'));
|
|
381
|
+
for (const s of missing) {
|
|
382
|
+
const method = selectInstallMethod(s.manifest);
|
|
383
|
+
const action = method ? describeMethod(method) : chalk.red('no compatible install method');
|
|
384
|
+
console.log(` ${chalk.cyan(s.manifest.name.padEnd(20))} ${chalk.gray(action)}`);
|
|
385
|
+
}
|
|
386
|
+
console.log('');
|
|
387
|
+
if (!skipPrompts) {
|
|
388
|
+
const proceed = await confirm({ message: `Install ${missing.length} missing CLI(s) now?`, default: true });
|
|
389
|
+
if (proceed) {
|
|
390
|
+
for (const s of missing) {
|
|
391
|
+
console.log(chalk.bold(`\n→ ${s.manifest.name}`));
|
|
392
|
+
const result = installCli(s.manifest);
|
|
393
|
+
if (result.error) {
|
|
394
|
+
console.log(chalk.red(` ${result.error}`));
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (result.installed) {
|
|
398
|
+
console.log(chalk.green(` installed`));
|
|
399
|
+
if (s.manifest.postInstall) {
|
|
400
|
+
console.log(chalk.gray(s.manifest.postInstall.trim().split('\n').map((l) => ' ' + l).join('\n')));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
console.log(chalk.yellow(` install ran but \`${s.manifest.check}\` still fails`));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
console.log(chalk.gray(`Skipped. Run 'agents cli install' later.`));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
console.log(chalk.gray(`Run 'agents cli install' to install them.`));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
if (!isPromptCancelled(err)) {
|
|
419
|
+
console.log(chalk.yellow(`CLI install skipped: ${err.message}`));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
369
422
|
console.log(chalk.green('\nPull complete'));
|
|
370
423
|
}
|
|
371
424
|
catch (err) {
|
|
@@ -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,11 +938,10 @@ 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);
|
|
944
|
-
const secretEnv = resolveBundleEnv(bundle);
|
|
945
|
-
const { spawn } = await import('child_process');
|
|
941
|
+
const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
|
|
946
942
|
const [cmd, ...args] = commandParts;
|
|
943
|
+
const { env: secretEnv } = readAndResolveBundleEnv(bundleName, { caller: `command ${cmd}` });
|
|
944
|
+
const { spawn } = await import('child_process');
|
|
947
945
|
const proc = spawn(cmd, args, {
|
|
948
946
|
stdio: 'inherit',
|
|
949
947
|
env: { ...process.env, ...secretEnv },
|
|
@@ -1,16 +1,45 @@
|
|
|
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
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
34
|
+
* When the session's originating version is known, uses the version-pinned
|
|
35
|
+
* binary (e.g. `claude@2.1.138`) so the resume always runs in the same
|
|
36
|
+
* isolated HOME where the JSONL was written — regardless of which version is
|
|
37
|
+
* currently the default. Falls back to the bare shim when version is unknown.
|
|
38
|
+
*
|
|
39
|
+
* If the versioned binary is missing (version was removed), the ENOENT
|
|
40
|
+
* handler in handlePickedSession retries via buildFallbackCommand.
|
|
12
41
|
*/
|
|
13
|
-
export declare function buildResumeCommand(session: SessionMeta
|
|
42
|
+
export declare function buildResumeCommand(session: SessionMeta): string[] | null;
|
|
14
43
|
/** Filter and rank sessions by a multi-term search query across metadata and content. */
|
|
15
44
|
export declare function filterSessionsByQuery(sessions: SessionMeta[], query: string | undefined): SessionMeta[];
|
|
16
45
|
/** Register the `agents sessions` command with all its options and help text. */
|
|
@@ -22,7 +22,7 @@ import { parseSession } from '../lib/session/parse.js';
|
|
|
22
22
|
import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList } from '../lib/session/render.js';
|
|
23
23
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
24
24
|
import { colorAgent, resolveAgentName } from '../lib/agents.js';
|
|
25
|
-
import {
|
|
25
|
+
import { resolveVersionAliasLoose } from '../lib/versions.js';
|
|
26
26
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
27
27
|
import { sessionPicker } from './sessions-picker.js';
|
|
28
28
|
import { setHelpSections } from '../lib/help.js';
|
|
@@ -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;
|
|
@@ -613,18 +677,12 @@ async function handlePickedSession(picked) {
|
|
|
613
677
|
const cwd = picked.session.cwd && fs.existsSync(picked.session.cwd)
|
|
614
678
|
? picked.session.cwd
|
|
615
679
|
: process.cwd();
|
|
616
|
-
const
|
|
617
|
-
const resume = buildResumeCommand(picked.session, activeVersion);
|
|
680
|
+
const resume = buildResumeCommand(picked.session);
|
|
618
681
|
if (!resume) {
|
|
619
682
|
console.log(chalk.yellow(`Resume is not supported for ${picked.session.agent} sessions yet. Showing summary instead.`));
|
|
620
683
|
await renderSession(picked.session, 'summary', {});
|
|
621
684
|
return;
|
|
622
685
|
}
|
|
623
|
-
if (picked.session.version && activeVersion && picked.session.version !== activeVersion) {
|
|
624
|
-
console.log(chalk.gray(`Cross-version handoff: session is ${picked.session.agent} ${picked.session.version}, ` +
|
|
625
|
-
`default is ${activeVersion}. Starting fresh and passing /continue so the new agent ` +
|
|
626
|
-
`reads the prior transcript via 'agents sessions'.`));
|
|
627
|
-
}
|
|
628
686
|
console.log(chalk.gray(`Resuming: ${resume.join(' ')} (cwd: ${cwd})`));
|
|
629
687
|
await new Promise((resolve) => {
|
|
630
688
|
const child = spawn(resume[0], resume.slice(1), {
|
|
@@ -633,6 +691,16 @@ async function handlePickedSession(picked) {
|
|
|
633
691
|
shell: false,
|
|
634
692
|
});
|
|
635
693
|
child.on('error', (err) => {
|
|
694
|
+
if (err.code === 'ENOENT' && picked.session.version) {
|
|
695
|
+
const fallback = buildFallbackCommand(picked.session);
|
|
696
|
+
if (fallback) {
|
|
697
|
+
console.log(chalk.gray(`Version ${picked.session.version} is not installed. Falling back to current version via /continue...`));
|
|
698
|
+
const fb = spawn(fallback[0], fallback.slice(1), { cwd, stdio: 'inherit', shell: false });
|
|
699
|
+
fb.on('error', (e) => { console.error(chalk.red(`Failed: ${e.message}`)); resolve(); });
|
|
700
|
+
fb.on('close', () => resolve());
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
636
704
|
console.error(chalk.red(`Failed to launch ${resume[0]}: ${err.message}`));
|
|
637
705
|
if (err.code === 'ENOENT') {
|
|
638
706
|
console.error(chalk.gray(`Make sure '${resume[0]}' is on your PATH.`));
|
|
@@ -645,23 +713,23 @@ async function handlePickedSession(picked) {
|
|
|
645
713
|
/**
|
|
646
714
|
* Build the shell command that resumes a picked session.
|
|
647
715
|
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
716
|
+
* When the session's originating version is known, uses the version-pinned
|
|
717
|
+
* binary (e.g. `claude@2.1.138`) so the resume always runs in the same
|
|
718
|
+
* isolated HOME where the JSONL was written — regardless of which version is
|
|
719
|
+
* currently the default. Falls back to the bare shim when version is unknown.
|
|
720
|
+
*
|
|
721
|
+
* If the versioned binary is missing (version was removed), the ENOENT
|
|
722
|
+
* handler in handlePickedSession retries via buildFallbackCommand.
|
|
654
723
|
*/
|
|
655
|
-
export function buildResumeCommand(session
|
|
656
|
-
const versionMismatch = !!(session.version && activeVersion && session.version !== activeVersion);
|
|
724
|
+
export function buildResumeCommand(session) {
|
|
657
725
|
switch (session.agent) {
|
|
658
726
|
case 'claude':
|
|
659
|
-
if (
|
|
660
|
-
return [
|
|
727
|
+
if (session.version)
|
|
728
|
+
return [`claude@${session.version}`, '--resume', session.id];
|
|
661
729
|
return ['claude', '--resume', session.id];
|
|
662
730
|
case 'codex':
|
|
663
|
-
if (
|
|
664
|
-
return [
|
|
731
|
+
if (session.version)
|
|
732
|
+
return [`codex@${session.version}`, 'resume', session.id];
|
|
665
733
|
return ['codex', 'resume', session.id];
|
|
666
734
|
case 'opencode':
|
|
667
735
|
return ['opencode', '--session', session.id];
|
|
@@ -669,10 +737,19 @@ export function buildResumeCommand(session, activeVersion) {
|
|
|
669
737
|
case 'openclaw':
|
|
670
738
|
case 'rush':
|
|
671
739
|
case 'hermes':
|
|
672
|
-
|
|
740
|
+
case 'grok':
|
|
741
|
+
// Grok (and some others) sessions are captured artifacts, not resumable the same way.
|
|
673
742
|
return null;
|
|
674
743
|
}
|
|
675
744
|
}
|
|
745
|
+
/** Fallback resume command when the versioned binary is unavailable (ENOENT). */
|
|
746
|
+
function buildFallbackCommand(session) {
|
|
747
|
+
switch (session.agent) {
|
|
748
|
+
case 'claude': return ['claude', `/continue ${session.id}`];
|
|
749
|
+
case 'codex': return ['codex', `/continue ${session.id}`];
|
|
750
|
+
default: return null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
676
753
|
// ---------------------------------------------------------------------------
|
|
677
754
|
// Cloud session source (--cloud)
|
|
678
755
|
// ---------------------------------------------------------------------------
|