@phnx-labs/agents-cli 1.22.50 → 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 +127 -0
- package/dist/bootstrap.js +1 -1
- 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/feedback.js +1 -1
- package/dist/commands/menubar.js +6 -4
- package/dist/commands/traces.js +32 -4
- 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/menubar/install-menubar.d.ts +71 -8
- package/dist/lib/menubar/install-menubar.js +183 -24
- 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/traces/sync.d.ts +30 -0
- package/dist/lib/traces/sync.js +91 -10
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,132 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.51
|
|
4
|
+
|
|
5
|
+
- **`publish-computer-helper-mac.sh` cuts the helper's own tag (PHNX-3228).**
|
|
6
|
+
It published `ComputerHelper.app.zip` to the CLI's `v<version>` release, but since the
|
|
7
|
+
client was repointed at per-helper tags the resolver asks for `computer-mac/v<x.y.z>`
|
|
8
|
+
(`helperTag` in `cli/src/lib/helper-versions.ts`). The asset therefore landed at an
|
|
9
|
+
address nothing requests, and there was **no way to cut a new computer-mac release at
|
|
10
|
+
all** — the last publisher still coupled to the CLI's version line. It now cuts
|
|
11
|
+
`computer-mac/v<x.y.z>`, requires the helper's version explicitly instead of defaulting
|
|
12
|
+
to `cli/package.json` (that default was the coupling), and refuses an existing tag, since
|
|
13
|
+
the upload uses `--clobber` and would otherwise replace a binary an installed CLI already
|
|
14
|
+
pins. Symmetric with `publish-computer-win.sh`. Source:
|
|
15
|
+
`cli/scripts/publish-computer-helper-mac.sh`.
|
|
16
|
+
It also cuts the tag itself now. `gh release create --verify-tag` refuses to invent a
|
|
17
|
+
tag absent from the remote, and nothing else pushed `computer-mac/v<x.y.z>` — `release.sh`
|
|
18
|
+
delegates helper tagging to "where the helper is released", which is this script. So it
|
|
19
|
+
ran green through build + notarize and then failed at the release step, meaning no new
|
|
20
|
+
helper version could be cut. The tag is pushed AFTER a successful build, deliberately:
|
|
21
|
+
pushing first would leave a published, immutable address with nothing behind it whenever
|
|
22
|
+
notarization failed.
|
|
23
|
+
Origin, not the local ref, is what makes a helper release immutable. Adding tag creation
|
|
24
|
+
introduced a wedge: a run that tagged and then failed to push left a local tag, and the
|
|
25
|
+
guard read that as "published" and told the operator to cut the next patch — burning a
|
|
26
|
+
version over a transient network error, and (since the guard precedes the build) making
|
|
27
|
+
that version uncuttable from the checkout without a manual `git tag -d`. A local-only tag
|
|
28
|
+
now resumes the interrupted publish instead.
|
|
29
|
+
Immutability keys on the published RELEASE, not the tag. A tag with no release is an
|
|
30
|
+
interrupted run — whether the push failed or the release creation did — and both now
|
|
31
|
+
resume rather than burning the version. The asset is what an installed CLI downloads, so
|
|
32
|
+
the release is the thing that must never be replaced.
|
|
33
|
+
The published-release check fails CLOSED. A non-zero `gh` is not evidence of absence —
|
|
34
|
+
unauthenticated, offline, and rate-limited all exit non-zero — so reading that as "not
|
|
35
|
+
published" would have carried on into `gh release upload --clobber` over a live binary,
|
|
36
|
+
the exact outcome the guard exists to prevent. Only an explicit not-found means not-found;
|
|
37
|
+
anything else stops the release and prints what `gh` actually said.
|
|
38
|
+
|
|
39
|
+
- **`agents traces sync` retains and reports failed sessions instead of stranding them (PHNX-3267).**
|
|
40
|
+
The sync watermark advanced to the max mtime of *successful* uploads, so a later
|
|
41
|
+
success moved it past an earlier failed session and the next run's
|
|
42
|
+
`file_mtime_ms > watermark` filter skipped that session forever — the "failures are
|
|
43
|
+
retried" comment was false. A live 1.22.49 sync on a real device reported 11,073
|
|
44
|
+
uploaded and 6,050 errors with no way to tell a transcript cleaned off disk from a
|
|
45
|
+
genuine upload failure. Sync now records each failed session's identity and typed,
|
|
46
|
+
redacted evidence in the ledger and unions those retry-worthy ids back into the row
|
|
47
|
+
query regardless of the watermark, so a stranded session is re-attempted until it
|
|
48
|
+
succeeds. Failures are classified `transcript-unavailable` (the file is gone —
|
|
49
|
+
expected history, not re-read, aged out after 14 days), `parse-failed`, or
|
|
50
|
+
`upload-failed` (both retried); `agents traces sync` prints the breakdown
|
|
51
|
+
(`… errors (N transcripts no longer on disk · M parse/upload failures — will retry)`)
|
|
52
|
+
and `agents traces status` lists the outstanding retry set with example detail.
|
|
53
|
+
Source: `cli/src/lib/traces/sync.ts`, `cli/src/commands/traces.ts`.
|
|
54
|
+
|
|
55
|
+
- **Helper binaries no longer download through a repository-rename redirect.**
|
|
56
|
+
`HELPER_RELEASE_REPO` still named `phnx-labs/agents-cli` after the repository was renamed
|
|
57
|
+
to `phnx-labs/agi-cli`. Nothing was broken — GitHub redirects a renamed repo, and both
|
|
58
|
+
slugs returned HTTP 200 — but every signed helper asset (`MenubarHelper.app.zip`,
|
|
59
|
+
`Agents_CLI.app.zip`, `ComputerHelper.app.zip`, `computer-helper-win.exe`) was resolving
|
|
60
|
+
through that redirect, which is one re-created repository away from pointing elsewhere.
|
|
61
|
+
What actually protects the download is the sha256 + `codesign` + designated-requirement +
|
|
62
|
+
Team ID verification in `helper-download.ts`; this removes the reliance on the redirect.
|
|
63
|
+
The `agents.yaml` `$schema` URL, the CHANGELOG link, the star nudge, and the
|
|
64
|
+
`package.json` repository/issues metadata moved with it.
|
|
65
|
+
**The npm package name is deliberately unchanged** — it is still
|
|
66
|
+
`@phnx-labs/agents-cli`, and renaming it would orphan every installed CLI. That
|
|
67
|
+
distinction is now pinned by a test. Source: `cli/src/lib/helper-download.ts`.
|
|
68
|
+
`publish-computer-helper-mac.sh` carried the old slug too, and that one is a **write**
|
|
69
|
+
path: `REPO_SLUG` reaches `gh release view` (the immutability guard), `gh release create`,
|
|
70
|
+
and `gh release upload --clobber`. Publishing signed helper binaries was going through the
|
|
71
|
+
rename redirect. Found by sweeping `scripts/` and `.github/` after the source tree was
|
|
72
|
+
already clean — the source sweep alone would have missed it.
|
|
73
|
+
Three further live paths carried the old slug and were missed by the first sweep:
|
|
74
|
+
`ssh-tunnel.ts:170` `WIN_HELPER_RELEASE_REPO` (a **separately hardcoded** constant — the
|
|
75
|
+
download URL for `computer-helper-win.exe`, the fourth signed asset, which the first
|
|
76
|
+
version of this change claimed to cover and did not); `commands/feedback.ts:14`, which
|
|
77
|
+
opens real GitHub issues; and `factory/snapshot.ts:30`, which polls this repo's PRs.
|
|
78
|
+
`installations/migrate.ts` also wrote the old URL into the `agents.yaml` header it
|
|
79
|
+
generates. Separately, `.github/workflows/tests-windows-host-e2e.yml:54` gated on
|
|
80
|
+
`github.repository == 'phnx-labs/agents-cli'`, which is permanently false after a rename
|
|
81
|
+
— that job had silently stopped running on every push to main and on its daily cron.
|
|
82
|
+
The JSON Schema's own `$id` moved with it — `state.ts` and `migrate.ts` write that URL as
|
|
83
|
+
the `$schema` hint into every user's `agents.yaml`, so a mismatched `$id` would make
|
|
84
|
+
editors reject their own config.
|
|
85
|
+
|
|
86
|
+
- **The menu-bar helper's staleness check compares the HELPER's version, not the CLI's
|
|
87
|
+
(RUSH-3230).** `installAndStartService` stamped `getCliVersion()` as the installed
|
|
88
|
+
helper's version, and `menubarSetupStale()` compared against `getCliVersion()` too. Once
|
|
89
|
+
helpers gained their own version line that was wrong in both directions: every CLI
|
|
90
|
+
release made an unchanged helper look stale and reinstalled it — recopying the bundle
|
|
91
|
+
under the running helper, which `KeepAlive` then restarts (the #2109 storm: a new pid
|
|
92
|
+
every 5-15s, 578 launches in one log) — while a genuinely newer helper at the same CLI
|
|
93
|
+
version never looked stale at all, so it could never install.
|
|
94
|
+
The stamp is now JSON recording what the helper actually IS: `release` with its helper
|
|
95
|
+
version, or `local` with the source path + mtime for a dev build (menubar's build.sh
|
|
96
|
+
hardcodes `CFBundleShortVersionString`, so a local build has no version to compare).
|
|
97
|
+
Staleness compares like with like, treats a kind change (local <-> release) as stale, and
|
|
98
|
+
never downgrades when the installed helper is ahead of the floor. A pre-JSON stamp is
|
|
99
|
+
stale exactly once and is re-stamped in the new format, so the migration cannot loop.
|
|
100
|
+
`agents menubar status` / `doctor` now print three labelled lines — helper installed,
|
|
101
|
+
helper available, CLI version — instead of conflating two axes into one, and the
|
|
102
|
+
permanent false "(mismatch — `agents menubar setup` updates it)" hint is gone.
|
|
103
|
+
Source: `cli/src/lib/menubar/install-menubar.ts`.
|
|
104
|
+
|
|
105
|
+
- **The Windows helper's "asset missing" error names a tag that exists (RUSH-3230).**
|
|
106
|
+
`downloadWinHelperExe` built its error message from `` `v${version}` `` — the CLI's tag
|
|
107
|
+
shape — while the URL it had just tried came from `helperTag('computer-win', version)`.
|
|
108
|
+
So a genuine 404 sent the reader looking for `v1.0.0`, which does not exist, instead of
|
|
109
|
+
`computer-win/v1.0.0`, which does. The mac path was corrected when helpers moved to their
|
|
110
|
+
own tags; the Windows path was left behind. Also drops a `getCliVersion` import that had
|
|
111
|
+
no call site — the last trace of the old CLI-version coupling in that file.
|
|
112
|
+
Source: `cli/src/lib/computer/ssh-tunnel.ts`.
|
|
113
|
+
|
|
114
|
+
- **Claude status-line delegate no longer fork-bombs the machine.** The delegate
|
|
115
|
+
self-reference guard compared the saved command against the exact literal
|
|
116
|
+
`agents __claude-statusline`, so a delegate seeded with the same private
|
|
117
|
+
subcommand under a *different* binary name (e.g. `agents-dev __claude-statusline`
|
|
118
|
+
from a dev install, or an absolute path) was not recognized as us. Every
|
|
119
|
+
status-line render then `spawnSync`ed that command, which read the same delegate
|
|
120
|
+
and spawned another — unbounded recursion. Observed on a real box: ~4,900 live
|
|
121
|
+
`node __claude-statusline` processes accumulated over a 2-day uptime, exhausting
|
|
122
|
+
96 GB of swap and driving load past 300, so keystrokes lagged. The guard now
|
|
123
|
+
matches the `__claude-statusline` subcommand under any binary name or path
|
|
124
|
+
(`isStatusLineSelfReference`), `installClaudeStatusLine` refuses to persist such
|
|
125
|
+
a command as a delegate (and deletes an already-poisoned delegate on re-install),
|
|
126
|
+
a `AGENTS_CLAUDE_STATUSLINE_DELEGATED` env marker hard-caps delegation at one hop,
|
|
127
|
+
and the delegate `spawnSync` now carries a 5 s timeout. Source:
|
|
128
|
+
`cli/src/lib/claude-statusline.ts`.
|
|
129
|
+
|
|
3
130
|
## 1.22.50
|
|
4
131
|
|
|
5
132
|
### Fixed
|
package/dist/bootstrap.js
CHANGED
|
@@ -322,7 +322,7 @@ async function showWhatsNew(fromVersion, toVersion) {
|
|
|
322
322
|
for (const line of relevantChanges) {
|
|
323
323
|
console.log(line);
|
|
324
324
|
}
|
|
325
|
-
console.log(chalk.gray('\nFull notes: https://github.com/phnx-labs/
|
|
325
|
+
console.log(chalk.gray('\nFull notes: https://github.com/phnx-labs/agi-cli/blob/main/CHANGELOG.md'));
|
|
326
326
|
console.log();
|
|
327
327
|
}
|
|
328
328
|
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { type BrowserProfile } from '../lib/browser/profiles.js';
|
|
3
3
|
import { listInstalledBrowsers } from '../lib/browser/chrome.js';
|
|
4
|
+
/**
|
|
5
|
+
* The fleet browser hub this box drives by default (`browser.device`), or
|
|
6
|
+
* undefined to drive locally. It is one fleet-synced value in the central
|
|
7
|
+
* agents.yaml, so every box reads the same hub name — but the hub itself resolves
|
|
8
|
+
* to a self-host, and a fleet-dispatched invocation is already ON its target, so
|
|
9
|
+
* both return undefined. That self short-circuit is what lets a single synced
|
|
10
|
+
* value be safe: only the boxes that are NOT the hub forward to it, and the hub
|
|
11
|
+
* never forwards to itself.
|
|
12
|
+
*/
|
|
13
|
+
export declare function defaultBrowserHub(): string | undefined;
|
|
4
14
|
export declare function registerBrowserCommand(program: Command): void;
|
|
5
15
|
export declare function registerBrowserSubcommands(program: Command): void;
|
|
6
16
|
interface BrowserUseOptions {
|
package/dist/commands/browser.js
CHANGED
|
@@ -248,6 +248,23 @@ function assertDeviceDeclaresProfile(device, profileName) {
|
|
|
248
248
|
throw new Error(`Device "${device}" does not declare browser profile "${profileName}". ${where}\n` +
|
|
249
249
|
`Next: run \`agents browser profiles add ${profileName} --browser <b>\` on ${device}.`);
|
|
250
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* The fleet browser hub this box drives by default (`browser.device`), or
|
|
253
|
+
* undefined to drive locally. It is one fleet-synced value in the central
|
|
254
|
+
* agents.yaml, so every box reads the same hub name — but the hub itself resolves
|
|
255
|
+
* to a self-host, and a fleet-dispatched invocation is already ON its target, so
|
|
256
|
+
* both return undefined. That self short-circuit is what lets a single synced
|
|
257
|
+
* value be safe: only the boxes that are NOT the hub forward to it, and the hub
|
|
258
|
+
* never forwards to itself.
|
|
259
|
+
*/
|
|
260
|
+
export function defaultBrowserHub() {
|
|
261
|
+
if (isFleetRemoteInvocation())
|
|
262
|
+
return undefined;
|
|
263
|
+
const hub = getConfigValue('browser.device').value;
|
|
264
|
+
if (!hub || isSelfHost(hub))
|
|
265
|
+
return undefined;
|
|
266
|
+
return hub;
|
|
267
|
+
}
|
|
251
268
|
// Help groups — surfaces the actual mental model an agent follows
|
|
252
269
|
// ("open a session / drive the page / capture evidence / rare extras")
|
|
253
270
|
// instead of an alphabetical dump. Everything not listed falls into a
|
|
@@ -356,7 +373,14 @@ export async function runBrowserUse(name, opts, interactive = isInteractiveTermi
|
|
|
356
373
|
console.log(current
|
|
357
374
|
? `Default browser profile (this machine): ${current}`
|
|
358
375
|
: 'Default browser profile (this machine): auto-detect');
|
|
376
|
+
const hub = getConfigValue('browser.device').value;
|
|
377
|
+
if (hub) {
|
|
378
|
+
console.log(isSelfHost(hub)
|
|
379
|
+
? `Browser hub (browser.device): ${hub} — this machine, so drives locally`
|
|
380
|
+
: `Browser hub (browser.device): ${hub} — bare \`agents browser start\` drives ${hub}'s browser`);
|
|
381
|
+
}
|
|
359
382
|
console.log('Usage: agents browser use <name> (or --unset)');
|
|
383
|
+
console.log('Fleet hub: agents config set browser.device <device> (drive that box\'s browser from every machine)');
|
|
360
384
|
return true;
|
|
361
385
|
}
|
|
362
386
|
const choices = buildBrowserUseChoices(await listProfiles(), listInstalledBrowsers(), current);
|
|
@@ -1353,7 +1377,7 @@ function registerTaskCommands(browser) {
|
|
|
1353
1377
|
.option('--title <label>', 'Human label shown in `browser status` (defaults to first navigated host)')
|
|
1354
1378
|
.option('-e, --endpoint <name>', 'Endpoint preset (defaults to the profile\'s default)')
|
|
1355
1379
|
.option('-u, --url <url>', 'Open URL in first tab')
|
|
1356
|
-
.option('--device <name>', 'Device that hosts this task;
|
|
1380
|
+
.option('--device <name>', 'Device that hosts this task (defaults to the browser.device hub when set; use `local` to force this machine). Later verbs resolve it from --task; not valid on page verbs')
|
|
1357
1381
|
.option('--fresh', 'Always open a new tab, skipping the reclaim of a tab an abandoned task is holding on that URL')
|
|
1358
1382
|
.option('--no-skills', 'Skip auto-discovery of site-specific SKILL.md from ~/.agents/skills/browser/domain-skills/')
|
|
1359
1383
|
.option('--record', 'Start recording right after the tab opens (shorthand for `agents browser record start` as a follow-up)')
|
|
@@ -1373,6 +1397,54 @@ function registerTaskCommands(browser) {
|
|
|
1373
1397
|
console.error(err instanceof Error ? err.message : String(err));
|
|
1374
1398
|
process.exit(1);
|
|
1375
1399
|
}
|
|
1400
|
+
// Thin-client hub (PHNX-2010): when this box has a fleet browser hub
|
|
1401
|
+
// (`browser.device`) and the caller gave no explicit --device, forward the
|
|
1402
|
+
// WHOLE start there — bare OR --profile — and skip the local profile/browser
|
|
1403
|
+
// resolution below. The hub owns its profiles, browser, and consent, so this
|
|
1404
|
+
// box needs no local browser and need not carry the named profile; forwarding
|
|
1405
|
+
// the local pre-checks here would reject a profile that lives only on the hub.
|
|
1406
|
+
// Later page verbs follow the task to the hub via the task→device index bound
|
|
1407
|
+
// here. `--device local`/`--device self` (below) force a local run instead;
|
|
1408
|
+
// `defaultBrowserHub()` is undefined on the hub itself and on a fleet-remote
|
|
1409
|
+
// re-exec, so neither forwards to itself.
|
|
1410
|
+
if (!opts.device) {
|
|
1411
|
+
const hub = defaultBrowserHub();
|
|
1412
|
+
if (hub) {
|
|
1413
|
+
// No local profile validation: the hub is the authority on its own
|
|
1414
|
+
// profiles (including fleet-scoped ones this box can't see in
|
|
1415
|
+
// `declaringDevices`), so a bad `--profile` is caught by the hub's own
|
|
1416
|
+
// start and streamed back with its exit code, rather than second-guessed
|
|
1417
|
+
// here against a registry that doesn't carry the hub's profiles.
|
|
1418
|
+
try {
|
|
1419
|
+
const result = await dispatchBrowserToDevice(hub, browserForwardedArgv(), 'capture');
|
|
1420
|
+
process.stdout.write(result.stdout);
|
|
1421
|
+
process.stderr.write(result.stderr);
|
|
1422
|
+
if (result.code !== 0)
|
|
1423
|
+
process.exit(result.code);
|
|
1424
|
+
const taskName = opts.task || result.stdout.trim().split('\n').find((line) => line.length > 0);
|
|
1425
|
+
if (!taskName) {
|
|
1426
|
+
console.error(`Remote start on ${hub} produced no task name.`);
|
|
1427
|
+
process.exit(1);
|
|
1428
|
+
}
|
|
1429
|
+
bindTask(taskName, {
|
|
1430
|
+
device: hub,
|
|
1431
|
+
// Only what the caller named; the hub's own default (bare start) is
|
|
1432
|
+
// opaque here. Name-based `done`/`stop --task` and the post-stop
|
|
1433
|
+
// unbind GC these regardless.
|
|
1434
|
+
profile: opts.profile,
|
|
1435
|
+
url: opts.url,
|
|
1436
|
+
sessionId: callerSessionId(),
|
|
1437
|
+
launchId: callerLaunchId(),
|
|
1438
|
+
createdAt: Date.now(),
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
catch (err) {
|
|
1442
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1443
|
+
process.exit(1);
|
|
1444
|
+
}
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1376
1448
|
// One resolution order for every command (RUSH-2709): `--profile default`
|
|
1377
1449
|
// means the same profile here as it does in stop / status / navigate.
|
|
1378
1450
|
// `start` is the one command that LAUNCHES, so its implicit path goes
|
|
@@ -1409,7 +1481,10 @@ function registerTaskCommands(browser) {
|
|
|
1409
1481
|
process.exit(1);
|
|
1410
1482
|
}
|
|
1411
1483
|
}
|
|
1412
|
-
|
|
1484
|
+
// The `browser.device` hub default is applied above (bare/--profile, no
|
|
1485
|
+
// --device). Here only an EXPLICIT --device remains — with `local`/`self` as
|
|
1486
|
+
// the escape that forces a local run when a hub is configured.
|
|
1487
|
+
const deviceName = opts.device === 'local' || opts.device === 'self' ? undefined : opts.device;
|
|
1413
1488
|
if (deviceName) {
|
|
1414
1489
|
const lowered = deviceName.toLowerCase();
|
|
1415
1490
|
if (lowered === 'all' || lowered === 'auto') {
|
package/dist/commands/config.js
CHANGED
|
@@ -111,6 +111,11 @@ function setConfig(parsed, value) {
|
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
case 'browser': {
|
|
114
|
+
if (parsed.property === 'device') {
|
|
115
|
+
// Fleet hub: user scope, one central value, never peer-targeted.
|
|
116
|
+
setConfigValue('browser.device', value);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
114
119
|
// Device-local default lives in the per-device doc's config: block
|
|
115
120
|
// (same store `agents devices config` / getConfigValue use). Bare
|
|
116
121
|
// browser.profile targets this machine; devices.<name>.browser.profile
|
|
@@ -155,6 +160,11 @@ function unsetConfig(parsed) {
|
|
|
155
160
|
return had;
|
|
156
161
|
}
|
|
157
162
|
case 'browser': {
|
|
163
|
+
if (parsed.property === 'device') {
|
|
164
|
+
const had = getConfigValue('browser.device').value !== undefined;
|
|
165
|
+
unsetConfigValue('browser.device');
|
|
166
|
+
return had;
|
|
167
|
+
}
|
|
158
168
|
const target = parsed.device ? { device: parsed.device } : undefined;
|
|
159
169
|
// Must follow parsed.property. Hardcoding 'browser.profile' here meant
|
|
160
170
|
// `config unset browser.viewer` deleted the user's browser.profile while
|
|
@@ -195,6 +205,9 @@ function getConfig(parsed) {
|
|
|
195
205
|
case 'auto':
|
|
196
206
|
return getConfigValue('auto.pool').value;
|
|
197
207
|
case 'browser': {
|
|
208
|
+
if (parsed.property === 'device') {
|
|
209
|
+
return getConfigValue('browser.device').value;
|
|
210
|
+
}
|
|
198
211
|
return getConfigValue(parsed.property === 'viewer' ? 'browser.viewer' : 'browser.profile', parsed.device ? { device: parsed.device } : undefined).value;
|
|
199
212
|
}
|
|
200
213
|
case 'project':
|
|
@@ -267,6 +280,13 @@ function* listCentralConfigEntries() {
|
|
|
267
280
|
const key = 'browser.viewer';
|
|
268
281
|
yield { key, value: browserViewer, hint: configKeyStorageHint(parseConfigKey(key)) };
|
|
269
282
|
}
|
|
283
|
+
// Fleet browser hub — user scope, so it belongs in the central listing next to
|
|
284
|
+
// its siblings. Omitting it repeats the browser.viewer invisibility bug.
|
|
285
|
+
const browserDevice = getConfigValue('browser.device').value;
|
|
286
|
+
if (browserDevice !== undefined) {
|
|
287
|
+
const key = 'browser.device';
|
|
288
|
+
yield { key, value: browserDevice, hint: configKeyStorageHint(parseConfigKey(key)) };
|
|
289
|
+
}
|
|
270
290
|
}
|
|
271
291
|
/** Collect device-scope config entries. */
|
|
272
292
|
function* listDeviceConfigEntries(device) {
|
|
@@ -7,7 +7,7 @@ import { arch, platform, release } from 'node:os';
|
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { showUrl } from '../lib/open-url.js';
|
|
10
|
-
const REPO = 'phnx-labs/
|
|
10
|
+
const REPO = 'phnx-labs/agi-cli';
|
|
11
11
|
const DISCUSSION_BASE = `https://github.com/${REPO}/discussions/new`;
|
|
12
12
|
const ISSUE_BASE = `https://github.com/${REPO}/issues/new`;
|
|
13
13
|
function readCliVersion() {
|
package/dist/commands/menubar.js
CHANGED
|
@@ -27,8 +27,9 @@ function printStatus(s, opts = {}) {
|
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
console.log(` app installed ${s.installedApp ? chalk.gray(s.installedApp) : chalk.gray('no')}`);
|
|
30
|
-
console.log(` installed
|
|
31
|
-
console.log(`
|
|
30
|
+
console.log(` helper installed ${s.installedVersion ? chalk.gray(s.installedVersion) : chalk.gray('unknown')}`);
|
|
31
|
+
console.log(` helper available ${chalk.gray(s.currentVersion)}`);
|
|
32
|
+
console.log(` CLI version ${chalk.gray(s.cliVersion)}`);
|
|
32
33
|
console.log(` bundle source ${s.source ? chalk.gray(s.source) : chalk.red('missing (cannot enable)')}`);
|
|
33
34
|
console.log(` disabled by user ${yn(s.disabledByUser)}`);
|
|
34
35
|
// Two copies of the INSTALLED bundle is the duplicate the user sees as two
|
|
@@ -80,8 +81,9 @@ function printSetupResult(r) {
|
|
|
80
81
|
function printDoctorReport(r) {
|
|
81
82
|
console.log(chalk.bold('AGI Menu doctor\n'));
|
|
82
83
|
console.log(` install path ${r.installPath ? chalk.gray(r.installPath) : chalk.red('not installed')}`);
|
|
83
|
-
console.log(` installed
|
|
84
|
-
console.log(`
|
|
84
|
+
console.log(` helper installed ${r.installedVersion ? chalk.gray(r.installedVersion) : chalk.gray('unknown')}`);
|
|
85
|
+
console.log(` helper available ${chalk.gray(r.currentVersion)}${r.versionMatches ? '' : chalk.yellow(' (newer helper available — `agents menubar setup` installs it)')}`);
|
|
86
|
+
console.log(` CLI version ${chalk.gray(r.cliVersion)}`);
|
|
85
87
|
const identity = r.signingIdentity === 'developer-id'
|
|
86
88
|
? chalk.green('Developer ID (update-stable)')
|
|
87
89
|
: r.signingIdentity === 'ad-hoc'
|
package/dist/commands/traces.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { setHelpSections } from '../lib/help.js';
|
|
3
|
-
import { syncTraces } from '../lib/traces/sync.js';
|
|
4
|
-
import { readSyncLedger } from '../lib/traces/sync.js';
|
|
3
|
+
import { readSyncLedger, syncTraces } from '../lib/traces/sync.js';
|
|
5
4
|
import { managedTracesBaseUrl, resolveTracesBackend } from '../lib/traces/backend.js';
|
|
6
5
|
import { showUrl } from '../lib/open-url.js';
|
|
7
6
|
import { DEFAULT_CF_BUNDLE, readCloudflareCreds } from '../lib/share/config.js';
|
|
@@ -93,7 +92,18 @@ async function handleSync(opts) {
|
|
|
93
92
|
parts.push(chalk.dim(`${result.skipped} skipped`));
|
|
94
93
|
}
|
|
95
94
|
if (result.errors > 0) {
|
|
96
|
-
|
|
95
|
+
// Distinguish expected history (transcripts cleaned off disk) from genuine
|
|
96
|
+
// failures that will be retried, so the operator knows which need attention.
|
|
97
|
+
const detail = [];
|
|
98
|
+
if (result.transcriptUnavailable > 0) {
|
|
99
|
+
detail.push(`${result.transcriptUnavailable} transcripts no longer on disk`);
|
|
100
|
+
}
|
|
101
|
+
const retryable = result.parseFailed + result.uploadFailed;
|
|
102
|
+
if (retryable > 0) {
|
|
103
|
+
detail.push(`${retryable} parse/upload failures — will retry`);
|
|
104
|
+
}
|
|
105
|
+
parts.push(chalk.yellow(`${result.errors} errors`) +
|
|
106
|
+
(detail.length ? chalk.dim(` (${detail.join(' · ')})`) : ''));
|
|
97
107
|
}
|
|
98
108
|
if (parts.length === 0) {
|
|
99
109
|
parts.push(chalk.dim('nothing new'));
|
|
@@ -108,7 +118,25 @@ async function handleStatus() {
|
|
|
108
118
|
}
|
|
109
119
|
const when = new Date(ledger.lastSyncMtime).toLocaleString();
|
|
110
120
|
console.log(`Last sync: ${chalk.bold(when)}`);
|
|
111
|
-
|
|
121
|
+
const failures = ledger.failures ?? [];
|
|
122
|
+
if (failures.length > 0) {
|
|
123
|
+
const byKind = new Map();
|
|
124
|
+
for (const f of failures) {
|
|
125
|
+
const list = byKind.get(f.kind) ?? [];
|
|
126
|
+
list.push(f);
|
|
127
|
+
byKind.set(f.kind, list);
|
|
128
|
+
}
|
|
129
|
+
console.log(chalk.yellow(`\n${failures.length} unresolved failure${failures.length === 1 ? '' : 's'}:`));
|
|
130
|
+
for (const [kind, list] of byKind) {
|
|
131
|
+
const retry = kind === 'transcript-unavailable' ? chalk.dim(' (not retried)') : chalk.dim(' (retried each sync)');
|
|
132
|
+
console.log(` ${chalk.bold(list.length)} ${kind}${retry}`);
|
|
133
|
+
const example = list[0];
|
|
134
|
+
if (example?.detail) {
|
|
135
|
+
console.log(chalk.dim(` e.g. ${example.id.slice(0, 8)} · ${example.detail}`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
console.log(chalk.dim('\nRun `agents traces sync` to push new sessions.'));
|
|
112
140
|
}
|
|
113
141
|
async function handleOpen() {
|
|
114
142
|
// M2 console — for now open the base URL which will route to the console once deployed.
|
|
@@ -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 {
|