@stage-labs/metro 0.1.0-beta.44 → 0.1.0-beta.46

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 (61) hide show
  1. package/dist/api.js +5 -5
  2. package/dist/bedrock-proxy.js +1 -1
  3. package/dist/cli.js +6 -6
  4. package/dist/plugin.js +1 -1
  5. package/dist/prompt.js +1 -1
  6. package/dist/runtime.js +1 -1
  7. package/dist/serve.js +1 -1
  8. package/package.json +1 -1
  9. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +0 -1
  10. package/runtime/node_modules/@metro-labs/mcp/package.json +1 -5
  11. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +2 -2
  12. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +1 -1
  13. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +2 -5
  14. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +5 -23
  15. package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-files.ts +1 -3
  16. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +2 -4
  17. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +2 -2
  18. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +2 -2
  19. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +3 -5
  20. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +1 -4
  21. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +2 -2
  22. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +1 -2
  23. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +4 -83
  24. package/runtime/node_modules/@metro-labs/mcp/src/daemon/is-record.ts +2 -0
  25. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-cli-api.ts +2 -2
  26. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +1 -37
  27. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +1 -4
  28. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +1 -4
  29. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +0 -33
  30. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +3 -3
  31. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +2 -2
  32. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +1 -6
  33. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +1 -42
  34. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +2 -11
  35. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +2 -2
  36. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +8 -90
  37. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +1 -1
  38. package/runtime/node_modules/@metro-labs/mcp/src/daemon/version.ts +1 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +1 -1
  40. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +1 -4
  41. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +1 -1
  42. package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +6 -4
  43. package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +1 -3
  44. package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +11 -22
  45. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +3 -13
  46. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +3 -3
  47. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +1 -1
  48. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -4
  49. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +1 -1
  50. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +1 -1
  51. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +1 -21
  52. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +1 -1
  53. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +1 -1
  54. package/runtime/node_modules/@metro-labs/telegram/package.json +0 -2
  55. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +0 -1
  56. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +0 -1
  57. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +2 -5
  58. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +0 -1
  59. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +0 -1
  60. package/runtime/runtime.json +1 -1
  61. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +0 -45
package/dist/api.js CHANGED
@@ -103,10 +103,10 @@ export async function claimCode(code) {
103
103
  const body = await post('/api/cli/claim', { code });
104
104
  if (!isRecord(body) ||
105
105
  typeof body.token !== 'string' ||
106
- typeof body.email !== 'string' ||
106
+ typeof body.subject !== 'string' ||
107
107
  typeof body.agent !== 'string')
108
108
  throw new Error('metro returned an unexpected response');
109
- return { token: body.token, email: body.email, agent: body.agent };
109
+ return { token: body.token, subject: body.subject, agent: body.agent };
110
110
  }
