@phnx-labs/agents-cli 1.22.90 → 1.22.92

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.92
4
+
5
+ - **AGI Menu 1.1.0 is the helper the CLI installs (PHNX-4007).** The menu-bar
6
+ helper floor moves from 1.0.0 to 1.1.0, so `agents menubar setup` and
7
+ `agents menubar enable` download and install the release that carries the
8
+ dispatch form, the Sessions window, actionable notification banners, the
9
+ screenshot OCR index, and the FeedStream data layer (PHNX-3999). The startup
10
+ self-heal stays network-free: it reinstalls only from a bundled or cached copy,
11
+ so an existing 1.0.0 install upgrades on the next `agents menubar setup`.
12
+ Source: `src/lib/helper-versions.ts`.
13
+
14
+ ## 1.22.91
15
+
16
+ - **`agents sessions` search/list execs the standalone `sessions` CLI (PHNX-4012).** Read queries intercept in `index.ts` before bootstrap so they skip the 6K-line in-process engine. Lifecycle verbs (`resume`, `stop`, `inject`, `watch`, `--active`, `--markdown`) still load the in-repo command. The `sessions` alias shim is retired like `secrets` so it cannot recurse. Install: `npm i -g @phnx-labs/sessions-cli`. Source: `cli/src/index.ts`, `cli/src/lib/sessions-client.ts`, `cli/scripts/postinstall.js`.
17
+
3
18
  ## 1.22.90
4
19
 
5
20
  - **`accounts migrate --apply` no longer aborts on a worker whose accounts already hold slots (PHNX-3940).** The daemon's auth-sync provisions a durable slot per account on every worker, so `--apply` hit `Slot already exists` on the first credential-bearing legacy home and cleaned nothing. The plan now routes such a home to `trash` (restorable with `agents trash restore`) with the reason "account already holds a provisioned slot on this device", rewrites any `<harness>@<label>` binding to the account, and clears the stale home record; a canonical install in that state keeps its binary, has its stale home moved to `~/.agents/.history/trash/homes/…` (manifest key `<harness>@<label>#home`), and gets an empty home back. A regular file at the slot path is still not a slot, so the apply-time guard still fails loud on that corruption. Source: `cli/src/lib/accounts/migrate.ts`.
@@ -2234,7 +2234,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2234
2234
  // dispatch time; the peer resolves ITS slot. This block is local-only.
2235
2235
  const remoteTarget = options.host || options.device;
