@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.
Files changed (102) hide show
  1. package/README.md +31 -3
  2. package/dist/commands/browser.d.ts +2 -0
  3. package/dist/commands/browser.js +388 -0
  4. package/dist/commands/daemon.js +1 -1
  5. package/dist/commands/doctor.d.ts +16 -9
  6. package/dist/commands/doctor.js +248 -12
  7. package/dist/commands/exec.js +17 -17
  8. package/dist/commands/prune.js +9 -3
  9. package/dist/commands/refresh-rules.d.ts +15 -0
  10. package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
  11. package/dist/commands/routines.js +1 -1
  12. package/dist/commands/rules.js +100 -4
  13. package/dist/commands/secrets.js +206 -12
  14. package/dist/commands/sync.js +19 -0
  15. package/dist/commands/teams.js +162 -22
  16. package/dist/commands/trash.d.ts +10 -0
  17. package/dist/commands/trash.js +187 -0
  18. package/dist/commands/view.js +46 -13
  19. package/dist/index.js +62 -4
  20. package/dist/lib/agents.js +2 -2
  21. package/dist/lib/browser/cdp.d.ts +24 -0
  22. package/dist/lib/browser/cdp.js +94 -0
  23. package/dist/lib/browser/chrome.d.ts +16 -0
  24. package/dist/lib/browser/chrome.js +157 -0
  25. package/dist/lib/browser/drivers/local.d.ts +8 -0
  26. package/dist/lib/browser/drivers/local.js +22 -0
  27. package/dist/lib/browser/drivers/ssh.d.ts +9 -0
  28. package/dist/lib/browser/drivers/ssh.js +129 -0
  29. package/dist/lib/browser/index.d.ts +5 -0
  30. package/dist/lib/browser/index.js +5 -0
  31. package/dist/lib/browser/input.d.ts +6 -0
  32. package/dist/lib/browser/input.js +52 -0
  33. package/dist/lib/browser/ipc.d.ts +12 -0
  34. package/dist/lib/browser/ipc.js +223 -0
  35. package/dist/lib/browser/profiles.d.ts +11 -0
  36. package/dist/lib/browser/profiles.js +61 -0
  37. package/dist/lib/browser/refs.d.ts +21 -0
  38. package/dist/lib/browser/refs.js +88 -0
  39. package/dist/lib/browser/service.d.ts +45 -0
  40. package/dist/lib/browser/service.js +404 -0
  41. package/dist/lib/browser/types.d.ts +73 -0
  42. package/dist/lib/browser/types.js +7 -0
  43. package/dist/lib/cloud/codex.js +1 -1
  44. package/dist/lib/cloud/registry.js +2 -2
  45. package/dist/lib/cloud/rush.js +2 -2
  46. package/dist/lib/cloud/store.js +2 -2
  47. package/dist/lib/daemon.d.ts +1 -1
  48. package/dist/lib/daemon.js +47 -11
  49. package/dist/lib/diff-text.d.ts +25 -0
  50. package/dist/lib/diff-text.js +47 -0
  51. package/dist/lib/doctor-diff.d.ts +64 -0
  52. package/dist/lib/doctor-diff.js +497 -0
  53. package/dist/lib/git.js +3 -3
  54. package/dist/lib/hooks.d.ts +6 -0
  55. package/dist/lib/hooks.js +6 -1
  56. package/dist/lib/migrate.js +77 -0
  57. package/dist/lib/pty-client.js +3 -3
  58. package/dist/lib/pty-server.js +36 -7
  59. package/dist/lib/resources.js +1 -1
  60. package/dist/lib/rotate.d.ts +43 -26
  61. package/dist/lib/rotate.js +99 -44
  62. package/dist/lib/rules/compile.d.ts +104 -0
  63. package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
  64. package/dist/lib/rules/compose.d.ts +78 -0
  65. package/dist/lib/rules/compose.js +170 -0
  66. package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
  67. package/dist/lib/{memory.js → rules/rules.js} +10 -10
  68. package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
  69. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  70. package/dist/lib/secrets/bundles.d.ts +61 -4
  71. package/dist/lib/secrets/bundles.js +222 -54
  72. package/dist/lib/secrets/index.d.ts +24 -5
  73. package/dist/lib/secrets/index.js +70 -41
  74. package/dist/lib/session/active.js +5 -5
  75. package/dist/lib/session/db.js +4 -4
  76. package/dist/lib/session/discover.js +2 -2
  77. package/dist/lib/session/render.js +21 -7
  78. package/dist/lib/shims.d.ts +28 -4
  79. package/dist/lib/shims.js +72 -14
  80. package/dist/lib/state.d.ts +22 -28
  81. package/dist/lib/state.js +83 -76
  82. package/dist/lib/sync-manifest.d.ts +2 -2
  83. package/dist/lib/sync-manifest.js +5 -5
  84. package/dist/lib/teams/agents.d.ts +4 -2
  85. package/dist/lib/teams/agents.js +11 -4
  86. package/dist/lib/teams/api.d.ts +1 -1
  87. package/dist/lib/teams/api.js +2 -2
  88. package/dist/lib/teams/index.d.ts +1 -0
  89. package/dist/lib/teams/index.js +1 -0
  90. package/dist/lib/teams/persistence.js +3 -3
  91. package/dist/lib/teams/registry.d.ts +8 -1
  92. package/dist/lib/teams/registry.js +8 -2
  93. package/dist/lib/teams/worktree.d.ts +30 -0
  94. package/dist/lib/teams/worktree.js +96 -0
  95. package/dist/lib/types.d.ts +13 -7
  96. package/dist/lib/types.js +3 -3
  97. package/dist/lib/versions.d.ts +30 -2
  98. package/dist/lib/versions.js +127 -105
  99. package/package.json +1 -1
  100. package/scripts/postinstall.js +29 -0
  101. package/dist/commands/refresh-memory.d.ts +0 -15
  102. 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
