@phnx-labs/agents-cli 1.14.7 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +78 -39
  2. package/README.md +74 -7
  3. package/dist/commands/alias.js +2 -2
  4. package/dist/commands/beta.js +6 -1
  5. package/dist/commands/browser-picker.d.ts +21 -0
  6. package/dist/commands/browser-picker.js +114 -0
  7. package/dist/commands/browser.js +546 -75
  8. package/dist/commands/commands.js +72 -22
  9. package/dist/commands/daemon.js +2 -2
  10. package/dist/commands/exec.js +9 -2
  11. package/dist/commands/fork.js +2 -2
  12. package/dist/commands/hooks.js +71 -26
  13. package/dist/commands/mcp.js +85 -43
  14. package/dist/commands/plugins.js +48 -15
  15. package/dist/commands/prune.d.ts +0 -20
  16. package/dist/commands/prune.js +291 -16
  17. package/dist/commands/pull.js +3 -3
  18. package/dist/commands/repo.js +1 -1
  19. package/dist/commands/routines.js +2 -2
  20. package/dist/commands/secrets.js +37 -1
  21. package/dist/commands/sessions.js +62 -19
  22. package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
  23. package/dist/commands/{init.js → setup.js} +32 -21
  24. package/dist/commands/skills.js +60 -19
  25. package/dist/commands/subagents.js +41 -13
  26. package/dist/commands/teams.js +2 -3
  27. package/dist/commands/usage.js +6 -0
  28. package/dist/commands/utils.d.ts +16 -0
  29. package/dist/commands/utils.js +32 -0
  30. package/dist/commands/versions.js +8 -6
  31. package/dist/commands/view.js +61 -16
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +17 -20
  34. package/dist/lib/agents.js +2 -2
  35. package/dist/lib/auto-pull-worker.js +2 -3
  36. package/dist/lib/auto-pull.js +2 -2
  37. package/dist/lib/browser/cdp.d.ts +7 -1
  38. package/dist/lib/browser/cdp.js +29 -1
  39. package/dist/lib/browser/chrome.js +6 -3
  40. package/dist/lib/browser/devices.d.ts +4 -0
  41. package/dist/lib/browser/devices.js +27 -0
  42. package/dist/lib/browser/drivers/local.js +9 -4
  43. package/dist/lib/browser/drivers/ssh.d.ts +1 -0
  44. package/dist/lib/browser/drivers/ssh.js +32 -4
  45. package/dist/lib/browser/ipc.js +145 -23
  46. package/dist/lib/browser/profiles.d.ts +5 -2
  47. package/dist/lib/browser/profiles.js +77 -37
  48. package/dist/lib/browser/service.d.ts +84 -13
  49. package/dist/lib/browser/service.js +806 -122
  50. package/dist/lib/browser/types.d.ts +81 -3
  51. package/dist/lib/browser/types.js +16 -0
  52. package/dist/lib/cloud/rush.js +2 -2
  53. package/dist/lib/cloud/store.js +2 -2
  54. package/dist/lib/commands.d.ts +1 -0
  55. package/dist/lib/commands.js +6 -2
  56. package/dist/lib/daemon.js +6 -7
  57. package/dist/lib/doctor-diff.js +4 -4
  58. package/dist/lib/events.d.ts +94 -1
  59. package/dist/lib/events.js +264 -6
  60. package/dist/lib/exec.js +16 -10
  61. package/dist/lib/hooks.d.ts +11 -7
  62. package/dist/lib/hooks.js +125 -49
  63. package/dist/lib/migrate.d.ts +1 -1
  64. package/dist/lib/migrate.js +1178 -21
  65. package/dist/lib/models.js +2 -2
  66. package/dist/lib/permissions.d.ts +14 -11
  67. package/dist/lib/permissions.js +46 -42
  68. package/dist/lib/plugins.d.ts +30 -1
  69. package/dist/lib/plugins.js +75 -3
  70. package/dist/lib/pty-server.js +9 -10
  71. package/dist/lib/resources/hooks.d.ts +5 -1
  72. package/dist/lib/resources/hooks.js +21 -4
  73. package/dist/lib/rotate.js +3 -4
  74. package/dist/lib/routines.d.ts +15 -0
  75. package/dist/lib/routines.js +68 -0
  76. package/dist/lib/runner.js +9 -5
  77. package/dist/lib/secrets/index.d.ts +14 -11
  78. package/dist/lib/secrets/index.js +49 -21
  79. package/dist/lib/secrets/linux.d.ts +27 -0
  80. package/dist/lib/secrets/linux.js +161 -0
  81. package/dist/lib/session/active.d.ts +3 -0
  82. package/dist/lib/session/active.js +92 -6
  83. package/dist/lib/session/cloud.js +2 -2
  84. package/dist/lib/session/db.d.ts +4 -0
  85. package/dist/lib/session/db.js +34 -3
  86. package/dist/lib/session/discover.js +30 -15
  87. package/dist/lib/session/team-filter.js +2 -2
  88. package/dist/lib/shims.d.ts +2 -2
  89. package/dist/lib/shims.js +6 -6
  90. package/dist/lib/skills.js +6 -2
  91. package/dist/lib/state.d.ts +86 -14
  92. package/dist/lib/state.js +150 -23
  93. package/dist/lib/subagents.d.ts +28 -0
  94. package/dist/lib/subagents.js +98 -1
  95. package/dist/lib/sync-manifest.d.ts +1 -1
  96. package/dist/lib/sync-manifest.js +3 -3
  97. package/dist/lib/teams/persistence.js +15 -5
  98. package/dist/lib/teams/registry.js +2 -2
  99. package/dist/lib/types.d.ts +32 -3
  100. package/dist/lib/types.js +3 -3
  101. package/dist/lib/usage.d.ts +1 -1
  102. package/dist/lib/usage.js +15 -48
  103. package/dist/lib/versions.js +31 -21
  104. package/package.json +1 -1
  105. package/scripts/postinstall.js +37 -9
