@phnx-labs/agents-cli 1.22.22 → 1.22.23
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 +209 -0
- package/README.md +8 -2
- package/dist/commands/doctor.js +15 -7
- package/dist/commands/exec.js +20 -6
- package/dist/commands/focus.d.ts +76 -4
- package/dist/commands/focus.js +219 -40
- package/dist/commands/fork.d.ts +20 -2
- package/dist/commands/fork.js +91 -64
- package/dist/commands/go.d.ts +25 -0
- package/dist/commands/go.js +63 -2
- package/dist/commands/harness-wizard.d.ts +206 -0
- package/dist/commands/harness-wizard.js +403 -0
- package/dist/commands/harness.d.ts +12 -0
- package/dist/commands/harness.js +97 -101
- package/dist/commands/resume.js +10 -3
- package/dist/commands/secrets.js +25 -30
- package/dist/commands/sessions-resume.d.ts +21 -3
- package/dist/commands/sessions-resume.js +50 -11
- package/dist/commands/sessions.d.ts +81 -5
- package/dist/commands/sessions.js +325 -66
- package/dist/commands/watchdog.js +13 -2
- package/dist/lib/agents.d.ts +1 -1
- package/dist/lib/agents.js +132 -0
- package/dist/lib/codex-policy.d.ts +17 -0
- package/dist/lib/codex-policy.js +48 -0
- package/dist/lib/crabbox/lease.d.ts +25 -0
- package/dist/lib/crabbox/lease.js +62 -0
- package/dist/lib/daemon.js +70 -0
- package/dist/lib/exec.d.ts +4 -0
- package/dist/lib/exec.js +88 -54
- package/dist/lib/feed-broadcast.d.ts +1 -20
- package/dist/lib/feed-broadcast.js +31 -1
- package/dist/lib/hooks.js +12 -2
- package/dist/lib/mcp.js +44 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
- package/dist/lib/models.d.ts +0 -5
- package/dist/lib/models.js +48 -0
- package/dist/lib/plugin-marketplace.js +9 -0
- package/dist/lib/pricing/prices.json +119 -92
- package/dist/lib/pricing/table.js +13 -0
- package/dist/lib/remote-agents-json.d.ts +29 -1
- package/dist/lib/remote-agents-json.js +47 -10
- package/dist/lib/resources/mcp.js +2 -0
- package/dist/lib/resources/permissions.js +3 -0
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/runner.js +28 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +118 -49
- package/dist/lib/secrets/reaper.d.ts +87 -0
- package/dist/lib/secrets/reaper.js +184 -0
- package/dist/lib/secrets/remote.d.ts +29 -0
- package/dist/lib/secrets/remote.js +37 -1
- package/dist/lib/session/active.d.ts +36 -1
- package/dist/lib/session/active.js +60 -19
- package/dist/lib/session/actor-sidecar.d.ts +14 -0
- package/dist/lib/session/actor-sidecar.js +67 -4
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +32 -1
- package/dist/lib/session/discover.js +168 -0
- package/dist/lib/session/parse.d.ts +10 -0
- package/dist/lib/session/parse.js +98 -0
- package/dist/lib/session/remote-list.d.ts +10 -1
- package/dist/lib/session/remote-list.js +2 -8
- package/dist/lib/session/remote.d.ts +57 -3
- package/dist/lib/session/remote.js +90 -26
- package/dist/lib/session/resume-command.d.ts +6 -0
- package/dist/lib/session/resume-command.js +8 -0
- package/dist/lib/session/session-cache.d.ts +173 -0
- package/dist/lib/session/session-cache.js +399 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/width.d.ts +1 -1
- package/dist/lib/session/width.js +12 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +40 -5
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/usage.d.ts +13 -0
- package/dist/lib/usage.js +215 -0
- package/dist/lib/versions.js +13 -2
- package/package.json +1 -1
- package/dist/bin/agents +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
package/dist/lib/agents.js
CHANGED
|
@@ -751,6 +751,66 @@ export const AGENTS = {
|
|
|
751
751
|
interactiveRepl: true,
|
|
752
752
|
},
|
|
753
753
|
},
|
|
754
|
+
// Meta Muse Code (`muse`) — first-class harness for Meta's coding agent (terminal/CI), built on
|
|
755
|
+
// Muse Spark. Native binary via curl installer (self-updating; no npm package).
|
|
756
|
+
// Config: `~/.config/muse/settings.json` (requires `"schema_version": 1`).
|
|
757
|
+
// Sessions: `~/.local/share/muse/sessions/YYYY/MM/DD/<uuid>/session.jsonl`.
|
|
758
|
+
// Skills: `~/.config/muse/skills` + `~/.agents/skills` + project `.agents/skills`.
|
|
759
|
+
// MCP: `mcp_servers` in settings.json (stdio / streamable_http transports).
|
|
760
|
+
// Headless: `muse exec "<prompt>"` with --model, --reasoning-effort, --json,
|
|
761
|
+
// --yolo / --disable-approval / --disable-sandbox. Auth: META_API_KEY or
|
|
762
|
+
// browser OAuth stored at `~/.config/muse/auth.json`. Default model:
|
|
763
|
+
// muse-spark-1.2. Docs: https://dev.meta.ai/docs/muse-code
|
|
764
|
+
muse: {
|
|
765
|
+
id: 'muse',
|
|
766
|
+
name: 'Muse',
|
|
767
|
+
color: 'blueBright',
|
|
768
|
+
cliCommand: 'muse',
|
|
769
|
+
npmPackage: '',
|
|
770
|
+
installScript: 'curl -fsSL https://dev.meta.ai/install.sh | sh',
|
|
771
|
+
configDir: path.join(HOME, '.config', 'muse'),
|
|
772
|
+
authFiles: ['auth.json'],
|
|
773
|
+
commandsDir: '',
|
|
774
|
+
commandsSubdir: '',
|
|
775
|
+
skillsDir: path.join(HOME, '.config', 'muse', 'skills'),
|
|
776
|
+
hooksDir: 'hooks',
|
|
777
|
+
instructionsFile: 'AGENTS.md',
|
|
778
|
+
format: 'markdown',
|
|
779
|
+
variableSyntax: '$ARGUMENTS',
|
|
780
|
+
// Muse hooks use the Claude-shaped settings.json hooks block (matcher +
|
|
781
|
+
// command groups) under ~/.config/muse/settings.json, plus project
|
|
782
|
+
// `.muse/hooks.json`. registerHooksForClaude is reused with Muse's config
|
|
783
|
+
// dir and schema_version: 1.
|
|
784
|
+
supportsHooks: true,
|
|
785
|
+
// Claude-compatible marketplace layout + native `.muse-plugin/` manifest.
|
|
786
|
+
pluginManifestDir: '.muse-plugin',
|
|
787
|
+
capabilities: {
|
|
788
|
+
// Hooks: Claude-compatible event→matcher→command groups in settings.json.
|
|
789
|
+
hooks: true,
|
|
790
|
+
mcp: true,
|
|
791
|
+
mcpHttp: true,
|
|
792
|
+
mcpHeaders: true,
|
|
793
|
+
// Muse's safety model is approval-mode + OS sandbox (CLI flags), not a
|
|
794
|
+
// Claude-style tool-name allow/deny list. No settings.permissions writer.
|
|
795
|
+
allowlist: false,
|
|
796
|
+
skills: true,
|
|
797
|
+
// No slash-command file dir; skills double as reusable workflows.
|
|
798
|
+
commands: false,
|
|
799
|
+
// Plugins: Claude marketplace + .muse-plugin (syncPluginToVersion).
|
|
800
|
+
plugins: true,
|
|
801
|
+
// Runtime multi-agent / subagents exist, but there is no installable
|
|
802
|
+
// subagent-definition directory for agents-cli to sync into (capability
|
|
803
|
+
// table must stay truthful — see subagents-registry completeness).
|
|
804
|
+
subagents: false,
|
|
805
|
+
rules: { file: 'AGENTS.md' },
|
|
806
|
+
workflows: false,
|
|
807
|
+
// Portable memory at <repo>/.agents/memory/ + personal scopes.
|
|
808
|
+
memory: true,
|
|
809
|
+
modes: ['plan', 'edit', 'auto', 'skip'],
|
|
810
|
+
rulesImports: false,
|
|
811
|
+
interactiveRepl: true,
|
|
812
|
+
},
|
|
813
|
+
},
|
|
754
814
|
};
|
|
755
815
|
/** All current and legacy agent IDs derived from the AGENTS registry. */
|
|
756
816
|
export const ALL_AGENT_IDS = Object.keys(AGENTS);
|
|
@@ -965,6 +1025,7 @@ export const UNMANAGED_DETECTION_CANDIDATES = [
|
|
|
965
1025
|
'grok',
|
|
966
1026
|
'copilot',
|
|
967
1027
|
'droid',
|
|
1028
|
+
'muse',
|
|
968
1029
|
];
|
|
969
1030
|
/**
|
|
970
1031
|
* Detect existing agent installations that are NOT yet managed by agents-cli.
|
|
@@ -1071,6 +1132,7 @@ export const ACCOUNT_INSPECTION_AGENT_IDS = [
|
|
|
1071
1132
|
'kimi',
|
|
1072
1133
|
'droid',
|
|
1073
1134
|
'opencode',
|
|
1135
|
+
'muse',
|
|
1074
1136
|
];
|
|
1075
1137
|
const ACCOUNT_INSPECTION_AGENTS = new Set(ACCOUNT_INSPECTION_AGENT_IDS);
|
|
1076
1138
|
/** Whether agents-cli can determine this agent's per-version sign-in state. */
|
|
@@ -1150,6 +1212,9 @@ const CREDENTIAL_FILE_SEGMENTS = {
|
|
|
1150
1212
|
droid: [['.factory', 'auth.v2.file']],
|
|
1151
1213
|
antigravity: [['.gemini', 'antigravity-cli', 'antigravity-oauth-token']],
|
|
1152
1214
|
opencode: [['.local', 'share', 'opencode', 'auth.json']],
|
|
1215
|
+
// Muse Code stores OAuth / API credentials at ~/.config/muse/auth.json
|
|
1216
|
+
// (or META_API_KEY in the environment, which is not a file).
|
|
1217
|
+
muse: [['.config', 'muse', 'auth.json']],
|
|
1153
1218
|
};
|
|
1154
1219
|
/** Whether an agent's credential file exists under a given home. */
|
|
1155
1220
|
function credentialFileExistsUnder(agentId, home) {
|
|
@@ -1440,6 +1505,31 @@ function isValidOpenCodeCredential(value) {
|
|
|
1440
1505
|
default: return false;
|
|
1441
1506
|
}
|
|
1442
1507
|
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Whether a Muse Code `~/.config/muse/auth.json` document holds any usable
|
|
1510
|
+
* access token. The launcher stores per-provider slots (e.g. `meta`) each with
|
|
1511
|
+
* an `access_token` string; a top-level `access_token` is also accepted.
|
|
1512
|
+
* Never returns the secret itself — presence only.
|
|
1513
|
+
*/
|
|
1514
|
+
function museAuthHasToken(value) {
|
|
1515
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1516
|
+
return false;
|
|
1517
|
+
const root = value;
|
|
1518
|
+
if (typeof root.access_token === 'string' && root.access_token.length > 0)
|
|
1519
|
+
return true;
|
|
1520
|
+
if (typeof root.api_key === 'string' && root.api_key.length > 0)
|
|
1521
|
+
return true;
|
|
1522
|
+
for (const slot of Object.values(root)) {
|
|
1523
|
+
if (!slot || typeof slot !== 'object' || Array.isArray(slot))
|
|
1524
|
+
continue;
|
|
1525
|
+
const entry = slot;
|
|
1526
|
+
if (typeof entry.access_token === 'string' && entry.access_token.length > 0)
|
|
1527
|
+
return true;
|
|
1528
|
+
if (typeof entry.api_key === 'string' && entry.api_key.length > 0)
|
|
1529
|
+
return true;
|
|
1530
|
+
}
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1443
1533
|
/**
|
|
1444
1534
|
* Whether a Claude version home's credential file is present but carries no
|
|
1445
1535
|
* token — the "must have a real credential" floor (see
|
|
@@ -1832,6 +1922,30 @@ export async function getAccountInfo(agentId, home) {
|
|
|
1832
1922
|
const accountKey = buildIdentityKey(agentId, [['providers', accountId]]);
|
|
1833
1923
|
return { ...empty, signedIn: true, accountId, accountKey, lastActive };
|
|
1834
1924
|
}
|
|
1925
|
+
case 'muse': {
|
|
1926
|
+
// Muse Code authenticates with META_API_KEY (env, highest priority) or
|
|
1927
|
+
// a stored OAuth/API credential at ~/.config/muse/auth.json. The auth
|
|
1928
|
+
// file is launcher-managed JSON with per-provider slots holding
|
|
1929
|
+
// access_token; there is no email claim, so we report signed-in + a
|
|
1930
|
+
// stable identity key (env-key vs provider slots) like kimi/opencode.
|
|
1931
|
+
if (process.env.META_API_KEY?.trim() || process.env.MODEL_API_KEY?.trim()) {
|
|
1932
|
+
const accountKey = buildIdentityKey(agentId, [['auth', 'env']]);
|
|
1933
|
+
return { ...empty, signedIn: true, accountId: 'env', accountKey, lastActive };
|
|
1934
|
+
}
|
|
1935
|
+
const authPath = resolveAccountCredentialPath(base, '.config', 'muse', 'auth.json');
|
|
1936
|
+
if (!authPath)
|
|
1937
|
+
return { ...empty, lastActive };
|
|
1938
|
+
const data = JSON.parse(await fs.promises.readFile(authPath, 'utf-8'));
|
|
1939
|
+
if (!data || typeof data !== 'object')
|
|
1940
|
+
return { ...empty, lastActive };
|
|
1941
|
+
// auth.json shape: { meta?: { access_token }, … } or similar slots.
|
|
1942
|
+
// Any object with a nested access_token (or a top-level one) counts.
|
|
1943
|
+
const hasToken = museAuthHasToken(data);
|
|
1944
|
+
if (!hasToken)
|
|
1945
|
+
return { ...empty, lastActive };
|
|
1946
|
+
const accountKey = buildIdentityKey(agentId, [['auth', 'file']]);
|
|
1947
|
+
return { ...empty, signedIn: true, accountId: 'file', accountKey, lastActive };
|
|
1948
|
+
}
|
|
1835
1949
|
default:
|
|
1836
1950
|
return { ...empty, lastActive };
|
|
1837
1951
|
}
|
|
@@ -1935,6 +2049,10 @@ function getSessionDir(agentId, base) {
|
|
|
1935
2049
|
return path.join(base, '.copilot', 'session-state');
|
|
1936
2050
|
case 'droid':
|
|
1937
2051
|
return path.join(base, '.factory', 'sessions');
|
|
2052
|
+
case 'muse':
|
|
2053
|
+
// Muse sessions live under XDG data, not the config dir:
|
|
2054
|
+
// ~/.local/share/muse/sessions/YYYY/MM/DD/<uuid>/session.jsonl
|
|
2055
|
+
return path.join(base, '.local', 'share', 'muse', 'sessions');
|
|
1938
2056
|
default:
|
|
1939
2057
|
return null;
|
|
1940
2058
|
}
|
|
@@ -1946,6 +2064,7 @@ function getSessionExtension(agentId) {
|
|
|
1946
2064
|
case 'codex':
|
|
1947
2065
|
case 'copilot':
|
|
1948
2066
|
case 'droid':
|
|
2067
|
+
case 'muse':
|
|
1949
2068
|
return '.jsonl';
|
|
1950
2069
|
case 'gemini':
|
|
1951
2070
|
return '.json';
|
|
@@ -2441,6 +2560,9 @@ export function getUserMcpConfigPath(agentId) {
|
|
|
2441
2560
|
case 'pi':
|
|
2442
2561
|
// omp reads user-scope MCP from ~/.omp/agent/.mcp.json (Claude schema).
|
|
2443
2562
|
return path.join(agent.configDir, '.mcp.json');
|
|
2563
|
+
case 'muse':
|
|
2564
|
+
// Muse Code: MCP lives in ~/.config/muse/settings.json under mcp_servers.
|
|
2565
|
+
return path.join(agent.configDir, 'settings.json');
|
|
2444
2566
|
default:
|
|
2445
2567
|
// Gemini and others use settings.json
|
|
2446
2568
|
return path.join(agent.configDir, 'settings.json');
|
|
@@ -2479,6 +2601,8 @@ export function getMcpConfigPathForHome(agentId, home) {
|
|
|
2479
2601
|
return path.join(home, '.hermes', 'config.yaml');
|
|
2480
2602
|
case 'pi':
|
|
2481
2603
|
return path.join(home, '.omp', 'agent', '.mcp.json');
|
|
2604
|
+
case 'muse':
|
|
2605
|
+
return path.join(home, '.config', 'muse', 'settings.json');
|
|
2482
2606
|
default:
|
|
2483
2607
|
return path.join(home, agentConfigDirName(agentId), 'settings.json');
|
|
2484
2608
|
}
|
|
@@ -2521,6 +2645,10 @@ export function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
|
|
|
2521
2645
|
case 'pi':
|
|
2522
2646
|
// omp reads project MCP from <root>/.mcp.json (Claude-compatible).
|
|
2523
2647
|
return path.join(cwd, '.mcp.json');
|
|
2648
|
+
case 'muse':
|
|
2649
|
+
// Muse project MCP rides the same settings schema under .muse/settings.json
|
|
2650
|
+
// when present; otherwise fall back to the user settings path.
|
|
2651
|
+
return path.join(cwd, '.muse', 'settings.json');
|
|
2524
2652
|
default:
|
|
2525
2653
|
return path.join(cwd, `.${agentId}`, 'settings.json');
|
|
2526
2654
|
}
|
|
@@ -2664,6 +2792,10 @@ export const AGENT_NAME_ALIASES = {
|
|
|
2664
2792
|
droid: 'droid',
|
|
2665
2793
|
hermes: 'hermes',
|
|
2666
2794
|
'hermes-agent': 'hermes',
|
|
2795
|
+
muse: 'muse',
|
|
2796
|
+
'muse-code': 'muse',
|
|
2797
|
+
'muse-spark': 'muse',
|
|
2798
|
+
'meta-muse': 'muse',
|
|
2667
2799
|
};
|
|
2668
2800
|
/**
|
|
2669
2801
|
* Resolve a user-provided agent name (alias, shorthand, or canonical) to its AgentId.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type CodexPolicyMode = 'plan' | 'edit' | 'skip';
|
|
2
|
+
export declare const CODEX_PLAN_PROFILE = "agents-plan";
|
|
3
|
+
export declare const CODEX_EDIT_PROFILE = "agents-edit";
|
|
4
|
+
export declare function codexEditWritableRoots(): string[];
|
|
5
|
+
export declare function codexPermissionProfileConfig(mode: Exclude<CodexPolicyMode, 'skip'>, writableRoots?: string[]): string;
|
|
6
|
+
/**
|
|
7
|
+
* Canonical Codex safety policy used by every native launch path.
|
|
8
|
+
*
|
|
9
|
+
* Config overrides are deliberately used instead of the legacy `--sandbox`
|
|
10
|
+
* flags: named permission profiles are the only Codex surface that can keep a
|
|
11
|
+
* plan run filesystem-read-only while independently enabling network access.
|
|
12
|
+
*/
|
|
13
|
+
export declare function codexPolicyArgs(mode: CodexPolicyMode, writableRoots?: string[]): string[];
|
|
14
|
+
/** Preserve whether --mode was omitted when a run is re-dispatched remotely. */
|
|
15
|
+
export declare function modeForRemoteDispatch(mode: string, source: string | undefined): string | undefined;
|
|
16
|
+
/** Only the untouched Commander default selects Codex's writable default. */
|
|
17
|
+
export declare function modeWasImplicit(source: string | undefined, hasConfiguredDefault: boolean): boolean;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getUserAgentsDir } from './state.js';
|
|
2
|
+
import { codexDefaultWritableRoots } from './permissions.js';
|
|
3
|
+
export const CODEX_PLAN_PROFILE = 'agents-plan';
|
|
4
|
+
export const CODEX_EDIT_PROFILE = 'agents-edit';
|
|
5
|
+
function unique(values) {
|
|
6
|
+
return [...new Set(values)];
|
|
7
|
+
}
|
|
8
|
+
export function codexEditWritableRoots() {
|
|
9
|
+
return unique([getUserAgentsDir(), ...codexDefaultWritableRoots()]);
|
|
10
|
+
}
|
|
11
|
+
function inlineWorkspaceRoots(roots) {
|
|
12
|
+
return roots.map((root) => `${JSON.stringify(root)} = true`).join(', ');
|
|
13
|
+
}
|
|
14
|
+
export function codexPermissionProfileConfig(mode, writableRoots = codexEditWritableRoots()) {
|
|
15
|
+
const parent = mode === 'plan' ? ':read-only' : ':workspace';
|
|
16
|
+
const roots = mode === 'edit'
|
|
17
|
+
? `, workspace_roots = { ${inlineWorkspaceRoots(unique(writableRoots))} }`
|
|
18
|
+
: '';
|
|
19
|
+
return `{ extends = ${JSON.stringify(parent)}${roots}, network = { enabled = true, allow_local_binding = true } }`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Canonical Codex safety policy used by every native launch path.
|
|
23
|
+
*
|
|
24
|
+
* Config overrides are deliberately used instead of the legacy `--sandbox`
|
|
25
|
+
* flags: named permission profiles are the only Codex surface that can keep a
|
|
26
|
+
* plan run filesystem-read-only while independently enabling network access.
|
|
27
|
+
*/
|
|
28
|
+
export function codexPolicyArgs(mode, writableRoots = codexEditWritableRoots()) {
|
|
29
|
+
if (mode === 'skip')
|
|
30
|
+
return ['--dangerously-bypass-approvals-and-sandbox'];
|
|
31
|
+
const profile = mode === 'plan' ? CODEX_PLAN_PROFILE : CODEX_EDIT_PROFILE;
|
|
32
|
+
return [
|
|
33
|
+
'-c',
|
|
34
|
+
'approval_policy="on-request"',
|
|
35
|
+
'-c',
|
|
36
|
+
`default_permissions=${JSON.stringify(profile)}`,
|
|
37
|
+
'-c',
|
|
38
|
+
`permissions.${profile}=${codexPermissionProfileConfig(mode, writableRoots)}`,
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
/** Preserve whether --mode was omitted when a run is re-dispatched remotely. */
|
|
42
|
+
export function modeForRemoteDispatch(mode, source) {
|
|
43
|
+
return source === 'default' ? undefined : mode;
|
|
44
|
+
}
|
|
45
|
+
/** Only the untouched Commander default selects Codex's writable default. */
|
|
46
|
+
export function modeWasImplicit(source, hasConfiguredDefault) {
|
|
47
|
+
return source === 'default' && !hasConfiguredDefault;
|
|
48
|
+
}
|
|
@@ -118,4 +118,29 @@ export interface LeaseRunResult {
|
|
|
118
118
|
* the credential files. Best-effort install steps never abort the run.
|
|
119
119
|
*/
|
|
120
120
|
export declare function buildBootstrapScript(opts: LeaseRunOptions): string;
|
|
121
|
+
/** Untouched-for-this-long ⇒ no active run holds the box, so an expired one is a stray. */
|
|
122
|
+
export declare const STRAY_GRACE_SECS = 600;
|
|
123
|
+
export interface StrayMatchOptions {
|
|
124
|
+
/** Pool the lease belongs to (defaults to DEFAULT_CRABBOX_PROFILE). */
|
|
125
|
+
profile?: string;
|
|
126
|
+
/** Network mode of the lease (default 'public'); a box is partitioned by it. */
|
|
127
|
+
netMode?: 'public' | 'tailscale';
|
|
128
|
+
/** The box this run is using — never a stray. */
|
|
129
|
+
keepSlug: string;
|
|
130
|
+
/** Injectable clock (unix seconds). */
|
|
131
|
+
nowSecs?: number;
|
|
132
|
+
/** Idle grace window (defaults to {@link STRAY_GRACE_SECS}). */
|
|
133
|
+
graceSecs?: number;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Whether `box` is an EXPIRED, idle stray in this run's pool — the boxes that
|
|
137
|
+
* accumulate cost. An expired box can never be reused (`poolReusableBoxes` gates
|
|
138
|
+
* on an unexpired lease), yet `keep:true` leaves it running, so without a sweep a
|
|
139
|
+
* fresh provision leaves the old one billing until `gc`'s 1h-idle window. Only a
|
|
140
|
+
* box that is running, in the SAME profile+netMode pool, has an EXPIRED lease, and
|
|
141
|
+
* has been untouched for the grace window is a stray — a mid-boot or in-use box
|
|
142
|
+
* (recent `lastTouchedAt`, or an unexpired lease) is never one. Pure — testable
|
|
143
|
+
* without a shell.
|
|
144
|
+
*/
|
|
145
|
+
export declare function isExpiredPoolStray(box: CrabboxBox, opts: StrayMatchOptions): boolean;
|
|
121
146
|
export declare function leaseAndRun(opts: LeaseRunOptions): Promise<LeaseRunResult>;
|
|
@@ -33,6 +33,7 @@ import * as yaml from 'yaml';
|
|
|
33
33
|
import { buildCredentialScript, buildHomeFileWriteScript, CLAUDE_TOKEN_REMOTE } from './runtimes.js';
|
|
34
34
|
import { LEASE_AGENT_MARKER, leasePhaseSentinel } from './progress.js';
|
|
35
35
|
import { copySetupToBox } from './setup-copy.js';
|
|
36
|
+
import { DEFAULT_CRABBOX_PROFILE } from './config.js';
|
|
36
37
|
/** POSIX single-quote for safe embedding in the generated bootstrap script. */
|
|
37
38
|
function q(s) {
|
|
38
39
|
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
@@ -173,6 +174,60 @@ function pickReadyPoolBox(opts) {
|
|
|
173
174
|
}
|
|
174
175
|
return null;
|
|
175
176
|
}
|
|
177
|
+
/** Untouched-for-this-long ⇒ no active run holds the box, so an expired one is a stray. */
|
|
178
|
+
export const STRAY_GRACE_SECS = 600;
|
|
179
|
+
/**
|
|
180
|
+
* Whether `box` is an EXPIRED, idle stray in this run's pool — the boxes that
|
|
181
|
+
* accumulate cost. An expired box can never be reused (`poolReusableBoxes` gates
|
|
182
|
+
* on an unexpired lease), yet `keep:true` leaves it running, so without a sweep a
|
|
183
|
+
* fresh provision leaves the old one billing until `gc`'s 1h-idle window. Only a
|
|
184
|
+
* box that is running, in the SAME profile+netMode pool, has an EXPIRED lease, and
|
|
185
|
+
* has been untouched for the grace window is a stray — a mid-boot or in-use box
|
|
186
|
+
* (recent `lastTouchedAt`, or an unexpired lease) is never one. Pure — testable
|
|
187
|
+
* without a shell.
|
|
188
|
+
*/
|
|
189
|
+
export function isExpiredPoolStray(box, opts) {
|
|
190
|
+
const profile = opts.profile ?? DEFAULT_CRABBOX_PROFILE;
|
|
191
|
+
const netMode = opts.netMode ?? 'public';
|
|
192
|
+
const nowSecs = opts.nowSecs ?? Math.floor(Date.now() / 1000);
|
|
193
|
+
const graceSecs = opts.graceSecs ?? STRAY_GRACE_SECS;
|
|
194
|
+
if (box.slug === opts.keepSlug)
|
|
195
|
+
return false;
|
|
196
|
+
if (box.status !== 'running')
|
|
197
|
+
return false;
|
|
198
|
+
if ((box.profile ?? DEFAULT_CRABBOX_PROFILE) !== profile)
|
|
199
|
+
return false;
|
|
200
|
+
const boxNet = box.tailscaleIPv4 || box.tailscaleFQDN ? 'tailscale' : 'public';
|
|
201
|
+
if (boxNet !== netMode)
|
|
202
|
+
return false;
|
|
203
|
+
if (box.expiresAt === null || box.expiresAt > nowSecs)
|
|
204
|
+
return false; // unexpired ⇒ reusable
|
|
205
|
+
if (box.lastTouchedAt !== null && nowSecs - box.lastTouchedAt < graceSecs)
|
|
206
|
+
return false; // maybe active
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Opportunistically stop expired, idle strays in this run's pool — rides the
|
|
211
|
+
* lease (no scheduler), best-effort (never throws, never blocks the run). Returns
|
|
212
|
+
* how many were stopped.
|
|
213
|
+
*/
|
|
214
|
+
function reapExpiredPoolStrays(opts, keepSlug) {
|
|
215
|
+
let boxes;
|
|
216
|
+
try {
|
|
217
|
+
boxes = crabboxList({ secretsBundle: opts.secretsBundle });
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
let reaped = 0;
|
|
223
|
+
for (const b of boxes) {
|
|
224
|
+
if (!isExpiredPoolStray(b, { profile: opts.profile, netMode: opts.netMode, keepSlug }))
|
|
225
|
+
continue;
|
|
226
|
+
if (crabboxStop(b.slug, { secretsBundle: opts.secretsBundle }))
|
|
227
|
+
reaped++;
|
|
228
|
+
}
|
|
229
|
+
return reaped;
|
|
230
|
+
}
|
|
176
231
|
export async function leaseAndRun(opts) {
|
|
177
232
|
const startedAt = Date.now();
|
|
178
233
|
let box;
|
|
@@ -212,6 +267,13 @@ export async function leaseAndRun(opts) {
|
|
|
212
267
|
}
|
|
213
268
|
}
|
|
214
269
|
opts.onPhase?.({ kind: 'ready', box, elapsedMs: Date.now() - startedAt });
|
|
270
|
+
// Reap expired, idle strays in this pool now that we hold our box. An expired
|
|
271
|
+
// box can never be reused, so these are pure cost the 1h-idle `gc` window
|
|
272
|
+
// leaves running. Skip when the caller named an explicit `--box` (they're
|
|
273
|
+
// driving box lifecycle by hand). Best-effort — never blocks or aborts the run.
|
|
274
|
+
if (!opts.reuseBox) {
|
|
275
|
+
reapExpiredPoolStrays(opts, box.slug);
|
|
276
|
+
}
|
|
215
277
|
// Setup-copy (F1, RUSH-1920): push the git-tracked ~/.agents config onto the
|
|
216
278
|
// box from the host, over crabbox's own per-lease ssh (a raw ssh fails
|
|
217
279
|
// publickey). rsync only here — the box has no agents-cli yet, so the matching
|
package/dist/lib/daemon.js
CHANGED
|
@@ -888,6 +888,45 @@ export async function runDaemon() {
|
|
|
888
888
|
};
|
|
889
889
|
const fleetCacheInterval = setInterval(() => { void runFleetCacheWarm(); }, 3 * 60_000);
|
|
890
890
|
const fleetCacheKickoff = setTimeout(() => { void runFleetCacheWarm(); }, 60_000);
|
|
891
|
+
// Session-status cache warm (RUSH-2062): publish THIS host's local active
|
|
892
|
+
// sessions so menubar / Factory / watchdog / CLI share one warm snapshot
|
|
893
|
+
// instead of each re-running a full ~9s / ~170MB gather. Publish-own only
|
|
894
|
+
// (no cross-host SSH — same N² lesson as RUSH-2061). Short interval keeps
|
|
895
|
+
// live status fresh; forceRefresh still re-gathers. Additive sibling of
|
|
896
|
+
// fleetCacheInterval — does not touch the reaper tick.
|
|
897
|
+
let warmingSessionCache = false;
|
|
898
|
+
// Import once so the interval/kickoff constants stay the single source of truth
|
|
899
|
+
// (SESSION_CACHE_WARM_* in session-cache.ts). Dynamic import of the warm fn
|
|
900
|
+
// itself stays inside the tick so a cold daemon boot does not pay the load.
|
|
901
|
+
const sessionCacheWarmTiming = import('./session/session-cache.js').then((m) => ({
|
|
902
|
+
intervalMs: m.SESSION_CACHE_WARM_INTERVAL_MS,
|
|
903
|
+
kickoffMs: m.SESSION_CACHE_WARM_KICKOFF_MS,
|
|
904
|
+
publish: m.publishLocalActiveSessions,
|
|
905
|
+
}));
|
|
906
|
+
const runSessionCacheWarm = async () => {
|
|
907
|
+
if (warmingSessionCache)
|
|
908
|
+
return;
|
|
909
|
+
warmingSessionCache = true;
|
|
910
|
+
try {
|
|
911
|
+
const { publish } = await sessionCacheWarmTiming;
|
|
912
|
+
const r = await publish();
|
|
913
|
+
log('INFO', `session cache warm: ${r.sessions.length} local session(s)`);
|
|
914
|
+
}
|
|
915
|
+
catch (err) {
|
|
916
|
+
log('ERROR', `session cache warm failed: ${err.message}`);
|
|
917
|
+
}
|
|
918
|
+
finally {
|
|
919
|
+
warmingSessionCache = false;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
// Intervals resolved from the module constants (fallback matches the defaults
|
|
923
|
+
// if the import is still pending — the first tick uses the live import).
|
|
924
|
+
let sessionCacheInterval;
|
|
925
|
+
let sessionCacheKickoff;
|
|
926
|
+
void sessionCacheWarmTiming.then(({ intervalMs, kickoffMs }) => {
|
|
927
|
+
sessionCacheInterval = setInterval(() => { void runSessionCacheWarm(); }, intervalMs);
|
|
928
|
+
sessionCacheKickoff = setTimeout(() => { void runSessionCacheWarm(); }, kickoffMs);
|
|
929
|
+
});
|
|
891
930
|
// Usage refresh: keep the usage cache the `agents run` router reads
|
|
892
931
|
// (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
|
|
893
932
|
// This host is the sole writer for its own local accounts. The tick wakes
|
|
@@ -955,6 +994,32 @@ export async function runDaemon() {
|
|
|
955
994
|
}
|
|
956
995
|
};
|
|
957
996
|
const brokerSelfHealInterval = setInterval(() => { void runBrokerSelfHeal(); }, 60_000);
|
|
997
|
+
// RUSH-2232: reap orphaned keychain helpers and `agents` processes stuck on a
|
|
998
|
+
// keychain call. Runs as a 5-min interval in the daemon (the single executor)
|
|
999
|
+
// so no UI surface can race it. The reaper shells `ps` once, plans kills in a
|
|
1000
|
+
// pure function, and executes via killTree.
|
|
1001
|
+
let reapingKeychain = false;
|
|
1002
|
+
const runKeychainReap = async () => {
|
|
1003
|
+
if (reapingKeychain)
|
|
1004
|
+
return;
|
|
1005
|
+
reapingKeychain = true;
|
|
1006
|
+
try {
|
|
1007
|
+
const { reapOrphanedKeychainProcesses } = await import('./secrets/reaper.js');
|
|
1008
|
+
const result = reapOrphanedKeychainProcesses();
|
|
1009
|
+
if (result.reaped > 0) {
|
|
1010
|
+
log('WARN', `Reaped ${result.reaped} keychain orphan/stuck process(es)`);
|
|
1011
|
+
for (const d of result.details)
|
|
1012
|
+
log('WARN', ` ${d}`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
catch (err) {
|
|
1016
|
+
log('ERROR', `Keychain reaper failed: ${err.message}`);
|
|
1017
|
+
}
|
|
1018
|
+
finally {
|
|
1019
|
+
reapingKeychain = false;
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
const keychainReapInterval = setInterval(() => { void runKeychainReap(); }, 5 * 60_000);
|
|
958
1023
|
const handleReload = () => {
|
|
959
1024
|
log('INFO', 'Reloading jobs (SIGHUP)');
|
|
960
1025
|
// Refresh user-layer copies of opted-in project routines BEFORE the
|
|
@@ -1013,9 +1078,14 @@ export async function runDaemon() {
|
|
|
1013
1078
|
clearTimeout(launchHealthKickoff);
|
|
1014
1079
|
clearInterval(fleetCacheInterval);
|
|
1015
1080
|
clearTimeout(fleetCacheKickoff);
|
|
1081
|
+
if (sessionCacheInterval)
|
|
1082
|
+
clearInterval(sessionCacheInterval);
|
|
1083
|
+
if (sessionCacheKickoff)
|
|
1084
|
+
clearTimeout(sessionCacheKickoff);
|
|
1016
1085
|
clearInterval(usageRefreshInterval);
|
|
1017
1086
|
clearTimeout(usageRefreshKickoff);
|
|
1018
1087
|
clearInterval(brokerSelfHealInterval);
|
|
1088
|
+
clearInterval(keychainReapInterval);
|
|
1019
1089
|
hostedBroker?.close();
|
|
1020
1090
|
removeDaemonPid();
|
|
1021
1091
|
removeHeartbeat();
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -86,6 +86,8 @@ export interface ModeWarningState {
|
|
|
86
86
|
* also maps an unsupported `'plan'` request onto this same value.
|
|
87
87
|
*/
|
|
88
88
|
export declare function defaultModeFor(agent: AgentId): Mode;
|
|
89
|
+
/** Safe mode used when the user did not provide --mode or a configured default. */
|
|
90
|
+
export declare function implicitModeFor(agent: AgentId): ExecMode;
|
|
89
91
|
/** Reasoning effort levels passed to agents that support them. 'auto' defers to the agent's default. */
|
|
90
92
|
export type ExecEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
|
|
91
93
|
/** Options for spawning an agent process. Omitting `prompt` launches the CLI interactively. */
|
|
@@ -97,6 +99,8 @@ export interface ExecOptions {
|
|
|
97
99
|
/** Force interactive mode even when a prompt is provided. Wins over `headless`. */
|
|
98
100
|
interactive?: boolean;
|
|
99
101
|
mode: ExecMode;
|
|
102
|
+
/** True when the caller omitted --mode; fallback agents resolve their own safe default. */
|
|
103
|
+
modeWasImplicit?: boolean;
|
|
100
104
|
effort: ExecEffort;
|
|
101
105
|
cwd?: string;
|
|
102
106
|
/** Force headless mode even when no prompt is provided (e.g. piping via stdin). */
|