@phnx-labs/agents-cli 1.22.91 → 1.22.93
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 +21 -0
- package/README.md +5 -0
- package/dist/commands/exec.js +6 -1
- package/dist/lib/claude-account-token.d.ts +13 -0
- package/dist/lib/claude-account-token.js +29 -0
- package/dist/lib/daemon/attention-notify-service.d.ts +5 -2
- package/dist/lib/daemon/attention-notify-service.js +9 -2
- package/dist/lib/exec-account-home.d.ts +19 -0
- package/dist/lib/exec-account-home.js +35 -1
- package/dist/lib/exec.js +11 -0
- package/dist/lib/feed/answer.js +9 -0
- package/dist/lib/feed/attention.d.ts +18 -2
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +6 -2
- package/dist/lib/feed/feed.js +10 -4
- package/dist/lib/harness/adapter.d.ts +23 -0
- package/dist/lib/harness/adapter.js +29 -0
- package/dist/lib/harness/adapters/claude.js +5 -3
- package/dist/lib/harness/adapters/copilot.js +4 -2
- package/dist/lib/harness/adapters/grok.js +4 -2
- package/dist/lib/harness/adapters/kimi.js +4 -2
- package/dist/lib/harness/adapters/muse.js +4 -3
- package/dist/lib/harness/adapters/opencode.js +4 -2
- package/dist/lib/helper-download.js +1 -1
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/installations/shims.d.ts +2 -2
- package/dist/lib/installations/shims.js +16 -8
- package/dist/lib/menubar/download-menubar.js +4 -1
- package/dist/lib/menubar/install-menubar.d.ts +10 -9
- package/dist/lib/menubar/install-menubar.js +17 -15
- package/dist/lib/secrets-policy.d.ts +5 -2
- package/dist/lib/secrets-policy.js +18 -4
- package/dist/lib/session/active.d.ts +18 -11
- package/dist/lib/session/active.js +52 -44
- package/dist/lib/session/session-cache.d.ts +1 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/state.d.ts +26 -0
- package/dist/lib/session/state.js +43 -27
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
-
import { stripForeignConfigDir } from '../adapter.js';
|
|
2
|
+
import { stripForeignConfigDir, slotAwareConfigEnvBash } from '../adapter.js';
|
|
3
3
|
export const kimiAdapter = {
|
|
4
4
|
id: 'kimi',
|
|
5
5
|
// Kimi honors KIMI_CODE_HOME (relocates ~/.kimi-code, including config,
|
|
@@ -14,7 +14,9 @@ export const kimiAdapter = {
|
|
|
14
14
|
return `
|
|
15
15
|
# Kimi Code CLI honors KIMI_CODE_HOME to relocate ~/.kimi-code (config.toml,
|
|
16
16
|
# mcp.json, sessions, skills, hooks). Point it at the versioned home.
|
|
17
|
-
|
|
17
|
+
# An account-slot launch has already chosen the home (AGENTS_EXEC_HOME); the pin
|
|
18
|
+
# yields to it — see slotAwareConfigEnvBash.
|
|
19
|
+
${slotAwareConfigEnvBash([{ env: 'KIMI_CODE_HOME', rel: ctx.configDirName }], '$VERSION_DIR/home')}
|
|
18
20
|
`;
|
|
19
21
|
},
|
|
20
22
|
execModeArgs(ctx) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
-
import { stripForeignConfigDir } from '../adapter.js';
|
|
2
|
+
import { stripForeignConfigDir, slotAwareConfigEnvBash } from '../adapter.js';
|
|
3
3
|
export const museAdapter = {
|
|
4
4
|
id: 'muse',
|
|
5
5
|
// Muse has no MUSE_CONFIG_DIR. Config is XDG-based:
|
|
@@ -23,8 +23,9 @@ export const museAdapter = {
|
|
|
23
23
|
# Pin XDG into the version home so managed runs never walk the adopt-time
|
|
24
24
|
# ~/.config/muse -> version-home symlink — Muse refuses agent-definition
|
|
25
25
|
# sources that are SymlinkOrReparse (exit 1). Same idea as CLAUDE_CONFIG_DIR.
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
# An account-slot launch has already chosen the home (AGENTS_EXEC_HOME); the pins
|
|
27
|
+
# yield to it — see slotAwareConfigEnvBash.
|
|
28
|
+
${slotAwareConfigEnvBash([{ env: 'XDG_CONFIG_HOME', rel: '.config' }, { env: 'XDG_DATA_HOME', rel: '.local/share' }], '$VERSION_DIR/home')}
|
|
28
29
|
`;
|
|
29
30
|
},
|
|
30
31
|
// muse exec: plan ≈ no non-shell writes; auto skips approval prompts but
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
-
import { stripForeignConfigDir } from '../adapter.js';
|
|
2
|
+
import { stripForeignConfigDir, slotAwareConfigEnvBash } from '../adapter.js';
|
|
3
3
|
// OpenCode reads plugins/agents/commands from OPENCODE_CONFIG_DIR and auth from
|
|
4
4
|
// $XDG_DATA_HOME/opencode (HARNESS_AUTH.slotEnv). Pin both at the slot / version
|
|
5
5
|
// home so two accounts in one install never share a credential (PHNX-3940 T5).
|
|
@@ -17,7 +17,9 @@ export const opencodeAdapter = {
|
|
|
17
17
|
# OpenCode reads plugins, agents, commands, and other config-directory
|
|
18
18
|
# resources from OPENCODE_CONFIG_DIR. Point it at the versioned global config
|
|
19
19
|
# tree where agents-cli syncs OpenCode resources.
|
|
20
|
-
|
|
20
|
+
# An account-slot launch has already chosen the home (AGENTS_EXEC_HOME); the pin
|
|
21
|
+
# yields to it — see slotAwareConfigEnvBash.
|
|
22
|
+
${slotAwareConfigEnvBash([{ env: 'OPENCODE_CONFIG_DIR', rel: '.config/opencode' }], '$VERSION_DIR/home')}
|
|
21
23
|
`;
|
|
22
24
|
},
|
|
23
25
|
};
|
|
@@ -170,7 +170,7 @@ export async function downloadHelperApp(spec, version) {
|
|
|
170
170
|
const { zip: zipUrl, sha256: shaUrl } = helperAssetUrls(spec, version);
|
|
171
171
|
const missing = (status, url) => new Error(`no ${spec.assetName} release asset for tag ${tag} (HTTP ${status} on ${url}). ` +
|
|
172
172
|
`The macOS helper ships as a GitHub release asset on its own helper tag; ` +
|
|
173
|
-
`from a repo checkout you can
|
|
173
|
+
`from a repo checkout you can stage it locally instead: ${spec.localBuildHint}`);
|
|
174
174
|
// Checksum first: it is tiny and 404s fast when the tag has no assets.
|
|
175
175
|
const shaRes = await fetch(shaUrl, { signal: AbortSignal.timeout(30_000) });
|
|
176
176
|
if (!shaRes.ok)
|
|
@@ -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.1.1' },
|
|
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
|
|
@@ -64,7 +64,7 @@ export interface ConflictInfo {
|
|
|
64
64
|
* top-level entry add/remove — deep edits to plugin contents won't
|
|
65
65
|
* trigger auto-resync, run `agents sync` for that.
|
|
66
66
|
*/
|
|
67
|
-
export declare const SHIM_SCHEMA_VERSION =
|
|
67
|
+
export declare const SHIM_SCHEMA_VERSION = 32;
|
|
68
68
|
/**
|
|
69
69
|
* Generate the full bash shim script for the given agent. The returned string
|
|
70
70
|
* is written to ~/.agents/shims/{cliCommand} and made executable.
|
|
@@ -179,7 +179,7 @@ export declare function removeShim(agent: AgentId): boolean;
|
|
|
179
179
|
* sorted alphabetically before the real self-updated grok binary and
|
|
180
180
|
* was silently launched instead.
|
|
181
181
|
*/
|
|
182
|
-
export declare const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
182
|
+
export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 21;
|
|
183
183
|
/**
|
|
184
184
|
* Agents whose config directory can be relocated by an environment variable
|
|
185
185
|
* (the per-agent `managedEnv` block in `generateVersionedAliasScript` below).
|
|
@@ -13,6 +13,7 @@ import { AGENTS, agentConfigDirName, readAuthAccountIdentity } from '../agents.j
|
|
|
13
13
|
import { acquireAuthOperationLock } from '../accounts/auth-operation-lock.js';
|
|
14
14
|
import { codexHomeShimBash } from '../codex-home.js';
|
|
15
15
|
import { resolveHarnessAdapter } from '../harness/index.js';
|
|
16
|
+
import { slotAwareConfigEnvBash } from '../harness/adapter.js';
|
|
16
17
|
import { randomUUID } from 'node:crypto';
|
|
17
18
|
import { captureProcessStartTime } from '../platform/process.js';
|
|
18
19
|
import { atomicWriteFileSync } from '../fs-atomic.js';
|
|
@@ -267,7 +268,10 @@ async function promptConflictStrategy(conflictInfos) {
|
|
|
267
268
|
// grok-0.2.118-* file — a 99-byte wrapper that exec'd cursor-agent —
|
|
268
269
|
// sorted alphabetically before the real self-updated grok binary and
|
|
269
270
|
// was silently launched instead.
|
|
270
|
-
|
|
271
|
+
// v32 — every config-dir pin (claude, grok, opencode, kimi, muse, copilot) yields to
|
|
272
|
+
// an account-slot launch (AGENTS_EXEC_HOME from `agents run`), so a slot run
|
|
273
|
+
// reads and writes its own slot home instead of the shared version home.
|
|
274
|
+
export const SHIM_SCHEMA_VERSION = 32;
|
|
271
275
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
272
276
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
273
277
|
function shellQuote(value) {
|
|
@@ -1095,7 +1099,12 @@ export function removeShim(agent) {
|
|
|
1095
1099
|
// v18 — Cursor aliases select the file credential store and swap HOME to the
|
|
1096
1100
|
// version home because current Cursor writes auth.json under ~/.cursor
|
|
1097
1101
|
// and ignores XDG_CONFIG_HOME for credential storage.
|
|
1098
|
-
|
|
1102
|
+
// v21 — every alias config-dir pin (claude, grok, opencode, kimi, muse, copilot)
|
|
1103
|
+
// yields to an account-slot launch (AGENTS_EXEC_HOME from `agents run`). Before,
|
|
1104
|
+
// the alias re-pinned every slot launch onto the shared version home, so a worker
|
|
1105
|
+
// run picked as one account onboarded from scratch and kept its history in
|
|
1106
|
+
// another account's home.
|
|
1107
|
+
export const VERSIONED_ALIAS_SCHEMA_VERSION = 21;
|
|
1099
1108
|
/** Internal marker string used to embed the schema version in versioned alias scripts. */
|
|
1100
1109
|
const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
|
|
1101
1110
|
// The version string is interpolated into a generated bash script and into
|
|
@@ -1229,35 +1238,34 @@ ${resolveHarnessAdapter(agent).shimConfigEnvBash?.({ configDirName }) ?? ''}`
|
|
|
1229
1238
|
# Copilot honors COPILOT_HOME to relocate ~/.copilot (settings, mcp-config.json,
|
|
1230
1239
|
# session-state, logs). Point direct aliases at the versioned home so per-
|
|
1231
1240
|
# version MCP and session state are isolated.
|
|
1232
|
-
|
|
1241
|
+
${slotAwareConfigEnvBash([{ env: 'COPILOT_HOME', rel: configDirName }], `$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home`)}
|
|
1233
1242
|
`
|
|
1234
1243
|
: agent === 'grok'
|
|
1235
1244
|
? `
|
|
1236
1245
|
# Grok Build uses GROK_HOME to isolate its entire configuration tree (skills,
|
|
1237
1246
|
# hooks, plugins, agents, memory, sessions, config.toml, MCP). Point direct
|
|
1238
1247
|
# aliases at the versioned home for isolation parity with the main shim.
|
|
1239
|
-
|
|
1248
|
+
${slotAwareConfigEnvBash([{ env: 'GROK_HOME', rel: configDirName }], `$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home`)}
|
|
1240
1249
|
`
|
|
1241
1250
|
: agent === 'opencode'
|
|
1242
1251
|
? `
|
|
1243
1252
|
# OpenCode reads plugins, agents, commands, and other config-directory
|
|
1244
1253
|
# resources from OPENCODE_CONFIG_DIR. Point direct aliases at the versioned
|
|
1245
1254
|
# global config tree where agents-cli syncs OpenCode resources.
|
|
1246
|
-
|
|
1255
|
+
${slotAwareConfigEnvBash([{ env: 'OPENCODE_CONFIG_DIR', rel: '.config/opencode' }], `$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home`)}
|
|
1247
1256
|
`
|
|
1248
1257
|
: agent === 'kimi'
|
|
1249
1258
|
? `
|
|
1250
1259
|
# Kimi Code CLI honors KIMI_CODE_HOME to relocate ~/.kimi-code (config.toml,
|
|
1251
1260
|
# mcp.json, sessions, skills, hooks). Point direct aliases at the versioned home.
|
|
1252
|
-
|
|
1261
|
+
${slotAwareConfigEnvBash([{ env: 'KIMI_CODE_HOME', rel: configDirName }], `$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home`)}
|
|
1253
1262
|
`
|
|
1254
1263
|
: agent === 'muse'
|
|
1255
1264
|
? `
|
|
1256
1265
|
# Muse Code: no dedicated config env var. Pin XDG so config/sessions live under
|
|
1257
1266
|
# the version home as real directories (not via the adopt symlink at
|
|
1258
1267
|
# ~/.config/muse, which Muse rejects with SymlinkOrReparse).
|
|
1259
|
-
|
|
1260
|
-
export XDG_DATA_HOME="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/.local/share"
|
|
1268
|
+
${slotAwareConfigEnvBash([{ env: 'XDG_CONFIG_HOME', rel: '.config' }, { env: 'XDG_DATA_HOME', rel: '.local/share' }], `$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home`)}
|
|
1261
1269
|
`
|
|
1262
1270
|
: agent === 'cursor'
|
|
1263
1271
|
? `
|
|
@@ -36,7 +36,10 @@ export const MENUBAR_HELPER_SPEC = {
|
|
|
36
36
|
cacheSubdir: ['menubar', 'mac-helper'],
|
|
37
37
|
expectedTeamId: EXPECTED_TEAM_ID,
|
|
38
38
|
expectedBundleId: MENUBAR_HELPER_BUNDLE_ID,
|
|
39
|
-
|
|
39
|
+
// No local build exists in this repo: the source is phnx-labs/agi-menu. The
|
|
40
|
+
// staging script downloads + verifies the same published asset into
|
|
41
|
+
// bin/MenubarHelper.app, which the installer resolves from a checkout.
|
|
42
|
+
localBuildHint: 'scripts/stage-menubar-helper.sh (stages the published menubar/v<floor> asset into bin/MenubarHelper.app; source: https://github.com/phnx-labs/agi-menu)',
|
|
40
43
|
};
|
|
41
44
|
/** Release-asset URLs for the menu-bar helper zip + its checksum at `v<version>`. */
|
|
42
45
|
export function menubarHelperAssetUrls(version) {
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
* (`SERVICE_LABEL_BASE`, `MENUBAR_HELPER_BUNDLE_ID` in download-menubar.ts) —
|
|
25
25
|
* those are what keep the existing Accessibility grant alive across upgrades.
|
|
26
26
|
* Every basename-matching check reads this constant rather than re-deriving
|
|
27
|
-
* the string; the Swift side
|
|
28
|
-
* `
|
|
27
|
+
* the string; the Swift side (phnx-labs/agi-menu,
|
|
28
|
+
* `Sources/MenubarHelper/HelperIdentity.swift`) has its own equal.
|
|
29
29
|
*/
|
|
30
30
|
export declare const MENUBAR_HELPER_EXECUTABLE_NAME = "AGI Menu";
|
|
31
31
|
/**
|
|
@@ -40,7 +40,7 @@ export declare function serviceLabel(): string;
|
|
|
40
40
|
*
|
|
41
41
|
* `source` matters because the two install paths have different notions of
|
|
42
42
|
* "changed": a release bundle is identified by its helper version, while a local
|
|
43
|
-
* dev build has none (
|
|
43
|
+
* dev build has none (agi-menu's build.sh hardcodes CFBundleShortVersionString),
|
|
44
44
|
* so it is identified by the source path + mtime it was copied from.
|
|
45
45
|
*/
|
|
46
46
|
/** Version label for a bundle that has none of its own (a local dev build). */
|
|
@@ -75,10 +75,11 @@ export declare function codesignVerifies(appPath: string): boolean;
|
|
|
75
75
|
* A Developer-ID-signed but un-notarized app is rejected by `spctl --assess`,
|
|
76
76
|
* which macOS surfaces as "the app is damaged" and can crash AppKit during
|
|
77
77
|
* launch. This is separate from `codesign --verify`: a signature can be valid
|
|
78
|
-
* while Gatekeeper still refuses to run it. The
|
|
79
|
-
*
|
|
80
|
-
* shipped bundle passes this; the
|
|
81
|
-
* bootstrap a helper macOS would
|
|
78
|
+
* while Gatekeeper still refuses to run it. The helper's own release
|
|
79
|
+
* (phnx-labs/agi-menu scripts/release.sh) notarizes + staples it and this repo's
|
|
80
|
+
* stage-menubar-helper.sh re-verifies it, so a shipped bundle passes this; the
|
|
81
|
+
* launch guards use it to fail loud rather than bootstrap a helper macOS would
|
|
82
|
+
* reject.
|
|
82
83
|
*/
|
|
83
84
|
export declare function gatekeeperAssesses(appPath: string): boolean;
|
|
84
85
|
/** True when the bundle carries a Developer ID TeamIdentifier (not ad-hoc). */
|
|
@@ -258,7 +259,7 @@ export declare function disableMenubarService(): void;
|
|
|
258
259
|
* There is deliberately NO content comparison here. Comparing the shipped helper
|
|
259
260
|
* against the installed one cannot distinguish "real upgrade" from "another
|
|
260
261
|
* install's copy": the helper is rebuilt, re-signed and re-notarized on every
|
|
261
|
-
* release (
|
|
262
|
+
* helper release (phnx-labs/agi-menu scripts/release.sh), so consecutive releases
|
|
262
263
|
* ship byte-different bundles from identical Swift source — 1.22.20/21/22 all
|
|
263
264
|
* have the same 2876288-byte executable and three different sha256s AND three
|
|
264
265
|
* different CDHashes. Any digest gate therefore reports "changed" for exactly
|
|
@@ -323,7 +324,7 @@ export declare function shouldMigrateMenubarTcc(opts: {
|
|
|
323
324
|
* Reset the stale Accessibility grant TCC recorded against the pre-migration
|
|
324
325
|
* ad-hoc identity, then stamp the marker so this never runs again on this
|
|
325
326
|
* machine. Targets `SERVICE_LABEL_BASE` — the bundle's actual
|
|
326
|
-
* `CFBundleIdentifier`/codesign `--identifier` (
|
|
327
|
+
* `CFBundleIdentifier`/codesign `--identifier` (set by agi-menu's build.sh) that
|
|
327
328
|
* TCC keys grants to — not the namespaced `serviceLabel()`, which exists only
|
|
328
329
|
* to keep launchd job labels apart under a redirected HOME (RUSH-2639) and is
|
|
329
330
|
* never the app's real identity. Best-effort: a missing/failing `tccutil`
|
|
@@ -40,8 +40,8 @@ const SERVICE_LABEL_BASE = 'com.phnx-labs.agents-menubar';
|
|
|
40
40
|
* (`SERVICE_LABEL_BASE`, `MENUBAR_HELPER_BUNDLE_ID` in download-menubar.ts) —
|
|
41
41
|
* those are what keep the existing Accessibility grant alive across upgrades.
|
|
42
42
|
* Every basename-matching check reads this constant rather than re-deriving
|
|
43
|
-
* the string; the Swift side
|
|
44
|
-
* `
|
|
43
|
+
* the string; the Swift side (phnx-labs/agi-menu,
|
|
44
|
+
* `Sources/MenubarHelper/HelperIdentity.swift`) has its own equal.
|
|
45
45
|
*/
|
|
46
46
|
export const MENUBAR_HELPER_EXECUTABLE_NAME = 'AGI Menu';
|
|
47
47
|
/**
|
|
@@ -64,8 +64,9 @@ export function serviceLabel() {
|
|
|
64
64
|
* respawn rate while staying well inside "the menu bar came back on its own".
|
|
65
65
|
*
|
|
66
66
|
* This only paces the restarts. What actually stops the pile-up is the helper
|
|
67
|
-
* bounding and group-killing its own children (
|
|
68
|
-
* ChildProcess.swift); the two are complementary, not
|
|
67
|
+
* bounding and group-killing its own children (agi-menu's
|
|
68
|
+
* `Sources/MenubarHelper/ChildProcess.swift`); the two are complementary, not
|
|
69
|
+
* alternatives.
|
|
69
70
|
*/
|
|
70
71
|
const MENUBAR_THROTTLE_SECONDS = 30;
|
|
71
72
|
function onDarwin() {
|
|
@@ -94,7 +95,7 @@ function installedVersionMarkerPath() {
|
|
|
94
95
|
*
|
|
95
96
|
* `source` matters because the two install paths have different notions of
|
|
96
97
|
* "changed": a release bundle is identified by its helper version, while a local
|
|
97
|
-
* dev build has none (
|
|
98
|
+
* dev build has none (agi-menu's build.sh hardcodes CFBundleShortVersionString),
|
|
98
99
|
* so it is identified by the source path + mtime it was copied from.
|
|
99
100
|
*/
|
|
100
101
|
/** Version label for a bundle that has none of its own (a local dev build). */
|
|
@@ -163,9 +164,10 @@ export function menubarServiceInstalled() {
|
|
|
163
164
|
/**
|
|
164
165
|
* Locate the source `.app` shipped alongside the compiled JS.
|
|
165
166
|
* 1. dist/lib/menubar/MenubarHelper.app — npm install layout (sibling of this file)
|
|
166
|
-
* 2. <repo>/bin/MenubarHelper.app — raw working tree (tsx/dev)
|
|
167
|
-
*
|
|
168
|
-
*
|
|
167
|
+
* 2. <repo>/bin/MenubarHelper.app — raw working tree (tsx/dev): the
|
|
168
|
+
* published bundle staged by scripts/stage-menubar-helper.sh, or a build
|
|
169
|
+
* copied in from a phnx-labs/agi-menu checkout
|
|
170
|
+
* 3. <on-disk install>/dist/lib/menubar/MenubarHelper.app — Bun single-file
|
|
169
171
|
* binary: `import.meta.url` is a virtual `/$bunfs/` path, so the sibling
|
|
170
172
|
* candidates above can't see the on-disk bundle; recover it via the
|
|
171
173
|
* `agents` launcher symlink.
|
|
@@ -176,7 +178,6 @@ function sourceAppPath() {
|
|
|
176
178
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
177
179
|
candidates.push(path.join(here, APP_BUNDLE_NAME));
|
|
178
180
|
candidates.push(path.resolve(here, '..', '..', '..', 'bin', APP_BUNDLE_NAME));
|
|
179
|
-
candidates.push(path.resolve(here, '..', '..', '..', 'menubar', 'dist', APP_BUNDLE_NAME));
|
|
180
181
|
}
|
|
181
182
|
catch {
|
|
182
183
|
/* import.meta.url unavailable */
|
|
@@ -242,10 +243,11 @@ export function codesignVerifies(appPath) {
|
|
|
242
243
|
* A Developer-ID-signed but un-notarized app is rejected by `spctl --assess`,
|
|
243
244
|
* which macOS surfaces as "the app is damaged" and can crash AppKit during
|
|
244
245
|
* launch. This is separate from `codesign --verify`: a signature can be valid
|
|
245
|
-
* while Gatekeeper still refuses to run it. The
|
|
246
|
-
*
|
|
247
|
-
* shipped bundle passes this; the
|
|
248
|
-
* bootstrap a helper macOS would
|
|
246
|
+
* while Gatekeeper still refuses to run it. The helper's own release
|
|
247
|
+
* (phnx-labs/agi-menu scripts/release.sh) notarizes + staples it and this repo's
|
|
248
|
+
* stage-menubar-helper.sh re-verifies it, so a shipped bundle passes this; the
|
|
249
|
+
* launch guards use it to fail loud rather than bootstrap a helper macOS would
|
|
250
|
+
* reject.
|
|
249
251
|
*/
|
|
250
252
|
export function gatekeeperAssesses(appPath) {
|
|
251
253
|
const r = spawnSync('spctl', ['--assess', '--type', 'exec', appPath], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
@@ -751,7 +753,7 @@ export function disableMenubarService() {
|
|
|
751
753
|
* There is deliberately NO content comparison here. Comparing the shipped helper
|
|
752
754
|
* against the installed one cannot distinguish "real upgrade" from "another
|
|
753
755
|
* install's copy": the helper is rebuilt, re-signed and re-notarized on every
|
|
754
|
-
* release (
|
|
756
|
+
* helper release (phnx-labs/agi-menu scripts/release.sh), so consecutive releases
|
|
755
757
|
* ship byte-different bundles from identical Swift source — 1.22.20/21/22 all
|
|
756
758
|
* have the same 2876288-byte executable and three different sha256s AND three
|
|
757
759
|
* different CDHashes. Any digest gate therefore reports "changed" for exactly
|
|
@@ -968,7 +970,7 @@ export function shouldMigrateMenubarTcc(opts) {
|
|
|
968
970
|
* Reset the stale Accessibility grant TCC recorded against the pre-migration
|
|
969
971
|
* ad-hoc identity, then stamp the marker so this never runs again on this
|
|
970
972
|
* machine. Targets `SERVICE_LABEL_BASE` — the bundle's actual
|
|
971
|
-
* `CFBundleIdentifier`/codesign `--identifier` (
|
|
973
|
+
* `CFBundleIdentifier`/codesign `--identifier` (set by agi-menu's build.sh) that
|
|
972
974
|
* TCC keys grants to — not the namespaced `serviceLabel()`, which exists only
|
|
973
975
|
* to keep launchd job labels apart under a redirected HOME (RUSH-2639) and is
|
|
974
976
|
* never the app's real identity. Best-effort: a missing/failing `tccutil`
|
|
@@ -240,13 +240,16 @@ export interface ReconcileWorkerSlotsDeps {
|
|
|
240
240
|
readMetaFn?: () => Pick<Meta, 'accounts' | 'deviceAccounts'>;
|
|
241
241
|
hasLocalKey?: (bundle: string, key: string) => boolean;
|
|
242
242
|
provision?: (account: NativeAccountRecord) => void;
|
|
243
|
+
/** True when an existing durable slot already carries everything provisioning seeds. */
|
|
244
|
+
slotSeeded?: (harness: AgentId, slotDir: string) => boolean;
|
|
243
245
|
}
|
|
244
246
|
/**
|
|
245
247
|
* Worker-side: after a durable key lands, materialize a slot for each portable
|
|
246
248
|
* account whose credential is now present locally. Runs only on a NON-headed
|
|
247
249
|
* (worker or unmarked) device -- a headed device provisions its slots from an
|
|
248
250
|
* interactive native login (`accounts add`), never from an injected durable key
|
|
249
|
-
* (invariant 7). Idempotent: an account already backed by a
|
|
250
|
-
* skipped
|
|
251
|
+
* (invariant 7). Idempotent: an account already backed by a fully seeded
|
|
252
|
+
* `durable` slot is skipped; a claude slot provisioned before onboarding was
|
|
253
|
+
* seeded (no `hasCompletedOnboarding`) is provisioned again so it converges.
|
|
251
254
|
*/
|
|
252
255
|
export declare function reconcileLocalWorkerSlots(deps?: ReconcileWorkerSlotsDeps): ReconcileWorkerSlotsResult;
|
|
@@ -19,7 +19,7 @@ import { machineId, normalizeHost } from './session/sync/config.js';
|
|
|
19
19
|
import { readFleetSharedDeviceStates, updateFleetSharedDeviceStateAsync, } from './fleet-shared-state.js';
|
|
20
20
|
import { getCacheDir, getUserAgentsDir, readMeta } from './state.js';
|
|
21
21
|
import { listNativeAccounts, readSlots } from './account-registry.js';
|
|
22
|
-
import { claudeAccountTokenKey, provisionWorkerSlot, readReservedCredential } from './claude-account-token.js';
|
|
22
|
+
import { claudeAccountTokenKey, isClaudeWorkerHomeSeeded, provisionWorkerSlot, readReservedCredential } from './claude-account-token.js';
|
|
23
23
|
import { configuredDeviceRole, isHeadedDeviceRole, selfConfiguredDeviceRole } from './device-config.js';
|
|
24
24
|
import { AUTH_STORE_ALIAS, AUTH_BUNDLE_NAME, AUTH_BUNDLE_BACKEND, RESERVED_BUNDLE_NAMES, isReservedBundleName, ReservedBundleWrongBackendError, assertReservedAuthBackend, isReservedBundleBackendError, RESERVED_STORES, reservedStoreName, isReservedStoreName, assertStorableCredentialKind, inspectReservedAuthBundle, } from './reserved-stores.js';
|
|
25
25
|
// --- reserved per-harness credential stores (PHNX-3940 / PHNX-3989) --------
|
|
@@ -469,13 +469,17 @@ export async function syncReservedStores(deps = {}) {
|
|
|
469
469
|
writeDeliveryMemo(memo, deps.cacheDir ?? getCacheDir());
|
|
470
470
|
return result;
|
|
471
471
|
}
|
|
472
|
+
function defaultSlotSeeded(harness, slotDir) {
|
|
473
|
+
return harness === 'claude' ? isClaudeWorkerHomeSeeded(slotDir) : true;
|
|
474
|
+
}
|
|
472
475
|
/**
|
|
473
476
|
* Worker-side: after a durable key lands, materialize a slot for each portable
|
|
474
477
|
* account whose credential is now present locally. Runs only on a NON-headed
|
|
475
478
|
* (worker or unmarked) device -- a headed device provisions its slots from an
|
|
476
479
|
* interactive native login (`accounts add`), never from an injected durable key
|
|
477
|
-
* (invariant 7). Idempotent: an account already backed by a
|
|
478
|
-
* skipped
|
|
480
|
+
* (invariant 7). Idempotent: an account already backed by a fully seeded
|
|
481
|
+
* `durable` slot is skipped; a claude slot provisioned before onboarding was
|
|
482
|
+
* seeded (no `hasCompletedOnboarding`) is provisioned again so it converges.
|
|
479
483
|
*/
|
|
480
484
|
export function reconcileLocalWorkerSlots(deps = {}) {
|
|
481
485
|
const result = { provisioned: [], skipped: [], errors: [] };
|
|
@@ -486,6 +490,7 @@ export function reconcileLocalWorkerSlots(deps = {}) {
|
|
|
486
490
|
const slots = readSlots(meta);
|
|
487
491
|
const hasLocalKey = deps.hasLocalKey ?? defaultHasLocalKey;
|
|
488
492
|
const provision = deps.provision ?? provisionWorkerSlot;
|
|
493
|
+
const slotSeeded = deps.slotSeeded ?? defaultSlotSeeded;
|
|
489
494
|
const byId = new Map(listNativeAccounts(meta).map((account) => [account.id, account]));
|
|
490
495
|
// Resolve each account to the one (bundle, key) the push plan uses: a T1 row's
|
|
491
496
|
// reserved `__<harness>__` key, or the legacy `auth` key by email for a claude
|
|
@@ -502,12 +507,21 @@ export function reconcileLocalWorkerSlots(deps = {}) {
|
|
|
502
507
|
result.skipped.push({ accountId: account.id, reason: 'durable key not synced yet' });
|
|
503
508
|
continue;
|
|
504
509
|
}
|
|
505
|
-
|
|
510
|
+
const existing = slots[account.id];
|
|
511
|
+
const reseed = existing?.authMode === 'durable';
|
|
512
|
+
if (reseed && slotSeeded(account.agent, existing.slotDir)) {
|
|
506
513
|
result.skipped.push({ accountId: account.id, reason: 'slot already provisioned' });
|
|
507
514
|
continue;
|
|
508
515
|
}
|
|
509
516
|
try {
|
|
510
517
|
provision(account);
|
|
518
|
+
// A re-seed that still reads as unseeded is a slot file this box cannot
|
|
519
|
+
// repair (a .claude.json that exists but does not parse is left alone by
|
|
520
|
+
// the seeder). Surface it instead of logging "provisioned" every tick.
|
|
521
|
+
if (reseed && !slotSeeded(account.agent, existing.slotDir)) {
|
|
522
|
+
result.errors.push({ accountId: account.id, message: `slot ${existing.slotDir} is still not fully seeded after re-provisioning; a config file there exists but cannot be parsed` });
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
511
525
|
result.provisioned.push(account.id);
|
|
512
526
|
}
|
|
513
527
|
catch (err) {
|
|
@@ -267,6 +267,16 @@ export interface ActiveSession {
|
|
|
267
267
|
* off this so an idle-but-old session doesn't read as freshly active.
|
|
268
268
|
*/
|
|
269
269
|
lastActivityMs?: number;
|
|
270
|
+
/**
|
|
271
|
+
* The transcript's own cursor — the harness stamp on the last meaningful event
|
|
272
|
+
* (message / tool call / tool result), from {@link SessionState.lastEventMs}.
|
|
273
|
+
* Unlike {@link lastActivityMs} it does not move when a hook-firing record is
|
|
274
|
+
* appended, so the attention reconciler uses it to decide whether the agent
|
|
275
|
+
* worked past a hook-raised prompt (PHNX-3999). Absent when the harness stamps
|
|
276
|
+
* no times, when no transcript was parsed, and on rows from a peer running an
|
|
277
|
+
* older CLI.
|
|
278
|
+
*/
|
|
279
|
+
lastEventMs?: number;
|
|
270
280
|
status: ActiveStatus;
|
|
271
281
|
/**
|
|
272
282
|
* Coarse lifecycle bucket derived once from {@link status} (see {@link SessionPhase}).
|
|
@@ -642,26 +652,23 @@ interface LiveSignals {
|
|
|
642
652
|
/** Rolling output-token throughput (tokens/sec). Undefined when no usage in the tail. */
|
|
643
653
|
tokPerSec?: number;
|
|
644
654
|
}
|
|
645
|
-
/** Test seam: drop the in-process
|
|
655
|
+
/** Test seam: drop the in-process parsed-tail memo. */
|
|
646
656
|
export declare function clearLiveSignalsCacheForTest(): void;
|
|
647
657
|
/**
|
|
648
658
|
* Derive the inferred state (working / waiting / idle + preview/badges) and, for
|
|
649
659
|
* the two harnesses whose raw lines carry it, the output-token throughput.
|
|
650
660
|
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
* rush, gemini, kimi, hermes, opencode, antigravity, cursor) is parsed with its own
|
|
655
|
-
* parser and run through the SAME {@link inferSessionState}, so a live
|
|
656
|
-
* non-claude/codex agent gets a real working/waiting/idle instead of the blanket
|
|
657
|
-
* `unknown` it used to fall through to. An opaque/untracked kind or an
|
|
661
|
+
* Every tracked harness runs through the SAME {@link inferSessionState}, so a
|
|
662
|
+
* live non-claude/codex agent gets a real working/waiting/idle instead of the
|
|
663
|
+
* blanket `unknown` it used to fall through to. An opaque/untracked kind or an
|
|
658
664
|
* unreadable/empty transcript yields an empty signal set, and the caller's
|
|
659
665
|
* {@link resolveFallbackStatus} reports the honest live floor (`running`).
|
|
660
666
|
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
667
|
+
* An unchanged-mtime call reuses the parsed tail (see {@link liveTailCache}) so a
|
|
668
|
+
* 30s active-session poll does not re-tail every quiet transcript — but it always
|
|
669
|
+
* re-classifies against `nowMs`, so an elapsed-time verdict is never frozen.
|
|
663
670
|
*/
|
|
664
|
-
export declare function computeLiveSignals(kind: string, sessionFile: string | undefined, cwd: string | undefined, pidAlive: boolean): LiveSignals;
|
|
671
|
+
export declare function computeLiveSignals(kind: string, sessionFile: string | undefined, cwd: string | undefined, pidAlive: boolean, nowMs?: number): LiveSignals;
|
|
665
672
|
/**
|
|
666
673
|
* One-line summary of a teammate's spawn prompt — the team's task/target. Takes
|
|
667
674
|
* the first non-empty line, strips a leading `MISSION:`/`CONTEXT:`/`TASK:` label,
|
|
@@ -760,36 +760,56 @@ function parseTailEventsForKind(agent, sessionFile) {
|
|
|
760
760
|
return events.length > LIVE_STATE_MAX_EVENTS ? events.slice(-LIVE_STATE_MAX_EVENTS) : events;
|
|
761
761
|
}
|
|
762
762
|
/**
|
|
763
|
-
* Process-local memo for {@link computeLiveSignals}. The
|
|
764
|
-
* poll re-reads every live session on a ~30s tick (#2047)
|
|
765
|
-
*
|
|
766
|
-
* the
|
|
767
|
-
*
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
763
|
+
* Process-local memo of the PARSED TAIL for {@link computeLiveSignals}. The
|
|
764
|
+
* menu-bar / `--active` poll re-reads every live session on a ~30s tick (#2047),
|
|
765
|
+
* and while a transcript's mtime is unchanged its bytes are unchanged — so the
|
|
766
|
+
* parse (the expensive half) is memoized on mtime. The CLASSIFICATION is not:
|
|
767
|
+
* `inferSessionState` is re-run on every call against the current clock and the
|
|
768
|
+
* current `pidAlive`, because a time-based verdict (the 30-minute prose-question
|
|
769
|
+
* decay) must expire on schedule even while the bytes sit still — a memo keyed
|
|
770
|
+
* on mtime froze that verdict for as long as nobody typed (PHNX-3999). Bound the
|
|
771
|
+
* map so a long-lived daemon that sees thousands of sessions cannot retain them
|
|
772
|
+
* forever.
|
|
773
|
+
*/
|
|
774
|
+
const LIVE_TAIL_CACHE_MAX = 512;
|
|
775
|
+
const liveTailCache = new Map();
|
|
776
|
+
/** Test seam: drop the in-process parsed-tail memo. */
|
|
772
777
|
export function clearLiveSignalsCacheForTest() {
|
|
773
|
-
|
|
778
|
+
liveTailCache.clear();
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Parse the transcript tail for one harness. Claude/Codex take the fast bounded
|
|
782
|
+
* byte-tail ({@link readSessionTailWithRaw}) — the hot path, and the only two
|
|
783
|
+
* that also yield throughput (their raw lines carry usage the event model drops).
|
|
784
|
+
* EVERY OTHER tracked harness (grok, droid, rush, gemini, kimi, hermes, opencode,
|
|
785
|
+
* antigravity, cursor) is parsed with its own parser. An opaque/untracked kind
|
|
786
|
+
* yields no events.
|
|
787
|
+
*/
|
|
788
|
+
function parseLiveTail(kind, sessionFile, mtimeMs) {
|
|
789
|
+
if (kind === 'claude' || kind === 'codex') {
|
|
790
|
+
const { events, content } = readSessionTailWithRaw(sessionFile, kind);
|
|
791
|
+
const tokPerSec = events.length > 0 ? computeTokPerSec(content, kind) : 0;
|
|
792
|
+
return { mtimeMs, events, tokPerSec: tokPerSec > 0 ? tokPerSec : undefined };
|
|
793
|
+
}
|
|
794
|
+
if (isSessionTrackedAgent(kind))
|
|
795
|
+
return { mtimeMs, events: parseTailEventsForKind(kind, sessionFile) };
|
|
796
|
+
return { mtimeMs, events: [] };
|
|
774
797
|
}
|
|
775
798
|
/**
|
|
776
799
|
* Derive the inferred state (working / waiting / idle + preview/badges) and, for
|
|
777
800
|
* the two harnesses whose raw lines carry it, the output-token throughput.
|
|
778
801
|
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
* rush, gemini, kimi, hermes, opencode, antigravity, cursor) is parsed with its own
|
|
783
|
-
* parser and run through the SAME {@link inferSessionState}, so a live
|
|
784
|
-
* non-claude/codex agent gets a real working/waiting/idle instead of the blanket
|
|
785
|
-
* `unknown` it used to fall through to. An opaque/untracked kind or an
|
|
802
|
+
* Every tracked harness runs through the SAME {@link inferSessionState}, so a
|
|
803
|
+
* live non-claude/codex agent gets a real working/waiting/idle instead of the
|
|
804
|
+
* blanket `unknown` it used to fall through to. An opaque/untracked kind or an
|
|
786
805
|
* unreadable/empty transcript yields an empty signal set, and the caller's
|
|
787
806
|
* {@link resolveFallbackStatus} reports the honest live floor (`running`).
|
|
788
807
|
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
808
|
+
* An unchanged-mtime call reuses the parsed tail (see {@link liveTailCache}) so a
|
|
809
|
+
* 30s active-session poll does not re-tail every quiet transcript — but it always
|
|
810
|
+
* re-classifies against `nowMs`, so an elapsed-time verdict is never frozen.
|
|
791
811
|
*/
|
|
792
|
-
export function computeLiveSignals(kind, sessionFile, cwd, pidAlive) {
|
|
812
|
+
export function computeLiveSignals(kind, sessionFile, cwd, pidAlive, nowMs = Date.now()) {
|
|
793
813
|
if (!sessionFile)
|
|
794
814
|
return {};
|
|
795
815
|
let mtimeMs;
|
|
@@ -798,30 +818,17 @@ export function computeLiveSignals(kind, sessionFile, cwd, pidAlive) {
|
|
|
798
818
|
}
|
|
799
819
|
catch { /* vanished between calls */ }
|
|
800
820
|
const cacheKey = `${kind}\0${sessionFile}\0${cwd ?? ''}`;
|
|
801
|
-
|
|
802
|
-
if (
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
signals = { state, tokPerSec: tokPerSec > 0 ? tokPerSec : undefined };
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
else if (isSessionTrackedAgent(kind)) {
|
|
816
|
-
const events = parseTailEventsForKind(kind, sessionFile);
|
|
817
|
-
if (events.length > 0) {
|
|
818
|
-
signals = { state: inferSessionState(events, ctx) };
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
if (liveSignalsCache.size >= LIVE_SIGNALS_CACHE_MAX)
|
|
822
|
-
liveSignalsCache.clear();
|
|
823
|
-
liveSignalsCache.set(cacheKey, { mtimeMs, pidAlive, signals });
|
|
824
|
-
return signals;
|
|
821
|
+
let tail = liveTailCache.get(cacheKey);
|
|
822
|
+
if (!tail || tail.mtimeMs !== mtimeMs) {
|
|
823
|
+
tail = parseLiveTail(kind, sessionFile, mtimeMs);
|
|
824
|
+
if (liveTailCache.size >= LIVE_TAIL_CACHE_MAX)
|
|
825
|
+
liveTailCache.clear();
|
|
826
|
+
liveTailCache.set(cacheKey, tail);
|
|
827
|
+
}
|
|
828
|
+
if (tail.events.length === 0)
|
|
829
|
+
return {};
|
|
830
|
+
const state = inferSessionState(tail.events, { cwd, pidAlive, mtimeMs, activeWindowMs: ACTIVE_MTIME_WINDOW_MS, nowMs });
|
|
831
|
+
return { state, tokPerSec: tail.tokPerSec };
|
|
825
832
|
}
|
|
826
833
|
/** Map inferred activity onto the coarse ActiveStatus used by the renderer and counts. */
|
|
827
834
|
function statusFromActivity(activity) {
|
|
@@ -850,6 +857,7 @@ function applyState(base, state, fallbackFile, pidAlive) {
|
|
|
850
857
|
activity: state.activity,
|
|
851
858
|
awaitingReason: state.awaitingReason,
|
|
852
859
|
question: state.question,
|
|
860
|
+
lastEventMs: state.lastEventMs,
|
|
853
861
|
plan: state.plan,
|
|
854
862
|
todos: state.todos,
|
|
855
863
|
tail: state.tail,
|
|
@@ -59,7 +59,7 @@ export declare const IMMUTABLE_FIELD_KEYS: readonly ["topic", "firstUserMessage"
|
|
|
59
59
|
* are short-window signals (preview, tok/s). The short snapshot TTL is the
|
|
60
60
|
* only cache that may carry them — and only as a whole-row snapshot.
|
|
61
61
|
*/
|
|
62
|
-
export declare const LIVE_STATUS_KEYS: readonly ["status", "activity", "preview", "tokPerSec", "awaitingReason", "question", "todos", "tail", "lastActivityMs", "hostLink", "presence", "pidAlive", "tmuxClients", "windowHeartbeatMs", "provenance", "rateLimited", "plan"];
|
|
62
|
+
export declare const LIVE_STATUS_KEYS: readonly ["status", "activity", "preview", "tokPerSec", "awaitingReason", "question", "todos", "tail", "lastActivityMs", "lastEventMs", "hostLink", "presence", "pidAlive", "tmuxClients", "windowHeartbeatMs", "provenance", "rateLimited", "plan"];
|
|
63
63
|
/**
|
|
64
64
|
* How long after the last reader heartbeat the daemon considers the journal
|
|
65
65
|
* to have no active consumer and skips the expensive `ps`+`lsof` gather.
|