@phnx-labs/agents-cli 1.20.57 → 1.20.59

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 (60) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/defaults.js +24 -0
  5. package/dist/commands/exec.js +29 -5
  6. package/dist/commands/output.d.ts +19 -0
  7. package/dist/commands/output.js +333 -0
  8. package/dist/commands/secrets.js +34 -25
  9. package/dist/commands/versions.js +11 -3
  10. package/dist/commands/view.js +19 -4
  11. package/dist/index.js +2 -1
  12. package/dist/lib/agents.d.ts +21 -0
  13. package/dist/lib/agents.js +42 -14
  14. package/dist/lib/daemon.d.ts +5 -5
  15. package/dist/lib/daemon.js +65 -17
  16. package/dist/lib/git.d.ts +9 -0
  17. package/dist/lib/git.js +12 -0
  18. package/dist/lib/hosts/dispatch.d.ts +21 -0
  19. package/dist/lib/hosts/dispatch.js +88 -5
  20. package/dist/lib/hosts/passthrough.js +1 -0
  21. package/dist/lib/mcp.js +1 -1
  22. package/dist/lib/output/git-output.d.ts +74 -0
  23. package/dist/lib/output/git-output.js +213 -0
  24. package/dist/lib/permissions.d.ts +31 -1
  25. package/dist/lib/permissions.js +210 -9
  26. package/dist/lib/project-root.d.ts +65 -0
  27. package/dist/lib/project-root.js +134 -0
  28. package/dist/lib/resources/mcp.js +1 -1
  29. package/dist/lib/resources/permissions.js +5 -1
  30. package/dist/lib/resources/skills.js +6 -1
  31. package/dist/lib/resources/types.d.ts +1 -1
  32. package/dist/lib/secrets/agent.d.ts +26 -23
  33. package/dist/lib/secrets/agent.js +196 -216
  34. package/dist/lib/secrets/remote.d.ts +7 -2
  35. package/dist/lib/secrets/remote.js +12 -10
  36. package/dist/lib/session/active.d.ts +3 -0
  37. package/dist/lib/session/active.js +1 -0
  38. package/dist/lib/session/db.d.ts +3 -0
  39. package/dist/lib/session/db.js +20 -4
  40. package/dist/lib/session/discover.d.ts +2 -0
  41. package/dist/lib/session/discover.js +40 -4
  42. package/dist/lib/session/parse.js +38 -15
  43. package/dist/lib/session/state.d.ts +4 -1
  44. package/dist/lib/session/state.js +18 -1
  45. package/dist/lib/session/types.d.ts +10 -0
  46. package/dist/lib/staleness/detectors/permissions.js +64 -1
  47. package/dist/lib/staleness/detectors/subagents.js +30 -0
  48. package/dist/lib/staleness/writers/commands.js +3 -3
  49. package/dist/lib/staleness/writers/subagents.js +13 -1
  50. package/dist/lib/startup/command-registry.d.ts +1 -0
  51. package/dist/lib/startup/command-registry.js +2 -0
  52. package/dist/lib/subagents.d.ts +23 -0
  53. package/dist/lib/subagents.js +161 -12
  54. package/dist/lib/teams/agents.d.ts +14 -0
  55. package/dist/lib/teams/agents.js +158 -22
  56. package/dist/lib/types.d.ts +13 -0
  57. package/dist/lib/versions.d.ts +39 -0
  58. package/dist/lib/versions.js +199 -12
  59. package/package.json +1 -1
  60. package/scripts/postinstall.js +26 -11
