@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.
Files changed (29) hide show
  1. package/dist/local.js +26 -0
  2. package/dist/runtime-install.js +84 -0
  3. package/dist/serve.js +8 -5
  4. package/package.json +1 -22
  5. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +1 -1
  6. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +3 -1
  7. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +15 -14
  8. package/runtime/node_modules/@metro-labs/mcp/src/daemon/bundle-api.ts +1 -1
  9. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +1 -1
  10. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +1 -1
  11. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-deps.ts +82 -0
  12. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +1 -4
  13. package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +2 -2
  14. package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-types.ts +12 -0
  15. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +1 -1
  16. package/runtime/node_modules/@metro-labs/mcp/src/db/address.ts +6 -0
  17. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +1 -1
  18. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +1 -1
  19. package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +2 -2
  20. package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +1 -1
  21. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +1 -1
  22. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -10
  23. package/runtime/node_modules/@metro-labs/mcp/src/db/stations.ts +12 -0
  24. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +1 -5
  25. package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +4 -14
  26. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +1 -1
  27. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +2 -0
  28. package/runtime/runtime.json +1 -1
  29. 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, runtimeDir, SERVER_ENTRY, spawnPlan, } from './runtime.js';
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: join(opts.dir, 'trains'),
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({ dir, port, tunnel, owner }));
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.52",
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
  }
@@ -13,7 +13,7 @@ import {
13
13
  parseAccountId,
14
14
  type AccountRef,
15
15
  } from '../db/account-attach.js';
16
- import type { StationName } from '../db/schema.js';
16
+ import type { StationName } from '../db/stations.js';
17
17
  import {
18
18
  ATTACHABLE_STATIONS,
19
19
  isAttachStation,
@@ -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/schema.js';
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 supervisor.requestReload(station);
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/schema.js';
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/schema.js';
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 = defaultDeps,
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/users.js';
5
+ import { normalizeAddress } from '../db/address.js';
6
6
  import { parseId } from '../db/ids.js';
7
- import { ENVELOPE_MAX, type VaultBundle, type VaultEntry } from '../db/vault.js';
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;
@@ -0,0 +1,12 @@
1
+ export interface VaultEntry {
2
+ id: string;
3
+ name: string;
4
+ stations: string[];
5
+ syncedAt: string;
6
+ }
7
+
8
+ export interface VaultBundle extends VaultEntry {
9
+ envelope: Record<string, unknown>;
10
+ }
11
+
12
+ export const ENVELOPE_MAX = 2 * 1024 * 1024;
@@ -1,4 +1,4 @@
1
- import { STATIONS, type StationName } from './schema.js';
1
+ import { STATIONS, type StationName } from './stations.js';
2
2
 
3
3
  const ACCOUNT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
4
4
 
@@ -0,0 +1,6 @@
1
+ const ADDRESS_RE = /^0x[0-9a-f]{40}$/;
2
+
3
+ export function normalizeAddress(raw: string): string | null {
4
+ const address = raw.trim().toLowerCase();
5
+ return ADDRESS_RE.test(address) ? address : null;
6
+ }
@@ -3,7 +3,7 @@ import {
3
3
  agentIdForAccount,
4
4
  stationAgentIds,
5
5
  } from './agent-map.js';
6
- import { STATIONS } from './schema.js';
6
+ import { STATIONS } from './stations.js';
7
7
 
8
8
  const ACCOUNT_STATIONS = new Set<string>(STATIONS);
9
9
 
@@ -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 './schema.js';
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 './schema.js';
29
- import { normalizeAddress } from './users.js';
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 './schema.js';
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 './schema.js';
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
- export const STATIONS = [
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 StationName = (typeof STATIONS)[number];
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',
@@ -0,0 +1,12 @@
1
+ export const STATIONS = [
2
+ 'xmtp',
3
+ 'telegram-bot',
4
+ 'telegram',
5
+ 'discord-bot',
6
+ 'whatsapp',
7
+ 'webhook',
8
+ ] as const;
9
+
10
+ export type StationName = (typeof STATIONS)[number];
11
+
12
+ export type ConnectorTransport = 'http' | 'sse';
@@ -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 { STATIONS, vault } from './schema.js';
7
- import { normalizeAddress } from './users.js';
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
- export interface VaultEntry {
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/schema.js';
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();
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.52"
2
+ "version": "0.1.0-beta.53"
3
3
  }
@@ -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
+ }