@stage-labs/metro 0.1.0-beta.32 → 0.1.0-beta.33
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 +4 -2
- package/dist/serve.js +20 -4
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +4 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +17 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +0 -14
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +120 -32
- package/runtime/runtime.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -16,9 +16,11 @@ const USAGE = `metro — the command line for your MCP connectors
|
|
|
16
16
|
|
|
17
17
|
metro start <agent-id> [--detach]
|
|
18
18
|
run that agent's stations on this machine
|
|
19
|
-
metro serve [--port <n>]
|
|
19
|
+
metro serve [--port <n>] [--tunnel]
|
|
20
20
|
run a daemon of your own: its agents and stations live in ~/.metro/agents
|
|
21
|
-
here, and metro.box manages it through the link it prints
|
|
21
|
+
here, and metro.box manages it through the link it prints; --tunnel adds
|
|
22
|
+
a public https address through a Cloudflare quick tunnel (needs
|
|
23
|
+
cloudflared, no account) so the link works from anywhere
|
|
22
24
|
metro stop [agent-id]
|
|
23
25
|
stop every metro daemon on this machine, however it was started
|
|
24
26
|
metro status <agent-id>
|
package/dist/serve.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
1
2
|
import { homedir } from 'node:os';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { findBun, localPort, runtimeDir, SERVER_ENTRY, spawnPlan, } from './runtime.js';
|
|
4
5
|
const SCRUBBED = new Set(['METRO_RUN_TOKEN', 'METRO_AGENT', 'DATABASE_URL']);
|
|
5
6
|
const PORT_FLAG = /^--port=(.*)$/;
|
|
6
|
-
const USAGE = 'usage: metro serve [--port <n>]';
|
|
7
|
+
const USAGE = 'usage: metro serve [--port <n>] [--tunnel]';
|
|
7
8
|
function portOf(raw) {
|
|
8
9
|
const n = Number(raw);
|
|
9
10
|
if (raw === undefined || raw === '' || !Number.isInteger(n) || n < 1 || n > 65535)
|
|
@@ -12,8 +13,13 @@ function portOf(raw) {
|
|
|
12
13
|
}
|
|
13
14
|
export function parseServeArgs(argv) {
|
|
14
15
|
let port = localPort();
|
|
16
|
+
let tunnel = false;
|
|
15
17
|
for (let i = 0; i < argv.length; i += 1) {
|
|
16
18
|
const arg = argv[i] ?? '';
|
|
19
|
+
if (arg === '--tunnel') {
|
|
20
|
+
tunnel = true;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
17
23
|
const inline = PORT_FLAG.exec(arg);
|
|
18
24
|
if (inline) {
|
|
19
25
|
port = portOf(inline[1]);
|
|
@@ -26,7 +32,14 @@ export function parseServeArgs(argv) {
|
|
|
26
32
|
}
|
|
27
33
|
throw new Error(`unknown argument '${arg}' — ${USAGE}`);
|
|
28
34
|
}
|
|
29
|
-
return { port };
|
|
35
|
+
return { port, tunnel };
|
|
36
|
+
}
|
|
37
|
+
export function findCloudflared() {
|
|
38
|
+
const found = spawnSync('cloudflared', ['--version'], { stdio: 'ignore' });
|
|
39
|
+
if (found.error !== undefined || found.status !== 0)
|
|
40
|
+
throw new Error('metro serve --tunnel needs cloudflared, which is not on PATH.\n' +
|
|
41
|
+
'Install it with: brew install cloudflared (macOS), or from\n' +
|
|
42
|
+
'https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/downloads/');
|
|
30
43
|
}
|
|
31
44
|
export function serveStateDir() {
|
|
32
45
|
const xdg = process.env.XDG_CACHE_HOME?.trim();
|
|
@@ -46,12 +59,15 @@ export function servePlan(opts) {
|
|
|
46
59
|
METRO_HTTP_HOST: process.env.METRO_HTTP_HOST ?? '127.0.0.1',
|
|
47
60
|
METRO_TRAINS_DIR: join(opts.dir, 'trains'),
|
|
48
61
|
METRO_STATE_DIR: process.env.METRO_STATE_DIR ?? serveStateDir(),
|
|
62
|
+
...(opts.tunnel ? { METRO_TUNNEL: 'quick' } : {}),
|
|
49
63
|
},
|
|
50
64
|
};
|
|
51
65
|
}
|
|
52
66
|
export function serve(argv) {
|
|
53
|
-
const { port } = parseServeArgs(argv);
|
|
54
|
-
|
|
67
|
+
const { port, tunnel } = parseServeArgs(argv);
|
|
68
|
+
if (tunnel)
|
|
69
|
+
findCloudflared();
|
|
70
|
+
const plan = servePlan({ dir: runtimeDir(), port, tunnel });
|
|
55
71
|
process.stderr.write(`Starting a metro daemon of your own on http://127.0.0.1:${String(port)}\n`);
|
|
56
72
|
return spawnPlan(plan);
|
|
57
73
|
}
|
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.33",
|
|
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": {
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { attachmentOwner, recordAttachmentOwner } from './attach-owner.js';
|
|
15
15
|
import { grantAllows, issueAttachmentGrant } from './attach-grant.js';
|
|
16
16
|
import { errMsg, log } from './log.js';
|
|
17
|
-
import {
|
|
17
|
+
import { configuredTunnelHost, currentTunnelUrl, webhookPort } from './tunnel.js';
|
|
18
18
|
import { isLocalMode } from './paths.js';
|
|
19
19
|
|
|
20
20
|
function authorized(req: IncomingMessage, name: string): boolean {
|
|
@@ -31,7 +31,9 @@ const DEFAULT_PUBLIC_BASE = 'https://mcp.metro.box';
|
|
|
31
31
|
export function publicBaseUrl(): string | null {
|
|
32
32
|
const env = process.env.METRO_PUBLIC_URL?.trim();
|
|
33
33
|
if (env) return env.replace(/\/+$/, '');
|
|
34
|
-
const
|
|
34
|
+
const live = currentTunnelUrl();
|
|
35
|
+
if (live !== null) return live;
|
|
36
|
+
const host = configuredTunnelHost();
|
|
35
37
|
return host ? `https://${host}` : null;
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -15,9 +15,15 @@ import { installCrashGuard, markDaemonReady } from './crash-guard.js';
|
|
|
15
15
|
import {
|
|
16
16
|
loadTunnelConfig,
|
|
17
17
|
Tunnel,
|
|
18
|
+
tunnelConfigFromEnv,
|
|
18
19
|
warnOnLegacyWebhooks,
|
|
19
20
|
webhookPort,
|
|
20
21
|
} from './tunnel.js';
|
|
22
|
+
import {
|
|
23
|
+
localConnectHint,
|
|
24
|
+
publicConnectHint,
|
|
25
|
+
tunnelPendingHint,
|
|
26
|
+
} from './connect-hint.js';
|
|
21
27
|
import { TrainSupervisor } from './supervisor.js';
|
|
22
28
|
import {
|
|
23
29
|
makeEmit,
|
|
@@ -39,7 +45,7 @@ import {
|
|
|
39
45
|
import { loadAgentForRuntime, localAgentKey } from '../db/materialize.js';
|
|
40
46
|
import { fileSource } from '../db/file-source.js';
|
|
41
47
|
import { ensureLocalSessionSecret } from './local-secret.js';
|
|
42
|
-
import {
|
|
48
|
+
import { localSessionApis } from './local-mode.js';
|
|
43
49
|
import type { ModeInfo } from './mode-api.js';
|
|
44
50
|
import type { SessionApis } from './session-apis.js';
|
|
45
51
|
import {
|
|
@@ -128,8 +134,12 @@ supervisor.onTrainEvent((env, train) => {
|
|
|
128
134
|
});
|
|
129
135
|
|
|
130
136
|
let webhookServer: Server | null = null;
|
|
131
|
-
const tunnelCfg = loadTunnelConfig();
|
|
132
|
-
const tunnel = tunnelCfg ? new Tunnel(tunnelCfg, webhookPort()) : null;
|
|
137
|
+
const tunnelCfg = loadTunnelConfig() ?? tunnelConfigFromEnv();
|
|
138
|
+
const tunnel = tunnelCfg ? new Tunnel(tunnelCfg, webhookPort(), announcePublic) : null;
|
|
139
|
+
|
|
140
|
+
function announcePublic(url: string): void {
|
|
141
|
+
process.stderr.write(`\n${publicConnectHint(url)}`);
|
|
142
|
+
}
|
|
133
143
|
|
|
134
144
|
setTrainCallBackend((train, action, args) =>
|
|
135
145
|
supervisor.call(train, action, args),
|
|
@@ -140,7 +150,10 @@ const linkedSource = runtime === null ? null : httpSource(runtime);
|
|
|
140
150
|
const localSource = linkedSource ?? (isLocalMode() ? fileSource : null);
|
|
141
151
|
|
|
142
152
|
function announceLocalEndpoint(): void {
|
|
143
|
-
if (isLocalMode())
|
|
153
|
+
if (isLocalMode())
|
|
154
|
+
process.stderr.write(
|
|
155
|
+
`\n${tunnel === null ? localConnectHint(webhookPort()) : tunnelPendingHint()}`,
|
|
156
|
+
);
|
|
144
157
|
const key = localAgentKey();
|
|
145
158
|
if (key === null) {
|
|
146
159
|
if (isLocalMode()) log.info('no agent on this machine yet');
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
|
|
3
|
+
const uiBase = (): string => process.env.METRO_UI_URL ?? 'https://metro.box';
|
|
4
|
+
|
|
5
|
+
export const connectLink = (base: string): string =>
|
|
6
|
+
`${uiBase()}/#/connect/${encodeURIComponent(base)}`;
|
|
7
|
+
|
|
8
|
+
export function localConnectHint(port: number): string {
|
|
9
|
+
const here = `http://127.0.0.1:${String(port)}`;
|
|
10
|
+
const forward = `ssh -L ${String(port)}:127.0.0.1:${String(port)} ${hostname()}`;
|
|
11
|
+
return [
|
|
12
|
+
'Manage this machine from the web UI:',
|
|
13
|
+
'',
|
|
14
|
+
` ${connectLink(here)}`,
|
|
15
|
+
'',
|
|
16
|
+
`From another computer, forward the port first: ${forward}`,
|
|
17
|
+
'',
|
|
18
|
+
].join('\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function publicConnectHint(url: string): string {
|
|
22
|
+
return [
|
|
23
|
+
'Manage this machine from the web UI, from anywhere:',
|
|
24
|
+
'',
|
|
25
|
+
` ${connectLink(url)}`,
|
|
26
|
+
'',
|
|
27
|
+
'Sign in right away: the first wallet to sign in owns this machine.',
|
|
28
|
+
'',
|
|
29
|
+
].join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const tunnelPendingHint = (): string =>
|
|
33
|
+
'Bringing up a public address through a Cloudflare quick tunnel…\n';
|
|
@@ -129,20 +129,6 @@ const agentConnectorApi: AgentConnectorApiDeps = {
|
|
|
129
129
|
Promise.reject(new ApiError('a local daemon has no pairing codes', 400)),
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
-
export function localConnectHint(port: number): string {
|
|
133
|
-
const ui = process.env.METRO_UI_URL ?? 'https://metro.box';
|
|
134
|
-
const here = `http://127.0.0.1:${String(port)}`;
|
|
135
|
-
const forward = `ssh -L ${String(port)}:127.0.0.1:${String(port)} ${hostname()}`;
|
|
136
|
-
return [
|
|
137
|
-
'Manage this machine from the web UI:',
|
|
138
|
-
'',
|
|
139
|
-
` ${ui}/#/connect/${encodeURIComponent(here)}`,
|
|
140
|
-
'',
|
|
141
|
-
`From another computer, forward the port first: ${forward}`,
|
|
142
|
-
'',
|
|
143
|
-
].join('\n');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
132
|
export function localModeInfo(): ModeInfo {
|
|
147
133
|
return { mode: 'local', owner: localOwner(), project: LOCAL_PROJECT_ID };
|
|
148
134
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
|
|
2
2
|
import { timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { Resolver } from 'node:dns/promises';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
import { join } from 'node:path';
|
|
5
6
|
import { STATE_DIR } from './paths.js';
|
|
@@ -10,11 +11,61 @@ const FILE = join(STATE_DIR, 'tunnel.json');
|
|
|
10
11
|
const LEGACY_WEBHOOKS_FILE = join(STATE_DIR, 'webhooks.json');
|
|
11
12
|
const RESTART_DELAY_MS = 2_000;
|
|
12
13
|
|
|
13
|
-
interface
|
|
14
|
+
interface NamedTunnel {
|
|
14
15
|
name: string;
|
|
15
16
|
hostname: string;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
interface QuickTunnel {
|
|
20
|
+
quick: true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type TunnelConfig = NamedTunnel | QuickTunnel;
|
|
24
|
+
|
|
25
|
+
const isQuick = (cfg: TunnelConfig): cfg is QuickTunnel => 'quick' in cfg;
|
|
26
|
+
|
|
27
|
+
export const tunnelConfigFromEnv = (): TunnelConfig | null =>
|
|
28
|
+
process.env.METRO_TUNNEL?.trim() === 'quick' ? { quick: true } : null;
|
|
29
|
+
|
|
30
|
+
const QUICK_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
31
|
+
|
|
32
|
+
export const quickTunnelUrlIn = (text: string): string | null =>
|
|
33
|
+
QUICK_URL_RE.exec(text)?.[0] ?? null;
|
|
34
|
+
|
|
35
|
+
let liveUrl: string | null = null;
|
|
36
|
+
|
|
37
|
+
export const currentTunnelUrl = (): string | null => liveUrl;
|
|
38
|
+
|
|
39
|
+
export type Resolves = (host: string) => Promise<boolean>;
|
|
40
|
+
|
|
41
|
+
const PUBLIC_RESOLVERS = ['1.1.1.1', '1.0.0.1'];
|
|
42
|
+
const RESOLVE_EVERY_MS = 3_000;
|
|
43
|
+
const RESOLVE_GIVE_UP_MS = 180_000;
|
|
44
|
+
|
|
45
|
+
export async function resolvesAtCloudflare(host: string): Promise<boolean> {
|
|
46
|
+
const resolver = new Resolver();
|
|
47
|
+
resolver.setServers(PUBLIC_RESOLVERS);
|
|
48
|
+
try {
|
|
49
|
+
return (await resolver.resolve4(host)).length > 0;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function untilResolvable(host: string, resolves: Resolves): Promise<boolean> {
|
|
56
|
+
const deadline = Date.now() + RESOLVE_GIVE_UP_MS;
|
|
57
|
+
while (Date.now() < deadline) {
|
|
58
|
+
if (await resolves(host)) return true;
|
|
59
|
+
await new Promise((r) => setTimeout(r, RESOLVE_EVERY_MS));
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function configuredTunnelHost(): string | null {
|
|
65
|
+
const cfg = loadTunnelConfig();
|
|
66
|
+
return cfg !== null && !isQuick(cfg) ? cfg.hostname : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
18
69
|
export interface Endpoint {
|
|
19
70
|
id: string;
|
|
20
71
|
webhookId?: string;
|
|
@@ -98,6 +149,12 @@ function fetchTunnelToken(name: string): string | null {
|
|
|
98
149
|
return token.length > 0 ? token : null;
|
|
99
150
|
}
|
|
100
151
|
|
|
152
|
+
interface Plan {
|
|
153
|
+
args: string[];
|
|
154
|
+
env: NodeJS.ProcessEnv;
|
|
155
|
+
mode: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
101
158
|
export class Tunnel {
|
|
102
159
|
private child: ChildProcess | null = null;
|
|
103
160
|
private closed = false;
|
|
@@ -106,57 +163,88 @@ export class Tunnel {
|
|
|
106
163
|
constructor(
|
|
107
164
|
private cfg: TunnelConfig,
|
|
108
165
|
private port: number,
|
|
166
|
+
private onUrl: (url: string) => void = () => undefined,
|
|
167
|
+
private resolves: Resolves = resolvesAtCloudflare,
|
|
109
168
|
) {}
|
|
110
169
|
|
|
111
170
|
get hostname(): string {
|
|
112
|
-
return this.cfg.hostname;
|
|
171
|
+
if (!isQuick(this.cfg)) return this.cfg.hostname;
|
|
172
|
+
return liveUrl === null ? '' : new URL(liveUrl).host;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private origin(): string {
|
|
176
|
+
return `http://127.0.0.1:${String(this.port)}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private plan(): Plan {
|
|
180
|
+
if (isQuick(this.cfg))
|
|
181
|
+
return {
|
|
182
|
+
args: ['--no-autoupdate', 'tunnel', '--url', this.origin()],
|
|
183
|
+
env: process.env,
|
|
184
|
+
mode: 'quick',
|
|
185
|
+
};
|
|
186
|
+
if (this.token === undefined) this.token = fetchTunnelToken(this.cfg.name);
|
|
187
|
+
const args = ['--no-autoupdate', 'tunnel', 'run', '--url', this.origin()];
|
|
188
|
+
if (!this.token) args.push(this.cfg.name);
|
|
189
|
+
return {
|
|
190
|
+
args,
|
|
191
|
+
env: this.token ? { ...process.env, TUNNEL_TOKEN: this.token } : process.env,
|
|
192
|
+
mode: this.token ? 'token' : 'named',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private noticeUrl(text: string): void {
|
|
197
|
+
if (!isQuick(this.cfg)) return;
|
|
198
|
+
const url = quickTunnelUrlIn(text);
|
|
199
|
+
if (url === null || url === liveUrl) return;
|
|
200
|
+
liveUrl = url;
|
|
201
|
+
log.info({ url }, 'quick tunnel up; waiting for its name to resolve');
|
|
202
|
+
this.announceWhenResolvable(url).catch((err: unknown) => {
|
|
203
|
+
log.warn({ err: errMsg(err) }, 'quick tunnel: announce failed');
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private async announceWhenResolvable(url: string): Promise<void> {
|
|
208
|
+
const resolved = await untilResolvable(new URL(url).host, this.resolves);
|
|
209
|
+
if (liveUrl !== url) return;
|
|
210
|
+
if (!resolved) log.warn({ url }, 'quick tunnel name still not resolving; announcing anyway');
|
|
211
|
+
this.onUrl(url);
|
|
113
212
|
}
|
|
114
213
|
|
|
115
214
|
start(): void {
|
|
116
215
|
if (this.closed) return;
|
|
117
|
-
|
|
118
|
-
const mode = this.token ? 'token' : 'named';
|
|
216
|
+
const plan = this.plan();
|
|
119
217
|
log.info(
|
|
120
|
-
{
|
|
121
|
-
name: this.cfg.name,
|
|
122
|
-
hostname: this.cfg.hostname,
|
|
123
|
-
port: this.port,
|
|
124
|
-
mode,
|
|
125
|
-
},
|
|
218
|
+
{ tunnel: isQuick(this.cfg) ? 'quick' : this.cfg.name, port: this.port, mode: plan.mode },
|
|
126
219
|
'cloudflared tunnel starting',
|
|
127
220
|
);
|
|
128
|
-
const
|
|
129
|
-
'--no-autoupdate',
|
|
130
|
-
'tunnel',
|
|
131
|
-
'run',
|
|
132
|
-
'--url',
|
|
133
|
-
`http://127.0.0.1:${this.port}`,
|
|
134
|
-
];
|
|
135
|
-
if (!this.token) args.push(this.cfg.name);
|
|
136
|
-
const env = this.token
|
|
137
|
-
? { ...process.env, TUNNEL_TOKEN: this.token }
|
|
138
|
-
: process.env;
|
|
139
|
-
this.child = spawn('cloudflared', args, {
|
|
221
|
+
const child = spawn('cloudflared', plan.args, {
|
|
140
222
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
141
|
-
env,
|
|
223
|
+
env: plan.env,
|
|
142
224
|
});
|
|
143
|
-
this.child
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
'cloudflared',
|
|
149
|
-
);
|
|
225
|
+
this.child = child;
|
|
226
|
+
child.stderr?.on('data', (d: Buffer | string) => {
|
|
227
|
+
const text = (typeof d === 'string' ? d : d.toString('utf8')).trim();
|
|
228
|
+
log.debug({ cloudflared: text }, 'cloudflared');
|
|
229
|
+
this.noticeUrl(text);
|
|
150
230
|
});
|
|
151
|
-
|
|
231
|
+
child.on('exit', (code) => {
|
|
152
232
|
this.child = null;
|
|
233
|
+
liveUrl = null;
|
|
153
234
|
if (this.closed) return;
|
|
154
235
|
log.warn({ code }, 'cloudflared exited; restarting');
|
|
155
236
|
setTimeout(() => {
|
|
156
237
|
this.start();
|
|
157
238
|
}, RESTART_DELAY_MS);
|
|
158
239
|
});
|
|
159
|
-
|
|
240
|
+
child.on('error', (err) => {
|
|
241
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
242
|
+
this.closed = true;
|
|
243
|
+
log.error(
|
|
244
|
+
'cloudflared is not installed, so there is no public address; install it and restart',
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
160
248
|
log.warn({ err: errMsg(err) }, 'cloudflared spawn error');
|
|
161
249
|
});
|
|
162
250
|
}
|
package/runtime/runtime.json
CHANGED