@phnx-labs/agents-cli 1.22.23 → 1.22.24
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 +193 -0
- package/README.md +5 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.js +5 -0
- package/dist/commands/exec.js +80 -28
- package/dist/commands/feed.d.ts +18 -0
- package/dist/commands/feed.js +44 -1
- package/dist/commands/focus.d.ts +34 -3
- package/dist/commands/focus.js +300 -56
- package/dist/commands/go.d.ts +14 -1
- package/dist/commands/go.js +49 -5
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +6 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +54 -17
- package/dist/commands/sessions-browser.d.ts +35 -0
- package/dist/commands/sessions-browser.js +140 -14
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +26 -11
- package/dist/commands/sessions.d.ts +10 -0
- package/dist/commands/sessions.js +34 -59
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/view.js +2 -0
- package/dist/index.js +2 -1
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/exec.d.ts +12 -0
- package/dist/lib/exec.js +88 -9
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +88 -38
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources.js +8 -5
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +22 -17
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/bundles.js +157 -65
- package/dist/lib/secrets/filestore.d.ts +5 -3
- package/dist/lib/secrets/filestore.js +12 -8
- package/dist/lib/secrets/index.js +34 -8
- package/dist/lib/secrets/reaper.d.ts +10 -0
- package/dist/lib/secrets/reaper.js +50 -15
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- package/dist/lib/session/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -0
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +24 -5
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* issued from, so the run appears in that project's session listing like any
|
|
12
12
|
* local run. The `[host/<name>]` label mirrors the cloud path's
|
|
13
13
|
* `[cloud/<status>]` convention.
|
|
14
|
+
*
|
|
15
|
+
* `machine` is the dispatch host, not this box. Both the transcript and the
|
|
16
|
+
* version-isolated harness home live there; omitting it makes the SQLite upsert
|
|
17
|
+
* infer this box from the empty file path and sends recovery to the wrong home.
|
|
14
18
|
*/
|
|
15
19
|
import * as fs from 'fs';
|
|
16
20
|
import { upsertSession } from '../session/db.js';
|
|
@@ -18,6 +22,7 @@ import { isSessionTrackedAgent } from '../session/types.js';
|
|
|
18
22
|
import { localLogPath, updateTask } from './tasks.js';
|
|
19
23
|
import { parseSessionIdMarker } from './session-marker.js';
|
|
20
24
|
import { deriveShortId } from '../session/short-id.js';
|
|
25
|
+
import { normalizeHost } from '../machine-id.js';
|
|
21
26
|
/**
|
|
22
27
|
* Build the SessionMeta for a host-dispatched run. Returns null when the run has
|
|
23
28
|
* no captured session id (nothing stable to key/resume on) or its agent isn't a
|
|
@@ -38,6 +43,7 @@ export function hostSessionMeta(task, ctx) {
|
|
|
38
43
|
// Remote transcript — no local file. Empty file_path is the sentinel the DB
|
|
39
44
|
// stale-filter treats as "always live" (see module doc).
|
|
40
45
|
filePath: '',
|
|
46
|
+
machine: normalizeHost(task.host),
|
|
41
47
|
topic: ctx.prompt.split('\n')[0]?.slice(0, 120) || undefined,
|
|
42
48
|
// The run's `--name` seeds the label (resolves `agents sessions <name>` and
|
|
43
49
|
// `agents hosts logs <name>`); an unnamed host run falls back to the
|
|
@@ -109,6 +115,7 @@ export function registerInteractiveHostSession(ctx) {
|
|
|
109
115
|
timestamp: ctx.createdAt ?? new Date().toISOString(),
|
|
110
116
|
cwd: ctx.cwd,
|
|
111
117
|
filePath: '',
|
|
118
|
+
machine: normalizeHost(ctx.host),
|
|
112
119
|
label: ctx.name || `[host/${ctx.host}]`,
|
|
113
120
|
}, '');
|
|
114
121
|
}
|
package/dist/lib/manifest.d.ts
CHANGED
|
@@ -3,8 +3,18 @@ import type { Manifest } from './types.js';
|
|
|
3
3
|
export declare const MANIFEST_FILENAME = "agents.yaml";
|
|
4
4
|
/** Parse a YAML string into a typed Manifest object. */
|
|
5
5
|
export declare function parseManifest(content: string): Manifest;
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Serialize a Manifest to YAML WITHOUT destroying hand-written comments.
|
|
8
|
+
*
|
|
9
|
+
* Plain `yaml.stringify(manifest)` drops every comment, so `agents mcp add`
|
|
10
|
+
* (and every other writeManifest caller) used to clobber annotations in
|
|
11
|
+
* agents.yaml. Matching `serializeCentral` in state.ts: when existing file
|
|
12
|
+
* text is provided, parse it into a `yaml.Document` (comments + key order
|
|
13
|
+
* preserved) and edit only keys that actually changed. Untouched keys and
|
|
14
|
+
* their comments stay byte-stable. Falls back to plain stringify when there
|
|
15
|
+
* is no existing document yet.
|
|
16
|
+
*/
|
|
17
|
+
export declare function serializeManifest(manifest: Manifest, existingContent?: string | null): string;
|
|
8
18
|
/** Read and parse agents.yaml from a directory. Returns null if the file does not exist. */
|
|
9
19
|
export declare function readManifest(repoPath: string): Manifest | null;
|
|
10
20
|
/** Write a Manifest object to agents.yaml in the given directory. */
|
package/dist/lib/manifest.js
CHANGED
|
@@ -16,9 +16,48 @@ const manifestLockDepth = new Map();
|
|
|
16
16
|
export function parseManifest(content) {
|
|
17
17
|
return yaml.parse(content);
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Serialize a Manifest to YAML WITHOUT destroying hand-written comments.
|
|
21
|
+
*
|
|
22
|
+
* Plain `yaml.stringify(manifest)` drops every comment, so `agents mcp add`
|
|
23
|
+
* (and every other writeManifest caller) used to clobber annotations in
|
|
24
|
+
* agents.yaml. Matching `serializeCentral` in state.ts: when existing file
|
|
25
|
+
* text is provided, parse it into a `yaml.Document` (comments + key order
|
|
26
|
+
* preserved) and edit only keys that actually changed. Untouched keys and
|
|
27
|
+
* their comments stay byte-stable. Falls back to plain stringify when there
|
|
28
|
+
* is no existing document yet.
|
|
29
|
+
*/
|
|
30
|
+
export function serializeManifest(manifest, existingContent) {
|
|
31
|
+
const entries = Object.entries(manifest).filter(([, v]) => v !== undefined);
|
|
32
|
+
const isEmpty = entries.length === 0;
|
|
33
|
+
if (existingContent == null || existingContent.trim() === '') {
|
|
34
|
+
return isEmpty ? '' : yaml.stringify(manifest, { indent: 2 });
|
|
35
|
+
}
|
|
36
|
+
const doc = yaml.parseDocument(existingContent);
|
|
37
|
+
const current = doc.toJSON() ?? {};
|
|
38
|
+
let changed = false;
|
|
39
|
+
for (const [k, v] of entries) {
|
|
40
|
+
if (JSON.stringify(current[k]) !== JSON.stringify(v)) {
|
|
41
|
+
doc.set(k, v);
|
|
42
|
+
changed = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Full-document write: callers do read-modify-write, so keys absent from the
|
|
46
|
+
// new manifest are intentional removals (e.g. clearing beta).
|
|
47
|
+
for (const k of Object.keys(current)) {
|
|
48
|
+
const next = manifest[k];
|
|
49
|
+
if (!(k in manifest) || next === undefined) {
|
|
50
|
+
doc.delete(k);
|
|
51
|
+
changed = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Nothing changed → keep the file byte-identical (comments intact).
|
|
55
|
+
if (!changed)
|
|
56
|
+
return existingContent;
|
|
57
|
+
// Force BLOCK style: an existing flow root (e.g. legacy `{}`) would otherwise
|
|
58
|
+
// make edited nodes render flow. collectionStyle pins the whole doc block
|
|
59
|
+
// while parseDocument still preserves comments + key ordering.
|
|
60
|
+
return isEmpty ? '' : doc.toString({ collectionStyle: 'block' });
|
|
22
61
|
}
|
|
23
62
|
/** Read and parse agents.yaml from a directory. Returns null if the file does not exist. */
|
|
24
63
|
export function readManifest(repoPath) {
|
|
@@ -55,8 +94,24 @@ function withManifestLock(filePath, fn) {
|
|
|
55
94
|
/** Write a Manifest object to agents.yaml in the given directory. */
|
|
56
95
|
export function writeManifest(repoPath, manifest) {
|
|
57
96
|
const manifestPath = safeJoin(repoPath, MANIFEST_FILENAME);
|
|
58
|
-
|
|
59
|
-
|
|
97
|
+
withManifestLock(manifestPath, () => {
|
|
98
|
+
let existing = null;
|
|
99
|
+
try {
|
|
100
|
+
existing = fs.readFileSync(manifestPath, 'utf-8');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* first write — no file yet (or empty lock target) */
|
|
104
|
+
}
|
|
105
|
+
// ensureLockTarget may have created an empty file for the lock path.
|
|
106
|
+
if (existing !== null && existing.trim() === '')
|
|
107
|
+
existing = null;
|
|
108
|
+
const content = serializeManifest(manifest, existing);
|
|
109
|
+
// Skip the atomic rewrite when nothing changed so comments stay byte-stable
|
|
110
|
+
// and concurrent readers never see a no-op churn.
|
|
111
|
+
if (existing !== null && content === existing)
|
|
112
|
+
return;
|
|
113
|
+
atomicWriteFileSync(manifestPath, content);
|
|
114
|
+
});
|
|
60
115
|
}
|
|
61
116
|
/** Create a Manifest with sensible defaults for a fresh agents repo. */
|
|
62
117
|
export function createDefaultManifest() {
|
|
Binary file
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
<key>CFBundleIdentifier</key>
|
|
8
8
|
<string>com.phnx-labs.agents-menubar</string>
|
|
9
9
|
<key>CFBundleName</key>
|
|
10
|
-
<string>
|
|
10
|
+
<string>AGI Menu</string>
|
|
11
|
+
<key>CFBundleDisplayName</key>
|
|
12
|
+
<string>AGI Menu</string>
|
|
13
|
+
<key>CFBundleIconFile</key>
|
|
14
|
+
<string>AppIcon</string>
|
|
11
15
|
<key>CFBundlePackageType</key>
|
|
12
16
|
<string>APPL</string>
|
|
13
17
|
<key>CFBundleShortVersionString</key>
|
|
Binary file
|
|
@@ -3,9 +3,22 @@
|
|
|
3
3
|
<plist version="1.0">
|
|
4
4
|
<dict>
|
|
5
5
|
<key>files</key>
|
|
6
|
-
<dict
|
|
6
|
+
<dict>
|
|
7
|
+
<key>Resources/AppIcon.icns</key>
|
|
8
|
+
<data>
|
|
9
|
+
jOjZVimcFRHoP2VPzgn8uM8mjkA=
|
|
10
|
+
</data>
|
|
11
|
+
</dict>
|
|
7
12
|
<key>files2</key>
|
|
8
|
-
<dict
|
|
13
|
+
<dict>
|
|
14
|
+
<key>Resources/AppIcon.icns</key>
|
|
15
|
+
<dict>
|
|
16
|
+
<key>hash2</key>
|
|
17
|
+
<data>
|
|
18
|
+
GFvSLeNYJ3ASxW3OzcuiB4aID0gZUBkpozmWZkbTFNw=
|
|
19
|
+
</data>
|
|
20
|
+
</dict>
|
|
21
|
+
</dict>
|
|
9
22
|
<key>rules</key>
|
|
10
23
|
<dict>
|
|
11
24
|
<key>^Resources/</key>
|
|
@@ -111,6 +111,54 @@ export declare function menubarPlistNeedsRepoint(opts: {
|
|
|
111
111
|
* upgrade migration won't re-enable it.
|
|
112
112
|
*/
|
|
113
113
|
export declare function disableMenubarService(): void;
|
|
114
|
+
/**
|
|
115
|
+
* Which install is allowed to (re)install the shared helper.
|
|
116
|
+
*
|
|
117
|
+
* The helper lives at ONE path in Application Support, but any number of
|
|
118
|
+
* agents-cli copies can be present on a box and every one of them runs the
|
|
119
|
+
* startup self-heal. The version stamp and the plist's baked `AGENTS_ENTRY` each
|
|
120
|
+
* record whichever copy acted last, so without an ownership rule every copy
|
|
121
|
+
* reads the others' marks as drift and recopies the bundle over them. Recopying
|
|
122
|
+
* replaces the executable under the live helper and kills it; launchd
|
|
123
|
+
* `KeepAlive` restarts it; the next copy repeats it. Measured on one box: a new
|
|
124
|
+
* pid every 5-15s, 578 launches in the helper's log, a status item that never
|
|
125
|
+
* stayed visible, and `agents menubar status` still reporting `running: yes`
|
|
126
|
+
* because a pid always existed (#2109).
|
|
127
|
+
*
|
|
128
|
+
* There is deliberately NO content comparison here. Comparing the shipped helper
|
|
129
|
+
* against the installed one cannot distinguish "real upgrade" from "another
|
|
130
|
+
* install's copy": the helper is rebuilt, re-signed and re-notarized on every
|
|
131
|
+
* release (`menubar/scripts/build.sh` via `release.sh`), so consecutive releases
|
|
132
|
+
* ship byte-different bundles from identical Swift source — 1.22.20/21/22 all
|
|
133
|
+
* have the same 2876288-byte executable and three different sha256s AND three
|
|
134
|
+
* different CDHashes. Any digest gate therefore reports "changed" for exactly
|
|
135
|
+
* the skew case it was meant to exempt.
|
|
136
|
+
*
|
|
137
|
+
* So ownership decides instead: the plist's `AGENTS_ENTRY` names the owner, and
|
|
138
|
+
* only the owner may reinstall. A non-owner takes over only once the recorded
|
|
139
|
+
* owner is gone from disk, which is what makes the rule converge — a dead
|
|
140
|
+
* install cannot hold the helper hostage, and a live one cannot be fought over.
|
|
141
|
+
* A same-install upgrade keeps its entry path, so `npm update` still installs
|
|
142
|
+
* the new helper normally. Pure so the truth table is unit-testable.
|
|
143
|
+
*/
|
|
144
|
+
export declare function mayInstallMenubarHelper(opts: {
|
|
145
|
+
/** `AGENTS_ENTRY` baked into the installed plist — the recorded owner. */
|
|
146
|
+
plistEntry: string | null;
|
|
147
|
+
/** `resolveCliEntry()` for the install now running `agents`. */
|
|
148
|
+
activeEntry: string | null;
|
|
149
|
+
/** Whether `plistEntry` still exists on disk. */
|
|
150
|
+
ownerEntryExists: boolean;
|
|
151
|
+
/** The App Support helper executable is absent — a repair, not a contest. */
|
|
152
|
+
helperExecMissing: boolean;
|
|
153
|
+
/** Installed copy is ad-hoc while the shipped source is Developer ID. */
|
|
154
|
+
needsDevIdHeal: boolean;
|
|
155
|
+
/** ms since the last self-heal reinstall, or null if none is recorded. */
|
|
156
|
+
msSinceLastHeal: number | null;
|
|
157
|
+
/** How long a non-owner waits before it may take over. */
|
|
158
|
+
cooldownMs: number;
|
|
159
|
+
/** This install's OWN shipped bundle is Developer-ID signed (not ad-hoc/dev). */
|
|
160
|
+
sourceIsDeveloperId: boolean;
|
|
161
|
+
}): boolean;
|
|
114
162
|
/**
|
|
115
163
|
* Startup self-heal, run on every darwin CLI invocation (see src/index.ts).
|
|
116
164
|
* No-ops cheaply (a couple of existsSync + a tiny file read) unless work is
|
|
@@ -121,8 +169,10 @@ export declare function disableMenubarService(): void;
|
|
|
121
169
|
*
|
|
122
170
|
* Without the staleness re-enable, `npm update` refreshed the CLI but left the
|
|
123
171
|
* menu bar running the previous release's helper binary on a possibly-stale
|
|
124
|
-
* plist.
|
|
125
|
-
*
|
|
172
|
+
* plist. Everything past the ownership gate is unchanged; the gate is what stops
|
|
173
|
+
* coexisting installs reinstalling over each other forever (#2109). No-ops if:
|
|
174
|
+
* not darwin, the user opted out, or no helper bundle ships. Best-effort — never
|
|
175
|
+
* throws into startup.
|
|
126
176
|
*/
|
|
127
177
|
export declare function installMenubarLaunchAgentOnUpgrade(): void;
|
|
128
178
|
/** One step of `agents menubar setup`, and how it came out. */
|
|
@@ -468,6 +468,115 @@ export function disableMenubarService() {
|
|
|
468
468
|
}
|
|
469
469
|
catch { /* best effort */ }
|
|
470
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* Which install is allowed to (re)install the shared helper.
|
|
473
|
+
*
|
|
474
|
+
* The helper lives at ONE path in Application Support, but any number of
|
|
475
|
+
* agents-cli copies can be present on a box and every one of them runs the
|
|
476
|
+
* startup self-heal. The version stamp and the plist's baked `AGENTS_ENTRY` each
|
|
477
|
+
* record whichever copy acted last, so without an ownership rule every copy
|
|
478
|
+
* reads the others' marks as drift and recopies the bundle over them. Recopying
|
|
479
|
+
* replaces the executable under the live helper and kills it; launchd
|
|
480
|
+
* `KeepAlive` restarts it; the next copy repeats it. Measured on one box: a new
|
|
481
|
+
* pid every 5-15s, 578 launches in the helper's log, a status item that never
|
|
482
|
+
* stayed visible, and `agents menubar status` still reporting `running: yes`
|
|
483
|
+
* because a pid always existed (#2109).
|
|
484
|
+
*
|
|
485
|
+
* There is deliberately NO content comparison here. Comparing the shipped helper
|
|
486
|
+
* against the installed one cannot distinguish "real upgrade" from "another
|
|
487
|
+
* install's copy": the helper is rebuilt, re-signed and re-notarized on every
|
|
488
|
+
* release (`menubar/scripts/build.sh` via `release.sh`), so consecutive releases
|
|
489
|
+
* ship byte-different bundles from identical Swift source — 1.22.20/21/22 all
|
|
490
|
+
* have the same 2876288-byte executable and three different sha256s AND three
|
|
491
|
+
* different CDHashes. Any digest gate therefore reports "changed" for exactly
|
|
492
|
+
* the skew case it was meant to exempt.
|
|
493
|
+
*
|
|
494
|
+
* So ownership decides instead: the plist's `AGENTS_ENTRY` names the owner, and
|
|
495
|
+
* only the owner may reinstall. A non-owner takes over only once the recorded
|
|
496
|
+
* owner is gone from disk, which is what makes the rule converge — a dead
|
|
497
|
+
* install cannot hold the helper hostage, and a live one cannot be fought over.
|
|
498
|
+
* A same-install upgrade keeps its entry path, so `npm update` still installs
|
|
499
|
+
* the new helper normally. Pure so the truth table is unit-testable.
|
|
500
|
+
*/
|
|
501
|
+
export function mayInstallMenubarHelper(opts) {
|
|
502
|
+
// Repairs are never gated: a missing binary or a broken signing identity leaves
|
|
503
|
+
// the menu bar dead or re-prompting for Accessibility, and no other install can
|
|
504
|
+
// be "fighting" for a bundle that isn't there. Blocking these behind ownership
|
|
505
|
+
// is what turned the first version of this gate into a silent stuck state.
|
|
506
|
+
if (opts.helperExecMissing || opts.needsDevIdHeal)
|
|
507
|
+
return true;
|
|
508
|
+
// Can't resolve which install we are (a dev/tsx run) — never churn the plist.
|
|
509
|
+
if (!opts.activeEntry)
|
|
510
|
+
return false;
|
|
511
|
+
// No owner recorded yet (fresh or pre-`AGENTS_ENTRY` plist) — adopt it.
|
|
512
|
+
if (!opts.plistEntry)
|
|
513
|
+
return true;
|
|
514
|
+
if (opts.plistEntry === opts.activeEntry)
|
|
515
|
+
return true; // we are the owner
|
|
516
|
+
if (!opts.ownerEntryExists)
|
|
517
|
+
return true; // the recorded owner is gone
|
|
518
|
+
// A foreign install while the owner still exists. Refusing outright bounds the
|
|
519
|
+
// loop but strands the user when the recorded owner is a stale copy that simply
|
|
520
|
+
// still sits on disk (an old nvm node dir) while their daily driver upgrades:
|
|
521
|
+
// that install would never heal again. So it may take over, but only once per
|
|
522
|
+
// cooldown — which turns an every-invocation storm into at most one restart per
|
|
523
|
+
// cooldown while keeping every install able to make progress.
|
|
524
|
+
//
|
|
525
|
+
// Except an ad-hoc/dev-signed copy, which never seizes a healthy helper on a
|
|
526
|
+
// timer. `scripts/install.sh` deliberately puts a dev build beside the npm
|
|
527
|
+
// global, and its bundle cannot be notarized; letting it win the timed takeover
|
|
528
|
+
// would recopy an ad-hoc bundle over a good Developer-ID one, and Gatekeeper
|
|
529
|
+
// then rejects the result as "damaged" and AppKit crashes at launch (RUSH-2134)
|
|
530
|
+
// — trading a cosmetic loop for a broken menu bar. It can still take over when
|
|
531
|
+
// the owner is genuinely gone (above), which is the case that must not deadlock.
|
|
532
|
+
if (!opts.sourceIsDeveloperId)
|
|
533
|
+
return false;
|
|
534
|
+
return opts.msSinceLastHeal === null || opts.msSinceLastHeal >= opts.cooldownMs;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* How long a non-owner install waits before it may take the helper over. Long
|
|
538
|
+
* enough that a multi-install box restarts the helper at most once an hour
|
|
539
|
+
* instead of every few seconds; short enough that a user who switched installs
|
|
540
|
+
* gets their upgrade without hunting for `agents menubar setup`.
|
|
541
|
+
*/
|
|
542
|
+
const MENUBAR_TAKEOVER_COOLDOWN_MS = 60 * 60 * 1000;
|
|
543
|
+
/** Timestamp of the last self-heal reinstall, next to the version stamp. */
|
|
544
|
+
function lastHealMarkerPath() {
|
|
545
|
+
return path.join(installDir(), '.menubar-last-heal');
|
|
546
|
+
}
|
|
547
|
+
function msSinceLastMenubarHeal() {
|
|
548
|
+
try {
|
|
549
|
+
const t = Number(fs.readFileSync(lastHealMarkerPath(), 'utf-8').trim());
|
|
550
|
+
if (!Number.isFinite(t))
|
|
551
|
+
return null;
|
|
552
|
+
return Math.max(0, Date.now() - t);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function stampMenubarHeal() {
|
|
559
|
+
try {
|
|
560
|
+
fs.mkdirSync(installDir(), { recursive: true });
|
|
561
|
+
fs.writeFileSync(lastHealMarkerPath(), String(Date.now()));
|
|
562
|
+
}
|
|
563
|
+
catch { /* best effort */ }
|
|
564
|
+
}
|
|
565
|
+
/** Whether this install may (re)install the helper (see `mayInstallMenubarHelper`). */
|
|
566
|
+
function mayHealMenubar(needsDevIdHeal) {
|
|
567
|
+
const plistEntry = readPlistEnvValue('AGENTS_ENTRY');
|
|
568
|
+
const src = sourceAppPath();
|
|
569
|
+
return mayInstallMenubarHelper({
|
|
570
|
+
plistEntry,
|
|
571
|
+
activeEntry: resolveCliEntry(),
|
|
572
|
+
ownerEntryExists: Boolean(plistEntry) && fs.existsSync(plistEntry),
|
|
573
|
+
helperExecMissing: !fs.existsSync(installedExecutablePath()),
|
|
574
|
+
needsDevIdHeal,
|
|
575
|
+
msSinceLastHeal: msSinceLastMenubarHeal(),
|
|
576
|
+
cooldownMs: MENUBAR_TAKEOVER_COOLDOWN_MS,
|
|
577
|
+
sourceIsDeveloperId: Boolean(src) && hasDeveloperIdSignature(src),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
471
580
|
/**
|
|
472
581
|
* Startup self-heal, run on every darwin CLI invocation (see src/index.ts).
|
|
473
582
|
* No-ops cheaply (a couple of existsSync + a tiny file read) unless work is
|
|
@@ -478,8 +587,10 @@ export function disableMenubarService() {
|
|
|
478
587
|
*
|
|
479
588
|
* Without the staleness re-enable, `npm update` refreshed the CLI but left the
|
|
480
589
|
* menu bar running the previous release's helper binary on a possibly-stale
|
|
481
|
-
* plist.
|
|
482
|
-
*
|
|
590
|
+
* plist. Everything past the ownership gate is unchanged; the gate is what stops
|
|
591
|
+
* coexisting installs reinstalling over each other forever (#2109). No-ops if:
|
|
592
|
+
* not darwin, the user opted out, or no helper bundle ships. Best-effort — never
|
|
593
|
+
* throws into startup.
|
|
483
594
|
*/
|
|
484
595
|
export function installMenubarLaunchAgentOnUpgrade() {
|
|
485
596
|
try {
|
|
@@ -495,12 +606,23 @@ export function installMenubarLaunchAgentOnUpgrade() {
|
|
|
495
606
|
}
|
|
496
607
|
// Re-enable (recopy helper + rewrite plist) when the version drifted OR the
|
|
497
608
|
// plist's baked interpreter/entry no longer point at the install now running
|
|
498
|
-
// `agents` — the
|
|
609
|
+
// `agents` — e.g. the owner moved between node interpreters — OR the
|
|
499
610
|
// installed copy is still ad-hoc while the shipped source is Developer ID
|
|
500
611
|
// (older heal path; Accessibility re-prompts until the identity is restored).
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
612
|
+
const needsDevIdHeal = installedNeedsDevIdHeal();
|
|
613
|
+
if (!(menubarSetupStale() || menubarSetupNeedsRepoint() || needsDevIdHeal))
|
|
614
|
+
return;
|
|
615
|
+
// ...but a copy that does not own the helper only gets to act on that drift
|
|
616
|
+
// once per cooldown. Without the gate every coexisting install recopies the
|
|
617
|
+
// bundle on every invocation, killing the live helper on a loop (#2109).
|
|
618
|
+
if (!mayHealMenubar(needsDevIdHeal))
|
|
619
|
+
return;
|
|
620
|
+
// Stamp only a heal that actually happened. `enableMenubarService` returns
|
|
621
|
+
// false without installing when the bundle fails the Gatekeeper check, and
|
|
622
|
+
// stamping first would spend the shared cooldown on a no-op — locking every
|
|
623
|
+
// non-owner out for another hour while nothing had been fixed.
|
|
624
|
+
if (enableMenubarService({ clearOptOut: false }))
|
|
625
|
+
stampMenubarHeal();
|
|
504
626
|
}
|
|
505
627
|
catch {
|
|
506
628
|
/* never block startup on the menu bar */
|
package/dist/lib/refresh.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface RefreshOptions {
|
|
|
17
17
|
skipPrompts?: boolean;
|
|
18
18
|
/** Skip CLI version install/upgrade from agents.yaml. */
|
|
19
19
|
skipClis?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Suppress human progress lines on stdout. Required for machine consumers
|
|
22
|
+
* (`agents sync --json` / fleet fan-out) so stdout stays a single JSON object.
|
|
23
|
+
*/
|
|
24
|
+
quiet?: boolean;
|
|
20
25
|
}
|
|
21
26
|
/**
|
|
22
27
|
* Re-materialize local state from declared configuration: install CLI versions,
|
package/dist/lib/refresh.js
CHANGED
|
@@ -29,7 +29,7 @@ import { isPromptCancelled } from '../commands/utils.js';
|
|
|
29
29
|
* hook reads from a fixed path so it survives version upgrades. If the root
|
|
30
30
|
* file doesn't exist yet but an agent-scoped one does, hoist the first one found.
|
|
31
31
|
*/
|
|
32
|
-
function migratePromptcutsToRoot(agentsDir) {
|
|
32
|
+
function migratePromptcutsToRoot(agentsDir, quiet = false) {
|
|
33
33
|
const rootPath = path.join(agentsDir, 'promptcuts.yaml');
|
|
34
34
|
if (fs.existsSync(rootPath))
|
|
35
35
|
return;
|
|
@@ -39,7 +39,8 @@ function migratePromptcutsToRoot(agentsDir) {
|
|
|
39
39
|
if (fs.existsSync(legacyPath)) {
|
|
40
40
|
try {
|
|
41
41
|
fs.renameSync(legacyPath, rootPath);
|
|
42
|
-
|
|
42
|
+
if (!quiet)
|
|
43
|
+
console.log(chalk.gray(`Moved ${dir}/promptcuts.yaml → promptcuts.yaml (repo root)`));
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
catch {
|
|
@@ -56,16 +57,19 @@ function migratePromptcutsToRoot(agentsDir) {
|
|
|
56
57
|
* Idempotent — safe to run repeatedly. No network operations.
|
|
57
58
|
*/
|
|
58
59
|
export async function refresh(options = {}) {
|
|
59
|
-
const { agentFilter, skipPrompts = false, skipClis = false } = options;
|
|
60
|
+
const { agentFilter, skipPrompts = false, skipClis = false, quiet = false } = options;
|
|
60
61
|
const agentsDir = getUserAgentsDir();
|
|
61
|
-
|
|
62
|
+
// Gate every human progress line so --json / fleet fan-out can parse stdout.
|
|
63
|
+
const log = (...args) => { if (!quiet)
|
|
64
|
+
console.log(...args); };
|
|
65
|
+
migratePromptcutsToRoot(agentsDir, quiet);
|
|
62
66
|
const manifest = readManifest(agentsDir);
|
|
63
67
|
if (!manifest) {
|
|
64
|
-
|
|
68
|
+
log(chalk.gray(`No ${MANIFEST_FILENAME} found`));
|
|
65
69
|
}
|
|
66
70
|
// 1. Install/upgrade CLI versions from agents.yaml
|
|
67
71
|
if (!skipClis && manifest?.agents) {
|
|
68
|
-
|
|
72
|
+
log(chalk.bold('\nCLI Versions:\n'));
|
|
69
73
|
const cliAgents = Object.keys(manifest.agents);
|
|
70
74
|
for (const agentId of cliAgents) {
|
|
71
75
|
if (agentFilter && agentId !== agentFilter)
|
|
@@ -94,7 +98,7 @@ export async function refresh(options = {}) {
|
|
|
94
98
|
}
|
|
95
99
|
// 2. Register MCP servers
|
|
96
100
|
if (manifest?.mcp && Object.keys(manifest.mcp).length > 0) {
|
|
97
|
-
|
|
101
|
+
log(chalk.bold('\nMCP Servers:\n'));
|
|
98
102
|
for (const [name, config] of Object.entries(manifest.mcp)) {
|
|
99
103
|
if (!config.command || config.transport === 'http')
|
|
100
104
|
continue;
|
|
@@ -109,7 +113,7 @@ export async function refresh(options = {}) {
|
|
|
109
113
|
const label = result.version
|
|
110
114
|
? `${agentLabel(result.agentId)}@${result.version}`
|
|
111
115
|
: agentLabel(result.agentId);
|
|
112
|
-
|
|
116
|
+
log(` ${chalk.green('+')} ${name} -> ${label}`);
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
}
|
|
@@ -147,13 +151,13 @@ export async function refresh(options = {}) {
|
|
|
147
151
|
forceFullSync = true;
|
|
148
152
|
}
|
|
149
153
|
else if (!hasAnySynced) {
|
|
150
|
-
|
|
154
|
+
log(chalk.yellow(`\n${agentLabel(agentId)}@${defaultVer} has no synced resources.`));
|
|
151
155
|
const userSelection = await promptResourceSelection(agentId);
|
|
152
156
|
if (userSelection)
|
|
153
157
|
selection = userSelection;
|
|
154
158
|
}
|
|
155
159
|
else if (hasNewResources(newResources, agentId, defaultVer)) {
|
|
156
|
-
|
|
160
|
+
log(chalk.cyan(`\n${agentLabel(agentId)}@${defaultVer}:`));
|
|
157
161
|
const userSelection = await promptNewResourceSelection(agentId, newResources, defaultVer);
|
|
158
162
|
if (userSelection)
|
|
159
163
|
selection = userSelection;
|
|
@@ -184,13 +188,13 @@ export async function refresh(options = {}) {
|
|
|
184
188
|
const verNote = versionsToSync.length > 1
|
|
185
189
|
? chalk.gray(` (${versionsToSync.length} versions)`)
|
|
186
190
|
: '';
|
|
187
|
-
|
|
191
|
+
log(chalk.green(` Synced: ${[...kinds].join(', ')}`) + verNote);
|
|
188
192
|
}
|
|
189
193
|
}
|
|
190
194
|
}
|
|
191
195
|
catch (err) {
|
|
192
196
|
if (isPromptCancelled(err)) {
|
|
193
|
-
|
|
197
|
+
log(chalk.gray('Skipped resource selection'));
|
|
194
198
|
}
|
|
195
199
|
else {
|
|
196
200
|
throw err;
|
|
@@ -215,24 +219,24 @@ export async function refresh(options = {}) {
|
|
|
215
219
|
const result = registerHooksToSettings(agentId, home, hookManifest);
|
|
216
220
|
hookRegistered += result.registered.length;
|
|
217
221
|
for (const error of result.errors) {
|
|
218
|
-
|
|
222
|
+
log(chalk.yellow(` Hook warning: ${error}`));
|
|
219
223
|
}
|
|
220
224
|
}
|
|
221
225
|
}
|
|
222
226
|
if (hookRegistered > 0) {
|
|
223
|
-
|
|
227
|
+
log(chalk.green(`\nRegistered ${hookRegistered} hook lifecycle event(s)`));
|
|
224
228
|
}
|
|
225
229
|
}
|
|
226
230
|
// 5. Auto-add shims to PATH
|
|
227
231
|
if (!isShimsInPath()) {
|
|
228
232
|
const pathResult = addShimsToPath();
|
|
229
233
|
if (pathResult.success && !pathResult.alreadyPresent) {
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
log(chalk.green(`\nAdded shims to ${pathResult.location}`));
|
|
235
|
+
log(chalk.gray(pathResult.reloadHint));
|
|
232
236
|
}
|
|
233
237
|
else if (!pathResult.success) {
|
|
234
|
-
|
|
235
|
-
|
|
238
|
+
log(chalk.yellow('\nCould not auto-add shims to PATH:'));
|
|
239
|
+
log(chalk.gray(getPathSetupInstructions()));
|
|
236
240
|
}
|
|
237
241
|
}
|
|
238
242
|
// 6. Prompt for missing default versions
|
|
@@ -273,63 +277,63 @@ export async function refresh(options = {}) {
|
|
|
273
277
|
setGlobalDefault(agentId, version);
|
|
274
278
|
const symlinkResult = await switchConfigSymlink(agentId, version);
|
|
275
279
|
if (!symlinkResult.success) {
|
|
276
|
-
|
|
280
|
+
log(chalk.yellow(`Warning: ${symlinkResult.error}`));
|
|
277
281
|
}
|
|
278
282
|
else if (symlinkResult.backupPath) {
|
|
279
|
-
|
|
283
|
+
log(chalk.gray(`Backed up existing config to: ${symlinkResult.backupPath}`));
|
|
280
284
|
}
|
|
281
285
|
switchHomeFileSymlinks(agentId, version);
|
|
282
|
-
|
|
286
|
+
log(chalk.green(`Set ${agentLabel(agent.id)}@${version} as default`));
|
|
283
287
|
}
|
|
284
288
|
}
|
|
285
289
|
// 7. Install declared host-CLIs
|
|
286
290
|
try {
|
|
287
291
|
const { statuses, errors } = listCliStatus(process.cwd());
|
|
288
292
|
for (const err of errors) {
|
|
289
|
-
|
|
293
|
+
log(chalk.yellow(` CLI manifest parse error: ${err.file}: ${err.reason}`));
|
|
290
294
|
}
|
|
291
295
|
const missing = statuses.filter((s) => !s.installed);
|
|
292
296
|
if (missing.length > 0) {
|
|
293
|
-
|
|
297
|
+
log(chalk.bold('\nDeclared CLIs missing from this host:'));
|
|
294
298
|
for (const s of missing) {
|
|
295
299
|
const method = selectInstallMethod(s.manifest);
|
|
296
300
|
const action = method ? describeMethod(method) : chalk.red('no compatible install method');
|
|
297
|
-
|
|
301
|
+
log(` ${chalk.cyan(s.manifest.name.padEnd(20))} ${chalk.gray(action)}`);
|
|
298
302
|
}
|
|
299
|
-
|
|
303
|
+
log('');
|
|
300
304
|
if (!skipPrompts) {
|
|
301
305
|
const proceed = await confirm({ message: `Install ${missing.length} missing CLI(s) now?`, default: true });
|
|
302
306
|
if (proceed) {
|
|
303
307
|
for (const s of missing) {
|
|
304
|
-
|
|
308
|
+
log(chalk.bold(`\n→ ${s.manifest.name}`));
|
|
305
309
|
const result = installCli(s.manifest);
|
|
306
310
|
if (result.error) {
|
|
307
|
-
|
|
311
|
+
log(chalk.red(` ${result.error}`));
|
|
308
312
|
continue;
|
|
309
313
|
}
|
|
310
314
|
if (result.installed) {
|
|
311
|
-
|
|
315
|
+
log(chalk.green(` installed`));
|
|
312
316
|
if (s.manifest.postInstall) {
|
|
313
|
-
|
|
317
|
+
log(chalk.gray(s.manifest.postInstall.trim().split('\n').map((l) => ' ' + l).join('\n')));
|
|
314
318
|
}
|
|
315
319
|
}
|
|
316
320
|
else {
|
|
317
|
-
|
|
321
|
+
log(chalk.yellow(` install ran but \`${describeCheck(s.manifest.check)}\` still fails`));
|
|
318
322
|
}
|
|
319
323
|
}
|
|
320
324
|
}
|
|
321
325
|
else {
|
|
322
|
-
|
|
326
|
+
log(chalk.gray(`Skipped. Run 'agents cli install' later.`));
|
|
323
327
|
}
|
|
324
328
|
}
|
|
325
329
|
else {
|
|
326
|
-
|
|
330
|
+
log(chalk.gray(`Run 'agents cli install' to install them.`));
|
|
327
331
|
}
|
|
328
332
|
}
|
|
329
333
|
}
|
|
330
334
|
catch (err) {
|
|
331
335
|
if (!isPromptCancelled(err)) {
|
|
332
|
-
|
|
336
|
+
log(chalk.yellow(`CLI install skipped: ${err.message}`));
|
|
333
337
|
}
|
|
334
338
|
}
|
|
335
339
|
}
|