@stage-labs/metro 0.1.0-beta.57 → 0.1.0-beta.58
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 +3 -3
- package/dist/serve.js +11 -26
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +3 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +2 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +3 -25
- package/runtime/runtime.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,9 +14,9 @@ const USAGE = `metro — run your agent on this machine
|
|
|
14
14
|
run the daemon: the agent, its channels and its connectors live in
|
|
15
15
|
~/.metro/agents here, and the page at metro.box manages it through the
|
|
16
16
|
link it prints; --owner names the one wallet that may sign in (remembered
|
|
17
|
-
after the first start); --tunnel
|
|
18
|
-
|
|
19
|
-
from anywhere
|
|
17
|
+
after the first start); --tunnel publishes the daemon through Tailscale
|
|
18
|
+
Funnel at a permanent https://<machine>.<tailnet>.ts.net (Tailscale
|
|
19
|
+
installed and signed in on this machine), so the link works from anywhere
|
|
20
20
|
metro stop stop the metro daemon on this machine
|
|
21
21
|
metro tail <agent-id>
|
|
22
22
|
follow this machine's inbound events, one JSON line each
|
package/dist/serve.js
CHANGED
|
@@ -10,7 +10,7 @@ const SCRUBBED = new Set(['METRO_RUN_TOKEN', 'METRO_AGENT', 'DATABASE_URL']);
|
|
|
10
10
|
const PORT_FLAG = /^--port=(.*)$/;
|
|
11
11
|
const OWNER_FLAG = /^--owner=(.*)$/;
|
|
12
12
|
const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
13
|
-
const USAGE = 'usage: metro serve [--port <n>] [--tunnel
|
|
13
|
+
const USAGE = 'usage: metro serve [--port <n>] [--tunnel] [--owner <address>]';
|
|
14
14
|
const TUNNEL_FLAG = /^--tunnel=(.*)$/;
|
|
15
15
|
const TAILSCALE_APP = '/Applications/Tailscale.app/Contents/MacOS/Tailscale';
|
|
16
16
|
function portOf(raw) {
|
|
@@ -20,22 +20,21 @@ function portOf(raw) {
|
|
|
20
20
|
return n;
|
|
21
21
|
}
|
|
22
22
|
function tunnelOf(raw) {
|
|
23
|
-
if (raw === undefined || raw === '' || raw === '
|
|
24
|
-
return
|
|
25
|
-
|
|
26
|
-
return 'tailscale';
|
|
27
|
-
throw new Error(`'${raw}' is not a tunnel kind (quick or tailscale) — ${USAGE}`);
|
|
23
|
+
if (raw === undefined || raw === '' || raw === 'tailscale')
|
|
24
|
+
return true;
|
|
25
|
+
throw new Error(`'${raw}' is not a tunnel metro knows; the public address is Tailscale Funnel — ${USAGE}`);
|
|
28
26
|
}
|
|
29
27
|
function tunnelFlag(argv, i) {
|
|
30
28
|
const arg = argv[i] ?? '';
|
|
31
29
|
const inline = TUNNEL_FLAG.exec(arg);
|
|
32
30
|
if (inline)
|
|
33
|
-
return {
|
|
31
|
+
return { consumed: Number(tunnelOf(inline[1])) - 1 };
|
|
34
32
|
if (arg !== '--tunnel')
|
|
35
33
|
return null;
|
|
36
34
|
const next = argv[i + 1];
|
|
37
35
|
const takesValue = next !== undefined && !next.startsWith('-');
|
|
38
|
-
|
|
36
|
+
tunnelOf(takesValue ? next : undefined);
|
|
37
|
+
return { consumed: takesValue ? 1 : 0 };
|
|
39
38
|
}
|
|
40
39
|
function ownerOf(raw) {
|
|
41
40
|
if (raw === undefined || !ADDRESS.test(raw))
|
|
@@ -44,13 +43,13 @@ function ownerOf(raw) {
|
|
|
44
43
|
}
|
|
45
44
|
export function parseServeArgs(argv) {
|
|
46
45
|
let port = localPort();
|
|
47
|
-
let tunnel =
|
|
46
|
+
let tunnel = false;
|
|
48
47
|
let owner = null;
|
|
49
48
|
for (let i = 0; i < argv.length; i += 1) {
|
|
50
49
|
const arg = argv[i] ?? '';
|
|
51
50
|
const tunnelArg = tunnelFlag(argv, i);
|
|
52
51
|
if (tunnelArg) {
|
|
53
|
-
tunnel =
|
|
52
|
+
tunnel = true;
|
|
54
53
|
i += tunnelArg.consumed;
|
|
55
54
|
continue;
|
|
56
55
|
}
|
|
@@ -111,18 +110,6 @@ export function findTailscale(candidates = ['tailscale', TAILSCALE_APP]) {
|
|
|
111
110
|
'Funnel also has to be enabled once on your tailnet: https://tailscale.com/kb/1223/funnel');
|
|
112
111
|
return bin;
|
|
113
112
|
}
|
|
114
|
-
function findCloudflared() {
|
|
115
|
-
const found = spawnSync('cloudflared', ['--version'], { stdio: 'ignore' });
|
|
116
|
-
if (found.error !== undefined || found.status !== 0)
|
|
117
|
-
throw new Error('metro serve --tunnel needs cloudflared, which is not on PATH.\n' +
|
|
118
|
-
'Debian/Ubuntu (it is not in the distro repos; this adds Cloudflare\'s signed one):\n' +
|
|
119
|
-
' sudo mkdir -p --mode=0755 /usr/share/keyrings\n' +
|
|
120
|
-
' curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null\n' +
|
|
121
|
-
" echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list\n" +
|
|
122
|
-
' sudo apt-get update && sudo apt-get install cloudflared\n' +
|
|
123
|
-
'macOS: brew install cloudflared\n' +
|
|
124
|
-
'Others: https://pkg.cloudflare.com/');
|
|
125
|
-
}
|
|
126
113
|
export function servePlan(opts) {
|
|
127
114
|
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !SCRUBBED.has(key)));
|
|
128
115
|
return {
|
|
@@ -141,7 +128,7 @@ export function servePlan(opts) {
|
|
|
141
128
|
? {}
|
|
142
129
|
: { METRO_RUNTIME_STORE: opts.runtime.dir, METRO_RUNTIME_MANIFEST: opts.runtime.manifest }),
|
|
143
130
|
METRO_STATE_DIR: process.env.METRO_STATE_DIR ?? serveStateDir(),
|
|
144
|
-
...(opts.tunnel
|
|
131
|
+
...(opts.tunnel ? { METRO_TUNNEL: 'tailscale' } : {}),
|
|
145
132
|
...(opts.tailscaleBin === undefined ? {} : { METRO_TAILSCALE_BIN: opts.tailscaleBin }),
|
|
146
133
|
...(opts.owner === null ? {} : { METRO_OWNER: opts.owner }),
|
|
147
134
|
},
|
|
@@ -154,9 +141,7 @@ export function serve(argv) {
|
|
|
154
141
|
throw new Error(`a metro serve daemon is already running on this machine (pid ${String(running)}). ` +
|
|
155
142
|
'Stop it first: metro stop');
|
|
156
143
|
requireOwner(owner);
|
|
157
|
-
|
|
158
|
-
findCloudflared();
|
|
159
|
-
const tailscaleBin = tunnel === 'tailscale' ? findTailscale() : undefined;
|
|
144
|
+
const tailscaleBin = tunnel ? findTailscale() : undefined;
|
|
160
145
|
process.stderr.write(`Starting a metro daemon of your own on http://127.0.0.1:${String(port)}\n`);
|
|
161
146
|
return spawnPlan(() => servePlan({ runtime: prepareRuntime(), port, tunnel, owner, ...(tailscaleBin === undefined ? {} : { tailscaleBin }) }));
|
|
162
147
|
}
|
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.58",
|
|
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": {
|
|
@@ -6,7 +6,7 @@ import { errMsg, log, logFatalSync } from './log.js';
|
|
|
6
6
|
import { acquireLock, isLocalMode, STATE_DIR, trainsDir } from './paths.js';
|
|
7
7
|
import { installCrashGuard, markDaemonReady } from './crash-guard.js';
|
|
8
8
|
import { METRO_VERSION } from './version.js';
|
|
9
|
-
import {
|
|
9
|
+
import { funnelDriver, Tunnel, tunnelWanted, webhookPort } from './tunnel.js';
|
|
10
10
|
import {
|
|
11
11
|
localConnectHint,
|
|
12
12
|
publicConnectHint,
|
|
@@ -61,8 +61,7 @@ supervisor.onTrainEvent((env, train) => {
|
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
let webhookServer: Server | null = null;
|
|
64
|
-
const
|
|
65
|
-
const tunnel = tunnelWanted === null ? null : new Tunnel(driverFor(tunnelWanted, webhookPort()), announcePublic);
|
|
64
|
+
const tunnel = tunnelWanted() ? new Tunnel(funnelDriver(webhookPort()), announcePublic) : null;
|
|
66
65
|
|
|
67
66
|
function announcePublic(url: string): void {
|
|
68
67
|
process.stderr.write(`\n${publicConnectHint(url, localOwner())}`);
|
|
@@ -74,7 +73,7 @@ setTrainCallBackend((train, action, args) =>
|
|
|
74
73
|
|
|
75
74
|
function announceLocalEndpoint(): void {
|
|
76
75
|
process.stderr.write(
|
|
77
|
-
`\n${
|
|
76
|
+
`\n${tunnel === null ? localConnectHint(webhookPort(), localOwner()) : tunnelPendingHint()}`,
|
|
78
77
|
);
|
|
79
78
|
const key = localAgentKey();
|
|
80
79
|
if (key === null) {
|
|
@@ -35,7 +35,5 @@ export function publicConnectHint(url: string, owner: string | null): string {
|
|
|
35
35
|
].join('\n');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export const tunnelPendingHint = (
|
|
39
|
-
|
|
40
|
-
? 'Bringing up a public address through a Cloudflare quick tunnel…\n'
|
|
41
|
-
: 'Publishing this daemon through Tailscale Funnel… (if Tailscale asks to enable Funnel, its link is in the log)\n';
|
|
38
|
+
export const tunnelPendingHint = (): string =>
|
|
39
|
+
'Publishing this daemon through Tailscale Funnel… (if Tailscale asks to enable Funnel, its link is in the log)\n';
|
|
@@ -11,22 +11,11 @@ const RESTART_DELAY_MAX_MS = 30_000;
|
|
|
11
11
|
const TAKEN_RE = /listener already exists/i;
|
|
12
12
|
const FUNNEL_ON_RE = /\(Funnel on\)/i;
|
|
13
13
|
|
|
14
|
-
export
|
|
14
|
+
export const tunnelWanted = (): boolean => process.env.METRO_TUNNEL?.trim() === 'tailscale';
|
|
15
15
|
|
|
16
|
-
export function tunnelKind(): TunnelKind | null {
|
|
17
|
-
const raw = process.env.METRO_TUNNEL?.trim();
|
|
18
|
-
return raw === 'quick' || raw === 'tailscale' ? raw : null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const quickTunnelWanted = (): boolean => tunnelKind() === 'quick';
|
|
22
|
-
|
|
23
|
-
const QUICK_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
24
16
|
const FUNNEL_URL_RE = /https:\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)+\.ts\.net(?::\d{2,5})?(?=[\s/]|$)/i;
|
|
25
17
|
const SILENT_MS = 20_000;
|
|
26
18
|
|
|
27
|
-
export const quickTunnelUrlIn = (text: string): string | null =>
|
|
28
|
-
QUICK_URL_RE.exec(text)?.[0] ?? null;
|
|
29
|
-
|
|
30
19
|
export const funnelUrlIn = (text: string): string | null =>
|
|
31
20
|
FUNNEL_URL_RE.exec(text)?.[0]?.toLowerCase() ?? null;
|
|
32
21
|
|
|
@@ -69,14 +58,6 @@ export function funnelAlreadyServing(status: string, port: number): Adopted {
|
|
|
69
58
|
return { url: null, hint: 'port 443 is held by another serve config on this node; run: tailscale serve reset' };
|
|
70
59
|
}
|
|
71
60
|
|
|
72
|
-
export const quickDriver = (port: number): TunnelDriver => ({
|
|
73
|
-
name: 'cloudflared quick tunnel',
|
|
74
|
-
command: 'cloudflared',
|
|
75
|
-
args: ['--no-autoupdate', 'tunnel', '--url', `http://127.0.0.1:${String(port)}`],
|
|
76
|
-
urlIn: quickTunnelUrlIn,
|
|
77
|
-
waitsForDns: true,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
61
|
function tailscaleBin(): string {
|
|
81
62
|
const configured = process.env.METRO_TAILSCALE_BIN?.trim() ?? '';
|
|
82
63
|
return configured === '' ? 'tailscale' : configured;
|
|
@@ -91,9 +72,6 @@ export const funnelDriver = (port: number, bin = tailscaleBin()): TunnelDriver =
|
|
|
91
72
|
adopt: async () => funnelAlreadyServing(await runText(bin, ['funnel', 'status']), port),
|
|
92
73
|
});
|
|
93
74
|
|
|
94
|
-
export const driverFor = (kind: TunnelKind, port: number): TunnelDriver =>
|
|
95
|
-
kind === 'quick' ? quickDriver(port) : funnelDriver(port);
|
|
96
|
-
|
|
97
75
|
let liveUrl: string | null = null;
|
|
98
76
|
|
|
99
77
|
export const currentTunnelUrl = (): string | null => liveUrl;
|
|
@@ -114,7 +92,7 @@ async function resolvesAt(server: string, host: string): Promise<boolean> {
|
|
|
114
92
|
return answers.some((a) => a.length > 0);
|
|
115
93
|
}
|
|
116
94
|
|
|
117
|
-
export async function
|
|
95
|
+
export async function resolvesPublicly(host: string): Promise<boolean> {
|
|
118
96
|
const found = await Promise.all(PUBLIC_RESOLVERS.map((server) => resolvesAt(server, host)));
|
|
119
97
|
return found.some(Boolean);
|
|
120
98
|
}
|
|
@@ -193,7 +171,7 @@ export class Tunnel {
|
|
|
193
171
|
constructor(
|
|
194
172
|
private driver: TunnelDriver,
|
|
195
173
|
private onUrl: (url: string) => void = () => undefined,
|
|
196
|
-
private resolves: Resolves =
|
|
174
|
+
private resolves: Resolves = resolvesPublicly,
|
|
197
175
|
) {}
|
|
198
176
|
|
|
199
177
|
private noticeUrl(text: string): void {
|
package/runtime/runtime.json
CHANGED