2236
2236
  if (!remoteTarget) {
2237
- const { adoptedConfigPointsAtHome, adoptedSymlinkMismatchError, isSymlinkAdoptedHarness, resolveNativeSpawnHome, symlinkAdoptedAccountError, } = await import('../lib/exec-account-home.js');
2237
+ const { adoptedConfigPointsAtHome, adoptedSymlinkMismatchError, durableSlotEnv, isSymlinkAdoptedHarness, resolveNativeSpawnHome, symlinkAdoptedAccountError, } = await import('../lib/exec-account-home.js');
2238
2238
  const { readMeta } = await import('../lib/state.js');
2239
2239
  const meta = readMeta();
2240
2240
  if (isSymlinkAdoptedHarness(agent)) {
@@ -2251,6 +2251,11 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2251
2251
  process.exit(1);
2252
2252
  }
2253
2253
  execHome = resolved.execHome;
2254
+ // A worker's durable api-key slot holds no file: the pushed key
2255
+ // rides the harness env var (CURSOR_API_KEY, …) on this launch.
2256
+ const durable = durableSlotEnv(agent, spawnAccount, resolved, meta);
2257
+ if (Object.keys(durable).length > 0)
2258
+ accountEnv = { ...accountEnv, ...durable };
2254
2259
  if (resolved.source === 'legacy-home') {
2255
2260
  accountConfigVersion = resolved.label;
2256
2261
  // A leftover version-labeled home is both the spawn HOME and
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  * - `__usage-ingest` / `__usage-export`
17
17
  * - `__harness-update-run`
18
18
  * - `__daemon-run`
19
+ * - `sessions` (read queries only — PHNX-4012)
19
20
  *
20
21
  * The synchronous secrets-broker fast paths (`__secrets-get` / `__secrets-ping`
21
22
  * / `__secrets-lock`) and `__vault-age-helper` moved out of this CLI entirely
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@
16
16
  * - `__usage-ingest` / `__usage-export`
17
17
  * - `__harness-update-run`
18
18
  * - `__daemon-run`
19
+ * - `sessions` (read queries only — PHNX-4012)
19
20
  *
20
21
  * The synchronous secrets-broker fast paths (`__secrets-get` / `__secrets-ping`
21
22
  * / `__secrets-lock`) and `__vault-age-helper` moved out of this CLI entirely
@@ -131,6 +132,43 @@ if (process.argv[2] === '__daemon-run') {
131
132
  }
132
133
  process.exit(process.exitCode ?? 0);
133
134
  }
135
+ // PHNX-4012: search/list/id lookup of `agents sessions` execs the standalone
136
+ // `sessions` binary without loading bootstrap or the 6K-line sessions module.
137
+ // Lifecycle verbs (resume/stop/inject/watch/--active/--markdown) fall through.
138
+ if (process.argv[2] === 'sessions') {
139
+ const forwarded = process.argv.slice(3);
140
+ const { isReadQuery, resolveSessionsBin, invocation, SessionsClientError, SESSIONS_INSTALL_HINT, } = await import('./lib/sessions-client.js');
141
+ if (isReadQuery(forwarded)) {
142
+ const { spawnSync } = await import('node:child_process');
143
+ let bin = null;
144
+ try {
145
+ bin = resolveSessionsBin();
146
+ }
147
+ catch (err) {
148
+ // No standalone on this box: fall through to the in-repo engine below
149
+ // rather than refusing the query. The fast path is an optimization for a
150
+ // box that has `sessions` installed (skips bootstrap and the sessions
151
+ // module); a box without it must still answer `agents sessions --json`
152
+ // exactly as it did before PHNX-4012 — every worker in the fleet and
153
+ // every CI runner is such a box until @phnx-labs/sessions-cli is
154
+ // published, and refusing here turned main's full suite red.
155
+ if (!(err instanceof SessionsClientError && err.code === 'SESSIONS_BIN_MISSING'))
156
+ throw err;
157
+ if (process.env.AGENTS_SESSIONS_FASTPATH_HINT !== '0') {
158
+ process.stderr.write(`agents: standalone \`sessions\` not installed; using the in-process engine (${SESSIONS_INSTALL_HINT})\n`);
159
+ }
160
+ }
161
+ if (bin) {
162
+ const { command, prefix } = invocation(bin);
163
+ const res = spawnSync(command, [...prefix, ...forwarded], { stdio: 'inherit' });
164
+ if (res.error) {
165
+ process.stderr.write(`Failed to run \`sessions\`: ${res.error.message}\n`);
166
+ process.exit(1);
167
+ }
168
+ process.exit(res.status ?? 1);
169
+ }
170
+ }
171
+ }
134
172
  // Full CLI: commander tree, update checks, migrations, parse. Static imports
135
173
  // inside bootstrap.js evaluate only when we reach this line.
136
174
  await import('./bootstrap.js');
@@ -1,3 +1,4 @@
1
+ import { selfConfiguredDeviceRole } from './device-config.js';
1
2
  import { isSymlinkAdoptedHarness } from './installations/shims.js';
2
3
  import type { AgentId, DeviceAccountSlot, Meta } from './types.js';
3
4
  export { isAccountSlotDir } from './agents.js';
@@ -10,6 +11,24 @@ export interface NativeSpawnHome {
10
11
  /** Installation label when `source` is `legacy-home`. */
11
12
  label?: string;
12
13
  }
14
+ /**
15
+ * The env a DURABLE slot injects at spawn (PHNX-3940 T5/T6). A worker's slot
16
+ * for an api-key harness (codex/grok/cursor/opencode/droid) holds no file — the
17
+ * daemon pushed the account's worker API key into the reserved
18
+ * `__<harness>__` store and `provisionWorkerSlot` recorded the slot as
19
+ * `durable`; the key rides the harness's own env var (`CURSOR_API_KEY`, …)
20
+ * on every launch. Anything else — a native login in the slot on a headed
21
+ * device, a claude durable slot (its setup-token is a `.oauth_token` file the
22
+ * adapter reads), a harness with no api-key worker kind, a headed device — injects
23
+ * nothing.
24
+ * Without this, `agents run cursor#gmail` on a worker reached Cursor with an
25
+ * empty env and got `Authentication required … set CURSOR_API_KEY`.
26
+ */
27
+ export declare function durableSlotEnv(agent: AgentId, account: {
28
+ id: string;
29
+ }, resolved: Pick<NativeSpawnHome, 'slot'>, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, deps?: {
30
+ selfRole?: () => ReturnType<typeof selfConfiguredDeviceRole>;
31
+ }): Record<string, string>;
13
32
  export declare function missingSlotHint(agent: AgentId, name: string): string;
14
33
  export declare function symlinkAdoptedAccountError(agent: AgentId, name: string, defaultName: string | undefined): string;
15
34
  export declare function adoptedSymlinkMismatchError(agent: AgentId, name: string, execHome: string): string;
@@ -13,7 +13,7 @@ import * as os from 'node:os';
13
13
  import * as path from 'node:path';
14
14
  import { listNativeAccounts, nativeAccountHome } from './account-registry.js';
15
15
  import { readSlots } from './accounts/slots.js';
16
- import { workerProvisioningHint } from './accounts/add.js';
16
+ import { workerProvisioningHint, workerApiKeyEnv } from './accounts/add.js';
17
17
  import { AGENTS, credentialPresence, getAccountInfo } from './agents.js';
18
18
  import { AUTH_BUNDLE, claudeAccountTokenKey, provisionWorkerSlot, readReservedCredential } from './claude-account-token.js';
19
19
  import { isHeadedDeviceRole, selfConfiguredDeviceRole } from './device-config.js';
@@ -22,6 +22,40 @@ import { getAgentConfigPath, isSymlinkAdoptedHarness, repointAdoptedConfigToHome
22
22
  import { getVersionHomePath, listInstalledVersions } from './installations/versions.js';
23
23
  export { isAccountSlotDir } from './agents.js';
24
24
  export { isSymlinkAdoptedHarness };
25
+ /**
26
+ * The env a DURABLE slot injects at spawn (PHNX-3940 T5/T6). A worker's slot
27
+ * for an api-key harness (codex/grok/cursor/opencode/droid) holds no file — the
28
+ * daemon pushed the account's worker API key into the reserved
29
+ * `__<harness>__` store and `provisionWorkerSlot` recorded the slot as
30
+ * `durable`; the key rides the harness's own env var (`CURSOR_API_KEY`, …)
31
+ * on every launch. Anything else — a native login in the slot on a headed
32
+ * device, a claude durable slot (its setup-token is a `.oauth_token` file the
33
+ * adapter reads), a harness with no api-key worker kind, a headed device — injects
34
+ * nothing.
35
+ * Without this, `agents run cursor#gmail` on a worker reached Cursor with an
36
+ * empty env and got `Authentication required … set CURSOR_API_KEY`.
37
+ */
38
+ export function durableSlotEnv(agent, account, resolved, meta, deps = {}) {
39
+ if (resolved.slot?.authMode !== 'durable')
40
+ return {};
41
+ // A headed device authenticates from its own native login (invariant 7). A
42
+ // `durable` record can outlive a role change (`agents devices role … personal`
43
+ // never rewrites slots), so the gate is the box's CURRENT role, the same
44
+ // predicate `isProvisionableWorker` applies — never a record on disk.
45
+ if (isHeadedDeviceRole((deps.selfRole ?? selfConfiguredDeviceRole)()))
46
+ return {};
47
+ const envName = workerApiKeyEnv(agent);
48
+ if (!envName)
49
+ return {};
50
+ const row = nativeRow(account.id, meta);
51
+ const cred = row?.workerCredential;
52
+ if (!cred || cred.kind !== 'api-key')
53
+ return {};
54
+ const value = readReservedCredential(cred.bundle, cred.key);
55
+ if (!value)
56
+ return {};
57
+ return { [envName]: value };
58
+ }
25
59
  export function missingSlotHint(agent, name) {
26
60
  const role = selfConfiguredDeviceRole();
27
61
  if (!isHeadedDeviceRole(role)) {
@@ -36,7 +36,7 @@
36
36
  * helper release now, off this table entirely.
37
37
  */
38
38
  export const HELPER_RELEASES = {
39
- menubar: { tagPrefix: 'menubar', floor: '1.0.0' },
39
+ menubar: { tagPrefix: 'menubar', floor: '1.1.0' },
40
40
  'computer-mac': { tagPrefix: 'computer-mac', floor: '1.0.0' },
41
41
  // The Windows helper is a bare .exe, not an .app bundle, so it does not share
42
42
  // helper-download.ts's zip/codesign/notarize machinery -- but it has the same
@@ -179,7 +179,7 @@ export declare function removeShim(agent: AgentId): boolean;
179
179
  * sorted alphabetically before the real self-updated grok binary and
180
180
  * was silently launched instead.
181
181
  */
182
- export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 19;
182
+ export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 20;
183
183
  /**
184
184
  * Agents whose config directory can be relocated by an environment variable
185
185
  * (the per-agent `managedEnv` block in `generateVersionedAliasScript` below).
@@ -1095,7 +1095,7 @@ export function removeShim(agent) {
1095
1095
  // v18 — Cursor aliases select the file credential store and swap HOME to the
1096
1096
  // version home because current Cursor writes auth.json under ~/.cursor
1097
1097
  // and ignores XDG_CONFIG_HOME for credential storage.
1098
- export const VERSIONED_ALIAS_SCHEMA_VERSION = 19;
1098
+ export const VERSIONED_ALIAS_SCHEMA_VERSION = 20;
1099
1099
  /** Internal marker string used to embed the schema version in versioned alias scripts. */
1100
1100
  const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
1101
1101
  // The version string is interpolated into a generated bash script and into
@@ -1220,52 +1220,56 @@ export function generateVersionedAliasScript(agent, version) {
1220
1220
  # already sources this same adapter block, so sharing it keeps the two in sync.
1221
1221
  # plain (not exported) so it feeds shimConfigEnvBash below but never leaks into the
1222
1222
  # launched agent process — matching the main shim's convention.
1223
- VERSION_DIR="$HOME/.agents/.history/versions/${agent}/${version}"
1223
+ VERSION_DIR="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}"
1224
1224
  ${resolveHarnessAdapter(agent).shimConfigEnvBash?.({ configDirName }) ?? ''}`
1225
1225
  : agent === 'codex'
1226
- ? codexHomeShimBash(`$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}`, `$HOME/.agents/.codex-homes/${version}`)
1226
+ ? codexHomeShimBash(`$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}`, `$AGENTS_REAL_HOME/.agents/.codex-homes/${version}`)
1227
1227
  : agent === 'copilot'
1228
1228
  ? `
1229
1229
  # Copilot honors COPILOT_HOME to relocate ~/.copilot (settings, mcp-config.json,
1230
1230
  # session-state, logs). Point direct aliases at the versioned home so per-
1231
1231
  # version MCP and session state are isolated.
1232
- export COPILOT_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
1232
+ export COPILOT_HOME="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
1233
1233
  `
1234
1234
  : agent === 'grok'
1235
1235
  ? `
1236
1236
  # Grok Build uses GROK_HOME to isolate its entire configuration tree (skills,
1237
1237
  # hooks, plugins, agents, memory, sessions, config.toml, MCP). Point direct
1238
1238
  # aliases at the versioned home for isolation parity with the main shim.
1239
- export GROK_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
1239
+ export GROK_HOME="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
1240
1240
  `
1241
1241
  : agent === 'opencode'
1242
1242
  ? `
1243
1243
  # OpenCode reads plugins, agents, commands, and other config-directory
1244
1244
  # resources from OPENCODE_CONFIG_DIR. Point direct aliases at the versioned
1245
1245
  # global config tree where agents-cli syncs OpenCode resources.
1246
- export OPENCODE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/home/.config/opencode"
1246
+ export OPENCODE_CONFIG_DIR="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/.config/opencode"
1247
1247
  `
1248
1248
  : agent === 'kimi'
1249
1249
  ? `
1250
1250
  # Kimi Code CLI honors KIMI_CODE_HOME to relocate ~/.kimi-code (config.toml,
1251
1251
  # mcp.json, sessions, skills, hooks). Point direct aliases at the versioned home.
1252
- export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
1252
+ export KIMI_CODE_HOME="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
1253
1253
  `
1254
1254
  : agent === 'muse'
1255
1255
  ? `
1256
1256
  # Muse Code: no dedicated config env var. Pin XDG so config/sessions live under
1257
1257
  # the version home as real directories (not via the adopt symlink at
1258
1258
  # ~/.config/muse, which Muse rejects with SymlinkOrReparse).
1259
- export XDG_CONFIG_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/.config"
1260
- export XDG_DATA_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/.local/share"
1259
+ export XDG_CONFIG_HOME="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/.config"
1260
+ export XDG_DATA_HOME="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/.local/share"
1261
1261
  `
1262
1262
  : agent === 'cursor'
1263
1263
  ? `
1264
1264
  # Cursor defaults to one machine-global OS-keychain login on macOS. Force its
1265
1265
  # file store and swap HOME, so ~/.cursor/auth.json belongs to this
1266
1266
  # version and direct aliases cannot fall through to the shared keychain login.
1267
- export AGENTS_REAL_HOME="\${AGENTS_REAL_HOME:-$HOME}"
1268
- export HOME="$HOME/.agents/.history/versions/${agent}/${version}/home"
1267
+ # A spawner that already chose HOME (an account slot, PHNX-3940 T5 — it left
1268
+ # the real home in AGENTS_REAL_HOME) keeps its choice: re-swapping here would
1269
+ # silently discard the slot.
1270
+ if [ "$HOME" = "$AGENTS_REAL_HOME" ]; then
1271
+ export HOME="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home"
1272
+ fi
1269
1273
  export AGENT_CLI_CREDENTIAL_STORE="file"
1270
1274
  `
1271
1275
  : '';
@@ -1283,7 +1287,7 @@ export AGENT_CLI_CREDENTIAL_STORE="file"
1283
1287
  // alias's sibling dispatcher shim and re-execs forever.
1284
1288
  // This template is unix-only — on Windows the .cmd companion delegates to
1285
1289
  // "agents __shim" which resolves via getBinaryPath() instead.
1286
- const versionDir = `$HOME/.agents/.history/versions/${agent}/${version}`;
1290
+ const versionDir = `$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}`;
1287
1291
  const binaryResolution = agent === 'grok'
1288
1292
  ? `# Grok ships its native binary in the versioned home's .grok/downloads (or,
1289
1293
  # for pre-fix installs, the global ~/.grok/downloads), not node_modules.
@@ -1295,7 +1299,7 @@ if [ -d "$GROK_DOWNLOADS" ]; then
1295
1299
  fi
1296
1300
  # Fall back to the global grok home (binary installed without GROK_HOME set).
1297
1301
  if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
1298
- GROK_GLOBAL_DOWNLOADS="$HOME/.grok/downloads"
1302
+ GROK_GLOBAL_DOWNLOADS="$AGENTS_REAL_HOME/.grok/downloads"
1299
1303
  if [ -d "$GROK_GLOBAL_DOWNLOADS" ]; then
1300
1304
  BINARY=$(_resolve_grok_binary "$GROK_GLOBAL_DOWNLOADS" "${version}")
1301
1305
  fi
@@ -1306,31 +1310,31 @@ fi
1306
1310
  if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
1307
1311
  BINARY=$(command -v grok 2>/dev/null || echo "")
1308
1312
  case "$BINARY" in
1309
- "$HOME/.agents/.cache/shims/"*) BINARY="" ;;
1313
+ "$AGENTS_REAL_HOME/.agents/.cache/shims/"*) BINARY="" ;;
1310
1314
  esac
1311
1315
  fi`
1312
1316
  : agent === 'droid'
1313
1317
  ? `# Droid (Factory AI) installs a standalone native binary at ~/.local/bin/droid;
1314
1318
  # there is no npm package and nothing lands in node_modules/.bin. The PATH
1315
1319
  # fallback refuses anything under our shims dir to avoid an infinite re-exec.
1316
- DROID_BINARY="$HOME/.local/bin/droid"
1320
+ DROID_BINARY="$AGENTS_REAL_HOME/.local/bin/droid"
1317
1321
  if [ -x "$DROID_BINARY" ]; then
1318
1322
  BINARY="$DROID_BINARY"
1319
1323
  else
1320
1324
  BINARY=$(command -v droid 2>/dev/null || echo "")
1321
1325
  case "$BINARY" in
1322
- "$HOME/.agents/.cache/shims/"*) BINARY="" ;;
1326
+ "$AGENTS_REAL_HOME/.agents/.cache/shims/"*) BINARY="" ;;
1323
1327
  esac