@@ -1,14 +1,44 @@
1
+ /**
2
+ * Top-level `agents prune` — destructive cleanup across the install.
3
+ *
4
+ * Cleanup targets:
5
+ * - Resource orphans: command/skill/hook files inside a version home that no
6
+ * longer come from any source (deleted from ~/.agents/ but never reconciled
7
+ * into the version install).
8
+ * - Version duplicates: older installed versions of an agent that share an
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.
12
+ * - Runs: routine execution logs, keeping only the last N per job.
13
+ *
14
+ * Sync (additive: copy missing/changed files into version homes) is no longer
15
+ * a user-facing verb — `syncResourcesToVersion` runs at agent launch and
16
+ * applies adds/updates automatically. Pruning, however, is destructive, so it
17
+ * stays explicit.
18
+ *
19
+ * Default scope: each agent's currently-pinned default version for orphan
20
+ * cleanup, plus the standard cross-agent version-dedup pass. Pass `--all`
21
+ * to widen orphan cleanup to every installed version.
22
+ */
23
+ import * as fs from 'fs';
24
+ import * as path from 'path';
1
25
  import chalk from 'chalk';
2
26
  import { confirm } from '@inquirer/prompts';
3
27
  import { diffVersionCommands, iterCommandsCapableVersions, removeCommandFromVersion, } from '../lib/commands.js';
4
28
  import { diffVersionSkills, iterSkillsCapableVersions, removeSkillFromVersion, } from '../lib/skills.js';
5
29
  import { diffVersionHooks, iterHooksCapableVersions, removeHookFromVersion, } from '../lib/hooks.js';
30
+ import { diffVersionPlugins, iterPluginsCapableVersions, removePluginSkillFromVersion, } from '../lib/plugins.js';
31
+ import { diffVersionSubagents, iterSubagentsCapableVersions, removeSubagentFromVersion, } from '../lib/subagents.js';
6
32
  import { getGlobalDefault } from '../lib/versions.js';
7
33
  import { resolveAgentName, formatAgentError } from '../lib/agents.js';
8
34
  import { pruneDuplicates } from './view.js';
