@stage-labs/metro 0.1.0-beta.6 → 0.1.0-beta.60
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 +14 -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 +94 -0
- package/dist/control.js +74 -0
- package/dist/local.js +93 -0
- package/dist/plugin.js +80 -0
- package/dist/runtime-install.js +84 -0
- package/dist/runtime.js +63 -0
- package/dist/serve.js +147 -0
- package/dist/tail.js +119 -0
- package/dist/update.js +45 -0
- package/dist/version.js +97 -0
- package/package.json +14 -25
- package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -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 +63 -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 +302 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +138 -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 +95 -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 +128 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +280 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +199 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/bundle-api.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-api.ts +97 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-files.ts +329 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +39 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +311 -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 +98 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +70 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +330 -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/http.ts +368 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-key.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-registry.ts +13 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-routes.ts +54 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +7 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/is-record.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-cli-api.ts +70 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +196 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-owner.ts +14 -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 +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +159 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +117 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +109 -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 +302 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/return-to.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-deps.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/server-types.ts +21 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/servers-api.ts +80 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +29 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +58 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/signed-identity.ts +36 -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 +352 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +116 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +294 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +78 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-types.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/version.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +19 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/address.ts +6 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +43 -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 +32 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +144 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +39 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +77 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +365 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +161 -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/local-connectors.ts +322 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +253 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +33 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/servers.ts +83 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/stations.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +109 -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 +191 -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 +47 -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 +237 -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 +60 -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 +184 -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 +26 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -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 +119 -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 +37 -0
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -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 +112 -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/stations.json +35 -0
- package/runtime/trains/.keep +0 -0
- package/LICENSE +0 -21
- package/README.md +0 -192
- package/dist/cache.js +0 -74
- package/dist/cli/actions.js +0 -156
- package/dist/cli/config.js +0 -184
- package/dist/cli/index.js +0 -162
- package/dist/cli/skill.js +0 -62
- package/dist/cli/util.js +0 -72
- package/dist/codex-rc.js +0 -236
- package/dist/dispatcher.js +0 -112
- package/dist/history.js +0 -84
- package/dist/ipc.js +0 -71
- package/dist/log.js +0 -15
- package/dist/paths.js +0 -68
- package/dist/stations/discord.js +0 -198
- package/dist/stations/index.js +0 -73
- package/dist/stations/telegram-md.js +0 -34
- package/dist/stations/telegram-upload.js +0 -113
- package/dist/stations/telegram.js +0 -235
- package/docs/agents.md +0 -168
- package/docs/uri-scheme.md +0 -71
- package/skills/metro/SKILL.md +0 -220
package/dist/control.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
export function serveStateDir() {
|
|
6
|
+
const xdg = process.env.XDG_CACHE_HOME?.trim();
|
|
7
|
+
const base = xdg === undefined || xdg === '' ? join(homedir(), '.cache') : xdg;
|
|
8
|
+
return join(base, 'metro', 'serve');
|
|
9
|
+
}
|
|
10
|
+
const serveLockPath = () => join(serveStateDir(), '.tail-lock');
|
|
11
|
+
function isAlive(pid) {
|
|
12
|
+
try {
|
|
13
|
+
process.kill(pid, 0);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function stopPid(pid, waitMs) {
|
|
21
|
+
process.kill(pid, 'SIGTERM');
|
|
22
|
+
const until = Date.now() + waitMs;
|
|
23
|
+
while (Date.now() < until) {
|
|
24
|
+
if (!isAlive(pid))
|
|
25
|
+
return;
|
|
26
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
27
|
+
}
|
|
28
|
+
process.kill(pid, 'SIGKILL');
|
|
29
|
+
}
|
|
30
|
+
const lockPath = () => process.env.METRO_STATE_DIR?.trim()
|
|
31
|
+
? join(process.env.METRO_STATE_DIR.trim(), '.tail-lock')
|
|
32
|
+
: join(homedir(), '.cache', 'metro', '.tail-lock');
|
|
33
|
+
function commandOf(pid) {
|
|
34
|
+
try {
|
|
35
|
+
return execFileSync('ps', ['-o', 'command=', '-p', String(pid)], {
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const looksLikeMetro = (command) => command.includes('server.ts') || command.includes('metro');
|
|
45
|
+
function holderOf(lockFile) {
|
|
46
|
+
let pid;
|
|
47
|
+
try {
|
|
48
|
+
pid = Number(readFileSync(lockFile, 'utf8').trim());
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (!Number.isInteger(pid) || pid <= 0 || !isAlive(pid))
|
|
54
|
+
return null;
|
|
55
|
+
return looksLikeMetro(commandOf(pid)) ? pid : null;
|
|
56
|
+
}
|
|
57
|
+
export const lockedBy = () => holderOf(lockPath());
|
|
58
|
+
export const serveLockedBy = () => holderOf(serveLockPath());
|
|
59
|
+
export async function stopAll(waitMs = 10_000) {
|
|
60
|
+
const stopped = [];
|
|
61
|
+
const holder = lockedBy();
|
|
62
|
+
if (holder !== null) {
|
|
63
|
+
await stopPid(holder, waitMs);
|
|
64
|
+
rmSync(lockPath(), { force: true });
|
|
65
|
+
stopped.push({ pid: holder, via: 'the machine lock' });
|
|
66
|
+
}
|
|
67
|
+
const served = serveLockedBy();
|
|
68
|
+
if (served !== null && served !== holder) {
|
|
69
|
+
await stopPid(served, waitMs);
|
|
70
|
+
rmSync(serveLockPath(), { force: true });
|
|
71
|
+
stopped.push({ pid: served, via: 'metro serve' });
|
|
72
|
+
}
|
|
73
|
+
return stopped;
|
|
74
|
+
}
|
package/dist/local.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { localUrl } from './runtime.js';
|
|
5
|
+
const PROBE_MS = 3_000;
|
|
6
|
+
const READ_MS = 20_000;
|
|
7
|
+
export function agentsDir() {
|
|
8
|
+
const explicit = process.env.METRO_AGENTS_DIR?.trim();
|
|
9
|
+
return explicit !== undefined && explicit !== '' ? explicit : join(homedir(), '.metro', 'agents');
|
|
10
|
+
}
|
|
11
|
+
export function localAgents(dir = agentsDir()) {
|
|
12
|
+
if (!existsSync(dir))
|
|
13
|
+
return [];
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
16
|
+
if (!entry.isDirectory())
|
|
17
|
+
continue;
|
|
18
|
+
const path = join(dir, entry.name, 'agent.json');
|
|
19
|
+
if (!existsSync(path))
|
|
20
|
+
continue;
|
|
21
|
+
try {
|
|
22
|
+
const file = JSON.parse(readFileSync(path, 'utf8'));
|
|
23
|
+
if (typeof file.id === 'string' && typeof file.name === 'string' && typeof file.key === 'string')
|
|
24
|
+
out.push({ id: file.id, name: file.name, key: file.key });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
31
|
+
}
|
|
32
|
+
export function localStations(dir = agentsDir()) {
|
|
33
|
+
if (!existsSync(dir))
|
|
34
|
+
return [];
|
|
35
|
+
const out = new Set();
|
|
36
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
37
|
+
if (!entry.isDirectory())
|
|
38
|
+
continue;
|
|
39
|
+
const path = join(dir, entry.name, 'agent.json');
|
|
40
|
+
if (!existsSync(path))
|
|
41
|
+
continue;
|
|
42
|
+
try {
|
|
43
|
+
const file = JSON.parse(readFileSync(path, 'utf8'));
|
|
44
|
+
if (!Array.isArray(file.stations))
|
|
45
|
+
continue;
|
|
46
|
+
for (const s of file.stations) {
|
|
47
|
+
const station = s.station;
|
|
48
|
+
if (typeof station === 'string')
|
|
49
|
+
out.add(station);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return [...out].sort();
|
|
57
|
+
}
|
|
58
|
+
export function pickLocalAgent(agents, wanted) {
|
|
59
|
+
if (wanted !== undefined && wanted !== '') {
|
|
60
|
+
const found = agents.find((a) => a.name === wanted || a.id === wanted);
|
|
61
|
+
if (found === undefined)
|
|
62
|
+
throw new Error(`no local agent named '${wanted}'`);
|
|
63
|
+
return found;
|
|
64
|
+
}
|
|
65
|
+
const only = agents[0];
|
|
66
|
+
if (only !== undefined && agents.length === 1)
|
|
67
|
+
return only;
|
|
68
|
+
throw new Error(agents.length === 0
|
|
69
|
+
? 'no agent on this machine yet'
|
|
70
|
+
: `several agents on this machine — name one: ${agents.map((a) => a.name).join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
export async function localDaemonUp(base = localUrl()) {
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch(`${base}/api/mode`, { signal: AbortSignal.timeout(PROBE_MS) });
|
|
75
|
+
const body = (await res.json());
|
|
76
|
+
return res.ok && body.mode === 'local';
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function localMcpServers(agent, base = localUrl()) {
|
|
83
|
+
const res = await fetch(`${base}/api/cli/mcp`, {
|
|
84
|
+
headers: { authorization: `Bearer ${agent.key}` },
|
|
85
|
+
signal: AbortSignal.timeout(READ_MS),
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok)
|
|
88
|
+
throw new Error(`the local daemon answered ${String(res.status)} for the connectors`);
|
|
89
|
+
const body = (await res.json());
|
|
90
|
+
if (typeof body.json !== 'string')
|
|
91
|
+
throw new Error('the local daemon returned an unexpected response');
|
|
92
|
+
return body.json;
|
|
93
|
+
}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
const MARKETPLACE_REPO = 'bonustrack/metro';
|
|
6
|
+
const PLUGIN_SPEC = 'metro@metro';
|
|
7
|
+
export function installPathFrom(raw) {
|
|
8
|
+
let parsed;
|
|
9
|
+
try {
|
|
10
|
+
parsed = JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
16
|
+
return null;
|
|
17
|
+
const plugins = parsed.plugins;
|
|
18
|
+
if (typeof plugins !== 'object' || plugins === null)
|
|
19
|
+
return null;
|
|
20
|
+
const entries = plugins[PLUGIN_SPEC];
|
|
21
|
+
if (!Array.isArray(entries))
|
|
22
|
+
return null;
|
|
23
|
+
const first = entries[0];
|
|
24
|
+
return typeof first?.installPath === 'string' ? first.installPath : null;
|
|
25
|
+
}
|
|
26
|
+
export function pluginInstallPath() {
|
|
27
|
+
const registry = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = readFileSync(registry, 'utf8');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const path = installPathFrom(raw);
|
|
36
|
+
if (path === null)
|
|
37
|
+
return null;
|
|
38
|
+
return existsSync(join(path, 'bin', 'metro-plugin.mjs')) ? path : null;
|
|
39
|
+
}
|
|
40
|
+
function runClaude(args) {
|
|
41
|
+
const res = spawnSync('claude', args, { encoding: 'utf8' });
|
|
42
|
+
if (res.error !== undefined)
|
|
43
|
+
throw new Error('the `claude` command is not on PATH — install Claude Code first');
|
|
44
|
+
const output = `${res.stdout ?? ''}${res.stderr ?? ''}`;
|
|
45
|
+
return { ok: res.status === 0, output };
|
|
46
|
+
}
|
|
47
|
+
const tolerable = (output) => /already|exists|latest version/i.test(output);
|
|
48
|
+
export function syncPluginServers() {
|
|
49
|
+
const path = pluginInstallPath();
|
|
50
|
+
if (path === null)
|
|
51
|
+
return false;
|
|
52
|
+
const script = join(path, 'bin', 'metro-plugin.mjs');
|
|
53
|
+
const refreshed = spawnSync(process.execPath, [script, 'refresh'], {
|
|
54
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
55
|
+
});
|
|
56
|
+
if (refreshed.status !== 0)
|
|
57
|
+
return false;
|
|
58
|
+
runClaude(['plugin', 'update', PLUGIN_SPEC]);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
export async function installPlugin() {
|
|
62
|
+
const added = runClaude(['plugin', 'marketplace', 'add', MARKETPLACE_REPO]);
|
|
63
|
+
if (!added.ok && !tolerable(added.output)) {
|
|
64
|
+
process.stderr.write(added.output);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
const installed = runClaude(['plugin', 'install', PLUGIN_SPEC]);
|
|
68
|
+
if (!installed.ok && !tolerable(installed.output)) {
|
|
69
|
+
process.stderr.write(installed.output);
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
process.stderr.write('Claude Code plugin installed.\n');
|
|
73
|
+
if (syncPluginServers()) {
|
|
74
|
+
process.stderr.write('Connector servers loaded — new Claude sessions have them; ' +
|
|
75
|
+
'run /reload-plugins in any session already open.\n');
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
process.stderr.write('Start the daemon on this machine (metro serve), then run /metro:refresh inside Claude Code.\n');
|
|
79
|
+
return Promise.resolve(0);
|
|
80
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { localStations } from './local.js';
|
|
6
|
+
import { SERVER_ENTRY, findBun, runtimeDir } from './runtime.js';
|
|
7
|
+
export const MANIFEST_FILE = 'stations.json';
|
|
8
|
+
const METRO_SOURCES = join('node_modules', '@metro-labs');
|
|
9
|
+
const INSTALL_TIMEOUT_MS = 15 * 60_000;
|
|
10
|
+
export function runtimeStore() {
|
|
11
|
+
const env = process.env.METRO_RUNTIME_STORE?.trim() ?? '';
|
|
12
|
+
return env === '' ? join(homedir(), '.metro', 'runtime') : env;
|
|
13
|
+
}
|
|
14
|
+
const isRecord = (v) => typeof v === 'object' && v !== null;
|
|
15
|
+
const ranges = (raw) => isRecord(raw)
|
|
16
|
+
? Object.fromEntries(Object.entries(raw).filter((e) => typeof e[1] === 'string'))
|
|
17
|
+
: {};
|
|
18
|
+
export function readManifest(path) {
|
|
19
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
20
|
+
if (!isRecord(raw))
|
|
21
|
+
throw new Error(`${path} is not a runtime manifest`);
|
|
22
|
+
const stations = isRecord(raw.stations) ? raw.stations : {};
|
|
23
|
+
return {
|
|
24
|
+
core: ranges(raw.core),
|
|
25
|
+
stations: Object.fromEntries(Object.entries(stations).map(([name, deps]) => [name, ranges(deps)])),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function dependenciesFor(manifest, stations) {
|
|
29
|
+
const out = { ...manifest.core };
|
|
30
|
+
for (const station of new Set(stations))
|
|
31
|
+
Object.assign(out, manifest.stations[station] ?? {});
|
|
32
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
|
|
33
|
+
}
|
|
34
|
+
const packageText = (deps) => `${JSON.stringify({ name: 'metro-runtime', private: true, dependencies: deps }, null, 2)}\n`;
|
|
35
|
+
function readOrNull(path) {
|
|
36
|
+
try {
|
|
37
|
+
return readFileSync(path, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function syncSources(sources, store) {
|
|
44
|
+
const stamp = readOrNull(join(sources, 'runtime.json'));
|
|
45
|
+
if (stamp !== null && readOrNull(join(store, 'runtime.json')) === stamp)
|
|
46
|
+
return false;
|
|
47
|
+
rmSync(join(store, METRO_SOURCES), { recursive: true, force: true });
|
|
48
|
+
cpSync(join(sources, METRO_SOURCES), join(store, METRO_SOURCES), { recursive: true });
|
|
49
|
+
if (stamp !== null)
|
|
50
|
+
writeFileSync(join(store, 'runtime.json'), stamp);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
export function installDependencies(store, deps, bun, log) {
|
|
54
|
+
const wanted = packageText(deps);
|
|
55
|
+
const marker = join(store, 'node_modules', '.metro-installed');
|
|
56
|
+
if (existsSync(marker) && readOrNull(join(store, 'package.json')) === wanted)
|
|
57
|
+
return false;
|
|
58
|
+
writeFileSync(join(store, 'package.json'), wanted);
|
|
59
|
+
log(`Installing the channel SDKs this machine needs into ${store}`);
|
|
60
|
+
const run = spawnSync(bun, ['install', '--no-summary', '--no-progress'], {
|
|
61
|
+
cwd: store,
|
|
62
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
63
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
64
|
+
});
|
|
65
|
+
if (run.error !== undefined || run.status !== 0)
|
|
66
|
+
throw new Error(`bun install failed in ${store}: ${run.error?.message ?? `exit ${String(run.status)}`}`);
|
|
67
|
+
writeFileSync(marker, `${new Date().toISOString()}\n`);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
export function prepareRuntime(opts = {}) {
|
|
71
|
+
const sources = opts.sources ?? runtimeDir();
|
|
72
|
+
const manifestPath = join(sources, MANIFEST_FILE);
|
|
73
|
+
if (!existsSync(manifestPath))
|
|
74
|
+
return { dir: sources, entry: join(sources, SERVER_ENTRY), trains: join(sources, 'trains'), manifest: null };
|
|
75
|
+
const store = opts.store ?? runtimeStore();
|
|
76
|
+
const log = opts.log ??
|
|
77
|
+
((line) => {
|
|
78
|
+
process.stderr.write(`${line}\n`);
|
|
79
|
+
});
|
|
80
|
+
mkdirSync(join(store, 'trains'), { recursive: true });
|
|
81
|
+
syncSources(sources, store);
|
|
82
|
+
installDependencies(store, dependenciesFor(readManifest(manifestPath), localStations(opts.agents)), opts.bun ?? findBun(), log);
|
|
83
|
+
return { dir: store, entry: join(store, SERVER_ENTRY), trains: join(store, 'trains'), manifest: manifestPath };
|
|
84
|
+
}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const AGENT_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{10}$/;
|
|
6
|
+
export const SERVER_ENTRY = join('node_modules', '@metro-labs', 'mcp', 'src', 'server.ts');
|
|
7
|
+
export class MissingRuntime extends Error {
|
|
8
|
+
}
|
|
9
|
+
export function runtimeDir() {
|
|
10
|
+
const explicit = process.env.METRO_RUNTIME_DIR?.trim();
|
|
11
|
+
const dir = explicit !== undefined && explicit !== ''
|
|
12
|
+
? explicit
|
|
13
|
+
: join(dirname(dirname(fileURLToPath(import.meta.url))), 'runtime');
|
|
14
|
+
if (!existsSync(join(dir, SERVER_ENTRY)))
|
|
15
|
+
throw new MissingRuntime(`no metro daemon at ${dir}. Reinstall with: npm i -g @stage-labs/metro@beta`);
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
class MissingBun extends Error {
|
|
19
|
+
}
|
|
20
|
+
export function assertAgentId(id) {
|
|
21
|
+
if (id === undefined || !AGENT_RE.test(id))
|
|
22
|
+
throw new Error(`'${id ?? ''}' is not an agent id — copy it from the agent's page in the web UI`);
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
export function findBun() {
|
|
26
|
+
const found = spawnSync('bun', ['--version'], { stdio: 'ignore' });
|
|
27
|
+
if (found.error !== undefined || found.status !== 0)
|
|
28
|
+
throw new MissingBun('metro serve needs Bun, which is not on PATH.\n' +
|
|
29
|
+
'Install it with: curl -fsSL https://bun.sh/install | bash');
|
|
30
|
+
return 'bun';
|
|
31
|
+
}
|
|
32
|
+
export const localPort = () => Number(process.env.METRO_WEBHOOK_PORT) || 8420;
|
|
33
|
+
export const localUrl = () => `http://127.0.0.1:${String(localPort())}`;
|
|
34
|
+
export const RESTART_CODE = 75;
|
|
35
|
+
function spawnOnce(plan) {
|
|
36
|
+
const child = spawn(plan.command, plan.args, {
|
|
37
|
+
cwd: plan.cwd,
|
|
38
|
+
env: plan.env,
|
|
39
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
40
|
+
});
|
|
41
|
+
const relay = (sig) => () => {
|
|
42
|
+
child.kill(sig);
|
|
43
|
+
};
|
|
44
|
+
const onInt = relay('SIGINT');
|
|
45
|
+
const onTerm = relay('SIGTERM');
|
|
46
|
+
process.on('SIGINT', onInt);
|
|
47
|
+
process.on('SIGTERM', onTerm);
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
child.on('exit', (code) => {
|
|
50
|
+
process.off('SIGINT', onInt);
|
|
51
|
+
process.off('SIGTERM', onTerm);
|
|
52
|
+
resolve(code ?? 0);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
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
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { agentsDir } from './local.js';
|
|
5
|
+
import { currentVersion } from './version.js';
|
|
6
|
+
import { serveLockedBy, serveStateDir } from './control.js';
|
|
7
|
+
import { findBun, localPort, SERVER_ENTRY, spawnPlan } from './runtime.js';
|
|
8
|
+
import { prepareRuntime } from './runtime-install.js';
|
|
9
|
+
const SCRUBBED = new Set(['METRO_RUN_TOKEN', 'METRO_AGENT', 'DATABASE_URL']);
|
|
10
|
+
const PORT_FLAG = /^--port=(.*)$/;
|
|
11
|
+
const OWNER_FLAG = /^--owner=(.*)$/;
|
|
12
|
+
const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
13
|
+
const USAGE = 'usage: metro serve [--port <n>] [--tunnel] [--owner <address>]';
|
|
14
|
+
const TUNNEL_FLAG = /^--tunnel=(.*)$/;
|
|
15
|
+
const TAILSCALE_APP = '/Applications/Tailscale.app/Contents/MacOS/Tailscale';
|
|
16
|
+
function portOf(raw) {
|
|
17
|
+
const n = Number(raw);
|
|
18
|
+
if (raw === undefined || raw === '' || !Number.isInteger(n) || n < 1 || n > 65535)
|
|
19
|
+
throw new Error(`'${raw ?? ''}' is not a port — ${USAGE}`);
|
|
20
|
+
return n;
|
|
21
|
+
}
|
|
22
|
+
function tunnelOf(raw) {
|
|
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}`);
|
|
26
|
+
}
|
|
27
|
+
function tunnelFlag(argv, i) {
|
|
28
|
+
const arg = argv[i] ?? '';
|
|
29
|
+
const inline = TUNNEL_FLAG.exec(arg);
|
|
30
|
+
if (inline)
|
|
31
|
+
return { consumed: Number(tunnelOf(inline[1])) - 1 };
|
|
32
|
+
if (arg !== '--tunnel')
|
|
33
|
+
return null;
|
|
34
|
+
const next = argv[i + 1];
|
|
35
|
+
const takesValue = next !== undefined && !next.startsWith('-');
|
|
36
|
+
tunnelOf(takesValue ? next : undefined);
|
|
37
|
+
return { consumed: takesValue ? 1 : 0 };
|
|
38
|
+
}
|
|
39
|
+
function ownerOf(raw) {
|
|
40
|
+
if (raw === undefined || !ADDRESS.test(raw))
|
|
41
|
+
throw new Error(`'${raw ?? ''}' is not an Ethereum address — ${USAGE}`);
|
|
42
|
+
return raw.toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
export function parseServeArgs(argv) {
|
|
45
|
+
let port = localPort();
|
|
46
|
+
let tunnel = false;
|
|
47
|
+
let owner = null;
|
|
48
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
49
|
+
const arg = argv[i] ?? '';
|
|
50
|
+
const tunnelArg = tunnelFlag(argv, i);
|
|
51
|
+
if (tunnelArg) {
|
|
52
|
+
tunnel = true;
|
|
53
|
+
i += tunnelArg.consumed;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const inlineOwner = OWNER_FLAG.exec(arg);
|
|
57
|
+
if (inlineOwner) {
|
|
58
|
+
owner = ownerOf(inlineOwner[1]);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (arg === '--owner') {
|
|
62
|
+
owner = ownerOf(argv[i + 1]);
|
|
63
|
+
i += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const inline = PORT_FLAG.exec(arg);
|
|
67
|
+
if (inline) {
|
|
68
|
+
port = portOf(inline[1]);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (arg === '--port' || arg === '-p') {
|
|
72
|
+
port = portOf(argv[i + 1]);
|
|
73
|
+
i += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`unknown argument '${arg}' — ${USAGE}`);
|
|
77
|
+
}
|
|
78
|
+
return { port, tunnel, owner };
|
|
79
|
+
}
|
|
80
|
+
export function requireOwner(owner, dir = agentsDir()) {
|
|
81
|
+
if (owner !== null || existsSync(join(dir, '.owner')))
|
|
82
|
+
return;
|
|
83
|
+
throw new Error('no owner is set for this machine, so no wallet could sign in.\n' +
|
|
84
|
+
'Pass the wallet that owns it once; it is remembered in ' +
|
|
85
|
+
join(dir, '.owner') +
|
|
86
|
+
':\n metro serve --owner <address>');
|
|
87
|
+
}
|
|
88
|
+
function tailscaleStatus(bin) {
|
|
89
|
+
const run = spawnSync(bin, ['status', '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
90
|
+
if (run.error !== undefined || run.status !== 0)
|
|
91
|
+
return { ok: false, state: 'not running' };
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(run.stdout);
|
|
94
|
+
return parsed.BackendState === 'Running' ? { ok: true } : { ok: false, state: String(parsed.BackendState) };
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return { ok: false, state: 'unreadable' };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function findTailscale(candidates = ['tailscale', TAILSCALE_APP]) {
|
|
101
|
+
const bin = candidates.find((c) => spawnSync(c, ['version'], { stdio: 'ignore' }).status === 0);
|
|
102
|
+
if (bin === undefined)
|
|
103
|
+
throw new Error('metro serve --tunnel tailscale needs Tailscale on this machine.\n' +
|
|
104
|
+
'macOS: brew install --cask tailscale (or the App Store app)\n' +
|
|
105
|
+
'Linux: curl -fsSL https://tailscale.com/install.sh | sh\n' +
|
|
106
|
+
'Then sign the machine in: tailscale up');
|
|
107
|
+
const status = tailscaleStatus(bin);
|
|
108
|
+
if (!status.ok)
|
|
109
|
+
throw new Error(`Tailscale is installed but this machine is not connected (${status.state}). Sign it in first: tailscale up\n` +
|
|
110
|
+
'Funnel also has to be enabled once on your tailnet: https://tailscale.com/kb/1223/funnel');
|
|
111
|
+
return bin;
|
|
112
|
+
}
|
|
113
|
+
export function servePlan(opts) {
|
|
114
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !SCRUBBED.has(key)));
|
|
115
|
+
return {
|
|
116
|
+
command: findBun(),
|
|
117
|
+
args: [SERVER_ENTRY],
|
|
118
|
+
cwd: opts.runtime.dir,
|
|
119
|
+
env: {
|
|
120
|
+
...env,
|
|
121
|
+
METRO_MODE: 'local',
|
|
122
|
+
METRO_VERSION: currentVersion(),
|
|
123
|
+
METRO_CLI_BIN: process.argv[1] ?? '',
|
|
124
|
+
METRO_WEBHOOK_PORT: String(opts.port),
|
|
125
|
+
METRO_HTTP_HOST: process.env.METRO_HTTP_HOST ?? '127.0.0.1',
|
|
126
|
+
METRO_TRAINS_DIR: opts.runtime.trains,
|
|
127
|
+
...(opts.runtime.manifest === null
|
|
128
|
+
? {}
|
|
129
|
+
: { METRO_RUNTIME_STORE: opts.runtime.dir, METRO_RUNTIME_MANIFEST: opts.runtime.manifest }),
|
|
130
|
+
METRO_STATE_DIR: process.env.METRO_STATE_DIR ?? serveStateDir(),
|
|
131
|
+
...(opts.tunnel ? { METRO_TUNNEL: 'tailscale' } : {}),
|
|
132
|
+
...(opts.tailscaleBin === undefined ? {} : { METRO_TAILSCALE_BIN: opts.tailscaleBin }),
|
|
133
|
+
...(opts.owner === null ? {} : { METRO_OWNER: opts.owner }),
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export function serve(argv) {
|
|
138
|
+
const { port, tunnel, owner } = parseServeArgs(argv);
|
|
139
|
+
const running = serveLockedBy();
|
|
140
|
+
if (running !== null)
|
|
141
|
+
throw new Error(`a metro serve daemon is already running on this machine (pid ${String(running)}). ` +
|
|
142
|
+
'Stop it first: metro stop');
|
|
143
|
+
requireOwner(owner);
|
|
144
|
+
const tailscaleBin = tunnel ? findTailscale() : undefined;
|
|
145
|
+
process.stderr.write(`Starting a metro daemon of your own on http://127.0.0.1:${String(port)}\n`);
|
|
146
|
+
return spawnPlan(() => servePlan({ runtime: prepareRuntime(), port, tunnel, owner, ...(tailscaleBin === undefined ? {} : { tailscaleBin }) }));
|
|
147
|
+
}
|
package/dist/tail.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { localAgents } from './local.js';
|
|
2
|
+
import { assertAgentId, localUrl } from './runtime.js';
|
|
3
|
+
const BACKOFF_MS = [1_000, 2_000, 5_000];
|
|
4
|
+
const STALL_MS = 90_000;
|
|
5
|
+
export const backoffAt = (attempt) => BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)] ?? 5_000;
|
|
6
|
+
export function sseDataLines(buffer, chunk) {
|
|
7
|
+
const lines = (buffer + chunk).split('\n');
|
|
8
|
+
const rest = lines.pop() ?? '';
|
|
9
|
+
const events = [];
|
|
10
|
+
for (const raw of lines) {
|
|
11
|
+
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
12
|
+
if (!line.startsWith('data:'))
|
|
13
|
+
continue;
|
|
14
|
+
const value = line.slice(5);
|
|
15
|
+
events.push(value.startsWith(' ') ? value.slice(1) : value);
|
|
16
|
+
}
|
|
17
|
+
return { rest, events };
|
|
18
|
+
}
|
|
19
|
+
function keySource(agentId) {
|
|
20
|
+
const fromEnv = process.env.METRO_AGENT_KEY?.trim();
|
|
21
|
+
if (fromEnv !== undefined && fromEnv !== '')
|
|
22
|
+
return { resolve: () => Promise.resolve(fromEnv), fixed: true };
|
|
23
|
+
return {
|
|
24
|
+
resolve: () => {
|
|
25
|
+
const agent = localAgents().find((a) => a.id === agentId);
|
|
26
|
+
if (agent === undefined)
|
|
27
|
+
throw new Error(`no agent ${agentId} on this machine — create or restore it in the web UI of metro serve, or set METRO_AGENT_KEY`);
|
|
28
|
+
return Promise.resolve(agent.key);
|
|
29
|
+
},
|
|
30
|
+
fixed: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const armStall = (control) => setTimeout(() => {
|
|
34
|
+
control.abort();
|
|
35
|
+
}, STALL_MS);
|
|
36
|
+
async function connect(key, signal) {
|
|
37
|
+
try {
|
|
38
|
+
return await fetch(`${localUrl()}/api/tail`, {
|
|
39
|
+
headers: { authorization: `Bearer ${key}` },
|
|
40
|
+
signal,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function pump(body, control, stall) {
|
|
48
|
+
const reader = body.getReader();
|
|
49
|
+
const decoder = new TextDecoder();
|
|
50
|
+
let buffer = '';
|
|
51
|
+
try {
|
|
52
|
+
for (;;) {
|
|
53
|
+
const { done, value } = await reader.read();
|
|
54
|
+
if (done)
|
|
55
|
+
return 'ended';
|
|
56
|
+
clearTimeout(stall.timer);
|
|
57
|
+
stall.timer = armStall(control);
|
|
58
|
+
const out = sseDataLines(buffer, decoder.decode(value, { stream: true }));
|
|
59
|
+
buffer = out.rest;
|
|
60
|
+
for (const event of out.events)
|
|
61
|
+
process.stdout.write(`${event}\n`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return 'ended';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function streamOnce(key) {
|
|
69
|
+
const control = new AbortController();
|
|
70
|
+
const stall = { timer: armStall(control) };
|
|
71
|
+
try {
|
|
72
|
+
const res = await connect(key, control.signal);
|
|
73
|
+
if (res === null)
|
|
74
|
+
return 'unreachable';
|
|
75
|
+
if (res.status === 401)
|
|
76
|
+
return 'unauthorized';
|
|
77
|
+
if (!res.ok || res.body === null)
|
|
78
|
+
return 'unreachable';
|
|
79
|
+
return await pump(res.body, control, stall);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
clearTimeout(stall.timer);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function note(state, message) {
|
|
86
|
+
if (state.last === message)
|
|
87
|
+
return;
|
|
88
|
+
state.last = message;
|
|
89
|
+
process.stderr.write(`metro: ${message}\n`);
|
|
90
|
+
}
|
|
91
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
92
|
+
async function settle(end, source, key, agentId, state) {
|
|
93
|
+
if (end === 'unauthorized') {
|
|
94
|
+
if (source.fixed)
|
|
95
|
+
throw new Error('the key in METRO_AGENT_KEY was refused by the local daemon');
|
|
96
|
+
note(state, 'key refused — fetching the current one');
|
|
97
|
+
return source.resolve();
|
|
98
|
+
}
|
|
99
|
+
if (end === 'unreachable')
|
|
100
|
+
note(state, `no metro daemon on ${localUrl()} — run 'metro serve' (agent ${agentId})`);
|
|
101
|
+
else
|
|
102
|
+
note(state, 'stream ended — reconnecting');
|
|
103
|
+
return key;
|
|
104
|
+
}
|
|
105
|
+
export async function tailEvents(argv) {
|
|
106
|
+
const agentId = assertAgentId(argv[0]);
|
|
107
|
+
const source = keySource(agentId);
|
|
108
|
+
let key = await source.resolve();
|
|
109
|
+
const state = { last: '' };
|
|
110
|
+
let attempt = 0;
|
|
111
|
+
for (;;) {
|
|
112
|
+
const end = await streamOnce(key);
|
|
113
|
+
if (end === 'ended')
|
|
114
|
+
attempt = 0;
|
|
115
|
+
key = await settle(end, source, key, agentId, state);
|
|
116
|
+
await delay(backoffAt(attempt));
|
|
117
|
+
attempt += 1;
|
|
118
|
+
}
|
|
119
|
+
}
|