@phnx-labs/agents-cli 1.22.85 → 1.22.87

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/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.87
4
+
5
+ - **Workers now materialize a slot for every registered Claude account, not only
6
+ the ones added through the v2 `accounts add` flow (PHNX-3940).** The daemon's
7
+ slot reconciliation skipped any account row without a `workerCredential` field —
8
+ every account registered before the v2 model — on the assumption that a legacy
9
+ row "resolves its token at spawn". That holds for `agents run claude#<name>` but
10
+ not for the interactive picker, so on a worker whose `auth` bundle already held
11
+ all eight setup-tokens `agents run claude --interactive --device auto` listed the
12
+ installed version labels, showed most of them `logged out`, and launching one put
13
+ a Claude login screen on a headless box. `reconcileLocalWorkerSlots` now resolves
14
+ each row through the same `reservedSyncTargets` the push plan uses (a v2 row's
15
+ reserved `__claude__` key, a legacy row's email-keyed `auth` token), so every
16
+ account whose token is on the box gets a `durable` slot with a 0600
17
+ `.oauth_token` and the seeded identity the adapter injects from. Two related
18
+ fixes ride along: the credential publisher is now a ready **headed** device
19
+ (`electPublisher`, headed-first then by name) instead of the alphabetically first
20
+ ready box, which had made a worker the fleet's source of truth; and slot
21
+ reconciliation runs first in the `auth-sync` tick, so a failed shared-state git
22
+ exchange no longer postpones local provisioning. Source:
23
+ `cli/src/lib/secrets/reserved-sync.ts`, `cli/src/lib/daemon/auth-sync-service.ts`.
24
+
25
+ - **`agents secrets` no longer recurses into the legacy `secrets` shim after an
26
+ in-place upgrade (PHNX-3989).** Boxes that ran agents-cli before the standalone
27
+ `secrets` engine still carry `~/.agents/.cache/shims/secrets`, a command shim that
28
+ `exec`s `agents secrets`. It sits first on PATH and survives an upgrade because its
29
+ baked entrypoint still exists, and 1.22.85 resolved the standalone with a plain
30
+ `which secrets` — so on every such box `agents secrets …` (and the daemon's
31
+ reserved-store sync, which uses the same resolver) re-entered itself through the
32
+ shim without bound. The resolver now skips agents-cli's own shims dir when looking
33
+ for the standalone (`SECRETS_BIN` still wins; a miss still fails loud with the
34
+ install command), and the self-heal shim pass removes that legacy `secrets` shim
35
+ even though its target install is alive, since it can only recurse. The package's
36
+ postinstall — which used to write that very shim on every install, so an upgrade
37
+ kept bringing it back — no longer writes a `secrets` alias at all and removes one
38
+ a previous install left behind; the bare-command aliases are now `sessions`,
39
+ `browser`, `pty`, and `teams`. Install the standalone with
40
+ `npm i -g @phnx-labs/secrets-cli` if `agents secrets` reports it missing.
41
+
3
42
  ## 1.22.85
4
43
 
5
44
  - **`agents run codex` marks an exhausted account so rotation stops re-picking it
@@ -8,9 +8,13 @@ import { setHelpSections } from '../lib/help.js';
8
8
  function aliasesFile() {
9
9
  return path.join(getUserAgentsDir(), 'aliases.json');
10
10
  }
11
- /** Names that would clobber an agent CLI shim or the `agents` binary itself. */
11
+ /**
12
+ * Names that would clobber an agent CLI shim, the `agents` binary itself, or the
13
+ * standalone `secrets` CLI (PHNX-3989): the shims dir sits first on PATH, so an
14
+ * alias named `secrets` would shadow `@phnx-labs/secrets-cli` for every caller.
15
+ */
12
16
  function reservedNames() {
13
- const reserved = new Set(['agents']);
17
+ const reserved = new Set(['agents', 'secrets']);
14
18
  for (const id of ALL_AGENT_IDS)
15
19
  reserved.add(AGENTS[id].cliCommand);
16
20
  return reserved;
@@ -85,7 +89,7 @@ export function registerAliasCommand(setupCmd) {
85
89
  process.exit(1);
86
90
  }
87
91
  if (reservedNames().has(name)) {
88
- console.error(chalk.red(`"${name}" is reserved (collides with an agent CLI or the agents binary).`));
92
+ console.error(chalk.red(`"${name}" is reserved (collides with an agent CLI, the agents binary, or the standalone secrets CLI).`));
89
93
  process.exit(1);
90
94
  }
91
95
  const target = shimPath(name);
@@ -127,6 +127,12 @@ export function findInPath(command, options = {}) {
127
127
  const stat = fs.statSync(full);
128
128
  if (!stat.isFile())
129
129
  continue;
130
+ // A shell only runs an executable file; `which` skips a mode-644 namesake and
131
+ // so must this, or a stray non-executable `secrets`/`claude` file earlier on
132
+ // PATH would shadow the real binary further down. On win32 the mode bit is
133
+ // meaningless (PATHEXT decides), so the check is POSIX-only.
134
+ if (process.platform !== 'win32')
135
+ fs.accessSync(full, fs.constants.X_OK);
130
136
  const native = resolveNativeBinaryPath(command, full, options);
131
137
  if (native)
132
138
  return native;
@@ -22,7 +22,24 @@ export class AuthSyncService extends BasePeriodicService {
22
22
  // Nothing to release — the supervisor's timer teardown is the only cleanup.
23
23
  }
24
24
  async onTick(ctx) {
25
- const { publishReservedAuthVerdict, syncReservedAuthBundle } = await import('../secrets-policy.js');
25
+ const { publishReservedAuthVerdict, reconcileLocalWorkerSlots, syncReservedAuthBundle, syncReservedStores, } = await import('../secrets-policy.js');
26
+ // Worker-side slot materialization FIRST (PHNX-3940 T6): for each registered
27
+ // account whose durable key is already on this box, create the HOME-shaped
28
+ // slot the picker and spawn read. It touches only local state — the registry
29
+ // copy and the file-backed store — so it never waits on the git exchange
30
+ // below. It used to sit after `if (!transport.success) return`, so a single
31
+ // `git rebase timed out` postponed every slot on the box by another tick.
32
+ // Self-gated on device role: a headed box returns immediately.
33
+ try {
34
+ const slots = reconcileLocalWorkerSlots();
35
+ if (slots.provisioned.length > 0)
36
+ ctx.log('INFO', `auth-sync: provisioned worker slot(s) for ${slots.provisioned.join(', ')}`);
37
+ for (const err of slots.errors)
38
+ ctx.log('WARN', `auth-sync: worker slot ${err.accountId}: ${err.message}`);
39
+ }
40
+ catch (err) {
41
+ ctx.log('WARN', `auth-sync: worker slot reconcile: ${err.message}`);
42
+ }
26
43
  const published = await publishReservedAuthVerdict();
27
44
  if (published.error)
28
45
  ctx.log('WARN', `auth-sync: verdict: ${published.error}`);
@@ -44,13 +61,10 @@ export class AuthSyncService extends BasePeriodicService {
44
61
  for (const err of result.errors) {
45
62
  ctx.log('WARN', `auth-sync: ${err.device}: ${err.message}`);
46
63
  }
47
- // Generalized per-account, per-key, per-role reserved-store sync (PHNX-3940
64
+ // Generalized per-account, per-key, per-role reserved-store push (PHNX-3940
48
65
  // T6). On the elected headed publisher this pushes every portable account's
49
- // reserved `__<harness>__` store to the worker peers missing it; on a worker
50
- // it materializes a slot for each account whose durable key has landed. Each
51
- // self-gates on device role, so exactly one arm acts per box. The push is the
52
- // only transport — provisioning writes only locally (invariant 1).
53
- const { syncReservedStores, reconcileLocalWorkerSlots } = await import('../secrets-policy.js');
66
+ // reserved `__<harness>__` store to the worker peers missing it. The push is
67
+ // the only transport — provisioning (above) writes only locally (invariant 1).
54
68
  try {
55
69
  const stores = await syncReservedStores();
56
70
  for (const p of stores.pushed)
@@ -61,15 +75,5 @@ export class AuthSyncService extends BasePeriodicService {
61
75
  catch (err) {
62
76
  ctx.log('WARN', `auth-sync: reserved-store sync: ${err.message}`);
63
77
  }
64
- try {
65
- const slots = reconcileLocalWorkerSlots();
66
- if (slots.provisioned.length > 0)
67
- ctx.log('INFO', `auth-sync: provisioned worker slot(s) for ${slots.provisioned.join(', ')}`);
68
- for (const err of slots.errors)
69
- ctx.log('WARN', `auth-sync: worker slot ${err.accountId}: ${err.message}`);
70
- }
71
- catch (err) {
72
- ctx.log('WARN', `auth-sync: worker slot reconcile: ${err.message}`);
73
- }
74
78
  }
75
79
  }
@@ -358,10 +358,12 @@ export declare function listShimFileNames(): string[];
358
358
  /**
359
359
  * Prune a stale, orphaned shim: one that is NOT a managed agent shim and NOT a user
360
360
  * alias, whose baked `AGENTS_BIN` points at an install that no longer exists. These
361
- * are legacy `exec "$AGENTS_BIN" <cmd>` command shims (browser/secrets/sessions/…)
362
- * left behind by a removed install — the current source never generates them, and
363
- * they either die with `exit 127` or shadow the real package bin on PATH. Only
364
- * removed when the baked target is gone, so a working shim is never touched.
361
+ * are `exec "$AGENTS_BIN" <cmd>` command shims (browser/sessions/pty/teams) that
362
+ * `scripts/postinstall.js` writes for the LIVE install; one baked at a removed
363
+ * install either dies with `exit 127` or shadows the real package bin on PATH.
364
+ * Only removed when the baked target is gone, so a working shim is never touched —
365
+ * except the `LEGACY_SHIMS_ALWAYS_PRUNED` set, which cannot work at all (and which
366
+ * postinstall no longer writes).
365
367
  * Returns true if removed.
366
368
  */
367
369
  export declare function pruneOrphanedCommandShim(fileName: string): boolean;
@@ -2165,13 +2165,25 @@ export function listShimFileNames() {
2165
2165
  return [];
2166
2166
  }
2167
2167
  }
2168
+ /**
2169
+ * Legacy command shims that are wrong even when their baked install is alive.
2170
+ * `secrets`: the shim `exec`s `agents secrets`, which since PHNX-3989 is a
2171
+ * passthrough to the STANDALONE `secrets` binary — so a `secrets` shim that
2172
+ * re-enters `agents secrets` is a recursion, never a working command. It sits
2173
+ * first on PATH and shadows the real `@phnx-labs/secrets-cli` bin, so it is
2174
+ * pruned unconditionally (the liveness check below is what kept it alive across
2175
+ * every in-place upgrade from 1.22.84).
2176
+ */
2177
+ const LEGACY_SHIMS_ALWAYS_PRUNED = new Set(['secrets']);
2168
2178
  /**
2169
2179
  * Prune a stale, orphaned shim: one that is NOT a managed agent shim and NOT a user
2170
2180
  * alias, whose baked `AGENTS_BIN` points at an install that no longer exists. These
2171
- * are legacy `exec "$AGENTS_BIN" <cmd>` command shims (browser/secrets/sessions/…)
2172
- * left behind by a removed install — the current source never generates them, and
2173
- * they either die with `exit 127` or shadow the real package bin on PATH. Only
2174
- * removed when the baked target is gone, so a working shim is never touched.
2181
+ * are `exec "$AGENTS_BIN" <cmd>` command shims (browser/sessions/pty/teams) that
2182
+ * `scripts/postinstall.js` writes for the LIVE install; one baked at a removed
2183
+ * install either dies with `exit 127` or shadows the real package bin on PATH.
2184
+ * Only removed when the baked target is gone, so a working shim is never touched —
2185
+ * except the `LEGACY_SHIMS_ALWAYS_PRUNED` set, which cannot work at all (and which
2186
+ * postinstall no longer writes).
2175
2187
  * Returns true if removed.
2176
2188
  */
2177
2189
  export function pruneOrphanedCommandShim(fileName) {
@@ -2192,7 +2204,7 @@ export function pruneOrphanedCommandShim(fileName) {
2192
2204
  const bin = readAgentsBinFromShim(shimPath);
2193
2205
  if (!bin)
2194
2206
  return false; // not an AGENTS_BIN-baked shim
2195
- if (fs.existsSync(bin))
2207
+ if (fs.existsSync(bin) && !LEGACY_SHIMS_ALWAYS_PRUNED.has(fileName))
2196
2208
  return false; // its install is still alive — leave it
2197
2209
  try {
2198
2210
  fs.rmSync(shimPath);
@@ -56,8 +56,16 @@ export declare function parseBundleValue(raw: BundleValue): {
56
56
  };
57
57
  /**
58
58
  * Resolve the standalone `secrets` executable: `$SECRETS_BIN` if set, else the
59
- * `secrets` command on PATH. Cached for the process. A miss throws with install
60
- * guidance — there is NO fallback to the embedded engine (DIST-1).
59
+ * `secrets` command on PATH — resolved with `findInPath`, which skips agents-cli's
60
+ * own shims dir. That skip is load-bearing, not tidiness: before PHNX-3989 the
61
+ * shims dir carried a `secrets` command shim that `exec`s `agents secrets`, it
62
+ * sits FIRST on PATH, and it survives an in-place upgrade (its baked entrypoint
63
+ * still exists). A plain `which secrets` returned that shim, so `agents secrets`
64
+ * → shim → `agents secrets` → … spawned an unbounded process chain on every box
65
+ * upgraded from 1.22.84 (the whole fleet, via R5 auto-update). A `secrets` inside
66
+ * our shims dir can never be the standalone, so it is not a candidate at all.
67
+ * Cached for the process. A miss throws with install guidance — there is NO
68
+ * fallback to the embedded engine (DIST-1).
61
69
  */
62
70
  export declare function resolveSecretsBin(): string;
63
71
  /**
@@ -45,7 +45,7 @@ import { spawn, spawnSync } from 'node:child_process';
45
45
  import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
46
46
  import { tmpdir } from 'node:os';
47
47
  import { join } from 'node:path';
48
- import { findExecutable } from './platform/exec.js';
48
+ import { findInPath } from './agent-spec/agents.js';
49
49
  import { getUserAgentsDir } from './state.js';
50
50
  /**
51
51
  * Wire contract, mirrored from `secrets-cli/src/protocol.ts`. Both sides MUST
@@ -190,14 +190,22 @@ export function parseBundleValue(raw) {
190
190
  let cachedBin;
191
191
  /**
192
192
  * Resolve the standalone `secrets` executable: `$SECRETS_BIN` if set, else the
193
- * `secrets` command on PATH. Cached for the process. A miss throws with install
194
- * guidance — there is NO fallback to the embedded engine (DIST-1).
193
+ * `secrets` command on PATH — resolved with `findInPath`, which skips agents-cli's
194
+ * own shims dir. That skip is load-bearing, not tidiness: before PHNX-3989 the
195
+ * shims dir carried a `secrets` command shim that `exec`s `agents secrets`, it
196
+ * sits FIRST on PATH, and it survives an in-place upgrade (its baked entrypoint
197
+ * still exists). A plain `which secrets` returned that shim, so `agents secrets`
198
+ * → shim → `agents secrets` → … spawned an unbounded process chain on every box
199
+ * upgraded from 1.22.84 (the whole fleet, via R5 auto-update). A `secrets` inside
200
+ * our shims dir can never be the standalone, so it is not a candidate at all.
201
+ * Cached for the process. A miss throws with install guidance — there is NO
202
+ * fallback to the embedded engine (DIST-1).
195
203
  */
196
204
  export function resolveSecretsBin() {
197
205
  if (cachedBin)
198
206
  return cachedBin;
199
207
  const explicit = process.env.SECRETS_BIN?.trim();
200
- const resolved = explicit && explicit.length > 0 ? explicit : findExecutable('secrets');
208
+ const resolved = explicit && explicit.length > 0 ? explicit : findInPath('secrets');
201
209
  if (!resolved) {
202
210
  throw new SecretsClientError('SECRETS_BIN_MISSING', 'The standalone `secrets` CLI was not found. Install it with:\n' +
203
211
  ' npm i -g @phnx-labs/secrets-cli\n' +
@@ -75,9 +75,21 @@ export interface AuthSyncDeps {
75
75
  localName?: string;
76
76
  userAgentsDir?: string;
77
77
  isPinned?: (name: string) => boolean;
78
+ peerRole?: (name: string) => ReturnType<typeof selfConfiguredDeviceRole>;
79
+ selfRole?: () => ReturnType<typeof selfConfiguredDeviceRole>;
78
80
  push?: (bundle: string, host: string) => Promise<PushBundleResult>;
79
81
  sshTarget?: (device: DeviceProfile) => string;
80
82
  }
83
+ /**
84
+ * The one device that pushes credentials this tick. A ready HEADED device
85
+ * (`personal`/`desktop`) wins over any ready worker: the headed box is where
86
+ * tokens are minted (invariant 7), so it is the copy of record; a worker holds
87
+ * only what was once pushed to it. Name order breaks ties so every box elects
88
+ * the same publisher from the same shared verdicts. Before this rule the sort
89
+ * was by name alone, which elected `mac-mini` (a worker with a 6-day-old copy)
90
+ * over `zion`, and zion then skipped every peer as a non-publisher.
91
+ */
92
+ export declare function electPublisher(ready: readonly string[], roleOf: (name: string) => ReturnType<typeof selfConfiguredDeviceRole>): string | null;
81
93
  export interface PublishAuthVerdictOptions {
82
94
  inspectLocal?: () => {
83
95
  exists: boolean;
@@ -181,6 +193,7 @@ export interface ReservedStoreSyncDeps {
181
193
  readMetaFn?: () => Pick<Meta, 'accounts' | 'deviceAccounts'>;
182
194
  isPinned?: (name: string) => boolean;
183
195
  peerRole?: (name: string) => ReturnType<typeof selfConfiguredDeviceRole>;
196
+ selfRole?: () => ReturnType<typeof selfConfiguredDeviceRole>;
184
197
  localReady?: boolean;
185
198
  /** Does THIS publisher hold (bundle, key) to push? Defaults to a local bundle read. */
186
199
  hasLocalKey?: (bundle: string, key: string) => boolean;
@@ -122,6 +122,19 @@ export function planAuthBundlePush(localAuthOk, localIsPublisher, devices) {
122
122
  return { action: 'push', device: device.name };
123
123
  });
124
124
  }
125
+ /**
126
+ * The one device that pushes credentials this tick. A ready HEADED device
127
+ * (`personal`/`desktop`) wins over any ready worker: the headed box is where
128
+ * tokens are minted (invariant 7), so it is the copy of record; a worker holds
129
+ * only what was once pushed to it. Name order breaks ties so every box elects
130
+ * the same publisher from the same shared verdicts. Before this rule the sort
131
+ * was by name alone, which elected `mac-mini` (a worker with a 6-day-old copy)
132
+ * over `zion`, and zion then skipped every peer as a non-publisher.
133
+ */
134
+ export function electPublisher(ready, roleOf) {
135
+ const rank = (name) => (isHeadedDeviceRole(roleOf(name)) ? 0 : 1);
136
+ return [...ready].sort((a, b) => rank(a) - rank(b) || normalizeHost(a).localeCompare(normalizeHost(b)))[0] ?? null;
137
+ }
125
138
  function defaultDevices() {
126
139
  return Object.values(loadDevicesSync());
127
140
  }
@@ -175,8 +188,9 @@ export async function syncReservedAuthBundle(deps = {}) {
175
188
  if (localStatus === 'ready' && !readyPublishers.some((name) => normalizeHost(name) === localNorm)) {
176
189
  readyPublishers.push(localName);
177
190
  }
178
- readyPublishers.sort((a, b) => normalizeHost(a).localeCompare(normalizeHost(b)));
179
- result.publisher = readyPublishers[0] ?? null;
191
+ const peerRole = deps.peerRole ?? configuredDeviceRole;
192
+ const selfRole = deps.selfRole ?? selfConfiguredDeviceRole;
193
+ result.publisher = electPublisher(readyPublishers, (name) => (normalizeHost(name) === localNorm ? selfRole() : peerRole(name)));
180
194
  const localIsPublisher = result.publisher !== null && normalizeHost(result.publisher) === localNorm;
181
195
  const pinned = deps.isPinned ?? ((name) => isHostPinned(name, managedKnownHostsPath()));
182
196
  const plan = planAuthBundlePush(localStatus === 'ready', localIsPublisher, devices.map((device) => ({
@@ -386,8 +400,9 @@ export async function syncReservedStores(deps = {}) {
386
400
  .map((s) => s.device);
387
401
  if (localReady && !readyPublishers.some((n) => normalizeHost(n) === localNorm))
388
402
  readyPublishers.push(localName);
389
- readyPublishers.sort((a, b) => normalizeHost(a).localeCompare(normalizeHost(b)));
390
- result.publisher = readyPublishers[0] ?? null;
403
+ const peerRole = deps.peerRole ?? configuredDeviceRole;
404
+ const selfRole = deps.selfRole ?? selfConfiguredDeviceRole;
405
+ result.publisher = electPublisher(readyPublishers, (name) => (normalizeHost(name) === localNorm ? selfRole() : peerRole(name)));
391
406
  const localIsPublisher = result.publisher !== null && normalizeHost(result.publisher) === localNorm;
392
407
  if (targets.length === 0 || !localIsPublisher) {
393
408
  for (const d of devices) {
@@ -397,7 +412,6 @@ export async function syncReservedStores(deps = {}) {
397
412
  }
398
413
  const memo = readDeliveryMemo(deps.cacheDir);
399
414
  const pinned = deps.isPinned ?? ((name) => isHostPinned(name, managedKnownHostsPath()));
400
- const peerRole = deps.peerRole ?? configuredDeviceRole;
401
415
  const peers = devices.map((d) => ({
402
416
  name: d.name,
403
417
  headed: isHeadedDeviceRole(peerRole(d.name)),
@@ -459,11 +473,19 @@ export function reconcileLocalWorkerSlots(deps = {}) {
459
473
  const slots = readSlots(meta);
460
474
  const hasLocalKey = deps.hasLocalKey ?? defaultHasLocalKey;
461
475
  const provision = deps.provision ?? provisionWorkerSlot;
462
- for (const account of listNativeAccounts(meta)) {
463
- const cred = account.workerCredential;
464
- if (!cred)
465
- continue; // legacy claude worker resolves its token at spawn
466
- if (!hasLocalKey(cred.bundle, cred.key)) {
476
+ const byId = new Map(listNativeAccounts(meta).map((account) => [account.id, account]));
477
+ // Resolve each account to the one (bundle, key) the push plan uses: a T1 row's
478
+ // reserved `__<harness>__` key, or the legacy `auth` key by email for a claude
479
+ // row predating T1. Both are worker credentials this box may already hold, so
480
+ // both get a slot. Skipping the legacy rows here is what left every registered
481
+ // pre-T1 account without a slot on every worker while its token sat in `auth`:
482
+ // the picker then fell back to identity-less version homes and Claude showed a
483
+ // login screen on a headless box (the mac-mini 2026-09-06 incident).
484
+ for (const target of reservedSyncTargets(meta)) {
485
+ const account = byId.get(target.accountId);
486
+ if (!account)
487
+ continue;
488
+ if (!hasLocalKey(target.bundle, target.key)) {
467
489
  result.skipped.push({ accountId: account.id, reason: 'durable key not synced yet' });
468
490
  continue;
469
491
  }
@@ -46,10 +46,12 @@ export const shimsCheck = {
46
46
  if (!ctx.dryRun && removeLegacyUserShim(agent))
47
47
  fixed.push(`removed legacy ${cmd} shim`);
48
48
  }
49
- // Prune orphaned legacy command shims (browser/secrets/sessions/… left by a
50
- // removed install) whose baked AGENTS_BIN is dead — the current source never
49
+ // Prune orphaned legacy command shims (browser/sessions/… left by a removed
50
+ // install) whose baked AGENTS_BIN is dead — the current source never
51
51
  // regenerates them, and they either die with exit 127 or shadow the real
52
- // package bin on PATH. Only removes shims whose target install is gone.
52
+ // package bin on PATH. Only removes shims whose target install is gone — plus
53
+ // the legacy `secrets` shim regardless: it re-enters `agents secrets`, which is
54
+ // a passthrough to the standalone binary since PHNX-3989, so it can only recurse.
53
55
  if (!ctx.dryRun) {
54
56
  for (const name of listShimFileNames()) {
55
57
  if (pruneOrphanedCommandShim(name))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.85",
3
+ "version": "1.22.87",
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",
@@ -50,9 +50,39 @@ function shellQuote(value) {
50
50
  }
51
51
 
52
52
  // Shorthands that delegate to the installed agents-cli entrypoint.
53
- const ALIASES = ['sessions', 'secrets', 'browser', 'pty', 'teams'];
53
+ const ALIASES = ['sessions', 'browser', 'pty', 'teams'];
54
+
55
+ // Aliases this script USED to write and must now remove on every install.
56
+ // `secrets`: the name belongs to the standalone `@phnx-labs/secrets-cli` binary
57
+ // (PHNX-3989); `agents secrets` is a passthrough to it, so an alias shim that
58
+ // `exec`s `agents secrets` can only re-enter the passthrough, and it shadows the
59
+ // real `secrets` bin because the shims dir sits first on PATH. The self-heal
60
+ // shim pass prunes it too, but that only runs from a daemon or a TTY session —
61
+ // postinstall is what every upgrade path actually executes, so it cleans up here.
62
+ const RETIRED_ALIASES = ['secrets'];
63
+
64
+ function removeRetiredAliasShims() {
65
+ for (const name of RETIRED_ALIASES) {
66
+ for (const file of [path.join(SHIMS_DIR, name), path.join(SHIMS_DIR, name + '.cmd')]) {
67
+ let content;
68
+ try {
69
+ content = fs.readFileSync(file, 'utf8');
70
+ } catch {
71
+ continue;
72
+ }
73
+ // Only ever remove OUR alias — the POSIX shim ends in `<name> "$@"`, the
74
+ // Windows companion in `<name> %*` — never an unrelated file someone placed
75
+ // under that name, and never a user's own `agents setup alias` shim, which
76
+ // is marked `# Alias shim:` (the same guard pruneOrphanedCommandShim keeps).
77
+ if (content.includes('# Alias shim:')) continue;
78
+ if (!content.includes(`${name} "$@"`) && !content.includes(`${name} %*`)) continue;
79
+ fs.rmSync(file, { force: true });
80
+ }
81
+ }
82
+ }
54
83
 
55
84
  function writeAliasShims() {
85
+ removeRetiredAliasShims();
56
86
  const written = [];
57
87
  for (const name of ALIASES) {
58
88
  const target = path.join(SHIMS_DIR, name);