@phnx-labs/agents-cli 1.22.102 → 1.22.103
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 +4 -0
- package/dist/bootstrap.js +15 -0
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/menubar/install-menubar.d.ts +56 -0
- package/dist/lib/menubar/install-menubar.js +133 -17
- package/dist/lib/menubar/resolve-version.d.ts +82 -0
- package/dist/lib/menubar/resolve-version.js +133 -0
- package/dist/lib/profiles.js +13 -2
- package/dist/lib/secrets-client.d.ts +20 -0
- package/dist/lib/secrets-client.js +42 -19
- package/dist/lib/self-heal/checks/menubar-helper.d.ts +2 -0
- package/dist/lib/self-heal/checks/menubar-helper.js +21 -0
- package/dist/lib/self-heal/registry.js +3 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.103
|
|
4
|
+
|
|
5
|
+
- **`agents run claude` no longer dies with `secrets request failed: spawnSync sh ETIMEDOUT` on a loaded machine (PHNX-4078).** Every synchronous secrets request spawns a fresh `secrets __serve` process, so its bound has to cover a cold Node boot of the standalone, not just the tens-of-milliseconds operation. The bound was 3 s; on a desktop at load average ~100 each spawn measured 0.4–2.6 s and sometimes more, so the account listing on the launch path timed out and the run aborted before Claude started (also why AGI EXT tabs opened to a bare shell there). The sync bound is now 30 s — enough to boot under load, still well under the standalone's own 60 s self-deadline — and a timeout names the cause ("the standalone secrets CLI did not answer within 30s … check with secrets --version") instead of the raw `spawnSync` errno. Source: `cli/src/lib/secrets-client.ts`, `cli/src/lib/secrets-client.test.ts`, `cli/docs/secrets-client.md`.
|
|
6
|
+
|
|
3
7
|
## 1.22.102
|
|
4
8
|
|
|
5
9
|
- **AGI Menu updates itself on npm-installed Macs (PHNX-4036).** The npm tarball ships no helper bundle, and the startup self-heal is network-free by design, so an `npm i -g` Mac kept whatever helper it had — the crashing 1.1.2, a dead 0.1.0 — until someone ran `agents menubar setup` by hand. The detached background sync now downloads and verifies the floor release into the helper cache whenever the installed helper is behind it (or no service exists yet and the user has not opted out), and the self-heal treats that cached bundle as a source, so the next `agents` invocation installs it with no foreground network. `agents menubar status` names the pending download instead of "missing (cannot enable)". Source: `cli/src/lib/menubar/install-menubar.ts`, `cli/src/lib/auto-pull-worker.ts`.
|
package/dist/bootstrap.js
CHANGED
|
@@ -385,6 +385,21 @@ async function installResolvedPackage(metadata) {
|
|
|
385
385
|
// The macOS Keychain helper this used to force-refresh on upgrade moved with
|
|
386
386
|
// the standalone `secrets` engine (PHNX-3989) — it downloads and verifies its
|
|
387
387
|
// own helper release now, off this CLI's upgrade path entirely.
|
|
388
|
+
//
|
|
389
|
+
// The menu-bar helper still rides this path: an installed release build moves
|
|
390
|
+
// to the newest published one (verified download, atomic swap at the same
|
|
391
|
+
// path and identity so the Accessibility grant survives, restart). The new
|
|
392
|
+
// package is on disk, so the import resolves the fresh module. Best-effort;
|
|
393
|
+
// the daemon's self-heal tick repeats it every six hours.
|
|
394
|
+
if (process.platform === 'darwin') {
|
|
395
|
+
try {
|
|
396
|
+
const { updateMenubarHelperIfNewer } = await import('./lib/menubar/install-menubar.js');
|
|
397
|
+
await updateMenubarHelperIfNewer({ force: true });
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Non-fatal.
|
|
401
|
+
}
|
|
402
|
+
}
|
|
388
403
|
}
|
|
389
404
|
/** Present an interactive upgrade prompt (TTY) or a one-line hint (non-TTY). */
|
|
390
405
|
async function promptUpgrade(latestVersion) {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* helper release now, off this table entirely.
|
|
37
37
|
*/
|
|
38
38
|
export const HELPER_RELEASES = {
|
|
39
|
-
menubar: { tagPrefix: 'menubar', floor: '1.
|
|
39
|
+
menubar: { tagPrefix: 'menubar', floor: '1.3.0' },
|
|
40
40
|
'computer-mac': { tagPrefix: 'computer-mac', floor: '1.0.0' },
|
|
41
41
|
// The Windows helper is a bare .exe, not an .app bundle, so it does not share
|
|
42
42
|
// helper-download.ts's zip/codesign/notarize machinery -- but it has the same
|
|
@@ -70,6 +70,20 @@ export declare function menubarDisabledByUser(): boolean;
|
|
|
70
70
|
export declare function menubarServiceInstalled(): boolean;
|
|
71
71
|
/** Where a downloaded copy of the floor release sits once fetched and verified. */
|
|
72
72
|
export declare function cachedFloorBundlePath(): string;
|
|
73
|
+
/**
|
|
74
|
+
* Where the downloaded copy of the RESOLVED release sits (`cachedMenubarVersion`,
|
|
75
|
+
* so the floor's cache dir until this machine has resolved something newer).
|
|
76
|
+
* The startup self-heal installs from here, network-free.
|
|
77
|
+
*/
|
|
78
|
+
export declare function cachedReleaseBundlePath(): string;
|
|
79
|
+
/**
|
|
80
|
+
* The release version an explicit install should fetch: the newest published
|
|
81
|
+
* build, but never below what this Mac already runs — a release deleted after
|
|
82
|
+
* it was installed must not roll the helper back through `setup`/`enable`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function menubarVersionToInstall(opts?: {
|
|
85
|
+
force?: boolean;
|
|
86
|
+
}): Promise<string>;
|
|
73
87
|
/** True when the bundle carries a signature the kernel will accept at launch. */
|
|
74
88
|
export declare function codesignVerifies(appPath: string): boolean;
|
|
75
89
|
/**
|
|
@@ -511,3 +525,45 @@ export declare function isMenubarProcessStaleAgainstBundle(pidStartedAtMs: numbe
|
|
|
511
525
|
* remains the command that fixes what this reports.
|
|
512
526
|
*/
|
|
513
527
|
export declare function buildMenubarDoctorReport(): MenubarDoctorReport;
|
|
528
|
+
/** Outcome of one auto-update pass (`updateMenubarHelperIfNewer`). */
|
|
529
|
+
export interface MenubarUpdateResult {
|
|
530
|
+
outcome: 'updated' | 'current' | 'skipped' | 'failed';
|
|
531
|
+
installed: string | null;
|
|
532
|
+
available: string;
|
|
533
|
+
detail: string;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Bring an installed release helper up to the newest published build, without
|
|
537
|
+
* a human running `agents menubar setup`.
|
|
538
|
+
*
|
|
539
|
+
* Runs from the daemon's self-heal tick and right after `agents upgrade`. It
|
|
540
|
+
* only ever moves a RELEASE install forward: a machine whose helper came from a
|
|
541
|
+
* local build (a dev checkout) keeps it, a machine that never enabled the menu
|
|
542
|
+
* bar or opted out is left alone, and the ownership contest that bounds
|
|
543
|
+
* multi-install churn (`mayInstallMenubarHelper`) still applies. The bundle is
|
|
544
|
+
* downloaded and verified by the same path every install uses (sha256,
|
|
545
|
+
* codesign, Team, designated-requirement pin, notarization), swapped
|
|
546
|
+
* atomically under the install lock at the same path and identity — which is
|
|
547
|
+
* what keeps the Accessibility grant — and the running helper is restarted
|
|
548
|
+
* from the new binary (`restartMenubarHelperAfterSwap`; launchd's KeepAlive
|
|
549
|
+
* relaunches it within seconds). `dryRun` reports what would happen. Never
|
|
550
|
+
* throws.
|
|
551
|
+
*/
|
|
552
|
+
/**
|
|
553
|
+
* Pure (no I/O): whether an auto-update pass should proceed, and why not.
|
|
554
|
+
* `shipped` is a bundle that ships WITH this install (never a downloaded
|
|
555
|
+
* release cache) — CLI upgrades own that one. Returns null to proceed.
|
|
556
|
+
*/
|
|
557
|
+
export declare function menubarUpdateSkipReason(opts: {
|
|
558
|
+
darwin: boolean;
|
|
559
|
+
disabledByUser: boolean;
|
|
560
|
+
serviceInstalled: boolean;
|
|
561
|
+
shipped: boolean;
|
|
562
|
+
installed: MenubarStamp | null;
|
|
563
|
+
}): string | null;
|
|
564
|
+
/** Pure (no I/O): what the pass does given the installed and available versions. */
|
|
565
|
+
export declare function menubarUpdateOutcome(installed: string, available: string): 'current' | 'updated';
|
|
566
|
+
export declare function updateMenubarHelperIfNewer(opts?: {
|
|
567
|
+
dryRun?: boolean;
|
|
568
|
+
force?: boolean;
|
|
569
|
+
}): Promise<MenubarUpdateResult>;
|
|
@@ -27,6 +27,7 @@ import { compareVersions } from '../agent-spec/primitives.js';
|
|
|
27
27
|
import { namespacedServiceLabel, serviceManifestHomeEnv, serviceManagerRegistrationAllowed } from '../service-manifest.js';
|
|
28
28
|
import { downloadMenubarHelperApp, menubarHelperCacheDir } from './download-menubar.js';
|
|
29
29
|
import { helperFloor } from '../helper-versions.js';
|
|
30
|
+
import { cachedMenubarVersion, resolveMenubarVersion } from './resolve-version.js';
|
|
30
31
|
const APP_BUNDLE_NAME = 'MenubarHelper.app';
|
|
31
32
|
const INSTALL_DIR_NAME = 'agents-cli';
|
|
32
33
|
const SERVICE_LABEL_BASE = 'com.phnx-labs.agents-menubar';
|
|
@@ -171,15 +172,30 @@ export function menubarServiceInstalled() {
|
|
|
171
172
|
* binary: `import.meta.url` is a virtual `/$bunfs/` path, so the sibling
|
|
172
173
|
* candidates above can't see the on-disk bundle; recover it via the
|
|
173
174
|
* `agents` launcher symlink.
|
|
174
|
-
* 4. the verified download cache for the
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
175
|
+
* 4. the verified download cache for the RELEASE this machine has resolved
|
|
176
|
+
* (`cachedMenubarVersion`: the newest published build it last saw, never
|
|
177
|
+
* below the floor, the floor itself before any resolution) — the npm
|
|
178
|
+
* tarball ships no bundle (PHNX-4036), so on an `npm i -g` machine this is
|
|
179
|
+
* the only source there is. It is filled by `agents menubar setup` or by
|
|
180
|
+
* the detached background worker (`prefetchMenubarHelper`), never here:
|
|
181
|
+
* this resolver stays network-free so the startup self-heal stays cheap.
|
|
182
|
+
* Last so a bundle that ships with the build always wins; keyed by the
|
|
183
|
+
* resolved version so an older cached release is never picked up.
|
|
184
|
+
*
|
|
185
|
+
* `shippedAppPath` is candidates 1–3 alone: a bundle that ships WITH this
|
|
186
|
+
* install, which CLI upgrades own. The auto-update pass replaces a cached
|
|
187
|
+
* release (candidate 4) but never a shipped bundle.
|
|
181
188
|
*/
|
|
182
189
|
function sourceAppPath() {
|
|
190
|
+
const shipped = shippedAppPath();
|
|
191
|
+
if (shipped)
|
|
192
|
+
return shipped;
|
|
193
|
+
const cached = cachedReleaseBundlePath();
|
|
194
|
+
if (fs.existsSync(cached))
|
|
195
|
+
return cached;
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
function shippedAppPath() {
|
|
183
199
|
const candidates = [];
|
|
184
200
|
try {
|
|
185
201
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -203,15 +219,32 @@ function sourceAppPath() {
|
|
|
203
219
|
if (fs.existsSync(p))
|
|
204
220
|
return p;
|
|
205
221
|
}
|
|
206
|
-
const cached = cachedFloorBundlePath();
|
|
207
|
-
if (fs.existsSync(cached))
|
|
208
|
-
return cached;
|
|
209
222
|
return null;
|
|
210
223
|
}
|
|
211
224
|
/** Where a downloaded copy of the floor release sits once fetched and verified. */
|
|
212
225
|
export function cachedFloorBundlePath() {
|
|
213
226
|
return path.join(menubarHelperCacheDir(helperFloor('menubar')), APP_BUNDLE_NAME);
|
|
214
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Where the downloaded copy of the RESOLVED release sits (`cachedMenubarVersion`,
|
|
230
|
+
* so the floor's cache dir until this machine has resolved something newer).
|
|
231
|
+
* The startup self-heal installs from here, network-free.
|
|
232
|
+
*/
|
|
233
|
+
export function cachedReleaseBundlePath() {
|
|
234
|
+
return path.join(menubarHelperCacheDir(cachedMenubarVersion()), APP_BUNDLE_NAME);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The release version an explicit install should fetch: the newest published
|
|
238
|
+
* build, but never below what this Mac already runs — a release deleted after
|
|
239
|
+
* it was installed must not roll the helper back through `setup`/`enable`.
|
|
240
|
+
*/
|
|
241
|
+
export async function menubarVersionToInstall(opts = {}) {
|
|
242
|
+
const resolved = await resolveMenubarVersion({ force: opts.force });
|
|
243
|
+
const installed = readInstalledMenubarStamp();
|
|
244
|
+
if (installed?.source === 'release' && compareVersions(installed.helperVersion, resolved) > 0)
|
|
245
|
+
return installed.helperVersion;
|
|
246
|
+
return resolved;
|
|
247
|
+
}
|
|
215
248
|
/** Resolve the compiled CLI entry (dist/index.js) so the helper can exec node directly. */
|
|
216
249
|
function resolveCliEntry() {
|
|
217
250
|
try {
|
|
@@ -517,9 +550,11 @@ function startMenubarServiceFromSource(opts = {}) {
|
|
|
517
550
|
export async function enableMenubarService(opts = { clearOptOut: true }) {
|
|
518
551
|
if (!onDarwin())
|
|
519
552
|
return false;
|
|
520
|
-
|
|
553
|
+
// A shipped bundle wins; otherwise fetch the newest published build (a cache
|
|
554
|
+
// hit when the background prefetch already has it), never below what runs.
|
|
555
|
+
let src = shippedAppPath();
|
|
521
556
|
if (!src)
|
|
522
|
-
src = await downloadMenubarHelperApp(
|
|
557
|
+
src = await downloadMenubarHelperApp(await menubarVersionToInstall());
|
|
523
558
|
return startMenubarServiceFromSource({ ...opts, sourceAppPath: src });
|
|
524
559
|
}
|
|
525
560
|
/** Drop the sticky `agents menubar disable` sentinel. */
|
|
@@ -567,8 +602,10 @@ export function stampVersionLabel(stamp) {
|
|
|
567
602
|
function availableStamp() {
|
|
568
603
|
const src = sourceAppPath();
|
|
569
604
|
// No local bundle means the release path: what would be installed is the
|
|
570
|
-
// helper
|
|
571
|
-
|
|
605
|
+
// newest published helper this machine has resolved (cached, day-old at
|
|
606
|
+
// most), never below the floor — and the floor itself before the first
|
|
607
|
+
// resolution or offline.
|
|
608
|
+
return src ? stampFor(src) : { source: 'release', helperVersion: cachedMenubarVersion() };
|
|
572
609
|
}
|
|
573
610
|
/** The helper version this install would put on disk right now, for display. */
|
|
574
611
|
function availableHelperLabel() {
|
|
@@ -687,7 +724,7 @@ export async function prefetchMenubarHelper() {
|
|
|
687
724
|
});
|
|
688
725
|
if (!needed)
|
|
689
726
|
return null;
|
|
690
|
-
return downloadMenubarHelperApp(
|
|
727
|
+
return downloadMenubarHelperApp(await menubarVersionToInstall());
|
|
691
728
|
}
|
|
692
729
|
/**
|
|
693
730
|
* Pure decision (no I/O): did THIS heal actually replace bundle content a live
|
|
@@ -1086,10 +1123,10 @@ export async function runMenubarSetup() {
|
|
|
1086
1123
|
// notarized release asset for this CLI version. Verified (sha256 + codesign +
|
|
1087
1124
|
// Team + designated-requirement pin + notarization) before install; the
|
|
1088
1125
|
// cached copy is the source for `ensureMenubarAppInstalled` below.
|
|
1089
|
-
let src =
|
|
1126
|
+
let src = shippedAppPath();
|
|
1090
1127
|
if (!src) {
|
|
1091
1128
|
try {
|
|
1092
|
-
src = await downloadMenubarHelperApp(
|
|
1129
|
+
src = await downloadMenubarHelperApp(await menubarVersionToInstall({ force: true }));
|
|
1093
1130
|
}
|
|
1094
1131
|
catch (e) {
|
|
1095
1132
|
step('bundle', 'failed', `no AGI Menu bundle ships with this install, and the release-asset download failed: ${e.message}`);
|
|
@@ -1354,3 +1391,82 @@ export function buildMenubarDoctorReport() {
|
|
|
1354
1391
|
accessibilityHintNeeded: signingIdentity === 'ad-hoc' || staleRunningProcess.some((p) => p.stale),
|
|
1355
1392
|
};
|
|
1356
1393
|
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Bring an installed release helper up to the newest published build, without
|
|
1396
|
+
* a human running `agents menubar setup`.
|
|
1397
|
+
*
|
|
1398
|
+
* Runs from the daemon's self-heal tick and right after `agents upgrade`. It
|
|
1399
|
+
* only ever moves a RELEASE install forward: a machine whose helper came from a
|
|
1400
|
+
* local build (a dev checkout) keeps it, a machine that never enabled the menu
|
|
1401
|
+
* bar or opted out is left alone, and the ownership contest that bounds
|
|
1402
|
+
* multi-install churn (`mayInstallMenubarHelper`) still applies. The bundle is
|
|
1403
|
+
* downloaded and verified by the same path every install uses (sha256,
|
|
1404
|
+
* codesign, Team, designated-requirement pin, notarization), swapped
|
|
1405
|
+
* atomically under the install lock at the same path and identity — which is
|
|
1406
|
+
* what keeps the Accessibility grant — and the running helper is restarted
|
|
1407
|
+
* from the new binary (`restartMenubarHelperAfterSwap`; launchd's KeepAlive
|
|
1408
|
+
* relaunches it within seconds). `dryRun` reports what would happen. Never
|
|
1409
|
+
* throws.
|
|
1410
|
+
*/
|
|
1411
|
+
/**
|
|
1412
|
+
* Pure (no I/O): whether an auto-update pass should proceed, and why not.
|
|
1413
|
+
* `shipped` is a bundle that ships WITH this install (never a downloaded
|
|
1414
|
+
* release cache) — CLI upgrades own that one. Returns null to proceed.
|
|
1415
|
+
*/
|
|
1416
|
+
export function menubarUpdateSkipReason(opts) {
|
|
1417
|
+
if (!opts.darwin)
|
|
1418
|
+
return 'macOS only';
|
|
1419
|
+
if (opts.disabledByUser)
|
|
1420
|
+
return 'the menu bar is disabled (agents menubar disable)';
|
|
1421
|
+
if (!opts.serviceInstalled)
|
|
1422
|
+
return 'the menu bar is not installed on this Mac';
|
|
1423
|
+
if (opts.shipped)
|
|
1424
|
+
return 'this install ships its own helper bundle; the startup self-heal owns it';
|
|
1425
|
+
if (!opts.installed || opts.installed.source !== 'release') {
|
|
1426
|
+
return `installed helper is ${stampVersionLabel(opts.installed) ?? 'unstamped'}, not a release`;
|
|
1427
|
+
}
|
|
1428
|
+
return null;
|
|
1429
|
+
}
|
|
1430
|
+
/** Pure (no I/O): what the pass does given the installed and available versions. */
|
|
1431
|
+
export function menubarUpdateOutcome(installed, available) {
|
|
1432
|
+
return compareVersions(available, installed) > 0 ? 'updated' : 'current';
|
|
1433
|
+
}
|
|
1434
|
+
export async function updateMenubarHelperIfNewer(opts = {}) {
|
|
1435
|
+
const installedStamp = readInstalledMenubarStamp();
|
|
1436
|
+
const installed = stampVersionLabel(installedStamp);
|
|
1437
|
+
const skip = (detail, available = cachedMenubarVersion()) => ({ outcome: 'skipped', installed, available, detail });
|
|
1438
|
+
const reason = menubarUpdateSkipReason({
|
|
1439
|
+
darwin: onDarwin(),
|
|
1440
|
+
disabledByUser: menubarDisabledByUser(),
|
|
1441
|
+
serviceInstalled: menubarServiceInstalled(),
|
|
1442
|
+
shipped: Boolean(shippedAppPath()),
|
|
1443
|
+
installed: installedStamp,
|
|
1444
|
+
});
|
|
1445
|
+
if (reason)
|
|
1446
|
+
return skip(reason);
|
|
1447
|
+
const release = installedStamp;
|
|
1448
|
+
const available = await resolveMenubarVersion({ force: opts.force });
|
|
1449
|
+
if (menubarUpdateOutcome(release.helperVersion, available) === 'current') {
|
|
1450
|
+
return { outcome: 'current', installed, available, detail: `AGI Menu ${installed} is the newest published build` };
|
|
1451
|
+
}
|
|
1452
|
+
if (!mayHealMenubar(false))
|
|
1453
|
+
return skip(`another install owns the helper; it will update on its own cooldown`, available);
|
|
1454
|
+
if (opts.dryRun)
|
|
1455
|
+
return { outcome: 'updated', installed, available, detail: `would update AGI Menu ${installed} → ${available}` };
|
|
1456
|
+
try {
|
|
1457
|
+
const src = await downloadMenubarHelperApp(available);
|
|
1458
|
+
const exec = ensureMenubarAppInstalled({ forceReinstall: true, sourceAppPath: src });
|
|
1459
|
+
if (!exec)
|
|
1460
|
+
return { outcome: 'failed', installed, available, detail: 'the verified bundle could not be installed' };
|
|
1461
|
+
try {
|
|
1462
|
+
fs.writeFileSync(installedVersionMarkerPath(), JSON.stringify(stampFor(src)));
|
|
1463
|
+
}
|
|
1464
|
+
catch { /* best effort */ }
|
|
1465
|
+
stampMenubarHeal();
|
|
1466
|
+
restartMenubarHelperAfterSwap(process.getuid?.() ?? 0, liveMenubarProcesses().own);
|
|
1467
|
+
return { outcome: 'updated', installed, available, detail: `AGI Menu ${installed} → ${available}` };
|
|
1468
|
+
}
|
|
1469
|
+
catch (e) {
|
|
1470
|
+
return { outcome: 'failed', installed, available, detail: e.message };
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which menu-bar helper build to install: the newest published release at or
|
|
3
|
+
* above this CLI's floor.
|
|
4
|
+
*
|
|
5
|
+
* `helper-versions.ts` records a FLOOR (the build this CLI was tested against)
|
|
6
|
+
* and promised that resolution "may pick a NEWER build; it must never pick an
|
|
7
|
+
* older one". Until now nothing resolved upward: every install path downloaded
|
|
8
|
+
* the floor, so a helper fix reached a machine only after a CLI release bumped
|
|
9
|
+
* the floor and the user ran `agents menubar setup`. This module is the
|
|
10
|
+
* missing half — discovery — and it is what makes the helper auto-update.
|
|
11
|
+
*
|
|
12
|
+
* Discovery reads the public release list of `phnx-labs/agi-cli` and keeps
|
|
13
|
+
* only tags shaped `menubar/v<x.y.z>` that carry the helper asset AND its
|
|
14
|
+
* sha256 sidecar (a half-uploaded release is not a candidate). The answer is
|
|
15
|
+
* cached for a day under the CLI's cache dir so the sync startup path and the
|
|
16
|
+
* daemon read a file, never the network; the floor stays the offline answer
|
|
17
|
+
* whenever the network or the cache cannot do better. The verified download
|
|
18
|
+
* (`downloadMenubarHelperApp`: sha256, codesign, Team, designated-requirement
|
|
19
|
+
* pin, notarization) is unchanged — resolution only chooses the version.
|
|
20
|
+
*/
|
|
21
|
+
/** The repo whose releases carry `menubar/v*` tags (same as the download URL). */
|
|
22
|
+
export declare const MENUBAR_RELEASES_API = "https://api.github.com/repos/phnx-labs/agi-cli/releases?per_page=100";
|
|
23
|
+
/** How long a resolved answer is trusted before the release list is re-read. */
|
|
24
|
+
export declare const MENUBAR_RESOLVE_TTL_MS: number;
|
|
25
|
+
/** One release as the resolver sees it — the subset of the GitHub shape it reads. */
|
|
26
|
+
export interface ReleaseCandidate {
|
|
27
|
+
tagName: string;
|
|
28
|
+
assets: string[];
|
|
29
|
+
draft?: boolean;
|
|
30
|
+
prerelease?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface MenubarResolveCache {
|
|
33
|
+
/** Epoch ms of the release-list read this answer came from. */
|
|
34
|
+
checkedAt: number;
|
|
35
|
+
/** The newest published helper version at that time (never below the floor then). */
|
|
36
|
+
version: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pure: the newest candidate at or above `floor`, else the floor. A tag must be
|
|
40
|
+
* exactly `menubar/v<x.y.z>` (no pre-release suffix), not a draft or
|
|
41
|
+
* pre-release, and carry both the zip and its `.sha256`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function pickNewestMenubarVersion(candidates: ReleaseCandidate[], floor: string): string;
|
|
44
|
+
/** Where the resolved answer lives: beside the helper's own download cache. */
|
|
45
|
+
export declare function menubarResolveCachePath(): string;
|
|
46
|
+
export declare function readMenubarResolveCache(file: string): MenubarResolveCache | null;
|
|
47
|
+
/**
|
|
48
|
+
* The cached answer when it is usable offline: at or above the floor, of any
|
|
49
|
+
* age. Sync and network-free — this is what the startup self-heal and the
|
|
50
|
+
* status/doctor displays read.
|
|
51
|
+
*/
|
|
52
|
+
export declare function cachedMenubarVersion(opts?: {
|
|
53
|
+
floor?: string;
|
|
54
|
+
cacheFile?: string;
|
|
55
|
+
}): string;
|
|
56
|
+
type FetchLike = (input: string, init?: {
|
|
57
|
+
headers?: Record<string, string>;
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
}) => Promise<{
|
|
60
|
+
ok: boolean;
|
|
61
|
+
status: number;
|
|
62
|
+
json(): Promise<unknown>;
|
|
63
|
+
}>;
|
|
64
|
+
/** Read the release list. Unauthenticated: release metadata is public and this runs at most once a day per machine. */
|
|
65
|
+
export declare function fetchMenubarReleaseCandidates(fetchImpl?: FetchLike): Promise<ReleaseCandidate[]>;
|
|
66
|
+
/**
|
|
67
|
+
* The helper version to install now: the newest published build >= floor.
|
|
68
|
+
*
|
|
69
|
+
* Reads the day-old cache first; re-reads the release list when the cache is
|
|
70
|
+
* missing, stale, or `force` is set; falls back to the cached answer, then the
|
|
71
|
+
* floor, when the network cannot answer. Never throws, never returns a version
|
|
72
|
+
* below the floor.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveMenubarVersion(opts?: {
|
|
75
|
+
floor?: string;
|
|
76
|
+
cacheFile?: string;
|
|
77
|
+
now?: number;
|
|
78
|
+
ttlMs?: number;
|
|
79
|
+
force?: boolean;
|
|
80
|
+
fetchImpl?: FetchLike;
|
|
81
|
+
}): Promise<string>;
|
|
82
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which menu-bar helper build to install: the newest published release at or
|
|
3
|
+
* above this CLI's floor.
|
|
4
|
+
*
|
|
5
|
+
* `helper-versions.ts` records a FLOOR (the build this CLI was tested against)
|
|
6
|
+
* and promised that resolution "may pick a NEWER build; it must never pick an
|
|
7
|
+
* older one". Until now nothing resolved upward: every install path downloaded
|
|
8
|
+
* the floor, so a helper fix reached a machine only after a CLI release bumped
|
|
9
|
+
* the floor and the user ran `agents menubar setup`. This module is the
|
|
10
|
+
* missing half — discovery — and it is what makes the helper auto-update.
|
|
11
|
+
*
|
|
12
|
+
* Discovery reads the public release list of `phnx-labs/agi-cli` and keeps
|
|
13
|
+
* only tags shaped `menubar/v<x.y.z>` that carry the helper asset AND its
|
|
14
|
+
* sha256 sidecar (a half-uploaded release is not a candidate). The answer is
|
|
15
|
+
* cached for a day under the CLI's cache dir so the sync startup path and the
|
|
16
|
+
* daemon read a file, never the network; the floor stays the offline answer
|
|
17
|
+
* whenever the network or the cache cannot do better. The verified download
|
|
18
|
+
* (`downloadMenubarHelperApp`: sha256, codesign, Team, designated-requirement
|
|
19
|
+
* pin, notarization) is unchanged — resolution only chooses the version.
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
import { getCacheDir } from '../state.js';
|
|
24
|
+
import { compareVersions } from '../agent-spec/primitives.js';
|
|
25
|
+
import { helperFloor } from '../helper-versions.js';
|
|
26
|
+
import { getCliVersion } from '../version.js';
|
|
27
|
+
import { MENUBAR_HELPER_ASSET } from './download-menubar.js';
|
|
28
|
+
/** The repo whose releases carry `menubar/v*` tags (same as the download URL). */
|
|
29
|
+
export const MENUBAR_RELEASES_API = 'https://api.github.com/repos/phnx-labs/agi-cli/releases?per_page=100';
|
|
30
|
+
/** How long a resolved answer is trusted before the release list is re-read. */
|
|
31
|
+
export const MENUBAR_RESOLVE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
32
|
+
const TAG = /^menubar\/v(\d+\.\d+\.\d+)$/;
|
|
33
|
+
/**
|
|
34
|
+
* Pure: the newest candidate at or above `floor`, else the floor. A tag must be
|
|
35
|
+
* exactly `menubar/v<x.y.z>` (no pre-release suffix), not a draft or
|
|
36
|
+
* pre-release, and carry both the zip and its `.sha256`.
|
|
37
|
+
*/
|
|
38
|
+
export function pickNewestMenubarVersion(candidates, floor) {
|
|
39
|
+
let best = floor;
|
|
40
|
+
for (const c of candidates) {
|
|
41
|
+
if (c.draft || c.prerelease)
|
|
42
|
+
continue;
|
|
43
|
+
const m = TAG.exec(c.tagName);
|
|
44
|
+
if (!m)
|
|
45
|
+
continue;
|
|
46
|
+
if (!c.assets.includes(MENUBAR_HELPER_ASSET) || !c.assets.includes(`${MENUBAR_HELPER_ASSET}.sha256`))
|
|
47
|
+
continue;
|
|
48
|
+
if (compareVersions(m[1], best) > 0)
|
|
49
|
+
best = m[1];
|
|
50
|
+
}
|
|
51
|
+
return best;
|
|
52
|
+
}
|
|
53
|
+
/** Where the resolved answer lives: beside the helper's own download cache. */
|
|
54
|
+
export function menubarResolveCachePath() {
|
|
55
|
+
return path.join(getCacheDir(), 'menubar', 'latest.json');
|
|
56
|
+
}
|
|
57
|
+
export function readMenubarResolveCache(file) {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
60
|
+
if (typeof parsed.checkedAt === 'number' && typeof parsed.version === 'string' && /^\d+\.\d+\.\d+$/.test(parsed.version)) {
|
|
61
|
+
return { checkedAt: parsed.checkedAt, version: parsed.version };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch { /* absent or unreadable: no cache */ }
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function writeMenubarResolveCache(file, cache) {
|
|
68
|
+
try {
|
|
69
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
70
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
71
|
+
fs.writeFileSync(tmp, JSON.stringify(cache));
|
|
72
|
+
fs.renameSync(tmp, file);
|
|
73
|
+
}
|
|
74
|
+
catch { /* a cache that cannot be written is just a cache miss next time */ }
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The cached answer when it is usable offline: at or above the floor, of any
|
|
78
|
+
* age. Sync and network-free — this is what the startup self-heal and the
|
|
79
|
+
* status/doctor displays read.
|
|
80
|
+
*/
|
|
81
|
+
export function cachedMenubarVersion(opts = {}) {
|
|
82
|
+
const floor = opts.floor ?? helperFloor('menubar');
|
|
83
|
+
const cache = readMenubarResolveCache(opts.cacheFile ?? menubarResolveCachePath());
|
|
84
|
+
return cache && compareVersions(cache.version, floor) >= 0 ? cache.version : floor;
|
|
85
|
+
}
|
|
86
|
+
/** Read the release list. Unauthenticated: release metadata is public and this runs at most once a day per machine. */
|
|
87
|
+
export async function fetchMenubarReleaseCandidates(fetchImpl = fetch) {
|
|
88
|
+
const res = await fetchImpl(MENUBAR_RELEASES_API, {
|
|
89
|
+
headers: {
|
|
90
|
+
Accept: 'application/vnd.github+json',
|
|
91
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
92
|
+
'User-Agent': `agents-cli/${getCliVersion()}`,
|
|
93
|
+
},
|
|
94
|
+
signal: AbortSignal.timeout(5_000),
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok)
|
|
97
|
+
throw new Error(`release list: HTTP ${res.status}`);
|
|
98
|
+
const body = (await res.json());
|
|
99
|
+
if (!Array.isArray(body))
|
|
100
|
+
throw new Error('release list: not an array');
|
|
101
|
+
return body.map((r) => ({
|
|
102
|
+
tagName: r.tag_name ?? '',
|
|
103
|
+
draft: r.draft,
|
|
104
|
+
prerelease: r.prerelease,
|
|
105
|
+
assets: (r.assets ?? []).map((a) => a.name ?? ''),
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The helper version to install now: the newest published build >= floor.
|
|
110
|
+
*
|
|
111
|
+
* Reads the day-old cache first; re-reads the release list when the cache is
|
|
112
|
+
* missing, stale, or `force` is set; falls back to the cached answer, then the
|
|
113
|
+
* floor, when the network cannot answer. Never throws, never returns a version
|
|
114
|
+
* below the floor.
|
|
115
|
+
*/
|
|
116
|
+
export async function resolveMenubarVersion(opts = {}) {
|
|
117
|
+
const floor = opts.floor ?? helperFloor('menubar');
|
|
118
|
+
const file = opts.cacheFile ?? menubarResolveCachePath();
|
|
119
|
+
const now = opts.now ?? Date.now();
|
|
120
|
+
const ttl = opts.ttlMs ?? MENUBAR_RESOLVE_TTL_MS;
|
|
121
|
+
const cached = readMenubarResolveCache(file);
|
|
122
|
+
if (cached && !opts.force && now - cached.checkedAt < ttl && compareVersions(cached.version, floor) >= 0) {
|
|
123
|
+
return cached.version;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const version = pickNewestMenubarVersion(await fetchMenubarReleaseCandidates(opts.fetchImpl), floor);
|
|
127
|
+
writeMenubarResolveCache(file, { checkedAt: now, version });
|
|
128
|
+
return version;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return cached && compareVersions(cached.version, floor) >= 0 ? cached.version : floor;
|
|
132
|
+
}
|
|
133
|
+
}
|
package/dist/lib/profiles.js
CHANGED
|
@@ -11,7 +11,7 @@ import * as crypto from 'node:crypto';
|
|
|
11
11
|
import * as yaml from 'yaml';
|
|
12
12
|
import { ALL_AGENT_IDS } from './agents.js';
|
|
13
13
|
import { getUserAgentsDir } from './state.js';
|
|
14
|
-
import { deleteKeychainTokenSync, getKeychainTokenSync, hasKeychainTokenSync, isSecretsClientError, profileKeychainItem } from './secrets-client.js';
|
|
14
|
+
import { deleteKeychainTokenSync, getKeychainTokenSync, hasKeychainTokenSync, isSecretsClientError, isSecretsTransportError, profileKeychainItem, } from './secrets-client.js';
|
|
15
15
|
import { getPreset } from './profiles-presets.js';
|
|
16
16
|
import { MODEL_TIERS, isTierToken } from './model-tiers.js';
|
|
17
17
|
import { addAccount, findAccount, resolveCredentialAccount } from './account-registry.js';
|
|
@@ -250,7 +250,18 @@ export function profileAuthLabel(profile) {
|
|
|
250
250
|
return `${provider} ${maskToken(token)}`;
|
|
251
251
|
}
|
|
252
252
|
if (profile.auth) {
|
|
253
|
-
|
|
253
|
+
// A status row, like the provider-account rows: a wedged or missing
|
|
254
|
+
// standalone degrades this one label instead of aborting the whole render.
|
|
255
|
+
let stored;
|
|
256
|
+
try {
|
|
257
|
+
stored = hasKeychainTokenSync(profile.auth.keychainItem);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
if (isSecretsTransportError(err))
|
|
261
|
+
return `${provider} unavailable`;
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
return `${provider} ${stored ? 'stored' : 'missing'}`;
|
|
254
265
|
}
|
|
255
266
|
return provider;
|
|
256
267
|
}
|
|
@@ -5,6 +5,24 @@ import type { SecretsBundle, SecretsBackend, ResolveBundleOptions, WriteBundleOp
|
|
|
5
5
|
* independent client legitimately re-declares rather than imports.
|
|
6
6
|
*/
|
|
7
7
|
export declare const PROTOCOL_VERSION = 1;
|
|
8
|
+
/**
|
|
9
|
+
* The synchronous path serves the surfaces that resolve secrets before they can
|
|
10
|
+
* continue — `agents view`, the account-catalog rows, and the account listing and
|
|
11
|
+
* setup-token read on the `agents run` launch path. Those must never hang for the
|
|
12
|
+
* standalone's own 60s deadline (the exact hang PHNX-3989 hit when the child ran
|
|
13
|
+
* under Bun), so the sync serve carries a hard bound below it instead of the
|
|
14
|
+
* async path's 65s. The bound covers a COLD PROCESS, not just the operation: every
|
|
15
|
+
* request spawns `secrets __serve` afresh, so it pays a Node boot plus the
|
|
16
|
+
* standalone's module load before the tens-of-milliseconds op runs: ~0.3s on a
|
|
17
|
+
* quiet box, 0.4–2.6s and occasionally more on a desktop at load average ~100
|
|
18
|
+
* (measured 2026-09-12), where the previous 3s bound turned load into a failed
|
|
19
|
+
* launch (`secrets request failed: spawnSync sh ETIMEDOUT` from `agents run`).
|
|
20
|
+
* 30s absorbs that boot under load and still fails a broken or wedged standalone
|
|
21
|
+
* well before its 60s self-deadline. It is NOT a fallback to the embedded engine
|
|
22
|
+
* (DIST-1); the standalone stays the only implementation. Exported so the
|
|
23
|
+
* real-standalone test can assert a round-trip beats the bound.
|
|
24
|
+
*/
|
|
25
|
+
export declare const SYNC_SERVE_TIMEOUT_MS = 30000;
|
|
8
26
|
export interface SecretsContext {
|
|
9
27
|
/** Bundle allowlist; absent ⇒ full trust (the local agents client today). */
|
|
10
28
|
allowedBundles?: string[];
|
|
@@ -111,6 +129,8 @@ export declare function secretsRequest<T = unknown>(op: string, args?: unknown[]
|
|
|
111
129
|
export declare function secretsRequestSync<T = unknown>(op: string, args?: unknown[], context?: SecretsContext): T;
|
|
112
130
|
/** Test hook: forget the cached binary + handshake so a new env is re-resolved. */
|
|
113
131
|
export declare function _resetSecretsClientForTest(): void;
|
|
132
|
+
/** Shorten the sync bound for a test that plants a hanging standalone; reset restores it. */
|
|
133
|
+
export declare function _setSyncServeTimeoutForTest(ms: number): void;
|
|
114
134
|
export declare function readAndResolveBundleEnv(name: string, opts?: ResolveBundleOptions, context?: SecretsContext): Promise<{
|
|
115
135
|
bundle: SecretsBundle;
|
|
116
136
|
env: Record<string, string>;
|
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
* is load-bearing: the standalone wraps fd 3 in a `net.Socket`, and a Socket
|
|
26
26
|
* over a NAMED FIFO reads the request but never fires EOF on macOS, so the
|
|
27
27
|
* older FIFO wiring hung the read loop for the full timeout. Bounded to
|
|
28
|
-
* `SYNC_SERVE_TIMEOUT_MS`
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* `SYNC_SERVE_TIMEOUT_MS` (30s — sized for a cold Node boot of the
|
|
29
|
+
* standalone on a loaded box, see the constant) so a broken standalone
|
|
30
|
+
* fails before the server's 60s deadline. POSIX only; Windows fails loud
|
|
31
|
+
* pointing at the async path.
|
|
31
32
|
*
|
|
32
33
|
* State root (MIG-1): the standalone selects its state root from `SECRETS_HOME`.
|
|
33
34
|
* agents-cli points it at the user agents dir (`~/.agents`) by default so the
|
|
@@ -59,20 +60,25 @@ const MAX_PROTOCOL_BYTES = 8 * 1024 * 1024;
|
|
|
59
60
|
/** Just over the server's own 60s deadline, so the server times out first. */
|
|
60
61
|
const SERVE_TIMEOUT_MS = 65_000;
|
|
61
62
|
/**
|
|
62
|
-
* The synchronous path
|
|
63
|
-
* the account-catalog rows, and
|
|
64
|
-
* `agents run`
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
63
|
+
* The synchronous path serves the surfaces that resolve secrets before they can
|
|
64
|
+
* continue — `agents view`, the account-catalog rows, and the account listing and
|
|
65
|
+
* setup-token read on the `agents run` launch path. Those must never hang for the
|
|
66
|
+
* standalone's own 60s deadline (the exact hang PHNX-3989 hit when the child ran
|
|
67
|
+
* under Bun), so the sync serve carries a hard bound below it instead of the
|
|
68
|
+
* async path's 65s. The bound covers a COLD PROCESS, not just the operation: every
|
|
69
|
+
* request spawns `secrets __serve` afresh, so it pays a Node boot plus the
|
|
70
|
+
* standalone's module load before the tens-of-milliseconds op runs: ~0.3s on a
|
|
71
|
+
* quiet box, 0.4–2.6s and occasionally more on a desktop at load average ~100
|
|
72
|
+
* (measured 2026-09-12), where the previous 3s bound turned load into a failed
|
|
73
|
+
* launch (`secrets request failed: spawnSync sh ETIMEDOUT` from `agents run`).
|
|
74
|
+
* 30s absorbs that boot under load and still fails a broken or wedged standalone
|
|
75
|
+
* well before its 60s self-deadline. It is NOT a fallback to the embedded engine
|
|
76
|
+
* (DIST-1); the standalone stays the only implementation. Exported so the
|
|
77
|
+
* real-standalone test can assert a round-trip beats the bound.
|
|
74
78
|
*/
|
|
75
|
-
const SYNC_SERVE_TIMEOUT_MS =
|
|
79
|
+
export const SYNC_SERVE_TIMEOUT_MS = 30_000;
|
|
80
|
+
/** Test seam: a hang test plants a never-answering standalone and must not wait 30s. */
|
|
81
|
+
let syncServeTimeoutMs = SYNC_SERVE_TIMEOUT_MS;
|
|
76
82
|
/** Serialize `Map`s the way the server's `decodeWire` expects to receive them. */
|
|
77
83
|
export function encodeWire(value) {
|
|
78
84
|
if (value instanceof Map) {
|
|
@@ -413,13 +419,25 @@ function serveOnceSync(op, args, context) {
|
|
|
413
419
|
input: request,
|
|
414
420
|
stdio: ['pipe', 'pipe', 'inherit'],
|
|
415
421
|
env: buildServeEnv(),
|
|
416
|
-
timeout:
|
|
422
|
+
timeout: syncServeTimeoutMs,
|
|
417
423
|
maxBuffer: MAX_PROTOCOL_BYTES + 4096,
|
|
418
424
|
});
|
|
419
425
|
if (result.error) {
|
|
420
426
|
const err = result.error;
|
|
421
|
-
|
|
422
|
-
|
|
427
|
+
if (err.code === 'ETIMEDOUT') {
|
|
428
|
+
throw new SecretsClientError('TIMEOUT', `the standalone \`secrets\` CLI did not answer within ${Math.round(syncServeTimeoutMs / 1000)}s ` +
|
|
429
|
+
`(${[command, ...prefix, '__serve'].join(' ')}). The machine may be too loaded to boot it in time, or the install is broken: ` +
|
|
430
|
+
'check with `secrets --version`.');
|
|
431
|
+
}
|
|
432
|
+
// A standalone that exits before draining fd 3 leaves `spawnSync`'s stdin
|
|
433
|
+
// write with EPIPE, yet its exit status and everything it wrote to fd 4 are
|
|
434
|
+
// still returned. Like the async path (`input.on('error', () => {})`), the
|
|
435
|
+
// outcome is whatever reached fd 4 — an empty or non-JSON answer is the
|
|
436
|
+
// diagnostic, not the errno. Seen on the GitHub-hosted runner, where a
|
|
437
|
+
// planted `exit 0` standalone exits faster than the request is written.
|
|
438
|
+
if (err.code !== 'EPIPE') {
|
|
439
|
+
throw new SecretsClientError('SPAWN_FAILED', `secrets request failed: ${err.message}`);
|
|
440
|
+
}
|
|
423
441
|
}
|
|
424
442
|
const raw = result.stdout ?? Buffer.alloc(0);
|
|
425
443
|
if (raw.length > MAX_PROTOCOL_BYTES) {
|
|
@@ -483,6 +501,11 @@ export function _resetSecretsClientForTest() {
|
|
|
483
501
|
handshakeReady = false;
|
|
484
502
|
handshakePromise = null;
|
|
485
503
|
requestCounter = 0;
|
|
504
|
+
syncServeTimeoutMs = SYNC_SERVE_TIMEOUT_MS;
|
|
505
|
+
}
|
|
506
|
+
/** Shorten the sync bound for a test that plants a hanging standalone; reset restores it. */
|
|
507
|
+
export function _setSyncServeTimeoutForTest(ms) {
|
|
508
|
+
syncServeTimeoutMs = ms;
|
|
486
509
|
}
|
|
487
510
|
// --- typed wrappers -------------------------------------------------------
|
|
488
511
|
//
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// menubar-helper check — keeps the installed AGI Menu on the newest published
|
|
2
|
+
// build. Discovery + trigger for the helper's auto-update: the daemon runs this
|
|
3
|
+
// on the periodic cadence; the same call runs right after `agents upgrade`.
|
|
4
|
+
// Detect-only under dryRun (what `agents doctor` shows); the repair is the
|
|
5
|
+
// verified download + atomic swap + restart in `updateMenubarHelperIfNewer`.
|
|
6
|
+
import { resultOf } from '../types.js';
|
|
7
|
+
export const menubarHelperCheck = {
|
|
8
|
+
id: 'menubar-helper',
|
|
9
|
+
title: 'AGI Menu helper is the newest published build',
|
|
10
|
+
platforms: ['darwin'],
|
|
11
|
+
cadence: 'periodic',
|
|
12
|
+
async run(ctx) {
|
|
13
|
+
const { updateMenubarHelperIfNewer } = await import('../../menubar/install-menubar.js');
|
|
14
|
+
const r = await updateMenubarHelperIfNewer({ dryRun: ctx.dryRun });
|
|
15
|
+
switch (r.outcome) {
|
|
16
|
+
case 'updated': return resultOf([r.detail], []);
|
|
17
|
+
case 'failed': return resultOf([], [`AGI Menu ${r.installed ?? '?'} → ${r.available}: ${r.detail}`]);
|
|
18
|
+
default: return resultOf([], []);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -11,6 +11,7 @@ import { shimsCheck } from './checks/shims.js';
|
|
|
11
11
|
import { shadowingCheck } from './checks/shadowing.js';
|
|
12
12
|
import { pathCheck } from './checks/path.js';
|
|
13
13
|
import { installStagingCheck } from './checks/install-staging.js';
|
|
14
|
+
import { menubarHelperCheck } from './checks/menubar-helper.js';
|
|
14
15
|
// Order matters: cheap structural fixes (shims, shadow adoption, PATH, generated
|
|
15
16
|
// hook wrappers) before the heavier resource reconciliation, so a freshly-
|
|
16
17
|
// repaired shim is in place first.
|
|
@@ -24,6 +25,8 @@ export const HEAL_CHECKS = [
|
|
|
24
25
|
hookManifestCheck,
|
|
25
26
|
resourcesCheck,
|
|
26
27
|
installStagingCheck,
|
|
28
|
+
// Last and network-touching: the menu-bar helper's auto-update (macOS only).
|
|
29
|
+
menubarHelperCheck,
|
|
27
30
|
];
|
|
28
31
|
/** Run the selected checks, isolating per-check failures. */
|
|
29
32
|
export async function runSelfHeal(opts = {}) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type HealCheckId = 'resources' | 'hook-runtime' | 'hook-manifest' | 'shims' | 'shadowing' | 'path' | 'install-staging';
|
|
1
|
+
export type HealCheckId = 'resources' | 'hook-runtime' | 'hook-manifest' | 'shims' | 'shadowing' | 'path' | 'install-staging' | 'menubar-helper';
|
|
2
2
|
/** When the daemon schedules a check. */
|
|
3
3
|
export type HealCadence = 'startup' | 'frequent' | 'periodic';
|
|
4
4
|
export interface HealCtx {
|
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.103",
|
|
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",
|