@phnx-labs/agents-cli 1.20.51 → 1.20.53

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 (142) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +12 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/browser.js +215 -7
  5. package/dist/commands/cloud.d.ts +3 -0
  6. package/dist/commands/cloud.js +8 -1
  7. package/dist/commands/events.d.ts +1 -1
  8. package/dist/commands/events.js +2 -3
  9. package/dist/commands/exec.js +82 -3
  10. package/dist/commands/factory.js +8 -0
  11. package/dist/commands/feed.d.ts +38 -0
  12. package/dist/commands/feed.js +274 -0
  13. package/dist/commands/logs.d.ts +5 -1
  14. package/dist/commands/logs.js +248 -3
  15. package/dist/commands/mcp.js +7 -0
  16. package/dist/commands/memory.d.ts +9 -0
  17. package/dist/commands/memory.js +164 -0
  18. package/dist/commands/message.d.ts +11 -6
  19. package/dist/commands/message.js +140 -5
  20. package/dist/commands/routines.js +12 -0
  21. package/dist/commands/secrets-migrate.d.ts +2 -1
  22. package/dist/commands/secrets-migrate.js +88 -13
  23. package/dist/commands/secrets.d.ts +22 -0
  24. package/dist/commands/secrets.js +176 -42
  25. package/dist/commands/sessions.js +10 -1
  26. package/dist/commands/teams.js +4 -0
  27. package/dist/commands/worktree.js +4 -2
  28. package/dist/index.js +21 -22
  29. package/dist/lib/agents.js +249 -17
  30. package/dist/lib/answer-router.d.ts +75 -0
  31. package/dist/lib/answer-router.js +149 -0
  32. package/dist/lib/ask-classifier.d.ts +71 -0
  33. package/dist/lib/ask-classifier.js +197 -0
  34. package/dist/lib/browser/login-detection.d.ts +94 -0
  35. package/dist/lib/browser/login-detection.js +274 -0
  36. package/dist/lib/browser/profiles.d.ts +17 -8
  37. package/dist/lib/browser/profiles.js +27 -8
  38. package/dist/lib/browser/secret-ref.d.ts +10 -0
  39. package/dist/lib/browser/secret-ref.js +14 -0
  40. package/dist/lib/browser/service.js +14 -12
  41. package/dist/lib/cloud/antigravity.d.ts +0 -2
  42. package/dist/lib/cloud/antigravity.js +2 -17
  43. package/dist/lib/cloud/codex.js +3 -18
  44. package/dist/lib/cloud/rush.d.ts +15 -0
  45. package/dist/lib/cloud/rush.js +10 -16
  46. package/dist/lib/cloud/stream.js +2 -0
  47. package/dist/lib/cloud/types.d.ts +21 -0
  48. package/dist/lib/cloud/types.js +81 -0
  49. package/dist/lib/crabbox/cli.d.ts +1 -1
  50. package/dist/lib/crabbox/cli.js +12 -2
  51. package/dist/lib/crabbox/lease.d.ts +19 -0
  52. package/dist/lib/crabbox/lease.js +22 -11
  53. package/dist/lib/crabbox/progress.d.ts +62 -0
  54. package/dist/lib/crabbox/progress.js +129 -0
  55. package/dist/lib/crabbox/runtimes.d.ts +38 -1
  56. package/dist/lib/crabbox/runtimes.js +98 -5
  57. package/dist/lib/daemon.d.ts +12 -9
  58. package/dist/lib/daemon.js +32 -17
  59. package/dist/lib/events.d.ts +31 -5
  60. package/dist/lib/events.js +291 -101
  61. package/dist/lib/exec.js +20 -1
  62. package/dist/lib/feed-outcome.d.ts +101 -0
  63. package/dist/lib/feed-outcome.js +244 -0
  64. package/dist/lib/feed-policy.d.ts +30 -0
  65. package/dist/lib/feed-policy.js +133 -0
  66. package/dist/lib/feed.d.ts +180 -0
  67. package/dist/lib/feed.js +627 -0
  68. package/dist/lib/git.d.ts +17 -1
  69. package/dist/lib/git.js +20 -1
  70. package/dist/lib/hooks.js +529 -14
  71. package/dist/lib/hosts/passthrough.d.ts +3 -3
  72. package/dist/lib/hosts/passthrough.js +3 -3
  73. package/dist/lib/mailbox-gc.d.ts +22 -0
  74. package/dist/lib/mailbox-gc.js +161 -0
  75. package/dist/lib/mailbox.d.ts +26 -2
  76. package/dist/lib/mailbox.js +80 -5
  77. package/dist/lib/mcp.js +82 -0
  78. package/dist/lib/memory.d.ts +55 -0
  79. package/dist/lib/memory.js +274 -0
  80. package/dist/lib/notify.d.ts +16 -0
  81. package/dist/lib/notify.js +61 -0
  82. package/dist/lib/operator.d.ts +26 -0
  83. package/dist/lib/operator.js +107 -0
  84. package/dist/lib/plugins.d.ts +35 -0
  85. package/dist/lib/plugins.js +217 -0
  86. package/dist/lib/remote-agents-json.d.ts +14 -0
  87. package/dist/lib/remote-agents-json.js +94 -0
  88. package/dist/lib/resources/mcp.js +44 -0
  89. package/dist/lib/resources/memory.d.ts +15 -0
  90. package/dist/lib/resources/memory.js +46 -0
  91. package/dist/lib/resources/types.d.ts +2 -2
  92. package/dist/lib/rotate.js +2 -0
  93. package/dist/lib/runner.d.ts +43 -0
  94. package/dist/lib/runner.js +323 -74
  95. package/dist/lib/sandbox.js +6 -0
  96. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  97. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  98. package/dist/lib/secrets/agent.d.ts +21 -0
  99. package/dist/lib/secrets/agent.js +63 -1
  100. package/dist/lib/secrets/bundles.d.ts +33 -1
  101. package/dist/lib/secrets/bundles.js +76 -21
  102. package/dist/lib/secrets/icloud-import.d.ts +79 -0
  103. package/dist/lib/secrets/icloud-import.js +203 -0
  104. package/dist/lib/secrets/index.d.ts +142 -2
  105. package/dist/lib/secrets/index.js +695 -30
  106. package/dist/lib/secrets/remote.js +1 -1
  107. package/dist/lib/secrets/sync.js +1 -1
  108. package/dist/lib/session/active.d.ts +18 -0
  109. package/dist/lib/session/active.js +47 -17
  110. package/dist/lib/session/db.d.ts +9 -1
  111. package/dist/lib/session/db.js +18 -3
  112. package/dist/lib/session/discover.d.ts +13 -0
  113. package/dist/lib/session/discover.js +32 -2
  114. package/dist/lib/session/parse.d.ts +8 -0
  115. package/dist/lib/session/parse.js +42 -21
  116. package/dist/lib/session/remote-active.js +8 -89
  117. package/dist/lib/session/state.d.ts +11 -0
  118. package/dist/lib/session/state.js +50 -1
  119. package/dist/lib/session/tail.d.ts +23 -4
  120. package/dist/lib/session/tail.js +34 -16
  121. package/dist/lib/session/throughput.d.ts +30 -0
  122. package/dist/lib/session/throughput.js +86 -0
  123. package/dist/lib/shim-heal.d.ts +12 -3
  124. package/dist/lib/shim-heal.js +12 -6
  125. package/dist/lib/staleness/detectors/subagents.js +57 -3
  126. package/dist/lib/staleness/writers/hooks.js +7 -3
  127. package/dist/lib/staleness/writers/subagents.js +37 -6
  128. package/dist/lib/startup/command-registry.d.ts +2 -0
  129. package/dist/lib/startup/command-registry.js +4 -0
  130. package/dist/lib/state.d.ts +2 -0
  131. package/dist/lib/state.js +25 -8
  132. package/dist/lib/subagents.d.ts +52 -0
  133. package/dist/lib/subagents.js +315 -12
  134. package/dist/lib/teams/agents.js +6 -3
  135. package/dist/lib/teams/worktree.d.ts +8 -0
  136. package/dist/lib/teams/worktree.js +8 -0
  137. package/dist/lib/types.d.ts +20 -2
  138. package/dist/lib/versions.js +38 -48
  139. package/dist/lib/whats-new.d.ts +5 -3
  140. package/dist/lib/whats-new.js +25 -5
  141. package/package.json +4 -3
  142. package/scripts/postinstall.js +61 -1
