@phnx-labs/agents-cli 1.14.7 → 1.15.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/README.md +68 -1
- package/dist/commands/beta.js +6 -1
- package/dist/commands/exec.js +9 -2
- package/dist/commands/init.js +10 -0
- package/dist/commands/mcp.js +4 -4
- package/dist/commands/prune.d.ts +0 -20
- package/dist/commands/prune.js +268 -15
- package/dist/commands/teams.js +2 -3
- package/dist/commands/usage.js +6 -0
- package/dist/commands/versions.js +8 -6
- package/dist/lib/browser/chrome.js +1 -1
- package/dist/lib/browser/drivers/ssh.d.ts +1 -0
- package/dist/lib/browser/drivers/ssh.js +23 -2
- package/dist/lib/browser/ipc.js +1 -0
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +84 -7
- package/dist/lib/daemon.js +4 -4
- package/dist/lib/events.d.ts +94 -1
- package/dist/lib/events.js +262 -4
- package/dist/lib/exec.js +16 -10
- package/dist/lib/permissions.d.ts +6 -3
- package/dist/lib/permissions.js +38 -34
- package/dist/lib/routines.d.ts +15 -0
- package/dist/lib/routines.js +68 -0
- package/dist/lib/runner.js +9 -5
- package/dist/lib/secrets/index.d.ts +14 -11
- package/dist/lib/secrets/index.js +49 -21
- package/dist/lib/secrets/linux.d.ts +27 -0
- package/dist/lib/secrets/linux.js +161 -0
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +26 -0
- package/dist/lib/usage.d.ts +1 -1
- package/dist/lib/usage.js +13 -46
- package/dist/lib/versions.js +11 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +37 -9
package/README.md
CHANGED
|
@@ -53,6 +53,7 @@ Also available as `ag` -- all commands work with both `agents` and `ag`.
|
|
|
53
53
|
- [PTY](#pty)
|
|
54
54
|
- [Portable setup](#portable-setup)
|
|
55
55
|
- [Private skills](#private-skills)
|
|
56
|
+
- [Security & Privacy](#security--privacy)
|
|
56
57
|
- [Compatibility](#compatibility)
|
|
57
58
|
- [FAQ](#faq)
|
|
58
59
|
|
|
@@ -453,6 +454,70 @@ Extras clone into `~/.agents-system/.repos/<alias>/` and ship the same layout as
|
|
|
453
454
|
|
|
454
455
|
---
|
|
455
456
|
|
|
457
|
+
## Security & Privacy
|
|
458
|
+
|
|
459
|
+
**Everything stays on your machine.** No telemetry, no cloud sync (unless you opt into iCloud Keychain for secrets), no phone-home. Here's exactly what `agents-cli` stores locally and why.
|
|
460
|
+
|
|
461
|
+
### Event log
|
|
462
|
+
|
|
463
|
+
Every agent run, version install, browser launch, and secrets access is logged to `~/.agents/logs/events-YYYY-MM-DD.jsonl`. This gives you a complete record of what agents did on your machine.
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
# What gets logged (example event):
|
|
467
|
+
{
|
|
468
|
+
"ts": "2026-05-09T10:23:45Z",
|
|
469
|
+
"event": "agent.run.end",
|
|
470
|
+
"agent": "claude",
|
|
471
|
+
"version": "2.1.121",
|
|
472
|
+
"prompt": "Fix the auth bug in...", # truncated to 200 chars
|
|
473
|
+
"durationMs": 45230,
|
|
474
|
+
"exitCode": 0,
|
|
475
|
+
"hostname": "your-mac",
|
|
476
|
+
"platform": "darwin"
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**What's logged:** Operation type, agent, version, timing, truncated prompts (first 200 chars), exit codes, errors. **What's NOT logged:** Full prompts, outputs, file contents, secret values (only bundle names).
|
|
481
|
+
|
|
482
|
+
**Permissions:** Logs directory is `0700` (owner-only), files are `0600`. Only you can read them.
|
|
483
|
+
|
|
484
|
+
**Retention:** 30 days by default, then auto-pruned.
|
|
485
|
+
|
|
486
|
+
**Opt out:** Set `AGENTS_DISABLE_EVENT_LOG=1` in your shell to disable completely.
|
|
487
|
+
|
|
488
|
+
### Session search
|
|
489
|
+
|
|
490
|
+
Conversations with Claude, Codex, Gemini, and other agents scatter across their native storage. Session search indexes them locally so you can find any conversation:
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
agents sessions "auth middleware" # Full-text search across all agents
|
|
494
|
+
agents sessions --agent claude --since 7d
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
The index lives at `~/.agents/sessions/sessions.db` (SQLite + FTS5). Nothing leaves your machine. See [Sessions](#sessions-across-agents) for full usage.
|
|
498
|
+
|
|
499
|
+
### Secrets
|
|
500
|
+
|
|
501
|
+
API keys and credentials are stored in macOS Keychain, never in plaintext files. Bundle definitions also live in Keychain.
|
|
502
|
+
|
|
503
|
+
```bash
|
|
504
|
+
agents secrets create my-keys
|
|
505
|
+
agents secrets add my-keys API_KEY # Prompts for value, stores in Keychain
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
With `--icloud-sync`, secrets sync via iCloud Keychain to your other Macs. Without it, they stay device-local. See [Secrets](#secrets) for full usage.
|
|
509
|
+
|
|
510
|
+
### Summary
|
|
511
|
+
|
|
512
|
+
| Data | Location | Who can read | Opt out |
|
|
513
|
+
|------|----------|--------------|---------|
|
|
514
|
+
| Event log | `~/.agents/logs/` | You only (0600) | `AGENTS_DISABLE_EVENT_LOG=1` |
|
|
515
|
+
| Session index | `~/.agents/sessions/` | You only | Delete the directory |
|
|
516
|
+
| Secrets | macOS Keychain | You + apps you authorize | Don't use `agents secrets` |
|
|
517
|
+
| Config | `~/.agents/` | You only | N/A |
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
456
521
|
## Compatibility
|
|
457
522
|
|
|
458
523
|
| Agent | Versions | MCP | Commands | Skills | Rules | Hooks | Plugins | Permissions | Routines | Teams |
|
|
@@ -505,7 +570,9 @@ Your choice. We hand off to the original CLI process — use your existing subsc
|
|
|
505
570
|
|
|
506
571
|
### Does it store my API keys or send telemetry?
|
|
507
572
|
|
|
508
|
-
No. API keys come from your shell environment or each agent CLI's existing auth.
|
|
573
|
+
**No telemetry, no phone-home.** Everything stays local. API keys come from your shell environment or each agent CLI's existing auth.
|
|
574
|
+
|
|
575
|
+
For full transparency: `agents-cli` keeps a local event log at `~/.agents/logs/` so you can see exactly what agents did on your machine. Logs are owner-readable only (0600) and auto-prune after 30 days. Set `AGENTS_DISABLE_EVENT_LOG=1` to disable. See [Security & Privacy](#security--privacy) for details.
|
|
509
576
|
|
|
510
577
|
### Which platforms?
|
|
511
578
|
|
package/dist/commands/beta.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { ALL_BETA_FEATURES, getBetaConfigLocation, getEnabledBetaFeatures, setBetaEnabled, } from '../lib/beta.js';
|
|
3
|
+
const BETA_DESCRIPTIONS = {
|
|
4
|
+
drive: 'Google Drive integration for reading and writing files',
|
|
5
|
+
factory: 'Cloud-based agent dispatch via Rush Factory',
|
|
6
|
+
};
|
|
3
7
|
function parseFeatures(values) {
|
|
4
8
|
const valid = new Set(ALL_BETA_FEATURES);
|
|
5
9
|
const invalid = values.filter((value) => !valid.has(value));
|
|
@@ -29,7 +33,8 @@ Examples:
|
|
|
29
33
|
console.log(chalk.bold('Beta Features'));
|
|
30
34
|
for (const feature of ALL_BETA_FEATURES) {
|
|
31
35
|
const state = enabled.has(feature) ? chalk.green('enabled') : chalk.gray('disabled');
|
|
32
|
-
|
|
36
|
+
const desc = BETA_DESCRIPTIONS[feature] || '';
|
|
37
|
+
console.log(` ${feature.padEnd(10)} ${state.padEnd(18)} ${chalk.dim(desc)}`);
|
|
33
38
|
}
|
|
34
39
|
console.log('');
|
|
35
40
|
console.log(chalk.gray(`Config: ${location.path}`));
|
package/dist/commands/exec.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { buildExecCommand, parseExecEnv, execAgent, runWithFallback, AGENT_COMMANDS, } from '../lib/exec.js';
|
|
10
10
|
import { profileExists, resolveProfileForRun } from '../lib/profiles.js';
|
|
11
|
-
import { readBundle, resolveBundleEnv } from '../lib/secrets/bundles.js';
|
|
11
|
+
import { readBundle, resolveBundleEnv, describeBundle } from '../lib/secrets/bundles.js';
|
|
12
12
|
import { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES, } from '../lib/rotate.js';
|
|
13
13
|
import { resolveVersionAlias } from '../lib/versions.js';
|
|
14
14
|
const VALID_AGENTS = Object.keys(AGENT_COMMANDS);
|
|
@@ -36,7 +36,7 @@ export function registerRunCommand(program) {
|
|
|
36
36
|
.option('--cwd <dir>', 'Working directory for the agent (defaults to current directory)')
|
|
37
37
|
.option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude only, repeatable)', (val, prev) => [...prev, val], [])
|
|
38
38
|
.option('--json', 'Stream events as JSON lines (for parsing by other tools)')
|
|
39
|
-
.option('--headless', 'Non-interactive mode (
|
|
39
|
+
.option('--headless', 'Non-interactive mode (auto-enabled when prompt provided)', false)
|
|
40
40
|
.option('-i, --interactive', 'Force interactive mode even when a prompt is provided')
|
|
41
41
|
.option('--session-id <id>', 'Resume a previous conversation (Claude only)')
|
|
42
42
|
.option('--verbose', 'Show detailed execution logs')
|
|
@@ -197,6 +197,13 @@ Examples:
|
|
|
197
197
|
for (const bundleName of options.secrets) {
|
|
198
198
|
try {
|
|
199
199
|
const bundle = readBundle(bundleName);
|
|
200
|
+
const entries = describeBundle(bundle);
|
|
201
|
+
const counts = {};
|
|
202
|
+
for (const e of entries) {
|
|
203
|
+
counts[e.kind] = (counts[e.kind] || 0) + 1;
|
|
204
|
+
}
|
|
205
|
+
const breakdown = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(', ');
|
|
206
|
+
console.log(chalk.gray(`[secrets] Resolved ${bundleName}: ${entries.length} keys (${breakdown})`));
|
|
200
207
|
secretsEnv = { ...secretsEnv, ...resolveBundleEnv(bundle) };
|
|
201
208
|
}
|
|
202
209
|
catch (err) {
|
package/dist/commands/init.js
CHANGED
|
@@ -82,6 +82,16 @@ export async function runInit(program, options = {}) {
|
|
|
82
82
|
spinner.succeed(`Updated to ${result.commit}`);
|
|
83
83
|
}
|
|
84
84
|
else {
|
|
85
|
+
// Check git is available
|
|
86
|
+
try {
|
|
87
|
+
const { execSync } = await import('child_process');
|
|
88
|
+
execSync('which git', { stdio: 'ignore' });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
spinner.fail('git is not installed');
|
|
92
|
+
console.log(chalk.gray('Install git first: https://git-scm.com/downloads'));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
85
95
|
const result = await cloneIntoExisting(DEFAULT_SYSTEM_REPO, agentsDir);
|
|
86
96
|
if (!result.success) {
|
|
87
97
|
spinner.fail(`Clone failed: ${result.error}`);
|
package/dist/commands/mcp.js
CHANGED
|
@@ -6,7 +6,7 @@ import { readManifest, writeManifest, createDefaultManifest } from '../lib/manif
|
|
|
6
6
|
import { listMcpServerConfigs } from '../lib/mcp.js';
|
|
7
7
|
import { getMcpDir } from '../lib/state.js';
|
|
8
8
|
import { getEffectiveHome, getGlobalDefault, listInstalledVersions, getVersionHomePath, resolveInstalledAgentTargets, resolveConfiguredAgentTargets, resolveVersionAlias, } from '../lib/versions.js';
|
|
9
|
-
import {
|
|
9
|
+
import { getUserAgentsDir } from '../lib/state.js';
|
|
10
10
|
import { isPromptCancelled, isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
|
|
11
11
|
import { showResourceList, buildTargetsSection, } from './resource-view.js';
|
|
12
12
|
/** Parse a comma-separated --agents string into validated agent IDs and optional version targets. */
|
|
@@ -163,7 +163,7 @@ Examples:
|
|
|
163
163
|
console.log(chalk.gray('HTTP: agents mcp add <name> <url> --transport http'));
|
|
164
164
|
process.exit(1);
|
|
165
165
|
}
|
|
166
|
-
const localPath =
|
|
166
|
+
const localPath = getUserAgentsDir();
|
|
167
167
|
const manifest = readManifest(localPath) || createDefaultManifest();
|
|
168
168
|
manifest.mcp = manifest.mcp || {};
|
|
169
169
|
const targetConfig = parseMcpAgentTargets(options.agents);
|
|
@@ -400,7 +400,7 @@ Examples:
|
|
|
400
400
|
agents mcp register --agents codex@0.116.0
|
|
401
401
|
`)
|
|
402
402
|
.action(async (name, options) => {
|
|
403
|
-
const localPath =
|
|
403
|
+
const localPath = getUserAgentsDir();
|
|
404
404
|
const manifest = readManifest(localPath);
|
|
405
405
|
if (!manifest?.mcp) {
|
|
406
406
|
console.log(chalk.yellow('No MCP servers in manifest'));
|
|
@@ -467,7 +467,7 @@ function buildMcpRows(opts) {
|
|
|
467
467
|
const centralServers = new Map();
|
|
468
468
|
for (const s of listMcpServerConfigs())
|
|
469
469
|
centralServers.set(s.name, s);
|
|
470
|
-
const manifest = readManifest(
|
|
470
|
+
const manifest = readManifest(getUserAgentsDir());
|
|
471
471
|
const manifestEntries = manifest?.mcp || {};
|
|
472
472
|
const targetPairs = iterMcpCapableVersions({
|
|
473
473
|
agent: opts.filterAgent,
|
package/dist/commands/prune.d.ts
CHANGED
|
@@ -1,22 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Top-level `agents prune` — destructive cleanup across the install.
|
|
3
|
-
*
|
|
4
|
-
* Two kinds of cleanup, one verb:
|
|
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 (the older copy
|
|
10
|
-
* is redundant; the newer one is what's signed in and active).
|
|
11
|
-
*
|
|
12
|
-
* Sync (additive: copy missing/changed files into version homes) is no longer
|
|
13
|
-
* a user-facing verb — `syncResourcesToVersion` runs at agent launch and
|
|
14
|
-
* applies adds/updates automatically. Pruning, however, is destructive, so it
|
|
15
|
-
* stays explicit.
|
|
16
|
-
*
|
|
17
|
-
* Default scope: each agent's currently-pinned default version for orphan
|
|
18
|
-
* cleanup, plus the standard cross-agent version-dedup pass. Pass `--all`
|
|
19
|
-
* to widen orphan cleanup to every installed version.
|
|
20
|
-
*/
|
|
21
1
|
import type { Command } from 'commander';
|
|
22
2
|
export declare function registerPruneCommand(program: Command): void;
|
package/dist/commands/prune.js
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
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';
|
|
@@ -7,8 +31,12 @@ import { getGlobalDefault } from '../lib/versions.js';
|
|
|
7
31
|
import { resolveAgentName, formatAgentError } from '../lib/agents.js';
|
|
8
32
|
import { pruneDuplicates } from './view.js';
|
|
9
33
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
34
|
+
import { getTrashDir } from '../lib/state.js';
|
|
35
|
+
import { countSessionsOlderThan, deleteSessionsOlderThan } from '../lib/session/db.js';
|
|
36
|
+
import { previewRunsPrune, pruneRuns, countAllRuns } from '../lib/routines.js';
|
|
10
37
|
const RESOURCE_TYPES = ['commands', 'skills', 'hooks'];
|
|
11
|
-
const
|
|
38
|
+
const STATE_TYPES = ['trash', 'sessions', 'runs'];
|
|
39
|
+
const ALL_TYPES = [...RESOURCE_TYPES, 'versions', ...STATE_TYPES];
|
|
12
40
|
function scopePairs(pairs, all) {
|
|
13
41
|
if (all)
|
|
14
42
|
return pairs;
|
|
@@ -59,6 +87,9 @@ function parseTarget(arg) {
|
|
|
59
87
|
if (RESOURCE_TYPES.includes(arg)) {
|
|
60
88
|
return { resourceTypes: [arg], includeVersions: false };
|
|
61
89
|
}
|
|
90
|
+
if (STATE_TYPES.includes(arg)) {
|
|
91
|
+
return { resourceTypes: [], includeVersions: false, stateType: arg };
|
|
92
|
+
}
|
|
62
93
|
if (arg === 'versions') {
|
|
63
94
|
return { resourceTypes: [], includeVersions: true };
|
|
64
95
|
}
|
|
@@ -72,6 +103,202 @@ function parseTarget(arg) {
|
|
|
72
103
|
console.log(chalk.gray(formatAgentError(arg)));
|
|
73
104
|
process.exit(1);
|
|
74
105
|
}
|
|
106
|
+
function parseDays(value, defaultDays) {
|
|
107
|
+
const match = value.match(/^(\d+)d?$/);
|
|
108
|
+
if (match)
|
|
109
|
+
return parseInt(match[1], 10);
|
|
110
|
+
return defaultDays;
|
|
111
|
+
}
|
|
112
|
+
function formatBytes(bytes) {
|
|
113
|
+
if (bytes < 1024)
|
|
114
|
+
return `${bytes} B`;
|
|
115
|
+
if (bytes < 1024 * 1024)
|
|
116
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
117
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
118
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
119
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
120
|
+
}
|
|
121
|
+
function getDirSize(dirPath) {
|
|
122
|
+
if (!fs.existsSync(dirPath))
|
|
123
|
+
return 0;
|
|
124
|
+
let size = 0;
|
|
125
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
size += getDirSize(fullPath);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
try {
|
|
133
|
+
size += fs.statSync(fullPath).size;
|
|
134
|
+
}
|
|
135
|
+
catch { /* ignore */ }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return size;
|
|
139
|
+
}
|
|
140
|
+
async function runTrashPrune(options) {
|
|
141
|
+
const trashDir = getTrashDir();
|
|
142
|
+
if (!fs.existsSync(trashDir)) {
|
|
143
|
+
console.log(chalk.green('Trash is empty.'));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const days = parseDays(options.olderThan || '30d', 30);
|
|
147
|
+
const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
148
|
+
const toPrune = [];
|
|
149
|
+
function scanDir(dir) {
|
|
150
|
+
if (!fs.existsSync(dir))
|
|
151
|
+
return;
|
|
152
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
153
|
+
const fullPath = path.join(dir, entry.name);
|
|
154
|
+
try {
|
|
155
|
+
const stat = fs.statSync(fullPath);
|
|
156
|
+
if (stat.mtimeMs < cutoffMs) {
|
|
157
|
+
toPrune.push({ path: fullPath, mtime: stat.mtimeMs, size: entry.isDirectory() ? getDirSize(fullPath) : stat.size });
|
|
158
|
+
}
|
|
159
|
+
else if (entry.isDirectory()) {
|
|
160
|
+
scanDir(fullPath);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch { /* skip inaccessible */ }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
scanDir(trashDir);
|
|
167
|
+
if (toPrune.length === 0) {
|
|
168
|
+
console.log(chalk.green(`No trash entries older than ${days} days.`));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const totalSize = toPrune.reduce((sum, e) => sum + e.size, 0);
|
|
172
|
+
console.log(chalk.bold(`Trash entries older than ${days} days\n`));
|
|
173
|
+
for (const entry of toPrune.slice(0, 20)) {
|
|
174
|
+
const age = Math.floor((Date.now() - entry.mtime) / (24 * 60 * 60 * 1000));
|
|
175
|
+
console.log(` ${chalk.gray(`${age}d ago`)} ${path.relative(trashDir, entry.path)}`);
|
|
176
|
+
}
|
|
177
|
+
if (toPrune.length > 20) {
|
|
178
|
+
console.log(chalk.gray(` ... and ${toPrune.length - 20} more`));
|
|
179
|
+
}
|
|
180
|
+
console.log();
|
|
181
|
+
if (options.dryRun) {
|
|
182
|
+
console.log(chalk.gray(`${toPrune.length} entries (${formatBytes(totalSize)}). Run without --dry-run to delete.`));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (!options.yes) {
|
|
186
|
+
if (!isInteractiveTerminal()) {
|
|
187
|
+
console.log(chalk.yellow('Non-interactive shell: pass -y to confirm, or --dry-run to preview.'));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
let ok = false;
|
|
191
|
+
try {
|
|
192
|
+
ok = await confirm({ message: `Delete ${toPrune.length} entries (${formatBytes(totalSize)})?`, default: false });
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
if (isPromptCancelled(err)) {
|
|
196
|
+
console.log(chalk.gray('Cancelled'));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
if (!ok) {
|
|
202
|
+
console.log(chalk.gray('Cancelled'));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
let deleted = 0;
|
|
207
|
+
for (const entry of toPrune) {
|
|
208
|
+
try {
|
|
209
|
+
fs.rmSync(entry.path, { recursive: true, force: true });
|
|
210
|
+
deleted++;
|
|
211
|
+
}
|
|
212
|
+
catch { /* ignore */ }
|
|
213
|
+
}
|
|
214
|
+
console.log(chalk.green(`Pruned ${deleted} trash entries (${formatBytes(totalSize)}).`));
|
|
215
|
+
}
|
|
216
|
+
async function runSessionsPrune(options) {
|
|
217
|
+
const days = parseDays(options.olderThan || '90d', 90);
|
|
218
|
+
const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
219
|
+
const count = countSessionsOlderThan(cutoffMs);
|
|
220
|
+
if (count === 0) {
|
|
221
|
+
console.log(chalk.green(`No sessions older than ${days} days.`));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
console.log(chalk.bold(`Sessions older than ${days} days: ${count}\n`));
|
|
225
|
+
if (options.dryRun) {
|
|
226
|
+
console.log(chalk.gray(`${count} session(s). Run without --dry-run to delete.`));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!options.yes) {
|
|
230
|
+
if (!isInteractiveTerminal()) {
|
|
231
|
+
console.log(chalk.yellow('Non-interactive shell: pass -y to confirm, or --dry-run to preview.'));
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
let ok = false;
|
|
235
|
+
try {
|
|
236
|
+
ok = await confirm({ message: `Delete ${count} session records?`, default: false });
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
if (isPromptCancelled(err)) {
|
|
240
|
+
console.log(chalk.gray('Cancelled'));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
if (!ok) {
|
|
246
|
+
console.log(chalk.gray('Cancelled'));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const deleted = deleteSessionsOlderThan(cutoffMs);
|
|
251
|
+
console.log(chalk.green(`Pruned ${deleted} session records.`));
|
|
252
|
+
}
|
|
253
|
+
async function runRunsPrune(options) {
|
|
254
|
+
const keep = options.keep ? parseInt(options.keep, 10) : 10;
|
|
255
|
+
if (isNaN(keep) || keep < 0) {
|
|
256
|
+
console.log(chalk.red('--keep must be a non-negative integer'));
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
const preview = previewRunsPrune(keep);
|
|
260
|
+
const total = countAllRuns();
|
|
261
|
+
if (preview.length === 0) {
|
|
262
|
+
console.log(chalk.green(`All jobs have ${keep} or fewer runs. Nothing to prune.`));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
console.log(chalk.bold(`Routine runs to prune (keeping last ${keep} per job)\n`));
|
|
266
|
+
const byJob = new Map();
|
|
267
|
+
for (const run of preview) {
|
|
268
|
+
byJob.set(run.jobName, (byJob.get(run.jobName) || 0) + 1);
|
|
269
|
+
}
|
|
270
|
+
for (const [job, count] of byJob) {
|
|
271
|
+
console.log(` ${chalk.cyan(job)}: ${count} old runs`);
|
|
272
|
+
}
|
|
273
|
+
console.log();
|
|
274
|
+
if (options.dryRun) {
|
|
275
|
+
console.log(chalk.gray(`${preview.length} of ${total} runs would be deleted. Run without --dry-run to delete.`));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (!options.yes) {
|
|
279
|
+
if (!isInteractiveTerminal()) {
|
|
280
|
+
console.log(chalk.yellow('Non-interactive shell: pass -y to confirm, or --dry-run to preview.'));
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
let ok = false;
|
|
284
|
+
try {
|
|
285
|
+
ok = await confirm({ message: `Delete ${preview.length} old runs?`, default: false });
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
if (isPromptCancelled(err)) {
|
|
289
|
+
console.log(chalk.gray('Cancelled'));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
throw err;
|
|
293
|
+
}
|
|
294
|
+
if (!ok) {
|
|
295
|
+
console.log(chalk.gray('Cancelled'));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const { deleted, bytesFreed } = pruneRuns(keep);
|
|
300
|
+
console.log(chalk.green(`Pruned ${deleted} runs (${formatBytes(bytesFreed)}).`));
|
|
301
|
+
}
|
|
75
302
|
async function runOrphanPrune(resourceTypes, options) {
|
|
76
303
|
const groups = collectOrphans(resourceTypes, options.all === true);
|
|
77
304
|
if (groups.length === 0) {
|
|
@@ -133,10 +360,12 @@ async function runOrphanPrune(resourceTypes, options) {
|
|
|
133
360
|
export function registerPruneCommand(program) {
|
|
134
361
|
program
|
|
135
362
|
.command('prune [target]')
|
|
136
|
-
.description('Remove orphan resources
|
|
363
|
+
.description('Remove orphan resources, old versions, trash, sessions, or routine runs')
|
|
137
364
|
.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')
|
|
365
|
+
.option('--dry-run', 'Show what would be removed without deleting (default for state targets)')
|
|
139
366
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
367
|
+
.option('--older-than <days>', 'For trash/sessions: delete entries older than N days (default: 30d for trash, 90d for sessions)')
|
|
368
|
+
.option('--keep <n>', 'For runs: keep the last N runs per job (default: 10)')
|
|
140
369
|
.addHelpText('after', `
|
|
141
370
|
Targets:
|
|
142
371
|
(none) Orphans across commands, skills, hooks + duplicate versions
|
|
@@ -145,6 +374,9 @@ Targets:
|
|
|
145
374
|
hooks Orphan hook scripts only
|
|
146
375
|
versions Older duplicate version installs only
|
|
147
376
|
<agent> Older duplicate versions for one agent (e.g. 'claude')
|
|
377
|
+
trash Soft-deleted resources older than --older-than days (default 30)
|
|
378
|
+
sessions Session records in sessions.db older than --older-than days (default 90)
|
|
379
|
+
runs Routine execution logs, keeping only --keep per job (default 10)
|
|
148
380
|
|
|
149
381
|
Examples:
|
|
150
382
|
# Full sweep: orphan resources + duplicate versions for current defaults
|
|
@@ -156,14 +388,26 @@ Examples:
|
|
|
156
388
|
# Just version dedup
|
|
157
389
|
agents prune versions
|
|
158
390
|
|
|
159
|
-
# Just version dedup for one agent
|
|
160
|
-
agents prune claude
|
|
161
|
-
|
|
162
391
|
# Sweep every installed version's orphans, not only the defaults
|
|
163
392
|
agents prune --all
|
|
164
393
|
|
|
165
|
-
# Preview
|
|
166
|
-
agents prune --dry-run
|
|
394
|
+
# Preview trash entries older than 30 days
|
|
395
|
+
agents prune trash --dry-run
|
|
396
|
+
|
|
397
|
+
# Delete trash entries older than 60 days
|
|
398
|
+
agents prune trash --older-than 60 -y
|
|
399
|
+
|
|
400
|
+
# Preview session cleanup (90+ days old)
|
|
401
|
+
agents prune sessions --dry-run
|
|
402
|
+
|
|
403
|
+
# Delete sessions older than 180 days
|
|
404
|
+
agents prune sessions --older-than 180 -y
|
|
405
|
+
|
|
406
|
+
# Preview runs cleanup (keeping last 10)
|
|
407
|
+
agents prune runs --dry-run
|
|
408
|
+
|
|
409
|
+
# Keep only the last 5 runs per job
|
|
410
|
+
agents prune runs --keep 5 -y
|
|
167
411
|
|
|
168
412
|
What's an orphan?
|
|
169
413
|
A command, skill, or hook present inside a version home but missing from every
|
|
@@ -171,18 +415,27 @@ What's an orphan?
|
|
|
171
415
|
repos). Usually leftovers from a resource that was deleted or moved but never
|
|
172
416
|
reconciled into the version install.
|
|
173
417
|
|
|
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
418
|
Soft-delete:
|
|
179
419
|
Version directories are NEVER hard-deleted. \`prune\` moves them to
|
|
180
|
-
~/.agents
|
|
181
|
-
|
|
182
|
-
The trash never auto-expires; \`rm -rf\` it manually when you're sure.
|
|
420
|
+
~/.agents/.trash/versions/<agent>/<version>/<timestamp>/. Use
|
|
421
|
+
\`agents prune trash\` to expire old trash entries.
|
|
183
422
|
`)
|
|
184
423
|
.action(async (target, options) => {
|
|
185
424
|
const parsed = parseTarget(target);
|
|
425
|
+
if (parsed.stateType) {
|
|
426
|
+
switch (parsed.stateType) {
|
|
427
|
+
case 'trash':
|
|
428
|
+
await runTrashPrune(options);
|
|
429
|
+
break;
|
|
430
|
+
case 'sessions':
|
|
431
|
+
await runSessionsPrune(options);
|
|
432
|
+
break;
|
|
433
|
+
case 'runs':
|
|
434
|
+
await runRunsPrune(options);
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
186
439
|
if (parsed.resourceTypes.length > 0) {
|
|
187
440
|
await runOrphanPrune(parsed.resourceTypes, options);
|
|
188
441
|
if (parsed.includeVersions)
|
package/dist/commands/teams.js
CHANGED
|
@@ -922,13 +922,12 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
922
922
|
.command('status [team]')
|
|
923
923
|
.aliases(['s', 'st', 'check'])
|
|
924
924
|
.description("Check in on a team: who's working, what files they touched, recent commands, last output. Pass --since for efficient delta polling.")
|
|
925
|
-
.option('-f, --filter <state>', 'Show only teammates in this state:
|
|
925
|
+
.option('-f, --filter <state>', 'Show only teammates in this state: running, completed, failed, stopped, or all (default: all)', 'all')
|
|
926
926
|
.option('-s, --since <iso>', 'Cursor from a previous status call; only show updates after this timestamp (enables efficient polling)')
|
|
927
927
|
.option('--agent-id <id>', 'Show only this one teammate (by UUID or UUID prefix)')
|
|
928
928
|
.option('--json', 'Output machine-readable JSON')
|
|
929
929
|
.action(async (team, opts) => {
|
|
930
|
-
|
|
931
|
-
const filter = opts.filter === 'working' ? 'running' : opts.filter;
|
|
930
|
+
const filter = opts.filter;
|
|
932
931
|
const mgr = mkManager();
|
|
933
932
|
// No team given → drop into the picker (TTY) or fail clearly (script).
|
|
934
933
|
if (!team) {
|
package/dist/commands/usage.js
CHANGED
|
@@ -8,6 +8,12 @@ export function registerUsageCommand(program) {
|
|
|
8
8
|
program
|
|
9
9
|
.command('usage [agent]')
|
|
10
10
|
.description('Show rate-limit / quota usage per agent')
|
|
11
|
+
.addHelpText('after', `
|
|
12
|
+
Examples:
|
|
13
|
+
agents usage Show usage for all installed agents
|
|
14
|
+
agents usage claude Show usage for Claude only
|
|
15
|
+
agents usage codex Show usage for Codex only
|
|
16
|
+
`)
|
|
11
17
|
.action(async (agentFilter) => {
|
|
12
18
|
const filter = agentFilter;
|
|
13
19
|
const targets = filter
|
|
@@ -104,7 +104,7 @@ When to use:
|
|
|
104
104
|
- Multi-account: install different versions for different accounts (each version has its own auth)
|
|
105
105
|
- Project-specific: lock a version for a repo with --project
|
|
106
106
|
|
|
107
|
-
Note: The first version you install
|
|
107
|
+
Note: The first version you install becomes the default automatically.
|
|
108
108
|
`)
|
|
109
109
|
.action(async (specs, options) => {
|
|
110
110
|
const isProject = options.project;
|
|
@@ -220,10 +220,14 @@ Note: The first version you install is NOT set as default automatically. Run 'ag
|
|
|
220
220
|
console.log(chalk.green(` Synced: ${synced.join(', ')}`));
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
|
-
//
|
|
223
|
+
// Set as default: auto-set if no default exists, otherwise prompt
|
|
224
224
|
const currentDefault = getGlobalDefault(agent);
|
|
225
225
|
if (currentDefault !== installedVersion) {
|
|
226
|
-
if (
|
|
226
|
+
if (!currentDefault) {
|
|
227
|
+
// First install for this agent - auto-set without prompting
|
|
228
|
+
await setDefaultVersion(agent, installedVersion);
|
|
229
|
+
}
|
|
230
|
+
else if (skipPrompts) {
|
|
227
231
|
await setDefaultVersion(agent, installedVersion);
|
|
228
232
|
}
|
|
229
233
|
else {
|
|
@@ -238,9 +242,7 @@ Note: The first version you install is NOT set as default automatically. Run 'ag
|
|
|
238
242
|
info,
|
|
239
243
|
});
|
|
240
244
|
const accountHint = formatAccountHint(info, usage.snapshot);
|
|
241
|
-
const message = currentDefault
|
|
242
|
-
? `Switch default from ${agentLabel(agentConfig.id)}@${currentDefault} to ${agentLabel(agentConfig.id)}@${installedVersion}${accountHint}?`
|
|
243
|
-
: `Set ${agentLabel(agentConfig.id)}@${installedVersion}${accountHint} as default?`;
|
|
245
|
+
const message = `Switch default from ${agentLabel(agentConfig.id)}@${currentDefault} to ${agentLabel(agentConfig.id)}@${installedVersion}${accountHint}?`;
|
|
244
246
|
const setAsDefault = await confirm({
|
|
245
247
|
message,
|
|
246
248
|
default: true,
|
|
@@ -7,3 +7,4 @@ export interface SSHConnection {
|
|
|
7
7
|
cleanup: () => void;
|
|
8
8
|
}
|
|
9
9
|
export declare function connectSSH(endpoint: string, profile: BrowserProfile): Promise<SSHConnection>;
|
|
10
|
+
export declare function restartRemoteBrowser(user: string, host: string, browserType: string, port: number, customBinary?: string): Promise<void>;
|