@stage-labs/metro 0.1.0-beta.51 → 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/cli.js +2 -2
- package/dist/local.js +26 -0
- package/dist/runtime-install.js +84 -0
- package/dist/runtime.js +16 -3
- package/dist/serve.js +9 -5
- package/dist/update.js +13 -1
- 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 +22 -15
- 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 +3 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-deps.ts +82 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +3 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +1 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +119 -0
- 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/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ const USAGE = `metro — run your agent on this machine
|
|
|
31
31
|
metro bedrock [args...]
|
|
32
32
|
the same, with inference on Amazon Bedrock through a local proxy so the
|
|
33
33
|
channel still works (needs AWS_BEARER_TOKEN_BEDROCK and AWS_REGION)
|
|
34
|
-
metro update update to the newest published version
|
|
34
|
+
metro update update to the newest published version (--check only reports)
|
|
35
35
|
metro version print this CLI's version
|
|
36
36
|
|
|
37
37
|
Start Claude Code with every connector, writing nothing to disk:
|
|
@@ -73,7 +73,7 @@ const COMMANDS = {
|
|
|
73
73
|
plugin: installPlugin,
|
|
74
74
|
claude: () => launchClaude(process.argv.slice(3)),
|
|
75
75
|
bedrock: () => bedrock(process.argv.slice(3)),
|
|
76
|
-
update,
|
|
76
|
+
update: () => update(process.argv.slice(3)),
|
|
77
77
|
version: async () => {
|
|
78
78
|
process.stdout.write(`${currentVersion()}\n`);
|
|
79
79
|
return Promise.resolve(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/runtime.js
CHANGED
|
@@ -31,7 +31,8 @@ export function findBun() {
|
|
|
31
31
|
}
|
|
32
32
|
export const localPort = () => Number(process.env.METRO_WEBHOOK_PORT) || 8420;
|
|
33
33
|
export const localUrl = () => `http://127.0.0.1:${String(localPort())}`;
|
|
34
|
-
export
|
|
34
|
+
export const RESTART_CODE = 75;
|
|
35
|
+
function spawnOnce(plan) {
|
|
35
36
|
const child = spawn(plan.command, plan.args, {
|
|
36
37
|
cwd: plan.cwd,
|
|
37
38
|
env: plan.env,
|
|
@@ -40,11 +41,23 @@ export function spawnPlan(plan) {
|
|
|
40
41
|
const relay = (sig) => () => {
|
|
41
42
|
child.kill(sig);
|
|
42
43
|
};
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
const onInt = relay('SIGINT');
|
|
45
|
+
const onTerm = relay('SIGTERM');
|
|
46
|
+
process.on('SIGINT', onInt);
|
|
47
|
+
process.on('SIGTERM', onTerm);
|
|
45
48
|
return new Promise((resolve) => {
|
|
46
49
|
child.on('exit', (code) => {
|
|
50
|
+
process.off('SIGINT', onInt);
|
|
51
|
+
process.off('SIGTERM', onTerm);
|
|
47
52
|
resolve(code ?? 0);
|
|
48
53
|
});
|
|
49
54
|
});
|
|
50
55
|
}
|
|
56
|
+
export async function spawnPlan(plan) {
|
|
57
|
+
for (;;) {
|
|
58
|
+
const code = await spawnOnce(plan());
|
|
59
|
+
if (code !== RESTART_CODE)
|
|
60
|
+
return code;
|
|
61
|
+
process.stderr.write('metro was updated; restarting the daemon on the new version\n');
|
|
62
|
+
}
|
|
63
|
+
}
|
package/dist/serve.js
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,14 +81,18 @@ 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',
|
|
87
88
|
METRO_VERSION: currentVersion(),
|
|
89
|
+
METRO_CLI_BIN: process.argv[1] ?? '',
|
|
88
90
|
METRO_WEBHOOK_PORT: String(opts.port),
|
|
89
91
|
METRO_HTTP_HOST: process.env.METRO_HTTP_HOST ?? '127.0.0.1',
|
|
90
|
-
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 }),
|
|
91
96
|
METRO_STATE_DIR: process.env.METRO_STATE_DIR ?? serveStateDir(),
|
|
92
97
|
...(opts.tunnel ? { METRO_TUNNEL: 'quick' } : {}),
|
|
93
98
|
...(opts.owner === null ? {} : { METRO_OWNER: opts.owner }),
|
|
@@ -103,7 +108,6 @@ export function serve(argv) {
|
|
|
103
108
|
requireOwner(owner);
|
|
104
109
|
if (tunnel)
|
|
105
110
|
findCloudflared();
|
|
106
|
-
const plan = servePlan({ dir: runtimeDir(), port, tunnel, owner });
|
|
107
111
|
process.stderr.write(`Starting a metro daemon of your own on http://127.0.0.1:${String(port)}\n`);
|
|
108
|
-
return spawnPlan(
|
|
112
|
+
return spawnPlan(() => servePlan({ runtime: prepareRuntime(), port, tunnel, owner }));
|
|
109
113
|
}
|
package/dist/update.js
CHANGED
|
@@ -9,7 +9,19 @@ export function installArgs(manager, version) {
|
|
|
9
9
|
? ['add', '--global', spec]
|
|
10
10
|
: ['install', '--global', spec];
|
|
11
11
|
}
|
|
12
|
-
export
|
|
12
|
+
export const checkReport = (current, latest) => ({
|
|
13
|
+
current,
|
|
14
|
+
latest,
|
|
15
|
+
newer: latest !== '' && isNewer(latest, current),
|
|
16
|
+
});
|
|
17
|
+
export async function updateCheck() {
|
|
18
|
+
return checkReport(currentVersion(), await publishedVersion());
|
|
19
|
+
}
|
|
20
|
+
export async function update(argv = []) {
|
|
21
|
+
if (argv.includes('--check')) {
|
|
22
|
+
process.stdout.write(`${JSON.stringify(await updateCheck())}\n`);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
13
25
|
const current = currentVersion();
|
|
14
26
|
const newest = await publishedVersion();
|
|
15
27
|
if (newest === '') {
|
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,23 +104,30 @@ 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,
|
|
111
|
+
restart: () => {
|
|
112
|
+
exitCode = RESTART_EXIT;
|
|
113
|
+
onShutdown();
|
|
114
|
+
},
|
|
113
115
|
closeAgentSession,
|
|
114
116
|
gatherAccounts: gatherAccountsForAgents,
|
|
115
117
|
capabilities: accountStationCapabilities,
|
|
116
118
|
liveness: agentLiveness,
|
|
117
119
|
prepareAccount,
|
|
118
120
|
});
|
|
121
|
+
const vault = await import('../db/vault.js');
|
|
122
|
+
const users = await import('../db/users.js');
|
|
119
123
|
return {
|
|
120
124
|
vaultApi: {
|
|
121
|
-
list: listVaultForOwner,
|
|
122
|
-
put: putVaultForOwner,
|
|
123
|
-
get: getVaultForOwner,
|
|
124
|
-
remove: deleteVaultForOwner,
|
|
125
|
+
list: vault.listVaultForOwner,
|
|
126
|
+
put: vault.putVaultForOwner,
|
|
127
|
+
get: vault.getVaultForOwner,
|
|
128
|
+
remove: vault.deleteVaultForOwner,
|
|
125
129
|
},
|
|
130
|
+
siwe: { ensureUser: users.ensureUserByAddress },
|
|
126
131
|
mode: hostedMode,
|
|
127
132
|
};
|
|
128
133
|
}
|
|
@@ -137,7 +142,7 @@ async function main(): Promise<void> {
|
|
|
137
142
|
const metroMcp = await createMetroMcp();
|
|
138
143
|
webhookServer = await startWebhookServer(
|
|
139
144
|
emit,
|
|
140
|
-
sessionApis(),
|
|
145
|
+
await sessionApis(),
|
|
141
146
|
metroMcp.httpHandler,
|
|
142
147
|
metroCall,
|
|
143
148
|
);
|
|
@@ -153,6 +158,8 @@ async function main(): Promise<void> {
|
|
|
153
158
|
}
|
|
154
159
|
|
|
155
160
|
const SHUTDOWN_TIMEOUT_MS = 3_000;
|
|
161
|
+
const RESTART_EXIT = 75;
|
|
162
|
+
let exitCode = 0;
|
|
156
163
|
let shuttingDown = false;
|
|
157
164
|
async function shutdown(): Promise<void> {
|
|
158
165
|
if (shuttingDown) return;
|
|
@@ -173,7 +180,7 @@ async function shutdown(): Promise<void> {
|
|
|
173
180
|
]);
|
|
174
181
|
}
|
|
175
182
|
await supervisor.stop();
|
|
176
|
-
process.exit(
|
|
183
|
+
process.exit(exitCode);
|
|
177
184
|
}
|
|
178
185
|
const onShutdown = (): void => {
|
|
179
186
|
shutdown().catch((err: unknown) => {
|
|
@@ -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,10 +48,11 @@ 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>;
|
|
55
|
+
restart: () => void;
|
|
55
56
|
closeAgentSession: (id: string) => Promise<boolean>;
|
|
56
57
|
gatherAccounts: AgentApiDeps['gatherAccounts'];
|
|
57
58
|
capabilities: AgentApiDeps['capabilities'];
|
|
@@ -189,6 +190,7 @@ export function localSessionApis(deps: LocalModeDeps): SessionApis {
|
|
|
189
190
|
relayApi,
|
|
190
191
|
localCli,
|
|
191
192
|
claudeApi: { authorize: (subject) => { assertLocalOwner(subject); } },
|
|
193
|
+
updateApi: { authorize: (subject) => { assertLocalOwner(subject); }, restart: deps.restart },
|
|
192
194
|
siwe: { ensureUser: ownerSignIn },
|
|
193
195
|
mode: localModeInfo,
|
|
194
196
|
};
|
|
@@ -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
|
+
}
|
|
@@ -16,12 +16,14 @@ import { handleClaudeRequest, type ClaudeApiDeps } from './claude-api.js';
|
|
|
16
16
|
import type { ModeInfo } from './mode-api.js';
|
|
17
17
|
import { handleVaultApiRequest, type VaultApiDeps } from './vault-api.js';
|
|
18
18
|
import { handleBundleRequest, type BundleApiDeps } from './bundle-api.js';
|
|
19
|
+
import { handleUpdateRequest, type UpdateApiDeps } from './update-api.js';
|
|
19
20
|
|
|
20
21
|
export interface SessionApis {
|
|
21
22
|
agentApi?: AgentApiDeps;
|
|
22
23
|
agentConnectorApi?: AgentConnectorApiDeps;
|
|
23
24
|
bundleApi?: BundleApiDeps;
|
|
24
25
|
vaultApi?: VaultApiDeps;
|
|
26
|
+
updateApi?: UpdateApiDeps;
|
|
25
27
|
localCli?: LocalCliDeps;
|
|
26
28
|
claudeApi?: ClaudeApiDeps;
|
|
27
29
|
connectorApi?: ConnectorApiDeps;
|
|
@@ -44,6 +46,7 @@ export function handleSessionApis(
|
|
|
44
46
|
...when(apis.connectorApi, (d) => handleConnectorApiRequest(req, res, d)),
|
|
45
47
|
...when(apis.bundleApi, (d) => handleBundleRequest(req, res, d)),
|
|
46
48
|
...when(apis.vaultApi, (d) => handleVaultApiRequest(req, res, d)),
|
|
49
|
+
...when(apis.updateApi, (d) => handleUpdateRequest(req, res, d)),
|
|
47
50
|
...when(apis.claudeApi, (d) => handleClaudeRequest(req, res, d)),
|
|
48
51
|
...when(apis.agentConnectorApi, (d) => handleAgentConnectorRequest(req, res, d)),
|
|
49
52
|
...when(apis.agentApi, (d) => handleAgentApiRequest(req, res, d)),
|
|
@@ -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;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
import { errMsg, log } from './log.js';
|
|
4
|
+
import { ApiError } from './api-error.js';
|
|
5
|
+
import { apiFailure, apiSession, cors, sendJson } from './api-http.js';
|
|
6
|
+
import { isRecord } from './is-record.js';
|
|
7
|
+
import { METRO_VERSION } from './version.js';
|
|
8
|
+
|
|
9
|
+
const PATH = '/api/update';
|
|
10
|
+
const CHECK_TTL_MS = 10 * 60_000;
|
|
11
|
+
const CHECK_TIMEOUT_MS = 30_000;
|
|
12
|
+
const UPDATE_TIMEOUT_MS = 5 * 60_000;
|
|
13
|
+
const RESTART_DELAY_MS = 500;
|
|
14
|
+
const OUTPUT_TAIL = 800;
|
|
15
|
+
|
|
16
|
+
export interface UpdateCheck {
|
|
17
|
+
current: string;
|
|
18
|
+
latest: string;
|
|
19
|
+
newer: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UpdateResult {
|
|
23
|
+
updated: boolean;
|
|
24
|
+
version: string;
|
|
25
|
+
restarting: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UpdateApiDeps {
|
|
29
|
+
authorize: (subject: string) => void;
|
|
30
|
+
restart: () => void;
|
|
31
|
+
cliBin?: () => string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cliBinFromEnv = (): string => process.env.METRO_CLI_BIN?.trim() ?? '';
|
|
35
|
+
|
|
36
|
+
function runCli(bin: string, args: string[], timeout: number): Promise<{ stdout: string; stderr: string }> {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
execFile(process.execPath, [bin, ...args], { timeout, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
39
|
+
if (err === null) resolve({ stdout, stderr });
|
|
40
|
+
else reject(new ApiError(`metro update failed: ${(stderr || err.message).trim().slice(-OUTPUT_TAIL)}`, 502));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let cached: { at: number; check: UpdateCheck } | null = null;
|
|
46
|
+
|
|
47
|
+
function parseCheck(raw: string): UpdateCheck {
|
|
48
|
+
let body: unknown;
|
|
49
|
+
try {
|
|
50
|
+
body = JSON.parse(raw.trim().split('\n').at(-1) ?? '');
|
|
51
|
+
} catch {
|
|
52
|
+
throw new ApiError('metro update --check returned no report', 502);
|
|
53
|
+
}
|
|
54
|
+
if (!isRecord(body) || typeof body.current !== 'string' || typeof body.latest !== 'string')
|
|
55
|
+
throw new ApiError('metro update --check returned an unexpected report', 502);
|
|
56
|
+
return { current: body.current, latest: body.latest, newer: body.newer === true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function check(bin: string, now = Date.now()): Promise<UpdateCheck> {
|
|
60
|
+
if (cached !== null && now - cached.at < CHECK_TTL_MS) return cached.check;
|
|
61
|
+
const out = await runCli(bin, ['update', '--check'], CHECK_TIMEOUT_MS);
|
|
62
|
+
cached = { at: now, check: parseCheck(out.stdout) };
|
|
63
|
+
return cached.check;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function apply(bin: string, deps: UpdateApiDeps): Promise<UpdateResult> {
|
|
67
|
+
const before = await check(bin);
|
|
68
|
+
if (!before.newer) return { updated: false, version: before.current, restarting: false };
|
|
69
|
+
const out = await runCli(bin, ['update'], UPDATE_TIMEOUT_MS);
|
|
70
|
+
cached = null;
|
|
71
|
+
log.info({ from: before.current, to: before.latest, output: out.stderr.trim().slice(-OUTPUT_TAIL) }, 'update-api: metro updated, restarting');
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
deps.restart();
|
|
74
|
+
}, RESTART_DELAY_MS).unref();
|
|
75
|
+
return { updated: true, version: before.latest, restarting: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function binOrThrow(deps: UpdateApiDeps): string {
|
|
79
|
+
const bin = (deps.cliBin ?? cliBinFromEnv)();
|
|
80
|
+
if (bin === '')
|
|
81
|
+
throw new ApiError('this daemon was not started by metro serve, so it cannot update itself: run metro update on the machine', 400);
|
|
82
|
+
return bin;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function handleUpdateRequest(req: IncomingMessage, res: ServerResponse, deps: UpdateApiDeps): boolean {
|
|
86
|
+
const path = (req.url ?? '').split('?')[0] ?? '';
|
|
87
|
+
if (path !== PATH) return false;
|
|
88
|
+
if (req.method === 'OPTIONS') {
|
|
89
|
+
res.writeHead(204, cors(req)).end();
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (req.method !== 'GET' && req.method !== 'POST') {
|
|
93
|
+
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const session = apiSession(req);
|
|
97
|
+
if (!session) {
|
|
98
|
+
sendJson(req, res, 401, { error: 'unauthorized' });
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
Promise.resolve()
|
|
102
|
+
.then(async (): Promise<unknown> => {
|
|
103
|
+
deps.authorize(session.subject);
|
|
104
|
+
const bin = binOrThrow(deps);
|
|
105
|
+
if (req.method === 'GET') return { ...(await check(bin)), running: METRO_VERSION };
|
|
106
|
+
return apply(bin, deps);
|
|
107
|
+
})
|
|
108
|
+
.then((body) => {
|
|
109
|
+
sendJson(req, res, 200, body);
|
|
110
|
+
})
|
|
111
|
+
.catch((err: unknown) => {
|
|
112
|
+
if (err instanceof ApiError) apiFailure(req, res, err, 'update-api');
|
|
113
|
+
else {
|
|
114
|
+
log.warn({ err: errMsg(err) }, 'update-api: request failed');
|
|
115
|
+
if (!res.headersSent) sendJson(req, res, 500, { error: 'update api failed' });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
@@ -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
|
+
}
|