@phnx-labs/agents-cli 1.19.2 → 1.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +72 -12
  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/cloud.js +1 -1
  8. package/dist/commands/commands.js +27 -10
  9. package/dist/commands/computer.js +18 -1
  10. package/dist/commands/doctor.d.ts +1 -1
  11. package/dist/commands/doctor.js +2 -2
  12. package/dist/commands/exec.js +38 -18
  13. package/dist/commands/factory.d.ts +3 -14
  14. package/dist/commands/factory.js +3 -3
  15. package/dist/commands/feedback.d.ts +7 -0
  16. package/dist/commands/feedback.js +89 -0
  17. package/dist/commands/helper.d.ts +12 -0
  18. package/dist/commands/helper.js +87 -0
  19. package/dist/commands/hooks.js +89 -10
  20. package/dist/commands/mcp.js +166 -10
  21. package/dist/commands/packages.js +196 -27
  22. package/dist/commands/permissions.js +21 -6
  23. package/dist/commands/plugins.js +11 -4
  24. package/dist/commands/profiles.d.ts +8 -0
  25. package/dist/commands/profiles.js +118 -5
  26. package/dist/commands/prune.js +39 -160
  27. package/dist/commands/pull.js +58 -5
  28. package/dist/commands/routines.js +107 -14
  29. package/dist/commands/rules.js +8 -4
  30. package/dist/commands/secrets-migrate.d.ts +24 -0
  31. package/dist/commands/secrets-migrate.js +198 -0
  32. package/dist/commands/secrets-sync.d.ts +11 -0
  33. package/dist/commands/secrets-sync.js +155 -0
  34. package/dist/commands/secrets.js +79 -46
  35. package/dist/commands/sessions.d.ts +28 -0
  36. package/dist/commands/sessions.js +98 -33
  37. package/dist/commands/setup.d.ts +1 -0
  38. package/dist/commands/setup.js +37 -28
  39. package/dist/commands/skills.js +25 -8
  40. package/dist/commands/subagents.js +69 -49
  41. package/dist/commands/teams.js +61 -10
  42. package/dist/commands/utils.d.ts +33 -0
  43. package/dist/commands/utils.js +139 -0
  44. package/dist/commands/versions.d.ts +4 -3
  45. package/dist/commands/versions.js +134 -130
  46. package/dist/commands/view.d.ts +6 -0
  47. package/dist/commands/view.js +175 -19
  48. package/dist/commands/workflows.js +29 -6
  49. package/dist/computer.js +0 -0
  50. package/dist/index.js +38 -6
  51. package/dist/lib/acp/client.js +6 -1
  52. package/dist/lib/acp/harnesses.js +8 -0
  53. package/dist/lib/agents.d.ts +4 -0
  54. package/dist/lib/agents.js +125 -34
  55. package/dist/lib/auto-pull-worker.js +18 -1
  56. package/dist/lib/browser/cdp.d.ts +8 -1
  57. package/dist/lib/browser/cdp.js +40 -3
  58. package/dist/lib/browser/chrome.d.ts +13 -0
  59. package/dist/lib/browser/chrome.js +46 -3
  60. package/dist/lib/browser/domain-skills.d.ts +51 -0
  61. package/dist/lib/browser/domain-skills.js +157 -0
  62. package/dist/lib/browser/drivers/local.js +45 -4
  63. package/dist/lib/browser/drivers/ssh.js +2 -2
  64. package/dist/lib/browser/ipc.d.ts +8 -1
  65. package/dist/lib/browser/ipc.js +37 -28
  66. package/dist/lib/browser/profiles.d.ts +16 -3
  67. package/dist/lib/browser/profiles.js +44 -4
  68. package/dist/lib/browser/service.d.ts +3 -0
  69. package/dist/lib/browser/service.js +40 -5
  70. package/dist/lib/browser/types.d.ts +11 -4
  71. package/dist/lib/cli-resources.d.ts +137 -0
  72. package/dist/lib/cli-resources.js +477 -0
  73. package/dist/lib/cloud/factory.d.ts +1 -1
  74. package/dist/lib/cloud/factory.js +1 -1
  75. package/dist/lib/cloud/rush.js +5 -5
  76. package/dist/lib/command-skills.js +0 -2
  77. package/dist/lib/computer-rpc.d.ts +3 -0
  78. package/dist/lib/computer-rpc.js +53 -0
  79. package/dist/lib/daemon.js +20 -0
  80. package/dist/lib/events.d.ts +16 -2
  81. package/dist/lib/events.js +33 -2
  82. package/dist/lib/exec.d.ts +42 -13
  83. package/dist/lib/exec.js +127 -33
  84. package/dist/lib/help.js +11 -5
  85. package/dist/lib/hooks/cache.d.ts +38 -0
  86. package/dist/lib/hooks/cache.js +242 -0
  87. package/dist/lib/hooks/profile.d.ts +33 -0
  88. package/dist/lib/hooks/profile.js +129 -0
  89. package/dist/lib/hooks.d.ts +0 -10
  90. package/dist/lib/hooks.js +246 -11
  91. package/dist/lib/mcp.d.ts +15 -0
  92. package/dist/lib/mcp.js +46 -0
  93. package/dist/lib/migrate.js +1 -1
  94. package/dist/lib/overdue.d.ts +26 -0
  95. package/dist/lib/overdue.js +101 -0
  96. package/dist/lib/permissions.d.ts +13 -0
  97. package/dist/lib/permissions.js +55 -1
  98. package/dist/lib/plugin-marketplace.js +1 -1
  99. package/dist/lib/plugins.js +15 -1
  100. package/dist/lib/profiles-presets.d.ts +26 -0
  101. package/dist/lib/profiles-presets.js +216 -0
  102. package/dist/lib/profiles.d.ts +34 -0
  103. package/dist/lib/profiles.js +112 -1
  104. package/dist/lib/resources/mcp.js +37 -0
  105. package/dist/lib/resources.d.ts +1 -1
  106. package/dist/lib/rotate.js +10 -4
  107. package/dist/lib/routines-format.d.ts +47 -0
  108. package/dist/lib/routines-format.js +194 -0
  109. package/dist/lib/routines.d.ts +8 -2
  110. package/dist/lib/routines.js +34 -14
  111. package/dist/lib/runner.js +83 -15
  112. package/dist/lib/scheduler.js +8 -1
  113. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  114. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  115. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  116. package/dist/lib/secrets/bundles.d.ts +34 -17
  117. package/dist/lib/secrets/bundles.js +210 -36
  118. package/dist/lib/secrets/index.d.ts +49 -30
  119. package/dist/lib/secrets/index.js +126 -115
  120. package/dist/lib/secrets/install-helper.d.ts +45 -0
  121. package/dist/lib/secrets/install-helper.js +165 -0
  122. package/dist/lib/secrets/linux.js +4 -4
  123. package/dist/lib/secrets/sync.d.ts +56 -0
  124. package/dist/lib/secrets/sync.js +180 -0
  125. package/dist/lib/session/active.d.ts +8 -0
  126. package/dist/lib/session/active.js +3 -2
  127. package/dist/lib/session/db.d.ts +0 -4
  128. package/dist/lib/session/db.js +0 -26
  129. package/dist/lib/session/parse.d.ts +1 -0
  130. package/dist/lib/session/parse.js +44 -0
  131. package/dist/lib/session/render.js +4 -4
  132. package/dist/lib/session/types.d.ts +2 -2
  133. package/dist/lib/session/types.js +1 -1
  134. package/dist/lib/shims.d.ts +5 -2
  135. package/dist/lib/shims.js +70 -38
  136. package/dist/lib/state.d.ts +14 -2
  137. package/dist/lib/state.js +51 -20
  138. package/dist/lib/teams/agents.d.ts +5 -4
  139. package/dist/lib/teams/agents.js +48 -22
  140. package/dist/lib/teams/api.d.ts +2 -1
  141. package/dist/lib/teams/api.js +4 -3
  142. package/dist/lib/teams/parsers.d.ts +1 -1
  143. package/dist/lib/teams/parsers.js +153 -3
  144. package/dist/lib/teams/summarizer.js +18 -2
  145. package/dist/lib/teams/worktree.js +14 -3
  146. package/dist/lib/types.d.ts +63 -4
  147. package/dist/lib/types.js +8 -3
  148. package/dist/lib/usage.d.ts +27 -2
  149. package/dist/lib/usage.js +100 -17
  150. package/dist/lib/versions.d.ts +45 -3
  151. package/dist/lib/versions.js +455 -60
  152. package/package.json +15 -14
  153. package/scripts/install-helper.js +97 -0
  154. package/scripts/postinstall.js +16 -0
  155. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  156. package/npm-shrinkwrap.json +0 -3162
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Top-level `agents prune` — destructive cleanup across the install.
2
+ * `agents prune cleanup` — destructive cleanup across the install.
3
3
  *
