@stage-labs/metro 0.1.0-beta.307 → 0.1.0-beta.309
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/dist/agent.js +23 -28
- package/dist/child.js +33 -0
- package/dist/claude-env.js +104 -0
- package/dist/cli.js +7 -21
- package/dist/lock.js +62 -0
- package/dist/memory.js +12 -27
- package/dist/root-release-io.js +1 -2
- package/dist/root-release.js +1 -1
- package/dist/route.js +0 -1
- package/hooks/guard.mjs +1 -5
- package/hooks/session-start.mjs +1 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +13 -38
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +21 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -9
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +14 -12
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +6 -18
- package/runtime/node_modules/@metro-labs/daemon/src/claude/github-skills.ts +3 -7
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +6 -5
- package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +1 -23
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +39 -118
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup-api.ts +14 -61
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +10 -42
- package/runtime/node_modules/@metro-labs/daemon/src/claude/usage-probe.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +1 -6
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/claude-logins.ts +53 -29
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/claude-store.ts +55 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +40 -62
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/delivered.ts +82 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/effective.ts +47 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/fallback.ts +18 -19
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +37 -77
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +86 -220
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +80 -283
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-import.ts +125 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-lists.ts +0 -17
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-v2.ts +79 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +0 -79
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/org-models.ts +231 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +4 -44
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage-refresh.ts +37 -57
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +0 -13
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +5 -15
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +0 -11
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +3 -7
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +12 -24
- package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +1 -4
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +1 -2
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +3 -4
- package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +9 -92
- package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +1 -4
- package/runtime/node_modules/@metro-labs/daemon/src/voice/scribe.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/voice/shared.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/voice/speech.ts +0 -4
- package/runtime/node_modules/@metro-labs/daemon/src/voice/store.ts +1 -14
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +0 -1
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
- package/dist/background.js +0 -141
- package/dist/claude.js +0 -204
- package/dist/mcp-config.js +0 -27
- package/dist/native-recovery-scan.js +0 -246
- package/dist/native-recovery-state.js +0 -154
- package/dist/native-recovery.js +0 -164
- package/dist/onboarding.js +0 -44
- package/dist/voice.js +0 -56
- package/runtime/node_modules/@metro-labs/daemon/src/claude/channel-check.ts +0 -56
- package/runtime/node_modules/@metro-labs/daemon/src/claude/pinned.ts +0 -16
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-cleanup.ts +0 -121
- package/runtime/node_modules/@metro-labs/daemon/src/claude/runner.ts +0 -45
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-continuity.ts +0 -57
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +0 -76
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +0 -219
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +0 -119
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +0 -160
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +0 -46
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +0 -150
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +0 -139
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +0 -236
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +0 -208
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +0 -157
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +0 -83
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter-login.ts +0 -192
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter-signin.ts +0 -64
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +0 -36
- package/runtime/node_modules/@metro-labs/daemon/src/voice/approvals.ts +0 -38
- package/runtime/node_modules/@metro-labs/daemon/src/voice/brain.ts +0 -146
- package/runtime/node_modules/@metro-labs/daemon/src/voice/speakable.ts +0 -54
- package/runtime/node_modules/@metro-labs/daemon/src/voice/talk.ts +0 -264
package/dist/agent.js
CHANGED
|
@@ -2,9 +2,11 @@ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } fr
|
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import { runChild } from './child.js';
|
|
6
|
+
import { headlessEnv, NO_UPDATES } from './claude-env.js';
|
|
6
7
|
import { localAgent } from './local.js';
|
|
7
|
-
import {
|
|
8
|
+
import { withLock } from './lock.js';
|
|
9
|
+
import { currentModel, permissionMode, systemPrompt } from './route.js';
|
|
8
10
|
import { installDependencies, ranges, readOrNull } from './runtime-install.js';
|
|
9
11
|
import { findBun, localPort, runtimeDir } from './runtime.js';
|
|
10
12
|
export const RUNNER_SOURCES = 'sdk-runner';
|
|
@@ -13,7 +15,8 @@ const ENTRY = join('src', 'main.ts');
|
|
|
13
15
|
const METRO_MODULES = join('node_modules', '@metro-labs');
|
|
14
16
|
const BUNFIG = '[install]\noptional = false\npeer = false\n';
|
|
15
17
|
const SDK = '@anthropic-ai/claude-agent-sdk';
|
|
16
|
-
const
|
|
18
|
+
const PREPARE_LOCK = '.prepare.lock';
|
|
19
|
+
const PREPARE_WAIT = { staleMs: 20 * 60_000, waitMs: 20 * 60_000, pollMs: 250 };
|
|
17
20
|
export function runnerStore(env = process.env) {
|
|
18
21
|
const explicit = env.METRO_RUNNER_STORE?.trim() ?? '';
|
|
19
22
|
return explicit === '' ? join(homedir(), '.metro', 'sdk-runner') : explicit;
|
|
@@ -49,14 +52,20 @@ function stageSources(sources, store, stamp) {
|
|
|
49
52
|
writeFileSync(join(store, 'bunfig.toml'), BUNFIG);
|
|
50
53
|
}
|
|
51
54
|
export function prepareRunner(opts = {}) {
|
|
52
|
-
const sources = opts.sources ?? join(runtimeDir(), RUNNER_SOURCES);
|
|
53
|
-
const stamp = readFileSync(join(sources, STAMP), 'utf8');
|
|
54
55
|
const store = opts.store ?? runnerStore();
|
|
55
|
-
stageSources(sources, store, stamp);
|
|
56
56
|
const log = opts.log ??
|
|
57
57
|
((line) => {
|
|
58
58
|
process.stderr.write(`metro agent: ${line}\n`);
|
|
59
59
|
});
|
|
60
|
+
const waiting = () => {
|
|
61
|
+
log(`waiting for another preparation of ${store} to finish`);
|
|
62
|
+
};
|
|
63
|
+
return withLock(join(store, PREPARE_LOCK), { ...PREPARE_WAIT, waiting }, () => prepareIn(store, opts, log));
|
|
64
|
+
}
|
|
65
|
+
function prepareIn(store, opts, log) {
|
|
66
|
+
const sources = opts.sources ?? join(runtimeDir(), RUNNER_SOURCES);
|
|
67
|
+
const stamp = readFileSync(join(sources, STAMP), 'utf8');
|
|
68
|
+
stageSources(sources, store, stamp);
|
|
60
69
|
const pkg = opts.claude ?? claudePackage();
|
|
61
70
|
const claude = join(store, 'node_modules', ...pkg.name.split('/'), pkg.binary);
|
|
62
71
|
const marker = join(store, 'node_modules', '.metro-installed');
|
|
@@ -79,17 +88,12 @@ export function runnerEnv(key, port, model, mode, prompt, claude) {
|
|
|
79
88
|
...(prompt === null ? {} : { METRO_RUNNER_PROMPT: prompt }),
|
|
80
89
|
};
|
|
81
90
|
}
|
|
82
|
-
export function sdkGatewayEnv(env) {
|
|
83
|
-
const own = (env.ANTHROPIC_CUSTOM_HEADERS ?? '').trim();
|
|
84
|
-
const header = 'x-metro-runner: sdk';
|
|
85
|
-
return { ...env, ANTHROPIC_CUSTOM_HEADERS: own === '' ? header : `${own}\n${header}` };
|
|
86
|
-
}
|
|
87
91
|
function checkBinary(bin) {
|
|
88
|
-
const run = spawnSync(bin, ['--version'], { stdio: 'ignore', timeout: 30_000 });
|
|
92
|
+
const run = spawnSync(bin, ['--version'], { env: { ...process.env, ...NO_UPDATES }, stdio: 'ignore', timeout: 30_000 });
|
|
89
93
|
if (run.error !== undefined || run.status !== 0)
|
|
90
94
|
throw new Error(`Runner preflight failed: ${bin} cannot run (${run.error?.message ?? `exit ${String(run.status)}`}).`);
|
|
91
95
|
}
|
|
92
|
-
function
|
|
96
|
+
export function prepareAgentRunner(opts = {}) {
|
|
93
97
|
const prepared = prepareRunner(opts);
|
|
94
98
|
try {
|
|
95
99
|
checkBinary(prepared.claude);
|
|
@@ -99,30 +103,21 @@ function checkPreparedRunner(opts) {
|
|
|
99
103
|
throw err;
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
|
-
|
|
103
|
-
if (runner === 'cli')
|
|
104
|
-
checkBinary('claude');
|
|
105
|
-
else if (runner === 'sdk')
|
|
106
|
-
checkPreparedRunner(opts);
|
|
107
|
-
else
|
|
108
|
-
throw new Error('metro agent --prepare needs cli or sdk');
|
|
109
|
-
}
|
|
106
|
+
const prepareOnly = (args) => args[0] === '--prepare' && (args.length === 1 || (args.length === 2 && args[1] === 'sdk'));
|
|
110
107
|
export async function agentSession(args = []) {
|
|
111
|
-
if (args
|
|
112
|
-
prepareAgentRunner(
|
|
108
|
+
if (prepareOnly(args)) {
|
|
109
|
+
prepareAgentRunner();
|
|
113
110
|
return 0;
|
|
114
111
|
}
|
|
115
112
|
if (args.length > 0)
|
|
116
|
-
throw new Error('metro agent accepts only --prepare
|
|
113
|
+
throw new Error('metro agent accepts only --prepare');
|
|
117
114
|
process.chdir(homedir());
|
|
118
|
-
if (harnessRunner() !== 'sdk')
|
|
119
|
-
throw new Error(NOT_THE_RUNNER);
|
|
120
115
|
const agent = localAgent();
|
|
121
116
|
if (agent === null)
|
|
122
117
|
throw new Error('metro agent needs the agent of this machine, and there is none yet');
|
|
123
118
|
const bun = findBun();
|
|
124
119
|
const runner = prepareRunner({ bun });
|
|
125
120
|
process.stderr.write('metro agent: this agent runs as one Agent SDK session: a light front talks, background workers do the work\n');
|
|
126
|
-
const env = { ...
|
|
127
|
-
return
|
|
121
|
+
const env = { ...(await headlessEnv(runner.claude, 'metro agent')), ...runnerEnv(agent.key, localPort(), currentModel(), permissionMode(), systemPrompt(), runner.claude) };
|
|
122
|
+
return runChild(bun, [runner.entry], env);
|
|
128
123
|
}
|
package/dist/child.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
const KILL_GRACE_MS = 30_000;
|
|
3
|
+
function deadline(child, timeoutMs) {
|
|
4
|
+
if (timeoutMs === undefined)
|
|
5
|
+
return () => undefined;
|
|
6
|
+
let kill;
|
|
7
|
+
const term = setTimeout(() => {
|
|
8
|
+
child.kill('SIGTERM');
|
|
9
|
+
kill = setTimeout(() => {
|
|
10
|
+
child.kill('SIGKILL');
|
|
11
|
+
}, KILL_GRACE_MS);
|
|
12
|
+
}, timeoutMs);
|
|
13
|
+
return () => {
|
|
14
|
+
clearTimeout(term);
|
|
15
|
+
clearTimeout(kill);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function runChild(command, args, env, headless) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const leaveToChild = () => undefined;
|
|
21
|
+
process.on('SIGINT', leaveToChild);
|
|
22
|
+
process.on('SIGTERM', leaveToChild);
|
|
23
|
+
const child = spawn(command, args, { stdio: [headless === undefined ? 'inherit' : 'ignore', 'inherit', 'inherit'], env });
|
|
24
|
+
const stop = deadline(child, headless?.timeoutMs);
|
|
25
|
+
child.on('error', (err) => {
|
|
26
|
+
reject(new Error(err.code === 'ENOENT' ? `${command} was not found` : err.message));
|
|
27
|
+
});
|
|
28
|
+
child.on('exit', (code) => {
|
|
29
|
+
stop();
|
|
30
|
+
resolve(code ?? 1);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { currentRoute, routeModelEnv } from './route.js';
|
|
3
|
+
import { settingsConflicts, settingsFiles } from './claude-settings.js';
|
|
4
|
+
import { localAgent } from './local.js';
|
|
5
|
+
import { PROVIDER_FLAGS } from './provider-flags.js';
|
|
6
|
+
import { localPort, localUrl } from './runtime.js';
|
|
7
|
+
const KEY_HEADER = 'x-metro-key';
|
|
8
|
+
const PROBE_MS = 3_000;
|
|
9
|
+
const AUTH_STATUS_MS = 20_000;
|
|
10
|
+
const RM_PROMPT_OFF = 'CLAUDE_CODE_DISABLE_INLINE_SHELL_RM_PROMPT';
|
|
11
|
+
const OWN_CREDENTIALS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'];
|
|
12
|
+
export const AGENT_VIEW_OFF = 'CLAUDE_CODE_DISABLE_AGENT_VIEW';
|
|
13
|
+
export const NO_UPDATES = { DISABLE_AUTOUPDATER: '1' };
|
|
14
|
+
const set = (env, name) => (env[name] ?? '').trim() !== '';
|
|
15
|
+
export function pinnedBy(env) {
|
|
16
|
+
if (set(env, 'ANTHROPIC_BASE_URL'))
|
|
17
|
+
return 'ANTHROPIC_BASE_URL';
|
|
18
|
+
return PROVIDER_FLAGS.find((flag) => set(env, flag)) ?? null;
|
|
19
|
+
}
|
|
20
|
+
export function gatewayEnv(base, agentKey, port) {
|
|
21
|
+
if (agentKey === null || pinnedBy(base) !== null)
|
|
22
|
+
return base;
|
|
23
|
+
const own = (base.ANTHROPIC_CUSTOM_HEADERS ?? '').trim();
|
|
24
|
+
const mine = `${KEY_HEADER}: ${agentKey}`;
|
|
25
|
+
return {
|
|
26
|
+
...base,
|
|
27
|
+
ANTHROPIC_BASE_URL: `http://127.0.0.1:${String(port)}/gateway`,
|
|
28
|
+
ANTHROPIC_CUSTOM_HEADERS: own === '' ? mine : `${own}\n${mine}`,
|
|
29
|
+
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: '1',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export const toolSearchEnv = (env) => set(env, 'ENABLE_TOOL_SEARCH') ? env : { ...env, ENABLE_TOOL_SEARCH: 'true' };
|
|
33
|
+
export const rmPromptEnv = (env) => set(env, RM_PROMPT_OFF) ? env : { ...env, [RM_PROMPT_OFF]: '1' };
|
|
34
|
+
export const keepInSession = (env) => set(env, AGENT_VIEW_OFF) ? env : { ...env, [AGENT_VIEW_OFF]: '1' };
|
|
35
|
+
export function credentialEnv(env, agentKey, signedIn) {
|
|
36
|
+
if (signedIn || OWN_CREDENTIALS.some((name) => set(env, name)))
|
|
37
|
+
return env;
|
|
38
|
+
return { ...env, ANTHROPIC_AUTH_TOKEN: agentKey };
|
|
39
|
+
}
|
|
40
|
+
export function claudeSignedIn(claude, env = process.env) {
|
|
41
|
+
const run = spawnSync(claude, ['auth', 'status', '--json'], { encoding: 'utf8', env: { ...env, ...NO_UPDATES }, stdio: ['ignore', 'pipe', 'ignore'], timeout: AUTH_STATUS_MS });
|
|
42
|
+
if (run.error !== undefined || typeof run.stdout !== 'string')
|
|
43
|
+
return false;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(run.stdout);
|
|
46
|
+
return parsed.authenticated === true || parsed.loggedIn === true || typeof parsed.email === 'string' || typeof parsed.organization === 'string';
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function servingDaemon(body) {
|
|
53
|
+
if (typeof body !== 'object' || body === null)
|
|
54
|
+
return false;
|
|
55
|
+
const mode = body;
|
|
56
|
+
return mode.mode === 'local' && mode.stopped !== true;
|
|
57
|
+
}
|
|
58
|
+
async function daemonServing(base = localUrl()) {
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(`${base}/api/mode`, { signal: AbortSignal.timeout(PROBE_MS) });
|
|
61
|
+
return res.ok && servingDaemon(await res.json());
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function agentKey(agent) {
|
|
68
|
+
return agent === null ? { skip: 'no agent lives on this machine yet, so Claude Code talks to Anthropic directly' } : { key: agent.key };
|
|
69
|
+
}
|
|
70
|
+
async function verdict() {
|
|
71
|
+
const pinned = pinnedBy(process.env);
|
|
72
|
+
if (pinned !== null)
|
|
73
|
+
return { skip: `${pinned} is set, so Claude Code keeps talking to it` };
|
|
74
|
+
const conflicts = settingsConflicts(settingsFiles());
|
|
75
|
+
if (conflicts.length > 0)
|
|
76
|
+
return { skip: `a settings file pins the provider (${conflicts.join(', ')}), so Claude Code keeps it` };
|
|
77
|
+
const picked = agentKey(localAgent());
|
|
78
|
+
if ('skip' in picked)
|
|
79
|
+
return picked;
|
|
80
|
+
if (await daemonServing())
|
|
81
|
+
return picked;
|
|
82
|
+
return { skip: 'the daemon is not serving here (stopped, or not running), so Claude Code talks to Anthropic directly' };
|
|
83
|
+
}
|
|
84
|
+
function gatewayLaunchEnv(key, port, claude, say) {
|
|
85
|
+
say(`inference goes through the daemon at http://127.0.0.1:${String(port)}/gateway (the Model page decides where)`);
|
|
86
|
+
const pointed = routeModelEnv(gatewayEnv(process.env, key, port), currentRoute());
|
|
87
|
+
const routed = toolSearchEnv(pointed);
|
|
88
|
+
if (routed !== pointed)
|
|
89
|
+
say('MCP tool search is on, so connector tools load when Claude needs them instead of all at start');
|
|
90
|
+
const env = credentialEnv(routed, key, claudeSignedIn(claude));
|
|
91
|
+
if (env !== routed)
|
|
92
|
+
say("Claude Code has no login of its own here, so metro's key stands in as its credential; the Model page must route to Bedrock, OpenRouter or Codex");
|
|
93
|
+
return env;
|
|
94
|
+
}
|
|
95
|
+
export async function headlessEnv(claude, command) {
|
|
96
|
+
const say = (line) => {
|
|
97
|
+
process.stderr.write(`${command}: ${line}\n`);
|
|
98
|
+
};
|
|
99
|
+
const decision = await verdict();
|
|
100
|
+
if ('skip' in decision)
|
|
101
|
+
say(decision.skip);
|
|
102
|
+
const env = 'key' in decision ? gatewayLaunchEnv(decision.key, localPort(), claude, say) : process.env;
|
|
103
|
+
return { ...keepInSession(rmPromptEnv(env)), ...NO_UPDATES };
|
|
104
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -4,13 +4,11 @@ import { stopAll } from './control.js';
|
|
|
4
4
|
import { tailEvents } from './tail.js';
|
|
5
5
|
import { agentSession } from './agent.js';
|
|
6
6
|
import { automationCommand } from './automation.js';
|
|
7
|
-
import { launchClaude } from './claude.js';
|
|
8
7
|
import { memoryPaths, memoryRoutine } from './memory.js';
|
|
9
8
|
import { update } from './update.js';
|
|
10
9
|
import { serve } from './serve.js';
|
|
11
10
|
import { service, serviceStopHint } from './service.js';
|
|
12
11
|
import { currentVersion } from './version.js';
|
|
13
|
-
import { voiceSession } from './voice.js';
|
|
14
12
|
const USAGE = `metro: run your agent on this machine
|
|
15
13
|
|
|
16
14
|
metro serve [--port <n>] [--owner <organization id>]
|
|
@@ -31,16 +29,12 @@ const USAGE = `metro: run your agent on this machine
|
|
|
31
29
|
metro tail [agent-id]
|
|
32
30
|
follow this machine's inbound events, one JSON line each
|
|
33
31
|
metro whoami print the agent this machine runs
|
|
34
|
-
metro claude [args...]
|
|
35
|
-
open Claude Code with the metro channel and the metro MCP server, its
|
|
36
|
-
inference routed through the daemon's model gateway (the Model page);
|
|
37
|
-
every argument is passed through the same
|
|
38
32
|
metro agent run chat, calls and background workers in one Agent SDK conversation,
|
|
39
|
-
|
|
33
|
+
using the Model page's model through the daemon's model gateway;
|
|
40
34
|
resume it after a restart; call audio runs independently
|
|
41
|
-
metro agent --prepare
|
|
42
|
-
|
|
43
|
-
without starting a session or connecting to chat
|
|
35
|
+
metro agent --prepare
|
|
36
|
+
install the Agent SDK runtime and the Claude Code it bundles if needed,
|
|
37
|
+
and check that it runs, without starting a session or connecting to chat
|
|
44
38
|
metro task submit <routine> <prompt-file> [--slot <UTC ISO timestamp>] [--dry-run]
|
|
45
39
|
metro task status [routine]
|
|
46
40
|
metro task finish <uuid> <token> completed|blocked
|
|
@@ -50,15 +44,9 @@ const USAGE = `metro: run your agent on this machine
|
|
|
50
44
|
--dry-run writes nothing; local authorization is not fresh owner approval
|
|
51
45
|
metro memory [skill file]
|
|
52
46
|
run the memory routine once: when Claude Code transcripts changed since
|
|
53
|
-
the last run, Claude Code
|
|
54
|
-
files them into its memory, with no metro
|
|
55
|
-
the memory folder
|
|
56
|
-
metro voice --model <model>
|
|
57
|
-
the agent's side of a voice call, which the daemon starts when a call
|
|
58
|
-
comes in: Claude Code reads the call's words as stream-json on stdin and
|
|
59
|
-
streams its replies on stdout, with the agent's memory, skills, hooks,
|
|
60
|
-
connectors and metro tools (on /mcp/voice, so the chat session keeps its
|
|
61
|
-
slot) but no metro channel; tool approvals come back on stdout
|
|
47
|
+
the last run, the Claude Code bundled with the Agent SDK reads the memory
|
|
48
|
+
skill (or the file given) and files them into its memory, with no metro
|
|
49
|
+
tools and writes only inside the memory folder
|
|
62
50
|
metro update update to the newest published version (--check only reports)
|
|
63
51
|
metro version print this CLI's version
|
|
64
52
|
|
|
@@ -91,11 +79,9 @@ const COMMANDS = {
|
|
|
91
79
|
service: () => service(process.argv.slice(3)),
|
|
92
80
|
stop: stopDaemon,
|
|
93
81
|
tail: () => tailEvents(process.argv.slice(3)),
|
|
94
|
-
claude: () => launchClaude(process.argv.slice(3)),
|
|
95
82
|
agent: () => agentSession(process.argv.slice(3)),
|
|
96
83
|
task: () => automationCommand(process.argv.slice(3)),
|
|
97
84
|
memory: () => memoryRoutine(memoryPaths(process.env, process.argv[3])),
|
|
98
|
-
voice: () => voiceSession(process.argv.slice(3)),
|
|
99
85
|
update: () => update(process.argv.slice(3)),
|
|
100
86
|
version: async () => {
|
|
101
87
|
process.stdout.write(`${currentVersion()}\n`);
|
package/dist/lock.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { closeSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { readOrNull } from './runtime-install.js';
|
|
4
|
+
const pause = new Int32Array(new SharedArrayBuffer(4));
|
|
5
|
+
export function processAlive(pid) {
|
|
6
|
+
try {
|
|
7
|
+
process.kill(pid, 0);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
return err.code === 'EPERM';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function lockHeld(path, now, staleMs) {
|
|
15
|
+
try {
|
|
16
|
+
if (now - statSync(path).mtimeMs >= staleMs)
|
|
17
|
+
return false;
|
|
18
|
+
const pid = Number(readFileSync(path, 'utf8').trim());
|
|
19
|
+
return !Number.isInteger(pid) || pid <= 0 || processAlive(pid);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
if (err.code === 'ENOENT')
|
|
23
|
+
return false;
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function takeLock(path, staleMs, now = Date.now(), retry = true) {
|
|
28
|
+
try {
|
|
29
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
30
|
+
const fd = openSync(path, 'wx');
|
|
31
|
+
writeSync(fd, String(process.pid));
|
|
32
|
+
closeSync(fd);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (err.code !== 'EEXIST')
|
|
37
|
+
throw err;
|
|
38
|
+
if (!retry || lockHeld(path, now, staleMs))
|
|
39
|
+
return false;
|
|
40
|
+
rmSync(path, { force: true });
|
|
41
|
+
return takeLock(path, staleMs, now, false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function withLock(path, wait, run) {
|
|
45
|
+
const deadline = Date.now() + wait.waitMs;
|
|
46
|
+
let told = false;
|
|
47
|
+
while (!takeLock(path, wait.staleMs)) {
|
|
48
|
+
if (Date.now() >= deadline)
|
|
49
|
+
throw new Error(`Another process still holds ${path}; try again later.`);
|
|
50
|
+
if (!told)
|
|
51
|
+
wait.waiting?.();
|
|
52
|
+
told = true;
|
|
53
|
+
Atomics.wait(pause, 0, 0, wait.pollMs);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return run();
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (readOrNull(path)?.trim() === String(process.pid))
|
|
60
|
+
rmSync(path, { force: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/memory.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { prepareRunner } from './agent.js';
|
|
5
|
+
import { runChild } from './child.js';
|
|
6
|
+
import { headlessEnv } from './claude-env.js';
|
|
7
|
+
import { takeLock } from './lock.js';
|
|
6
8
|
import { systemPrompt } from './route.js';
|
|
7
9
|
export const MEMORY_FOLDERS = [
|
|
8
10
|
'entities/people',
|
|
@@ -24,6 +26,8 @@ const FINISHED = /^\s*status:\s*["']?(completed|parked|abandoned)["']?\s*$/m;
|
|
|
24
26
|
const LISTED_MAX = 60;
|
|
25
27
|
const WALK_DEPTH = 4;
|
|
26
28
|
const given = (value) => (typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined);
|
|
29
|
+
const claudeDir = (env) => given(env.CLAUDE_CONFIG_DIR) ?? join(given(env.HOME) ?? homedir(), '.claude');
|
|
30
|
+
export const projectDir = (dir, cwd) => join(dir, 'projects', cwd.replace(/[^A-Za-z0-9]/g, '-'));
|
|
27
31
|
function realHome(home) {
|
|
28
32
|
try {
|
|
29
33
|
return realpathSync(home);
|
|
@@ -122,27 +126,6 @@ export function routineArgs(prompt, memory, appended) {
|
|
|
122
126
|
];
|
|
123
127
|
}
|
|
124
128
|
export const routineEnv = (env) => ({ ...env, METRO_SESSION: 'memory' });
|
|
125
|
-
function lockHeld(path, now) {
|
|
126
|
-
const pid = Number(readFileSync(path, 'utf8').trim());
|
|
127
|
-
return now - statSync(path).mtimeMs < LOCK_STALE_MS && Number.isInteger(pid) && pid > 0 && processAlive(pid);
|
|
128
|
-
}
|
|
129
|
-
export function takeLock(path, now = Date.now(), retry = true) {
|
|
130
|
-
try {
|
|
131
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
132
|
-
const fd = openSync(path, 'wx');
|
|
133
|
-
writeSync(fd, String(process.pid));
|
|
134
|
-
closeSync(fd);
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
if (err.code !== 'EEXIST')
|
|
139
|
-
throw err;
|
|
140
|
-
if (!retry || lockHeld(path, now))
|
|
141
|
-
return false;
|
|
142
|
-
rmSync(path, { force: true });
|
|
143
|
-
return takeLock(path, now, false);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
129
|
const frontmatter = (text) => (text.startsWith('---\n') ? text.slice(4, text.indexOf('\n---', 4)) : '');
|
|
147
130
|
export function settleWorkstreams(memory) {
|
|
148
131
|
const active = join(memory, 'workstreams', 'active');
|
|
@@ -193,7 +176,9 @@ async function digest(paths, now) {
|
|
|
193
176
|
for (const folder of MEMORY_FOLDERS)
|
|
194
177
|
mkdirSync(join(paths.memory, folder), { recursive: true });
|
|
195
178
|
const prompt = routinePrompt(now, since, paths, files);
|
|
196
|
-
const
|
|
179
|
+
const { claude } = prepareRunner({ log: say });
|
|
180
|
+
const env = routineEnv(await headlessEnv(claude, 'metro memory'));
|
|
181
|
+
const code = await runChild(claude, routineArgs(prompt, paths.memory, systemPrompt()), env, { timeoutMs: TIMEOUT_MS });
|
|
197
182
|
if (code === 0) {
|
|
198
183
|
writeFileSync(paths.state, `${JSON.stringify({ lastRun: now.toISOString() })}\n`);
|
|
199
184
|
const moved = settleWorkstreams(paths.memory);
|
|
@@ -208,7 +193,7 @@ export async function memoryRoutine(paths = memoryPaths(), now = new Date()) {
|
|
|
208
193
|
say(`skipped: no memory skill at ${paths.skill}`);
|
|
209
194
|
return 0;
|
|
210
195
|
}
|
|
211
|
-
if (!takeLock(paths.lock)) {
|
|
196
|
+
if (!takeLock(paths.lock, LOCK_STALE_MS)) {
|
|
212
197
|
say('skipped: the previous run is still going');
|
|
213
198
|
return 0;
|
|
214
199
|
}
|
package/dist/root-release-io.js
CHANGED
|
@@ -4,8 +4,7 @@ import { X509Certificate } from 'node:crypto';
|
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
|
-
import { isRecord } from './
|
|
8
|
-
import { ISSUER, isVersion, PUBLISHER, RELEASE_DIR } from './root-release.js';
|
|
7
|
+
import { ISSUER, isRecord, isVersion, PUBLISHER, RELEASE_DIR } from './root-release.js';
|
|
9
8
|
import { PACKAGE_NAME } from './version.js';
|
|
10
9
|
const REGISTRY = 'https://registry.npmjs.org';
|
|
11
10
|
const ENCODED = PACKAGE_NAME.replace('/', '%2F');
|
package/dist/root-release.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { isRecord } from './background.js';
|
|
3
2
|
import { compareVersions, PACKAGE_NAME } from './version.js';
|
|
4
3
|
export const RELEASE_DIR = '/usr/local/lib/metro/release';
|
|
5
4
|
export const PUBLISHER = 'https://github.com/bonustrack/metro/.github/workflows/publish-cli.yml@refs/heads/main';
|
|
@@ -8,6 +7,7 @@ const REGISTRY = 'https://registry.npmjs.org';
|
|
|
8
7
|
const PROVENANCE = 'https://slsa.dev/provenance/v1';
|
|
9
8
|
const IN_TOTO = 'application/vnd.in-toto+json';
|
|
10
9
|
const VERSION_RE = /^\d{1,9}\.\d{1,9}\.\d{1,9}(?:-[0-9A-Za-z]{1,20}(?:\.[0-9A-Za-z]{1,20}){0,4})?$/;
|
|
10
|
+
export const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
11
11
|
export const isVersion = (raw) => VERSION_RE.test(raw);
|
|
12
12
|
export const tarballUrl = (version) => `${REGISTRY}/${PACKAGE_NAME}/-/metro-${version}.tgz`;
|
|
13
13
|
const subjectName = (version) => `pkg:npm/${PACKAGE_NAME.replace('@', '%40')}@${version}`;
|
package/dist/route.js
CHANGED
|
@@ -51,7 +51,6 @@ function setupField(dir, field) {
|
|
|
51
51
|
return undefined;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
-
export const harnessRunner = (dir = agentsDir()) => (setupField(dir, 'runner') === 'sdk' ? 'sdk' : 'cli');
|
|
55
54
|
export function systemPrompt(dir = agentsDir()) {
|
|
56
55
|
const path = join(dir, 'system-prompt.md');
|
|
57
56
|
if (!existsSync(path))
|
package/hooks/guard.mjs
CHANGED
|
@@ -26,7 +26,6 @@ const IMAGE = /\.(png|jpe?g|gif|webp|bmp)$/i;
|
|
|
26
26
|
const MCP = /^mcp__/;
|
|
27
27
|
const METRO_MCP = /^mcp__metro__/;
|
|
28
28
|
const CONNECTOR_CALL = 'mcp__metro__tool_call';
|
|
29
|
-
const VOICE = process.env.METRO_SESSION === 'voice';
|
|
30
29
|
|
|
31
30
|
const connectorWork = (tool) =>
|
|
32
31
|
`A connector call is work, not orchestration: ${tool} reads somebody's files, mail or records, and its answer can be long enough to crowd out the chat you are here to follow. Only metro's own tools run on this thread. Delegate it to a subagent with the Agent tool (run_in_background: true; the worker reaches every connector) and relay what it finds.`;
|
|
@@ -55,10 +54,7 @@ function verdict(payload, input) {
|
|
|
55
54
|
return `${tool} blocks the session waiting on the terminal, and nobody is watching it. Decide with your best judgement and state the assumption, or ask over chat with the metro tools and keep working in the meantime.`;
|
|
56
55
|
if (typeof payload.agent_id === 'string' && payload.agent_id !== '') return null;
|
|
57
56
|
if (tool === 'Agent' && input.run_in_background === false)
|
|
58
|
-
return
|
|
59
|
-
? 'On a voice call a foreground subagent leaves the caller in silence until it finishes. Re-issue the identical Agent call with run_in_background: true, tell the caller you will come back to it, and keep talking.'
|
|
60
|
-
: 'A foreground subagent blinds the main thread: it cannot see or acknowledge inbound metro messages until the agent finishes. Re-issue the identical Agent call with run_in_background: true, then wait for the task notification.';
|
|
61
|
-
if (VOICE) return null;
|
|
57
|
+
return 'A foreground subagent blinds the main thread: it cannot see or acknowledge inbound metro messages until the agent finishes. Re-issue the identical Agent call with run_in_background: true, then wait for the task notification.';
|
|
62
58
|
if (ORCHESTRATION.has(tool)) return null;
|
|
63
59
|
if (tool === CONNECTOR_CALL) return connectorWork(tool);
|
|
64
60
|
if (METRO_MCP.test(tool)) return null;
|
package/hooks/session-start.mjs
CHANGED
|
@@ -24,7 +24,7 @@ function guidance() {
|
|
|
24
24
|
return '';
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const text =
|
|
27
|
+
const text = process.env.METRO_SESSION === 'memory' ? '' : guidance();
|
|
28
28
|
if (text !== '')
|
|
29
29
|
process.stdout.write(
|
|
30
30
|
`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: `This machine runs as a metro agent. Standing rules, kept in the metro skill:\n\n${text}` } })}\n`,
|
package/package.json
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync } from './agent-fs.js';
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import { chmodSync } from 'node:fs';
|
|
5
4
|
import { homedir } from 'node:os';
|
|
6
5
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
7
|
-
import { AGENT_NAME, agentUser, agentUserExpected, asUser,
|
|
6
|
+
import { AGENT_NAME, agentUser, agentUserExpected, asUser, forgetAgentUser, type AgentUser } from './user.js';
|
|
8
7
|
import { watchAgentView } from './view.js';
|
|
9
8
|
import { closeMetroHome } from './metro-home.js';
|
|
10
9
|
import { mustHelper } from '../metro-user/privilege.js';
|
|
11
|
-
import {
|
|
12
|
-
import { pinClaudeCode } from '../claude/version.js';
|
|
13
|
-
|
|
14
|
-
const INSTALL_MS = 10 * 60_000;
|
|
15
|
-
const INSTALLER = `curl -fsSL https://claude.ai/install.sh | bash -s ${CLAUDE_CODE_VERSION}`;
|
|
10
|
+
import { prepareRunnerStore } from '../claude/session.js';
|
|
16
11
|
|
|
17
12
|
export type Provisioned = 'off' | 'failed' | 'ready';
|
|
18
13
|
|
|
@@ -39,40 +34,11 @@ function ensureUser(): AgentUser | null {
|
|
|
39
34
|
return user;
|
|
40
35
|
}
|
|
41
36
|
|
|
42
|
-
function
|
|
43
|
-
if (existsSync(claudeBin(user))) return Promise.resolve();
|
|
44
|
-
const [file, args] = asUser(user, 'bash', ['-c', INSTALLER]);
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
const child = spawn(file, args, { stdio: ['ignore', 'ignore', 'pipe'], cwd: '/', timeout: INSTALL_MS });
|
|
47
|
-
let said = '';
|
|
48
|
-
child.stderr.on('data', (chunk: Buffer) => {
|
|
49
|
-
said = `${said}${chunk.toString('utf8')}`.slice(-2000);
|
|
50
|
-
});
|
|
51
|
-
child.on('error', reject);
|
|
52
|
-
child.on('close', (code) => {
|
|
53
|
-
if (code === 0 && existsSync(claudeBin(user))) {
|
|
54
|
-
log.info({ user: user.name }, 'agent-user: installed Claude Code for the agent user');
|
|
55
|
-
resolve();
|
|
56
|
-
} else reject(new Error(`the Claude Code installer failed: ${said.trim() || `exit ${String(code)}`}`));
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function pinCheckedClaude(): void {
|
|
62
|
-
try {
|
|
63
|
-
pinClaudeCode();
|
|
64
|
-
} catch (err) {
|
|
65
|
-
log.warn({ want: CLAUDE_CODE_VERSION, err: errMsg(err) }, 'agent-user: could not install the Claude Code version metro was checked with; keeping the installed one');
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export async function provisionAgentUser(): Promise<Provisioned> {
|
|
37
|
+
export function provisionAgentUser(): Provisioned {
|
|
70
38
|
if (!agentUserExpected()) return 'off';
|
|
71
39
|
try {
|
|
72
40
|
const user = ensureUser();
|
|
73
41
|
if (user === null) throw new Error(`the user ${AGENT_NAME} could not be found after creating it`);
|
|
74
|
-
await installClaude(user);
|
|
75
|
-
pinCheckedClaude();
|
|
76
42
|
watchAgentView();
|
|
77
43
|
return 'ready';
|
|
78
44
|
} catch (err) {
|
|
@@ -80,3 +46,12 @@ export async function provisionAgentUser(): Promise<Provisioned> {
|
|
|
80
46
|
return 'failed';
|
|
81
47
|
}
|
|
82
48
|
}
|
|
49
|
+
|
|
50
|
+
export async function prepareAgentRunner(): Promise<void> {
|
|
51
|
+
try {
|
|
52
|
+
await prepareRunnerStore();
|
|
53
|
+
log.info('agent-user: the Agent SDK runner and the Claude Code it bundles are ready for the agent user');
|
|
54
|
+
} catch (err) {
|
|
55
|
+
log.error({ err: errMsg(err) }, 'agent-user: could not prepare the Agent SDK runner; the session start runs metro agent, which prepares it again');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
2
4
|
import { join } from 'node:path';
|
|
3
5
|
import { runningAsMetro } from '../metro-user/privilege.js';
|
|
4
6
|
|
|
@@ -81,8 +83,26 @@ export const agentCommand = (command: readonly string[], extra: Record<string, s
|
|
|
81
83
|
return [bin, ...argv];
|
|
82
84
|
};
|
|
83
85
|
|
|
86
|
+
const SDK_PACKAGE = '@anthropic-ai/claude-agent-sdk';
|
|
87
|
+
|
|
88
|
+
export const CLAUDE_ENV: Readonly<Record<string, string>> = { DISABLE_AUTOUPDATER: '1' };
|
|
89
|
+
|
|
90
|
+
let musl: boolean | null = null;
|
|
91
|
+
|
|
92
|
+
function muslFirst(): boolean {
|
|
93
|
+
if (process.platform !== 'linux') return false;
|
|
94
|
+
if (musl === null) {
|
|
95
|
+
const report = process.report.getReport() as { header?: { glibcVersionRuntime?: string } };
|
|
96
|
+
musl = report.header?.glibcVersionRuntime === undefined;
|
|
97
|
+
}
|
|
98
|
+
return musl;
|
|
99
|
+
}
|
|
100
|
+
|
|
84
101
|
export function claudeBin(user = agentUser()): string {
|
|
85
|
-
|
|
102
|
+
const name = `${SDK_PACKAGE}-${process.platform}-${process.arch}`;
|
|
103
|
+
const packages = muslFirst() ? [`${name}-musl`, name] : [name, `${name}-musl`];
|
|
104
|
+
const [first = '', other = ''] = packages.map((pkg) => join(user?.home ?? homedir(), '.metro', 'sdk-runner', 'node_modules', pkg, 'claude'));
|
|
105
|
+
return existsSync(first) || !existsSync(other) ? first : other;
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
export const claudeHome = (user = agentUser()): string | null => (user === null ? null : user.home);
|