@phnx-labs/agents-cli 1.14.1 → 1.14.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.
- package/README.md +31 -3
- package/dist/commands/browser.d.ts +2 -0
- package/dist/commands/browser.js +388 -0
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +16 -9
- package/dist/commands/doctor.js +248 -12
- package/dist/commands/exec.js +17 -17
- package/dist/commands/prune.js +9 -3
- package/dist/commands/refresh-rules.d.ts +15 -0
- package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
- package/dist/commands/routines.js +1 -1
- package/dist/commands/rules.js +100 -4
- package/dist/commands/secrets.js +206 -12
- package/dist/commands/sync.js +19 -0
- package/dist/commands/teams.js +162 -22
- package/dist/commands/trash.d.ts +10 -0
- package/dist/commands/trash.js +187 -0
- package/dist/commands/view.js +46 -13
- package/dist/index.js +62 -4
- package/dist/lib/agents.js +2 -2
- package/dist/lib/browser/cdp.d.ts +24 -0
- package/dist/lib/browser/cdp.js +94 -0
- package/dist/lib/browser/chrome.d.ts +16 -0
- package/dist/lib/browser/chrome.js +157 -0
- package/dist/lib/browser/drivers/local.d.ts +8 -0
- package/dist/lib/browser/drivers/local.js +22 -0
- package/dist/lib/browser/drivers/ssh.d.ts +9 -0
- package/dist/lib/browser/drivers/ssh.js +129 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.js +5 -0
- package/dist/lib/browser/input.d.ts +6 -0
- package/dist/lib/browser/input.js +52 -0
- package/dist/lib/browser/ipc.d.ts +12 -0
- package/dist/lib/browser/ipc.js +223 -0
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +61 -0
- package/dist/lib/browser/refs.d.ts +21 -0
- package/dist/lib/browser/refs.js +88 -0
- package/dist/lib/browser/service.d.ts +45 -0
- package/dist/lib/browser/service.js +404 -0
- package/dist/lib/browser/types.d.ts +73 -0
- package/dist/lib/browser/types.js +7 -0
- package/dist/lib/cloud/codex.js +1 -1
- package/dist/lib/cloud/registry.js +2 -2
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/daemon.d.ts +1 -1
- package/dist/lib/daemon.js +47 -11
- package/dist/lib/diff-text.d.ts +25 -0
- package/dist/lib/diff-text.js +47 -0
- package/dist/lib/doctor-diff.d.ts +64 -0
- package/dist/lib/doctor-diff.js +497 -0
- package/dist/lib/git.js +3 -3
- package/dist/lib/hooks.d.ts +6 -0
- package/dist/lib/hooks.js +6 -1
- package/dist/lib/migrate.js +77 -0
- package/dist/lib/pty-client.js +3 -3
- package/dist/lib/pty-server.js +36 -7
- package/dist/lib/resources.js +1 -1
- package/dist/lib/rotate.d.ts +43 -26
- package/dist/lib/rotate.js +99 -44
- package/dist/lib/rules/compile.d.ts +104 -0
- package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
- package/dist/lib/rules/compose.d.ts +78 -0
- package/dist/lib/rules/compose.js +170 -0
- package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
- package/dist/lib/{memory.js → rules/rules.js} +10 -10
- package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/dist/lib/secrets/bundles.d.ts +61 -4
- package/dist/lib/secrets/bundles.js +222 -54
- package/dist/lib/secrets/index.d.ts +24 -5
- package/dist/lib/secrets/index.js +70 -41
- package/dist/lib/session/active.js +5 -5
- package/dist/lib/session/db.js +4 -4
- package/dist/lib/session/discover.js +2 -2
- package/dist/lib/session/render.js +21 -7
- package/dist/lib/shims.d.ts +28 -4
- package/dist/lib/shims.js +72 -14
- package/dist/lib/state.d.ts +22 -28
- package/dist/lib/state.js +83 -76
- package/dist/lib/sync-manifest.d.ts +2 -2
- package/dist/lib/sync-manifest.js +5 -5
- package/dist/lib/teams/agents.d.ts +4 -2
- package/dist/lib/teams/agents.js +11 -4
- package/dist/lib/teams/api.d.ts +1 -1
- package/dist/lib/teams/api.js +2 -2
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/persistence.js +3 -3
- package/dist/lib/teams/registry.d.ts +8 -1
- package/dist/lib/teams/registry.js +8 -2
- package/dist/lib/teams/worktree.d.ts +30 -0
- package/dist/lib/teams/worktree.js +96 -0
- package/dist/lib/types.d.ts +13 -7
- package/dist/lib/types.js +3 -3
- package/dist/lib/versions.d.ts +30 -2
- package/dist/lib/versions.js +127 -105
- package/package.json +1 -1
- package/scripts/postinstall.js +29 -0
- package/dist/commands/refresh-memory.d.ts +0 -15
- package/dist/lib/memory-compile.d.ts +0 -66
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* macOS Keychain integration for secure credential storage.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* All reads/writes go through a signed Swift helper (keychain-helper.swift)
|
|
5
|
+
* compiled into AgentsKeychain.app. The .app embeds a provisioning profile
|
|
6
|
+
* that grants the application-identifier + keychain-access-groups entitlement
|
|
7
|
+
* macOS requires for kSecAttrSynchronizable writes (iCloud Keychain).
|
|
8
|
+
* For device-local writes the helper is invoked with the `nosync` arg.
|
|
7
9
|
*/
|
|
8
10
|
import { fileURLToPath } from 'url';
|
|
9
11
|
import { execFileSync, spawnSync } from 'child_process';
|
|
@@ -12,7 +14,7 @@ import * as os from 'os';
|
|
|
12
14
|
import * as path from 'path';
|
|
13
15
|
const SERVICE_PREFIX = 'agents-cli';
|
|
14
16
|
const REF_PATTERN = /^(keychain|env|file|exec):(.+)$/s;
|
|
15
|
-
/** Parse a bundle
|
|
17
|
+
/** Parse a bundle value into either a literal string or a typed secret ref. */
|
|
16
18
|
export function parseBundleValue(raw) {
|
|
17
19
|
if (typeof raw === 'object' && raw !== null && typeof raw.value === 'string') {
|
|
18
20
|
return { literal: raw.value };
|
|
@@ -53,58 +55,64 @@ function ensureKeychainHelper() {
|
|
|
53
55
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
54
56
|
const binPath = path.join(here, 'AgentsKeychain.app', 'Contents', 'MacOS', 'AgentsKeychain');
|
|
55
57
|
if (!fs.existsSync(binPath)) {
|
|
56
|
-
throw new Error(`
|
|
57
|
-
'This npm package was built without the signed helper bundle. '
|
|
58
|
-
'Reinstall agents-cli, or create the bundle without --icloud-sync to use device-local storage.');
|
|
58
|
+
throw new Error(`Keychain helper missing at ${binPath}. ` +
|
|
59
|
+
'This npm package was built without the signed helper bundle. Reinstall agents-cli.');
|
|
59
60
|
}
|
|
60
61
|
return binPath;
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
let backend = null;
|
|
64
|
+
/** Install a custom keychain backend (test only). Returns the previous backend so callers can restore. */
|
|
65
|
+
export function setKeychainBackendForTest(b) {
|
|
66
|
+
const prev = backend;
|
|
67
|
+
backend = b;
|
|
68
|
+
return prev;
|
|
69
|
+
}
|
|
70
|
+
// Backend routing: non-sync items go through /usr/bin/security so they share
|
|
71
|
+
// an ACL identity with items created by previous CLI versions (no prompts on
|
|
72
|
+
// existing data). Sync items must go through the signed .app — only the .app
|
|
73
|
+
// holds the keychain-access-groups entitlement macOS requires for
|
|
74
|
+
// kSecAttrSynchronizable. Enumeration also goes through the .app because the
|
|
75
|
+
// security CLI doesn't expose listing by service prefix.
|
|
66
76
|
/** Check if a keychain item exists (macOS only). */
|
|
67
77
|
export function hasKeychainToken(item, sync = false) {
|
|
78
|
+
if (backend)
|
|
79
|
+
return backend.has(item, sync);
|
|
68
80
|
assertMacOS();
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
// Try security first (no prompts for local items), fall back to binary for synced items.
|
|
82
|
+
if (spawnSync('security', ['find-generic-password', '-a', os.userInfo().username, '-s', item, '-w'], {
|
|
83
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
84
|
+
}).status === 0)
|
|
85
|
+
return true;
|
|
86
|
+
// Fallback: binary searches both synced and non-synced via kSecAttrSynchronizableAny
|
|
87
|
+
const bin = ensureKeychainHelper();
|
|
88
|
+
return spawnSync(bin, ['has', item, os.userInfo().username], {
|
|
76
89
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
77
90
|
}).status === 0;
|
|
78
91
|
}
|
|
79
92
|
/** Retrieve a secret value from the macOS Keychain. Throws if not found. */
|
|
80
93
|
export function getKeychainToken(item, sync = false) {
|
|
94
|
+
if (backend)
|
|
95
|
+
return backend.get(item, sync);
|
|
81
96
|
assertMacOS();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const msg = result.stderr?.toString().trim();
|
|
91
|
-
throw new Error(msg || `Failed to read keychain item '${item}'.`);
|
|
92
|
-
}
|
|
93
|
-
const token = result.stdout?.toString().trim();
|
|
94
|
-
if (!token)
|
|
95
|
-
throw new Error(`Keychain item '${item}' exists but is empty.`);
|
|
96
|
-
return token;
|
|
97
|
+
// Try security first (no prompts for local items)
|
|
98
|
+
const secResult = spawnSync('security', ['find-generic-password', '-a', os.userInfo().username, '-s', item, '-w'], {
|
|
99
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
|
+
});
|
|
101
|
+
if (secResult.status === 0) {
|
|
102
|
+
const token = secResult.stdout?.toString().trim();
|
|
103
|
+
if (token)
|
|
104
|
+
return token;
|
|
97
105
|
}
|
|
98
|
-
|
|
106
|
+
// Fallback: binary searches both synced and non-synced via kSecAttrSynchronizableAny
|
|
107
|
+
const bin = ensureKeychainHelper();
|
|
108
|
+
const result = spawnSync(bin, ['get', item, os.userInfo().username], {
|
|
99
109
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
110
|
});
|
|
101
|
-
if (result.status ===
|
|
111
|
+
if (result.status === 1)
|
|
102
112
|
throw new Error(`Keychain item '${item}' not found.`);
|
|
103
113
|
if (result.status !== 0) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
throw new Error(`Keychain item '${item}' not found.`);
|
|
107
|
-
throw new Error(`Failed to read keychain item '${item}': ${stderr.trim() || `exit ${result.status}`}`);
|
|
114
|
+
const msg = result.stderr?.toString().trim();
|
|
115
|
+
throw new Error(msg || `Failed to read keychain item '${item}'.`);
|
|
108
116
|
}
|
|
109
117
|
const token = result.stdout?.toString().trim();
|
|
110
118
|
if (!token)
|
|
@@ -113,6 +121,10 @@ export function getKeychainToken(item, sync = false) {
|
|
|
113
121
|
}
|
|
114
122
|
/** Store or update a secret value in the macOS Keychain. iCloud-synced when sync=true. */
|
|
115
123
|
export function setKeychainToken(item, value, sync = false) {
|
|
124
|
+
if (backend) {
|
|
125
|
+
backend.set(item, value, sync);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
116
128
|
assertMacOS();
|
|
117
129
|
if (!value || !value.trim())
|
|
118
130
|
throw new Error('Secret value is empty.');
|
|
@@ -130,7 +142,7 @@ export function setKeychainToken(item, value, sync = false) {
|
|
|
130
142
|
}
|
|
131
143
|
return;
|
|
132
144
|
}
|
|
133
|
-
//
|
|
145
|
+
// `security -i` keeps the value out of argv (and `ps`).
|
|
134
146
|
const user = os.userInfo().username;
|
|
135
147
|
const cmd = `add-generic-password -a ${quoteForSecurityCli(user)} -s ${quoteForSecurityCli(item)} -w ${quoteForSecurityCli(value)} -U\n`;
|
|
136
148
|
const result = spawnSync('security', ['-i'], {
|
|
@@ -143,6 +155,8 @@ export function setKeychainToken(item, value, sync = false) {
|
|
|
143
155
|
}
|
|
144
156
|
/** Delete a keychain item. Returns true if it existed. */
|
|
145
157
|
export function deleteKeychainToken(item, sync = false) {
|
|
158
|
+
if (backend)
|
|
159
|
+
return backend.delete(item, sync);
|
|
146
160
|
assertMacOS();
|
|
147
161
|
if (sync) {
|
|
148
162
|
const bin = ensureKeychainHelper();
|
|
@@ -154,10 +168,25 @@ export function deleteKeychainToken(item, sync = false) {
|
|
|
154
168
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
155
169
|
}).status === 0;
|
|
156
170
|
}
|
|
157
|
-
// Quote a value for `security -i`'s shell-like tokenizer so it stays out of argv.
|
|
158
171
|
function quoteForSecurityCli(s) {
|
|
159
172
|
return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
160
173
|
}
|
|
174
|
+
/** Enumerate keychain item service names whose name starts with the given prefix. */
|
|
175
|
+
export function listKeychainItems(prefix) {
|
|
176
|
+
if (backend)
|
|
177
|
+
return backend.list(prefix);
|
|
178
|
+
assertMacOS();
|
|
179
|
+
const bin = ensureKeychainHelper();
|
|
180
|
+
const result = spawnSync(bin, ['list', prefix], {
|
|
181
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
182
|
+
});
|
|
183
|
+
if (result.status !== 0) {
|
|
184
|
+
const msg = result.stderr?.toString().trim();
|
|
185
|
+
throw new Error(msg || `Failed to enumerate keychain items with prefix '${prefix}'.`);
|
|
186
|
+
}
|
|
187
|
+
const out = result.stdout?.toString() || '';
|
|
188
|
+
return out.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
189
|
+
}
|
|
161
190
|
function expandHome(p) {
|
|
162
191
|
if (p.startsWith('~/') || p === '~') {
|
|
163
192
|
return path.join(os.homedir(), p.slice(1));
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Active-session detection across every context an agent can run in:
|
|
3
3
|
*
|
|
4
4
|
* - `terminal` — agents launched from VS Code / Cursor / Codium via the
|
|
5
|
-
* agents-cli extension. Published to `~/.agents
|
|
5
|
+
* agents-cli extension. Published to `~/.agents/runtime/live-terminals.json`
|
|
6
6
|
* with PID + session UUID per entry.
|
|
7
7
|
* - `teams` — agents spawned by `agents teams add`, tracked in
|
|
8
|
-
* `~/.agents
|
|
8
|
+
* `~/.agents/teams/agents/<id>/meta.json` with a PID the manager polls.
|
|
9
9
|
* - `cloud` — dispatched to Rush / Codex Cloud / Factory, tracked in
|
|
10
|
-
* the SQLite cache at `~/.agents
|
|
10
|
+
* the SQLite cache at `~/.agents/cloud/tasks.db`.
|
|
11
11
|
* - `headless` — bare `claude` / `codex` / `gemini` / `cursor-agent` /
|
|
12
12
|
* `opencode` processes that don't belong to any of the above. Detected
|
|
13
13
|
* by `ps` minus the PIDs we've already attributed.
|
|
@@ -23,10 +23,10 @@ import { execFile } from 'child_process';
|
|
|
23
23
|
import { promisify } from 'util';
|
|
24
24
|
import { listActiveTasks } from '../cloud/store.js';
|
|
25
25
|
import { AgentManager } from '../teams/agents.js';
|
|
26
|
-
import {
|
|
26
|
+
import { getUserAgentsDir } from '../state.js';
|
|
27
27
|
const execFileAsync = promisify(execFile);
|
|
28
28
|
const HOME = os.homedir();
|
|
29
|
-
const LIVE_TERMINALS_FILE = path.join(
|
|
29
|
+
const LIVE_TERMINALS_FILE = path.join(getUserAgentsDir(), 'runtime', 'live-terminals.json');
|
|
30
30
|
/**
|
|
31
31
|
* A process is classified `running` if its session file was touched in the
|
|
32
32
|
* last 2 minutes. Every Claude/Codex tool-call appends an event, so a
|
package/dist/lib/session/db.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import Database from '../sqlite.js';
|
|
12
|
-
import {
|
|
13
|
-
const SESSIONS_DIR = path.join(
|
|
12
|
+
import { getUserAgentsDir } from '../state.js';
|
|
13
|
+
const SESSIONS_DIR = path.join(getUserAgentsDir(), 'sessions');
|
|
14
14
|
const DB_PATH = path.join(SESSIONS_DIR, 'sessions.db');
|
|
15
15
|
/** Current schema version; bumped when migrations are added. */
|
|
16
16
|
const SCHEMA_VERSION = 5;
|
|
@@ -18,7 +18,7 @@ const SCHEMA_VERSION = 5;
|
|
|
18
18
|
* Canonicalize a file path for use as a scan_ledger key. The same physical
|
|
19
19
|
* session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
|
|
20
20
|
* (when `~/.claude` is a symlink to a versioned home) and
|
|
21
|
-
* `~/.agents
|
|
21
|
+
* `~/.agents/versions/claude/<v>/home/.claude/projects/x.jsonl`. Keying the
|
|
22
22
|
* ledger by the raw path means switching between these aliases (e.g. via
|
|
23
23
|
* `agents use`) misses the cache and forces a full re-parse. Realpath collapses
|
|
24
24
|
* all aliases to one stable key.
|
|
@@ -213,7 +213,7 @@ export function getScanStampsForPaths(filePaths) {
|
|
|
213
213
|
return result;
|
|
214
214
|
const db = getDB();
|
|
215
215
|
// Multiple input paths can resolve to the same canonical key (e.g. the same
|
|
216
|
-
// session JSONL reachable via `~/.claude/...` and `~/.agents
|
|
216
|
+
// session JSONL reachable via `~/.claude/...` and `~/.agents/versions/...`).
|
|
217
217
|
// We query DB by canonical key, then fan results back out to every original
|
|
218
218
|
// alias so callers can `.get(filePath)` with the path they passed in.
|
|
219
219
|
const canonicalToOriginals = new Map();
|
|
@@ -1451,12 +1451,12 @@ function normalizeVersion(version) {
|
|
|
1451
1451
|
const trimmed = version?.trim();
|
|
1452
1452
|
return trimmed ? trimmed : undefined;
|
|
1453
1453
|
}
|
|
1454
|
-
/** Extract the version number from a managed ~/.agents
|
|
1454
|
+
/** Extract the version number from a managed ~/.agents/versions/<agent>/<version>/... path. */
|
|
1455
1455
|
function extractVersionFromManagedPath(agent, sourcePath) {
|
|
1456
1456
|
if (!sourcePath)
|
|
1457
1457
|
return undefined;
|
|
1458
1458
|
const candidates = [sourcePath, safeRealpathSync(sourcePath) || ''];
|
|
1459
|
-
const marker = `/.agents
|
|
1459
|
+
const marker = `/.agents/versions/${agent}/`;
|
|
1460
1460
|
for (const candidate of candidates) {
|
|
1461
1461
|
if (!candidate)
|
|
1462
1462
|
continue;
|
|
@@ -424,6 +424,7 @@ export function renderSummary(events, cwd) {
|
|
|
424
424
|
// Plan items
|
|
425
425
|
const todoItems = [];
|
|
426
426
|
let exitPlanContent = null;
|
|
427
|
+
let planFilePath = null;
|
|
427
428
|
// Subagent spawns
|
|
428
429
|
const subagents = [];
|
|
429
430
|
// Errors
|
|
@@ -444,8 +445,14 @@ export function renderSummary(events, cwd) {
|
|
|
444
445
|
filesReadAbs.add(p);
|
|
445
446
|
}
|
|
446
447
|
else if (['Write', 'Edit', 'write_file', 'edit_file', 'create_file', 'replace', 'patch'].includes(tool)) {
|
|
447
|
-
if (p)
|
|
448
|
-
(
|
|
448
|
+
if (p) {
|
|
449
|
+
if (p.includes('.claude/plans/') && p.endsWith('.md')) {
|
|
450
|
+
planFilePath = p;
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
(isInsideCwd(p) || !cwd ? filesModifiedAbs : filesModifiedExternal).add(p);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
449
456
|
}
|
|
450
457
|
if (event.command) {
|
|
451
458
|
const cmd = event.command.replace(/\n/g, ' ').trim();
|
|
@@ -543,14 +550,19 @@ export function renderSummary(events, cwd) {
|
|
|
543
550
|
if (firstUserMessage || attachments.length > 0)
|
|
544
551
|
lines.push('');
|
|
545
552
|
// 2. Plan
|
|
546
|
-
if (todoItems.length > 0 || exitPlanContent) {
|
|
553
|
+
if (todoItems.length > 0 || exitPlanContent || planFilePath) {
|
|
547
554
|
lines.push(chalk.bold('Plan'));
|
|
555
|
+
if (planFilePath) {
|
|
556
|
+
const home = process.env.HOME ?? '';
|
|
557
|
+
const displayPath = home && planFilePath.startsWith(home) ? planFilePath.replace(home, '~') : planFilePath;
|
|
558
|
+
lines.push(' ' + chalk.cyan(displayPath));
|
|
559
|
+
}
|
|
548
560
|
if (exitPlanContent) {
|
|
549
561
|
const planLines = exitPlanContent.split('\n').slice(0, 10);
|
|
550
562
|
for (const l of planLines)
|
|
551
563
|
lines.push(' ' + l);
|
|
552
564
|
}
|
|
553
|
-
else {
|
|
565
|
+
else if (todoItems.length > 0) {
|
|
554
566
|
for (const item of todoItems.slice(0, 20)) {
|
|
555
567
|
lines.push(' · ' + item);
|
|
556
568
|
}
|
|
@@ -574,12 +586,14 @@ export function renderSummary(events, cwd) {
|
|
|
574
586
|
lines.push('');
|
|
575
587
|
}
|
|
576
588
|
// 4b. External edits (files edited outside the project root — typically /tmp)
|
|
577
|
-
|
|
578
|
-
|
|
589
|
+
// Filter out plan files (already shown in Plan section)
|
|
590
|
+
const externalNonPlan = [...filesModifiedExternal].filter(p => !(p.includes('.claude/plans/') && p.endsWith('.md')));
|
|
591
|
+
if (externalNonPlan.length > 0) {
|
|
592
|
+
const externalList = externalNonPlan.sort();
|
|
579
593
|
const home = process.env.HOME ?? '';
|
|
580
594
|
const display = externalList.slice(0, 3).map(p => home && p.startsWith(home) ? p.replace(home, '~') : p);
|
|
581
595
|
const more = externalList.length > 3 ? chalk.gray(` +${externalList.length - 3} more`) : '';
|
|
582
|
-
lines.push(chalk.gray(`External edits (${
|
|
596
|
+
lines.push(chalk.gray(`External edits (${externalList.length}): ${display.join(', ')}${more}`));
|
|
583
597
|
lines.push('');
|
|
584
598
|
}
|
|
585
599
|
// 5. Read files
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -42,9 +42,9 @@ export declare function promptConflictStrategy(conflictInfos: ConflictInfo[]): P
|
|
|
42
42
|
*
|
|
43
43
|
* History:
|
|
44
44
|
* v1 — initial shim (implicit, no marker).
|
|
45
|
-
* v2 — `--version=...` form in sync/refresh-
|
|
45
|
+
* v2 — `--version=...` form in sync/refresh-rules calls; refresh-rules
|
|
46
46
|
* shim hook for non-@-capable agents.
|
|
47
|
-
* v3 — sync/refresh-
|
|
47
|
+
* v3 — sync/refresh-rules flag renamed `--version` → `--agent-version`
|
|
48
48
|
* so it no longer collides with commander's top-level `--version`.
|
|
49
49
|
* v4 — project version marker changed from `.agents-version` to a
|
|
50
50
|
* root-level `agents.yaml`; shim now skips ~/.agents/agents.yaml
|
|
@@ -53,8 +53,12 @@ export declare function promptConflictStrategy(conflictInfos: ConflictInfo[]): P
|
|
|
53
53
|
* sandbox_mode, rules/agents-deny.rules) is actually read by the codex
|
|
54
54
|
* binary instead of $HOME/.codex.
|
|
55
55
|
* v6 — hard-disable Codex startup update checks in the generated shims.
|
|
56
|
+
* v7 — rename `agents refresh-memory` invocation to `agents refresh-rules`
|
|
57
|
+
* and capability flag `memoryImports` → `rulesImports`.
|
|
58
|
+
* v8 — versions moved from ~/.agents-system/versions to ~/.agents/versions
|
|
59
|
+
* (two-repo split: system = shipped defaults, user = operational state).
|
|
56
60
|
*/
|
|
57
|
-
export declare const SHIM_SCHEMA_VERSION =
|
|
61
|
+
export declare const SHIM_SCHEMA_VERSION = 8;
|
|
58
62
|
/**
|
|
59
63
|
* Generate the full bash shim script for the given agent. The returned string
|
|
60
64
|
* is written to ~/.agents/shims/{cliCommand} and made executable.
|
|
@@ -83,8 +87,10 @@ export declare function removeShim(agent: AgentId): boolean;
|
|
|
83
87
|
* read the versioned permissions/rules instead of $HOME/.codex.
|
|
84
88
|
* v4 — direct aliases read binaries and config homes from ~/.agents-system.
|
|
85
89
|
* v5 — hard-disable Codex startup update checks in versioned aliases.
|
|
90
|
+
* v6 — versions moved from ~/.agents-system/versions to ~/.agents/versions
|
|
91
|
+
* (two-repo split: system = shipped defaults, user = operational state).
|
|
86
92
|
*/
|
|
87
|
-
export declare const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
93
|
+
export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 6;
|
|
88
94
|
/**
|
|
89
95
|
* Generate a versioned alias script that directly execs a specific version.
|
|
90
96
|
* e.g., claude@2.0.65 -> directly runs that version's binary
|
|
@@ -219,8 +225,26 @@ export declare function getShimPath(agent: AgentId): string;
|
|
|
219
225
|
/**
|
|
220
226
|
* Return the first executable path that would be launched for this agent when
|
|
221
227
|
* resolving against PATH, excluding the managed shim itself.
|
|
228
|
+
*
|
|
229
|
+
* Legacy ~/.agents/shims/<cli> (from the pre-split single-root layout) is NOT
|
|
230
|
+
* treated as a shadow when a current managed shim exists at getShimPath() —
|
|
231
|
+
* that file is dead weight from the old layout and the repair flow removes it
|
|
232
|
+
* separately. Treating it as "shadowing" caused an infinite repair-prompt
|
|
233
|
+
* loop because addShimsToPath() only edits the rc file, never the legacy
|
|
234
|
+
* shim file itself.
|
|
222
235
|
*/
|
|
223
236
|
export declare function getPathShadowingExecutable(agent: AgentId): string | null;
|
|
237
|
+
/**
|
|
238
|
+
* Delete the legacy ~/.agents/shims/<cli> file if it exists, returning whether
|
|
239
|
+
* anything was removed. Pre-split installs put shims under ~/.agents/shims/;
|
|
240
|
+
* the new layout uses ~/.agents-system/shims/. The leftover file causes the
|
|
241
|
+
* repair-prompt loop reported in RUSH-664 — `getPathShadowingExecutable` flags
|
|
242
|
+
* it as a shadow but `addShimsToPath` only edits rc files, never the file
|
|
243
|
+
* itself. Removing it ends the loop.
|
|
244
|
+
*/
|
|
245
|
+
export declare function removeLegacyUserShim(agent: AgentId, overrides?: {
|
|
246
|
+
homeDir?: string;
|
|
247
|
+
}): boolean;
|
|
224
248
|
/**
|
|
225
249
|
* Check if the agent's CLI command is shadowed by a shell alias.
|
|
226
250
|
*
|
package/dist/lib/shims.js
CHANGED
|
@@ -156,9 +156,9 @@ export async function promptConflictStrategy(conflictInfos) {
|
|
|
156
156
|
*
|
|
157
157
|
* History:
|
|
158
158
|
* v1 — initial shim (implicit, no marker).
|
|
159
|
-
* v2 — `--version=...` form in sync/refresh-
|
|
159
|
+
* v2 — `--version=...` form in sync/refresh-rules calls; refresh-rules
|
|
160
160
|
* shim hook for non-@-capable agents.
|
|
161
|
-
* v3 — sync/refresh-
|
|
161
|
+
* v3 — sync/refresh-rules flag renamed `--version` → `--agent-version`
|
|
162
162
|
* so it no longer collides with commander's top-level `--version`.
|
|
163
163
|
* v4 — project version marker changed from `.agents-version` to a
|
|
164
164
|
* root-level `agents.yaml`; shim now skips ~/.agents/agents.yaml
|
|
@@ -167,8 +167,12 @@ export async function promptConflictStrategy(conflictInfos) {
|
|
|
167
167
|
* sandbox_mode, rules/agents-deny.rules) is actually read by the codex
|
|
168
168
|
* binary instead of $HOME/.codex.
|
|
169
169
|
* v6 — hard-disable Codex startup update checks in the generated shims.
|
|
170
|
+
* v7 — rename `agents refresh-memory` invocation to `agents refresh-rules`
|
|
171
|
+
* and capability flag `memoryImports` → `rulesImports`.
|
|
172
|
+
* v8 — versions moved from ~/.agents-system/versions to ~/.agents/versions
|
|
173
|
+
* (two-repo split: system = shipped defaults, user = operational state).
|
|
170
174
|
*/
|
|
171
|
-
export const SHIM_SCHEMA_VERSION =
|
|
175
|
+
export const SHIM_SCHEMA_VERSION = 8;
|
|
172
176
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
173
177
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
174
178
|
/**
|
|
@@ -194,16 +198,16 @@ export CLAUDE_CONFIG_DIR="$VERSION_DIR/home/${configDirName}"
|
|
|
194
198
|
export CODEX_HOME="$VERSION_DIR/home/${configDirName}"
|
|
195
199
|
`
|
|
196
200
|
: '';
|
|
197
|
-
// Agents that don't natively resolve @-imports in their
|
|
201
|
+
// Agents that don't natively resolve @-imports in their rules file need
|
|
198
202
|
// agents-cli to recompile when the user edits a rule/preset file. The
|
|
199
203
|
// check is fast (sha256 of ~8 small files) and skips the recompile when
|
|
200
204
|
// sources haven't changed.
|
|
201
|
-
const
|
|
205
|
+
const refreshRulesCall = !agentConfig.capabilities.rulesImports
|
|
202
206
|
? `
|
|
203
|
-
# Recompile
|
|
207
|
+
# Recompile rules if any rule/preset source has changed since last sync.
|
|
204
208
|
# Fast-path check (~10-20ms) when nothing changed; full recompile only on
|
|
205
209
|
# actual diff. Non-blocking failure — if the refresh errors, we still launch.
|
|
206
|
-
agents refresh-
|
|
210
|
+
agents refresh-rules --agent "$AGENT" --agent-version "$VERSION" --quiet 2>/dev/null || true
|
|
207
211
|
`
|
|
208
212
|
: '';
|
|
209
213
|
const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
|
|
@@ -283,7 +287,7 @@ if [ -z "$VERSION" ]; then
|
|
|
283
287
|
exit 1
|
|
284
288
|
fi
|
|
285
289
|
|
|
286
|
-
VERSION_DIR="$
|
|
290
|
+
VERSION_DIR="$AGENTS_USER_DIR/versions/$AGENT/$VERSION"
|
|
287
291
|
BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
|
|
288
292
|
|
|
289
293
|
# Auto-install if not present
|
|
@@ -328,7 +332,7 @@ PROJECT_AGENTS_DIR=$(find_project_agents_dir)
|
|
|
328
332
|
if [ -n "$PROJECT_AGENTS_DIR" ]; then
|
|
329
333
|
agents sync --agent "$AGENT" --agent-version "$VERSION" --project-dir "$PROJECT_AGENTS_DIR" --quiet >/dev/null 2>&1
|
|
330
334
|
fi
|
|
331
|
-
${
|
|
335
|
+
${refreshRulesCall}${managedEnv}
|
|
332
336
|
|
|
333
337
|
exec "$BINARY"${launchArgs} "$@"
|
|
334
338
|
`;
|
|
@@ -373,8 +377,10 @@ export function removeShim(agent) {
|
|
|
373
377
|
* read the versioned permissions/rules instead of $HOME/.codex.
|
|
374
378
|
* v4 — direct aliases read binaries and config homes from ~/.agents-system.
|
|
375
379
|
* v5 — hard-disable Codex startup update checks in versioned aliases.
|
|
380
|
+
* v6 — versions moved from ~/.agents-system/versions to ~/.agents/versions
|
|
381
|
+
* (two-repo split: system = shipped defaults, user = operational state).
|
|
376
382
|
*/
|
|
377
|
-
export const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
383
|
+
export const VERSIONED_ALIAS_SCHEMA_VERSION = 6;
|
|
378
384
|
/** Internal marker string used to embed the schema version in versioned alias scripts. */
|
|
379
385
|
const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
|
|
380
386
|
// The version string is interpolated into a generated bash script and into
|
|
@@ -399,14 +405,14 @@ export function generateVersionedAliasScript(agent, version) {
|
|
|
399
405
|
? `
|
|
400
406
|
# Claude stores OAuth credentials in the macOS keychain. Scope them to this
|
|
401
407
|
# version's config directory so direct aliases also switch the live account.
|
|
402
|
-
export CLAUDE_CONFIG_DIR="$HOME/.agents
|
|
408
|
+
export CLAUDE_CONFIG_DIR="$HOME/.agents/versions/${agent}/${version}/home/${configDirName}"
|
|
403
409
|
`
|
|
404
410
|
: agent === 'codex'
|
|
405
411
|
? `
|
|
406
412
|
# Codex reads its config (approval_policy, sandbox_mode, MCP servers, rules)
|
|
407
413
|
# from CODEX_HOME. Point direct aliases at the versioned home so permissions
|
|
408
414
|
# and rules written by agents-cli actually take effect.
|
|
409
|
-
export CODEX_HOME="$HOME/.agents
|
|
415
|
+
export CODEX_HOME="$HOME/.agents/versions/${agent}/${version}/home/${configDirName}"
|
|
410
416
|
`
|
|
411
417
|
: '';
|
|
412
418
|
const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
|
|
@@ -415,7 +421,7 @@ export CODEX_HOME="$HOME/.agents-system/versions/${agent}/${version}/home/${conf
|
|
|
415
421
|
# ${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}
|
|
416
422
|
# Direct alias for ${agentConfig.name}@${version}
|
|
417
423
|
|
|
418
|
-
BINARY="$HOME/.agents
|
|
424
|
+
BINARY="$HOME/.agents/versions/${agent}/${version}/node_modules/.bin/${agentConfig.cliCommand}"
|
|
419
425
|
|
|
420
426
|
if [ ! -x "$BINARY" ]; then
|
|
421
427
|
echo "agents: ${agent}@${version} not installed" >&2
|
|
@@ -1021,20 +1027,72 @@ export function getShimPath(agent) {
|
|
|
1021
1027
|
/**
|
|
1022
1028
|
* Return the first executable path that would be launched for this agent when
|
|
1023
1029
|
* resolving against PATH, excluding the managed shim itself.
|
|
1030
|
+
*
|
|
1031
|
+
* Legacy ~/.agents/shims/<cli> (from the pre-split single-root layout) is NOT
|
|
1032
|
+
* treated as a shadow when a current managed shim exists at getShimPath() —
|
|
1033
|
+
* that file is dead weight from the old layout and the repair flow removes it
|
|
1034
|
+
* separately. Treating it as "shadowing" caused an infinite repair-prompt
|
|
1035
|
+
* loop because addShimsToPath() only edits the rc file, never the legacy
|
|
1036
|
+
* shim file itself.
|
|
1024
1037
|
*/
|
|
1025
1038
|
export function getPathShadowingExecutable(agent) {
|
|
1026
1039
|
const pathDirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
1027
1040
|
const shimPath = path.resolve(getShimPath(agent));
|
|
1028
1041
|
const cliCommand = AGENTS[agent].cliCommand;
|
|
1042
|
+
const legacyUserShim = path.resolve(path.join(os.homedir(), '.agents', 'shims', cliCommand));
|
|
1043
|
+
const managedShimExists = fs.existsSync(shimPath);
|
|
1029
1044
|
for (const dir of pathDirs) {
|
|
1030
1045
|
const candidate = path.resolve(dir, cliCommand);
|
|
1031
1046
|
if (!fs.existsSync(candidate)) {
|
|
1032
1047
|
continue;
|
|
1033
1048
|
}
|
|
1034
|
-
|
|
1049
|
+
if (candidate === shimPath)
|
|
1050
|
+
return null;
|
|
1051
|
+
if (candidate === legacyUserShim && managedShimExists) {
|
|
1052
|
+
// Legacy file from the pre-split layout. Don't treat as shadow — the
|
|
1053
|
+
// repair flow deletes it via removeLegacyUserShim instead. Continue
|
|
1054
|
+
// scanning so a real binary later in PATH is still detected.
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
return candidate;
|
|
1035
1058
|
}
|
|
1036
1059
|
return null;
|
|
1037
1060
|
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Delete the legacy ~/.agents/shims/<cli> file if it exists, returning whether
|
|
1063
|
+
* anything was removed. Pre-split installs put shims under ~/.agents/shims/;
|
|
1064
|
+
* the new layout uses ~/.agents-system/shims/. The leftover file causes the
|
|
1065
|
+
* repair-prompt loop reported in RUSH-664 — `getPathShadowingExecutable` flags
|
|
1066
|
+
* it as a shadow but `addShimsToPath` only edits rc files, never the file
|
|
1067
|
+
* itself. Removing it ends the loop.
|
|
1068
|
+
*/
|
|
1069
|
+
export function removeLegacyUserShim(agent, overrides) {
|
|
1070
|
+
const cliCommand = AGENTS[agent].cliCommand;
|
|
1071
|
+
const homeDir = overrides?.homeDir || os.homedir();
|
|
1072
|
+
const legacyPath = path.join(homeDir, '.agents', 'shims', cliCommand);
|
|
1073
|
+
if (!fs.existsSync(legacyPath))
|
|
1074
|
+
return false;
|
|
1075
|
+
// Belt-and-suspenders: only remove if the current managed shim location is
|
|
1076
|
+
// different (it always should be — getShimsDir() returns the system dir —
|
|
1077
|
+
// but guard against future refactors that might collapse the two).
|
|
1078
|
+
const currentShim = path.resolve(getShimPath(agent));
|
|
1079
|
+
if (path.resolve(legacyPath) === currentShim)
|
|
1080
|
+
return false;
|
|
1081
|
+
try {
|
|
1082
|
+
fs.unlinkSync(legacyPath);
|
|
1083
|
+
// Best-effort: clean up the legacy shims dir if empty.
|
|
1084
|
+
try {
|
|
1085
|
+
const legacyDir = path.dirname(legacyPath);
|
|
1086
|
+
if (fs.readdirSync(legacyDir).length === 0)
|
|
1087
|
+
fs.rmdirSync(legacyDir);
|
|
1088
|
+
}
|
|
1089
|
+
catch { /* best-effort */ }
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
catch {
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1038
1096
|
/**
|
|
1039
1097
|
* Check if the agent's CLI command is shadowed by a shell alias.
|
|
1040
1098
|
*
|