@stage-labs/metro 0.1.0-beta.4 → 0.1.0-beta.41
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/api.js +131 -0
- package/dist/bedrock-eventstream.js +120 -0
- package/dist/bedrock-proxy.js +260 -0
- package/dist/bedrock-upstream.js +72 -0
- package/dist/bedrock.js +106 -0
- package/dist/claude.js +15 -0
- package/dist/cli.js +181 -207
- package/dist/control.js +173 -0
- package/dist/plugin.js +82 -0
- package/dist/prompt.js +61 -0
- package/dist/runtime.js +117 -0
- package/dist/serve.js +77 -0
- package/dist/store.js +93 -0
- package/dist/tail.js +120 -0
- package/dist/update.js +33 -0
- package/dist/version.js +97 -0
- package/package.json +34 -21
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +119 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/package.json +67 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +334 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +76 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +153 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +133 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +383 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +143 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +315 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/hosted-connectors.ts +118 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +369 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +86 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/import-api.ts +67 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-connectors-api.ts +56 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +187 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-secret.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +31 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +145 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +303 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +139 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +63 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +27 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-nonces.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +89 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +320 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +257 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +214 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +84 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +326 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +319 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +152 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +373 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +309 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +24 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +118 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +141 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
- package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
- package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
- package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
- package/runtime/node_modules/@metro-labs/telegram/package.json +28 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
- package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
- package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
- package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
- package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
- package/runtime/runtime.json +3 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -117
- package/dist/agents/claude.js +0 -207
- package/dist/agents/codex.js +0 -207
- package/dist/agents/types.js +0 -2
- package/dist/channels/discord.js +0 -104
- package/dist/channels/telegram.js +0 -189
- package/dist/helpers/scope-cache.js +0 -65
- package/dist/helpers/streaming.js +0 -209
- package/dist/helpers/telegram-format.js +0 -39
- package/dist/helpers/turn.js +0 -40
- package/dist/log.js +0 -10
- package/dist/orchestrator.js +0 -208
- package/dist/paths.js +0 -68
package/dist/claude.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
const CHANNEL_FLAGS = ['--dangerously-load-development-channels', 'server:metro'];
|
|
3
|
+
export const claudeArgs = (extra) => [
|
|
4
|
+
...CHANNEL_FLAGS,
|
|
5
|
+
...extra,
|
|
6
|
+
];
|
|
7
|
+
export function launchClaude(extra) {
|
|
8
|
+
const leaveToChild = () => undefined;
|
|
9
|
+
process.on('SIGINT', leaveToChild);
|
|
10
|
+
process.on('SIGTERM', leaveToChild);
|
|
11
|
+
const res = spawnSync('claude', claudeArgs(extra), { stdio: 'inherit' });
|
|
12
|
+
if (res.error !== undefined)
|
|
13
|
+
throw new Error('the `claude` command is not on PATH — install Claude Code first');
|
|
14
|
+
return Promise.resolve(res.status ?? 1);
|
|
15
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,221 +1,195 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { claimCode, claimRuntime, mcpServers, NotSignedIn, whoisAuthorized, } from './api.js';
|
|
4
|
+
import { askSecret } from './prompt.js';
|
|
5
|
+
import { clearToken, credentialsPath, metroUrl, metroWebUrl, writeToken, } from './store.js';
|
|
6
|
+
import { detach, lockedBy, probe, runningPid, stopAll, tail } from './control.js';
|
|
7
|
+
import { tailEvents } from './tail.js';
|
|
8
|
+
import { assertAgentId, daemonPlan, localUrl, readRunToken, runDaemon, runtimeDir, runTokenState, writeRunToken, } from './runtime.js';
|
|
9
|
+
import { launchClaude } from './claude.js';
|
|
10
|
+
import { bedrock } from './bedrock.js';
|
|
11
|
+
import { installPlugin, syncPluginServers } from './plugin.js';
|
|
12
|
+
import { update } from './update.js';
|
|
13
|
+
import { serve } from './serve.js';
|
|
14
|
+
import { currentVersion } from './version.js';
|
|
15
|
+
const USAGE = `metro — the command line for your MCP connectors
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
metro
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
metro start <agent-id> [--detach]
|
|
18
|
+
run that agent's stations on this machine
|
|
19
|
+
metro serve [--port <n>] [--tunnel]
|
|
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; --tunnel adds
|
|
22
|
+
a public https address through a Cloudflare quick tunnel (needs
|
|
23
|
+
cloudflared, no account) so the link works from anywhere
|
|
24
|
+
metro stop [agent-id]
|
|
25
|
+
stop every metro daemon on this machine, however it was started
|
|
26
|
+
metro status <agent-id>
|
|
27
|
+
is it running, and is it healthy
|
|
28
|
+
metro logs <agent-id> [-f]
|
|
29
|
+
show the detached daemon's log
|
|
30
|
+
metro tail <agent-id>
|
|
31
|
+
follow this machine's inbound events, one JSON line each
|
|
32
|
+
metro login [code]
|
|
33
|
+
authorize this machine for one agent with a code from its page
|
|
34
|
+
metro logout forget this machine's sign-in
|
|
35
|
+
metro whoami print the account and agent this machine may read
|
|
36
|
+
metro mcp print the mcpServers block of the authorized agent's connectors
|
|
37
|
+
metro plugin set up the Claude Code plugin (connector servers + /metro:login)
|
|
38
|
+
metro claude [args...]
|
|
39
|
+
open Claude Code with the metro channel; every argument is passed through
|
|
40
|
+
metro bedrock [args...]
|
|
41
|
+
the same, with inference on Amazon Bedrock through a local proxy so the
|
|
42
|
+
channel still works (needs AWS_BEARER_TOKEN_BEDROCK and AWS_REGION)
|
|
43
|
+
metro update update to the newest published version
|
|
44
|
+
metro version print this CLI's version
|
|
19
45
|
|
|
20
|
-
|
|
46
|
+
Start Claude Code with all of them, without writing them to disk:
|
|
47
|
+
|
|
48
|
+
claude --mcp-config <(metro mcp)
|
|
49
|
+
|
|
50
|
+
METRO_URL the metro to talk to (default https://mcp.metro.box)
|
|
51
|
+
METRO_UI_URL where the web UI lives (default https://metro.box)
|
|
52
|
+
METRO_TOKEN use this agent sign-in instead of the stored one; with neither,
|
|
53
|
+
a machine running metro start uses its run token
|
|
54
|
+
METRO_RUN_TOKEN use this runtime authorization instead of the stored one
|
|
55
|
+
METRO_BEDROCK_MODEL
|
|
56
|
+
send every request to this Bedrock model id (default: derive from the
|
|
57
|
+
model Claude Code asks for, e.g. eu.anthropic.claude-sonnet-4-6)
|
|
58
|
+
METRO_RUNTIME_DIR run the daemon from this directory instead of the bundled one
|
|
21
59
|
`;
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
process.stdout.write(isJson(f) ? JSON.stringify(structured) + '\n' : human + '\n');
|
|
26
|
-
};
|
|
27
|
-
const maskToken = (t) => !t ? '' : t.length <= 8 ? '••••' : `${t.slice(0, 6)}…${t.slice(-2)}`;
|
|
28
|
-
const TOKEN_KEYS = { telegram: 'TELEGRAM_BOT_TOKEN', discord: 'DISCORD_BOT_TOKEN' };
|
|
29
|
-
function parseArgs(argv) {
|
|
30
|
-
const positional = [];
|
|
31
|
-
const flags = {};
|
|
32
|
-
for (let i = 0; i < argv.length; i++) {
|
|
33
|
-
const a = argv[i];
|
|
34
|
-
if (!a.startsWith('--')) {
|
|
35
|
-
positional.push(a);
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
const eq = a.indexOf('=');
|
|
39
|
-
if (eq !== -1) {
|
|
40
|
-
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
const key = a.slice(2);
|
|
44
|
-
const next = argv[i + 1];
|
|
45
|
-
if (next !== undefined && !next.startsWith('--')) {
|
|
46
|
-
flags[key] = next;
|
|
47
|
-
i++;
|
|
48
|
-
}
|
|
49
|
-
else
|
|
50
|
-
flags[key] = true;
|
|
51
|
-
}
|
|
52
|
-
return { positional, flags };
|
|
60
|
+
function hostLabel() {
|
|
61
|
+
const name = hostname().trim();
|
|
62
|
+
return name === '' ? 'unnamed machine' : name;
|
|
53
63
|
}
|
|
54
|
-
async function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (!flags['no-validate']) {
|
|
64
|
-
process.env[TOKEN_KEYS[sub]] = trimmed;
|
|
65
|
-
try {
|
|
66
|
-
identity = sub === 'telegram' ? `@${(await telegram.getMe()).username}` : (await discord.getMe()).username;
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
delete process.env[TOKEN_KEYS[sub]];
|
|
70
|
-
throw exitErr(`token rejected by ${sub}: ${errMsg(err)} (use --no-validate to save anyway)`, 3);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
const env = readDotenv(CONFIG_ENV_FILE);
|
|
74
|
-
env[TOKEN_KEYS[sub]] = trimmed;
|
|
75
|
-
writeDotenv(CONFIG_ENV_FILE, env);
|
|
76
|
-
const human = `saved ${TOKEN_KEYS[sub]}${identity ? ` (verified as ${identity})` : ''} to ${CONFIG_ENV_FILE} (chmod 0600)`;
|
|
77
|
-
emit(flags, human, { ok: true, saved: TOKEN_KEYS[sub], verified_as: identity ?? null });
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
if (sub === 'clear') {
|
|
81
|
-
const target = value ?? 'all';
|
|
82
|
-
const env = readDotenv(CONFIG_ENV_FILE);
|
|
83
|
-
if (target === 'all') {
|
|
84
|
-
delete env.TELEGRAM_BOT_TOKEN;
|
|
85
|
-
delete env.DISCORD_BOT_TOKEN;
|
|
86
|
-
}
|
|
87
|
-
else if (target === 'telegram' || target === 'discord')
|
|
88
|
-
delete env[TOKEN_KEYS[target]];
|
|
89
|
-
else
|
|
90
|
-
throw new Error(`metro setup clear <telegram|discord|all> — got '${target}'`);
|
|
91
|
-
writeDotenv(CONFIG_ENV_FILE, env);
|
|
92
|
-
emit(flags, `cleared ${target === 'all' ? 'all metro tokens' : TOKEN_KEYS[target]}`, { ok: true, cleared: target });
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
throw new Error(`unknown setup subcommand '${sub}' (try: telegram, discord, clear)`);
|
|
64
|
+
async function authorizeRuntime(agentId) {
|
|
65
|
+
process.stderr.write(`Authorize this machine at ${metroWebUrl()}/#/authorize/${agentId}\n`);
|
|
66
|
+
const code = (await askSecret('Paste the code (input is hidden): ')).trim();
|
|
67
|
+
if (code === '')
|
|
68
|
+
throw new Error('no code given');
|
|
69
|
+
const claimed = await claimRuntime(code, hostLabel());
|
|
70
|
+
writeRunToken(claimed.agent, claimed.token);
|
|
71
|
+
process.stderr.write(`Authorized as '${claimed.label}'.\n`);
|
|
72
|
+
return claimed.token;
|
|
96
73
|
}
|
|
97
|
-
async function
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
74
|
+
async function usableRunToken(agentId) {
|
|
75
|
+
const stored = readRunToken(agentId);
|
|
76
|
+
if (stored === null)
|
|
77
|
+
return authorizeRuntime(agentId);
|
|
78
|
+
const state = await runTokenState(stored);
|
|
79
|
+
if (state !== 'stale')
|
|
80
|
+
return stored;
|
|
81
|
+
process.stderr.write(`metro no longer recognises this machine as the holder of ${agentId}: ` +
|
|
82
|
+
'it was taken back, or another machine claimed it since. Authorize it again.\n');
|
|
83
|
+
return authorizeRuntime(agentId);
|
|
84
|
+
}
|
|
85
|
+
async function start(argv) {
|
|
86
|
+
const agentId = assertAgentId(argv[0]);
|
|
87
|
+
if (runningPid(agentId) !== null || lockedBy() !== null)
|
|
88
|
+
throw new Error('a metro daemon is already running on this machine. ' +
|
|
89
|
+
'Stop it first: metro stop');
|
|
90
|
+
const dir = runtimeDir();
|
|
91
|
+
const token = await usableRunToken(agentId);
|
|
92
|
+
const detached = argv.includes('--detach');
|
|
93
|
+
const plan = daemonPlan({ agentId, token, dir });
|
|
94
|
+
if (!detached)
|
|
95
|
+
return runDaemon(agentId, plan);
|
|
96
|
+
const pid = detach({ agentId, ...plan });
|
|
97
|
+
process.stderr.write(`metro is running for ${agentId} (pid ${String(pid)})\n` +
|
|
98
|
+
` logs: metro logs ${agentId} -f\n` +
|
|
99
|
+
` stop: metro stop ${agentId}\n`);
|
|
100
|
+
return 0;
|
|
116
101
|
}
|
|
117
|
-
async function
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
detail: cfg.telegram || cfg.discord ? `loaded from ${existsSync(CONFIG_ENV_FILE) ? CONFIG_ENV_FILE : 'process env'}` : 'no platform configured — run `metro setup telegram|discord <token>`',
|
|
124
|
-
}];
|
|
125
|
-
for (const [p, getMe] of [['telegram', telegram.getMe], ['discord', discord.getMe]]) {
|
|
126
|
-
if (!cfg[p]) {
|
|
127
|
-
checks.push({ name: p, ok: null, detail: 'not configured' });
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
try {
|
|
131
|
-
const me = await getMe();
|
|
132
|
-
checks.push({ name: p, ok: true, detail: `getMe → ${p === 'telegram' ? '@' : ''}${me.username}` });
|
|
133
|
-
}
|
|
134
|
-
catch (err) {
|
|
135
|
-
checks.push({ name: p, ok: false, detail: errMsg(err) });
|
|
136
|
-
}
|
|
102
|
+
async function stopDaemon(argv) {
|
|
103
|
+
const agentId = argv[0] === undefined ? undefined : assertAgentId(argv[0]);
|
|
104
|
+
const stopped = await stopAll(agentId);
|
|
105
|
+
if (stopped.length === 0) {
|
|
106
|
+
process.stderr.write('no metro daemon is running on this machine\n');
|
|
107
|
+
return 1;
|
|
137
108
|
}
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
checks.push({ name: 'orchestrator', ok: null, detail: 'stale lockfile (will auto-reclaim on next start)' });
|
|
151
|
-
}
|
|
152
|
-
if (isJson(flags))
|
|
153
|
-
process.stdout.write(JSON.stringify({ checks }) + '\n');
|
|
154
|
-
else {
|
|
155
|
-
process.stdout.write('metro doctor\n\n');
|
|
156
|
-
for (const c of checks)
|
|
157
|
-
process.stdout.write(` ${c.ok === true ? '✓' : c.ok === false ? '✗' : '–'} ${c.name.padEnd(15)} ${c.detail}\n`);
|
|
158
|
-
process.stdout.write('\n');
|
|
109
|
+
for (const d of stopped)
|
|
110
|
+
process.stderr.write(`Stopped metro (pid ${String(d.pid)}, via ${d.via})\n`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
async function status(argv) {
|
|
114
|
+
const agentId = assertAgentId(argv[0]);
|
|
115
|
+
const pid = runningPid(agentId);
|
|
116
|
+
if (pid === null) {
|
|
117
|
+
process.stdout.write(`${agentId}: not running\n`);
|
|
118
|
+
return 1;
|
|
159
119
|
}
|
|
160
|
-
|
|
161
|
-
|
|
120
|
+
const health = await probe(localUrl());
|
|
121
|
+
process.stdout.write(`${agentId}: running (pid ${String(pid)}) · ` +
|
|
122
|
+
(health === null
|
|
123
|
+
? `not answering on ${localUrl()}\n`
|
|
124
|
+
: `healthy, up ${String(health.uptime)}s\n`));
|
|
125
|
+
return health === null ? 1 : 0;
|
|
126
|
+
}
|
|
127
|
+
function logs(argv) {
|
|
128
|
+
const agentId = assertAgentId(argv[0]);
|
|
129
|
+
return tail(agentId, argv.includes('-f') || argv.includes('--follow'));
|
|
130
|
+
}
|
|
131
|
+
async function pastedCode(given) {
|
|
132
|
+
if (given !== undefined && given.trim() !== '')
|
|
133
|
+
return given.trim();
|
|
134
|
+
process.stderr.write(`Get a pairing code from the agent's page, or pick the agent at ${metroWebUrl()}/#/authorize\n`);
|
|
135
|
+
return askSecret('Paste the code here (input is hidden): ');
|
|
136
|
+
}
|
|
137
|
+
async function login(given) {
|
|
138
|
+
const { token, email, agent } = await claimCode(await pastedCode(given));
|
|
139
|
+
writeToken(token);
|
|
140
|
+
process.stderr.write(`Authorized '${agent}' for ${email}. Stored in ${credentialsPath()}\n`);
|
|
141
|
+
if (syncPluginServers())
|
|
142
|
+
process.stderr.write('Claude Code plugin refreshed — new sessions have the connectors; ' +
|
|
143
|
+
'run /reload-plugins in any session already open.\n');
|
|
162
144
|
}
|
|
163
|
-
async function
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
if (!res.ok)
|
|
167
|
-
throw new Error(`npm registry: ${res.status}`);
|
|
168
|
-
const latest = (await res.json())['dist-tags']?.[tag];
|
|
169
|
-
if (!latest)
|
|
170
|
-
throw new Error(`no '${tag}' dist-tag for @stage-labs/metro`);
|
|
171
|
-
if (latest === pkg.version)
|
|
172
|
-
return emit(flags, `already on ${pkg.version} (latest ${tag})`, { ok: true, current: pkg.version, latest, upgraded: false });
|
|
173
|
-
const argv1 = process.argv[1] ?? '';
|
|
174
|
-
const spec = `@stage-labs/metro@${tag}`;
|
|
175
|
-
const argv = argv1.includes('/.bun/') || argv1.includes('\\bun\\') ? ['bun', 'add', '-g', spec]
|
|
176
|
-
: argv1.includes('/pnpm/') || argv1.includes('\\pnpm\\') ? ['pnpm', 'add', '-g', spec]
|
|
177
|
-
: ['npm', 'install', '-g', spec];
|
|
178
|
-
if (isJson(flags))
|
|
179
|
-
process.stdout.write(JSON.stringify({ ok: true, current: pkg.version, latest, command: argv.join(' ') }) + '\n');
|
|
180
|
-
else
|
|
181
|
-
process.stdout.write(`metro ${pkg.version} → ${latest}\n$ ${argv.join(' ')}\n`);
|
|
182
|
-
await new Promise((resolve, reject) => {
|
|
183
|
-
const child = spawn(argv[0], argv.slice(1), { stdio: isJson(flags) ? 'ignore' : 'inherit' });
|
|
184
|
-
child.on('exit', code => code === 0 ? resolve() : reject(new Error(`${argv[0]} exited ${code}`)));
|
|
185
|
-
child.on('error', reject);
|
|
186
|
-
});
|
|
145
|
+
async function whoami() {
|
|
146
|
+
const { email, agent } = await whoisAuthorized();
|
|
147
|
+
process.stdout.write(`${email} · agent '${agent}' on ${metroUrl()}\n`);
|
|
187
148
|
}
|
|
149
|
+
const HELP = new Set([undefined, 'help', '--help', '-h']);
|
|
188
150
|
const COMMANDS = {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
151
|
+
login: async () => {
|
|
152
|
+
await login(process.argv[3]);
|
|
153
|
+
return 0;
|
|
154
|
+
},
|
|
155
|
+
logout: async () => {
|
|
156
|
+
clearToken();
|
|
157
|
+
process.stderr.write('Signed out.\n');
|
|
158
|
+
return Promise.resolve(0);
|
|
159
|
+
},
|
|
160
|
+
whoami: async () => {
|
|
161
|
+
await whoami();
|
|
162
|
+
return 0;
|
|
163
|
+
},
|
|
164
|
+
start: () => start(process.argv.slice(3)),
|
|
165
|
+
serve: () => serve(process.argv.slice(3)),
|
|
166
|
+
stop: () => stopDaemon(process.argv.slice(3)),
|
|
167
|
+
status: () => status(process.argv.slice(3)),
|
|
168
|
+
logs: () => logs(process.argv.slice(3)),
|
|
169
|
+
tail: () => tailEvents(process.argv.slice(3)),
|
|
170
|
+
mcp: async () => {
|
|
171
|
+
process.stdout.write(`${await mcpServers()}\n`);
|
|
172
|
+
return 0;
|
|
173
|
+
},
|
|
174
|
+
plugin: installPlugin,
|
|
175
|
+
claude: () => launchClaude(process.argv.slice(3)),
|
|
176
|
+
bedrock: () => bedrock(process.argv.slice(3)),
|
|
177
|
+
update,
|
|
178
|
+
version: async () => {
|
|
179
|
+
process.stdout.write(`${currentVersion()}\n`);
|
|
180
|
+
return Promise.resolve(0);
|
|
181
|
+
},
|
|
192
182
|
};
|
|
193
|
-
async function
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
196
|
-
return
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (!cmd) {
|
|
200
|
-
await import('./orchestrator.js');
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
const handler = COMMANDS[cmd];
|
|
204
|
-
if (!handler) {
|
|
205
|
-
process.stderr.write(`unknown command '${cmd}'\n\n${USAGE}`);
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
const { positional, flags } = parseArgs(process.argv.slice(3));
|
|
209
|
-
try {
|
|
210
|
-
await handler(positional, flags);
|
|
211
|
-
}
|
|
212
|
-
catch (err) {
|
|
213
|
-
const code = err.code;
|
|
214
|
-
if (isJson(flags))
|
|
215
|
-
process.stdout.write(JSON.stringify({ ok: false, error: errMsg(err), code: code ?? 1 }) + '\n');
|
|
216
|
-
else
|
|
217
|
-
process.stderr.write(`error: ${errMsg(err)}\n`);
|
|
218
|
-
process.exit(typeof code === 'number' ? code : 1);
|
|
219
|
-
}
|
|
183
|
+
async function run(command) {
|
|
184
|
+
const handler = command === undefined ? undefined : COMMANDS[command];
|
|
185
|
+
if (handler !== undefined)
|
|
186
|
+
return handler();
|
|
187
|
+
process.stderr.write(USAGE);
|
|
188
|
+
return Promise.resolve(HELP.has(command) ? 0 : 1);
|
|
220
189
|
}
|
|
221
|
-
await
|
|
190
|
+
const code = await run(process.argv[2]).catch((err) => {
|
|
191
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
192
|
+
process.stderr.write(`metro: ${message}\n`);
|
|
193
|
+
return err instanceof NotSignedIn ? 2 : 1;
|
|
194
|
+
});
|
|
195
|
+
process.exit(code);
|
package/dist/control.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
const STATE = () => join(homedir(), '.metro');
|
|
6
|
+
export function serveStateDir() {
|
|
7
|
+
const xdg = process.env.XDG_CACHE_HOME?.trim();
|
|
8
|
+
const base = xdg === undefined || xdg === '' ? join(homedir(), '.cache') : xdg;
|
|
9
|
+
return join(base, 'metro', 'serve');
|
|
10
|
+
}
|
|
11
|
+
const serveLockPath = () => join(serveStateDir(), '.tail-lock');
|
|
12
|
+
const pidPath = (agentId) => join(STATE(), `run-${agentId}.pid`);
|
|
13
|
+
export const logPath = (agentId) => join(STATE(), 'logs', `${agentId}.log`);
|
|
14
|
+
function ensureStateDirs() {
|
|
15
|
+
mkdirSync(STATE(), { recursive: true, mode: 0o700 });
|
|
16
|
+
mkdirSync(join(STATE(), 'logs'), { recursive: true, mode: 0o700 });
|
|
17
|
+
}
|
|
18
|
+
function readPid(agentId) {
|
|
19
|
+
try {
|
|
20
|
+
const pid = Number(readFileSync(pidPath(agentId), 'utf8').trim());
|
|
21
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function isAlive(pid) {
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function runningPid(agentId) {
|
|
37
|
+
const pid = readPid(agentId);
|
|
38
|
+
if (pid === null)
|
|
39
|
+
return null;
|
|
40
|
+
if (isAlive(pid))
|
|
41
|
+
return pid;
|
|
42
|
+
rmSync(pidPath(agentId), { force: true });
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function writePid(agentId, pid) {
|
|
46
|
+
writeFileSync(pidPath(agentId), String(pid));
|
|
47
|
+
}
|
|
48
|
+
function clearPid(agentId) {
|
|
49
|
+
rmSync(pidPath(agentId), { force: true });
|
|
50
|
+
}
|
|
51
|
+
export function detach(opts) {
|
|
52
|
+
ensureStateDirs();
|
|
53
|
+
const out = openSync(logPath(opts.agentId), 'a');
|
|
54
|
+
const child = spawn(opts.command, opts.args, {
|
|
55
|
+
cwd: opts.cwd,
|
|
56
|
+
env: opts.env,
|
|
57
|
+
detached: true,
|
|
58
|
+
stdio: ['ignore', out, out],
|
|
59
|
+
});
|
|
60
|
+
child.unref();
|
|
61
|
+
const pid = child.pid ?? 0;
|
|
62
|
+
if (pid === 0)
|
|
63
|
+
throw new Error('could not start the metro daemon');
|
|
64
|
+
writePid(opts.agentId, pid);
|
|
65
|
+
return pid;
|
|
66
|
+
}
|
|
67
|
+
async function stopPid(pid, waitMs) {
|
|
68
|
+
process.kill(pid, 'SIGTERM');
|
|
69
|
+
const until = Date.now() + waitMs;
|
|
70
|
+
while (Date.now() < until) {
|
|
71
|
+
if (!isAlive(pid))
|
|
72
|
+
return;
|
|
73
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
74
|
+
}
|
|
75
|
+
process.kill(pid, 'SIGKILL');
|
|
76
|
+
}
|
|
77
|
+
const lockPath = () => process.env.METRO_STATE_DIR?.trim()
|
|
78
|
+
? join(process.env.METRO_STATE_DIR.trim(), '.tail-lock')
|
|
79
|
+
: join(homedir(), '.cache', 'metro', '.tail-lock');
|
|
80
|
+
function commandOf(pid) {
|
|
81
|
+
try {
|
|
82
|
+
return execFileSync('ps', ['-o', 'command=', '-p', String(pid)], {
|
|
83
|
+
encoding: 'utf8',
|
|
84
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const looksLikeMetro = (command) => command.includes('server.ts') || command.includes('metro');
|
|
92
|
+
function holderOf(lockFile) {
|
|
93
|
+
let pid;
|
|
94
|
+
try {
|
|
95
|
+
pid = Number(readFileSync(lockFile, 'utf8').trim());
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (!Number.isInteger(pid) || pid <= 0 || !isAlive(pid))
|
|
101
|
+
return null;
|
|
102
|
+
return looksLikeMetro(commandOf(pid)) ? pid : null;
|
|
103
|
+
}
|
|
104
|
+
export const lockedBy = () => holderOf(lockPath());
|
|
105
|
+
export const serveLockedBy = () => holderOf(serveLockPath());
|
|
106
|
+
function trackedAgents(agentId) {
|
|
107
|
+
const tracked = new Set();
|
|
108
|
+
if (agentId !== undefined)
|
|
109
|
+
tracked.add(agentId);
|
|
110
|
+
try {
|
|
111
|
+
for (const name of readdirSync(STATE())) {
|
|
112
|
+
const found = /^run-(.+)\.pid$/.exec(name);
|
|
113
|
+
if (found?.[1] !== undefined)
|
|
114
|
+
tracked.add(found[1]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return tracked;
|
|
119
|
+
}
|
|
120
|
+
return tracked;
|
|
121
|
+
}
|
|
122
|
+
export async function stopAll(agentId, waitMs = 10_000) {
|
|
123
|
+
const stopped = [];
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
for (const id of trackedAgents(agentId)) {
|
|
126
|
+
const pid = runningPid(id);
|
|
127
|
+
if (pid === null || seen.has(pid))
|
|
128
|
+
continue;
|
|
129
|
+
seen.add(pid);
|
|
130
|
+
await stopPid(pid, waitMs);
|
|
131
|
+
clearPid(id);
|
|
132
|
+
stopped.push({ pid, via: id });
|
|
133
|
+
}
|
|
134
|
+
const holder = lockedBy();
|
|
135
|
+
if (holder !== null && !seen.has(holder)) {
|
|
136
|
+
seen.add(holder);
|
|
137
|
+
await stopPid(holder, waitMs);
|
|
138
|
+
rmSync(lockPath(), { force: true });
|
|
139
|
+
stopped.push({ pid: holder, via: 'the machine lock' });
|
|
140
|
+
}
|
|
141
|
+
const served = serveLockedBy();
|
|
142
|
+
if (served !== null && !seen.has(served)) {
|
|
143
|
+
await stopPid(served, waitMs);
|
|
144
|
+
rmSync(serveLockPath(), { force: true });
|
|
145
|
+
stopped.push({ pid: served, via: 'metro serve' });
|
|
146
|
+
}
|
|
147
|
+
return stopped;
|
|
148
|
+
}
|
|
149
|
+
export function tail(agentId, follow) {
|
|
150
|
+
const path = logPath(agentId);
|
|
151
|
+
if (!existsSync(path))
|
|
152
|
+
throw new Error(`no log for ${agentId} yet — it is only written when started with --detach`);
|
|
153
|
+
const args = follow ? ['-n', '200', '-f', path] : ['-n', '200', path];
|
|
154
|
+
const child = spawn('tail', args, { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
child.on('exit', (code) => {
|
|
157
|
+
resolve(code ?? 0);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
export async function probe(url) {
|
|
162
|
+
try {
|
|
163
|
+
const res = await fetch(`${url}/health`, {
|
|
164
|
+
signal: AbortSignal.timeout(3_000),
|
|
165
|
+
});
|
|
166
|
+
if (!res.ok)
|
|
167
|
+
return null;
|
|
168
|
+
return (await res.json());
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|