4
4
  * Cleanup targets:
5
5
  * - Resource orphans: command/skill/hook files inside a version home that no
@@ -7,8 +7,9 @@
7
7
  * into the version install).
8
8
  * - Version duplicates: older installed versions of an agent that share an
9
9
  * account with a newer installed version of the same agent.
10
- * - Trash: soft-deleted resources in ~/.agents/.trash/ older than N days.
11
- * - Sessions: session records in sessions.db older than N days.
10
+ * - Trash/session targets are retained as no-op compatibility shims: version
11
+ * homes and session history are durable and must not be hard-deleted by
12
+ * agents-cli.
12
13
  * - Runs: routine execution logs, keeping only the last N per job.
13
14
  *
14
15
  * Sync (additive: copy missing/changed files into version homes) is no longer
@@ -21,7 +22,6 @@
21
22
  * to widen orphan cleanup to every installed version.
22
23
  */
23
24
  import * as fs from 'fs';
24
- import * as path from 'path';
25
25
  import chalk from 'chalk';
26
26
  import { confirm } from '@inquirer/prompts';
27
27
  import { diffVersionCommands, iterCommandsCapableVersions, removeCommandFromVersion, } from '../lib/commands.js';
@@ -34,7 +34,6 @@ import { resolveAgentName, formatAgentError } from '../lib/agents.js';
34
34
  import { pruneDuplicates } from './view.js';
