@phnx-labs/agents-cli 1.22.116 → 1.22.117

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.117
4
+
5
+ - **`agents run --local`, and `--device <this machine>` no longer SSHes to itself.** A bare
6
+ interactive run places itself like `--device auto` (PHNX-4083), and the documented way to
7
+ stay put was `--device <this machine>` — which took the remote path and probed its own
8
+ login shell over SSH, timing out on a loaded box. `--local` is the explicit local pin
9
+ (the same door as `--where local`, which previously expanded to nothing and was then
10
+ overridden by the default placement); any host flag naming this machine by short id,
11
+ MagicDNS name, loopback, or the `interactive` sentinel now resolves to the same local
12
+ run. The placement-failure hint names `--local`. Source: `cli/src/commands/exec.ts`
13
+ (`pinLocalWhenTargetIsSelf`), `cli/src/lib/placement.ts`, `cli/src/lib/hosts/remote-cmd.ts`.
14
+
3
15
  ## 1.22.116
4
16
 
5
17
  - **`agents feed answer` is bounded, exactly-once, and truthful about delivery
@@ -62,6 +62,7 @@ interface ExecCommandActionOptions {
62
62
  until?: string;
63
63
  interval?: string;
64
64
  where?: string;
65
+ local?: boolean;
65
66
  host?: string;
66
67
  device?: string;
67
68
  on?: string;
@@ -130,6 +131,20 @@ export declare function hostTargetGiven(options: {
130
131
  on?: string;
131
132
  computer?: string;
132
133
  }): string[];
134
+ /**
135
+ * Turn a host target that names THIS machine into an explicit local pin: the
136
+ * host-family flags are cleared and `local` is set, so no dispatch path
137
+ * self-SSHes and the bare-interactive default (`bareInteractiveRunDefaultsToDeviceAuto`)
138
+ * sees a decided placement. Returns true when the pin was applied. Pure over
139
+ * the `isSelf` predicate so the rule is testable without a device registry.
140
+ */
141
+ export declare function pinLocalWhenTargetIsSelf(options: {
142
+ host?: string;
143
+ device?: string;
144
+ on?: string;
145
+ computer?: string;
146
+ local?: boolean;
147
+ }, isSelf: (name: string) => boolean): boolean;
133
148
  /**
134
149
  * Return every option whose selection semantics conflict with an account
135
150
  * choice (`agent#`). Device routing is deliberately absent: the marker rides
@@ -159,6 +174,7 @@ export declare function runDevicePickerConflicts(options: {
159
174
  device?: string;
160
175
  on?: string;
161
176
  computer?: string;
177
+ local?: boolean;
162
178
  }): string[];
163
179
  export { RUN_AUTO_KEYWORD };
164
180
  /**
@@ -179,6 +195,7 @@ export declare function runAutoDefaultsToAffinity(options: {
179
195
  device?: string;
180
196
  on?: string;
181
197
  computer?: string;
198
+ local?: boolean;
182
199
  }, env?: NodeJS.ProcessEnv): boolean;
183
200
  /**
184
201
  * Whether a bare human-facing `agents run <harness>` — no prompt, so an
@@ -210,6 +227,7 @@ export declare function bareInteractiveRunDefaultsToDeviceAuto(options: {
210
227
  lease?: string | boolean;
211
228
  box?: string;
212
229
  cloud?: boolean;
230
+ local?: boolean;
213
231
  }, run: {
214
232
  prompt?: string;
215
233
  devicePickerRequested?: boolean;
@@ -90,6 +90,24 @@ export function parseRunPickerMarkers(agentSpec) {
90
90
  export function hostTargetGiven(options) {
91
91
  return [options.host, options.device, options.on, options.computer].filter((v) => !!v);
92
92
  }
93
+ /**
94
+ * Turn a host target that names THIS machine into an explicit local pin: the
95
+ * host-family flags are cleared and `local` is set, so no dispatch path
96
+ * self-SSHes and the bare-interactive default (`bareInteractiveRunDefaultsToDeviceAuto`)
97
+ * sees a decided placement. Returns true when the pin was applied. Pure over
98
+ * the `isSelf` predicate so the rule is testable without a device registry.
99
+ */
100
+ export function pinLocalWhenTargetIsSelf(options, isSelf) {
101
+ const targets = hostTargetGiven(options);
102
+ if (targets.length === 0 || !targets.every(isSelf))
103
+ return false;
104
+ options.host = undefined;
105
+ options.device = undefined;
106
+ options.on = undefined;
107
+ options.computer = undefined;
108
+ options.local = true;
109
+ return true;
110
+ }
93
111
  /**
94
112
  * Return every option whose selection semantics conflict with an account
95
113
  * choice (`agent#`). Device routing is deliberately absent: the marker rides
@@ -118,6 +136,8 @@ export function runAccountPickerConflicts(options) {
118
136
  */
119
137
  export function runDevicePickerConflicts(options) {
120
138
  const conflicts = hostTargetGiven(options).map((h) => `--device ${h}`);
139
+ if (options.local)
140
+ conflicts.push('--local');
121
141
  if (options.lease)
122
142
  conflicts.push('--lease');
123
143
  if (options.box)
@@ -145,6 +165,11 @@ export { RUN_AUTO_KEYWORD };
145
165
  * fleet. Pure so the pinning matrix is unit-testable.
146
166
  */
147
167
  export function runAutoDefaultsToAffinity(options, env = process.env) {
168
+ // An explicit local pin (--local, --where local, --device <this machine>)
169
+ // is a decided host layer with an empty host set; read it here so the
170
+ // `run auto` and bare-interactive defaults share one rule.
171
+ if (options.local)
172
+ return false;
148
173
  if (hostTargetGiven(options).length > 0)
149
174
  return false;
150
175
  if (env.AGENTS_RUN_AUTO_HOST_RESOLVED === '1')
@@ -639,7 +664,8 @@ export function registerRunCommand(program) {
639
664
  .option('--until <signal>', 'Loop stop condition. `signal` reads <runDir>/loop-signal.json {continue,reason} each iteration; absent or continue:false stops (fail-closed). Loop only.')
640
665
  .option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
641
666
  .option('--where <spec>', 'Where this run\'s body executes (one placement door): local | device:<name> | auto | lease[:backend] | cloud[:provider]. Expands to --device/--lease/--cloud. Do not combine with those flags. See docs/00-concepts.md#placement.')
642
- .option('-D, --device <name>', 'Offload this run onto another machine over SSH — a registered device, or user@host. Pass "auto" to pick the least-loaded reachable device where the requested agent is installed and signed in, keeping the run local when no remote is better, or "interactive" for the machine pinned as interactive.host (the box a human is sitting at). Same as --where device:<name>. See `agents devices`.')
667
+ .option('--local', 'Run on this machine. A bare interactive run otherwise places itself like --device auto; --local pins it here. Same as --where local or --device <this machine>.')
668
+ .option('-D, --device <name>', 'Offload this run onto another machine over SSH — a registered device, or user@host. Pass "auto" to pick the least-loaded reachable device where the requested agent is installed and signed in, keeping the run local when no remote is better, or "interactive" for the machine pinned as interactive.host (the box a human is sitting at). Naming this machine runs locally, no SSH. Same as --where device:<name>. See `agents devices`.')
643
669
  .option('--remote-cwd <dir>', "Explicit device working directory for --device runs, used VERBATIM (overrides --cwd; usually --cwd suffices — it re-roots a local-home path onto the remote home). Pass a single-quoted '$HOME/…' or a valid remote absolute path; a local ~ expands here and won't exist there (/Users/you vs /home/you).")
644
670
  .option('--no-follow', 'With --device, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
645
671
  .option('--any', 'With --device <cap> (a capability tag), pick any matching device instead of erroring when several match.')
@@ -680,9 +706,9 @@ export function registerRunCommand(program) {
680
706
  agents run claude "fix lint errors in src/" --mode edit
681
707
 
682
708
  # Interactive (TUI): a bare run places itself like --device auto (a fleet
683
- # worker, TUI forwarded over SSH); pin the device to stay on this machine
709
+ # worker, TUI forwarded over SSH); --local keeps it on this machine
684
710
  agents run claude
685
- agents run claude --device zion # stay local (or pick this machine in claude@)
711
+ agents run claude --local # stay local (or pick this machine in claude@)
686
712
 
687
713
  # Pick a signed-in account/version for only this run (# = account picker)
688
714
  agents run claude#
@@ -790,10 +816,10 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
790
816
  Interactive placement: a bare 'agents run <harness>' (no prompt, real TTY)
791
817
  places itself like --device auto — a fleet worker runs it, with the TUI
792
818
  forwarded over SSH. Headless runs (any prompt, --json, no TTY) are
793
- unchanged: they run in place. To stay on this machine, pass
794
- --device <this machine>, or pick this machine (listed first) in the
795
- '<harness>@' device picker. When placement finds no healthy device the
796
- run fails loud and names the local spellings.
819
+ unchanged: they run in place. To stay on this machine, pass --local
820
+ (--device <this machine> means the same), or pick this machine (listed
821
+ first) in the '<harness>@' device picker. When placement finds no
822
+ healthy device the run fails loud and names the local spelling.
797
823
 
798
824
  Fallback: --fallback codex,antigravity retries on rate-limit failure via /continue handoff. Each entry accepts @version.
799
825
 
@@ -891,6 +917,11 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
891
917
  const { placementFromRunFlags, expandPlacementToRunFlags, PlacementError } = await import('../lib/placement.js');
892
918
  try {
893
919
  const placement = placementFromRunFlags(options);
920
+ // An explicit local placement (--local, --where local) must outlive
921
+ // this block: expansion yields no host flag, and the bare-interactive
922
+ // default below reads an empty host set as "decide for me".
923
+ if (placement.kind === 'local' && placement.source !== 'default')
924
+ options.local = true;
894
925
  if (options.where) {
895
926
  const expanded = expandPlacementToRunFlags(placement);
896
927
  if (expanded.host !== undefined)
@@ -918,6 +949,16 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
918
949
  throw err;
919
950
  }
920
951
  }
952
+ // --device naming this machine (short id, MagicDNS name, loopback, or the
953
+ // `interactive` sentinel pinned here) is a local run, not a self-SSH: the
954
+ // hop probes its own login shell and times out under load. Pinned here,
955
+ // ahead of every placement gate, so the bare-interactive default sees an
956
+ // explicit local choice rather than an empty host set.
957
+ {
958
+ const { isSelfHost } = await import('../lib/devices/self-host.js');
959
+ const { isDeviceInteractive, resolveInteractiveDevice } = await import('../lib/devices/interactive-host.js');
960
+ pinLocalWhenTargetIsSelf(options, (name) => isSelfHost(isDeviceInteractive(name) ? resolveInteractiveDevice() ?? name : name));
961
+ }
921
962
  // Cloud refinement flags without the placement are a typo, not a no-op.
922
963
  if (!options.cloud) {
923
964
  const { cloudFlagsWithoutCloud } = await import('./run-cloud.js');
@@ -1101,13 +1142,13 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1101
1142
  const { sessionRecoveryDestinationMatches, sessionRecoveryPeer, } = await import('../lib/session/recovery.js');
1102
1143
  const sourceMachine = resolvedResumeSource.machine;
1103
1144
  const sourcePeer = sessionRecoveryPeer(resolvedResumeSource);
1104
- const explicitPlacement = hostTargetGiven(options).length > 0;
1145
+ const explicitPlacement = hostTargetGiven(options).length > 0 || options.local === true;
1105
1146
  if (sourcePeer && !explicitPlacement) {
1106
1147
  options.host = sourcePeer;
1107
1148
  }
1108
1149
  else if (sourcePeer && explicitPlacement && !hostTargetGiven(options).some((host) => sessionRecoveryDestinationMatches(resolvedResumeSource, host))) {
1109
1150
  console.error(chalk.red(`Session ${resolvedResumeSource.shortId} must recover on ${sourcePeer}, where its conversation state is stored; ` +
1110
- `the requested device was ${hostTargetGiven(options).join(', ')}.`));
1151
+ `the requested device was ${hostTargetGiven(options).join(', ') || 'this machine'}.`));
1111
1152
  process.exit(1);
1112
1153
  }
1113
1154
  // Recovery is resolved on the device that owns the transcript. A remote
@@ -1246,8 +1287,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1246
1287
  if (!defaultPlacement)
1247
1288
  throw err;
1248
1289
  console.error(chalk.red(err.message));
1249
- const { machineId } = await import('../lib/machine-id.js');
1250
- console.error(chalk.gray(`Run here instead: agents run ${runBaseAgentName} --device ${machineId()}`));
1290
+ console.error(chalk.gray(`Run here instead: agents run ${runBaseAgentName} --local`));
1251
1291
  process.exit(1);
1252
1292
  }
1253
1293
  if (!options.quiet && result.banner) {
@@ -2,9 +2,10 @@
2
2
  * `agents setup term` — install the standalone `term` CLI if missing (PHNX-4092).
3
3
  *
4
4
  * The PTY engine lives in `@phnx-labs/term-cli` (extracted PHNX-4091);
5
- * agents-cli never rebundles it. The OAuth device-code driver (`agents fleet
6
- * login`, `agents auth mint`) spawns `term` on demand and fails loud when it is
7
- * absent, so onboarding installs it here like every other standalone tool.
5
+ * agents-cli never rebundles it. The setup-token mint behind `agents accounts
6
+ * add` / `agents accounts login` (auth-mint.ts → term-driver.ts) spawns `term`
7
+ * on demand and fails loud when it is absent, so onboarding installs it here
8
+ * like every other standalone tool.
8
9
  *
9
10
  * Unlike browser/computer/secrets there is nothing to configure — no profile,
10
11
  * no OS permission, no migrate step. A missing binary is a routine install
@@ -2,9 +2,10 @@
2
2
  * `agents setup term` — install the standalone `term` CLI if missing (PHNX-4092).
3
3
  *
4
4
  * The PTY engine lives in `@phnx-labs/term-cli` (extracted PHNX-4091);
5
- * agents-cli never rebundles it. The OAuth device-code driver (`agents fleet
6
- * login`, `agents auth mint`) spawns `term` on demand and fails loud when it is
7
- * absent, so onboarding installs it here like every other standalone tool.
5
+ * agents-cli never rebundles it. The setup-token mint behind `agents accounts
6
+ * add` / `agents accounts login` (auth-mint.ts → term-driver.ts) spawns `term`
7
+ * on demand and fails loud when it is absent, so onboarding installs it here
8
+ * like every other standalone tool.
8
9
  *
9
10
  * Unlike browser/computer/secrets there is nothing to configure — no profile,
10
11
  * no OS permission, no migrate step. A missing binary is a routine install
@@ -45,7 +46,7 @@ export async function runTermWizard() {
45
46
  export function registerSetupTermCommand(setupCmd) {
46
47
  setupCmd
47
48
  .command('term')
48
- .description('Install the standalone `term` CLI (the PTY engine fleet login and auth mint spawn) if missing.')
49
+ .description('Install the standalone `term` CLI (the PTY engine `agents accounts add`/`login` spawn) if missing.')
49
50
  .option('--install-only', 'Install the standalone term CLI (identical to the wizard; term needs no further setup)')
50
51
  .option('--terminal [backend]', 'Open interactive setup in a detected or selected terminal')
51
52
  .action(async (options) => {
@@ -289,7 +289,7 @@ export async function getSetupStatus() {
289
289
  { phase: 'browser', state: browserReady ? 'ready' : 'missing', detail: browserReady ? `profile ${browserProfile.name}` : browserProfile ? `profile ${browserProfile.name} cannot launch here` : installedBrowsers.length ? 'no default profile' : 'no supported browser found' },
290
290
  { phase: 'computer', state: computerState, detail: computerState === 'ready' ? 'helper trusted' : computerState === 'n/a' ? 'macOS local setup only' : 'helper not running or not trusted' },
291
291
  { phase: 'secrets', state: secretsReady ? 'ready' : 'missing', detail: secretsReady ? 'defaults chosen' : 'defaults not chosen' },
292
- { phase: 'term', state: termReady ? 'ready' : 'missing', detail: termReady ? 'installed' : 'not installed (fleet login / auth mint spawn it)' },
292
+ { phase: 'term', state: termReady ? 'ready' : 'missing', detail: termReady ? 'installed' : 'not installed (accounts add/login spawn it)' },
293
293
  { phase: 'accounts', state: minted.ready ? 'ready' : 'missing', detail: minted.detail },
294
294
  { phase: 'fleet', state: Object.keys(devices).length ? 'ready' : 'missing', detail: Object.keys(devices).length ? `${Object.keys(devices).length} device${Object.keys(devices).length === 1 ? '' : 's'} registered` : 'no devices registered' },
295
295
  { phase: 'watchdog', state: watchdogEnabled ? 'ready' : 'missing', detail: watchdogEnabled ? 'enabled on this device' : 'disabled on this device' },
@@ -462,7 +462,7 @@ export function registerSetupCommand(program) {
462
462
  agents setup browser # detect a browser + create the default profile
463
463
  agents setup computer # install the signed macOS helper + grant permissions
464
464
  agents setup secrets # choose secrets backend/policy defaults + import
465
- agents setup term # install the standalone term CLI (PTY engine for fleet login / auth mint)
465
+ agents setup term # install the standalone term CLI (PTY engine agents accounts add/login spawn)
466
466
  agents setup accounts # mint a Claude setup-token for unattended usage/probe
467
467
  agents setup fleet # discover Tailscale devices + configure SSH access
468
468
  agents setup watchdog # choose which devices run the daemon watchdog pass
@@ -109,6 +109,7 @@ export const RUN_OPTION_FORWARDING = {
109
109
  disableTmux: 'local-only',
110
110
  device: 'local-only',
111
111
  where: 'local-only', // expands into host/lease before dispatch; never re-forwarded
112
+ local: 'local-only', // this machine by definition — a local run never reaches the SSH boundary
112
113
  on: 'local-only',
113
114
  computer: 'local-only',
114
115
  any: 'local-only',
@@ -45,6 +45,8 @@ interface RunPlacementFlags {
45
45
  cloud?: boolean;
46
46
  /** --provider: refines the cloud placement; not a placement on its own. */
47
47
  provider?: string;
48
+ /** --local: this machine, said explicitly — the pin a bare interactive run needs to stay off `--device auto`. */
49
+ local?: boolean;
48
50
  }
49
51
  export declare class PlacementError extends Error {
50
52
  constructor(message: string);
@@ -94,9 +94,12 @@ export function placementFromRunFlags(flags) {
94
94
  const hasLease = flags.lease !== undefined && flags.lease !== false;
95
95
  const hasBox = !!flags.box;
96
96
  const hasCloud = flags.cloud === true;
97
+ const hasLocal = flags.local === true;
97
98
  const placementFlags = [];
98
99
  if (where)
99
100
  placementFlags.push('--where');
101
+ if (hasLocal)
102
+ placementFlags.push('--local');
100
103
  if (hostT)
101
104
  placementFlags.push('--device');
102
105
  if (hasLease)
@@ -111,6 +114,8 @@ export function placementFromRunFlags(flags) {
111
114
  }
112
115
  if (where)
113
116
  return parseWhereSpec(where, '--where');
117
+ if (hasLocal)
118
+ return { kind: 'local', source: '--local' };
114
119
  if (hasCloud)
115
120
  return { kind: 'cloud', target: flags.provider, source: '--cloud' };
116
121
  if (hasBox)
@@ -140,10 +140,11 @@ async function checkTool(row) {
140
140
  if (row.tool === 'secrets')
141
141
  return { ...row, checkedAtMs: Date.now(), detail: 'Installed. Secret access is checked when used; this check does not unlock secrets.' };
142
142
  // term is a headless PTY engine with no `status --json` health surface; it is
143
- // spawned on demand by the OAuth device-code driver (fleet login / auth mint).
143
+ // spawned on demand by the setup-token mint (`agents accounts add`/`login`,
144
+ // via auth-mint.ts → term-driver.ts, the sole term-client consumers).
144
145
  // Presence on PATH is the whole readiness signal — do not probe it.
145
146
  if (row.tool === 'term')
146
- return { ...row, readiness: 'ready', detail: 'Installed. Spawned on demand by fleet login and auth mint.', checkedAtMs: Date.now() };
147
+ return { ...row, readiness: 'ready', detail: 'Installed. Spawned on demand by `agents accounts add`/`login`.', checkedAtMs: Date.now() };
147
148
  try {
148
149
  const { command, prefix } = invocation(row.executable);
149
150
  const { stdout } = await probeCapture(command, [...prefix, 'status', '--json'], 8000, { acceptedExitCodes: [0, 1], maxOutputBytes: 256 * 1024 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.116",
3
+ "version": "1.22.117",
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",