1324
1328
  fi`
1325
1329
  : agent === 'muse'
1326
1330
  ? `# Muse Code installs a self-updating launcher at ~/.local/bin/muse.
1327
- MUSE_BINARY="$HOME/.local/bin/muse"
1331
+ MUSE_BINARY="$AGENTS_REAL_HOME/.local/bin/muse"
1328
1332
  if [ -x "$MUSE_BINARY" ]; then
1329
1333
  BINARY="$MUSE_BINARY"
1330
1334
  else
1331
1335
  BINARY=$(command -v muse 2>/dev/null || echo "")
1332
1336
  case "$BINARY" in
1333
- "$HOME/.agents/.cache/shims/"*) BINARY="" ;;
1337
+ "$AGENTS_REAL_HOME/.agents/.cache/shims/"*) BINARY="" ;;
1334
1338
  esac
1335
1339
  fi`
1336
1340
  : agent === 'warp'
@@ -1339,7 +1343,7 @@ fi`
1339
1343
  # PATH, refusing anything under our shims dir.
1340
1344
  BINARY=$(command -v warp 2>/dev/null || echo "")
1341
1345
  case "$BINARY" in
1342
- "$HOME/.agents/.cache/shims/"*) BINARY="" ;;
1346
+ "$AGENTS_REAL_HOME/.agents/.cache/shims/"*) BINARY="" ;;
1343
1347
  esac`
1344
1348
  : `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
