@phnx-labs/agents-cli 1.22.49 → 1.22.51
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 +229 -0
- package/README.md +10 -0
- package/dist/bootstrap.js +1 -1
- package/dist/commands/accounts.js +1 -13
- package/dist/commands/browser.d.ts +10 -0
- package/dist/commands/browser.js +77 -2
- package/dist/commands/config.js +20 -0
- package/dist/commands/daemon-test-harness.d.ts +42 -0
- package/dist/commands/daemon-test-harness.js +90 -0
- package/dist/commands/device-config-test-harness.d.ts +11 -0
- package/dist/commands/device-config-test-harness.js +59 -0
- package/dist/commands/exec.js +24 -1
- package/dist/commands/feedback.js +1 -1
- package/dist/commands/menubar.js +6 -4
- package/dist/commands/share.js +9 -4
- package/dist/commands/teams.d.ts +12 -0
- package/dist/commands/teams.js +44 -67
- package/dist/commands/traces.js +36 -4
- package/dist/lib/account-provider-registry.d.ts +9 -0
- package/dist/lib/account-provider-registry.js +20 -0
- package/dist/lib/accounting/account-pool-collect.d.ts +34 -0
- package/dist/lib/accounting/account-pool-collect.js +71 -0
- package/dist/lib/accounting/account-pool.d.ts +46 -0
- package/dist/lib/accounting/account-pool.js +33 -0
- package/dist/lib/accounting/capacity.d.ts +24 -0
- package/dist/lib/accounting/capacity.js +30 -0
- package/dist/lib/accounting/rotate.d.ts +11 -18
- package/dist/lib/accounting/rotate.js +6 -24
- package/dist/lib/claude-statusline.d.ts +9 -0
- package/dist/lib/claude-statusline.js +45 -4
- package/dist/lib/computer/ssh-tunnel.d.ts +7 -6
- package/dist/lib/computer/ssh-tunnel.js +13 -8
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +9 -2
- package/dist/lib/device-config.js +23 -0
- package/dist/lib/factory/snapshot.d.ts +1 -1
- package/dist/lib/factory/snapshot.js +1 -1
- package/dist/lib/helper-download.d.ts +12 -2
- package/dist/lib/helper-download.js +12 -2
- package/dist/lib/installations/migrate.js +4 -4
- package/dist/lib/installations/store.js +6 -3
- package/dist/lib/installations/versions.js +48 -46
- package/dist/lib/menubar/install-menubar.d.ts +71 -8
- package/dist/lib/menubar/install-menubar.js +183 -24
- package/dist/lib/share/publish.d.ts +8 -3
- package/dist/lib/share/publish.js +6 -3
- package/dist/lib/share/worker-template.js +239 -22
- package/dist/lib/star-nudge.d.ts +2 -2
- package/dist/lib/star-nudge.js +2 -2
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +2 -2
- package/dist/lib/teams/agents.d.ts +14 -12
- package/dist/lib/teams/agents.js +188 -73
- package/dist/lib/teams/api.d.ts +6 -1
- package/dist/lib/teams/api.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +7 -0
- package/dist/lib/teams/scheduler.js +26 -8
- package/dist/lib/traces/sync.d.ts +30 -0
- package/dist/lib/traces/sync.js +103 -10
- package/package.json +3 -3
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { providerAuthenticatesHarness } from '../account-provider-registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which provider accounts can authenticate `agent`. An account is a candidate for
|
|
4
|
+
* harness H only when its provider adapter has an `envFor(H, kind)` mapping
|
|
5
|
+
* (`providerAuthenticatesHarness`, the same check the account UI uses) — so a
|
|
6
|
+
* Cursor key never enters Claude's pool and a Claude setup-token never enters
|
|
7
|
+
* Codex's. This is registry-driven, no per-harness `else if`: a harness gets a
|
|
8
|
+
* provider-account pool exactly when the adapter table maps a provider to it —
|
|
9
|
+
* today claude, codex, grok, cursor, opencode (plus gemini/antigravity). A harness
|
|
10
|
+
* with only a native OAuth login and no provider adapter — kimi — returns `[]`
|
|
11
|
+
* here and keeps balancing its native logins only.
|
|
12
|
+
*
|
|
13
|
+
* `accountKey` is synthetic here (`${agent}:name=${name}`) — a stable per-account
|
|
14
|
+
* key so balancing can include it immediately; the real agent-scoped identity
|
|
15
|
+
* (email / org uuid) is backfilled by identity capture and replaces it. `email`
|
|
16
|
+
* is null until then, which routes the account as usage-unverified but still a
|
|
17
|
+
* candidate, never excluded.
|
|
18
|
+
*/
|
|
19
|
+
export function registryPoolCandidates(records, agent) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const r of records) {
|
|
22
|
+
if (!providerAuthenticatesHarness(r.provider, r.auth, agent))
|
|
23
|
+
continue;
|
|
24
|
+
out.push({
|
|
25
|
+
accountKey: `${agent}:name=${r.name}`,
|
|
26
|
+
email: null,
|
|
27
|
+
name: r.name,
|
|
28
|
+
provider: r.provider,
|
|
29
|
+
auth: r.auth,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remaining-capacity weighting for account selection — a pure, dependency-free
|
|
3
|
+
* primitive shared by the version-home router (`rotate.ts`) and the account pool
|
|
4
|
+
* (`account-pool.ts`). Kept in its own module so either can import it without
|
|
5
|
+
* dragging in the heavy usage/secrets graph.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* How far from its projected cap an account must be to keep its FULL headroom
|
|
9
|
+
* weight. Inside this horizon the weight is scaled down linearly toward the
|
|
10
|
+
* floor, so an account racing toward its 5h cap loses priority before it maxes.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PROJECTION_HORIZON_MIN = 30;
|
|
13
|
+
/**
|
|
14
|
+
* Weight one candidate by remaining routing capacity, deprioritized by how soon
|
|
15
|
+
* it is projected to cap. The base is weekly headroom (`max(1, 100 - used)`);
|
|
16
|
+
* an account with no live snapshot is treated as full-capacity (100) since there
|
|
17
|
+
* is no signal to deprioritize it. `minutesToLimit` (the daemon's burn-rate
|
|
18
|
+
* projection on the 5h session window) then scales that base: >= horizon (or
|
|
19
|
+
* unknown) keeps full weight, and closer-to-cap scales toward the floor of 1 —
|
|
20
|
+
* so a launch avoids an account projected to cap soon, not just a 100%-maxed
|
|
21
|
+
* one. Pure + exported so the deprioritization is unit-tested directly (a
|
|
22
|
+
* weighted-random draw is not).
|
|
23
|
+
*/
|
|
24
|
+
export declare function capacityWeight(usedPercent: number | null, minutesToLimit: number | null): number;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remaining-capacity weighting for account selection — a pure, dependency-free
|
|
3
|
+
* primitive shared by the version-home router (`rotate.ts`) and the account pool
|
|
4
|
+
* (`account-pool.ts`). Kept in its own module so either can import it without
|
|
5
|
+
* dragging in the heavy usage/secrets graph.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* How far from its projected cap an account must be to keep its FULL headroom
|
|
9
|
+
* weight. Inside this horizon the weight is scaled down linearly toward the
|
|
10
|
+
* floor, so an account racing toward its 5h cap loses priority before it maxes.
|
|
11
|
+
*/
|
|
12
|
+
export const PROJECTION_HORIZON_MIN = 30;
|
|
13
|
+
/**
|
|
14
|
+
* Weight one candidate by remaining routing capacity, deprioritized by how soon
|
|
15
|
+
* it is projected to cap. The base is weekly headroom (`max(1, 100 - used)`);
|
|
16
|
+
* an account with no live snapshot is treated as full-capacity (100) since there
|
|
17
|
+
* is no signal to deprioritize it. `minutesToLimit` (the daemon's burn-rate
|
|
18
|
+
* projection on the 5h session window) then scales that base: >= horizon (or
|
|
19
|
+
* unknown) keeps full weight, and closer-to-cap scales toward the floor of 1 —
|
|
20
|
+
* so a launch avoids an account projected to cap soon, not just a 100%-maxed
|
|
21
|
+
* one. Pure + exported so the deprioritization is unit-tested directly (a
|
|
22
|
+
* weighted-random draw is not).
|
|
23
|
+
*/
|
|
24
|
+
export function capacityWeight(usedPercent, minutesToLimit) {
|
|
25
|
+
const base = usedPercent === null ? 100 : Math.max(1, 100 - usedPercent);
|
|
26
|
+
if (minutesToLimit === null || !Number.isFinite(minutesToLimit))
|
|
27
|
+
return base;
|
|
28
|
+
const factor = Math.max(0, Math.min(1, minutesToLimit / PROJECTION_HORIZON_MIN));
|
|
29
|
+
return Math.max(1, base * factor);
|
|
30
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentId, RunStrategy } from '../types.js';
|
|
8
8
|
import type { FallbackEntry } from '../exec.js';
|
|
9
|
+
import { PROJECTION_HORIZON_MIN, capacityWeight } from './capacity.js';
|
|
9
10
|
import { type AccountInfo, type CredentialPresence } from '../agents.js';
|
|
10
11
|
import { type UsageSnapshot } from './usage.js';
|
|
11
12
|
import { type AuthVerdict } from '../auth-health.js';
|
|
@@ -49,6 +50,15 @@ export interface RotateCandidate {
|
|
|
49
50
|
*/
|
|
50
51
|
authVerdict: AuthVerdict | null;
|
|
51
52
|
lastActive: Date | null;
|
|
53
|
+
/**
|
|
54
|
+
* Set only for a candidate that comes from a provider account bundle rather
|
|
55
|
+
* than a native version-home login (RUSH-3182): the account name to inject via
|
|
56
|
+
* the `--account` spawn path (`resolveSpawnAccount` → `accountEnv`). Undefined
|
|
57
|
+
* for a native login, whose credential already lives in `version`'s home. The
|
|
58
|
+
* run path routes on this so a balanced pick of a setup-token / API-key account
|
|
59
|
+
* authenticates through the existing provider-account injection.
|
|
60
|
+
*/
|
|
61
|
+
providerAccount?: string;
|
|
52
62
|
}
|
|
53
63
|
export interface RotateResult {
|
|
54
64
|
/** The version picked for this run. */
|
|
@@ -212,24 +222,7 @@ export declare function checkRunAccountReadiness(agent: AgentId, version: string
|
|
|
212
222
|
* version so behavior stays predictable.
|
|
213
223
|
*/
|
|
214
224
|
export declare function pickBalancedCandidate(candidates: RotateCandidate[], nowMs?: number): RotateResult | null;
|
|
215
|
-
|
|
216
|
-
* How far from its projected cap an account must be to keep its FULL headroom
|
|
217
|
-
* weight. Inside this horizon the weight is scaled down linearly toward the
|
|
218
|
-
* floor, so an account racing toward its 5h cap loses priority before it maxes.
|
|
219
|
-
*/
|
|
220
|
-
export declare const PROJECTION_HORIZON_MIN = 30;
|
|
221
|
-
/**
|
|
222
|
-
* Weight one candidate by remaining routing capacity, deprioritized by how soon
|
|
223
|
-
* it is projected to cap. The base is weekly headroom (`max(1, 100 - used)`);
|
|
224
|
-
* an account with no live snapshot is treated as full-capacity (100) since there
|
|
225
|
-
* is no signal to deprioritize it. `minutesToLimit` (the daemon's burn-rate
|
|
226
|
-
* projection on the 5h session window) then scales that base: >= horizon (or
|
|
227
|
-
* unknown) keeps full weight, and closer-to-cap scales toward the floor of 1 —
|
|
228
|
-
* so a launch avoids an account projected to cap soon, not just a 100%-maxed
|
|
229
|
-
* one. Pure + exported so the deprioritization is unit-tested directly (a
|
|
230
|
-
* weighted-random draw is not).
|
|
231
|
-
*/
|
|
232
|
-
export declare function capacityWeight(usedPercent: number | null, minutesToLimit: number | null): number;
|
|
225
|
+
export { PROJECTION_HORIZON_MIN, capacityWeight };
|
|
233
226
|
/**
|
|
234
227
|
* Pick an available candidate. Prefers the configured pinned version when that
|
|
235
228
|
* version has usage available; otherwise routes to the candidate with the most
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
+
import { PROJECTION_HORIZON_MIN, capacityWeight } from './capacity.js';
|
|
9
10
|
import { accountDisplayLabel, getAccountInfo, credentialPresence, ALL_AGENT_IDS, } from '../agents.js';
|
|
10
11
|
import { readMeta, writeMeta, getHelpersDir } from '../state.js';
|
|
11
12
|
import { listInstalledVersions, getVersionHomePath, resolveVersion } from '../installations/versions.js';
|
|
@@ -365,30 +366,11 @@ function preferVerified(pool, nowMs, choose, narrowing = 'any-verified') {
|
|
|
365
366
|
usageUnverified: !isUsageVerified(picked, nowMs),
|
|
366
367
|
};
|
|
367
368
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
export const PROJECTION_HORIZON_MIN = 30;
|
|
374
|
-
/**
|
|
375
|
-
* Weight one candidate by remaining routing capacity, deprioritized by how soon
|
|
376
|
-
* it is projected to cap. The base is weekly headroom (`max(1, 100 - used)`);
|
|
377
|
-
* an account with no live snapshot is treated as full-capacity (100) since there
|
|
378
|
-
* is no signal to deprioritize it. `minutesToLimit` (the daemon's burn-rate
|
|
379
|
-
* projection on the 5h session window) then scales that base: >= horizon (or
|
|
380
|
-
* unknown) keeps full weight, and closer-to-cap scales toward the floor of 1 —
|
|
381
|
-
* so a launch avoids an account projected to cap soon, not just a 100%-maxed
|
|
382
|
-
* one. Pure + exported so the deprioritization is unit-tested directly (a
|
|
383
|
-
* weighted-random draw is not).
|
|
384
|
-
*/
|
|
385
|
-
export function capacityWeight(usedPercent, minutesToLimit) {
|
|
386
|
-
const base = usedPercent === null ? 100 : Math.max(1, 100 - usedPercent);
|
|
387
|
-
if (minutesToLimit === null || !Number.isFinite(minutesToLimit))
|
|
388
|
-
return base;
|
|
389
|
-
const factor = Math.max(0, Math.min(1, minutesToLimit / PROJECTION_HORIZON_MIN));
|
|
390
|
-
return Math.max(1, base * factor);
|
|
391
|
-
}
|
|
369
|
+
// capacityWeight + PROJECTION_HORIZON_MIN moved to ./capacity.js (a pure,
|
|
370
|
+
// dependency-free module the account pool can import without this file's heavy
|
|
371
|
+
// graph). Imported at the top for internal use; re-exported here so existing
|
|
372
|
+
// importers keep resolving them from rotate.
|
|
373
|
+
export { PROJECTION_HORIZON_MIN, capacityWeight };
|
|
392
374
|
/**
|
|
393
375
|
* Pick one candidate from `sorted` using weights proportional to remaining
|
|
394
376
|
* routing capacity (see {@link capacityWeight}). Floor each weight at 1 so a
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export declare const CLAUDE_STATUSLINE_COMMAND = "agents __claude-statusline";
|
|
2
|
+
/**
|
|
3
|
+
* True when `command` re-invokes THIS status-line producer (our private
|
|
4
|
+
* `__claude-statusline` subcommand) under any binary name or path. Delegating to
|
|
5
|
+
* such a command is the fork bomb, so both the read side (renderDelegate) and the
|
|
6
|
+
* write side (installClaudeStatusLine) treat it as "not a real external
|
|
7
|
+
* producer" and never chain to it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isStatusLineSelfReference(command: string): boolean;
|
|
2
10
|
interface ClaudeStatusLinePayload {
|
|
3
11
|
cwd?: string;
|
|
4
12
|
workspace?: {
|
|
@@ -20,6 +28,7 @@ interface ClaudeStatusLinePayload {
|
|
|
20
28
|
};
|
|
21
29
|
}
|
|
22
30
|
export declare function ingestClaudeStatusLineUsage(payload: ClaudeStatusLinePayload, versionHome: string): boolean;
|
|
31
|
+
export declare function renderDelegate(payload: string, versionHome: string): string;
|
|
23
32
|
export declare function renderClaudeStatusLine(payload: ClaudeStatusLinePayload, host?: string, delegated?: string): string;
|
|
24
33
|
export declare function runClaudeStatusLine(): Promise<number>;
|
|
25
34
|
export declare function installClaudeStatusLine(versionHome: string): {
|
|
@@ -7,6 +7,39 @@ import { atomicWriteFileSync } from './fs-atomic.js';
|
|
|
7
7
|
import { mergeClaudeUsageCacheWindows } from './accounting/usage.js';
|
|
8
8
|
export const CLAUDE_STATUSLINE_COMMAND = 'agents __claude-statusline';
|
|
9
9
|
const DELEGATE_FILE = path.join('.agents', 'claude-statusline-delegate');
|
|
10
|
+
// The private subcommand this feature runs. It is only ever invoked internally,
|
|
11
|
+
// so ANY command that contains it — under any binary name or path (`agents`,
|
|
12
|
+
// `agents-dev`, `ag`, an absolute path, a wrapper) — IS this status-line
|
|
13
|
+
// producer, and delegating to it recurses without bound. Match the subcommand,
|
|
14
|
+
// not the exact `agents __claude-statusline` string, or a delegate seeded with a
|
|
15
|
+
// differently-named binary (e.g. `agents-dev __claude-statusline`) fork-bombs the
|
|
16
|
+
// machine: each render spawns a copy that reads the same delegate and spawns
|
|
17
|
+
// another, forever.
|
|
18
|
+
const STATUSLINE_SUBCOMMAND = '__claude-statusline';
|
|
19
|
+
// Set on the child env before spawning a delegate. If it is already present we
|
|
20
|
+
// are ourselves running as someone's delegate, so we refuse to delegate again —
|
|
21
|
+
// a hard depth-1 backstop that bounds the blast radius even if a self-reference
|
|
22
|
+
// somehow slips past isStatusLineSelfReference(). One hop is the contract:
|
|
23
|
+
// installClaudeStatusLine only ever preserves a single prior command.
|
|
24
|
+
const DELEGATE_GUARD_ENV = 'AGENTS_CLAUDE_STATUSLINE_DELEGATED';
|
|
25
|
+
// A hung or slow delegate must never pin a status-line render open — the render
|
|
26
|
+
// is re-invoked on every refresh, so an unbounded delegate accumulates processes.
|
|
27
|
+
const DELEGATE_TIMEOUT_MS = 5_000;
|
|
28
|
+
/**
|
|
29
|
+
* True when `command` re-invokes THIS status-line producer (our private
|
|
30
|
+
* `__claude-statusline` subcommand) under any binary name or path. Delegating to
|
|
31
|
+
* such a command is the fork bomb, so both the read side (renderDelegate) and the
|
|
32
|
+
* write side (installClaudeStatusLine) treat it as "not a real external
|
|
33
|
+
* producer" and never chain to it.
|
|
34
|
+
*/
|
|
35
|
+
export function isStatusLineSelfReference(command) {
|
|
36
|
+
const trimmed = command.trim();
|
|
37
|
+
if (!trimmed)
|
|
38
|
+
return false;
|
|
39
|
+
if (trimmed === CLAUDE_STATUSLINE_COMMAND)
|
|
40
|
+
return true;
|
|
41
|
+
return new RegExp(`(^|\\s)${STATUSLINE_SUBCOMMAND}(\\s|$)`).test(trimmed);
|
|
42
|
+
}
|
|
10
43
|
function isRecord(value) {
|
|
11
44
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
45
|
}
|
|
@@ -48,7 +81,10 @@ export function ingestClaudeStatusLineUsage(payload, versionHome) {
|
|
|
48
81
|
function delegatePath(versionHome) {
|
|
49
82
|
return path.join(versionHome, DELEGATE_FILE);
|
|
50
83
|
}
|
|
51
|
-
function renderDelegate(payload, versionHome) {
|
|
84
|
+
export function renderDelegate(payload, versionHome) {
|
|
85
|
+
// Already running as a delegate hop — never spawn another. Hard recursion stop.
|
|
86
|
+
if (process.env[DELEGATE_GUARD_ENV])
|
|
87
|
+
return '';
|
|
52
88
|
let command = '';
|
|
53
89
|
try {
|
|
54
90
|
command = fs.readFileSync(delegatePath(versionHome), 'utf8').trim();
|
|
@@ -56,13 +92,14 @@ function renderDelegate(payload, versionHome) {
|
|
|
56
92
|
catch {
|
|
57
93
|
return '';
|
|
58
94
|
}
|
|
59
|
-
if (!command || command
|
|
95
|
+
if (!command || isStatusLineSelfReference(command))
|
|
60
96
|
return '';
|
|
61
97
|
const result = spawnSync(command, {
|
|
62
98
|
shell: true,
|
|
63
99
|
input: payload,
|
|
64
100
|
encoding: 'utf8',
|
|
65
|
-
env: process.env,
|
|
101
|
+
env: { ...process.env, [DELEGATE_GUARD_ENV]: '1' },
|
|
102
|
+
timeout: DELEGATE_TIMEOUT_MS,
|
|
66
103
|
});
|
|
67
104
|
return result.status === 0 ? result.stdout.trim() : '';
|
|
68
105
|
}
|
|
@@ -117,11 +154,15 @@ export function installClaudeStatusLine(versionHome) {
|
|
|
117
154
|
: '';
|
|
118
155
|
if (existing === CLAUDE_STATUSLINE_COMMAND)
|
|
119
156
|
return { changed: false };
|
|
120
|
-
if (existing) {
|
|
157
|
+
if (existing && !isStatusLineSelfReference(existing)) {
|
|
158
|
+
// A genuine third-party status-line command → preserve it as a delegate.
|
|
121
159
|
fs.mkdirSync(path.dirname(delegatePath(versionHome)), { recursive: true });
|
|
122
160
|
atomicWriteFileSync(delegatePath(versionHome), `${existing}\n`);
|
|
123
161
|
}
|
|
124
162
|
else {
|
|
163
|
+
// Empty, or our own subcommand under a different binary name
|
|
164
|
+
// (`agents-dev __claude-statusline`, an absolute path, …). Saving that as a
|
|
165
|
+
// delegate is the fork bomb — never persist it. Drop any prior delegate.
|
|
125
166
|
fs.rmSync(delegatePath(versionHome), { force: true });
|
|
126
167
|
}
|
|
127
168
|
settings.statusLine = {
|
|
@@ -59,11 +59,11 @@ export declare const WIN_HELPER_TOKEN_FILE = "helper-token";
|
|
|
59
59
|
* the release-asset download (`ensureWinHelperExe`).
|
|
60
60
|
*/
|
|
61
61
|
export declare function resolveWinHelperExe(): string | null;
|
|
62
|
-
/** GitHub repo whose `v<version>` releases carry the exe as an asset. */
|
|
63
|
-
export declare const WIN_HELPER_RELEASE_REPO = "phnx-labs/
|
|
62
|
+
/** GitHub repo whose `computer-win/v<version>` releases carry the exe as an asset. */
|
|
63
|
+
export declare const WIN_HELPER_RELEASE_REPO = "phnx-labs/agi-cli";
|
|
64
64
|
/** Cache dir for downloaded helper exes, one subdir per release tag. */
|
|
65
65
|
export declare function winHelperCacheDir(version: string): string;
|
|
66
|
-
/** Release-asset URLs for the exe + its checksum at one exact `v<version>` tag. */
|
|
66
|
+
/** Release-asset URLs for the exe + its checksum at one exact `computer-win/v<version>` tag. */
|
|
67
67
|
export declare function winHelperAssetUrls(version: string): {
|
|
68
68
|
exe: string;
|
|
69
69
|
sha256: string;
|
|
@@ -73,10 +73,11 @@ export declare function winHelperAssetUrls(version: string): {
|
|
|
73
73
|
* a bare hex digest. Throws on anything that does not lead with 64 hex chars.
|
|
74
74
|
*/
|
|
75
75
|
/**
|
|
76
|
-
* Download the exe release asset for this
|
|
76
|
+
* Download the exe release asset for this HELPER version, verify its sha256
|
|
77
77
|
* against the published `.sha256` asset, and cache it under the agents cache
|
|
78
|
-
* dir. Only the exact `v<version>` tag is consulted — a missing
|
|
79
|
-
* hard error naming that tag, never a silent fallback to another
|
|
78
|
+
* dir. Only the exact `computer-win/v<version>` tag is consulted — a missing
|
|
79
|
+
* asset is a hard error naming that tag, never a silent fallback to another
|
|
80
|
+
* release. The version is the HELPER's own, not the CLI's.
|
|
80
81
|
*/
|
|
81
82
|
export declare function downloadWinHelperExe(version: string): Promise<string>;
|
|
82
83
|
/**
|
|
@@ -127,13 +127,13 @@ export function resolveWinHelperExe() {
|
|
|
127
127
|
}
|
|
128
128
|
return null;
|
|
129
129
|
}
|
|
130
|
-
/** GitHub repo whose `v<version>` releases carry the exe as an asset. */
|
|
131
|
-
export const WIN_HELPER_RELEASE_REPO = 'phnx-labs/
|
|
130
|
+
/** GitHub repo whose `computer-win/v<version>` releases carry the exe as an asset. */
|
|
131
|
+
export const WIN_HELPER_RELEASE_REPO = 'phnx-labs/agi-cli';
|
|
132
132
|
/** Cache dir for downloaded helper exes, one subdir per release tag. */
|
|
133
133
|
export function winHelperCacheDir(version) {
|
|
134
134
|
return path.join(getCacheDir(), 'computer', 'win-helper', `v${version}`);
|
|
135
135
|
}
|
|
136
|
-
/** Release-asset URLs for the exe + its checksum at one exact `v<version>` tag. */
|
|
136
|
+
/** Release-asset URLs for the exe + its checksum at one exact `computer-win/v<version>` tag. */
|
|
137
137
|
export function winHelperAssetUrls(version) {
|
|
138
138
|
// The helper's OWN tag, not the CLI's. This exe is not an .app bundle so it
|
|
139
139
|
// cannot share helper-download.ts's zip/codesign machinery, but it had the
|
|
@@ -148,19 +148,24 @@ export function winHelperAssetUrls(version) {
|
|
|
148
148
|
* a bare hex digest. Throws on anything that does not lead with 64 hex chars.
|
|
149
149
|
*/
|
|
150
150
|
/**
|
|
151
|
-
* Download the exe release asset for this
|
|
151
|
+
* Download the exe release asset for this HELPER version, verify its sha256
|
|
152
152
|
* against the published `.sha256` asset, and cache it under the agents cache
|
|
153
|
-
* dir. Only the exact `v<version>` tag is consulted — a missing
|
|
154
|
-
* hard error naming that tag, never a silent fallback to another
|
|
153
|
+
* dir. Only the exact `computer-win/v<version>` tag is consulted — a missing
|
|
154
|
+
* asset is a hard error naming that tag, never a silent fallback to another
|
|
155
|
+
* release. The version is the HELPER's own, not the CLI's.
|
|
155
156
|
*/
|
|
156
157
|
export async function downloadWinHelperExe(version) {
|
|
157
158
|
const cached = path.join(winHelperCacheDir(version), WIN_HELPER_EXE);
|
|
158
159
|
if (fs.existsSync(cached))
|
|
159
160
|
return cached;
|
|
160
|
-
|
|
161
|
+
// The SAME tag the URL is built from. This said `v${version}` — the CLI's tag
|
|
162
|
+
// shape — while winHelperAssetUrls uses helperTag('computer-win', ...), so a
|
|
163
|
+
// missing asset sent you looking for a tag that does not exist. The mac path
|
|
164
|
+
// had this fixed already; the Windows path was left behind by the tag split.
|
|
165
|
+
const tag = helperTag('computer-win', version);
|
|
161
166
|
const { exe: exeUrl, sha256: shaUrl } = winHelperAssetUrls(version);
|
|
162
167
|
const missing = (status, url) => new Error(`no ${WIN_HELPER_EXE} release asset for tag ${tag} (HTTP ${status} on ${url}). ` +
|
|
163
|
-
`The Windows helper ships as a GitHub release asset
|
|
168
|
+
`The Windows helper ships as a GitHub release asset on its own helper tag; ` +
|
|
164
169
|
`from a repo checkout you can build it locally instead: bash scripts/build-win.sh`);
|
|
165
170
|
// Checksum first: it is tiny and 404s fast when the tag has no assets.
|
|
166
171
|
const shaRes = await fetch(shaUrl, { signal: AbortSignal.timeout(30_000) });
|
|
@@ -28,11 +28,12 @@ export interface ParsedAutoConfigKey {
|
|
|
28
28
|
scope: 'auto';
|
|
29
29
|
property: 'pool';
|
|
30
30
|
}
|
|
31
|
-
/** A
|
|
32
|
-
*
|
|
31
|
+
/** A browser key: the profile agents drive or the browser that shows the user a
|
|
32
|
+
* page (both device-scope, self or peer), or `device` — the user-scope fleet
|
|
33
|
+
* hub every box drives by default, which is central and never peer-targeted. */
|
|
33
34
|
export interface ParsedBrowserConfigKey {
|
|
34
35
|
scope: 'browser';
|
|
35
|
-
property: 'profile' | 'viewer';
|
|
36
|
+
property: 'profile' | 'viewer' | 'device';
|
|
36
37
|
device?: string;
|
|
37
38
|
}
|
|
38
39
|
export interface ParsedProjectConfigKey {
|
package/dist/lib/config-keys.js
CHANGED
|
@@ -88,6 +88,9 @@ export function parseConfigKey(key) {
|
|
|
88
88
|
if (raw === 'browser.viewer') {
|
|
89
89
|
return { scope: 'browser', property: 'viewer' };
|
|
90
90
|
}
|
|
91
|
+
if (raw === 'browser.device') {
|
|
92
|
+
return { scope: 'browser', property: 'device' };
|
|
93
|
+
}
|
|
91
94
|
if (raw === 'project.root') {
|
|
92
95
|
return { scope: 'project', property: 'root' };
|
|
93
96
|
}
|
|
@@ -110,7 +113,7 @@ export function parseConfigKey(key) {
|
|
|
110
113
|
throw new Error(`Invalid auto config key '${key}'. Use auto.pool.`);
|
|
111
114
|
}
|
|
112
115
|
if (raw.startsWith('browser.')) {
|
|
113
|
-
throw new Error(`Invalid browser config key '${key}'. Use browser.profile or browser.
|
|
116
|
+
throw new Error(`Invalid browser config key '${key}'. Use browser.profile, browser.viewer, or browser.device.`);
|
|
114
117
|
}
|
|
115
118
|
if (raw.startsWith('project.')) {
|
|
116
119
|
throw new Error(`Invalid project config key '${key}'. Use project.root.`);
|
|
@@ -149,7 +152,7 @@ export function listKnownConfigKeys() {
|
|
|
149
152
|
for (const tier of MODEL_TIERS) {
|
|
150
153
|
keys.push(`run.<agent@version>.tier.${tier}`);
|
|
151
154
|
}
|
|
152
|
-
keys.push('interactive.host', 'auto.pool', 'browser.profile', 'browser.viewer', 'project.root');
|
|
155
|
+
keys.push('interactive.host', 'auto.pool', 'browser.profile', 'browser.viewer', 'browser.device', 'project.root');
|
|
153
156
|
for (const prop of DEVICE_CONFIG_PROPERTIES) {
|
|
154
157
|
keys.push(`devices.<name>.${prop}`);
|
|
155
158
|
}
|
|
@@ -202,6 +205,10 @@ export function configKeyStorageHint(parsed) {
|
|
|
202
205
|
case 'auto':
|
|
203
206
|
return 'config.autoPool';
|
|
204
207
|
case 'browser': {
|
|
208
|
+
if (parsed.property === 'device') {
|
|
209
|
+
// User scope: one value in the central agents.yaml that syncs fleet-wide.
|
|
210
|
+
return 'config.defaultBrowserDevice (central agents.yaml; syncs fleet-wide)';
|
|
211
|
+
}
|
|
205
212
|
const yamlKey = parsed.property === 'viewer' ? 'browserViewer' : 'defaultBrowserProfile';
|
|
206
213
|
return parsed.device
|
|
207
214
|
? `devices/${parsed.device}/agents.yaml config.${yamlKey}`
|
|
@@ -94,6 +94,29 @@ export const CONFIG_KEYS = [
|
|
|
94
94
|
type: 'string',
|
|
95
95
|
description: 'Browser profile `agents browser start` resolves to without --profile (set via `agents browser use`).',
|
|
96
96
|
},
|
|
97
|
+
{
|
|
98
|
+
name: 'browser.device',
|
|
99
|
+
yamlKey: 'defaultBrowserDevice',
|
|
100
|
+
// user scope, so a SINGLE value in the central agents.yaml syncs to every box:
|
|
101
|
+
// the fleet's browser hub. A worker with this set forwards its browser drives
|
|
102
|
+
// to the hub (as if `--device <hub>` was passed) with no per-command flag, so
|
|
103
|
+
// every agent shares the hub's one logged-in browser. The hub names itself, so
|
|
104
|
+
// there it resolves to a self-host and runs locally — which is why one synced
|
|
105
|
+
// value is safe. Unset = drive this box's own browser (today's behavior).
|
|
106
|
+
scope: 'user',
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: 'Fleet browser hub: the device whose browser `agents browser` drive verbs target by default, with no --device. ' +
|
|
109
|
+
'The hub itself runs locally; every other box forwards to it. Unset = each box drives its own browser.',
|
|
110
|
+
validate: (v) => {
|
|
111
|
+
try {
|
|
112
|
+
assertValidDeviceName(v);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
return err?.message ?? String(err);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
97
120
|
{
|
|
98
121
|
name: 'agents.max-concurrent',
|
|
99
122
|
yamlKey: 'maxAgents',
|
|
@@ -13,7 +13,7 @@ export declare const FACTORY_PROJECTS: readonly [{
|
|
|
13
13
|
readonly repo: "phnx-labs/rush-cli";
|
|
14
14
|
}, {
|
|
15
15
|
readonly name: "Agents CLI";
|
|
16
|
-
readonly repo: "phnx-labs/
|
|
16
|
+
readonly repo: "phnx-labs/agi-cli";
|
|
17
17
|
}, {
|
|
18
18
|
readonly name: "Linear CLI";
|
|
19
19
|
readonly repo: "phnx-labs/linear-cli";
|
|
@@ -25,7 +25,7 @@ export const FACTORY_PROJECTS = [
|
|
|
25
25
|
{ name: 'Prix', repo: 'phnx-labs/prix' },
|
|
26
26
|
{ name: 'Rush App', repo: 'phnx-labs/rush' },
|
|
27
27
|
{ name: 'Rush CLI', repo: 'phnx-labs/rush-cli' },
|
|
28
|
-
{ name: 'Agents CLI', repo: 'phnx-labs/
|
|
28
|
+
{ name: 'Agents CLI', repo: 'phnx-labs/agi-cli' },
|
|
29
29
|
{ name: 'Linear CLI', repo: 'phnx-labs/linear-cli' },
|
|
30
30
|
];
|
|
31
31
|
const defaults = () => ({
|
|
@@ -16,8 +16,18 @@
|
|
|
16
16
|
* lands for both helpers at once.
|
|
17
17
|
*/
|
|
18
18
|
import { type HelperName } from './helper-versions.js';
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* GitHub repo whose `<helper>/v<version>` releases carry the helper assets.
|
|
21
|
+
*
|
|
22
|
+
* This is the SLUG, which is not the npm package name: the package is still
|
|
23
|
+
* `@phnx-labs/agents-cli`, but the repository was renamed to `agi-cli`. The old
|
|
24
|
+
* slug kept working only because GitHub redirects a renamed repo, which is a
|
|
25
|
+
* poor thing to hang signed-binary delivery on — a redirect is one re-created
|
|
26
|
+
* repo away from resolving somewhere else. What actually protects the download
|
|
27
|
+
* is the sha256 + codesign + designated-requirement + Team ID verification
|
|
28
|
+
* below; this just stops relying on the redirect.
|
|
29
|
+
*/
|
|
30
|
+
export declare const HELPER_RELEASE_REPO = "phnx-labs/agi-cli";
|
|
21
31
|
/** Apple Developer ID Team every helper must be signed by ("Developer ID
|
|
22
32
|
* Application: Muqit Nawaz"). Defense in depth on top of `spctl` notarization. */
|
|
23
33
|
export declare const EXPECTED_TEAM_ID = "2HTP252L87";
|
|
@@ -23,8 +23,18 @@ import { pipeline } from 'node:stream/promises';
|
|
|
23
23
|
import { getCacheDir } from './state.js';
|
|
24
24
|
import { parseSha256Asset, sha256File } from './sha256-asset.js';
|
|
25
25
|
import { helperTag } from './helper-versions.js';
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* GitHub repo whose `<helper>/v<version>` releases carry the helper assets.
|
|
28
|
+
*
|
|
29
|
+
* This is the SLUG, which is not the npm package name: the package is still
|
|
30
|
+
* `@phnx-labs/agents-cli`, but the repository was renamed to `agi-cli`. The old
|
|
31
|
+
* slug kept working only because GitHub redirects a renamed repo, which is a
|
|
32
|
+
* poor thing to hang signed-binary delivery on — a redirect is one re-created
|
|
33
|
+
* repo away from resolving somewhere else. What actually protects the download
|
|
34
|
+
* is the sha256 + codesign + designated-requirement + Team ID verification
|
|
35
|
+
* below; this just stops relying on the redirect.
|
|
36
|
+
*/
|
|
37
|
+
export const HELPER_RELEASE_REPO = 'phnx-labs/agi-cli';
|
|
28
38
|
/** Apple Developer ID Team every helper must be signed by ("Developer ID
|
|
29
39
|
* Application: Muqit Nawaz"). Defense in depth on top of `spctl` notarization. */
|
|
30
40
|
export const EXPECTED_TEAM_ID = '2HTP252L87';
|
|
@@ -298,7 +298,7 @@ function foldUserHooksYamlIntoAgentsYaml() {
|
|
|
298
298
|
meta.hooks = merged;
|
|
299
299
|
const header = `# agents-cli metadata
|
|
300
300
|
# Auto-generated - do not edit manually
|
|
301
|
-
# https://github.com/phnx-labs/
|
|
301
|
+
# https://github.com/phnx-labs/agi-cli
|
|
302
302
|
|
|
303
303
|
`;
|
|
304
304
|
try {
|
|
@@ -414,7 +414,7 @@ function foldBrowserProfilesIntoAgentsYaml() {
|
|
|
414
414
|
meta.browser = merged;
|
|
415
415
|
const header = `# agents-cli metadata
|
|
416
416
|
# Auto-generated - do not edit manually
|
|
417
|
-
# https://github.com/phnx-labs/
|
|
417
|
+
# https://github.com/phnx-labs/agi-cli
|
|
418
418
|
|
|
419
419
|
`;
|
|
420
420
|
try {
|
|
@@ -1641,7 +1641,7 @@ function migrateVersionResourcesToPatterns() {
|
|
|
1641
1641
|
}
|
|
1642
1642
|
}
|
|
1643
1643
|
if (changed) {
|
|
1644
|
-
const META_HEADER = '# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/
|
|
1644
|
+
const META_HEADER = '# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agi-cli\n# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/agi-cli/main/cli/schema/agents-yaml.schema.json\n\n';
|
|
1645
1645
|
fs.writeFileSync(metaFile, META_HEADER + yaml.stringify(meta), 'utf-8');
|
|
1646
1646
|
console.error('Migrated agents.yaml versions: entries to pattern format');
|
|
1647
1647
|
}
|
|
@@ -1668,7 +1668,7 @@ function migrateSplitDeviceLocalMeta() {
|
|
|
1668
1668
|
const agents = meta.agents;
|
|
1669
1669
|
const versions = meta.versions;
|
|
1670
1670
|
const hasLocal = (!!agents && Object.keys(agents).length > 0) || (!!versions && Object.keys(versions).length > 0);
|
|
1671
|
-
const HEADER = '# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/
|
|
1671
|
+
const HEADER = '# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agi-cli\n\n';
|
|
1672
1672
|
// Only rewrite central when it actually carries machine-local fields — a
|
|
1673
1673
|
// machine whose agents.yaml is already portable-only is left untouched.
|
|
1674
1674
|
if (hasLocal) {
|
|
@@ -269,10 +269,13 @@ export function getBinaryPath(agent, version) {
|
|
|
269
269
|
const agentConfig = AGENTS[agent];
|
|
270
270
|
if (agent === 'grok') {
|
|
271
271
|
const grokDownloads = path.join(getVersionHomePath(agent, version), '.grok', 'downloads');
|
|
272
|
-
//
|
|
272
|
+
// The directory token is the stable installation/account label. A
|
|
273
|
+
// self-updating slot may carry a newer vendor release, whose binary keeps
|
|
274
|
+
// the release in its filename; resolve through the frozen install record.
|
|
275
|
+
const releaseVersion = readInstallation(agent, version)?.releaseVersion ?? version;
|
|
273
276
|
try {
|
|
274
277
|
const entries = fs.readdirSync(grokDownloads);
|
|
275
|
-
const match = entries.find((e) => e.includes(
|
|
278
|
+
const match = entries.find((e) => e.includes(releaseVersion) && e.startsWith('grok-'));
|
|
276
279
|
if (match)
|
|
277
280
|
return path.join(grokDownloads, match);
|
|
278
281
|
}
|
|
@@ -280,7 +283,7 @@ export function getBinaryPath(agent, version) {
|
|
|
280
283
|
const fallback = resolveGrokFallbackBinary(grokDownloads);
|
|
281
284
|
if (fallback)
|
|
282
285
|
return fallback;
|
|
283
|
-
return path.join(grokDownloads, `grok-${
|
|
286
|
+
return path.join(grokDownloads, `grok-${releaseVersion}`);
|
|
284
287
|
}
|
|
285
288
|
if (agent === 'droid') {
|
|
286
289
|
// Factory.ai's installer drops a standalone native binary (no npm package,
|