111
111
  export async function claimRuntime(code, label) {
112
112
  const body = await post('/api/run/claim', { code, label });
@@ -133,7 +133,7 @@ export async function whoisAuthorized(wanted) {
133
133
  const agents = localAgents();
134
134
  if (agents.length > 0) {
135
135
  const agent = pickLocalAgent(agents, wanted);
136
- return { email: 'this machine', agent: agent.name, where: `${localUrl()} (local)` };
136
+ return { subject: 'this machine', agent: agent.name, where: `${localUrl()} (local)` };
137
137
  }
138
138
  }
139
139
  return { ...(await hostedIdentity()), where: metroUrl() };
@@ -141,8 +141,8 @@ export async function whoisAuthorized(wanted) {
141
141
  async function hostedIdentity() {
142
142
  const body = await get('/api/cli/session');
143
143
  if (!isRecord(body) ||
144
- typeof body.email !== 'string' ||
144
+ typeof body.subject !== 'string' ||
145
145
  typeof body.agent !== 'string')
146
146
  throw new Error('metro returned an unexpected response');
147
- return { email: body.email, agent: body.agent };
147
+ return { subject: body.subject, agent: body.agent };
148
148
  }
@@ -3,7 +3,7 @@ import { EventStreamDecoder } from './bedrock-eventstream.js';
3
3
  import { callBedrock, freshAdaptations, invoke, invokeUrl, upstreamMessage, } from './bedrock-upstream.js';
4
4
  export class BedrockConfigError extends Error {
5
5
  }
6
- export class ProxyError extends Error {
6
+ class ProxyError extends Error {
7
7
  status;
8
8
  kind;
9
9
  constructor(status, kind, message) {
package/dist/cli.js CHANGED
@@ -65,7 +65,7 @@ function hostLabel() {
65
65
  return name === '' ? 'unnamed machine' : name;
66
66
  }
67
67
  async function authorizeRuntime(agentId) {
68
- process.stderr.write(`Authorize this machine at ${metroWebUrl()}/#/authorize/${agentId}\n`);
68
+ process.stderr.write(`Authorize this machine with a pairing code for agent ${agentId} from ${metroWebUrl()}\n`);
69
69
  const code = (await askSecret('Paste the code (input is hidden): ')).trim();
70
70
  if (code === '')
71
71
  throw new Error('no code given');
@@ -134,20 +134,20 @@ function logs(argv) {
134
134
  async function pastedCode(given) {
135
135
  if (given !== undefined && given.trim() !== '')
136
136
  return given.trim();
137
- process.stderr.write(`Get a pairing code from the agent's page, or pick the agent at ${metroWebUrl()}/#/authorize\n`);
137
+ process.stderr.write(`Get a pairing code for the agent from ${metroWebUrl()}\n`);
138
138
  return askSecret('Paste the code here (input is hidden): ');
139
139
  }
140
140
  async function login(given) {
141
- const { token, email, agent } = await claimCode(await pastedCode(given));
141
+ const { token, subject, agent } = await claimCode(await pastedCode(given));
142
142
  writeToken(token);
143
- process.stderr.write(`Authorized '${agent}' for ${email}. Stored in ${credentialsPath()}\n`);
143
+ process.stderr.write(`Authorized '${agent}' for ${subject}. Stored in ${credentialsPath()}\n`);
144
144
  if (syncPluginServers())
145
145
  process.stderr.write('Claude Code plugin refreshed — new sessions have the connectors; ' +
146
146
  'run /reload-plugins in any session already open.\n');
147
147
  }
148
148
  async function whoami(wanted) {
149
- const { email, agent, where } = await whoisAuthorized(wanted);
150
- process.stdout.write(`${email} · agent '${agent}' on ${where}\n`);
149
+ const { subject, agent, where } = await whoisAuthorized(wanted);
150
+ process.stdout.write(`${subject} · agent '${agent}' on ${where}\n`);
151
151
  }
152
152
  const HELP = new Set([undefined, 'help', '--help', '-h']);
153
153
  const COMMANDS = {
package/dist/plugin.js CHANGED
@@ -76,7 +76,7 @@ export async function installPlugin() {
76
76
  'run /reload-plugins in any session already open.\n');
77
77
  return 0;
78
78
  }
79
- process.stderr.write('Now sign in: metro login (get a code at https://metro.box/#/authorize), ' +
79
+ process.stderr.write('Now sign in: metro login (with a pairing code from https://metro.box), ' +
80
80
  'or run /metro:login <code> inside Claude Code.\n');
81
81
  return Promise.resolve(0);
82
82
  }
package/dist/prompt.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createInterface } from 'node:readline';
2
2
  const CTRL_C = '\u0003';
3
3
  const BACKSPACE = '\u007f';
4
- export function askLine(question) {
4
+ function askLine(question) {
5
5
  const rl = createInterface({ input: process.stdin, output: process.stderr });
6
6
  return new Promise((resolve) => {
7
7
  rl.question(question, (answer) => {
package/dist/runtime.js CHANGED
@@ -18,7 +18,7 @@ export function runtimeDir() {
18
18
  throw new MissingRuntime(`no metro daemon at ${dir}. Reinstall with: npm i -g @stage-labs/metro@beta`);
19
19
  return dir;
20
20
  }
21
- export class MissingBun extends Error {
21
+ class MissingBun extends Error {
22
22
  }
23
23
  export function assertAgentId(id) {
24
24
  if (id === undefined || !AGENT_RE.test(id))
package/dist/serve.js CHANGED
@@ -34,7 +34,7 @@ export function parseServeArgs(argv) {
34
34
  }
35
35
  return { port, tunnel };
36
36
  }
37
- export function findCloudflared() {
37
+ function findCloudflared() {
38
38
  const found = spawnSync('cloudflared', ['--version'], { stdio: 'ignore' });
39
39
  if (found.error !== undefined || found.status !== 0)
40
40
  throw new Error('metro serve --tunnel needs cloudflared, which is not on PATH.\n' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.44",
3
+ "version": "0.1.0-beta.46",
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": {
@@ -22,7 +22,6 @@ export interface AccountConfig {
22
22
  export const { loadAccounts } = makeAccountStore<AccountConfig>({
23
23
  prefix: 'discord-bot',
24
24
  file: ACCOUNTS_FILE,
25
- allowlistEnv: ['DISCORD_BOT_ONLY_ACCOUNTS', 'DISCORD_BOT_ACCOUNTS'],
26
25
  validate(raw, die) {
27
26
  const seen = new Set<string>();
28
27
  for (const a of raw) {
@@ -2,7 +2,7 @@
2
2
  "name": "@metro-labs/mcp",
3
3
  "version": "0.1.0-beta.15",
4
4
  "private": true,
5
- "description": "Event-interception wire. Supervises train subprocesses in ~/.metro/trains/, multiplexes their JSON event stream onto stdout, and routes outbound action calls back via stdin. Per-platform code is written by the agent (or user) as train scripts — metro core is pure transport.",
5
+ "description": "Event-interception wire. Supervises train subprocesses in ~/.metro/trains/, multiplexes their JSON event stream onto stdout, and routes outbound action calls back via stdin. Per-platform code is written by the agent (or user) as train scripts \u2014 metro core is pure transport.",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -14,12 +14,8 @@
14
14
  "engines": {
15
15
  "node": ">=22"
16
16
  },
17
- "bin": {
18
- "metro-daemon": "./dist/server.js"
19
- },
20
17
  "exports": {
21
18
  ".": "./src/mcp/index.ts",
22
- "./server": "./dist/server.js",
23
19
  "./log": "./src/daemon/log.ts",
24
20
  "./train-error": "./src/daemon/train-error.ts",
25
21
  "./secure-fs": "./src/daemon/secure-fs.ts",
@@ -9,7 +9,7 @@ import type { LoadedAgent } from '../db/materialize.js';
9
9
  const CLAIM_TIMEOUT_MS = 15_000;
10
10
  const CODE_RE = /^ma_[A-Za-z0-9_-]{16}$/;
11
11
 
12
- export function metroBaseUrl(): string {
12
+ function metroBaseUrl(): string {
13
13
  const raw = (process.env.METRO_URL?.trim() ?? '').replace(/\/+$/, '');
14
14
  const url = raw === '' ? 'https://mcp.metro.box' : raw;
15
15
  if (!carriesSecretsSafely(url))
@@ -53,7 +53,7 @@ async function claim(base: string, code: string, label: string): Promise<string>
53
53
  return token;
54
54
  }
55
55
 
56
- export function storeRunToken(agentId: string, token: string, url: string): string {
56
+ function storeRunToken(agentId: string, token: string, url: string): string {
57
57
  const dir = configDir();
58
58
  mkdirSync(dir, { recursive: true, mode: 0o700 });
59
59
  const path = join(dir, `runtime-${agentId}.json`);
@@ -34,7 +34,7 @@ function parseGrant(raw: string): AttachmentGrant | undefined {
34
34
  };
35
35
  }
36
36
 
37
- export function readGrant(path: string): AttachmentGrant | undefined {
37
+ function readGrant(path: string): AttachmentGrant | undefined {
38
38
  try {
39
39
  return parseGrant(readFileSync(`${path}${GRANT_SUFFIX}`, 'utf8'));
40
40
  } catch {
@@ -14,7 +14,7 @@ import {
14
14
  import { attachmentOwner, recordAttachmentOwner } from './attach-owner.js';
15
15
  import { grantAllows, issueAttachmentGrant } from './attach-grant.js';
16
16
  import { errMsg, log } from './log.js';
17
- import { configuredTunnelHost, currentTunnelUrl, webhookPort } from './tunnel.js';
17
+ import { currentTunnelUrl, webhookPort } from './tunnel.js';
18
18
  import { isLocalMode } from './paths.js';
19
19
 
20
20
  function authorized(req: IncomingMessage, name: string): boolean {
@@ -31,10 +31,7 @@ const DEFAULT_PUBLIC_BASE = 'https://mcp.metro.box';
31
31
  export function publicBaseUrl(): string | null {
32
32
  const env = process.env.METRO_PUBLIC_URL?.trim();
33
33
  if (env) return env.replace(/\/+$/, '');
34
- const live = currentTunnelUrl();
35
- if (live !== null) return live;
36
- const host = configuredTunnelHost();
37
- return host ? `https://${host}` : null;
34
+ return currentTunnelUrl();
38
35
  }
39
36
 
40
37
  const servesLocally = (): boolean =>
@@ -1,24 +1,12 @@
1
1
  import type { ProjectApiDeps } from './project-api.js';
2
2
  import { join } from 'node:path';
3
3
  import { type Server } from 'node:http';
4
- import { selfLine, userSelf } from './events.js';
4
+ import { userSelf } from './events.js';
5
5
  import { setTrainCallBackend } from './train-call.js';
6
6
  import { errMsg, log, logFatalSync } from './log.js';
7
- import {
8
- acquireLock,
9
- isLocalMode,
10
- loadMetroEnv,
11
- STATE_DIR,
12
- trainsDir,
13
- } from './paths.js';
7
+ import { acquireLock, isLocalMode, STATE_DIR, trainsDir } from './paths.js';
14
8
  import { installCrashGuard, markDaemonReady } from './crash-guard.js';
15
- import {
16
- loadTunnelConfig,
17
- Tunnel,
18
- tunnelConfigFromEnv,
19
- warnOnLegacyWebhooks,
20
- webhookPort,
21
- } from './tunnel.js';
9
+ import { quickTunnelWanted, Tunnel, webhookPort } from './tunnel.js';
22
10
  import {
23
11
  localConnectHint,
24
12
  publicConnectHint,
@@ -115,11 +103,9 @@ import type { AgentApiDeps } from './agent-api.js';
115
103
  import type { ConnectorApiDeps } from './connector-api.js';
116
104
 
117
105
  installCrashGuard();
118
- loadMetroEnv();
119
106
  acquireLock(join(STATE_DIR, '.tail-lock'));
120
107
 
121
- const self = userSelf();
122
- log.info({ self, line: selfLine() }, 'user identity');
108
+ log.info({ self: userSelf() }, 'user identity');
123
109
 
124
110
  process.stdout.on('error', (err) => {
125
111
  if ((err as NodeJS.ErrnoException).code !== 'EPIPE')
@@ -135,8 +121,7 @@ supervisor.onTrainEvent((env, train) => {
135
121
  });
136
122
 
137
123
  let webhookServer: Server | null = null;
138
- const tunnelCfg = loadTunnelConfig() ?? tunnelConfigFromEnv();
139
- const tunnel = tunnelCfg ? new Tunnel(tunnelCfg, webhookPort(), announcePublic) : null;
124
+ const tunnel = quickTunnelWanted() ? new Tunnel(webhookPort(), announcePublic) : null;
140
125
 
141
126
  function announcePublic(url: string): void {
142
127
  process.stderr.write(`\n${publicConnectHint(url)}`);
@@ -321,7 +306,6 @@ async function main(): Promise<void> {
321
306
  if (isLocalMode()) ensureLocalSessionSecret();
322
307
  if (localSource === null) await materializeFromDb();
323
308
  else await materializeFrom(localSource, { allowEmpty: linkedSource === null });
324
- warnOnLegacyWebhooks();
325
309
  supervisor.start();
326
310
  const metroMcp = await createMetroMcp();
327
311
  webhookServer = await startWebhookServer(
@@ -373,8 +357,6 @@ const onShutdown = (): void => {
373
357
  process.exit(1);
374
358
  });
375
359
  };
376
- if (process.env.METRO_STDIN_SHUTDOWN === '1')
377
- process.stdin.on('end', onShutdown).on('close', onShutdown);
378
360
  for (const sig of ['SIGINT', 'SIGTERM'] as const) process.on(sig, onShutdown);
379
361
 
380
362
  await main().catch((err: unknown) => {
@@ -12,6 +12,7 @@ import { homedir } from 'node:os';
12
12
  import { join } from 'node:path';
13
13
  import { createInterface } from 'node:readline';
14
14
  import { ApiError } from './api-error.js';
15
+ import { isRecord } from './is-record.js';
15
16
 
16
17
  const PROJECT_RE = /^[A-Za-z0-9._-]{1,200}$/;
17
18
  const SESSION_RE = /^[A-Za-z0-9][A-Za-z0-9-]{7,63}$/;
@@ -31,9 +32,6 @@ export function claudeDir(): string {
31
32
  return join(homedir(), '.claude');
32
33
  }
33
34
 
34
- const isRecord = (v: unknown): v is Record<string, unknown> =>
35
- typeof v === 'object' && v !== null && !Array.isArray(v);
36
-
37
35
  const str = (v: unknown): string | null => (typeof v === 'string' && v !== '' ? v : null);
38
36
 
39
37
  function safeName(value: string, re: RegExp, what: string): string {
@@ -13,7 +13,7 @@ import { extractToken } from '../mcp/request-identity.js';
13
13
  import { publicBaseOrDefault } from './attach-serve.js';
14
14
  import { AGENT_CODE_RE, takeAgentCode } from './agent-pair.js';
15
15
  import { relayServersJson } from './connector-json.js';
16
- import { sessionTtlFromEnv } from './session-config.js';
16
+ import { SESSION_TTL_SEC } from './session-config.js';
17
17
  import { signAgentToken } from './session.js';
18
18
  import type { ConnectorApiDeps } from './connector-api.js';
19
19
 
@@ -64,12 +64,11 @@ async function handleClaim(
64
64
  const token = signAgentToken(
65
65
  { subject: taken.subject, agentId: agent.id },
66
66
  secret,
67
- { ttlSec: sessionTtlFromEnv() },
67
+ { ttlSec: SESSION_TTL_SEC },
68
68
  );
69
69
  log.info({ subject: taken.subject, agent: agent.id }, 'cli: code claimed');
70
70
  sendJson(req, res, 200, {
71
71
  token,
72
- email: taken.subject,
73
72
  subject: taken.subject,
74
73
  agent: agent.name,
75
74
  });
@@ -90,7 +89,6 @@ async function handleRead(
90
89
  const agent = await deps.agentConnectors(who.subject, who.agentId);
91
90
  if (path === SESSION_PATH) {
92
91
  sendJson(req, res, 200, {
93
- email: who.subject,
94
92
  subject: who.subject,
95
93
  agent: agent.name,
96
94
  });
@@ -2,8 +2,8 @@ import { hostname } from 'node:os';
2
2
 
3
3
  const uiBase = (): string => process.env.METRO_UI_URL ?? 'https://metro.box';
4
4
 
5
- export const connectLink = (base: string): string =>
6
- `${uiBase()}/#/connect/${encodeURIComponent(base)}`;
5
+ const connectLink = (base: string): string =>
6
+ `${uiBase()}/#/${new URL(base).host}`;
7
7
 
8
8
  export function localConnectHint(port: number): string {
9
9
  const here = `http://127.0.0.1:${String(port)}`;
@@ -19,9 +19,9 @@ export { takePending, type PendingAuth };
19
19
 
20
20
  const REFRESH_SKEW_MS = 300_000;
21
21
 
22
- export const CALLBACK_PATH = '/api/connectors/callback';
22
+ const CALLBACK_PATH = '/api/connectors/callback';
23
23
 
24
- export const callbackUri = (): string =>
24
+ const callbackUri = (): string =>
25
25
  `${publicBaseOrDefault()}${CALLBACK_PATH}`;
26
26
 
27
27
  function refused(message: string): ConnectorVerifyError {
@@ -1,3 +1,5 @@
1
+ import { isRecord } from './is-record.js';
2
+
1
3
  export const TOOL_KINDS = ['read', 'write'] as const;
2
4
 
3
5
  export type ToolKind = (typeof TOOL_KINDS)[number];
@@ -16,10 +18,6 @@ export interface ToolInfo {
16
18
  const MAX_TOOLS = 500;
17
19
  const MAX_DESCRIPTION = 400;
18
20
 
19
- function isRecord(value: unknown): value is Record<string, unknown> {
20
- return typeof value === 'object' && value !== null && !Array.isArray(value);
21
- }
22
-
23
21
  const str = (value: unknown, cap: number): string =>
24
22
  typeof value === 'string' ? value.slice(0, cap) : '';
25
23
 
@@ -66,7 +64,7 @@ export function toToolList(raw: unknown): ToolInfo[] {
66
64
  return out;
67
65
  }
68
66
 
69
- export function toStoredTool(raw: unknown): ToolInfo | null {
67
+ function toStoredTool(raw: unknown): ToolInfo | null {
70
68
  if (!isRecord(raw)) return null;
71
69
  const name = str(raw.name, 200);
72
70
  if (name === '') return null;
@@ -13,6 +13,7 @@ export {
13
13
  connectorUrlText,
14
14
  parseConnectorUrl,
15
15
  } from './connector-url.js';
16
+ import { isRecord } from './is-record.js';
16
17
 
17
18
  export interface OAuthTokens {
18
19
  accessToken: string;
@@ -58,10 +59,6 @@ function notMcp(url: URL): ConnectorNotMcp {
58
59
  const PROBE_TIMEOUT_MS = 10_000;
59
60
  const ACCEPT = 'application/json, text/event-stream';
60
61
 
61
- function isRecord(value: unknown): value is Record<string, unknown> {
62
- return typeof value === 'object' && value !== null && !Array.isArray(value);
63
- }
64
-
65
62
  async function discard(res: Response): Promise<void> {
66
63
  await res.body?.cancel().catch((err: unknown) => {
67
64
  log.debug(
@@ -1,7 +1,7 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { Line } from '../stations/lines.js';
3
3
 
4
- export { userSelf, daemonSelf, selfLine } from './identity.js';
4
+ export { userSelf, daemonSelf } from './identity.js';
5
5
 
6
6
  export type StructuredEvent =
7
7
  | { type: 'msg' }
@@ -81,7 +81,7 @@ export interface BufferedEvent {
81
81
  event: MetroEvent;
82
82
  }
83
83
 
84
- export const BUS_BUFFER_MAX = 500;
84
+ const BUS_BUFFER_MAX = 500;
85
85
 
86
86
  const listeners = new Set<BusListener>();
87
87
  const buffer: BufferedEvent[] = [];
@@ -35,11 +35,10 @@ import {
35
35
  type MonitorCall,
36
36
  } from '../monitor/api.js';
37
37
  import { applyMcpCors, handleMcpPreflight } from './cors.js';
38
+ import { METRO_VERSION } from './version.js';
38
39
 
39
40
  const LRU_CAP = 2_000;
40
41
 
41
- const METRO_VERSION = process.env.npm_package_version ?? '0.1.0-beta.15';
42
-
43
42
  function dedupKey(
44
43
  e: Pick<MetroEvent, 'station' | 'line' | 'messageId'>,
45
44
  ): string | null {
@@ -1,86 +1,7 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { errMsg, log } from './log.js';
3
1
  import { Line } from '../stations/lines.js';
4
2
 
5
- const TTL_MS = 5_000;
6
- type Cache = { id: string; at: number } | null;
3
+ export const userSelf = (): Line =>
4
+ (process.env.METRO_FROM ?? 'metro://user') as Line;
7
5
 
8
- function memo(loader: () => string): () => string {
9
- let cache: Cache = null;
10
- return () => {
11
- if (cache && Date.now() - cache.at < TTL_MS) return cache.id;
12
- const id = loader();
13
- cache = { id, at: Date.now() };
14
- return id;
15
- };
16
- }
17
-
18
- const claudeAccountId = memo(() => {
19
- let raw: string;
20
- try {
21
- raw = execFileSync('claude', ['auth', 'status', '--json'], {
22
- encoding: 'utf8',
23
- stdio: ['ignore', 'pipe', 'pipe'],
24
- });
25
- } catch (e) {
26
- throw new Error(
27
- `metro: failed to run 'claude auth status --json' — is Claude Code installed? (${errMsg(e)})`,
28
- );
29
- }
30
- let p: { loggedIn?: boolean; orgId?: string };
31
- try {
32
- p = JSON.parse(raw) as { loggedIn?: boolean; orgId?: string };
33
- } catch {
34
- throw new Error(
35
- `metro: 'claude auth status --json' returned non-JSON: ${raw.slice(0, 200)}`,
36
- );
37
- }
38
- if (!p.loggedIn || !p.orgId)
39
- throw new Error(
40
- "metro: Claude Code is not logged in — run 'claude auth login'",
41
- );
42
- return p.orgId;
43
- });
44
-
45
- export const claudeUserId = (): string =>
46
- process.env.METRO_USER_ID ?? claudeAccountId();
47
-
48
- export const claudeSessionId = (): string | null =>
49
- process.env.METRO_USER_SESSION_ID ??
50
- process.env.CLAUDE_CODE_SESSION_ID ??
51
- null;
52
-
53
- export function userSelf(): Line {
54
- const explicit = process.env.METRO_FROM;
55
- if (explicit) return explicit as Line;
56
- if (!process.env.CLAUDECODE) return 'metro://user' as Line;
57
- try {
58
- return Line.user('claude', claudeUserId());
59
- } catch (e) {
60
- log.warn(
61
- { reason: errMsg(e) },
62
- 'could not resolve a Claude account id; relaying as metro://user',
63
- );
64
- return 'metro://user' as Line;
65
- }
66
- }
67
-
68
- export function daemonSelf(): Line {
69
- const explicit = process.env.METRO_FROM ?? process.env.METRO_SELF_URI;
70
- return (explicit ?? 'metro://user') as Line;
71
- }
72
-
73
- export function selfLine(): Line | null {
74
- if (!process.env.CLAUDECODE) return null;
75
- const s = claudeSessionId();
76
- if (!s) return null;
77
- try {
78
- return Line.claude(claudeUserId(), s);
79
- } catch (e) {
80
- log.warn(
81
- { reason: errMsg(e) },
82
- 'could not resolve a Claude account id; relaying with no self line',
83
- );
84
- return null;
85
- }
86
- }
6
+ export const daemonSelf = (): Line =>
7
+ (process.env.METRO_FROM ?? process.env.METRO_SELF_URI ?? 'metro://user') as Line;
@@ -0,0 +1,2 @@
1
+ export const isRecord = (v: unknown): v is Record<string, unknown> =>
2
+ typeof v === 'object' && v !== null && !Array.isArray(v);
@@ -14,7 +14,7 @@ export interface LocalCliDeps {
14
14
  connectorSummaries: (agentId: string) => Promise<ConnectorSummary[]>;
15
15
  }
16
16
 
17
- export function keyOf(req: IncomingMessage): string {
17
+ function keyOf(req: IncomingMessage): string {
18
18
  const header = req.headers.authorization ?? '';
19
19
  if (header.toLowerCase().startsWith('bearer ')) return header.slice(7).trim();
20
20
  const query = new URLSearchParams((req.url ?? '').split('?')[1] ?? '');
@@ -29,7 +29,7 @@ export function keyIdentity(req: IncomingMessage): AgentIdentity | null {
29
29
 
30
30
  async function answer(path: string, key: string, agentId: string, deps: LocalCliDeps): Promise<unknown> {
31
31
  const agent = deps.agentName(agentId) ?? '';
32
- if (path === `${PREFIX}/session`) return { email: 'this machine', subject: 'this machine', agent };
32
+ if (path === `${PREFIX}/session`) return { subject: 'this machine', agent };
33
33
  if (path === `${PREFIX}/connectors`) return { agent, connectors: await deps.connectorSummaries(agentId) };
34
34
  return { json: relayServersJson(await deps.connectorEntries(agentId), publicBaseOrDefault(), key), agent };
35
35
  }
@@ -5,7 +5,6 @@ import { AttachSessions } from './attach-session.js';
5
5
  import type { AgentApiDeps } from './agent-api.js';
6
6
  import { ATTACHABLE, type AccountApiDeps } from './account-api.js';
7
7
  import type { AgentConnectorApiDeps } from './agent-connector-api.js';
8
- import type { ProjectApiDeps } from './project-api.js';
9
8
  import type { SessionApis } from './session-apis.js';
10
9
  import type { ModeInfo } from './mode-api.js';
11
10
  import type { ImportApiDeps } from './import-api.js';
@@ -46,9 +45,7 @@ import {
46
45
  localOwner,
47
46
  localResetAgentKey,
48
47
  } from '../db/file-admin.js';
49
- import { normalizeAddress } from '../db/users.js';
50
48
  import { listAgentFiles, readAgentFile } from '../db/file-source.js';
51
- import type { Project } from '../db/projects.js';
52
49
  import type { StationName } from '../db/schema.js';
53
50
 
54
51
  export interface LocalModeDeps {
@@ -66,16 +63,6 @@ const notHere = (what: string): Promise<never> =>
66
63
  new ApiError(`${what} are managed on metro.box, not on a local daemon`, 400),
67
64
  );
68
65
 
69
- function localProject(): Project {
70
- return {
71
- id: LOCAL_PROJECT_ID,
72
- name: hostname(),
73
- isDefault: true,
74
- owner: true,
75
- role: 'admin',
76
- };
77
- }
78
-
79
66
  function attachSessions(deps: LocalModeDeps): AttachSessions {
80
67
  return new AttachSessions({
81
68
  authorize: async (owner) => {
@@ -124,28 +111,6 @@ function agentApi(deps: LocalModeDeps): AgentApiDeps {
124
111
  };
125
112
  }
126
113
 
127
- const projectApi: ProjectApiDeps = {
128
- listProjects: (subject) => {
129
- const owner = localOwner();
130
- const mine = owner !== null && owner === normalizeAddress(subject);
131
- return Promise.resolve(mine ? [localProject()] : []);
132
- },
133
- createProject: () => notHere('projects'),
134
- renameProject: () => notHere('projects'),
135
- deleteProject: () => notHere('projects'),
136
- listMembers: (subject, id) => {
137
- const owner = localOwner();
138
- if (id !== LOCAL_PROJECT_ID || owner === null || owner !== normalizeAddress(subject))
139
- return Promise.reject(new ApiError('no such project', 404));
140
- return Promise.resolve([
141
- { id: 'owner', address: owner, email: null, role: 'admin', owner: true },
142
- ]);
143
- },
144
- addMember: () => notHere('members'),
145
- setMemberRole: () => notHere('members'),
146
- removeMember: () => notHere('members'),
147
- };
148
-
149
114
  function connectorIdsOfLocalAgents(ids: string[]): Promise<Map<string, string[]>> {
150
115
  return Promise.resolve(new Map(ids.map((id) => [id, connectorIdsOfLocalAgent(id) ?? []] as const)));
151
116
  }
@@ -211,7 +176,7 @@ function importApi(deps: LocalModeDeps): ImportApiDeps {
211
176
  };
212
177
  }
213
178
 
214
- export function localModeInfo(): ModeInfo {
179
+ function localModeInfo(): ModeInfo {
215
180
  return { mode: 'local', owner: localOwner(), project: LOCAL_PROJECT_ID };
216
181
  }
217
182
 
@@ -225,7 +190,6 @@ export function localSessionApis(deps: LocalModeDeps): SessionApis {
225
190
  relayApi,
226
191
  localCli,
227
192
  claudeApi: { authorize: (subject) => { assertLocalOwner(subject); } },
228
- projectApi,
229
193
  siwe: { ensureUser: claimLocalOwner },
230
194
  mode: localModeInfo,
231
195
  };
@@ -4,6 +4,7 @@ import {
4
4
  type OAuthTokens,
5
5
  } from './connector-verify.js';
6
6
  import type { OAuthServer } from './oauth-discovery.js';
7
+ import { isRecord } from './is-record.js';
7
8
 
8
9
  const TOKEN_TIMEOUT_MS = 15_000;
9
10
  const CLIENT_NAME = 'Metro';
@@ -17,10 +18,6 @@ function refused(message: string): ConnectorVerifyError {
17
18
  return new ConnectorVerifyError(message, 400);
18
19
  }
19
20
 
20
- function isRecord(value: unknown): value is Record<string, unknown> {
21
- return typeof value === 'object' && value !== null && !Array.isArray(value);
22
- }
23
-
24
21
  export function newVerifier(): string {
25
22
  return randomBytes(32).toString('base64url');
26
23
  }