1345
1349
  return `#!/bin/bash
@@ -1347,22 +1351,32 @@ esac`
1347
1351
  # ${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}
1348
1352
  # Direct alias for ${agentConfig.name}@${version}
1349
1353
 
1354
+ # The real home. A spawner may have swapped HOME already — an account slot
1355
+ # launch (PHNX-3940 T5) sets HOME to the slot dir and leaves the real home in
1356
+ # AGENTS_REAL_HOME — so every agents-owned path below (the installation, the
1357
+ # version home, the shims dir) is anchored here, never on whatever HOME is now.
1358
+ # Anchoring on HOME made a slot launch of cursor#<name> answer "not installed"
1359
+ # and the launch lease fail with "No installation directory".
1360
+ export AGENTS_REAL_HOME="\${AGENTS_REAL_HOME:-$HOME}"
1361
+
1350
1362
  ${binaryResolution}
1351
1363
 
1352
1364
  if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
1353
1365
  echo "agents: ${agent}@${version} not installed" >&2
1354
1366
  exit 1
1355
1367
  fi
1356
- ${managedEnv}
1357
1368
 
1358
1369
  # Register a launch lease for THIS pid before the exec below (PHNX-3940) — see
1359
1370
  # generateShimScript's identical call for what this closes and why it fails
