@stage-labs/metro 0.1.0-beta.52 → 0.1.0-beta.53
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/local.js +26 -0
- package/dist/runtime-install.js +84 -0
- package/dist/serve.js +8 -5
- package/package.json +1 -22
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +3 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +15 -14
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/bundle-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-deps.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +1 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-types.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/address.ts +6 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -10
- package/runtime/node_modules/@metro-labs/mcp/src/db/stations.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +1 -5
- package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +4 -14
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +2 -0
- package/runtime/runtime.json +1 -1
- package/runtime/stations.json +34 -0
package/dist/local.js
CHANGED
|
@@ -29,6 +29,32 @@ export function localAgents(dir = agentsDir()) {
|
|
|
29
29
|
}
|
|
30
30
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
31
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
|
+
}
|
|
32
58
|
export function pickLocalAgent(agents, wanted) {
|
|
33
59
|
if (wanted !== undefined && wanted !== '') {
|
|
34
60
|
const found = agents.find((a) => a.name === wanted || a.id === wanted);
|
|
@@ -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/serve.js
CHANGED
|
@@ -4,7 +4,8 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { agentsDir } from './local.js';
|
|
5
5
|
import { currentVersion } from './version.js';
|
|
6
6
|
import { serveLockedBy, serveStateDir } from './control.js';
|
|
7
|
-
import { findBun, localPort,
|
|
7
|
+
import { findBun, localPort, SERVER_ENTRY, spawnPlan } from './runtime.js';
|
|
8
|
+
import { prepareRuntime } from './runtime-install.js';
|
|
8
9
|
const SCRUBBED = new Set(['METRO_RUN_TOKEN', 'METRO_AGENT', 'DATABASE_URL']);
|
|
9
10
|
const PORT_FLAG = /^--port=(.*)$/;
|
|
10
11
|
const OWNER_FLAG = /^--owner=(.*)$/;
|
|
@@ -80,7 +81,7 @@ export function servePlan(opts) {
|
|
|
80
81
|
return {
|
|
81
82
|
command: findBun(),
|
|
82
83
|
args: [SERVER_ENTRY],
|
|
83
|
-
cwd: opts.dir,
|
|
84
|
+
cwd: opts.runtime.dir,
|
|
84
85
|
env: {
|
|
85
86
|
...env,
|
|
86
87
|
METRO_MODE: 'local',
|
|
@@ -88,7 +89,10 @@ export function servePlan(opts) {
|
|
|
88
89
|
METRO_CLI_BIN: process.argv[1] ?? '',
|
|
89
90
|
METRO_WEBHOOK_PORT: String(opts.port),
|
|
90
91
|
METRO_HTTP_HOST: process.env.METRO_HTTP_HOST ?? '127.0.0.1',
|
|
91
|
-
METRO_TRAINS_DIR:
|
|
92
|
+
METRO_TRAINS_DIR: opts.runtime.trains,
|
|
93
|
+
...(opts.runtime.manifest === null
|
|
94
|
+
? {}
|
|
95
|
+
: { METRO_RUNTIME_STORE: opts.runtime.dir, METRO_RUNTIME_MANIFEST: opts.runtime.manifest }),
|
|
92
96
|
METRO_STATE_DIR: process.env.METRO_STATE_DIR ?? serveStateDir(),
|
|
93
97
|
...(opts.tunnel ? { METRO_TUNNEL: 'quick' } : {}),
|
|
94
98
|
...(opts.owner === null ? {} : { METRO_OWNER: opts.owner }),
|
|
@@ -104,7 +108,6 @@ export function serve(argv) {
|
|
|
104
108
|
requireOwner(owner);
|
|
105
109
|
if (tunnel)
|
|
106
110
|
findCloudflared();
|
|
107
|
-
const dir = runtimeDir();
|
|
108
111
|
process.stderr.write(`Starting a metro daemon of your own on http://127.0.0.1:${String(port)}\n`);
|
|
109
|
-
return spawnPlan(() => servePlan({
|
|
112
|
+
return spawnPlan(() => servePlan({ runtime: prepareRuntime(), port, tunnel, owner }));
|
|
110
113
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.53",
|
|
4
4
|
"description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,26 +35,5 @@
|
|
|
35
35
|
"knip": "^6.15.0",
|
|
36
36
|
"madge": "^8.0.0",
|
|
37
37
|
"typescript": "^5"
|
|
38
|
-
},
|
|
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
38
|
}
|
|
60
39
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { ApiError } from './api-error.js';
|
|
3
3
|
import { errMsg, log } from './log.js';
|
|
4
|
-
import type { StationName } from '../db/
|
|
4
|
+
import type { StationName } from '../db/stations.js';
|
|
5
|
+
import { ensureStationDeps } from './runtime-deps.js';
|
|
5
6
|
import {
|
|
6
7
|
startInteractiveAttach,
|
|
7
8
|
type AttachDriver,
|
|
@@ -216,6 +217,7 @@ export class AttachSessions {
|
|
|
216
217
|
};
|
|
217
218
|
this.sessions.set(attachId, session);
|
|
218
219
|
try {
|
|
220
|
+
ensureStationDeps(station);
|
|
219
221
|
const started = await (this.deps.start ?? startInteractiveAttach)(
|
|
220
222
|
station,
|
|
221
223
|
input,
|
|
@@ -23,6 +23,7 @@ import { fileSource } from '../db/file-source.js';
|
|
|
23
23
|
import { ensureLocalSessionSecret } from './local-secret.js';
|
|
24
24
|
import { applyLocalOwner } from './local-owner.js';
|
|
25
25
|
import { localOwner } from '../db/file-admin.js';
|
|
26
|
+
import { ensureStationDeps } from './runtime-deps.js';
|
|
26
27
|
import { localSessionApis } from './local-mode.js';
|
|
27
28
|
import type { ModeInfo } from './mode-api.js';
|
|
28
29
|
import type { SessionApis } from './session-apis.js';
|
|
@@ -39,13 +40,7 @@ import {
|
|
|
39
40
|
} from '../stations/registry.js';
|
|
40
41
|
import { prepareAccount } from '../stations/attach.js';
|
|
41
42
|
import { materializeFrom, reloadFrom } from '../db/materialize.js';
|
|
42
|
-
import {
|
|
43
|
-
deleteVaultForOwner,
|
|
44
|
-
getVaultForOwner,
|
|
45
|
-
listVaultForOwner,
|
|
46
|
-
putVaultForOwner,
|
|
47
|
-
} from '../db/vault.js';
|
|
48
|
-
import type { StationName } from '../db/schema.js';
|
|
43
|
+
import type { StationName } from '../db/stations.js';
|
|
49
44
|
import { startUploadReaper } from './upload-store.js';
|
|
50
45
|
|
|
51
46
|
installCrashGuard();
|
|
@@ -96,7 +91,10 @@ async function syncStations(station: StationName): Promise<void> {
|
|
|
96
91
|
const { removed } = await reloadFrom(fileSource);
|
|
97
92
|
if (stationByName(station)?.hasTrain === false) return;
|
|
98
93
|
if (removed.includes(station)) await supervisor.stopTrain(station);
|
|
99
|
-
else
|
|
94
|
+
else {
|
|
95
|
+
ensureStationDeps(station);
|
|
96
|
+
supervisor.requestReload(station);
|
|
97
|
+
}
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
const hostedMode = (): ModeInfo => ({
|
|
@@ -106,7 +104,7 @@ const hostedMode = (): ModeInfo => ({
|
|
|
106
104
|
version: METRO_VERSION,
|
|
107
105
|
});
|
|
108
106
|
|
|
109
|
-
function sessionApis(): SessionApis {
|
|
107
|
+
async function sessionApis(): Promise<SessionApis> {
|
|
110
108
|
if (isLocalMode())
|
|
111
109
|
return localSessionApis({
|
|
112
110
|
syncStations,
|
|
@@ -120,13 +118,16 @@ function sessionApis(): SessionApis {
|
|
|
120
118
|
liveness: agentLiveness,
|
|
121
119
|
prepareAccount,
|
|
122
120
|
});
|
|
121
|
+
const vault = await import('../db/vault.js');
|
|
122
|
+
const users = await import('../db/users.js');
|
|
123
123
|
return {
|
|
124
124
|
vaultApi: {
|
|
125
|
-
list: listVaultForOwner,
|
|
126
|
-
put: putVaultForOwner,
|
|
127
|
-
get: getVaultForOwner,
|
|
128
|
-
remove: deleteVaultForOwner,
|
|
125
|
+
list: vault.listVaultForOwner,
|
|
126
|
+
put: vault.putVaultForOwner,
|
|
127
|
+
get: vault.getVaultForOwner,
|
|
128
|
+
remove: vault.deleteVaultForOwner,
|
|
129
129
|
},
|
|
130
|
+
siwe: { ensureUser: users.ensureUserByAddress },
|
|
130
131
|
mode: hostedMode,
|
|
131
132
|
};
|
|
132
133
|
}
|
|
@@ -141,7 +142,7 @@ async function main(): Promise<void> {
|
|
|
141
142
|
const metroMcp = await createMetroMcp();
|
|
142
143
|
webhookServer = await startWebhookServer(
|
|
143
144
|
emit,
|
|
144
|
-
sessionApis(),
|
|
145
|
+
await sessionApis(),
|
|
145
146
|
metroMcp.httpHandler,
|
|
146
147
|
metroCall,
|
|
147
148
|
);
|
|
@@ -5,7 +5,7 @@ import { ApiError } from './api-error.js';
|
|
|
5
5
|
import { isRecord } from './is-record.js';
|
|
6
6
|
import { parseId } from '../db/ids.js';
|
|
7
7
|
import type { LoadedAccount, LoadedAgent, LoadedConnector } from '../db/materialize.js';
|
|
8
|
-
import { STATIONS } from '../db/
|
|
8
|
+
import { STATIONS } from '../db/stations.js';
|
|
9
9
|
|
|
10
10
|
const AGENTS = '/api/agents';
|
|
11
11
|
const RESTORE_PATH = `${AGENTS}/restore`;
|
|
@@ -328,7 +328,7 @@ function handleSignInRoutes(
|
|
|
328
328
|
res: ServerResponse,
|
|
329
329
|
apis: SessionApis,
|
|
330
330
|
): boolean {
|
|
331
|
-
if (handleSiweAuthRequest(req, res, apis.siwe)) return true;
|
|
331
|
+
if (apis.siwe !== undefined && handleSiweAuthRequest(req, res, apis.siwe)) return true;
|
|
332
332
|
return apis.mode !== undefined && handleModeRequest(req, res, apis.mode);
|
|
333
333
|
}
|
|
334
334
|
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
readLocalAgentFile,
|
|
49
49
|
} from '../db/file-admin.js';
|
|
50
50
|
import { listAgentFiles, readAgentFile } from '../db/file-source.js';
|
|
51
|
-
import type { StationName } from '../db/
|
|
51
|
+
import type { StationName } from '../db/stations.js';
|
|
52
52
|
|
|
53
53
|
export interface LocalModeDeps {
|
|
54
54
|
syncStations: (station: StationName) => Promise<void>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { log } from './log.js';
|
|
5
|
+
import { isRecord } from './is-record.js';
|
|
6
|
+
import type { StationName } from '../db/stations.js';
|
|
7
|
+
import { knownAccounts } from '../db/agent-map.js';
|
|
8
|
+
|
|
9
|
+
const INSTALL_TIMEOUT_MS = 15 * 60_000;
|
|
10
|
+
|
|
11
|
+
export interface RuntimeManifest {
|
|
12
|
+
core: Record<string, string>;
|
|
13
|
+
stations: Record<string, Record<string, string>>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RuntimeStore {
|
|
17
|
+
dir: string;
|
|
18
|
+
manifest: RuntimeManifest;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ranges = (raw: unknown): Record<string, string> =>
|
|
22
|
+
isRecord(raw)
|
|
23
|
+
? Object.fromEntries(Object.entries(raw).filter((e): e is [string, string] => typeof e[1] === 'string'))
|
|
24
|
+
: {};
|
|
25
|
+
|
|
26
|
+
export function readManifest(path: string): RuntimeManifest {
|
|
27
|
+
const raw: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
28
|
+
if (!isRecord(raw)) throw new Error(`${path} is not a runtime manifest`);
|
|
29
|
+
const stations = isRecord(raw.stations) ? raw.stations : {};
|
|
30
|
+
return {
|
|
31
|
+
core: ranges(raw.core),
|
|
32
|
+
stations: Object.fromEntries(Object.entries(stations).map(([name, deps]) => [name, ranges(deps)])),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function runtimeStore(): RuntimeStore | null {
|
|
37
|
+
const dir = process.env.METRO_RUNTIME_STORE?.trim() ?? '';
|
|
38
|
+
const manifest = process.env.METRO_RUNTIME_MANIFEST?.trim() ?? '';
|
|
39
|
+
if (dir === '' || manifest === '') return null;
|
|
40
|
+
return { dir, manifest: readManifest(manifest) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function dependenciesFor(manifest: RuntimeManifest, stations: Iterable<string>): Record<string, string> {
|
|
44
|
+
const out: Record<string, string> = { ...manifest.core };
|
|
45
|
+
for (const station of new Set(stations)) Object.assign(out, manifest.stations[station] ?? {});
|
|
46
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const packageText = (deps: Record<string, string>): string =>
|
|
50
|
+
`${JSON.stringify({ name: 'metro-runtime', private: true, dependencies: deps }, null, 2)}\n`;
|
|
51
|
+
|
|
52
|
+
function current(dir: string): string | null {
|
|
53
|
+
try {
|
|
54
|
+
return readFileSync(join(dir, 'package.json'), 'utf8');
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function installRuntime(store: RuntimeStore, stations: Iterable<string>): boolean {
|
|
61
|
+
const wanted = packageText(dependenciesFor(store.manifest, stations));
|
|
62
|
+
const installed = existsSync(join(store.dir, 'node_modules', '.metro-installed'));
|
|
63
|
+
if (installed && current(store.dir) === wanted) return false;
|
|
64
|
+
mkdirSync(store.dir, { recursive: true });
|
|
65
|
+
writeFileSync(join(store.dir, 'package.json'), wanted);
|
|
66
|
+
log.info({ dir: store.dir }, 'runtime: installing the channel SDKs this machine needs');
|
|
67
|
+
const run = spawnSync('bun', ['install', '--no-summary', '--no-progress'], {
|
|
68
|
+
cwd: store.dir,
|
|
69
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
70
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
71
|
+
});
|
|
72
|
+
if (run.error !== undefined || run.status !== 0)
|
|
73
|
+
throw new Error(`bun install failed in ${store.dir}: ${run.error?.message ?? `exit ${String(run.status)}`}`);
|
|
74
|
+
writeFileSync(join(store.dir, 'node_modules', '.metro-installed'), `${new Date().toISOString()}\n`);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function ensureStationDeps(station: StationName): void {
|
|
79
|
+
const store = runtimeStore();
|
|
80
|
+
if (store === null) return;
|
|
81
|
+
installRuntime(store, [...knownAccounts().map((a) => a.station), station]);
|
|
82
|
+
}
|
|
@@ -11,7 +11,6 @@ import { mintNonce, takeNonce } from './siwe-nonces.js';
|
|
|
11
11
|
import { verifySiweLogin } from './siwe-auth.js';
|
|
12
12
|
import { signSession } from './session.js';
|
|
13
13
|
import { SESSION_TTL_SEC } from './session-config.js';
|
|
14
|
-
import { ensureUserByAddress } from '../db/users.js';
|
|
15
14
|
|
|
16
15
|
const NONCE_PATH = '/auth/siwe/nonce';
|
|
17
16
|
const VERIFY_PATH = '/auth/siwe/verify';
|
|
@@ -20,8 +19,6 @@ export interface SiweRouteDeps {
|
|
|
20
19
|
ensureUser: (address: string) => Promise<string>;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
const defaultDeps: SiweRouteDeps = { ensureUser: ensureUserByAddress };
|
|
24
|
-
|
|
25
22
|
async function handleVerify(
|
|
26
23
|
req: IncomingMessage,
|
|
27
24
|
res: ServerResponse,
|
|
@@ -68,7 +65,7 @@ function refused(
|
|
|
68
65
|
export function handleSiweAuthRequest(
|
|
69
66
|
req: IncomingMessage,
|
|
70
67
|
res: ServerResponse,
|
|
71
|
-
deps: SiweRouteDeps
|
|
68
|
+
deps: SiweRouteDeps,
|
|
72
69
|
): boolean {
|
|
73
70
|
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
74
71
|
if (ROUTES[path] === undefined) return false;
|
|
@@ -2,9 +2,9 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
2
2
|
import { errMsg, log } from './log.js';
|
|
3
3
|
import { verifyMessage } from 'viem';
|
|
4
4
|
import { apiFailure, cors, readJsonBody, sendJson } from './api-http.js';
|
|
5
|
-
import { normalizeAddress } from '../db/
|
|
5
|
+
import { normalizeAddress } from '../db/address.js';
|
|
6
6
|
import { parseId } from '../db/ids.js';
|
|
7
|
-
import { ENVELOPE_MAX, type VaultBundle, type VaultEntry } from '
|
|
7
|
+
import { ENVELOPE_MAX, type VaultBundle, type VaultEntry } from './vault-types.js';
|
|
8
8
|
|
|
9
9
|
const PREFIX = '/api/vault';
|
|
10
10
|
const BODY_MAX = ENVELOPE_MAX + 64 * 1024;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ConnectorAuth, VerifiedRecord } from '../daemon/connector-verify.js';
|
|
2
2
|
import { readConfig, signInState, type ConnectorSignIn } from './connector-config.js';
|
|
3
|
-
import type { ConnectorTransport } from './
|
|
3
|
+
import type { ConnectorTransport } from './stations.js';
|
|
4
4
|
|
|
5
5
|
export interface Connector {
|
|
6
6
|
id: string;
|
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
import { newId } from './ids.js';
|
|
26
26
|
import { registerKey, rotateAgentKey, unregisterAgentKey } from './key-map.js';
|
|
27
27
|
import { MOVABLE_STATIONS, type LoadedAgent } from './materialize.js';
|
|
28
|
-
import type { StationName } from './
|
|
29
|
-
import { normalizeAddress } from './
|
|
28
|
+
import type { StationName } from './stations.js';
|
|
29
|
+
import { normalizeAddress } from './address.js';
|
|
30
30
|
|
|
31
31
|
export const LOCAL_PROJECT_ID = 'localdaemon';
|
|
32
32
|
const OWNER_FILE = '.owner';
|
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { AGENT_NAME_RE, ID_RE } from './ids.js';
|
|
5
|
-
import { STATIONS, type StationName } from './
|
|
5
|
+
import { STATIONS, type StationName } from './stations.js';
|
|
6
6
|
import type { LoadedAccount, LoadedAgent, StationSource } from './materialize.js';
|
|
7
7
|
import { isRecord } from '../daemon/is-record.js';
|
|
8
8
|
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type AllowlistMap,
|
|
19
19
|
} from './agent-map.js';
|
|
20
20
|
import { setKeyMap } from './key-map.js';
|
|
21
|
-
import { STATIONS, type StationName } from './
|
|
21
|
+
import { STATIONS, type StationName } from './stations.js';
|
|
22
22
|
|
|
23
23
|
interface StationTarget {
|
|
24
24
|
file: string;
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { index, jsonb, pgTable, text } from 'drizzle-orm/pg-core';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
'xmtp',
|
|
5
|
-
'telegram-bot',
|
|
6
|
-
'telegram',
|
|
7
|
-
'discord-bot',
|
|
8
|
-
'whatsapp',
|
|
9
|
-
'webhook',
|
|
10
|
-
] as const;
|
|
3
|
+
import { STATIONS, type ConnectorTransport, type StationName } from './stations.js';
|
|
11
4
|
|
|
12
|
-
export type
|
|
5
|
+
export { STATIONS, type ConnectorTransport, type StationName };
|
|
13
6
|
|
|
14
7
|
export const users = pgTable('users', {
|
|
15
8
|
id: text('id').primaryKey(),
|
|
@@ -19,7 +12,6 @@ export const users = pgTable('users', {
|
|
|
19
12
|
|
|
20
13
|
export type ProjectRole = 'admin' | 'member';
|
|
21
14
|
|
|
22
|
-
export type ConnectorTransport = 'http' | 'sse';
|
|
23
15
|
|
|
24
16
|
export const vault = pgTable(
|
|
25
17
|
'vault',
|
|
@@ -3,9 +3,9 @@ import { ApiError } from '../daemon/api-error.js';
|
|
|
3
3
|
import { getDb } from './client.js';
|
|
4
4
|
import { newId } from './ids.js';
|
|
5
5
|
import { users } from './schema.js';
|
|
6
|
+
import { normalizeAddress } from './address.js';
|
|
6
7
|
|
|
7
8
|
const UNIQUE_VIOLATION = '23505';
|
|
8
|
-
const ADDRESS_RE = /^0x[0-9a-f]{40}$/;
|
|
9
9
|
|
|
10
10
|
export class UserError extends ApiError {}
|
|
11
11
|
|
|
@@ -22,10 +22,6 @@ export function isUniqueViolation(err: unknown): boolean {
|
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function normalizeAddress(raw: string): string | null {
|
|
26
|
-
const address = raw.trim().toLowerCase();
|
|
27
|
-
return ADDRESS_RE.test(address) ? address : null;
|
|
28
|
-
}
|
|
29
25
|
|
|
30
26
|
export async function ensureUserByAddress(raw: string): Promise<string> {
|
|
31
27
|
const address = normalizeAddress(raw);
|
|
@@ -3,29 +3,19 @@ import { ApiError } from '../daemon/api-error.js';
|
|
|
3
3
|
import { isRecord } from '../daemon/is-record.js';
|
|
4
4
|
import { getDb } from './client.js';
|
|
5
5
|
import { AGENT_NAME_RE, parseId } from './ids.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { vault } from './schema.js';
|
|
7
|
+
import { STATIONS } from './stations.js';
|
|
8
|
+
import { normalizeAddress } from './address.js';
|
|
8
9
|
|
|
9
10
|
export class VaultError extends ApiError {}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
id: string;
|
|
13
|
-
name: string;
|
|
14
|
-
stations: string[];
|
|
15
|
-
syncedAt: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface VaultBundle extends VaultEntry {
|
|
19
|
-
envelope: Record<string, unknown>;
|
|
20
|
-
}
|
|
12
|
+
import { ENVELOPE_MAX, type VaultBundle, type VaultEntry } from '../daemon/vault-types.js';
|
|
21
13
|
|
|
22
14
|
export interface VaultInput {
|
|
23
15
|
name: string;
|
|
24
16
|
stations: string[];
|
|
25
17
|
envelope: Record<string, unknown>;
|
|
26
18
|
}
|
|
27
|
-
|
|
28
|
-
export const ENVELOPE_MAX = 2 * 1024 * 1024;
|
|
29
19
|
const STATION_NAMES = new Set<string>(STATIONS);
|
|
30
20
|
|
|
31
21
|
const missing = (): VaultError => new VaultError('no such bundle', 404);
|
|
@@ -7,7 +7,7 @@ import { listEndpoints } from '../daemon/tunnel.js';
|
|
|
7
7
|
import { hookUrl } from '../stations/attach.js';
|
|
8
8
|
import { agentIdForAccount, knownAccounts, type KnownAccount } from '../db/agent-map.js';
|
|
9
9
|
import { stationRunsHere } from '../db/materialize.js';
|
|
10
|
-
import { type StationName } from '../db/
|
|
10
|
+
import { type StationName } from '../db/stations.js';
|
|
11
11
|
|
|
12
12
|
const accountId = (acc: unknown): string | undefined => {
|
|
13
13
|
const id = (acc as { id?: unknown }).id;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
verifyTelegramBotToken,
|
|
9
9
|
} from '@metro-labs/telegram-bot/verify';
|
|
10
10
|
import { ApiError } from '../daemon/api-error.js';
|
|
11
|
+
import { ensureStationDeps } from '../daemon/runtime-deps.js';
|
|
11
12
|
import { publicBaseOrDefault } from '../daemon/attach-serve.js';
|
|
12
13
|
import {
|
|
13
14
|
discardXmtpDb,
|
|
@@ -175,6 +176,7 @@ export async function prepareAccount(
|
|
|
175
176
|
input: AttachInput,
|
|
176
177
|
verify: VerifyXmtpKey = verifyXmtpKeyOutOfProcess,
|
|
177
178
|
): Promise<PreparedAccount> {
|
|
179
|
+
ensureStationDeps(input.station);
|
|
178
180
|
if (input.station === 'discord-bot') return prepareDiscord(input.token);
|
|
179
181
|
if (input.station === 'telegram-bot') return prepareTelegram(input.token);
|
|
180
182
|
if (input.station === 'webhook') return prepareWebhook();
|
package/runtime/runtime.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"core": {
|
|
3
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
4
|
+
"pino": "^9.5.0",
|
|
5
|
+
"pino-pretty": "^13.1.3",
|
|
6
|
+
"viem": "2.52.2",
|
|
7
|
+
"zod": "^3.23.8"
|
|
8
|
+
},
|
|
9
|
+
"stations": {
|
|
10
|
+
"xmtp": {
|
|
11
|
+
"@xmtp/content-type-primitives": "^3.0.1",
|
|
12
|
+
"@xmtp/content-type-reaction": "^2.0.2",
|
|
13
|
+
"@xmtp/content-type-remote-attachment": "^2.0.4",
|
|
14
|
+
"@xmtp/content-type-reply": "^2.0.3",
|
|
15
|
+
"@xmtp/content-type-wallet-send-calls": "^2.0.0",
|
|
16
|
+
"@xmtp/node-bindings": "1.9.1",
|
|
17
|
+
"@xmtp/node-sdk": "^6.0.0",
|
|
18
|
+
"viem": "^2.52.2"
|
|
19
|
+
},
|
|
20
|
+
"telegram-bot": {},
|
|
21
|
+
"telegram": {
|
|
22
|
+
"@mtcute/bun": "0.30.1"
|
|
23
|
+
},
|
|
24
|
+
"discord-bot": {
|
|
25
|
+
"@discordjs/voice": "^0.19.2",
|
|
26
|
+
"discord.js": "^14.26.4",
|
|
27
|
+
"prism-media": "^1.3.5"
|
|
28
|
+
},
|
|
29
|
+
"whatsapp": {
|
|
30
|
+
"baileys": "7.0.0-rc14"
|
|
31
|
+
},
|
|
32
|
+
"webhook": {}
|
|
33
|
+
}
|
|
34
|
+
}
|