@stage-labs/metro 0.1.0-beta.2 → 0.1.0-beta.20
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 +103 -0
- package/dist/cli.js +134 -565
- package/dist/control.js +102 -0
- package/dist/prompt.js +10 -0
- package/dist/runtime.js +94 -0
- package/dist/store.js +70 -0
- package/dist/update.js +33 -0
- package/dist/version.js +97 -0
- package/package.json +34 -22
- package/runtime/node_modules/@metro-labs/discord/package.json +28 -0
- package/runtime/node_modules/@metro-labs/discord/src/accounts.ts +119 -0
- package/runtime/node_modules/@metro-labs/discord/src/actions.ts +388 -0
- package/runtime/node_modules/@metro-labs/discord/src/api-base.ts +1 -0
- package/runtime/node_modules/@metro-labs/discord/src/attachments.ts +29 -0
- package/runtime/node_modules/@metro-labs/discord/src/format.ts +210 -0
- package/runtime/node_modules/@metro-labs/discord/src/group-actions.ts +50 -0
- package/runtime/node_modules/@metro-labs/discord/src/group.ts +115 -0
- package/runtime/node_modules/@metro-labs/discord/src/index.ts +127 -0
- package/runtime/node_modules/@metro-labs/discord/src/members.ts +215 -0
- package/runtime/node_modules/@metro-labs/discord/src/send-files.ts +43 -0
- package/runtime/node_modules/@metro-labs/discord/src/station.ts +19 -0
- package/runtime/node_modules/@metro-labs/discord/src/verify.ts +82 -0
- package/runtime/node_modules/@metro-labs/discord/src/voice-speak.ts +195 -0
- package/runtime/node_modules/@metro-labs/discord/src/voice-transcribe.ts +292 -0
- package/runtime/node_modules/@metro-labs/discord/src/voice.ts +197 -0
- package/runtime/node_modules/@metro-labs/discord/src/wire.ts +5 -0
- package/runtime/node_modules/@metro-labs/mcp/package.json +66 -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 +335 -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 +129 -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 +124 -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 +322 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +111 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +338 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +31 -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/google-auth.ts +164 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +357 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -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 +134 -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/run-api.ts +125 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -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 +41 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -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 +323 -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 +169 -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 +215 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -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-collections.ts +218 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +329 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -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 +340 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +28 -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 +131 -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 +57 -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 +115 -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 +25 -0
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +120 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +172 -0
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +48 -0
- package/runtime/node_modules/@metro-labs/telegram/src/verify.ts +38 -0
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +3 -0
- package/runtime/node_modules/@metro-labs/telegram-user/package.json +28 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +81 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +254 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +87 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/client.ts +77 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +141 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/group.ts +261 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/history.ts +52 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/inbound.ts +78 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +46 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/login.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +80 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +75 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/normalize.ts +22 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/reactions.ts +167 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +24 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +7 -0
- package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +1 -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 -78
- package/dist/channels/discord.js +0 -148
- package/dist/channels/telegram.js +0 -137
- package/dist/lib/address.js +0 -21
- package/dist/lib/codex-rc.js +0 -274
- package/dist/lib/dotenv.js +0 -31
- package/dist/log.js +0 -12
- package/dist/paths.js +0 -45
- package/dist/tail.js +0 -161
- package/skills/metro/SKILL.md +0 -122
package/dist/control.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, openSync, 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
|
+
const pidPath = (agentId) => join(STATE(), `run-${agentId}.pid`);
|
|
7
|
+
export const logPath = (agentId) => join(STATE(), 'logs', `${agentId}.log`);
|
|
8
|
+
function ensureStateDirs() {
|
|
9
|
+
mkdirSync(STATE(), { recursive: true, mode: 0o700 });
|
|
10
|
+
mkdirSync(join(STATE(), 'logs'), { recursive: true, mode: 0o700 });
|
|
11
|
+
}
|
|
12
|
+
function readPid(agentId) {
|
|
13
|
+
try {
|
|
14
|
+
const pid = Number(readFileSync(pidPath(agentId), 'utf8').trim());
|
|
15
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function isAlive(pid) {
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function runningPid(agentId) {
|
|
31
|
+
const pid = readPid(agentId);
|
|
32
|
+
if (pid === null)
|
|
33
|
+
return null;
|
|
34
|
+
if (isAlive(pid))
|
|
35
|
+
return pid;
|
|
36
|
+
rmSync(pidPath(agentId), { force: true });
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function writePid(agentId, pid) {
|
|
40
|
+
writeFileSync(pidPath(agentId), String(pid));
|
|
41
|
+
}
|
|
42
|
+
function clearPid(agentId) {
|
|
43
|
+
rmSync(pidPath(agentId), { force: true });
|
|
44
|
+
}
|
|
45
|
+
export function detach(opts) {
|
|
46
|
+
ensureStateDirs();
|
|
47
|
+
const out = openSync(logPath(opts.agentId), 'a');
|
|
48
|
+
const child = spawn(opts.command, opts.args, {
|
|
49
|
+
cwd: opts.cwd,
|
|
50
|
+
env: opts.env,
|
|
51
|
+
detached: true,
|
|
52
|
+
stdio: ['ignore', out, out],
|
|
53
|
+
});
|
|
54
|
+
child.unref();
|
|
55
|
+
const pid = child.pid ?? 0;
|
|
56
|
+
if (pid === 0)
|
|
57
|
+
throw new Error('could not start the metro daemon');
|
|
58
|
+
writePid(opts.agentId, pid);
|
|
59
|
+
return pid;
|
|
60
|
+
}
|
|
61
|
+
export async function stop(agentId, waitMs = 10_000) {
|
|
62
|
+
const pid = runningPid(agentId);
|
|
63
|
+
if (pid === null)
|
|
64
|
+
return false;
|
|
65
|
+
process.kill(pid, 'SIGTERM');
|
|
66
|
+
const until = Date.now() + waitMs;
|
|
67
|
+
while (Date.now() < until) {
|
|
68
|
+
if (!isAlive(pid)) {
|
|
69
|
+
clearPid(agentId);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
73
|
+
}
|
|
74
|
+
process.kill(pid, 'SIGKILL');
|
|
75
|
+
clearPid(agentId);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
export function tail(agentId, follow) {
|
|
79
|
+
const path = logPath(agentId);
|
|
80
|
+
if (!existsSync(path))
|
|
81
|
+
throw new Error(`no log for ${agentId} yet — it is only written when started with --detach`);
|
|
82
|
+
const args = follow ? ['-n', '200', '-f', path] : ['-n', '200', path];
|
|
83
|
+
const child = spawn('tail', args, { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
child.on('exit', (code) => {
|
|
86
|
+
resolve(code ?? 0);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export async function probe(url) {
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(`${url}/health`, {
|
|
93
|
+
signal: AbortSignal.timeout(3_000),
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok)
|
|
96
|
+
return null;
|
|
97
|
+
return (await res.json());
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
export function askLine(question) {
|
|
3
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
rl.question(question, (answer) => {
|
|
6
|
+
rl.close();
|
|
7
|
+
resolve(answer.trim());
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { metroUrl } from './store.js';
|
|
8
|
+
const AGENT_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{10}$/;
|
|
9
|
+
const SERVER_ENTRY = join('node_modules', '@metro-labs', 'mcp', 'src', 'server.ts');
|
|
10
|
+
export class MissingRuntime extends Error {
|
|
11
|
+
}
|
|
12
|
+
export function runtimeDir() {
|
|
13
|
+
const explicit = process.env.METRO_RUNTIME_DIR?.trim();
|
|
14
|
+
const dir = explicit !== undefined && explicit !== ''
|
|
15
|
+
? explicit
|
|
16
|
+
: join(dirname(dirname(fileURLToPath(import.meta.url))), 'runtime');
|
|
17
|
+
if (!existsSync(join(dir, SERVER_ENTRY)))
|
|
18
|
+
throw new MissingRuntime(`no metro daemon at ${dir}. Reinstall with: npm i -g @stage-labs/metro@beta`);
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
export class MissingBun extends Error {
|
|
22
|
+
}
|
|
23
|
+
export function assertAgentId(id) {
|
|
24
|
+
if (id === undefined || !AGENT_RE.test(id))
|
|
25
|
+
throw new Error(`'${id ?? ''}' is not an agent id — copy it from the agent's page in the web UI`);
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
export function findBun() {
|
|
29
|
+
const found = spawnSync('bun', ['--version'], { stdio: 'ignore' });
|
|
30
|
+
if (found.error !== undefined || found.status !== 0)
|
|
31
|
+
throw new MissingBun('metro start needs Bun, which is not on PATH.\n' +
|
|
32
|
+
'Install it with: curl -fsSL https://bun.sh/install | bash');
|
|
33
|
+
return 'bun';
|
|
34
|
+
}
|
|
35
|
+
function tokenPath(agentId) {
|
|
36
|
+
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
37
|
+
const base = xdg === undefined || xdg === '' ? join(homedir(), '.config') : xdg;
|
|
38
|
+
return join(base, 'metro', `runtime-${agentId}.json`);
|
|
39
|
+
}
|
|
40
|
+
export function readRunToken(agentId) {
|
|
41
|
+
const fromEnv = process.env.METRO_RUN_TOKEN?.trim();
|
|
42
|
+
if (fromEnv !== undefined && fromEnv !== '')
|
|
43
|
+
return fromEnv;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(readFileSync(tokenPath(agentId), 'utf8'));
|
|
46
|
+
if (typeof parsed.token !== 'string' || parsed.token === '')
|
|
47
|
+
return null;
|
|
48
|
+
return parsed.url === metroUrl() ? parsed.token : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function writeRunToken(agentId, token) {
|
|
55
|
+
const path = tokenPath(agentId);
|
|
56
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
57
|
+
writeFileSync(path, JSON.stringify({ token, url: metroUrl() }, null, 2));
|
|
58
|
+
chmodSync(path, 0o600);
|
|
59
|
+
}
|
|
60
|
+
export const localPort = () => Number(process.env.METRO_WEBHOOK_PORT) || 8420;
|
|
61
|
+
export const localUrl = () => `http://127.0.0.1:${String(localPort())}`;
|
|
62
|
+
export function daemonPlan(opts) {
|
|
63
|
+
return {
|
|
64
|
+
command: findBun(),
|
|
65
|
+
args: [SERVER_ENTRY],
|
|
66
|
+
cwd: opts.dir,
|
|
67
|
+
env: {
|
|
68
|
+
...process.env,
|
|
69
|
+
METRO_RUN_TOKEN: opts.token,
|
|
70
|
+
METRO_AGENT: opts.agentId,
|
|
71
|
+
METRO_URL: metroUrl(),
|
|
72
|
+
METRO_HTTP_HOST: process.env.METRO_HTTP_HOST ?? '127.0.0.1',
|
|
73
|
+
METRO_TRAINS_DIR: join(opts.dir, 'trains'),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function runDaemon(agentId, plan) {
|
|
78
|
+
process.stderr.write(`Starting metro for ${agentId}\n`);
|
|
79
|
+
const child = spawn(plan.command, plan.args, {
|
|
80
|
+
cwd: plan.cwd,
|
|
81
|
+
env: plan.env,
|
|
82
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
83
|
+
});
|
|
84
|
+
const relay = (sig) => () => {
|
|
85
|
+
child.kill(sig);
|
|
86
|
+
};
|
|
87
|
+
process.on('SIGINT', relay('SIGINT'));
|
|
88
|
+
process.on('SIGTERM', relay('SIGTERM'));
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
child.on('exit', (code) => {
|
|
91
|
+
resolve(code ?? 0);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
const DEFAULT_BASE = 'https://mcp.metro.box';
|
|
5
|
+
const DEFAULT_WEB = 'https://metro.box';
|
|
6
|
+
function fromEnv(name, fallback) {
|
|
7
|
+
const raw = process.env[name]?.trim();
|
|
8
|
+
return raw === undefined || raw === '' ? fallback : raw.replace(/\/+$/, '');
|
|
9
|
+
}
|
|
10
|
+
export function metroUrl() {
|
|
11
|
+
return fromEnv('METRO_URL', DEFAULT_BASE);
|
|
12
|
+
}
|
|
13
|
+
export function metroWebUrl() {
|
|
14
|
+
return fromEnv('METRO_UI_URL', DEFAULT_WEB);
|
|
15
|
+
}
|
|
16
|
+
function configDir() {
|
|
17
|
+
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
18
|
+
const base = xdg === undefined || xdg === '' ? join(homedir(), '.config') : xdg;
|
|
19
|
+
return join(base, 'metro');
|
|
20
|
+
}
|
|
21
|
+
export function credentialsPath() {
|
|
22
|
+
return join(configDir(), 'credentials.json');
|
|
23
|
+
}
|
|
24
|
+
function readStored() {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = readFileSync(credentialsPath(), 'utf8');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
40
|
+
return null;
|
|
41
|
+
const { token, url } = parsed;
|
|
42
|
+
if (typeof token !== 'string' || token === '')
|
|
43
|
+
return null;
|
|
44
|
+
return { token, url: typeof url === 'string' ? url : DEFAULT_BASE };
|
|
45
|
+
}
|
|
46
|
+
export function readToken() {
|
|
47
|
+
const fromEnv = process.env.METRO_TOKEN?.trim();
|
|
48
|
+
if (fromEnv !== undefined && fromEnv !== '')
|
|
49
|
+
return fromEnv;
|
|
50
|
+
const stored = readStored();
|
|
51
|
+
if (stored === null)
|
|
52
|
+
return null;
|
|
53
|
+
return stored.url === metroUrl() ? stored.token : null;
|
|
54
|
+
}
|
|
55
|
+
export function writeToken(token) {
|
|
56
|
+
const path = credentialsPath();
|
|
57
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
58
|
+
writeFileSync(path, `${JSON.stringify({ token, url: metroUrl() })}\n`, {
|
|
59
|
+
mode: 0o600,
|
|
60
|
+
});
|
|
61
|
+
chmodSync(path, 0o600);
|
|
62
|
+
}
|
|
63
|
+
export function clearToken() {
|
|
64
|
+
try {
|
|
65
|
+
rmSync(credentialsPath());
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { currentVersion, isNewer, PACKAGE_NAME, publishedVersion, } from './version.js';
|
|
3
|
+
export function managerFor(location) {
|
|
4
|
+
return location.includes('/.bun/') ? 'bun' : 'npm';
|
|
5
|
+
}
|
|
6
|
+
export function installArgs(manager, version) {
|
|
7
|
+
const spec = `${PACKAGE_NAME}@${version}`;
|
|
8
|
+
return manager === 'bun'
|
|
9
|
+
? ['add', '--global', spec]
|
|
10
|
+
: ['install', '--global', spec];
|
|
11
|
+
}
|
|
12
|
+
export async function update() {
|
|
13
|
+
const current = currentVersion();
|
|
14
|
+
const newest = await publishedVersion();
|
|
15
|
+
if (newest === '') {
|
|
16
|
+
process.stderr.write('npm published no version this CLI understands\n');
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
if (!isNewer(newest, current)) {
|
|
20
|
+
process.stderr.write(`metro ${current} is the newest published version\n`);
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
const manager = managerFor(import.meta.url);
|
|
24
|
+
const args = installArgs(manager, newest);
|
|
25
|
+
process.stderr.write(`Updating metro ${current} to ${newest} with ${manager}\n`);
|
|
26
|
+
const run = spawnSync(manager, args, { stdio: 'inherit' });
|
|
27
|
+
if (run.error !== undefined || run.status !== 0) {
|
|
28
|
+
process.stderr.write(`That did not work. Run it yourself:\n ${manager} ${args.join(' ')}\n`);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
process.stderr.write(`metro is now ${newest}\n`);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
export const PACKAGE_NAME = '@stage-labs/metro';
|
|
3
|
+
const REGISTRY = 'https://registry.npmjs.org';
|
|
4
|
+
const TIMEOUT_MS = 15_000;
|
|
5
|
+
export function parseVersion(raw) {
|
|
6
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(raw.trim());
|
|
7
|
+
if (match === null)
|
|
8
|
+
return null;
|
|
9
|
+
const core = [match[1], match[2], match[3]].map((n) => Number(n));
|
|
10
|
+
const pre = match[4] === undefined
|
|
11
|
+
? []
|
|
12
|
+
: match[4]
|
|
13
|
+
.split('.')
|
|
14
|
+
.map((part) => (/^\d+$/.test(part) ? Number(part) : part));
|
|
15
|
+
return { core, pre };
|
|
16
|
+
}
|
|
17
|
+
function comparePart(left, right) {
|
|
18
|
+
if (left === undefined)
|
|
19
|
+
return -1;
|
|
20
|
+
if (right === undefined)
|
|
21
|
+
return 1;
|
|
22
|
+
if (left === right)
|
|
23
|
+
return 0;
|
|
24
|
+
if (typeof left === 'number' && typeof right === 'number')
|
|
25
|
+
return left < right ? -1 : 1;
|
|
26
|
+
if (typeof left === 'number')
|
|
27
|
+
return -1;
|
|
28
|
+
if (typeof right === 'number')
|
|
29
|
+
return 1;
|
|
30
|
+
return left < right ? -1 : 1;
|
|
31
|
+
}
|
|
32
|
+
function comparePre(a, b) {
|
|
33
|
+
if (a.length === 0 && b.length === 0)
|
|
34
|
+
return 0;
|
|
35
|
+
if (a.length === 0)
|
|
36
|
+
return 1;
|
|
37
|
+
if (b.length === 0)
|
|
38
|
+
return -1;
|
|
39
|
+
for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
|
|
40
|
+
const cmp = comparePart(a[i], b[i]);
|
|
41
|
+
if (cmp !== 0)
|
|
42
|
+
return cmp;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
export function compareVersions(a, b) {
|
|
47
|
+
const left = parseVersion(a);
|
|
48
|
+
const right = parseVersion(b);
|
|
49
|
+
if (left === null || right === null)
|
|
50
|
+
return 0;
|
|
51
|
+
for (let i = 0; i < 3; i += 1) {
|
|
52
|
+
const l = left.core[i] ?? 0;
|
|
53
|
+
const r = right.core[i] ?? 0;
|
|
54
|
+
if (l !== r)
|
|
55
|
+
return l < r ? -1 : 1;
|
|
56
|
+
}
|
|
57
|
+
return comparePre(left.pre, right.pre);
|
|
58
|
+
}
|
|
59
|
+
export function isNewer(candidate, current) {
|
|
60
|
+
return compareVersions(candidate, current) > 0;
|
|
61
|
+
}
|
|
62
|
+
export function currentVersion() {
|
|
63
|
+
const path = new URL('../package.json', import.meta.url);
|
|
64
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
65
|
+
return typeof raw.version === 'string' ? raw.version : '0.0.0';
|
|
66
|
+
}
|
|
67
|
+
export function newestOf(tags) {
|
|
68
|
+
let best = '';
|
|
69
|
+
for (const value of Object.values(tags)) {
|
|
70
|
+
if (typeof value !== 'string' || parseVersion(value) === null)
|
|
71
|
+
continue;
|
|
72
|
+
if (best === '' || isNewer(value, best))
|
|
73
|
+
best = value;
|
|
74
|
+
}
|
|
75
|
+
return best;
|
|
76
|
+
}
|
|
77
|
+
export async function publishedVersion() {
|
|
78
|
+
let res;
|
|
79
|
+
try {
|
|
80
|
+
res = await fetch(`${REGISTRY}/${PACKAGE_NAME.replace('/', '%2F')}`, {
|
|
81
|
+
headers: { accept: 'application/json' },
|
|
82
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new Error('could not reach the npm registry');
|
|
87
|
+
}
|
|
88
|
+
if (!res.ok)
|
|
89
|
+
throw new Error(`npm answered ${String(res.status)}`);
|
|
90
|
+
const body = await res.json();
|
|
91
|
+
if (typeof body !== 'object' || body === null)
|
|
92
|
+
throw new Error('npm returned an unexpected response');
|
|
93
|
+
const tags = body['dist-tags'];
|
|
94
|
+
if (typeof tags !== 'object' || tags === null)
|
|
95
|
+
throw new Error('npm returned no dist-tags');
|
|
96
|
+
return newestOf(tags);
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0-beta.20",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
@@ -14,35 +14,47 @@
|
|
|
14
14
|
"node": ">=22"
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
|
-
"metro": "
|
|
17
|
+
"metro": "dist/cli.js"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
|
-
"
|
|
22
|
-
"README.md",
|
|
23
|
-
"LICENSE"
|
|
21
|
+
"runtime"
|
|
24
22
|
],
|
|
25
23
|
"publishConfig": {
|
|
26
|
-
"access": "public"
|
|
24
|
+
"access": "public",
|
|
25
|
+
"tag": "beta"
|
|
27
26
|
},
|
|
28
27
|
"scripts": {
|
|
29
|
-
"build": "tsc",
|
|
30
|
-
"prepublishOnly": "tsc",
|
|
31
|
-
"
|
|
32
|
-
"lint:fix": "eslint src/ --fix",
|
|
33
|
-
"typecheck": "tsc --noEmit"
|
|
34
|
-
},
|
|
35
|
-
"dependencies": {
|
|
36
|
-
"discord.js": "^14.14.0",
|
|
37
|
-
"pino": "^9.5.0",
|
|
38
|
-
"ws": "^8.20.0"
|
|
28
|
+
"build": "tsc && node scripts/stage-runtime.mjs",
|
|
29
|
+
"prepublishOnly": "tsc && node scripts/stage-runtime.mjs",
|
|
30
|
+
"test": "tsc --noEmit && node scripts/stage-runtime.mjs && bun test test/"
|
|
39
31
|
},
|
|
40
32
|
"devDependencies": {
|
|
41
|
-
"@
|
|
42
|
-
"@types/ws": "^8.18.1",
|
|
33
|
+
"@stage-labs/config": "0.1.0-beta.2",
|
|
43
34
|
"eslint": "^10.3.0",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
35
|
+
"knip": "^6.15.0",
|
|
36
|
+
"madge": "^8.0.0",
|
|
37
|
+
"typescript": "^5"
|
|
46
38
|
},
|
|
47
|
-
"
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@discordjs/voice": "^0.19.2",
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
42
|
+
"@mtcute/bun": "0.30.1",
|
|
43
|
+
"@xmtp/content-type-primitives": "^3.0.1",
|
|
44
|
+
"@xmtp/content-type-reaction": "^2.0.2",
|
|
45
|
+
"@xmtp/content-type-remote-attachment": "^2.0.4",
|
|
46
|
+
"@xmtp/content-type-reply": "^2.0.3",
|
|
47
|
+
"@xmtp/content-type-wallet-send-calls": "^2.0.0",
|
|
48
|
+
"@xmtp/node-bindings": "1.9.1",
|
|
49
|
+
"@xmtp/node-sdk": "^6.0.0",
|
|
50
|
+
"baileys": "7.0.0-rc14",
|
|
51
|
+
"discord.js": "^14.26.4",
|
|
52
|
+
"drizzle-orm": "^0.44.0",
|
|
53
|
+
"pino": "^9.5.0",
|
|
54
|
+
"pino-pretty": "^13.1.3",
|
|
55
|
+
"postgres": "^3.4.5",
|
|
56
|
+
"prism-media": "^1.3.5",
|
|
57
|
+
"viem": "^2.52.2",
|
|
58
|
+
"zod": "^3.23.8"
|
|
59
|
+
}
|
|
48
60
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@metro-labs/discord",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/station.ts",
|
|
8
|
+
"./train": "./src/index.ts",
|
|
9
|
+
"./verify": "./src/verify.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"test": "tsc --noEmit && bun test test/"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@metro-labs/mcp": "workspace:*",
|
|
17
|
+
"@discordjs/voice": "^0.19.2",
|
|
18
|
+
"discord.js": "^14.26.4",
|
|
19
|
+
"prism-media": "^1.3.5"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@stage-labs/config": "0.1.0-beta.2",
|
|
23
|
+
"eslint": "^10.3.0",
|
|
24
|
+
"knip": "^6.15.0",
|
|
25
|
+
"madge": "^8.0.0",
|
|
26
|
+
"typescript": "^5"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Client } from 'discord.js';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
makeAccountStore,
|
|
6
|
+
resolveAccountId,
|
|
7
|
+
} from '@metro-labs/mcp/stations/account-store';
|
|
8
|
+
import { Line } from '@metro-labs/mcp/lines';
|
|
9
|
+
import { emit } from './wire.js';
|
|
10
|
+
import { API } from './api-base.js';
|
|
11
|
+
|
|
12
|
+
const ACCOUNTS_FILE =
|
|
13
|
+
process.env.DISCORD_ACCOUNTS_FILE ??
|
|
14
|
+
join(homedir(), '.metro', 'discord-accounts.json');
|
|
15
|
+
|
|
16
|
+
export interface AccountConfig {
|
|
17
|
+
id: string;
|
|
18
|
+
token: string;
|
|
19
|
+
owner?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const { loadAccounts } = makeAccountStore<AccountConfig>({
|
|
23
|
+
prefix: 'discord',
|
|
24
|
+
file: ACCOUNTS_FILE,
|
|
25
|
+
allowlistEnv: ['DISCORD_ONLY_ACCOUNTS', 'DISCORD_ACCOUNTS'],
|
|
26
|
+
validate(raw, die) {
|
|
27
|
+
const seen = new Set<string>();
|
|
28
|
+
for (const a of raw) {
|
|
29
|
+
if (!a.id) die('account missing id');
|
|
30
|
+
if (!a.token || typeof a.token !== 'string')
|
|
31
|
+
die(`account '${a.id}' missing token`);
|
|
32
|
+
if (seen.has(a.id)) die(`duplicate account id '${a.id}'`);
|
|
33
|
+
seen.add(a.id);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export interface Account {
|
|
39
|
+
cfg: AccountConfig;
|
|
40
|
+
client: Client;
|
|
41
|
+
}
|
|
42
|
+
export const accounts = new Map<string, Account>();
|
|
43
|
+
|
|
44
|
+
function restBody(
|
|
45
|
+
body: unknown,
|
|
46
|
+
isForm: boolean,
|
|
47
|
+
): RequestInit['body'] | undefined {
|
|
48
|
+
if (body === undefined) return undefined;
|
|
49
|
+
return isForm ? (body as RequestInit['body']) : JSON.stringify(body);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function restResult<T>(res: Response): Promise<T> {
|
|
53
|
+
if (res.status === 204) return undefined as T;
|
|
54
|
+
const ctype = res.headers.get('content-type') ?? '';
|
|
55
|
+
if (ctype.includes('application/json')) return res.json() as Promise<T>;
|
|
56
|
+
return res.arrayBuffer().then((b) => Buffer.from(b) as unknown as T);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function rest<T = unknown>(
|
|
60
|
+
accountId: string,
|
|
61
|
+
method: string,
|
|
62
|
+
path: string,
|
|
63
|
+
body?: unknown,
|
|
64
|
+
isForm = false,
|
|
65
|
+
): Promise<T> {
|
|
66
|
+
const acct = accounts.get(accountId);
|
|
67
|
+
if (!acct) throw new Error(`unknown account '${accountId}'`);
|
|
68
|
+
const headers: Record<string, string> = {
|
|
69
|
+
Authorization: `Bot ${acct.cfg.token}`,
|
|
70
|
+
'User-Agent': 'metro-discord-train (https://github.com/bonustrack/stage)',
|
|
71
|
+
};
|
|
72
|
+
if (body !== undefined && !isForm)
|
|
73
|
+
headers['Content-Type'] = 'application/json';
|
|
74
|
+
emit({ op: 'log', text: `discord[${accountId}] api ${method} ${path}` });
|
|
75
|
+
const res = await fetch(`${API}${path}`, {
|
|
76
|
+
method,
|
|
77
|
+
headers,
|
|
78
|
+
body: restBody(body, isForm),
|
|
79
|
+
signal: AbortSignal.timeout(30_000),
|
|
80
|
+
});
|
|
81
|
+
emit({
|
|
82
|
+
op: 'log',
|
|
83
|
+
text: `discord[${accountId}] api ${method} ${path} -> ${res.status}`,
|
|
84
|
+
});
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
const text = await res.text().catch(() => '');
|
|
87
|
+
throw new Error(`discord ${method} ${path}: ${res.status} ${text}`);
|
|
88
|
+
}
|
|
89
|
+
return restResult<T>(res);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function lineOf(accountId: string, channelId: string): string {
|
|
93
|
+
return `metro://discord/${accountId}/${channelId}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parseLine(
|
|
97
|
+
line: string,
|
|
98
|
+
): { accountId: string; channelId: string } | null {
|
|
99
|
+
const p = Line.parseDiscord(line);
|
|
100
|
+
return p ? { accountId: p.accountId, channelId: p.resource } : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function accountFor(args: { account?: string; line?: string }): string {
|
|
104
|
+
return resolveAccountId(accounts, args, (line) => parseLine(line)?.accountId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function routeOf(
|
|
108
|
+
line: string,
|
|
109
|
+
account?: string,
|
|
110
|
+
): { accountId: string; channelId: string } {
|
|
111
|
+
const parsed = parseLine(line);
|
|
112
|
+
if (!parsed) throw new Error(`bad discord line: ${line}`);
|
|
113
|
+
const accountId = account ?? parsed.accountId;
|
|
114
|
+
if (!accounts.has(accountId))
|
|
115
|
+
throw new Error(`unknown account '${accountId}' in line ${line}`);
|
|
116
|
+
return { accountId, channelId: parsed.channelId };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const encodeEmoji = (e: string): string => encodeURIComponent(e);
|