- * Calls a compiled Swift helper (keychain-helper.swift) to store and retrieve
5
- * API keys and tokens via the Security framework, with kSecAttrSynchronizable
6
- * set so iCloud Keychain syncs them across the user's Macs.
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 YAML value into either a literal string or a typed secret ref. */
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(`iCloud Keychain helper missing at ${binPath}. ` +
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
- // iCloud Keychain sync is opt-in per bundle. When sync=true we route through
63
- // the Swift helper (kSecAttrSynchronizable=true). When sync is false/undefined
64
- // we use /usr/bin/security, which is always present on macOS — so a user who
65
- // never opts in to iCloud sync never needs Xcode Command Line Tools.
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
- if (sync) {
70
- const bin = ensureKeychainHelper();
71
- return spawnSync(bin, ['has', item, os.userInfo().username], {
72
- stdio: ['ignore', 'pipe', 'pipe'],
73
- }).status === 0;
74
- }
75
- return spawnSync('security', ['find-generic-password', '-a', os.userInfo().username, '-s', item, '-w'], {
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
- if (sync) {
83
- const bin = ensureKeychainHelper();
84
- const result = spawnSync(bin, ['get', item, os.userInfo().username], {
85
- stdio: ['ignore', 'pipe', 'pipe'],
86
- });
87
- if (result.status === 1)
88
- throw new Error(`Keychain item '${item}' not found.`);
89
- if (result.status !== 0) {
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
- const result = spawnSync('security', ['find-generic-password', '-a', os.userInfo().username, '-s', item, '-w'], {
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 === 44)
111
+ if (result.status === 1)
102
112
  throw new Error(`Keychain item '${item}' not found.`);
103
113
  if (result.status !== 0) {
104
- const stderr = result.stderr?.toString() || '';
105
- if (/could not be found/i.test(stderr))
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
- // The `security -i` interactive form keeps the value out of argv (and `ps`).
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-system/runtime/live-terminals.json`
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-system/teams/agents/<id>/meta.json` with a PID the manager polls.
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-system/cloud/tasks.db`.
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 { getAgentsDir } from '../state.js';
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(getAgentsDir(), 'runtime', 'live-terminals.json');
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
@@ -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 { getAgentsDir } from '../state.js';
13
- const SESSIONS_DIR = path.join(getAgentsDir(), 'sessions');
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-system/versions/claude/<v>/home/.claude/projects/x.jsonl`. Keying the
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-system/versions/...`).
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-system/versions/<agent>/<version>/... path. */
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-system/versions/${agent}/`;
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
- (isInsideCwd(p) || !cwd ? filesModifiedAbs : filesModifiedExternal).add(p);
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
- if (filesModifiedExternal.size > 0) {
578
- const externalList = [...filesModifiedExternal].sort();
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 (${filesModifiedExternal.size}): ${display.join(', ')}${more}`));
596
+ lines.push(chalk.gray(`External edits (${externalList.length}): ${display.join(', ')}${more}`));
583
597
  lines.push('');
584
598
  }
585
599
  // 5. Read files
@@ -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-memory calls; refresh-memory
45
+ * v2 — `--version=...` form in sync/refresh-rules calls; refresh-rules
46
46
  * shim hook for non-@-capable agents.
47
- * v3 — sync/refresh-memory flag renamed `--version` → `--agent-version`
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 = 6;
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 = 5;
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-memory calls; refresh-memory
159
+ * v2 — `--version=...` form in sync/refresh-rules calls; refresh-rules
160
160
  * shim hook for non-@-capable agents.
161
- * v3 — sync/refresh-memory flag renamed `--version` → `--agent-version`
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 = 6;
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 memory file need
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 refreshMemoryCall = !agentConfig.capabilities.memoryImports
205
+ const refreshRulesCall = !agentConfig.capabilities.rulesImports
202
206
  ? `
203
- # Recompile memory if any rule/preset source has changed since last sync.
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-memory --agent "$AGENT" --agent-version "$VERSION" --quiet 2>/dev/null || true
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="$AGENTS_SYSTEM_DIR/versions/$AGENT/$VERSION"
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
- ${refreshMemoryCall}${managedEnv}
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 = 5;
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-system/versions/${agent}/${version}/home/${configDirName}"
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-system/versions/${agent}/${version}/home/${configDirName}"
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-system/versions/${agent}/${version}/node_modules/.bin/${agentConfig.cliCommand}"
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
- return candidate === shimPath ? null : candidate;
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
  *