@stage-labs/metro 0.1.0-beta.51 → 0.1.0-beta.52
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/cli.js +2 -2
- package/dist/runtime.js +16 -3
- package/dist/serve.js +3 -2
- package/dist/update.js +13 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +7 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +3 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +119 -0
- package/runtime/runtime.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ const USAGE = `metro — run your agent on this machine
|
|
|
31
31
|
metro bedrock [args...]
|
|
32
32
|
the same, with inference on Amazon Bedrock through a local proxy so the
|
|
33
33
|
channel still works (needs AWS_BEARER_TOKEN_BEDROCK and AWS_REGION)
|
|
34
|
-
metro update update to the newest published version
|
|
34
|
+
metro update update to the newest published version (--check only reports)
|
|
35
35
|
metro version print this CLI's version
|
|
36
36
|
|
|
37
37
|
Start Claude Code with every connector, writing nothing to disk:
|
|
@@ -73,7 +73,7 @@ const COMMANDS = {
|
|
|
73
73
|
plugin: installPlugin,
|
|
74
74
|
claude: () => launchClaude(process.argv.slice(3)),
|
|
75
75
|
bedrock: () => bedrock(process.argv.slice(3)),
|
|
76
|
-
update,
|
|
76
|
+
update: () => update(process.argv.slice(3)),
|
|
77
77
|
version: async () => {
|
|
78
78
|
process.stdout.write(`${currentVersion()}\n`);
|
|
79
79
|
return Promise.resolve(0);
|
package/dist/runtime.js
CHANGED
|
@@ -31,7 +31,8 @@ export function findBun() {
|
|
|
31
31
|
}
|
|
32
32
|
export const localPort = () => Number(process.env.METRO_WEBHOOK_PORT) || 8420;
|
|
33
33
|
export const localUrl = () => `http://127.0.0.1:${String(localPort())}`;
|
|
34
|
-
export
|
|
34
|
+
export const RESTART_CODE = 75;
|
|
35
|
+
function spawnOnce(plan) {
|
|
35
36
|
const child = spawn(plan.command, plan.args, {
|
|
36
37
|
cwd: plan.cwd,
|
|
37
38
|
env: plan.env,
|
|
@@ -40,11 +41,23 @@ export function spawnPlan(plan) {
|
|
|
40
41
|
const relay = (sig) => () => {
|
|
41
42
|
child.kill(sig);
|
|
42
43
|
};
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
const onInt = relay('SIGINT');
|
|
45
|
+
const onTerm = relay('SIGTERM');
|
|
46
|
+
process.on('SIGINT', onInt);
|
|
47
|
+
process.on('SIGTERM', onTerm);
|
|
45
48
|
return new Promise((resolve) => {
|
|
46
49
|
child.on('exit', (code) => {
|
|
50
|
+
process.off('SIGINT', onInt);
|
|
51
|
+
process.off('SIGTERM', onTerm);
|
|
47
52
|
resolve(code ?? 0);
|
|
48
53
|
});
|
|
49
54
|
});
|
|
50
55
|
}
|
|
56
|
+
export async function spawnPlan(plan) {
|
|
57
|
+
for (;;) {
|
|
58
|
+
const code = await spawnOnce(plan());
|
|
59
|
+
if (code !== RESTART_CODE)
|
|
60
|
+
return code;
|
|
61
|
+
process.stderr.write('metro was updated; restarting the daemon on the new version\n');
|
|
62
|
+
}
|
|
63
|
+
}
|
package/dist/serve.js
CHANGED
|
@@ -85,6 +85,7 @@ export function servePlan(opts) {
|
|
|
85
85
|
...env,
|
|
86
86
|
METRO_MODE: 'local',
|
|
87
87
|
METRO_VERSION: currentVersion(),
|
|
88
|
+
METRO_CLI_BIN: process.argv[1] ?? '',
|
|
88
89
|
METRO_WEBHOOK_PORT: String(opts.port),
|
|
89
90
|
METRO_HTTP_HOST: process.env.METRO_HTTP_HOST ?? '127.0.0.1',
|
|
90
91
|
METRO_TRAINS_DIR: join(opts.dir, 'trains'),
|
|
@@ -103,7 +104,7 @@ export function serve(argv) {
|
|
|
103
104
|
requireOwner(owner);
|
|
104
105
|
if (tunnel)
|
|
105
106
|
findCloudflared();
|
|
106
|
-
const
|
|
107
|
+
const dir = runtimeDir();
|
|
107
108
|
process.stderr.write(`Starting a metro daemon of your own on http://127.0.0.1:${String(port)}\n`);
|
|
108
|
-
return spawnPlan(
|
|
109
|
+
return spawnPlan(() => servePlan({ dir, port, tunnel, owner }));
|
|
109
110
|
}
|
package/dist/update.js
CHANGED
|
@@ -9,7 +9,19 @@ export function installArgs(manager, version) {
|
|
|
9
9
|
? ['add', '--global', spec]
|
|
10
10
|
: ['install', '--global', spec];
|
|
11
11
|
}
|
|
12
|
-
export
|
|
12
|
+
export const checkReport = (current, latest) => ({
|
|
13
|
+
current,
|
|
14
|
+
latest,
|
|
15
|
+
newer: latest !== '' && isNewer(latest, current),
|
|
16
|
+
});
|
|
17
|
+
export async function updateCheck() {
|
|
18
|
+
return checkReport(currentVersion(), await publishedVersion());
|
|
19
|
+
}
|
|
20
|
+
export async function update(argv = []) {
|
|
21
|
+
if (argv.includes('--check')) {
|
|
22
|
+
process.stdout.write(`${JSON.stringify(await updateCheck())}\n`);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
13
25
|
const current = currentVersion();
|
|
14
26
|
const newest = await publishedVersion();
|
|
15
27
|
if (newest === '') {
|
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.52",
|
|
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": {
|
|
@@ -110,6 +110,10 @@ function sessionApis(): SessionApis {
|
|
|
110
110
|
if (isLocalMode())
|
|
111
111
|
return localSessionApis({
|
|
112
112
|
syncStations,
|
|
113
|
+
restart: () => {
|
|
114
|
+
exitCode = RESTART_EXIT;
|
|
115
|
+
onShutdown();
|
|
116
|
+
},
|
|
113
117
|
closeAgentSession,
|
|
114
118
|
gatherAccounts: gatherAccountsForAgents,
|
|
115
119
|
capabilities: accountStationCapabilities,
|
|
@@ -153,6 +157,8 @@ async function main(): Promise<void> {
|
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
const SHUTDOWN_TIMEOUT_MS = 3_000;
|
|
160
|
+
const RESTART_EXIT = 75;
|
|
161
|
+
let exitCode = 0;
|
|
156
162
|
let shuttingDown = false;
|
|
157
163
|
async function shutdown(): Promise<void> {
|
|
158
164
|
if (shuttingDown) return;
|
|
@@ -173,7 +179,7 @@ async function shutdown(): Promise<void> {
|
|
|
173
179
|
]);
|
|
174
180
|
}
|
|
175
181
|
await supervisor.stop();
|
|
176
|
-
process.exit(
|
|
182
|
+
process.exit(exitCode);
|
|
177
183
|
}
|
|
178
184
|
const onShutdown = (): void => {
|
|
179
185
|
shutdown().catch((err: unknown) => {
|
|
@@ -52,6 +52,7 @@ import type { StationName } from '../db/schema.js';
|
|
|
52
52
|
|
|
53
53
|
export interface LocalModeDeps {
|
|
54
54
|
syncStations: (station: StationName) => Promise<void>;
|
|
55
|
+
restart: () => void;
|
|
55
56
|
closeAgentSession: (id: string) => Promise<boolean>;
|
|
56
57
|
gatherAccounts: AgentApiDeps['gatherAccounts'];
|
|
57
58
|
capabilities: AgentApiDeps['capabilities'];
|
|
@@ -189,6 +190,7 @@ export function localSessionApis(deps: LocalModeDeps): SessionApis {
|
|
|
189
190
|
relayApi,
|
|
190
191
|
localCli,
|
|
191
192
|
claudeApi: { authorize: (subject) => { assertLocalOwner(subject); } },
|
|
193
|
+
updateApi: { authorize: (subject) => { assertLocalOwner(subject); }, restart: deps.restart },
|
|
192
194
|
siwe: { ensureUser: ownerSignIn },
|
|
193
195
|
mode: localModeInfo,
|
|
194
196
|
};
|
|
@@ -16,12 +16,14 @@ import { handleClaudeRequest, type ClaudeApiDeps } from './claude-api.js';
|
|
|
16
16
|
import type { ModeInfo } from './mode-api.js';
|
|
17
17
|
import { handleVaultApiRequest, type VaultApiDeps } from './vault-api.js';
|
|
18
18
|
import { handleBundleRequest, type BundleApiDeps } from './bundle-api.js';
|
|
19
|
+
import { handleUpdateRequest, type UpdateApiDeps } from './update-api.js';
|
|
19
20
|
|
|
20
21
|
export interface SessionApis {
|
|
21
22
|
agentApi?: AgentApiDeps;
|
|
22
23
|
agentConnectorApi?: AgentConnectorApiDeps;
|
|
23
24
|
bundleApi?: BundleApiDeps;
|
|
24
25
|
vaultApi?: VaultApiDeps;
|
|
26
|
+
updateApi?: UpdateApiDeps;
|
|
25
27
|
localCli?: LocalCliDeps;
|
|
26
28
|
claudeApi?: ClaudeApiDeps;
|
|
27
29
|
connectorApi?: ConnectorApiDeps;
|
|
@@ -44,6 +46,7 @@ export function handleSessionApis(
|
|
|
44
46
|
...when(apis.connectorApi, (d) => handleConnectorApiRequest(req, res, d)),
|
|
45
47
|
...when(apis.bundleApi, (d) => handleBundleRequest(req, res, d)),
|
|
46
48
|
...when(apis.vaultApi, (d) => handleVaultApiRequest(req, res, d)),
|
|
49
|
+
...when(apis.updateApi, (d) => handleUpdateRequest(req, res, d)),
|
|
47
50
|
...when(apis.claudeApi, (d) => handleClaudeRequest(req, res, d)),
|
|
48
51
|
...when(apis.agentConnectorApi, (d) => handleAgentConnectorRequest(req, res, d)),
|
|
49
52
|
...when(apis.agentApi, (d) => handleAgentApiRequest(req, res, d)),
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
import { errMsg, log } from './log.js';
|
|
4
|
+
import { ApiError } from './api-error.js';
|
|
5
|
+
import { apiFailure, apiSession, cors, sendJson } from './api-http.js';
|
|
6
|
+
import { isRecord } from './is-record.js';
|
|
7
|
+
import { METRO_VERSION } from './version.js';
|
|
8
|
+
|
|
9
|
+
const PATH = '/api/update';
|
|
10
|
+
const CHECK_TTL_MS = 10 * 60_000;
|
|
11
|
+
const CHECK_TIMEOUT_MS = 30_000;
|
|
12
|
+
const UPDATE_TIMEOUT_MS = 5 * 60_000;
|
|
13
|
+
const RESTART_DELAY_MS = 500;
|
|
14
|
+
const OUTPUT_TAIL = 800;
|
|
15
|
+
|
|
16
|
+
export interface UpdateCheck {
|
|
17
|
+
current: string;
|
|
18
|
+
latest: string;
|
|
19
|
+
newer: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UpdateResult {
|
|
23
|
+
updated: boolean;
|
|
24
|
+
version: string;
|
|
25
|
+
restarting: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UpdateApiDeps {
|
|
29
|
+
authorize: (subject: string) => void;
|
|
30
|
+
restart: () => void;
|
|
31
|
+
cliBin?: () => string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cliBinFromEnv = (): string => process.env.METRO_CLI_BIN?.trim() ?? '';
|
|
35
|
+
|
|
36
|
+
function runCli(bin: string, args: string[], timeout: number): Promise<{ stdout: string; stderr: string }> {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
execFile(process.execPath, [bin, ...args], { timeout, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
39
|
+
if (err === null) resolve({ stdout, stderr });
|
|
40
|
+
else reject(new ApiError(`metro update failed: ${(stderr || err.message).trim().slice(-OUTPUT_TAIL)}`, 502));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let cached: { at: number; check: UpdateCheck } | null = null;
|
|
46
|
+
|
|
47
|
+
function parseCheck(raw: string): UpdateCheck {
|
|
48
|
+
let body: unknown;
|
|
49
|
+
try {
|
|
50
|
+
body = JSON.parse(raw.trim().split('\n').at(-1) ?? '');
|
|
51
|
+
} catch {
|
|
52
|
+
throw new ApiError('metro update --check returned no report', 502);
|
|
53
|
+
}
|
|
54
|
+
if (!isRecord(body) || typeof body.current !== 'string' || typeof body.latest !== 'string')
|
|
55
|
+
throw new ApiError('metro update --check returned an unexpected report', 502);
|
|
56
|
+
return { current: body.current, latest: body.latest, newer: body.newer === true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function check(bin: string, now = Date.now()): Promise<UpdateCheck> {
|
|
60
|
+
if (cached !== null && now - cached.at < CHECK_TTL_MS) return cached.check;
|
|
61
|
+
const out = await runCli(bin, ['update', '--check'], CHECK_TIMEOUT_MS);
|
|
62
|
+
cached = { at: now, check: parseCheck(out.stdout) };
|
|
63
|
+
return cached.check;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function apply(bin: string, deps: UpdateApiDeps): Promise<UpdateResult> {
|
|
67
|
+
const before = await check(bin);
|
|
68
|
+
if (!before.newer) return { updated: false, version: before.current, restarting: false };
|
|
69
|
+
const out = await runCli(bin, ['update'], UPDATE_TIMEOUT_MS);
|
|
70
|
+
cached = null;
|
|
71
|
+
log.info({ from: before.current, to: before.latest, output: out.stderr.trim().slice(-OUTPUT_TAIL) }, 'update-api: metro updated, restarting');
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
deps.restart();
|
|
74
|
+
}, RESTART_DELAY_MS).unref();
|
|
75
|
+
return { updated: true, version: before.latest, restarting: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function binOrThrow(deps: UpdateApiDeps): string {
|
|
79
|
+
const bin = (deps.cliBin ?? cliBinFromEnv)();
|
|
80
|
+
if (bin === '')
|
|
81
|
+
throw new ApiError('this daemon was not started by metro serve, so it cannot update itself: run metro update on the machine', 400);
|
|
82
|
+
return bin;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function handleUpdateRequest(req: IncomingMessage, res: ServerResponse, deps: UpdateApiDeps): boolean {
|
|
86
|
+
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
87
|
+
if (path !== PATH) return false;
|
|
88
|
+
if (req.method === 'OPTIONS') {
|
|
89
|
+
res.writeHead(204, cors(req)).end();
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (req.method !== 'GET' && req.method !== 'POST') {
|
|
93
|
+
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const session = apiSession(req);
|
|
97
|
+
if (!session) {
|
|
98
|
+
sendJson(req, res, 401, { error: 'unauthorized' });
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
Promise.resolve()
|
|
102
|
+
.then(async (): Promise<unknown> => {
|
|
103
|
+
deps.authorize(session.subject);
|
|
104
|
+
const bin = binOrThrow(deps);
|
|
105
|
+
if (req.method === 'GET') return { ...(await check(bin)), running: METRO_VERSION };
|
|
106
|
+
return apply(bin, deps);
|
|
107
|
+
})
|
|
108
|
+
.then((body) => {
|
|
109
|
+
sendJson(req, res, 200, body);
|
|
110
|
+
})
|
|
111
|
+
.catch((err: unknown) => {
|
|
112
|
+
if (err instanceof ApiError) apiFailure(req, res, err, 'update-api');
|
|
113
|
+
else {
|
|
114
|
+
log.warn({ err: errMsg(err) }, 'update-api: request failed');
|
|
115
|
+
if (!res.headersSent) sendJson(req, res, 500, { error: 'update api failed' });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
return true;
|
|
119
|
+
}
|
package/runtime/runtime.json
CHANGED