9
35
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
10
- const RESOURCE_TYPES = ['commands', 'skills', 'hooks'];
11
- const ALL_TYPES = [...RESOURCE_TYPES, 'versions'];
36
+ import { getTrashDir } from '../lib/state.js';
37
+ import { countSessionsOlderThan, deleteSessionsOlderThan } from '../lib/session/db.js';
38
+ import { previewRunsPrune, pruneRuns, countAllRuns } from '../lib/routines.js';
39
+ const RESOURCE_TYPES = ['commands', 'skills', 'hooks', 'plugins', 'subagents'];
40
+ const STATE_TYPES = ['trash', 'sessions', 'runs'];
41
+ const ALL_TYPES = [...RESOURCE_TYPES, 'versions', ...STATE_TYPES];
12
42
  function scopePairs(pairs, all) {
13
43
  if (all)
14
44
  return pairs;
@@ -40,6 +70,22 @@ function collectOrphans(types, all) {
40
70
  }
41
71
  }
42
72
  }
73
+ if (types.includes('plugins')) {
74
+ for (const { agent, version } of scopePairs(iterPluginsCapableVersions(), all)) {
75
+ const diff = diffVersionPlugins(agent, version);
76
+ if (diff.orphans.length > 0) {
77
+ groups.push({ type: 'plugins', agent, version, orphans: diff.orphans });
78
+ }
79
+ }
80
+ }
81
+ if (types.includes('subagents')) {
82
+ for (const { agent, version } of scopePairs(iterSubagentsCapableVersions(), all)) {
83
+ const diff = diffVersionSubagents(agent, version);
84
+ if (diff.orphans.length > 0) {
85
+ groups.push({ type: 'subagents', agent, version, orphans: diff.orphans });
86
+ }
87
+ }
88
+ }
43
89
  return groups;
44
90
  }
45
91
  function removeOne(group, name) {
@@ -50,6 +96,10 @@ function removeOne(group, name) {
50
96
  return removeSkillFromVersion(group.agent, group.version, name);
51
97
  case 'hooks':
52
98
  return removeHookFromVersion(group.agent, group.version, name);
99
+ case 'plugins':
100
+ return removePluginSkillFromVersion(group.agent, group.version, name);
101
+ case 'subagents':
102
+ return removeSubagentFromVersion(group.agent, group.version, name);
53
103
  }
54
104
  }