35
35
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
36
36
  import { getTrashDir } from '../lib/state.js';
37
- import { countSessionsOlderThan, deleteSessionsOlderThan } from '../lib/session/db.js';
38
37
  import { previewRunsPrune, pruneRuns, countAllRuns } from '../lib/routines.js';
39
38
  const RESOURCE_TYPES = ['commands', 'skills', 'hooks', 'plugins', 'subagents'];
40
39
  const STATE_TYPES = ['trash', 'sessions', 'runs'];
@@ -125,12 +124,6 @@ function parseTarget(arg) {
125
124
  console.log(chalk.gray(formatAgentError(arg)));
126
125
  process.exit(1);
127
126
  }
128
- function parseDays(value, defaultDays) {
129
- const match = value.match(/^(\d+)d?$/);
130
- if (match)
131
- return parseInt(match[1], 10);
132
- return defaultDays;
133
- }
134
127
  function formatBytes(bytes) {
135
128
  if (bytes < 1024)
136
129
  return `${bytes} B`;
@@ -140,137 +133,25 @@ function formatBytes(bytes) {
140
133
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
141
134
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
142
135
  }
143
- function getDirSize(dirPath) {
144
- if (!fs.existsSync(dirPath))
145
- return 0;
146
- let size = 0;
147
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
148
- for (const entry of entries) {
149
- const fullPath = path.join(dirPath, entry.name);
150
- if (entry.isDirectory()) {
151
- size += getDirSize(fullPath);
152
- }
153
- else {
154
- try {
155
- size += fs.statSync(fullPath).size;
156
- }
157
- catch { /* ignore */ }
158
- }
159
- }
160
- return size;
161
- }
162
136
  async function runTrashPrune(options) {
163
137
  const trashDir = getTrashDir();
164
138
  if (!fs.existsSync(trashDir)) {
165
139
  console.log(chalk.green('Trash is empty.'));
166
140
  return;
167
141
  }
168
- const days = parseDays(options.olderThan || '30d', 30);
169
- const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1000;
170
- const toPrune = [];
171
- function scanDir(dir) {
172
- if (!fs.existsSync(dir))
173
- return;
174
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
175
- const fullPath = path.join(dir, entry.name);
176
- try {
177
- const stat = fs.statSync(fullPath);
178
- if (stat.mtimeMs < cutoffMs) {
179
- toPrune.push({ path: fullPath, mtime: stat.mtimeMs, size: entry.isDirectory() ? getDirSize(fullPath) : stat.size });
180
- }
181
- else if (entry.isDirectory()) {
182
- scanDir(fullPath);
183
- }
184
- }
185
- catch { /* skip inaccessible */ }
186
- }
187
- }
188
- scanDir(trashDir);
189
- if (toPrune.length === 0) {
190
- console.log(chalk.green(`No trash entries older than ${days} days.`));
191
- return;
192
- }
193
- const totalSize = toPrune.reduce((sum, e) => sum + e.size, 0);
194
- console.log(chalk.bold(`Trash entries older than ${days} days\n`));
195
- for (const entry of toPrune.slice(0, 20)) {
196
- const age = Math.floor((Date.now() - entry.mtime) / (24 * 60 * 60 * 1000));
197
- console.log(` ${chalk.gray(`${age}d ago`)} ${path.relative(trashDir, entry.path)}`);
198
- }
199
- if (toPrune.length > 20) {
200
- console.log(chalk.gray(` ... and ${toPrune.length - 20} more`));
201
- }
202
- console.log();
203
- if (options.dryRun) {
204
- console.log(chalk.gray(`${toPrune.length} entries (${formatBytes(totalSize)}). Run without --dry-run to delete.`));
205
- return;
206
- }
207
- if (!options.yes) {
208
- if (!isInteractiveTerminal()) {
209
- console.log(chalk.yellow('Non-interactive shell: pass -y to confirm, or --dry-run to preview.'));
210
- process.exit(1);
211
- }
212
- let ok = false;
213
- try {
214
- ok = await confirm({ message: `Delete ${toPrune.length} entries (${formatBytes(totalSize)})?`, default: false });
215
- }
216
- catch (err) {
217
- if (isPromptCancelled(err)) {
218
- console.log(chalk.gray('Cancelled'));
219
- return;
220
- }
221
- throw err;
222
- }
223
- if (!ok) {
224
- console.log(chalk.gray('Cancelled'));
225
- return;
226
- }
227
- }
228
- let deleted = 0;
229
- for (const entry of toPrune) {
230
- try {
231
- fs.rmSync(entry.path, { recursive: true, force: true });
232
- deleted++;
233
- }
234
- catch { /* ignore */ }
142
+ if (options.olderThan || options.yes || options.dryRun) {
143
+ console.log(chalk.gray('Trash expiry flags are accepted for compatibility but do not delete data.'));
235
144
  }
236
- console.log(chalk.green(`Pruned ${deleted} trash entries (${formatBytes(totalSize)}).`));
145
+ console.log(chalk.yellow('Trash is durable. agents-cli does not hard-delete soft-deleted version data.'));
146
+ console.log(chalk.gray('Inspect recoverable versions with: agents trash list'));
147
+ console.log(chalk.gray(`Trash path: ${trashDir}`));
237
148
  }
238
149
  async function runSessionsPrune(options) {
239
- const days = parseDays(options.olderThan || '90d', 90);
240
- const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1000;
241
- const count = countSessionsOlderThan(cutoffMs);
242
- if (count === 0) {
243
- console.log(chalk.green(`No sessions older than ${days} days.`));
244
- return;
150
+ if (options.olderThan || options.yes || options.dryRun) {
151
+ console.log(chalk.gray('Session prune flags are accepted for compatibility but do not delete data.'));
245
152
  }
246
- console.log(chalk.bold(`Sessions older than ${days} days: ${count}\n`));
247
- if (options.dryRun) {
248
- console.log(chalk.gray(`${count} session(s). Run without --dry-run to delete.`));
249
- return;
250
- }
251
- if (!options.yes) {
252
- if (!isInteractiveTerminal()) {
253
- console.log(chalk.yellow('Non-interactive shell: pass -y to confirm, or --dry-run to preview.'));
254
- process.exit(1);
255
- }
256
- let ok = false;
257
- try {
258
- ok = await confirm({ message: `Delete ${count} session records?`, default: false });
259
- }
260
- catch (err) {
261
- if (isPromptCancelled(err)) {
262
- console.log(chalk.gray('Cancelled'));
263
- return;
264
- }
265
- throw err;
266
- }
267
- if (!ok) {
268
- console.log(chalk.gray('Cancelled'));
269
- return;
270
- }
271
- }
272
- const deleted = deleteSessionsOlderThan(cutoffMs);
273
- console.log(chalk.green(`Pruned ${deleted} session records.`));
153
+ console.log(chalk.yellow('Session history is durable. agents-cli does not hard-delete session records.'));
154
+ console.log(chalk.gray('Browse sessions with: agents sessions'));
274
155
  }
275
156
  async function runRunsPrune(options) {
276
157
  const keep = options.keep ? parseInt(options.keep, 10) : 10;
@@ -380,13 +261,16 @@ async function runOrphanPrune(resourceTypes, options) {
380
261
  console.log(chalk.green(summary) + (failures > 0 ? chalk.red(`, ${failures} failed`) : '') + '.');
381
262
  }
382
263
  export function registerPruneCommand(program) {
383
- program
384
- .command('prune [target]')
385
- .description('Remove orphan resources, old versions, trash, sessions, or routine runs')
264
+ const pruneCmd = program.commands.find((cmd) => cmd.name() === 'prune') ?? program
265
+ .command('prune <specs...>')
266
+ .description('Uninstall agent CLI versions. Moves version data to trash for recovery.');
267
+ pruneCmd
268
+ .command('cleanup [target]')
269
+ .description('Remove orphan resources, old versions, or routine runs')
386
270
  .option('--all', 'For orphan cleanup: sweep every installed version (default: current default version per agent)')
387
271
  .option('--dry-run', 'Show what would be removed without deleting (default for state targets)')
388
272
  .option('-y, --yes', 'Skip confirmation prompt')
389
- .option('--older-than <days>', 'For trash/sessions: delete entries older than N days (default: 30d for trash, 90d for sessions)')
273
+ .option('--older-than <days>', 'Deprecated for trash/sessions; accepted but no data is deleted')
390
274
  .option('--keep <n>', 'For runs: keep the last N runs per job (default: 10)')
391
275
  .addHelpText('after', `
392
276
  Targets:
@@ -396,46 +280,40 @@ Targets:
396
280
  hooks Orphan hook scripts only
397
281
  versions Older duplicate version installs only
398
282
  <agent> Older duplicate versions for one agent (e.g. 'claude')
399
- trash Soft-deleted resources older than --older-than days (default 30)
400
- sessions Session records in sessions.db older than --older-than days (default 90)
283
+ trash No-op compatibility target; trash is durable
284
+ sessions No-op compatibility target; session history is durable
401
285
  runs Routine execution logs, keeping only --keep per job (default 10)
402
286
 
403
287
  Examples:
404
288
  # Full sweep: orphan resources + duplicate versions for current defaults
405
- agents prune
289
+ agents prune cleanup
406
290
 
407
291
  # Preview what a full sweep would remove
408
- agents prune --dry-run
292
+ agents prune cleanup --dry-run
409
293
 
410
294
  # Just orphan skills
411
- agents prune skills
295
+ agents prune cleanup skills
412
296
 
413
297
  # Just version dedup
414
- agents prune versions
298
+ agents prune cleanup versions
415
299
 
416
300
  # Deduplicate versions for one agent only
417
- agents prune claude
301
+ agents prune cleanup claude
418
302
 
419
303
  # Sweep every installed version's orphans, not only the defaults
420
- agents prune --all
421
-
422
- # Preview trash entries older than 30 days
423
- agents prune trash --dry-run
424
-
425
- # Delete trash entries older than 60 days
426
- agents prune trash --older-than 60 -y
304
+ agents prune cleanup --all
427
305
 
428
- # Preview session cleanup (90+ days old)
429
- agents prune sessions --dry-run
306
+ # Show the durable-trash notice
307
+ agents prune cleanup trash --dry-run
430
308
 
431
- # Delete sessions older than 180 days
432
- agents prune sessions --older-than 180 -y
309
+ # Show the durable-session notice
310
+ agents prune cleanup sessions --dry-run
433
311
 
434
312
  # Preview runs cleanup (keeping last 10)
435
- agents prune runs --dry-run
313
+ agents prune cleanup runs --dry-run
436
314
 
437
315
  # Keep only the last 5 runs per job
438
- agents prune runs --keep 5 -y
316
+ agents prune cleanup runs --keep 5 -y
439
317
 
440
318
  What's an orphan?
441
319
  A command, skill, or hook present inside a version home but missing from every
@@ -443,10 +321,11 @@ What's an orphan?
443
321
  repos). Usually leftovers from a resource that was deleted or moved but never
444
322
  reconciled into the version install.
445
323
 
446
- Soft-delete:
447
- Version directories are NEVER hard-deleted. \`prune\` moves them to
448
- ~/.agents/.trash/versions/<agent>/<version>/<timestamp>/. Use
449
- \`agents prune trash\` to expire old trash entries.
324
+ Durability:
325
+ Version directories are NEVER hard-deleted by agents-cli. Version prune and
326
+ cleanup move them to ~/.agents/.history/trash/versions/<agent>/<version>/<timestamp>/.
327
+ Session records are also durable; the sessions target remains only as a no-op
328
+ compatibility shim.
450
329
  `)
451
330
  .action(async (target, options) => {
452
331
  const parsed = parseTarget(target);
@@ -13,11 +13,12 @@ import { getUserAgentsDir, ensureAgentsDir, getEnabledExtraRepos, } from '../lib
13
13
  import { isGitRepo, pullRepo, isSystemRepoOrigin, } from '../lib/git.js';
14
14
  import * as fs from 'fs';
15
15
  import * as path from 'path';
16
- import { installVersion, listInstalledVersions, getGlobalDefault, setGlobalDefault, getVersionHomePath, syncResourcesToVersion, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, promptNewResourceSelection, promptResourceSelection, resolveConfiguredAgentTargets, } from '../lib/versions.js';
16
+ import { installVersion, listInstalledVersions, getGlobalDefault, setGlobalDefault, getVersionHomePath, syncResourcesToVersion, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, promptResourceSelection, resolveConfiguredAgentTargets, } from '../lib/versions.js';
17
+ import { listCliStatus, installCli, describeMethod, describeCheck, 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).
@@ -218,7 +219,7 @@ export function registerPullCommand(program) {
218
219
  if (!defaultVer)
219
220
  continue;
220
221
  const actuallySynced = getActuallySyncedResources(agentId, defaultVer);
221
- const newResources = getNewResources(available, actuallySynced);
222
+ const newResources = getNewResources(available, actuallySynced, getProjectOnlyResources());
222
223
  const hasAnySynced = actuallySynced.commands.length > 0 ||
223
224
  actuallySynced.skills.length > 0 ||
224
225
  actuallySynced.hooks.length > 0 ||
@@ -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 \`${describeCheck(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, REPO_DISPLAY_MAX } 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
+ // Guarded on process.stdout.isTTY so that piped/redirected output never
137
+ // contains raw ESC ] 8 ;; ... BEL escape sequences.
138
+ const link = (label, url) => url && process.stdout.isTTY ? `\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 = REPO_DISPLAY_MAX;
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
- .option('-m, --mode <mode>', 'Execution mode: plan (read-only) or edit (can write files)', 'plan')
187
+ .option('-m, --mode <mode>', "Execution mode: plan (read-only), edit (can write files), auto (smart classifier), or skip (bypass all permission prompts). 'full' accepted as alias for skip.", '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.')
@@ -7,11 +7,11 @@ import { select, checkbox } from '@inquirer/prompts';
7
7
  import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
8
8
  import { cloneRepo } from '../lib/git.js';
9
9
  import { discoverInstructionsFromRepo, discoverRuleFilesFromRepo, installInstructionsCentrally, uninstallInstructions, listInstalledInstructionsWithScope, instructionsExists, getInstructionsContent, listCentralRules, } from '../lib/rules/rules.js';
10
- import { listInstalledVersions, getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, resolveAgentVersionTargets, } from '../lib/versions.js';
10
+ import { listInstalledVersions, getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, } from '../lib/versions.js';
11
11
  import { recordVersionResources, getActiveRulesPreset, setActiveRulesPreset } from '../lib/state.js';
12
12
  import { discoverRulesLayers } from '../lib/rules/compose.js';
13
13
  import * as yaml from 'yaml';
14
- import { isPromptCancelled, formatPath, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, requireDestructiveArg, } from './utils.js';
14
+ import { isPromptCancelled, formatPath, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, requireDestructiveArg, resolveAgentTargetsAutoInstalling, } from './utils.js';
15
15
  /** Register the `agents rules` command tree (list, add, view, remove). */
16
16
  export function registerRulesCommands(program) {
17
17
  const rulesCmd = program
@@ -317,13 +317,17 @@ Examples:
317
317
  let selectedAgents;
318
318
  let versionSelections;
319
319
  if (options.agents) {
320
- const result = resolveAgentVersionTargets(options.agents, ALL_AGENT_IDS);
320
+ const result = await resolveAgentTargetsAutoInstalling(options.agents, ALL_AGENT_IDS, { yes: options.yes });
321
+ if (!result) {
322
+ console.log(chalk.gray('Cancelled.'));
323
+ return;
324
+ }
321
325
  selectedAgents = result.selectedAgents;
322
326
  versionSelections = result.versionSelections;
323
327
  }
324
328
  else {
325
329
  const result = await promptAgentVersionSelection(ALL_AGENT_IDS, {
326
- skipPrompts: options.yes || !isInteractiveTerminal(),
330
+ skipPrompts: options.yes,
327
331
  });
328
332
  selectedAgents = result.selectedAgents;
329
333
  versionSelections = result.versionSelections;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `agents secrets migrate-acl` — refresh existing keychain items so they pick
3
+ * up the new SecAccess ACL written by the signed Agents CLI.app helper.
4
+ *
5
+ * Items created before 1.19.2 (or by `security add-generic-password` directly)
6
+ * may carry the legacy "this-app-only" ACL that prompts the user for a
7
+ * password on every read. Re-writing them through the helper bakes in the
8
+ * empty trusted-app ACL that suppresses the prompt and lets the helper read
9
+ * them under LocalAuthentication instead.
10
+ *
11
+ * Sequence per item:
12
+ * 1. Read the current value (no auth prompt path — uses the unauthenticated
13
+ * `security` CLI for non-sync items, helper `get` for sync items).
14
+ * 2. Append (item, value, sync) to an encrypted backup before any writes.
15
+ * 3. Delete + rewrite via the helper so macOS hands us a fresh ACL on the
16
+ * new item.
17
+ * 4. Read back via the helper to verify the value round-trips.
18
+ *
19
+ * `--dry-run` (default) reports the planned actions. `--commit` performs the
20
+ * writes and produces the backup.
21
+ */
22
+ import type { Command } from 'commander';
23
+ /** Register `agents secrets migrate-acl` on the parent secrets Command. */
24
+ export declare function registerSecretsMigrateAclCommand(secrets: Command): void;