@phnx-labs/agents-cli 1.22.26 → 1.22.27
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 +244 -0
- package/README.md +18 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +17 -16
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +47 -1
- package/dist/commands/sessions.js +345 -37
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +3 -3
- package/dist/index.js +16 -20
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +27 -1
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/exec.bench.d.ts +1 -0
- package/dist/lib/exec.bench.js +186 -0
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +10 -6
- package/dist/lib/hosts/dispatch.js +7 -3
- package/dist/lib/hosts/passthrough.d.ts +22 -0
- package/dist/lib/hosts/passthrough.js +14 -8
- package/dist/lib/hosts/progress.d.ts +2 -1
- package/dist/lib/hosts/progress.js +4 -3
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +4 -2
- package/dist/lib/hosts/ready.js +24 -8
- package/dist/lib/hosts/reconcile.d.ts +1 -1
- package/dist/lib/hosts/reconcile.js +16 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +90 -34
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +3 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +2 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/models.d.ts +30 -1
- package/dist/lib/models.js +42 -144
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/redact.js +8 -3
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.js +3 -2
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/db.d.ts +19 -2
- package/dist/lib/session/db.js +54 -5
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Device
|
|
2
|
+
* Device placement for `--device auto` / `--host auto`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Explicit auto placement probes live fleet health and harness readiness.
|
|
5
|
+
* Historical affinity remains for generic host resolution callers.
|
|
6
6
|
* Account selection is separate (`--strategy balanced` / rotate.ts).
|
|
7
7
|
*/
|
|
8
8
|
import { type AffinityRow } from './session/db.js';
|
|
9
|
+
import { type DevicePlacementSignal } from './teams/scheduler.js';
|
|
10
|
+
import type { AgentType } from './teams/agents.js';
|
|
9
11
|
/** Peakiness of usage weights; >1 amplifies the most-used option. */
|
|
10
12
|
export declare const DEFAULT_AFFINITY_ALPHA = 1.3;
|
|
11
13
|
export interface WeightedCandidate {
|
|
@@ -45,6 +47,29 @@ export interface DeviceAffinityPlan {
|
|
|
45
47
|
deviceCandidates: WeightedCandidate[];
|
|
46
48
|
pickedDeviceKey: string | null;
|
|
47
49
|
}
|
|
50
|
+
/** Live placement plan used by the explicit `--device auto` surface. */
|
|
51
|
+
export interface DeviceAutoPlan {
|
|
52
|
+
/** null means the local machine won the comparison. */
|
|
53
|
+
host: string | null;
|
|
54
|
+
candidates: Array<{
|
|
55
|
+
key: string;
|
|
56
|
+
loadPercent?: number;
|
|
57
|
+
installed?: boolean;
|
|
58
|
+
signedIn?: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
pickedDeviceKey: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pick the least-loaded healthy device that can run `agent` when the harness is
|
|
64
|
+
* known. `run auto` omits the agent and ranks the same live health/load signals
|
|
65
|
+
* without an installed-harness filter. The local machine participates in the
|
|
66
|
+
* same probe; a fully unusable fleet degrades to local instead of stranding a run.
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveDeviceAuto(agent?: string, opts?: {
|
|
69
|
+
eligibleHosts?: string[];
|
|
70
|
+
localMachine?: string;
|
|
71
|
+
probe?: (pool: string[], agent?: AgentType) => Promise<Map<string, DevicePlacementSignal>>;
|
|
72
|
+
}): Promise<DeviceAutoPlan>;
|
|
48
73
|
/**
|
|
49
74
|
* Resolve host for `--device auto`. Does NOT pick harness or accounts.
|
|
50
75
|
*/
|
|
@@ -81,6 +106,7 @@ export type DeviceAutoApplyResult = {
|
|
|
81
106
|
* returns `skipped` — never throws; callers must not hard-exit.
|
|
82
107
|
*/
|
|
83
108
|
export declare function applyDeviceAutoToOptions(options: DeviceAutoHostOptions, deps?: {
|
|
84
|
-
resolve?: () =>
|
|
109
|
+
resolve?: () => DeviceAutoPlan | Promise<DeviceAutoPlan>;
|
|
110
|
+
agent?: string;
|
|
85
111
|
accountPickerRequested?: boolean;
|
|
86
|
-
}): DeviceAutoApplyResult
|
|
112
|
+
}): Promise<DeviceAutoApplyResult>;
|
package/dist/lib/smart-launch.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Device
|
|
2
|
+
* Device placement for `--device auto` / `--host auto`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Explicit auto placement probes live fleet health and harness readiness.
|
|
5
|
+
* Historical affinity remains for generic host resolution callers.
|
|
6
6
|
* Account selection is separate (`--strategy balanced` / rotate.ts).
|
|
7
7
|
*/
|
|
8
8
|
import { queryAffinityRollup } from './session/db.js';
|
|
9
9
|
import { localMachineId } from './session/origin-machine.js';
|
|
10
10
|
import { loadDevicesSync } from './devices/registry.js';
|
|
11
11
|
import { normalizeHost } from './machine-id.js';
|
|
12
|
+
import { probePoolSignals } from './teams/placement-probe.js';
|
|
13
|
+
import { pickBestDevice } from './teams/scheduler.js';
|
|
12
14
|
/** Peakiness of usage weights; >1 amplifies the most-used option. */
|
|
13
15
|
export const DEFAULT_AFFINITY_ALPHA = 1.3;
|
|
14
16
|
/**
|
|
@@ -62,6 +64,39 @@ export function listOnlineDeviceNames(localName = localMachineId()) {
|
|
|
62
64
|
}
|
|
63
65
|
return [...names];
|
|
64
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Pick the least-loaded healthy device that can run `agent` when the harness is
|
|
69
|
+
* known. `run auto` omits the agent and ranks the same live health/load signals
|
|
70
|
+
* without an installed-harness filter. The local machine participates in the
|
|
71
|
+
* same probe; a fully unusable fleet degrades to local instead of stranding a run.
|
|
72
|
+
*/
|
|
73
|
+
export async function resolveDeviceAuto(agent, opts = {}) {
|
|
74
|
+
const local = normalizeHost(opts.localMachine ?? localMachineId());
|
|
75
|
+
const pool = [...new Set((opts.eligibleHosts ?? listOnlineDeviceNames(local)).map(normalizeHost))];
|
|
76
|
+
if (!pool.includes(local))
|
|
77
|
+
pool.push(local);
|
|
78
|
+
try {
|
|
79
|
+
const signals = await (opts.probe ?? probePoolSignals)(pool, agent);
|
|
80
|
+
const picked = pickBestDevice(pool, [], { signals, agentLabel: agent });
|
|
81
|
+
return {
|
|
82
|
+
host: picked === local ? null : picked,
|
|
83
|
+
pickedDeviceKey: picked,
|
|
84
|
+
candidates: pool.map((key) => ({
|
|
85
|
+
key,
|
|
86
|
+
loadPercent: signals.get(key)?.loadPercent,
|
|
87
|
+
installed: signals.get(key)?.installed,
|
|
88
|
+
signedIn: signals.get(key)?.signedIn,
|
|
89
|
+
})),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return {
|
|
94
|
+
host: null,
|
|
95
|
+
pickedDeviceKey: local,
|
|
96
|
+
candidates: [{ key: local }],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
65
100
|
/**
|
|
66
101
|
* Resolve host for `--device auto`. Does NOT pick harness or accounts.
|
|
67
102
|
*/
|
|
@@ -120,7 +155,7 @@ const HOST_SLOTS = ['host', 'device', 'on', 'computer'];
|
|
|
120
155
|
* Mutates `options` in place. On affinity failure, clears auto slots (local) and
|
|
121
156
|
* returns `skipped` — never throws; callers must not hard-exit.
|
|
122
157
|
*/
|
|
123
|
-
export function applyDeviceAutoToOptions(options, deps = {}) {
|
|
158
|
+
export async function applyDeviceAutoToOptions(options, deps = {}) {
|
|
124
159
|
let deprecationSmart = false;
|
|
125
160
|
if (options.smart) {
|
|
126
161
|
const anyHost = HOST_SLOTS.some((k) => typeof options[k] === 'string' && options[k].trim() !== '');
|
|
@@ -133,7 +168,8 @@ export function applyDeviceAutoToOptions(options, deps = {}) {
|
|
|
133
168
|
return { attempted: false, deprecationSmart };
|
|
134
169
|
}
|
|
135
170
|
try {
|
|
136
|
-
const
|
|
171
|
+
const resolve = deps.resolve ?? (() => resolveDeviceAuto(deps.agent));
|
|
172
|
+
const plan = await resolve();
|
|
137
173
|
const concrete = plan.host; // null = local
|
|
138
174
|
for (const k of HOST_SLOTS) {
|
|
139
175
|
if (isDeviceAuto(options[k])) {
|
|
@@ -145,9 +181,9 @@ export function applyDeviceAutoToOptions(options, deps = {}) {
|
|
|
145
181
|
options.balanced = true;
|
|
146
182
|
}
|
|
147
183
|
const hostLabel = concrete ?? 'local';
|
|
148
|
-
const deviceHint = plan.
|
|
184
|
+
const deviceHint = plan.candidates
|
|
149
185
|
.slice(0, 4)
|
|
150
|
-
.map((c) => `${c.key}:${c.
|
|
186
|
+
.map((c) => `${c.key}:${c.loadPercent === undefined ? '?' : `${Math.round(c.loadPercent)}%`}`)
|
|
151
187
|
.join(', ');
|
|
152
188
|
const acctNote = accountPickerRequested ? 'accounts=picker' : 'accounts=balanced';
|
|
153
189
|
return {
|
package/dist/lib/ssh-exec.d.ts
CHANGED
|
@@ -28,6 +28,31 @@ export declare function shellQuote(s: string): string;
|
|
|
28
28
|
* long-lived `-N` tunnel can't leave a zombie ssh + socket pinned on the laptop.
|
|
29
29
|
*/
|
|
30
30
|
export declare const SSH_OPTS: readonly string[];
|
|
31
|
+
/**
|
|
32
|
+
* Hard ceiling on the stdout one peer may return before its capture is aborted.
|
|
33
|
+
* A cross-machine fan-out streams every peer's `agents … --json` into memory in
|
|
34
|
+
* parallel, so an unbounded buffer means one runaway peer (a corrupt or
|
|
35
|
+
* pathologically large payload) can exhaust the caller's heap and take the whole
|
|
36
|
+
* sweep down with it — RUSH-2065 observed ~170MB retained across a single
|
|
37
|
+
* `--active` gather. 16 MiB is far above any legitimate JSON listing yet small
|
|
38
|
+
* enough that N peers in flight stay bounded. A peer that overflows is treated as
|
|
39
|
+
* unreachable rather than trusted with partial output.
|
|
40
|
+
*/
|
|
41
|
+
export declare const REMOTE_STDOUT_MAX_BYTES: number;
|
|
42
|
+
/**
|
|
43
|
+
* Accumulate SSH stdout as UTF-8 without corrupting a multi-byte code point that
|
|
44
|
+
* a chunk boundary splits — the streaming `on('data')` callbacks hand us raw
|
|
45
|
+
* `Buffer`s, and a naive `buf.toString()` per chunk mangles any character the
|
|
46
|
+
* kernel cut in half. `StringDecoder` holds the trailing partial bytes until the
|
|
47
|
+
* next chunk completes them.
|
|
48
|
+
*/
|
|
49
|
+
export declare class RemoteUtf8Accumulator {
|
|
50
|
+
private readonly decoder;
|
|
51
|
+
private value;
|
|
52
|
+
write(chunk: Buffer): void;
|
|
53
|
+
end(): string;
|
|
54
|
+
current(): string;
|
|
55
|
+
}
|
|
31
56
|
export declare function controlOpts(): string[];
|
|
32
57
|
/**
|
|
33
58
|
* Compose an ssh connection-option prefix.
|
|
@@ -128,6 +153,8 @@ export interface SshStreamOptions {
|
|
|
128
153
|
* interactive credential-copy path (RUSH-1767).
|
|
129
154
|
*/
|
|
130
155
|
hostKeyOpts?: string[];
|
|
156
|
+
/** Additional OpenSSH argv placed before the target (for example `-i <path>`). */
|
|
157
|
+
extraSshArgs?: string[];
|
|
131
158
|
}
|
|
132
159
|
/**
|
|
133
160
|
* Foreground counterpart to `sshExec`: run `remoteCmd` on `target` with the
|
package/dist/lib/ssh-exec.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* canonical definition; `commands/secrets.ts` re-exports it.
|
|
9
9
|
*/
|
|
10
10
|
import { spawn, spawnSync } from 'child_process';
|
|
11
|
+
import { StringDecoder } from 'string_decoder';
|
|
11
12
|
import * as fs from 'fs';
|
|
12
13
|
import * as path from 'path';
|
|
13
14
|
import { getCacheDir } from './state.js';
|
|
@@ -46,6 +47,38 @@ export const SSH_OPTS = [
|
|
|
46
47
|
'-o', 'ServerAliveInterval=15',
|
|
47
48
|
'-o', 'ServerAliveCountMax=3',
|
|
48
49
|
];
|
|
50
|
+
/**
|
|
51
|
+
* Hard ceiling on the stdout one peer may return before its capture is aborted.
|
|
52
|
+
* A cross-machine fan-out streams every peer's `agents … --json` into memory in
|
|
53
|
+
* parallel, so an unbounded buffer means one runaway peer (a corrupt or
|
|
54
|
+
* pathologically large payload) can exhaust the caller's heap and take the whole
|
|
55
|
+
* sweep down with it — RUSH-2065 observed ~170MB retained across a single
|
|
56
|
+
* `--active` gather. 16 MiB is far above any legitimate JSON listing yet small
|
|
57
|
+
* enough that N peers in flight stay bounded. A peer that overflows is treated as
|
|
58
|
+
* unreachable rather than trusted with partial output.
|
|
59
|
+
*/
|
|
60
|
+
export const REMOTE_STDOUT_MAX_BYTES = 16 * 1024 * 1024;
|
|
61
|
+
/**
|
|
62
|
+
* Accumulate SSH stdout as UTF-8 without corrupting a multi-byte code point that
|
|
63
|
+
* a chunk boundary splits — the streaming `on('data')` callbacks hand us raw
|
|
64
|
+
* `Buffer`s, and a naive `buf.toString()` per chunk mangles any character the
|
|
65
|
+
* kernel cut in half. `StringDecoder` holds the trailing partial bytes until the
|
|
66
|
+
* next chunk completes them.
|
|
67
|
+
*/
|
|
68
|
+
export class RemoteUtf8Accumulator {
|
|
69
|
+
decoder = new StringDecoder('utf8');
|
|
70
|
+
value = '';
|
|
71
|
+
write(chunk) {
|
|
72
|
+
this.value += this.decoder.write(chunk);
|
|
73
|
+
}
|
|
74
|
+
end() {
|
|
75
|
+
this.value += this.decoder.end();
|
|
76
|
+
return this.value;
|
|
77
|
+
}
|
|
78
|
+
current() {
|
|
79
|
+
return this.value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
49
82
|
/**
|
|
50
83
|
* OpenSSH connection-multiplexing options. The first connection to a host opens
|
|
51
84
|
* a control socket; subsequent connections (even from a *separate* `agents`
|
|
@@ -292,7 +325,7 @@ export function sshStream(target, remoteCmd, opts = {}) {
|
|
|
292
325
|
assertValidSshTarget(target);
|
|
293
326
|
const mux = opts.multiplex === false ? [] : controlOpts();
|
|
294
327
|
const tty = opts.tty ? ['-tt'] : [];
|
|
295
|
-
const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...tty, target, remoteCmd];
|
|
328
|
+
const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...(opts.extraSshArgs ?? []), ...tty, target, remoteCmd];
|
|
296
329
|
const res = spawnSync('ssh', args, { stdio: 'inherit' });
|
|
297
330
|
if (typeof res.status === 'number')
|
|
298
331
|
return res.status;
|
package/dist/lib/ssh-tunnel.d.ts
CHANGED
|
@@ -27,13 +27,14 @@ export interface StartTunnelOptions {
|
|
|
27
27
|
* of one CDP session and kills it on cleanup.
|
|
28
28
|
*/
|
|
29
29
|
detached?: boolean;
|
|
30
|
+
extraSshArgs?: string[];
|
|
30
31
|
}
|
|
31
32
|
/** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
|
|
32
33
|
*
|
|
33
34
|
* Composes the shared hardened baseline (`SSH_OPTS`) rather than re-listing it,
|
|
34
35
|
* so the tunnel inherits the same options — crucially the keepalive, which lets
|
|
35
36
|
* a dropped `-N` tunnel exit instead of lingering as a zombie on the laptop. */
|
|
36
|
-
export declare function buildTunnelArgs(user: string, host: string, localPort: number, remotePort: number): string[];
|
|
37
|
+
export declare function buildTunnelArgs(user: string, host: string, localPort: number, remotePort: number, extraSshArgs?: string[]): string[];
|
|
37
38
|
/**
|
|
38
39
|
* Spawn `ssh -L localPort:127.0.0.1:remotePort -N user@host`.
|
|
39
40
|
*
|
|
@@ -151,7 +152,7 @@ export declare function scpRemotePath(remotePath: string): string;
|
|
|
151
152
|
* assert the real binary copy path keeps BatchMode and does not route bytes
|
|
152
153
|
* through a PowerShell decoder.
|
|
153
154
|
*/
|
|
154
|
-
export declare function buildScpArgs(target: string, remotePath: string, filePath: string): string[];
|
|
155
|
+
export declare function buildScpArgs(target: string, remotePath: string, filePath: string, extraSshArgs?: string[]): string[];
|
|
155
156
|
/**
|
|
156
157
|
* `setup --host`: push the exe, then register + start the LOGON task. Remote
|
|
157
158
|
* PowerShell hops go through `sshExec` (BatchMode key auth — the same hardening
|
package/dist/lib/ssh-tunnel.js
CHANGED
|
@@ -28,7 +28,7 @@ import { sshExec, SSH_OPTS, assertValidSshTarget } from './ssh-exec.js';
|
|
|
28
28
|
import { backgroundSpawnOptions } from './platform/process.js';
|
|
29
29
|
import { encodePowerShell } from './browser/drivers/ssh.js';
|
|
30
30
|
import { getDevice } from './devices/registry.js';
|
|
31
|
-
import { sshTargetFor } from './devices/connect.js';
|
|
31
|
+
import { deviceIdentityArgs, sshTargetFor } from './devices/connect.js';
|
|
32
32
|
import { hostNameFor } from './devices/ssh-config.js';
|
|
33
33
|
import { getCacheDir } from './state.js';
|
|
34
34
|
import { getCliVersion } from './version.js';
|
|
@@ -38,12 +38,13 @@ import { openComputerClient, resolveTcpEndpoint } from './computer-rpc.js';
|
|
|
38
38
|
* Composes the shared hardened baseline (`SSH_OPTS`) rather than re-listing it,
|
|
39
39
|
* so the tunnel inherits the same options — crucially the keepalive, which lets
|
|
40
40
|
* a dropped `-N` tunnel exit instead of lingering as a zombie on the laptop. */
|
|
41
|
-
export function buildTunnelArgs(user, host, localPort, remotePort) {
|
|
41
|
+
export function buildTunnelArgs(user, host, localPort, remotePort, extraSshArgs = []) {
|
|
42
42
|
return [
|
|
43
43
|
'-L',
|
|
44
44
|
`${localPort}:127.0.0.1:${remotePort}`,
|
|
45
45
|
`${user}@${host}`,
|
|
46
46
|
'-N',
|
|
47
|
+
...extraSshArgs,
|
|
47
48
|
...SSH_OPTS,
|
|
48
49
|
];
|
|
49
50
|
}
|
|
@@ -69,7 +70,7 @@ export function startSSHTunnel(user, host, localPort, remotePort, opts = {}) {
|
|
|
69
70
|
reject(err);
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
72
|
-
const args = buildTunnelArgs(user, host, localPort, remotePort);
|
|
73
|
+
const args = buildTunnelArgs(user, host, localPort, remotePort, opts.extraSshArgs);
|
|
73
74
|
const tunnel = spawn('ssh', args, {
|
|
74
75
|
stdio: opts.detached ? 'ignore' : ['ignore', 'ignore', 'pipe'],
|
|
75
76
|
...(opts.detached ? backgroundSpawnOptions() : { detached: false, windowsHide: true }),
|
|
@@ -360,16 +361,16 @@ export function scpRemotePath(remotePath) {
|
|
|
360
361
|
* assert the real binary copy path keeps BatchMode and does not route bytes
|
|
361
362
|
* through a PowerShell decoder.
|
|
362
363
|
*/
|
|
363
|
-
export function buildScpArgs(target, remotePath, filePath) {
|
|
364
|
-
return [...SSH_OPTS, filePath, `${target}:${scpRemotePath(remotePath)}`];
|
|
364
|
+
export function buildScpArgs(target, remotePath, filePath, extraSshArgs = []) {
|
|
365
|
+
return [...extraSshArgs, ...SSH_OPTS, filePath, `${target}:${scpRemotePath(remotePath)}`];
|
|
365
366
|
}
|
|
366
367
|
/**
|
|
367
368
|
* Copy a local file to the remote destination with scp. This is a binary
|
|
368
369
|
* transfer; no base64 transform runs on either side.
|
|
369
370
|
*/
|
|
370
|
-
function copyFileOverScp(target, remotePath, filePath, timeoutMs = 600_000) {
|
|
371
|
+
function copyFileOverScp(target, remotePath, filePath, extraSshArgs = [], timeoutMs = 600_000) {
|
|
371
372
|
return new Promise((resolve, reject) => {
|
|
372
|
-
const child = spawn('scp', buildScpArgs(target, remotePath, filePath), {
|
|
373
|
+
const child = spawn('scp', buildScpArgs(target, remotePath, filePath, extraSshArgs), {
|
|
373
374
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
374
375
|
});
|
|
375
376
|
let stderr = '';
|
|
@@ -397,11 +398,12 @@ function copyFileOverScp(target, remotePath, filePath, timeoutMs = 600_000) {
|
|
|
397
398
|
* scp transfer. Throws with the remote stderr on any failure.
|
|
398
399
|
*/
|
|
399
400
|
export async function setupRemoteHelper(name) {
|
|
400
|
-
const { target } = await resolveRemoteDevice(name);
|
|
401
|
+
const { device, target } = await resolveRemoteDevice(name);
|
|
402
|
+
const identityArgs = deviceIdentityArgs(device);
|
|
401
403
|
// Local build output, else the checksum-verified GitHub release asset for
|
|
402
404
|
// this CLI version. Throws naming the tag it checked when neither exists.
|
|
403
405
|
const exe = await ensureWinHelperExe();
|
|
404
|
-
const prep = sshExec(target, encodePowerShell(buildPushScript()), { timeoutMs: 60_000 });
|
|
406
|
+
const prep = sshExec(target, encodePowerShell(buildPushScript()), { timeoutMs: 60_000, extraSshArgs: identityArgs });
|
|
405
407
|
if (prep.code !== 0) {
|
|
406
408
|
throw new Error(`preparing helper exe path on '${name}' failed (exit ${prep.code ?? 'null'}): ${prep.stderr.trim() || prep.stdout.trim()}`);
|
|
407
409
|
}
|
|
@@ -409,12 +411,12 @@ export async function setupRemoteHelper(name) {
|
|
|
409
411
|
if (!remotePath) {
|
|
410
412
|
throw new Error(`preparing helper exe path on '${name}' did not return a destination path`);
|
|
411
413
|
}
|
|
412
|
-
const push = await copyFileOverScp(target, remotePath, exe);
|
|
414
|
+
const push = await copyFileOverScp(target, remotePath, exe, identityArgs);
|
|
413
415
|
if (push.code !== 0) {
|
|
414
416
|
throw new Error(`pushing helper exe to '${name}' failed (exit ${push.code ?? 'null'}): ${push.stderr.trim()}`);
|
|
415
417
|
}
|
|
416
418
|
const expectedBytes = fs.statSync(exe).size;
|
|
417
|
-
const verify = sshExec(target, encodePowerShell(buildVerifyPushScript(remotePath, expectedBytes)), { timeoutMs: 60_000 });
|
|
419
|
+
const verify = sshExec(target, encodePowerShell(buildVerifyPushScript(remotePath, expectedBytes)), { timeoutMs: 60_000, extraSshArgs: identityArgs });
|
|
418
420
|
if (verify.code !== 0) {
|
|
419
421
|
throw new Error(`verifying helper exe on '${name}' failed (exit ${verify.code ?? 'null'}): ${verify.stderr.trim() || verify.stdout.trim()}`);
|
|
420
422
|
}
|
|
@@ -423,7 +425,7 @@ export async function setupRemoteHelper(name) {
|
|
|
423
425
|
// refuses to start without a token, so a token-less (open-to-any-local-process)
|
|
424
426
|
// daemon can no longer be stood up through the CLI.
|
|
425
427
|
const token = generateToken();
|
|
426
|
-
const tokWrite = sshExec(target, encodePowerShell(buildWriteTokenScript(token)), { timeoutMs: 60_000 });
|
|
428
|
+
const tokWrite = sshExec(target, encodePowerShell(buildWriteTokenScript(token)), { timeoutMs: 60_000, extraSshArgs: identityArgs });
|
|
427
429
|
if (tokWrite.code !== 0) {
|
|
428
430
|
throw new Error(`writing helper token on '${name}' failed (exit ${tokWrite.code ?? 'null'}): ${tokWrite.stderr.trim() || tokWrite.stdout.trim()}`);
|
|
429
431
|
}
|
|
@@ -434,6 +436,7 @@ export async function setupRemoteHelper(name) {
|
|
|
434
436
|
// Register + start the LOGON task, pointing it at the token file.
|
|
435
437
|
const reg = sshExec(target, encodePowerShell(buildRegisterTaskScript(REMOTE_HELPER_PORT, REMOTE_TASK_NAME, remoteTokenPath)), {
|
|
436
438
|
timeoutMs: 60_000,
|
|
439
|
+
extraSshArgs: identityArgs,
|
|
437
440
|
});
|
|
438
441
|
if (reg.code !== 0) {
|
|
439
442
|
throw new Error(`registering scheduled task on '${name}' failed (exit ${reg.code ?? 'null'}): ${reg.stderr.trim() || reg.stdout.trim()}`);
|
|
@@ -460,10 +463,13 @@ export function pickFreePort() {
|
|
|
460
463
|
* the state (and leaves the tunnel running in the background).
|
|
461
464
|
*/
|
|
462
465
|
export async function startRemoteTunnel(name) {
|
|
463
|
-
const { target, user, host } = await resolveRemoteDevice(name);
|
|
466
|
+
const { device, target, user, host } = await resolveRemoteDevice(name);
|
|
464
467
|
const remotePort = REMOTE_HELPER_PORT;
|
|
465
468
|
const localPort = await pickFreePort();
|
|
466
|
-
const tunnel = await startSSHTunnel(user, host, localPort, remotePort, {
|
|
469
|
+
const tunnel = await startSSHTunnel(user, host, localPort, remotePort, {
|
|
470
|
+
detached: true,
|
|
471
|
+
extraSshArgs: deviceIdentityArgs(device),
|
|
472
|
+
});
|
|
467
473
|
const tunnelPid = tunnel.pid ?? 0;
|
|
468
474
|
// Verify the daemon answers through the tunnel before we record it. This is
|
|
469
475
|
// the real end-to-end check: tunnel up + daemon listening + RPC round-trips.
|
|
@@ -543,8 +549,11 @@ export async function stopRemoteHelper(name) {
|
|
|
543
549
|
}
|
|
544
550
|
let taskRemoved = false;
|
|
545
551
|
try {
|
|
546
|
-
const { target } = await resolveRemoteDevice(name);
|
|
547
|
-
const res = sshExec(target, encodePowerShell(buildUnregisterTaskScript(REMOTE_TASK_NAME)), {
|
|
552
|
+
const { device, target } = await resolveRemoteDevice(name);
|
|
553
|
+
const res = sshExec(target, encodePowerShell(buildUnregisterTaskScript(REMOTE_TASK_NAME)), {
|
|
554
|
+
timeoutMs: 60_000,
|
|
555
|
+
extraSshArgs: deviceIdentityArgs(device),
|
|
556
|
+
});
|
|
548
557
|
taskRemoved = res.code === 0;
|
|
549
558
|
}
|
|
550
559
|
catch {
|
|
@@ -76,6 +76,7 @@ export declare const loadUsage: ModuleLoader;
|
|
|
76
76
|
export declare const loadCost: ModuleLoader;
|
|
77
77
|
export declare const loadInsights: ModuleLoader;
|
|
78
78
|
export declare const loadPerf: ModuleLoader;
|
|
79
|
+
export declare const loadBench: ModuleLoader;
|
|
79
80
|
export declare const loadTrends: ModuleLoader;
|
|
80
81
|
export declare const loadOutput: ModuleLoader;
|
|
81
82
|
export declare const loadBudget: ModuleLoader;
|
|
@@ -54,6 +54,7 @@ export const loadUsage = async () => (await import('../../commands/usage.js')).r
|
|
|
54
54
|
export const loadCost = async () => (await import('../../commands/cost.js')).registerCostCommand;
|
|
55
55
|
export const loadInsights = async () => (await import('../../commands/insights.js')).registerInsightsCommand;
|
|
56
56
|
export const loadPerf = async () => (await import('../../commands/perf.js')).registerPerfCommand;
|
|
57
|
+
export const loadBench = async () => (await import('../../commands/bench.js')).registerBenchCommand;
|
|
57
58
|
// Thin deprecated alias of `agents insights mix` — no second mix implementation.
|
|
58
59
|
export const loadTrends = async () => (await import('../../commands/trends.js')).registerTrendsCommand;
|
|
59
60
|
export const loadOutput = async () => (await import('../../commands/output.js')).registerOutputCommand;
|
|
@@ -191,6 +192,7 @@ export const COMMAND_LOADERS = {
|
|
|
191
192
|
cost: [loadCost],
|
|
192
193
|
insights: [loadInsights],
|
|
193
194
|
perf: [loadPerf],
|
|
195
|
+
bench: [loadBench],
|
|
194
196
|
trends: [loadTrends],
|
|
195
197
|
output: [loadOutput],
|
|
196
198
|
budget: [loadBudget],
|
|
@@ -15,7 +15,7 @@ export declare function clearPlacementSignalCache(): void;
|
|
|
15
15
|
* excludes nor prefers them). Never throws — a probe failure degrades to a
|
|
16
16
|
* missing/partial signal.
|
|
17
17
|
*/
|
|
18
|
-
export declare function probePoolSignals(pool: string[], agent
|
|
18
|
+
export declare function probePoolSignals(pool: string[], agent?: AgentType, opts?: {
|
|
19
19
|
force?: boolean;
|
|
20
20
|
now?: number;
|
|
21
21
|
}): Promise<Map<string, DevicePlacementSignal>>;
|
|
@@ -7,26 +7,25 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - reachability + headroom + load ← {@link probeFleetStats} (one parallel
|
|
9
9
|
* SSH fan-out over the pool; the local box is measured directly).
|
|
10
|
-
* - requested agent installed
|
|
10
|
+
* - requested agent installed + signed in (when known) ← a one-shot readiness
|
|
11
11
|
* probe per remote device ({@link buildReadyProbeCommand} → `agents view`),
|
|
12
12
|
* the local box via {@link checkCliAvailable}/{@link checkCliSignedIn}.
|
|
13
13
|
*
|
|
14
|
-
* The result is cached briefly per (pool, agent) so a `teams start` wave that
|
|
14
|
+
* The result is cached briefly per (pool, agent-or-any) so a `teams start` wave that
|
|
15
15
|
* places N teammates probes the pool ONCE, not N times — the roster-count part
|
|
16
16
|
* of the rank stays live (the pure pick recounts the roster each call), only the
|
|
17
17
|
* SSH-measured load/harness snapshot is reused within the TTL.
|
|
18
18
|
*
|
|
19
19
|
* All SSH here is via `execFile` (async, bounded) so the whole pool is probed in
|
|
20
20
|
* parallel; a slow or wedged box degrades to "no signal" instead of blocking the
|
|
21
|
-
* launch.
|
|
22
|
-
*
|
|
23
|
-
* only, and the install gate (not sign-in) is what drives the fail-loud.
|
|
21
|
+
* launch. The readiness payload is `agents view --json`, so remote and local
|
|
22
|
+
* candidates carry the same installed/sign-in verdict.
|
|
24
23
|
*/
|
|
25
24
|
import { execFile } from 'child_process';
|
|
26
25
|
import { probeFleetStats, headroom } from '../devices/health.js';
|
|
27
26
|
import { loadDevicesSync } from '../devices/registry.js';
|
|
28
27
|
import { buildSshInvocation, writeAskpassShim } from '../devices/connect.js';
|
|
29
|
-
import { buildReadyProbeCommand, parseReadyProbe, viewHasAgent } from '../hosts/ready.js';
|
|
28
|
+
import { buildReadyProbeCommand, parseReadyProbe, viewAgentSignedIn, viewHasAgent, } from '../hosts/ready.js';
|
|
30
29
|
import { localMachineId } from '../session/origin-machine.js';
|
|
31
30
|
import { normalizeHost } from '../machine-id.js';
|
|
32
31
|
import { checkCliAvailable, checkCliSignedIn } from './agents.js';
|
|
@@ -39,7 +38,7 @@ export const READY_PROBE_TIMEOUT_MS = 8_000;
|
|
|
39
38
|
export const SIGNAL_TTL_MS = 15_000;
|
|
40
39
|
const cache = new Map();
|
|
41
40
|
function cacheKey(pool, agent) {
|
|
42
|
-
return `${agent}::${[...pool].map(normalizeHost).sort().join(',')}`;
|
|
41
|
+
return `${agent ?? 'any-agent'}::${[...pool].map(normalizeHost).sort().join(',')}`;
|
|
43
42
|
}
|
|
44
43
|
/** Clear the probe cache — for tests and after a device-registry change. */
|
|
45
44
|
export function clearPlacementSignalCache() {
|
|
@@ -51,7 +50,7 @@ export function clearPlacementSignalCache() {
|
|
|
51
50
|
* failure) — reachability is then left to {@link probeFleetStats}; `false` when
|
|
52
51
|
* the box answered but agents-cli or the agent is absent (a genuine can't-run).
|
|
53
52
|
*/
|
|
54
|
-
function
|
|
53
|
+
function probeRemoteReadiness(device, agent) {
|
|
55
54
|
let args;
|
|
56
55
|
let env;
|
|
57
56
|
try {
|
|
@@ -62,18 +61,19 @@ function probeRemoteInstalled(device, agent) {
|
|
|
62
61
|
({ args, env } = buildSshInvocation(device, [cmd], shim, {}, { agentOnly: true }));
|
|
63
62
|
}
|
|
64
63
|
catch {
|
|
65
|
-
return Promise.resolve(undefined);
|
|
64
|
+
return Promise.resolve({ installed: undefined, signedIn: undefined });
|
|
66
65
|
}
|
|
67
66
|
return new Promise((resolve) => {
|
|
68
67
|
execFile('ssh', args, { encoding: 'utf-8', env: { ...process.env, ...env }, timeout: READY_PROBE_TIMEOUT_MS }, (err, stdout) => {
|
|
69
68
|
if (err || !stdout)
|
|
70
|
-
return resolve(undefined);
|
|
69
|
+
return resolve({ installed: undefined, signedIn: undefined });
|
|
71
70
|
const probe = parseReadyProbe(stdout);
|
|
72
71
|
if (!probe.reachable)
|
|
73
|
-
return resolve(
|
|
72
|
+
return resolve({ installed: undefined, signedIn: undefined });
|
|
74
73
|
if (!probe.version)
|
|
75
|
-
return resolve(
|
|
76
|
-
|
|
74
|
+
return resolve({ installed: false, signedIn: false });
|
|
75
|
+
const installed = viewHasAgent(probe.view, agent);
|
|
76
|
+
resolve({ installed, signedIn: installed ? viewAgentSignedIn(probe.view, agent) : false });
|
|
77
77
|
});
|
|
78
78
|
});
|
|
79
79
|
}
|
|
@@ -103,17 +103,17 @@ export async function probePoolSignals(pool, agent, opts = {}) {
|
|
|
103
103
|
}
|
|
104
104
|
const selfProfile = profiles.find((d) => normalizeHost(d.name) === self);
|
|
105
105
|
const stats = await probeFleetStats(profiles, { selfName: selfProfile?.name });
|
|
106
|
-
const installed = new Map(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
const installed = new Map(agent
|
|
107
|
+
? await Promise.all(profiles.map(async (d) => {
|
|
108
|
+
const isSelf = normalizeHost(d.name) === self;
|
|
109
|
+
if (isSelf) {
|
|
110
|
+
const inst = checkCliAvailable(agent)[0];
|
|
111
|
+
const signedIn = inst ? await checkCliSignedIn(agent) : undefined;
|
|
112
|
+
return [d.name, { installed: inst, signedIn }];
|
|
113
|
+
}
|
|
114
|
+
return [d.name, await probeRemoteReadiness(d, agent)];
|
|
115
|
+
}))
|
|
116
|
+
: []);
|
|
117
117
|
const signals = new Map();
|
|
118
118
|
for (const name of pool) {
|
|
119
119
|
const d = lookup(name);
|
|
@@ -179,6 +179,14 @@ export declare const AGENT_HOOK_SCHEMA = 5;
|
|
|
179
179
|
export declare function agentPaneDiedHook(sessionName: string, agentPane: string): string;
|
|
180
180
|
/** Stamp a session's hook-schema marker to the current version. */
|
|
181
181
|
export declare function markSessionHookSchema(name: string, socket?: string): Promise<void>;
|
|
182
|
+
/**
|
|
183
|
+
* Decide whether a native resume may attach an existing managed tmux session.
|
|
184
|
+
* Only a positively resolved, living agent pane is reusable. Metadata-less
|
|
185
|
+
* legacy sessions and stale metadata resolve their real first pane through
|
|
186
|
+
* tmux before the decision; a dead or unreadable session is reaped so the
|
|
187
|
+
* caller can create a fresh wrapper for the resumed harness.
|
|
188
|
+
*/
|
|
189
|
+
export declare function prepareSessionForResume(name: string, socket?: string): Promise<'attach' | 'create'>;
|
|
182
190
|
/**
|
|
183
191
|
* Retrofit the current guarded `pane-died` hook onto every managed `agents run`
|
|
184
192
|
* session whose hook predates AGENT_HOOK_SCHEMA. Idempotent and NON-DESTRUCTIVE:
|
package/dist/lib/tmux/session.js
CHANGED
|
@@ -359,6 +359,28 @@ async function lowestPaneId(name, socket) {
|
|
|
359
359
|
return undefined;
|
|
360
360
|
return ids.reduce((lo, id) => (parseInt(id.slice(1), 10) < parseInt(lo.slice(1), 10) ? id : lo));
|
|
361
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Decide whether a native resume may attach an existing managed tmux session.
|
|
364
|
+
* Only a positively resolved, living agent pane is reusable. Metadata-less
|
|
365
|
+
* legacy sessions and stale metadata resolve their real first pane through
|
|
366
|
+
* tmux before the decision; a dead or unreadable session is reaped so the
|
|
367
|
+
* caller can create a fresh wrapper for the resumed harness.
|
|
368
|
+
*/
|
|
369
|
+
export async function prepareSessionForResume(name, socket) {
|
|
370
|
+
const sock = socket ?? getDefaultSocketPath();
|
|
371
|
+
if (!(await hasSession(name, sock)))
|
|
372
|
+
return 'create';
|
|
373
|
+
const recordedPane = readSessionMeta(name)?.pane;
|
|
374
|
+
const recordedState = recordedPane ? await paneExitStatus(recordedPane, sock) : undefined;
|
|
375
|
+
const pane = recordedPane && recordedState?.found
|
|
376
|
+
? recordedPane
|
|
377
|
+
: await lowestPaneId(name, sock);
|
|
378
|
+
const state = pane ? await paneExitStatus(pane, sock) : undefined;
|
|
379
|
+
if (pane && state?.found && !state.dead)
|
|
380
|
+
return 'attach';
|
|
381
|
+
await killSession(name, sock);
|
|
382
|
+
return 'create';
|
|
383
|
+
}
|
|
362
384
|
/**
|
|
363
385
|
* Retrofit the current guarded `pane-died` hook onto every managed `agents run`
|
|
364
386
|
* session whose hook predates AGENT_HOOK_SCHEMA. Idempotent and NON-DESTRUCTIVE:
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -1064,6 +1064,8 @@ export interface HostEntry {
|
|
|
1064
1064
|
address?: string;
|
|
1065
1065
|
/** SSH user — inline hosts only. */
|
|
1066
1066
|
user?: string;
|
|
1067
|
+
/** Explicit private key inherited from a fleet device profile. */
|
|
1068
|
+
identityFile?: string;
|
|
1067
1069
|
/** Captured at enroll probe. */
|
|
1068
1070
|
os?: string;
|
|
1069
1071
|
/** Free-form capability tags for routing (e.g. ['gpu']). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.27",
|
|
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",
|