@stage-labs/metro 0.1.0-beta.100 → 0.1.0-beta.102
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/claude.js +40 -11
- package/dist/mcp-config.js +25 -0
- package/dist/onboarding.js +29 -0
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +3 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +34 -0
- package/runtime/runtime.json +1 -1
package/dist/claude.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { markOnboardingDone } from './onboarding.js';
|
|
3
|
+
import { writeMcpConfig } from './mcp-config.js';
|
|
2
4
|
import { settingsConflicts, settingsFiles } from './claude-settings.js';
|
|
3
5
|
import { localAgents, pickLocalAgent } from './local.js';
|
|
4
6
|
import { PROVIDER_FLAGS } from './provider-flags.js';
|
|
@@ -6,8 +8,9 @@ import { localPort, localUrl } from './runtime.js';
|
|
|
6
8
|
const CHANNEL_FLAGS = ['--dangerously-load-development-channels', 'server:metro'];
|
|
7
9
|
const KEY_HEADER = 'x-metro-key';
|
|
8
10
|
const PROBE_MS = 3_000;
|
|
9
|
-
export const claudeArgs = (extra) => [
|
|
11
|
+
export const claudeArgs = (extra, mcpConfig) => [
|
|
10
12
|
...CHANNEL_FLAGS,
|
|
13
|
+
...(mcpConfig === undefined ? [] : ['--mcp-config', mcpConfig]),
|
|
11
14
|
...extra,
|
|
12
15
|
];
|
|
13
16
|
const set = (env, name) => (env[name] ?? '').trim() !== '';
|
|
@@ -113,17 +116,43 @@ export function runClaude(args, env) {
|
|
|
113
116
|
});
|
|
114
117
|
});
|
|
115
118
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
async function servedKey(decision) {
|
|
120
|
+
if ('key' in decision)
|
|
121
|
+
return decision.key;
|
|
122
|
+
const picked = agentKey(localAgentList(), process.env.METRO_AGENT);
|
|
123
|
+
if ('skip' in picked)
|
|
124
|
+
return null;
|
|
125
|
+
return (await daemonServing()) ? picked.key : null;
|
|
126
|
+
}
|
|
127
|
+
function mcpConfigFor(key, port) {
|
|
128
|
+
if (key === null)
|
|
129
|
+
return null;
|
|
130
|
+
const file = writeMcpConfig(key, port);
|
|
131
|
+
process.stderr.write(`metro claude: the metro MCP server is loaded for this session from the daemon at http://127.0.0.1:${String(port)}/mcp, nothing to add by hand\n`);
|
|
132
|
+
return file;
|
|
133
|
+
}
|
|
134
|
+
function gatewayLaunchEnv(key, port) {
|
|
123
135
|
process.stderr.write(`metro claude: inference goes through the daemon at http://127.0.0.1:${String(port)}/gateway (the Model page decides where)\n`);
|
|
124
|
-
const routed = gatewayEnv(process.env,
|
|
125
|
-
const env = credentialEnv(routed,
|
|
136
|
+
const routed = gatewayEnv(process.env, key, port);
|
|
137
|
+
const env = credentialEnv(routed, key, claudeSignedIn());
|
|
126
138
|
if (env !== routed)
|
|
127
139
|
process.stderr.write("metro claude: 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\n");
|
|
128
|
-
|
|
140
|
+
if (markOnboardingDone() === 'marked')
|
|
141
|
+
process.stderr.write("metro claude: skipped Claude Code's first-run setup, since it already has a credential here\n");
|
|
142
|
+
return env;
|
|
143
|
+
}
|
|
144
|
+
export async function launchClaude(extra) {
|
|
145
|
+
const decision = await verdict();
|
|
146
|
+
const port = localPort();
|
|
147
|
+
const mcp = mcpConfigFor(await servedKey(decision), port);
|
|
148
|
+
try {
|
|
149
|
+
if ('skip' in decision) {
|
|
150
|
+
process.stderr.write(`metro claude: ${decision.skip}\n`);
|
|
151
|
+
return await runClaude(claudeArgs(extra, mcp?.path), process.env);
|
|
152
|
+
}
|
|
153
|
+
return await runClaude(claudeArgs(extra, mcp?.path), gatewayLaunchEnv(decision.key, port));
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
mcp?.cleanup();
|
|
157
|
+
}
|
|
129
158
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const METRO_SERVER = 'metro';
|
|
5
|
+
export function metroMcpConfig(agentKey, port) {
|
|
6
|
+
const servers = {
|
|
7
|
+
[METRO_SERVER]: {
|
|
8
|
+
type: 'http',
|
|
9
|
+
url: `http://127.0.0.1:${String(port)}/mcp`,
|
|
10
|
+
headers: { Authorization: `Bearer ${agentKey}` },
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
return `${JSON.stringify({ mcpServers: servers }, null, 2)}\n`;
|
|
14
|
+
}
|
|
15
|
+
export function writeMcpConfig(agentKey, port, base = tmpdir()) {
|
|
16
|
+
const dir = mkdtempSync(join(base, 'metro-claude-'));
|
|
17
|
+
const path = join(dir, 'mcp.json');
|
|
18
|
+
writeFileSync(path, metroMcpConfig(agentKey, port), { mode: 0o600 });
|
|
19
|
+
return {
|
|
20
|
+
path,
|
|
21
|
+
cleanup: () => {
|
|
22
|
+
rmSync(dir, { recursive: true, force: true });
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const FILE = '.claude.json';
|
|
5
|
+
const FLAG = 'hasCompletedOnboarding';
|
|
6
|
+
const given = (value) => value !== undefined && value.trim() !== '' ? value.trim() : undefined;
|
|
7
|
+
export function claudeConfigPath(env = process.env) {
|
|
8
|
+
return join(given(env.CLAUDE_CONFIG_DIR) ?? given(env.HOME) ?? homedir(), FILE);
|
|
9
|
+
}
|
|
10
|
+
function readConfig(path) {
|
|
11
|
+
if (!existsSync(path))
|
|
12
|
+
return {};
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
15
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function markOnboardingDone(path = claudeConfigPath()) {
|
|
22
|
+
const config = readConfig(path);
|
|
23
|
+
if (config === null)
|
|
24
|
+
return 'unreadable';
|
|
25
|
+
if (config[FLAG] === true)
|
|
26
|
+
return 'already';
|
|
27
|
+
writeFileSync(path, `${JSON.stringify({ ...config, [FLAG]: true }, null, 2)}\n`, { mode: 0o600 });
|
|
28
|
+
return 'marked';
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.102",
|
|
4
4
|
"description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.102",
|
|
4
4
|
"description": "Metro connectors for Claude Code. Runs beside the metro daemon on this machine: each connector the agent holds is one MCP server relayed through the daemon, the daemon keeps that list current on its own, and /reload-plugins picks up a new one without leaving your session. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { markOnboardingDone } from './onboarding.js';
|
|
3
4
|
import { ApiError } from '@metro-labs/http/api-error';
|
|
4
5
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
5
6
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
@@ -125,7 +126,8 @@ export function startClaudeLogin(deps: LoginDeps = {}, now = Date.now()): LoginV
|
|
|
125
126
|
session.state = code === 0 ? 'done' : 'failed';
|
|
126
127
|
if (code !== 0) session.error = `the login ended with status ${String(code)}`;
|
|
127
128
|
terminal.close();
|
|
128
|
-
|
|
129
|
+
const onboarding = code === 0 ? markOnboardingDone() : 'skipped';
|
|
130
|
+
log.info({ state: session.state, onboarding }, 'claude-login: the official login finished');
|
|
129
131
|
})
|
|
130
132
|
.catch((err: unknown) => {
|
|
131
133
|
session.state = 'failed';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
5
|
+
|
|
6
|
+
const FILE = '.claude.json';
|
|
7
|
+
const FLAG = 'hasCompletedOnboarding';
|
|
8
|
+
|
|
9
|
+
export type OnboardingMark = 'marked' | 'already' | 'unreadable';
|
|
10
|
+
|
|
11
|
+
const given = (value: string | undefined): string | undefined =>
|
|
12
|
+
value !== undefined && value.trim() !== '' ? value.trim() : undefined;
|
|
13
|
+
|
|
14
|
+
export function claudeConfigPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
15
|
+
return join(given(env.CLAUDE_CONFIG_DIR) ?? given(env.HOME) ?? homedir(), FILE);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readConfig(path: string): Record<string, unknown> | null {
|
|
19
|
+
if (!existsSync(path)) return {};
|
|
20
|
+
try {
|
|
21
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
22
|
+
return isRecord(parsed) ? parsed : null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function markOnboardingDone(path = claudeConfigPath()): OnboardingMark {
|
|
29
|
+
const config = readConfig(path);
|
|
30
|
+
if (config === null) return 'unreadable';
|
|
31
|
+
if (config[FLAG] === true) return 'already';
|
|
32
|
+
writeFileSync(path, `${JSON.stringify({ ...config, [FLAG]: true }, null, 2)}\n`, { mode: 0o600 });
|
|
33
|
+
return 'marked';
|
|
34
|
+
}
|
package/runtime/runtime.json
CHANGED