1360
- # closed rather than falling through on error. \$\$ survives exec.
1361
- if ! ${agentsBin} __launch-lease "${agent}" "${version}" "\$\$"; then
1371
+ # closed rather than falling through on error. \$\$ survives exec. It runs
1372
+ # under the REAL home, before any harness HOME swap below: agents-cli's own
1373
+ # state root is $HOME/.agents, so a swapped HOME cannot find the installation.
1374
+ if ! HOME="$AGENTS_REAL_HOME" ${agentsBin} __launch-lease "${agent}" "${version}" "\$\$"; then
1362
1375
  echo "agents: could not safely coordinate this launch with a possibly in-progress update of ${agent}@${version}." >&2
1363
1376
  echo " Check: agents update ${agent}@${version} --check Retry once any update finishes." >&2
1364
1377
  exit 1
1365
1378
  fi
1379
+ ${managedEnv}
1366
1380
 
1367
1381
  ${resolveHarnessAdapter(agent).shimExecTail?.(launchArgs) ?? `exec "$BINARY"${launchArgs} "$@"`}
1368
1382
  `;
@@ -2167,14 +2181,11 @@ export function listShimFileNames() {
2167
2181
  }
2168
2182
  /**
2169
2183
  * 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).
2184
+ * `secrets` (PHNX-3989) and `sessions` (PHNX-4012): the shim `exec`s
2185
+ * `agents <name>`, which is a passthrough to the STANDALONE binary — so the
2186
+ * shim re-enters itself (the 1.22.85 fork bomb). Pruned unconditionally.
2176
2187
  */
2177
- const LEGACY_SHIMS_ALWAYS_PRUNED = new Set(['secrets']);
2188
+ const LEGACY_SHIMS_ALWAYS_PRUNED = new Set(['secrets', 'sessions']);
2178
2189
  /**
2179
2190
  * Prune a stale, orphaned shim: one that is NOT a managed agent shim and NOT a user
2180
2191
  * alias, whose baked `AGENTS_BIN` points at an install that no longer exists. These
@@ -185,6 +185,19 @@ export declare function storeDelete(backend: SecretsBackend, item: string): Prom
185
185
  export declare function remoteResolveEnv(target: string, bundle: string, opts?: {
186
186
  osLookupName?: string;
187
187
  }): Promise<Record<string, string>>;
188
+ /**
189
+ * The user agents dir as the RECEIVING agents-cli resolves it — `~/.agents` on
190
+ * every box, the same root {@link buildServeEnv} pins the local standalone to
191
+ * (MIG-1). A push must name it, because the remote `secrets` otherwise runs
192
+ * under its own default `~/.secrets`: the import succeeds, the file store there
193
+ * is keyed by a different machine-local passphrase, and the receiving daemon's
194
+ * `readReservedCredential` never finds the bundle (yosemite-m0, 2026-09-07:
195
+ * `pushed __cursor__` on the publisher, `no readable durable key` on the worker).
196
+ * Remote-relative on purpose — the local absolute dir means nothing over there.
197
+ */
198
+ export declare const REMOTE_USER_AGENTS_DIR = "~/.agents";
199
+ /** Fill in the remote state root unless the caller named one. Pure, for the seam test. */
200
+ export declare function withRemoteStateRoot(opts: PushBundleOptions): PushBundleOptions;
188
201
  export declare function pushBundleToHost(bundle: string, host: string, opts: PushBundleOptions): Promise<PushBundleResult>;
189
202
  export declare function pushBundleToHostAsync(bundle: string, host: string, opts: PushBundleOptions): Promise<PushBundleResult>;
190
203
  export declare function listRemoteBundles(context?: SecretsContext): Promise<RemoteBundleSummary[]>;
@@ -646,11 +646,26 @@ export function storeDelete(backend, item) {
646
646
  export function remoteResolveEnv(target, bundle, opts) {
647
647
  return secretsRequest('remote.remoteResolveEnv', [target, bundle, opts ?? {}]);
648
648
  }
649
+ /**
650
+ * The user agents dir as the RECEIVING agents-cli resolves it — `~/.agents` on
651
+ * every box, the same root {@link buildServeEnv} pins the local standalone to
652
+ * (MIG-1). A push must name it, because the remote `secrets` otherwise runs
653
+ * under its own default `~/.secrets`: the import succeeds, the file store there
654
+ * is keyed by a different machine-local passphrase, and the receiving daemon's
655
+ * `readReservedCredential` never finds the bundle (yosemite-m0, 2026-09-07:
656
+ * `pushed __cursor__` on the publisher, `no readable durable key` on the worker).
657
+ * Remote-relative on purpose — the local absolute dir means nothing over there.
658
+ */
659
+ export const REMOTE_USER_AGENTS_DIR = '~/.agents';
660
+ /** Fill in the remote state root unless the caller named one. Pure, for the seam test. */
661
+ export function withRemoteStateRoot(opts) {
662
+ return { ...opts, remoteSecretsHome: opts.remoteSecretsHome ?? REMOTE_USER_AGENTS_DIR };
663
+ }
649
664
  export function pushBundleToHost(bundle, host, opts) {
650
- return secretsRequest('push.pushBundleToHost', [bundle, host, opts]);
665
+ return secretsRequest('push.pushBundleToHost', [bundle, host, withRemoteStateRoot(opts)]);
651
666
  }
652
667
  export function pushBundleToHostAsync(bundle, host, opts) {
653
- return secretsRequest('push.pushBundleToHostAsync', [bundle, host, opts]);
668
+ return secretsRequest('push.pushBundleToHostAsync', [bundle, host, withRemoteStateRoot(opts)]);
654
669
  }
655
670
  // sync.* (transport pull, used by the `agents sync --secrets` umbrella stage)
656
671
  export function listRemoteBundles(context) {
@@ -155,6 +155,14 @@ export interface PushBundleOptions {
155
155
  literalValues?: Record<string, string>;
156
156
  /** Per-SSH-operation deadline. Async daemon callers must set this explicitly. */
157
157
  timeoutMs?: number;
158
+ /**
159
+ * State root (`SECRETS_HOME`) the remote `secrets` runs under for the whole
160
+ * push (import, read-back verify, literal restoration); remote-relative, a
161
+ * leading `~/` resolves against the remote user's home. The client wrapper
162
+ * fills in the user agents dir (`REMOTE_USER_AGENTS_DIR`) so a push lands
163
+ * where the receiving agents-cli reads (MIG-1 on both ends).
164
+ */
165
+ remoteSecretsHome?: string;
158
166
  }
159
167
  export interface PushBundleResult {
160
168
  ok: boolean;
@@ -0,0 +1,13 @@
1
+ export declare class SessionsClientError extends Error {
2
+ code: string;
3
+ constructor(code: string, message: string);
4
+ }
5
+ export declare function resolveSessionsBin(): string;
6
+ export declare function invocation(bin: string): {
7
+ command: string;
8
+ prefix: string[];
9
+ };
10
+ export declare function isReadQuery(args: string[]): boolean;
11
+ /** Test seam: drop the memoized bin so PATH fixtures can re-resolve. */
12
+ export declare function _resetSessionsClientForTest(): void;
13
+ export declare const SESSIONS_INSTALL_HINT = "npm i -g @phnx-labs/sessions-cli";
@@ -0,0 +1,107 @@
1
+ /**
2
+ * sessions-client.ts — agents-cli's process client for the standalone
3
+ * `sessions` CLI (PHNX-4012). This client never falls back itself: a missing
4
+ * binary throws `SESSIONS_BIN_MISSING` (loud, DIST-1). The caller decides what
5
+ * that means — the read fast-path in `index.ts` falls through to the in-repo
6
+ * `lib/session` engine when the standalone is not installed (every worker and
7
+ * CI runner, until @phnx-labs/sessions-cli is published), and takes the fast
8
+ * path when it is.
9
+ *
10
+ * Bin resolution uses `findInPath`, which skips `~/.agents/.cache/shims`.
11
+ * That skip is load-bearing: the leftover `sessions` alias shim execs
12
+ * `agents sessions`, and resolving it would recurse (the 1.22.85 secrets
13
+ * fork bomb with the names swapped — agi-cli#3532).
14
+ */
15
+ import { findInPath } from './agent-spec/agents.js';
16
+ const INSTALL_HINT = 'npm i -g @phnx-labs/sessions-cli';
17
+ export class SessionsClientError extends Error {
18
+ code;
19
+ constructor(code, message) {
20
+ super(message);
21
+ this.code = code;
22
+ this.name = 'SessionsClientError';
23
+ }
24
+ }
25
+ let cachedBin;
26
+ export function resolveSessionsBin() {
27
+ if (cachedBin)
28
+ return cachedBin;
29
+ const explicit = process.env.SESSIONS_BIN?.trim();
30
+ const resolved = explicit && explicit.length > 0 ? explicit : findInPath('sessions');
31
+ if (!resolved) {
32
+ throw new SessionsClientError('SESSIONS_BIN_MISSING', 'The standalone `sessions` CLI was not found. Install it with:\n' +
33
+ ` ${INSTALL_HINT}\n` +
34
+ 'or point $SESSIONS_BIN at its executable.');
35
+ }
36
+ cachedBin = resolved;
37
+ return resolved;
38
+ }
39
+ export function invocation(bin) {
40
+ if (/\.[mc]?js$/.test(bin))
41
+ return { command: process.execPath, prefix: [bin] };
42
+ return { command: bin, prefix: [] };
43
+ }
44
+ /**
45
+ * Allowlist of what sessions-cli v1 actually implements. Anything else —
46
+ * picker (no args), `--since`, `-D`, `--waiting`, `render`, `stats`,
47
+ * lifecycle verbs — stays on the in-repo engine. A denylist leaked those
48
+ * through `allowUnknownOption()` as FTS tokens (PR review on #3554).
49
+ */
50
+ const READ_FLAGS = new Set([
51
+ '--json',
52
+ '--local',
53
+ '--no-interactive',
54
+ ]);
55
+ const ENGINE_VERBS = new Set([
56
+ 'resume',
57
+ 'detach',
58
+ 'stop',
59
+ 'inject',
60
+ 'fork',
61
+ 'backfill',
62
+ 'migrate',
63
+ 'share',
64
+ 'export',
65
+ 'import',
66
+ 'optimize',
67
+ 'trace',
68
+ 'insights',
69
+ 'bookmark',
70
+ 'tail',
71
+ 'watch',
72
+ 'preview',
73
+ 'attach',
74
+ 'focus',
75
+ 'render',
76
+ 'stats',
77
+ 'go',
78
+ 'reconnect',
79
+ 'migrations',
80
+ 'export',
81
+ ]);
82
+ export function isReadQuery(args) {
83
+ if (args.length === 0)
84
+ return false;
85
+ for (let i = 0; i < args.length; i++) {
86
+ const arg = args[i];
87
+ if (arg === '--limit' || arg === '--agent') {
88
+ i += 1;
89
+ continue;
90
+ }
91
+ if (arg.startsWith('--limit=') || arg.startsWith('--agent='))
92
+ continue;
93
+ if (arg.startsWith('-')) {
94
+ if (!READ_FLAGS.has(arg))
95
+ return false;
96
+ continue;
97
+ }
98
+ if (ENGINE_VERBS.has(arg))
99
+ return false;
100
+ }
101
+ return true;
102
+ }
103
+ /** Test seam: drop the memoized bin so PATH fixtures can re-resolve. */
104
+ export function _resetSessionsClientForTest() {
105
+ cachedBin = undefined;
106
+ }
107
+ export const SESSIONS_INSTALL_HINT = INSTALL_HINT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.90",
3
+ "version": "1.22.92",
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,16 +50,15 @@ function shellQuote(value) {
50
50
  }
51
51
 
52
52
  // Shorthands that delegate to the installed agents-cli entrypoint.
53
- const ALIASES = ['sessions', 'browser', 'pty', 'teams'];
53
+ const ALIASES = ['browser', 'pty', 'teams'];
54
54
 
55
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'];
56
+ // `secrets` (PHNX-3989) and `sessions` (PHNX-4012): those names belong to the
57
+ // standalone CLIs. An alias shim that `exec`s `agents <name>` re-enters the
58
+ // passthrough and shadows the real binary because the shims dir sits first on
59
+ // PATH — the 1.22.85 fork bomb. Postinstall is the upgrade path that actually
60
+ // runs, so it prunes here.
61
+ const RETIRED_ALIASES = ['secrets', 'sessions'];
63
62
 
64
63
  function removeRetiredAliasShims() {
65
64
  for (const name of RETIRED_ALIASES) {