55
105
  function parseTarget(arg) {
@@ -59,6 +109,9 @@ function parseTarget(arg) {
59
109
  if (RESOURCE_TYPES.includes(arg)) {
60
110
  return { resourceTypes: [arg], includeVersions: false };
61
111
  }
112
+ if (STATE_TYPES.includes(arg)) {
113
+ return { resourceTypes: [], includeVersions: false, stateType: arg };
114
+ }
62
115
  if (arg === 'versions') {
63
116
  return { resourceTypes: [], includeVersions: true };
64
117
  }
@@ -72,6 +125,202 @@ function parseTarget(arg) {
72
125
  console.log(chalk.gray(formatAgentError(arg)));
73
126
  process.exit(1);
74
127
  }
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
+ function formatBytes(bytes) {
135
+ if (bytes < 1024)
136
+ return `${bytes} B`;
137
+ if (bytes < 1024 * 1024)
138
+ return `${(bytes / 1024).toFixed(1)} KB`;
139
+ if (bytes < 1024 * 1024 * 1024)
140
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
141
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
142
+ }
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
+ async function runTrashPrune(options) {
163
+ const trashDir = getTrashDir();
164
+ if (!fs.existsSync(trashDir)) {
165
+ console.log(chalk.green('Trash is empty.'));
166
+ return;
167
+ }
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 */ }
235
+ }
236
+ console.log(chalk.green(`Pruned ${deleted} trash entries (${formatBytes(totalSize)}).`));
237
+ }
238
+ 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;
245
+ }
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.`));
274
+ }
275
+ async function runRunsPrune(options) {
276
+ const keep = options.keep ? parseInt(options.keep, 10) : 10;
277
+ if (isNaN(keep) || keep < 0) {
278
+ console.log(chalk.red('--keep must be a non-negative integer'));
279
+ process.exit(1);
280
+ }
281
+ const preview = previewRunsPrune(keep);
282
+ const total = countAllRuns();
283
+ if (preview.length === 0) {
284
+ console.log(chalk.green(`All jobs have ${keep} or fewer runs. Nothing to prune.`));
285
+ return;
286
+ }
287
+ console.log(chalk.bold(`Routine runs to prune (keeping last ${keep} per job)\n`));
288
+ const byJob = new Map();
289
+ for (const run of preview) {
290
+ byJob.set(run.jobName, (byJob.get(run.jobName) || 0) + 1);
291
+ }
292
+ for (const [job, count] of byJob) {
293
+ console.log(` ${chalk.cyan(job)}: ${count} old runs`);
294
+ }
295
+ console.log();
296
+ if (options.dryRun) {
297
+ console.log(chalk.gray(`${preview.length} of ${total} runs would be deleted. Run without --dry-run to delete.`));
298
+ return;
299
+ }
300
+ if (!options.yes) {
301
+ if (!isInteractiveTerminal()) {
302
+ console.log(chalk.yellow('Non-interactive shell: pass -y to confirm, or --dry-run to preview.'));
303
+ process.exit(1);
304
+ }
305
+ let ok = false;
306
+ try {
307
+ ok = await confirm({ message: `Delete ${preview.length} old runs?`, default: false });
308
+ }
309
+ catch (err) {
310
+ if (isPromptCancelled(err)) {
311
+ console.log(chalk.gray('Cancelled'));
312
+ return;
313
+ }
314
+ throw err;
315
+ }
316
+ if (!ok) {
317
+ console.log(chalk.gray('Cancelled'));
318
+ return;
319
+ }
320
+ }
321
+ const { deleted, bytesFreed } = pruneRuns(keep);
322
+ console.log(chalk.green(`Pruned ${deleted} runs (${formatBytes(bytesFreed)}).`));
323
+ }
75
324
  async function runOrphanPrune(resourceTypes, options) {
76
325
  const groups = collectOrphans(resourceTypes, options.all === true);
77
326
  if (groups.length === 0) {
@@ -133,10 +382,12 @@ async function runOrphanPrune(resourceTypes, options) {
133
382
  export function registerPruneCommand(program) {
134
383
  program
135
384
  .command('prune [target]')
136
- .description('Remove orphan resources (commands/skills/hooks) and/or older duplicate version installs (versions soft-deleted to ~/.agents-system/trash/)')
385
+ .description('Remove orphan resources, old versions, trash, sessions, or routine runs')
137
386
  .option('--all', 'For orphan cleanup: sweep every installed version (default: current default version per agent)')
138
- .option('--dry-run', 'Show what would be removed without deleting')
387
+ .option('--dry-run', 'Show what would be removed without deleting (default for state targets)')
139
388
  .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)')
390
+ .option('--keep <n>', 'For runs: keep the last N runs per job (default: 10)')
140
391
  .addHelpText('after', `
141
392
  Targets:
142
393
  (none) Orphans across commands, skills, hooks + duplicate versions
@@ -145,6 +396,9 @@ Targets:
145
396
  hooks Orphan hook scripts only
146
397
  versions Older duplicate version installs only
147
398
  <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)
401
+ runs Routine execution logs, keeping only --keep per job (default 10)
148
402
 
149
403
  Examples:
150
404
  # Full sweep: orphan resources + duplicate versions for current defaults
@@ -156,14 +410,26 @@ Examples:
156
410
  # Just version dedup
157
411
  agents prune versions
158
412
 
159
- # Just version dedup for one agent
160
- agents prune claude
161
-
162
413
  # Sweep every installed version's orphans, not only the defaults
163
414
  agents prune --all
164
415
 
165
- # Preview without deleting
166
- agents prune --dry-run
416
+ # Preview trash entries older than 30 days
417
+ agents prune trash --dry-run
418
+
419
+ # Delete trash entries older than 60 days
420
+ agents prune trash --older-than 60 -y
421
+
422
+ # Preview session cleanup (90+ days old)
423
+ agents prune sessions --dry-run
424
+
425
+ # Delete sessions older than 180 days
426
+ agents prune sessions --older-than 180 -y
427
+
428
+ # Preview runs cleanup (keeping last 10)
429
+ agents prune runs --dry-run
430
+
431
+ # Keep only the last 5 runs per job
432
+ agents prune runs --keep 5 -y
167
433
 
168
434
  What's an orphan?
169
435
  A command, skill, or hook present inside a version home but missing from every
@@ -171,18 +437,27 @@ What's an orphan?
171
437
  repos). Usually leftovers from a resource that was deleted or moved but never