@@ -643,6 +643,12 @@ export interface Meta {
643
643
  registries?: Record<RegistryType, Record<string, RegistryConfig>>;
644
644
  versions?: Partial<Record<AgentId, Record<string, VersionResources>>>;
645
645
  source?: string;
646
+ /**
647
+ * Projects root for the `agents run --project <slug>` shorthand, e.g.
648
+ * `~/src/github.com/<user>`. Auto-inferred from the repo you launch inside and
649
+ * cached here; stored home-relative (`~/…`) so it resolves on remote hosts too.
650
+ */
651
+ projectRoot?: string;
646
652
  /**
647
653
  * Extra DotAgent repos merged after ~/.agents/. Managed clones live as peer
648
654
  * dirs at ~/.agents-<alias>/; user-owned repos can point at arbitrary paths
@@ -771,6 +777,13 @@ export interface ClaudePermissions {
771
777
  additionalDirectories?: string[];
772
778
  };
773
779
  }
780
+ /** Cursor CLI native format in ~/.cursor/cli-config.json (Shell/Read/Write/WebFetch/Mcp). */
781
+ export interface CursorPermissions {
782
+ permissions: {
783
+ allow: string[];
784
+ deny: string[];
785
+ };
786
+ }
774
787
  /** OpenCode's native permission format (per-command allow/deny/ask). */
775
788
  export interface OpenCodePermissions {
776
789
  permission: {
@@ -114,6 +114,41 @@ export declare function getVersionDir(agent: AgentId, version: string): string;
114
114
  * Get the binary path for a specific agent version.
115
115
  */
116
116
  export declare function getBinaryPath(agent: AgentId, version: string): string;
117
+ /**
118
+ * Does this agent resolve to ONE global binary that is the same file regardless
119
+ * of the `version` argument? (droid → always `~/.local/bin/droid`.) Computed
120
+ * generically by probing `getBinaryPath` with two distinct versions rather than
121
+ * hardcoding an agent id, so it stays correct if another global-binary agent is
122
+ * added.
123
+ *
124
+ * This is the narrower cousin of `isSelfUpdatingAgent`: every global-binary
125
+ * agent is self-updating, but grok is self-updating WITHOUT a global binary — it
126
+ * stores a real per-version binary copy under each version-home
127
+ * (`versions/grok/<v>/home/.grok/downloads/grok-<v>`), so its version-homes are
128
+ * genuinely distinct and must NOT be collapsed. Gate the single-binary
129
+ * collapse/live-version logic on THIS predicate; gate pin-refusal / "switch
130
+ * profile" copy on `isSelfUpdatingAgent`.
131
+ */
132
+ export declare function isGlobalBinaryAgent(agent: AgentId): boolean;
133
+ /** Drop the live-version cache (call after an install/remove that changes the
134
+ * running binary, e.g. `agents add droid@latest`). */
135
+ export declare function invalidateLiveVersionCache(agent?: AgentId): void;
136
+ /**
137
+ * Resolve the version the ONE globally-installed binary actually reports via
138
+ * `<cli> --version`, cached for {@link LIVE_VERSION_TTL_MS}. For a self-updating
139
+ * global-binary agent (droid) this is the single source of truth for "which
140
+ * version is installed" — the on-disk version-dir NAMES are just stale labels
141
+ * left behind by successive `agents add`/self-update cycles. Returns null when
142
+ * the binary isn't on PATH or the probe fails.
143
+ */
144
+ export declare function getLiveVersion(agent: AgentId): Promise<string | null>;
145
+ /**
146
+ * Synchronous, non-blocking read of the live-version cache — returns the value
147
+ * only if a recent {@link getLiveVersion} call already warmed it, else null.
148
+ * `listInstalledVersions` is sync and must not shell out, so it prefers this
149
+ * warm value (accurate) and otherwise falls back to the newest on-disk dir.
150
+ */
151
+ export declare function getCachedLiveVersion(agent: AgentId): string | null;
117
152
  /**
118
153
  * Get the isolated HOME directory for a specific agent version.
119
154
  * Each version has its own config isolation (like jobs sandbox).
@@ -155,6 +190,10 @@ export declare function isOldestInstalled(agent: AgentId): Promise<{
155
190
  export declare function invalidateInstalledVersionsCache(agent?: AgentId): void;
156
191
  /**
157
192
  * List all installed versions for an agent (cached by versions-dir mtime).
193
+ *
194
+ * For a self-updating global-binary agent (droid) every version dir resolves to
195
+ * the SAME binary, so this collapses them to a single canonical entry — one
196
+ * install, one row in `agents view`, never the phantom set of semver dir names.
158
197
  */
159
198
  export declare function listInstalledVersions(agent: AgentId): string[];
160
199
  /**
@@ -28,7 +28,7 @@ import { listResources } from './resources.js';
28
28
  // (single source of truth). Re-exported below so existing importers of
29
29
  // `compareVersions` from './versions.js' keep working.
30
30
  import { VERSION_RE, compareVersions } from './agent-spec/primitives.js';
31
- import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath } from './agents.js';
31
+ import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath, isSelfUpdatingAgent } from './agents.js';
32
32
  import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
33
33
  import { parseMcpServerConfig } from './mcp.js';
34
34
  import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
@@ -835,6 +835,65 @@ export function getBinaryPath(agent, version) {
835
835
  const versionDir = getVersionDir(agent, version);
836
836
  return path.join(versionDir, 'node_modules', '.bin', agentConfig.cliCommand);
837
837
  }
838
+ /**
839
+ * Does this agent resolve to ONE global binary that is the same file regardless
840
+ * of the `version` argument? (droid → always `~/.local/bin/droid`.) Computed
841
+ * generically by probing `getBinaryPath` with two distinct versions rather than
842
+ * hardcoding an agent id, so it stays correct if another global-binary agent is
843
+ * added.
844
+ *
845
+ * This is the narrower cousin of `isSelfUpdatingAgent`: every global-binary
846
+ * agent is self-updating, but grok is self-updating WITHOUT a global binary — it
847
+ * stores a real per-version binary copy under each version-home
848
+ * (`versions/grok/<v>/home/.grok/downloads/grok-<v>`), so its version-homes are
849
+ * genuinely distinct and must NOT be collapsed. Gate the single-binary
850
+ * collapse/live-version logic on THIS predicate; gate pin-refusal / "switch
851
+ * profile" copy on `isSelfUpdatingAgent`.
852
+ */
853
+ export function isGlobalBinaryAgent(agent) {
854
+ return getBinaryPath(agent, '0.0.0-probe-a') === getBinaryPath(agent, '0.0.0-probe-b');
855
+ }
856
+ // Live-version cache for self-updating global binaries. `<cli> --version` is a
857
+ // ~real shell-out, so hold the result briefly: the same `agents view` render
858
+ // asks for it from both listInstalledVersions (sync) and the label path (async).
859
+ const LIVE_VERSION_TTL_MS = 5000;
860
+ const liveVersionCache = new Map();
861
+ /** Drop the live-version cache (call after an install/remove that changes the
862
+ * running binary, e.g. `agents add droid@latest`). */
863
+ export function invalidateLiveVersionCache(agent) {
864
+ if (agent)
865
+ liveVersionCache.delete(agent);
866
+ else
867
+ liveVersionCache.clear();
868
+ }
869
+ /**
870
+ * Resolve the version the ONE globally-installed binary actually reports via
871
+ * `<cli> --version`, cached for {@link LIVE_VERSION_TTL_MS}. For a self-updating
872
+ * global-binary agent (droid) this is the single source of truth for "which
873
+ * version is installed" — the on-disk version-dir NAMES are just stale labels
874
+ * left behind by successive `agents add`/self-update cycles. Returns null when
875
+ * the binary isn't on PATH or the probe fails.
876
+ */
877
+ export async function getLiveVersion(agent) {
878
+ const cached = liveVersionCache.get(agent);
879
+ if (cached && Date.now() - cached.at < LIVE_VERSION_TTL_MS)
880
+ return cached.version;
881
+ const version = await getCliVersionFromPath(agent);
882
+ liveVersionCache.set(agent, { at: Date.now(), version });
883
+ return version;
884
+ }
885
+ /**
886
+ * Synchronous, non-blocking read of the live-version cache — returns the value
887
+ * only if a recent {@link getLiveVersion} call already warmed it, else null.
888
+ * `listInstalledVersions` is sync and must not shell out, so it prefers this
889
+ * warm value (accurate) and otherwise falls back to the newest on-disk dir.
890
+ */
891
+ export function getCachedLiveVersion(agent) {
892
+ const cached = liveVersionCache.get(agent);
893
+ if (cached && Date.now() - cached.at < LIVE_VERSION_TTL_MS)
894
+ return cached.version;
895
+ return null;
896
+ }
838
897
  /**
839
898
  * Get the isolated HOME directory for a specific agent version.
840
899
  * Each version has its own config isolation (like jobs sandbox).
@@ -970,8 +1029,43 @@ export function invalidateInstalledVersionsCache(agent) {
970
1029
  else
971
1030
  installedVersionsCache.clear();
972
1031
  }
1032
+ /**
1033
+ * Choose the single canonical version-dir to represent a self-updating
1034
+ * global-binary agent (droid). All its version dirs map to ONE binary, so
1035
+ * exactly one is real; the rest are stale labels. Prefer, in order: the dir the
1036
+ * live config symlink points at (what actually runs), the recorded global
1037
+ * default, the live `--version` (when the cache is warm), else the newest dir.
1038
+ * `versions` MUST be sorted ascending. Callers guarantee it is non-empty.
1039
+ */
1040
+ function pickCanonicalGlobalBinaryVersion(agent, versions) {
1041
+ const symlinkVersion = getConfigSymlinkVersion(agent);
1042
+ if (symlinkVersion && versions.includes(symlinkVersion))
1043
+ return symlinkVersion;
1044
+ const globalDefault = getGlobalDefault(agent);
1045
+ if (globalDefault && versions.includes(globalDefault))
1046
+ return globalDefault;
1047
+ const live = getCachedLiveVersion(agent);
1048
+ if (live && versions.includes(live))
1049
+ return live;
1050
+ return versions[versions.length - 1];
1051
+ }
1052
+ /**
1053
+ * Collapse a global-binary agent's phantom version dirs to the single canonical
1054
+ * one (see {@link pickCanonicalGlobalBinaryVersion}). No-op for npm-packaged and
1055
+ * per-version agents (claude/codex/grok/…), whose version dirs are genuinely
1056
+ * distinct installs.
1057
+ */
1058
+ function collapseGlobalBinaryVersions(agent, versions) {
1059
+ if (!isGlobalBinaryAgent(agent) || versions.length === 0)
1060
+ return versions;
1061
+ return [pickCanonicalGlobalBinaryVersion(agent, versions)];
1062
+ }
973
1063
  /**
974
1064
  * List all installed versions for an agent (cached by versions-dir mtime).
1065
+ *
1066
+ * For a self-updating global-binary agent (droid) every version dir resolves to
1067
+ * the SAME binary, so this collapses them to a single canonical entry — one
1068
+ * install, one row in `agents view`, never the phantom set of semver dir names.
975
1069
  */
976
1070
  export function listInstalledVersions(agent) {
977
1071
  const agentVersionsDir = path.join(getVersionsDir(), agent);
@@ -985,7 +1079,9 @@ export function listInstalledVersions(agent) {
985
1079
  }
986
1080
  const cached = installedVersionsCache.get(agent);
987
1081
  if (cached && cached.stamp === stamp) {
988
- return cached.versions;
1082
+ // Collapse is applied per-call (not cached): it depends on the live-version
1083
+ // cache + config symlink, which can change without the versions-dir mtime.
1084
+ return collapseGlobalBinaryVersions(agent, cached.versions);
989
1085
  }
990
1086
  const entries = fs.readdirSync(agentVersionsDir, { withFileTypes: true });
991
1087
  const versions = [];
@@ -1001,7 +1097,7 @@ export function listInstalledVersions(agent) {
1001
1097
  }
1002
1098
  versions.sort(compareVersions);
1003
1099
  installedVersionsCache.set(agent, { stamp, versions });
1004
- return versions;
1100
+ return collapseGlobalBinaryVersions(agent, versions);
1005
1101
  }
1006
1102
  /**
1007
1103
  * List every version directory for an agent, including ones missing the
@@ -1067,18 +1163,31 @@ export async function installVersion(agent, version, onProgress, opts) {
1067
1163
  if (!agentConfig.installScript) {
1068
1164
  return { success: false, installedVersion: version, error: 'Agent has no npm package' };
1069
1165
  }
1070
- if (version !== 'latest' && !agentConfig.installScript.includes('VERSION')) {
1071
- return {
1072
- success: false,
1073
- installedVersion: version,
1074
- error: `${agentConfig.name} installer does not support version-pinned installs. Use ${agent}@latest.`,
1075
- };
1166
+ // A self-updating agent (droid, grok, …) is a single global binary whose
1167
+ // installer only ever fetches the CURRENT release — there is no semver to
1168
+ // pin. Rather than hard-refuse `<agent>@1.2.3` (the old behavior):
1169
+ // - if the binary is already installed, a pin is a no-op — it self-updates
1170
+ // in place, so skip the installer and just refresh our bookkeeping;
1171
+ // - otherwise redirect the pin to a current-release install.
1172
+ let runInstaller = true;
1173
+ if (version !== 'latest' && isSelfUpdatingAgent(agent)) {
1174
+ const liveVersion = await getLiveVersion(agent);
1175
+ if (liveVersion) {
1176
+ onProgress?.(`${agentConfig.name} is a single self-updating binary — @${version} maps to the already-installed current release (${liveVersion}); nothing to install.`);
1177
+ runInstaller = false;
1178
+ }
1179
+ else {
1180
+ onProgress?.(`${agentConfig.name} is a single self-updating binary with no pinnable versions — installing the current release (ignoring @${version}).`);
1181
+ }
1182
+ version = 'latest';
1076
1183
  }
1077
1184
  let installedVersion = version;
1078
1185
  try {
1079
- const script = agentConfig.installScript.replaceAll('VERSION', version);
1080
- onProgress?.(`Installing ${agentConfig.name}@${version} via official installer...`);
1081
- await execAsync(script, { timeout: 120000 });
1186
+ if (runInstaller) {
1187
+ const script = agentConfig.installScript.replaceAll('VERSION', version);
1188
+ onProgress?.(`Installing ${agentConfig.name}@${version} via official installer...`);
1189
+ await execAsync(script, { timeout: 120000 });
1190
+ }
1082
1191
  if (version === 'latest') {
1083
1192
  installedVersion = await getCliVersionFromPath(agent) || version;
1084
1193
  // Fold any stale literal `latest` dir from an earlier probe-failed
@@ -1125,6 +1234,9 @@ export async function installVersion(agent, version, onProgress, opts) {
1125
1234
  correctly reports it uninstalled. */
1126
1235
  }
1127
1236
  createVersionedAlias(agent, installedVersion);
1237
+ // The self-updating binary just changed on disk — drop the cached
1238
+ // `--version` so `agents view` reflects the freshly-installed release.
1239
+ invalidateLiveVersionCache(agent);
1128
1240
  emit('version.install', { agent, version: installedVersion });
1129
1241
  return { success: true, installedVersion };
1130
1242
  }
@@ -1319,6 +1431,13 @@ function removeInstallArtifacts(versionDir) {
1319
1431
  * renaming that dir would dangle the live symlink.
1320
1432
  */
1321
1433
  export async function reconcileStaleLatestForAgent(agent) {
1434
+ // Global-binary agents (droid) can accumulate MANY stale semver dirs — not
1435
+ // just a literal `latest` — because every `agents add` after an in-place
1436
+ // self-update creates a fresh dir for the same one binary. Fold them all.
1437
+ if (isGlobalBinaryAgent(agent)) {
1438
+ await reconcileGlobalBinaryVersions(agent);
1439
+ return;
1440
+ }
1322
1441
  if (!fs.existsSync(getVersionDir(agent, 'latest')))
1323
1442
  return;
1324
1443
  if (getConfigSymlinkVersion(agent) === 'latest')
@@ -1328,6 +1447,74 @@ export async function reconcileStaleLatestForAgent(agent) {
1328
1447
  await reconcileStaleLatestDir(agent, concrete);
1329
1448
  }
1330
1449
  }
1450
+ /**
1451
+ * Fold every stale version-dir of a global-binary agent (droid) into the single
1452
+ * canonical survivor. All its dirs point at ONE binary, so the extras — left by
1453
+ * successive `agents add` + in-place self-updates, and by probe-failed installs
1454
+ * that created a literal `latest` dir — are phantom labels. Soft-delete each
1455
+ * non-survivor dir (its `home/` stays recoverable via `agents restore`) so disk
1456
+ * converges to a single install matching what `agents view` shows.
1457
+ *
1458
+ * Survivor selection matches listInstalledVersions' canonical choice
1459
+ * (config-symlink target → global default → live version → newest). The survivor
1460
+ * is NOT renamed: droid's ~/.factory symlink points inside its home, and the
1461
+ * live version label is surfaced by the view renderer via getLiveVersion.
1462
+ */
1463
+ async function reconcileGlobalBinaryVersions(agent) {
1464
+ // Step 1 — preserve the original literal-`latest` reconcile: a probe-failed
1465
+ // install leaves a `versions/<agent>/latest/` dir; fold it onto the concrete
1466
+ // live version (rename if that dir is absent, else trash). Skipped while the
1467
+ // config symlink still points at `latest` (renaming would dangle it).
1468
+ if (fs.existsSync(getVersionDir(agent, 'latest')) && getConfigSymlinkVersion(agent) !== 'latest') {
1469
+ const concrete = await getCliVersionFromPath(agent);
1470
+ if (concrete && concrete !== 'latest') {
1471
+ await reconcileStaleLatestDir(agent, concrete);
1472
+ }
1473
+ }
1474
+ // Step 2 — collapse any remaining phantom SEMVER dirs (successive `agents add`
1475
+ // after in-place self-updates) into a single survivor.
1476
+ const agentVersionsDir = path.join(getVersionsDir(), agent);
1477
+ let entries;
1478
+ try {
1479
+ entries = fs.readdirSync(agentVersionsDir, { withFileTypes: true });
1480
+ }
1481
+ catch {
1482
+ return;
1483
+ }
1484
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort(compareVersions);
1485
+ if (dirs.length <= 1)
1486
+ return;
1487
+ // Warm the live-version cache so the survivor pick (and later view labels) can
1488
+ // prefer the version the binary actually reports.
1489
+ await getLiveVersion(agent);
1490
+ const survivor = pickCanonicalGlobalBinaryVersion(agent, dirs);
1491
+ const symlinkVersion = getConfigSymlinkVersion(agent);
1492
+ let foldedAny = false;
1493
+ for (const version of dirs) {
1494
+ if (version === survivor)
1495
+ continue;
1496
+ // Never trash the dir the live config symlink points at — that would dangle
1497
+ // the symlink. pickCanonical already prefers it as survivor; this guards the
1498
+ // rare mismatch.
1499
+ if (version === symlinkVersion)
1500
+ continue;
1501
+ const staleDir = getVersionDir(agent, version);
1502
+ const trashPath = softDeleteVersionDir(agent, version);
1503
+ if (trashPath) {
1504
+ const { updateSessionFilePaths } = await import('./session/db.js');
1505
+ updateSessionFilePaths(staleDir, trashPath);
1506
+ foldedAny = true;
1507
+ }
1508
+ }
1509
+ if (foldedAny) {
1510
+ invalidateInstalledVersionsCache(agent);
1511
+ // Keep the recorded default pointing at a dir that still exists on disk.
1512
+ const def = getGlobalDefault(agent);
1513
+ if (!def || !fs.existsSync(getVersionDir(agent, def))) {
1514
+ setGlobalDefault(agent, survivor);
1515
+ }
1516
+ }
1517
+ }
1331
1518
  export async function reconcileStaleLatestDir(agent, installedVersion) {
1332
1519
  if (installedVersion === 'latest')
1333
1520
  return 'none';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.57",
3
+ "version": "1.20.59",
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",
@@ -386,23 +386,38 @@ function retargetManagedLinksToNativeBin() {
386
386
  async function healLongRunningProcesses() {
387
387
  if (process.platform !== 'darwin') return;
388
388
  if (process.env.CI || process.env.AGENTS_NO_HEAL === '1') return;
389
+
390
+ // Retire the legacy standalone secrets-agent service FIRST (#416 step 2) so
391
+ // that when the daemon (re)starts below its coexistence guard sees no
392
+ // standalone broker and hosts the broker socket itself. No-op if no legacy
393
+ // plist is present; never blocks.
394
+ let retiredLegacyBroker = false;
395
+ try {
396
+ const a = await import('../dist/lib/secrets/agent.js');
397
+ if (a.secretsAgentServiceInstalled?.()) {
398
+ a.retireLegacySecretsAgentService?.();
399
+ retiredLegacyBroker = true;
400
+ console.log(' Retired the standalone secrets-agent service — the daemon now hosts the broker.');
401
+ }
402
+ } catch { /* best effort */ }
403
+
389
404
  // Routines daemon: restart so it reloads new code (e.g. picks up keychain
390
- // read-memoization / broker fast-path that a stale daemon wouldn't have).
405
+ // read-memoization / broker fast-path that a stale daemon wouldn't have) and
406
+ // (re)hosts the broker socket now the legacy service is gone. If it wasn't
407
+ // running but we just retired a broker the user relied on, start it so the
408
+ // socket has a host.
391
409
  try {
392
410
  const d = await import('../dist/lib/daemon.js');
393
411
  if (d.isDaemonRunning?.()) {
394
412
  d.stopDaemon?.();
395
- d.startDaemon?.();
413
+ // This lifecycle script is process.argv[1] while npm is installing. Pin
414
+ // the restart to the installed CLI entrypoint so the service manifest
415
+ // never records scripts/postinstall.js as the daemon command.
416
+ d.startDaemon?.(AGENTS_BIN);
396
417
  console.log(' Restarted the routines daemon onto this version.');
397
- }
398
- } catch { /* best effort */ }
399
- // Persistent secrets-agent broker: kickstart so launchd relaunches it on the
400
- // new code. No-op if the service isn't installed; never blocks.
401
- try {
402
- const a = await import('../dist/lib/secrets/agent.js');
403
- if (a.secretsAgentServiceInstalled?.()) {
404
- a.kickstartSecretsAgentService?.();
405
- console.log(' Reloaded the secrets-agent service onto this version.');
418
+ } else if (retiredLegacyBroker) {
419
+ d.startDaemon?.(AGENTS_BIN);
420
+ console.log(' Started the daemon to host the secrets broker.');
406
421
  }
407
422
  } catch { /* best effort */ }
408
423
  }