@@ -42,6 +42,7 @@ import { emit } from './events.js';
42
42
  import { safeJoin } from './paths.js';
43
43
  import { readSkillSourceCommandMarker, shouldInstallCommandAsSkill } from './command-skills.js';
44
44
  import { getWriter, getDetector } from './staleness/registry.js';
45
+ import { syncMemoryToVersionHome } from './memory.js';
45
46
  /** Promisified exec for running shell commands. */
46
47
  const execAsync = promisify(exec);
47
48
  const execFileAsync = promisify(execFile);
@@ -1127,19 +1128,28 @@ export async function installVersion(agent, version, onProgress, opts) {
1127
1128
  emit('version.install', { agent, version: installedVersion });
1128
1129
  return { success: true, installedVersion };
1129
1130
  }
1130
- // Resolve the `oldest` alias to a concrete npm version up front so the rest
1131
- // of the install path treats it as an ordinary pinned install. (`latest`
1132
- // keeps its bare-package-name + post-install-rename handling below.)
1133
- if (version === 'oldest') {
1134
- const oldest = await getOldestNpmVersion(agent);
1135
- if (!oldest) {
1131
+ // Resolve the `latest`/`oldest` aliases to a concrete npm version up front so
1132
+ // the rest of the install path treats them as an ordinary pinned install.
1133
+ // This is what keeps concurrent installs safe: resolving `latest` only AFTER
1134
+ // npm finished meant the install ran in a shared, well-known
1135
+ // `versions/<agent>/latest/` scratch dir that was renamed to the real version
1136
+ // at the end — so a concurrent `agents view` reconcile
1137
+ // (reconcileStaleLatestForAgent) or a second `latest` install could rename
1138
+ // that dir out from under npm mid-extraction and corrupt the install (the
1139
+ // seeded package.json and half-extracted node_modules would vanish, yielding
1140
+ // ENOENT). A concrete dir per version has no shared name to race on.
1141
+ if (version === 'latest' || version === 'oldest') {
1142
+ const resolved = version === 'latest'
1143
+ ? await getLatestNpmVersion(agent)
1144
+ : await getOldestNpmVersion(agent);
1145
+ if (!resolved) {
1136
1146
  return {
1137
1147
  success: false,
1138
1148
  installedVersion: version,
1139
- error: `Could not resolve the oldest published version for ${agentConfig.name} from npm.`,
1149
+ error: `Could not resolve the ${version} published version for ${agentConfig.name} from npm.`,
1140
1150
  };
1141
1151
  }
1142
- version = oldest;
1152
+ version = resolved;
1143
1153
  }
1144
1154
  ensureAgentsDir();
1145
1155
  const versionDir = getVersionDir(agent, version);
@@ -1161,12 +1171,11 @@ export async function installVersion(agent, version, onProgress, opts) {
1161
1171
  private: true,
1162
1172
  };
1163
1173
  fs.writeFileSync(path.join(versionDir, 'package.json'), JSON.stringify(packageJson, null, 2));
1164
- // Install the package
1165
- const packageSpec = version === 'latest'
1166
- ? agentConfig.npmPackage
1167
- : `${agentConfig.npmPackage}@${version}`;
1168
- // The `${agentConfig.npmPackage}@` prefix is load-bearing: it ensures `version`
1169
- // (which VERSION_RE permits to start with `-`) is never passed as a standalone npm CLI flag.
1174
+ // Install the package. `version` is always concrete here (`latest`/`oldest`
1175
+ // were resolved above), so the spec is always pinned. The `@` prefix is
1176
+ // load-bearing: it ensures `version` (which VERSION_RE permits to start with
1177
+ // `-`) is never passed as a standalone npm CLI flag.
1178
+ const packageSpec = `${agentConfig.npmPackage}@${version}`;
1170
1179
  try {
1171
1180
  // Check npm is available
1172
1181
  const winShell = process.platform === 'win32';
@@ -1181,37 +1190,12 @@ export async function installVersion(agent, version, onProgress, opts) {
1181
1190
  };
1182
1191
  }
1183
1192
  onProgress?.(`Installing ${packageSpec}...`);
1184
- const { stdout } = await execFileAsync('npm', ['install', packageSpec, '--ignore-scripts'], { cwd: versionDir, shell: winShell });
1185
- // Determine the actual installed version
1186
- let installedVersion = version;
1187
- if (version === 'latest') {
1188
- const pkgJsonPath = path.join(versionDir, 'node_modules', agentConfig.npmPackage.replace(/^@/, '').split('/')[0], 'package.json');
1189
- // Try to read the actual version from installed package
1190
- try {
1191
- const installedPkgPath = path.join(versionDir, 'node_modules', agentConfig.npmPackage, 'package.json');
1192
- if (fs.existsSync(installedPkgPath)) {
1193
- const installedPkg = JSON.parse(fs.readFileSync(installedPkgPath, 'utf-8'));
1194
- installedVersion = installedPkg.version;
1195
- // Rename the directory to the actual version
1196
- if (installedVersion !== 'latest') {
1197
- const actualVersionDir = getVersionDir(agent, installedVersion);
1198
- if (!fs.existsSync(actualVersionDir)) {
1199
- fs.renameSync(versionDir, actualVersionDir);
1200
- }
1201
- else {
1202
- // Already exists — drop the 'latest' install artifacts but keep
1203
- // `home/` (may contain conversation history from sessions that
1204
- // ran while the user was on `latest`).
1205
- removeInstallArtifacts(versionDir);
1206
- }
1207
- }
1208
- }
1209
- }
1210
- catch (e) {
1211
- // Failed to determine version - this shouldn't happen
1212
- throw new Error(`Failed to determine installed version: ${e.message}`);
1213
- }
1214
- }
1193
+ await execFileAsync('npm', ['install', packageSpec, '--ignore-scripts'], { cwd: versionDir, shell: winShell });
1194
+ // `version` is concrete (the `latest`/`oldest` aliases were resolved up
1195
+ // front), so the package installed directly into its final versioned dir —
1196
+ // no post-install rename, and no shared `latest/` dir for a concurrent
1197
+ // process to move out from under us.
1198
+ const installedVersion = version;
1215
1199
  // Create versioned alias (e.g., claude@2.0.65)
1216
1200
  createVersionedAlias(agent, installedVersion);
1217
1201
  // Claude reads its global config from CLAUDE_CONFIG_DIR/.claude.json —
@@ -1241,9 +1225,9 @@ export async function installVersion(agent, version, onProgress, opts) {
1241
1225
  // binary (postinstall failed, or a platform with no published native dep)
1242
1226
  // fails there with the correct message rather than throwing here.
1243
1227
  if (agentConfig.npmPackage) {
1244
- // Recompute from installedVersion, not the (possibly renamed) `versionDir`:
1245
- // the `latest` branch above may have renamed the dir to its concrete version.
1246
- const pkgRoot = path.join(getVersionDir(agent, installedVersion), 'node_modules', agentConfig.npmPackage);
1228
+ // `installedVersion === version`, so this is exactly `versionDir` — the
1229
+ // install landed in its final dir with no rename to chase.
1230
+ const pkgRoot = path.join(versionDir, 'node_modules', agentConfig.npmPackage);
1247
1231
  try {
1248
1232
  const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8'));
1249
1233
  const postinstall = pkg?.scripts?.postinstall;
@@ -2335,6 +2319,12 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2335
2319
  const r = workflowsWriter.write({ version, versionHome, selection: workflowsToSync, cwd });
2336
2320
  result.workflows.push(...r.synced);
2337
2321
  }
2322
+ // Knowledge memory (RUSH-1330) — distinct from selection.memory which still
2323
+ // means the composed *rules* file. Always fan out ~/.agents/memory/ facts
2324
+ // into capable agent version homes on every full or partial sync.
2325
+ if (supports(agent, 'memory', version).ok) {
2326
+ syncMemoryToVersionHome(agent, versionHome, cwd);
2327
+ }
2338
2328
  // Write manifest after a full sync (no user-passed selection) so the next
2339
2329
  // launch can skip the slow path. Pattern-derived selections still count as
2340
2330
  // "full" — the agents.yaml patterns describe the intended scope, not a
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Render a compact "What's new" summary from a CHANGELOG.md body: one bullet
3
- * per feature/fix heading (the `**...**` lines) for each version in the range
4
- * the user actually moved through, `(fromVersion, toVersion]`. The verbose
5
- * sub-bullets are intentionally dropped — the full notes live in the changelog.
3
+ * per feature/fix heading for each version in the range the user actually
4
+ * moved through, `(fromVersion, toVersion]`. Headings are recognized in both
5
+ * changelog formats — the current `- **Title.** prose…` single-line bullets
6
+ * and the older standalone `**Heading**` lines. The verbose prose/sub-bullets
7
+ * are intentionally dropped — the full notes live in the changelog.
6
8
  *
7
9
  * Returns colored lines ready to print, empty when nothing is in range.
8
10
  */
@@ -2,15 +2,23 @@ import chalk from 'chalk';
2
2
  import { compareVersions } from './agent-spec/primitives.js';
3
3
  /**
4
4
  * Render a compact "What's new" summary from a CHANGELOG.md body: one bullet
5
- * per feature/fix heading (the `**...**` lines) for each version in the range
6
- * the user actually moved through, `(fromVersion, toVersion]`. The verbose
7
- * sub-bullets are intentionally dropped — the full notes live in the changelog.
5
+ * per feature/fix heading for each version in the range the user actually
6
+ * moved through, `(fromVersion, toVersion]`. Headings are recognized in both
7
+ * changelog formats — the current `- **Title.** prose…` single-line bullets
8
+ * and the older standalone `**Heading**` lines. The verbose prose/sub-bullets
9
+ * are intentionally dropped — the full notes live in the changelog.
8
10
  *
9
11
  * Returns colored lines ready to print, empty when nothing is in range.
10
12
  */
11
13
  export function renderWhatsNew(changelog, fromVersion, toVersion) {
12
14
  const out = [];
13
15
  let inRelevantSection = false;
16
+ // Whether the CURRENT version section uses the old standalone-heading format.
17
+ // Old sections nest `-` sub-bullets under each `**Heading**` line, and some
18
+ // sub-bullets are themselves bold-led (`- **Claim.** detail…`) — once a
19
+ // standalone heading is seen, `- **` lines in that section are sub-bullets,
20
+ // not entries, and must not render.
21
+ let sectionUsesStandaloneHeadings = false;
14
22
  for (const line of changelog.split('\n')) {
15
23
  const versionMatch = line.match(/^## (\d+\.\d+\.\d+)/);
16
24
  if (versionMatch) {
@@ -20,15 +28,27 @@ export function renderWhatsNew(changelog, fromVersion, toVersion) {
20
28
  inRelevantSection =
21
29
  compareVersions(currentVersion, fromVersion) > 0 &&
22
30
  compareVersions(currentVersion, toVersion) <= 0;
31
+ sectionUsesStandaloneHeadings = false;
23
32
  if (inRelevantSection) {
24
33
  out.push('');
25
34
  out.push(chalk.bold(`v${currentVersion}`));
26
35
  }
27
36
  continue;
28
37
  }
29
- // Only the bold headings — one bullet per feature/fix.
30
- if (inRelevantSection && line.startsWith('**') && line.endsWith('**')) {
38
+ // Only the entry headings — one bullet per feature/fix. Two formats exist
39
+ // across the changelog's history: the current single-line bullets
40
+ // (`- **Title.** verbose prose…`, heading kept, prose dropped) and the
41
+ // older standalone `**Heading**` lines with `-` sub-bullets beneath.
42
+ if (!inRelevantSection)
43
+ continue;
44
+ if (line.startsWith('**') && line.endsWith('**')) {
45
+ sectionUsesStandaloneHeadings = true;
31
46
  out.push(` ${chalk.cyan('•')} ${line.replace(/\*\*/g, '')}`);
47
+ continue;
48
+ }
49
+ const entryBullet = sectionUsesStandaloneHeadings ? null : line.match(/^- \*\*(.+?)\*\*/);
50
+ if (entryBullet) {
51
+ out.push(` ${chalk.cyan('•')} ${entryBullet[1].replace(/\*\*/g, '')}`);
32
52
  }
33
53
  }
34
54
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.51",
3
+ "version": "1.20.53",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,6 +24,7 @@
24
24
  "dist/**/*.js",
25
25
  "dist/**/*.d.ts",
26
26
  "dist/**/*.json",
27
+ "dist/bin/**",
27
28
  "dist/lib/secrets/Agents CLI.app/**",
28
29
  "dist/lib/menubar/MenubarHelper.app/**",
29
30
  "scripts/postinstall.js",
@@ -44,10 +45,10 @@
44
45
  "url": "https://github.com/phnx-labs/agents-cli/issues"
45
46
  },
46
47
  "scripts": {
47
- "build": "tsc && rm -rf 'dist/lib/secrets/AgentsKeychain.app' 'dist/lib/secrets/Agents CLI.app' 'dist/lib/menubar/MenubarHelper.app' && ([ -d 'bin/Agents CLI.app' ] && cp -R 'bin/Agents CLI.app' 'dist/lib/secrets/Agents CLI.app' || true) && ([ -d 'bin/MenubarHelper.app' ] && mkdir -p 'dist/lib/menubar' && cp -R 'bin/MenubarHelper.app' 'dist/lib/menubar/MenubarHelper.app' || true)",
48
+ "build": "tsc && rm -rf 'dist/lib/secrets/AgentsKeychain.app' 'dist/lib/secrets/Agents CLI.app' 'dist/lib/menubar/MenubarHelper.app' && ([ -d 'bin/Agents CLI.app' ] && cp -R 'bin/Agents CLI.app' 'dist/lib/secrets/Agents CLI.app' || true) && ([ -d 'bin/MenubarHelper.app' ] && mkdir -p 'dist/lib/menubar' && cp -R 'bin/MenubarHelper.app' 'dist/lib/menubar/MenubarHelper.app' || true) && ([ -f 'bin/agents-macos' ] && mkdir -p dist/bin && cp -f 'bin/agents-macos' 'dist/bin/agents' && chmod 755 'dist/bin/agents' || true)",
48
49
  "build:bin": "scripts/build-bin.sh",
49
50
  "prepare": "npm run build",
50
- "prepack": "cp ../../README.md README.md && scripts/verify-keychain-helper.sh && scripts/verify-menubar-helper.sh",
51
+ "prepack": "cp ../../README.md README.md && scripts/verify-keychain-helper.sh && scripts/verify-menubar-helper.sh && scripts/verify-cli-binary.sh",
51
52
  "postinstall": "node scripts/postinstall.js",
52
53
  "dev": "tsx src/index.ts",
53
54
  "start": "node dist/index.js",
@@ -16,9 +16,36 @@ const SHIMS_DIR = path.join(USER_DIR, '.cache', 'shims');
16
16
  // System repo lives inside the user repo (folded in v1.21). Legacy installs at
17
17
  // ~/.agents-system/ are migrated by src/lib/migrate.ts on first CLI invocation.
18
18
  const SYSTEM_DIR = path.join(USER_DIR, '.system');
19
- const AGENTS_BIN = fileURLToPath(new URL('../dist/index.js', import.meta.url));
19
+ const AGENTS_JS_ENTRYPOINT = fileURLToPath(new URL('../dist/index.js', import.meta.url));
20
+ // Signed + notarized standalone Mach-O, macOS only (scripts/sign-cli-binary.sh).
21
+ // Preferred over the node-shebang JS entrypoint on darwin: the unsigned JS shim
22
+ // is what EDR (CrowdStrike Falcon) flags when an editor child spawns it (#315).
23
+ const AGENTS_NATIVE_MACOS_BIN = fileURLToPath(new URL('../dist/bin/agents', import.meta.url));
24
+ const AGENTS_BIN = resolveAgentsBin();
20
25
  const INSTALL_HELPER_SCRIPT = fileURLToPath(new URL('./install-helper.js', import.meta.url));
21
26
 
27
+ function resolveAgentsBin() {
28
+ if (process.platform !== 'darwin') return AGENTS_JS_ENTRYPOINT;
29
+ try {
30
+ fs.accessSync(AGENTS_NATIVE_MACOS_BIN, fs.constants.X_OK);
31
+ } catch {
32
+ return AGENTS_JS_ENTRYPOINT; // no native binary in this install (dev build, arch without one)
33
+ }
34
+ // Real execution probe, not just a stat: catches a quarantined, EDR-blocked,
35
+ // or wrong-arch (Intel) binary at install time, where we can still fall back
36
+ // loudly, instead of breaking every later `agents` call.
37
+ const probe = spawnSync(AGENTS_NATIVE_MACOS_BIN, ['--version'], {
38
+ stdio: ['ignore', 'pipe', 'pipe'],
39
+ timeout: 15000,
40
+ encoding: 'utf-8',
41
+ });
42
+ if (probe.status === 0) return AGENTS_NATIVE_MACOS_BIN;
43
+ const why = probe.error ? probe.error.message : `exit status ${probe.status}`;
44
+ console.warn(` warning: the signed agents binary at ${AGENTS_NATIVE_MACOS_BIN} failed to run (${why}).`);
45
+ console.warn(` Falling back to the JS entrypoint; 'agents' still works but is not code-signed.`);
46
+ return AGENTS_JS_ENTRYPOINT;
47
+ }
48
+
22
49
  function installKeychainHelper() {
23
50
  if (process.platform !== 'darwin') return;
24
51
  if (!fs.existsSync(INSTALL_HELPER_SCRIPT)) return;
@@ -276,6 +303,7 @@ To enable version-aware shims, add this to your shell config:
276
303
  */
277
304
  async function ensureAgentsResolvablePosix() {
278
305
  if (process.env.CI || process.env.AGENTS_NO_HEAL === '1') return;
306
+ retargetManagedLinksToNativeBin();
279
307
  try {
280
308
  const { localBinDir, ensureLocalBinSymlink, loginShellResolves, dirOnLoginPath } =
281
309
  await import('../dist/lib/platform/posixpath.js');
@@ -314,6 +342,38 @@ async function ensureAgentsResolvablePosix() {
314
342
  }
315
343
  }
316
344
 
345
+ /**
346
+ * Upgrade path for #315: an earlier install symlinked ~/.local/bin/agents at
347
+ * the JS entrypoint. ensureLocalBinSymlink never repoints an existing link,
348
+ * and the loginShellResolves() early-return means ensureAgentsResolvablePosix
349
+ * would not even try - so without this, a machine healed before the signed
350
+ * binary existed keeps the unsigned JS shim forever. Repoint ONLY symlinks
351
+ * that resolve exactly to OUR dist/index.js; a dev build or foreign link is
352
+ * never touched.
353
+ */
354
+ function retargetManagedLinksToNativeBin() {
355
+ if (AGENTS_BIN === AGENTS_JS_ENTRYPOINT) return;
356
+ const binDir = path.join(HOME, '.local', 'bin');
357
+ // Compare realpaths: dist/index.js can be reached through path aliases
358
+ // (/var vs /private/var on macOS) depending on who created the link.
359
+ let want = AGENTS_JS_ENTRYPOINT;
360
+ try { want = fs.realpathSync(AGENTS_JS_ENTRYPOINT); } catch { /* keep the literal path */ }
361
+ for (const name of ['agents', 'ag']) {
362
+ const linkPath = path.join(binDir, name);
363
+ try {
364
+ const current = fs.readlinkSync(linkPath); // throws unless a symlink
365
+ let resolved = path.isAbsolute(current) ? current : path.resolve(binDir, current);
366
+ try { resolved = fs.realpathSync(resolved); } catch { /* dangling - compare as-is */ }
367
+ if (resolved !== want) continue;
368
+ fs.unlinkSync(linkPath);
369
+ fs.symlinkSync(AGENTS_BIN, linkPath);
370
+ console.log(` Repointed ${linkPath} at the signed agents binary.`);
371
+ } catch {
372
+ /* absent, unreadable, or not a symlink - nothing to retarget */
373
+ }
374
+ }
375
+ }
376
+
317
377
  /**
318
378
  * Self-heal long-running processes onto the just-installed code (macOS).
319
379
  *