172
438
  reconciled into the version install.
173
439
 
174
- What this does NOT do:
175
- Adds and updates flow through the auto-sync that runs when you launch the
176
- agent — there is no manual sync verb.
177
-
178
440
  Soft-delete:
179
441
  Version directories are NEVER hard-deleted. \`prune\` moves them to
180
- ~/.agents-system/trash/versions/<agent>/<version>/<timestamp>/. Recover
181
- with \`agents trash list\` and \`agents trash restore <agent>@<version>\`.
182
- The trash never auto-expires; \`rm -rf\` it manually when you're sure.
442
+ ~/.agents/.trash/versions/<agent>/<version>/<timestamp>/. Use
443
+ \`agents prune trash\` to expire old trash entries.
183
444
  `)
184
445
  .action(async (target, options) => {
185
446
  const parsed = parseTarget(target);
447
+ if (parsed.stateType) {
448
+ switch (parsed.stateType) {
449
+ case 'trash':
450
+ await runTrashPrune(options);
451
+ break;
452
+ case 'sessions':
453
+ await runSessionsPrune(options);
454
+ break;
455
+ case 'runs':
456
+ await runRunsPrune(options);
457
+ break;
458
+ }
459
+ return;
460
+ }
186
461
  if (parsed.resourceTypes.length > 0) {
187
462
  await runOrphanPrune(parsed.resourceTypes, options);
188
463
  if (parsed.includeVersions)
@@ -80,12 +80,12 @@ Skip CLI installs with --skip-clis when you only want config updates, not versio
80
80
  // auto-syncs the system repo in the background and surfaces upstream
81
81
  // changes for user/extra repos as one-line notices. Repo lifecycle is
82
82
  // managed under `agents repo`. We keep this command functional today
83
- // because `agents init` still invokes it for first-time setup; once
84
- // init is refactored to call the bootstrap helpers directly, this
83
+ // because `agents setup` still invokes it for first-time setup; once
84
+ // setup is refactored to call the bootstrap helpers directly, this
85
85
  // command will hard-error like `agents memory` does.
86
86
  if (!options.yes && process.argv[2] === 'pull') {
87
87
  process.stderr.write('agents-cli: "agents pull" is deprecated.\n' +
88
- ' First-time setup: agents init\n' +
88
+ ' First-time setup: agents setup\n' +
89
89
  ' Force a sync now: agents repo pull\n' +
90
90
  ' Push your repo: agents repo push\n\n');
91
91
  }
@@ -257,7 +257,7 @@ Examples:
257
257
  const systemStatus = !systemOnDisk
258
258
  ? chalk.red('missing')
259
259
  : !systemIsGit
260
- ? chalk.yellow('not a git repo — run: agents init')
260
+ ? chalk.yellow('not a git repo — run: agents setup')
261
261
  : chalk.green('cloned');
262
262
  const systemCommitLabel = systemCommit ? chalk.gray(`(${systemCommit})`) : '';
263
263
  console.log(chalk.bold('System (~/.agents-system/)'));
@@ -571,8 +571,8 @@ Examples:
571
571
  .action(async (options) => {
572
572
  if (options.follow) {
573
573
  const { exec: execCb } = await import('child_process');
574
- const { getAgentsDir } = await import('../lib/state.js');
575
- const logPath = path.join(getAgentsDir(), 'helpers/daemon/logs.jsonl');
574
+ const { getDaemonDir } = await import('../lib/state.js');
575
+ const logPath = path.join(getDaemonDir(), 'logs.jsonl');
576
576
  const child = execCb(`tail -f "${logPath}"`);
577
577
  child.stdout?.pipe(process.stdout);
578
578
  child.stderr?.pipe(process.stderr);
@@ -280,6 +280,10 @@ Examples:
280
280
  # Eval the bundle into your current shell
281
281
  eval "$(agents secrets export prod --plaintext)"
282
282
 
283
+ # Run a command with secrets injected
284
+ agents secrets exec prod -- ./deploy.sh
285
+ agents secrets exec hetzner.com -- crabbox list
286
+
283
287
  # Remove one key (purges the keychain item by default)
284
288
  agents secrets remove prod STRIPE_API_KEY
285
289
 
@@ -297,7 +301,7 @@ Examples:
297
301
  registerCommandGroups(cmd, [
298
302
  { title: 'Bundle commands', names: ['list', 'view', 'create', 'delete'] },
299
303
  { title: 'Secret commands', names: ['add', 'rotate', 'remove', 'import', 'export'] },
300
- { title: 'Utilities', names: ['generate'] },
304
+ { title: 'Utilities', names: ['exec', 'generate'] },
301
305
  ]);
302
306
  cmd
303
307
  .command('list')
@@ -711,6 +715,38 @@ Examples:
711
715
  process.exit(1);
712
716
  }
713
717
  });
718
+ cmd
719
+ .command('exec <bundle> [command...]')
720
+ .description('Run a command with the bundle\'s secrets injected into the environment')
721
+ .allowUnknownOption()
722
+ .action(async (bundleName, commandParts) => {
723
+ try {
724
+ if (commandParts.length === 0) {
725
+ console.error(chalk.red('Usage: agents secrets exec <bundle> -- <command...>'));
726
+ process.exit(1);
727
+ }
728
+ const { resolveBundleEnv } = await import('../lib/secrets/bundles.js');
729
+ const bundle = readBundle(bundleName);
730
+ const secretEnv = resolveBundleEnv(bundle);
731
+ const { spawn } = await import('child_process');
732
+ const [cmd, ...args] = commandParts;
733
+ const proc = spawn(cmd, args, {
734
+ stdio: 'inherit',
735
+ env: { ...process.env, ...secretEnv },
736
+ });
737
+ proc.on('close', (code) => process.exit(code ?? 0));
738
+ proc.on('error', (err) => {
739
+ console.error(chalk.red(`Failed to run '${cmd}': ${err.message}`));
740
+ process.exit(1);
741
+ });
742
+ }
743
+ catch (err) {
744
+ if (isPromptCancelled(err))
745
+ return;
746
+ console.error(chalk.red(err.message));
747
+ process.exit(1);
748
+ }
749
+ });
714
750
  cmd
715
751
  .command('generate [length]')
716
752
  .description('Generate a random password')
@@ -160,6 +160,26 @@ function formatStartedAt(startedAtMs) {
160
160
  return '-';
161
161
  return formatRelativeTime(new Date(startedAtMs).toISOString());
162
162
  }
163
+ /** Build a display-friendly description for an active session (label or topic). */
164
+ function buildSessionDescription(s) {
165
+ if (s.context === 'cloud') {
166
+ return `${s.cloudProvider ?? ''}${s.cloudTaskId ? ` · ${s.cloudTaskId.slice(0, 12)}` : ''}`;
167
+ }
168
+ if (s.context === 'teams') {
169
+ const parts = [s.teamName];
170
+ if (s.label)
171
+ parts.push(s.label);
172
+ else if (s.topic)
173
+ parts.push(s.topic);
174
+ return parts.filter(Boolean).join(' · ');
175
+ }
176
+ // Terminal or headless: prefer label, then topic
177
+ if (s.label)
178
+ return s.label;
179
+ if (s.topic)
180
+ return s.topic;
181
+ return '';
182
+ }
163
183
  /** Render the unified active-session view. */
164
184
  async function renderActiveSessions(asJson) {
165
185
  const sessions = await getActiveSessions();
@@ -171,26 +191,49 @@ async function renderActiveSessions(asJson) {
171
191
  console.log(chalk.gray('No active agent sessions.'));
172
192
  return;
173
193
  }
194
+ // Group sessions by workspace (cwd), with cloud/undefined grouped separately
195
+ const byWorkspace = new Map();
174
196
  for (const s of sessions) {
175
- const kindCol = colorAgent(s.kind)(padRight(truncate(s.kind, 8), 9));
176
- const ctxCol = contextColor(s.context)(padRight(truncate(s.context, 8), 9));
177
- const hostCol = chalk.gray(padRight(truncate(s.host ?? '-', 8), 9));
178
- const statusCol = statusColor(s.status)(padRight(truncate(s.status, 8), 9));
179
- const pidCol = chalk.yellow(padRight(s.pid ? String(s.pid) : '-', 7));
180
- const idCol = chalk.white(padRight(s.sessionId ? s.sessionId.slice(0, 8) : '-', 10));
181
- const detail = s.context === 'cloud'
182
- ? `${s.cloudProvider ?? ''}${s.cloudTaskId ? ` · ${s.cloudTaskId.slice(0, 12)}` : ''}`
183
- : s.context === 'teams'
184
- ? `${s.teamName ?? ''}${s.label ? ` · ${s.label}` : ''}`
185
- : s.label ?? shortCwd(s.cwd);
186
- console.log(pidCol +
187
- kindCol +
188
- ctxCol +
189
- hostCol +
190
- statusCol +
191
- idCol +
192
- chalk.cyan(padRight(truncate(detail || '-', 30), 32)) +
193
- chalk.gray(formatStartedAt(s.startedAtMs)));
197
+ const key = s.cwd ?? (s.context === 'cloud' ? '__cloud__' : '__unknown__');
198
+ const list = byWorkspace.get(key) || [];
199
+ list.push(s);
200
+ byWorkspace.set(key, list);
201
+ }
202
+ // Sort workspaces: most sessions first, then alphabetically
203
+ const sortedKeys = Array.from(byWorkspace.keys()).sort((a, b) => {
204
+ const aCount = byWorkspace.get(a).length;
205
+ const bCount = byWorkspace.get(b).length;
206
+ if (aCount !== bCount)
207
+ return bCount - aCount;
208
+ return a.localeCompare(b);
209
+ });
210
+ let first = true;
211
+ for (const key of sortedKeys) {
212
+ const group = byWorkspace.get(key);
213
+ if (!first)
214
+ console.log();
215
+ first = false;
216
+ // Print workspace header
217
+ const header = key === '__cloud__'
218
+ ? chalk.magenta.bold('cloud')
219
+ : key === '__unknown__'
220
+ ? chalk.gray.bold('unknown')
221
+ : chalk.cyan.bold(shortCwd(key));
222
+ console.log(`${header} ${chalk.gray(`(${group.length})`)}`);
223
+ // Print each session in this workspace
224
+ for (const s of group) {
225
+ const kindCol = colorAgent(s.kind)(padRight(truncate(s.kind, 8), 9));
226
+ const hostCol = chalk.gray(padRight(truncate(s.host ?? '-', 8), 9));
227
+ const statusCol = statusColor(s.status)(padRight(truncate(s.status, 7), 8));
228
+ const pidCol = chalk.yellow(padRight(s.pid ? String(s.pid) : '-', 7));
229
+ const desc = buildSessionDescription(s);
230
+ console.log(' ' +
231
+ pidCol +
232
+ kindCol +
233
+ hostCol +
234
+ statusCol +
235
+ chalk.white(truncate(desc || '-', 50)));
236
+ }
194
237
  }
195
238
  const runningCount = sessions.filter(s => s.status === 'running').length;
196
239
  const idleCount = sessions.filter(s => s.status === 'idle').length;
@@ -1,20 +1,21 @@
1
1
  /**
2
- * First-run initialization command.
2
+ * First-run setup command.
3
3
  *
4
- * Registers the `agents init` command which clones the system repo into
4
+ * Registers the `agents setup` command which clones the system repo into
5
5
  * ~/.agents-system/ and installs agent CLIs with resource syncing.
6
6
  */
7
7
  import type { Command } from 'commander';
8
8
  /** First-run setup. Clones ~/.agents-system/ from the system repo if needed. */
9
- export declare function runInit(program: Command, options?: {
9
+ export declare function runSetup(program: Command, options?: {
10
10
  force?: boolean;
11
+ suppressFooter?: boolean;
11
12
  }): Promise<void>;
12
13
  /**
13
14
  * Ensure the system repo exists before running a command that needs it.
14
15
  * If ~/.agents-system/ is not a git repo AND we're in an interactive TTY,
15
- * prompt the user to run init now. In non-interactive mode, print a clear
16
+ * prompt the user to run setup now. In non-interactive mode, print a clear
16
17
  * error and exit.
17
18
  */
18
19
  export declare function ensureInitialized(program: Command): Promise<void>;
19
- /** Register the `agents init` command. */
20
- export declare function registerInitCommand(program: Command): void;
20
+ /** Register the `agents setup` command. */
21
+ export declare function registerSetupCommand(program: Command): void;