@stage-labs/metro 0.1.0-beta.81 → 0.1.0-beta.83
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
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { settingsConflicts, settingsFiles } from './claude-settings.js';
|
|
3
3
|
import { localAgents, pickLocalAgent } from './local.js';
|
|
4
4
|
import { PROVIDER_FLAGS } from './provider-flags.js';
|
|
@@ -28,6 +28,24 @@ export function gatewayEnv(base, agentKey, port) {
|
|
|
28
28
|
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: '1',
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
const OWN_CREDENTIALS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'];
|
|
32
|
+
export function credentialEnv(env, agentKey, signedIn) {
|
|
33
|
+
if (signedIn || OWN_CREDENTIALS.some((name) => set(env, name)))
|
|
34
|
+
return env;
|
|
35
|
+
return { ...env, ANTHROPIC_AUTH_TOKEN: agentKey };
|
|
36
|
+
}
|
|
37
|
+
export function claudeSignedIn() {
|
|
38
|
+
const run = spawnSync('claude', ['auth', 'status', '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
39
|
+
if (run.error !== undefined || typeof run.stdout !== 'string')
|
|
40
|
+
return false;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(run.stdout);
|
|
43
|
+
return parsed.authenticated === true || parsed.loggedIn === true || typeof parsed.email === 'string' || typeof parsed.organization === 'string';
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
31
49
|
export function servingDaemon(body) {
|
|
32
50
|
if (typeof body !== 'object' || body === null)
|
|
33
51
|
return false;
|
|
@@ -103,5 +121,9 @@ export async function launchClaude(extra) {
|
|
|
103
121
|
}
|
|
104
122
|
const port = localPort();
|
|
105
123
|
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`);
|
|
106
|
-
|
|
124
|
+
const routed = gatewayEnv(process.env, decision.key, port);
|
|
125
|
+
const env = credentialEnv(routed, decision.key, claudeSignedIn());
|
|
126
|
+
if (env !== routed)
|
|
127
|
+
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
|
+
return runClaude(claudeArgs(extra), env);
|
|
107
129
|
}
|
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.83",
|
|
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": {
|
|
@@ -70,6 +70,11 @@ function requestedModel(body: Record<string, unknown>): string {
|
|
|
70
70
|
return model;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
const standsInFor = (req: IncomingMessage): boolean => {
|
|
74
|
+
const metroKey = req.headers['x-metro-key'];
|
|
75
|
+
return typeof metroKey === 'string' && metroKey !== '' && req.headers.authorization === `Bearer ${metroKey}`;
|
|
76
|
+
};
|
|
77
|
+
|
|
73
78
|
async function toAnthropic(
|
|
74
79
|
req: IncomingMessage,
|
|
75
80
|
res: ServerResponse,
|
|
@@ -81,8 +86,14 @@ async function toAnthropic(
|
|
|
81
86
|
const explicit = typeof body.model === 'string' && body.model !== route.model;
|
|
82
87
|
const payload = explicit ? Buffer.from(JSON.stringify({ ...body, model: route.model })) : raw;
|
|
83
88
|
const url = `${deps.anthropicBase ?? ANTHROPIC_BASE}${(req.url ?? '').slice(GATEWAY_PREFIX.length)}`;
|
|
84
|
-
const watch = watchUpstream(res);
|
|
85
89
|
const key = deps.config().anthropic.apiKey;
|
|
90
|
+
if (key === '' && standsInFor(req))
|
|
91
|
+
throw new GatewayError(
|
|
92
|
+
403,
|
|
93
|
+
'permission_error',
|
|
94
|
+
'Claude Code on this machine has no Anthropic login of its own; choose Bedrock, OpenRouter or Codex on the Model page, or sign in on the Claude tab',
|
|
95
|
+
);
|
|
96
|
+
const watch = watchUpstream(res);
|
|
86
97
|
const upstream = await fetch(url, {
|
|
87
98
|
method: 'POST',
|
|
88
99
|
headers: key === '' ? forwardedHeaders(req) : anthropicHeaders(req, key),
|
|
@@ -7,7 +7,6 @@ import { mintTerminalTicket } from './tickets.js';
|
|
|
7
7
|
|
|
8
8
|
const PREFIX = '/api/terminal';
|
|
9
9
|
const TICKETS = `${PREFIX}/tickets`;
|
|
10
|
-
export const TMUX_SESSION = 'metro';
|
|
11
10
|
const SESSION_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
|
|
12
11
|
|
|
13
12
|
export const tmuxCommand = (session: string): string[] => ['tmux', 'new-session', '-A', '-D', '-s', session, ';', 'set-option', '-g', 'mouse', 'on'];
|
|
@@ -33,7 +32,8 @@ export function tmuxSessions(): string[] {
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
export function sessionOf(raw: unknown): string {
|
|
36
|
-
if (raw === undefined || raw === null || raw === '')
|
|
35
|
+
if (raw === undefined || raw === null || raw === '')
|
|
36
|
+
throw new ApiError('name the tmux session to open; GET /api/terminal lists the ones that exist', 400);
|
|
37
37
|
if (typeof raw !== 'string' || !SESSION_RE.test(raw))
|
|
38
38
|
throw new ApiError('a session name is 1 to 32 letters, digits, dots, dashes or underscores', 400);
|
|
39
39
|
return raw;
|
|
@@ -56,7 +56,7 @@ export function handleTerminalRequest(req: IncomingMessage, res: ServerResponse,
|
|
|
56
56
|
if (!session) throw new ApiError('unauthorized', 401);
|
|
57
57
|
deps.authorize(session.subject);
|
|
58
58
|
if (path === PREFIX) {
|
|
59
|
-
sendJson(req, res, 200, { available: tmuxAvailable(deps),
|
|
59
|
+
sendJson(req, res, 200, { available: tmuxAvailable(deps), sessions: tmuxSessions() });
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
const wanted = sessionOf(bodyField(await readJsonBody(req), 'session'));
|
package/runtime/runtime.json
CHANGED