@stage-labs/metro 0.1.0-beta.172 → 0.1.0-beta.174

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 (147) hide show
  1. package/dist/api.js +6 -4
  2. package/dist/claude.js +5 -24
  3. package/dist/cli.js +3 -3
  4. package/dist/local.js +18 -52
  5. package/dist/serve.js +17 -14
  6. package/dist/service.js +4 -4
  7. package/dist/tail.js +8 -8
  8. package/package.json +1 -1
  9. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
  10. package/runtime/marketplace/plugin/bin/metro-plugin.mjs +9 -19
  11. package/runtime/node_modules/@metro-labs/core/package.json +4 -3
  12. package/runtime/node_modules/@metro-labs/core/src/events.ts +1 -3
  13. package/runtime/node_modules/@metro-labs/core/src/ids.ts +3 -0
  14. package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +17 -0
  15. package/runtime/node_modules/@metro-labs/core/src/station-names.ts +0 -1
  16. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +2 -5
  17. package/runtime/node_modules/@metro-labs/core/src/stations/train-boot.ts +42 -0
  18. package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +89 -0
  19. package/runtime/node_modules/@metro-labs/core/src/tickets.ts +37 -0
  20. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +11 -28
  21. package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +0 -9
  22. package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +11 -91
  23. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +7 -8
  24. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +16 -93
  25. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +1 -34
  26. package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +0 -5
  27. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +6 -7
  28. package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +11 -20
  29. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +1 -13
  30. package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +1 -1
  31. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +5 -9
  32. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +2 -7
  33. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +1 -16
  34. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +3 -20
  35. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +15 -26
  36. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +4 -10
  37. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +26 -55
  38. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +1 -8
  39. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +1 -5
  40. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +8 -21
  41. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +20 -9
  42. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +4 -14
  43. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +75 -64
  44. package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +4 -32
  45. package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +2 -24
  46. package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +25 -20
  47. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-reaper.ts +174 -0
  48. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +3 -1
  49. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +3 -19
  50. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +15 -41
  51. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +17 -40
  52. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +5 -5
  53. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +17 -132
  54. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +9 -18
  55. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +35 -140
  56. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +43 -2
  57. package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +118 -0
  58. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +5 -8
  59. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +18 -40
  60. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +15 -15
  61. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +14 -26
  62. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +12 -21
  63. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +30 -133
  64. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +0 -1
  65. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +0 -1
  66. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +4 -4
  67. package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +36 -0
  68. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +5 -5
  69. package/runtime/node_modules/@metro-labs/daemon/src/gateway/subscription.ts +120 -0
  70. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +15 -0
  71. package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +1 -1
  72. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +5 -0
  73. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +14 -15
  74. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +22 -71
  75. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +4 -5
  76. package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +5 -6
  77. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +48 -0
  78. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +12 -31
  79. package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +1 -6
  80. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +2 -75
  81. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +3 -8
  82. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +19 -29
  83. package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +1 -1
  84. package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +15 -41
  85. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +3 -25
  86. package/runtime/node_modules/@metro-labs/daemon/src/server/owner.ts +3 -25
  87. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +7 -34
  88. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +1 -6
  89. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +1 -13
  90. package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +51 -0
  91. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +8 -31
  92. package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +6 -31
  93. package/runtime/node_modules/@metro-labs/discord-bot/package.json +1 -3
  94. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +0 -1
  95. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +0 -8
  96. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +7 -66
  97. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +5 -10
  98. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +0 -4
  99. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +39 -7
  100. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +1 -1
  101. package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +3 -4
  102. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +0 -1
  103. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +0 -48
  104. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +11 -33
  105. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +4 -43
  106. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +0 -1
  107. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +0 -1
  108. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +0 -8
  109. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +9 -50
  110. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +7 -13
  111. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +3 -2
  112. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +0 -2
  113. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +0 -1
  114. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +0 -1
  115. package/runtime/node_modules/@metro-labs/threema/src/files.ts +8 -43
  116. package/runtime/node_modules/@metro-labs/threema/src/format.ts +3 -16
  117. package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +5 -6
  118. package/runtime/node_modules/@metro-labs/threema/src/index.ts +2 -7
  119. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +0 -1
  120. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +1 -56
  121. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +10 -27
  122. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +4 -37
  123. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +0 -1
  124. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +0 -4
  125. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +1 -2
  126. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +0 -27
  127. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +0 -10
  128. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +4 -23
  129. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +3 -5
  130. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +4 -8
  131. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +0 -47
  132. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +23 -43
  133. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +3 -5
  134. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +0 -2
  135. package/runtime/runtime.json +1 -1
  136. package/runtime/stations.json +1 -3
  137. package/runtime/node_modules/@metro-labs/core/src/address.ts +0 -6
  138. package/runtime/node_modules/@metro-labs/daemon/src/connectors/model.ts +0 -69
  139. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-pending.ts +0 -65
  140. package/runtime/node_modules/@metro-labs/daemon/src/mcp/channel-owner.ts +0 -29
  141. package/runtime/node_modules/@metro-labs/daemon/src/mcp/frame-scope.ts +0 -21
  142. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-registry.ts +0 -172
  143. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-route.ts +0 -43
  144. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +0 -195
  145. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +0 -292
  146. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +0 -197
  147. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +0 -63
package/dist/api.js CHANGED
@@ -1,9 +1,11 @@
1
- import { localAgents, localDaemonUp, pickLocalAgent } from './local.js';
1
+ import { localAgent, localDaemonUp } from './local.js';
2
2
  import { localUrl } from './runtime.js';
3
3
  const NO_DAEMON = `no metro daemon on ${localUrl()} — start one with: metro serve`;
4
- export async function whoisAuthorized(wanted) {
4
+ export async function whoisAuthorized() {
5
5
  if (!(await localDaemonUp()))
6
6
  throw new Error(NO_DAEMON);
7
- const agent = pickLocalAgent(localAgents(), wanted);
8
- return { agent: agent.name, where: localUrl() };
7
+ const agent = localAgent();
8
+ if (agent === null)
9
+ throw new Error('no agent on this machine yet');
10
+ return { agent: agent.id, where: localUrl() };
9
11
  }
package/dist/claude.js CHANGED
@@ -3,7 +3,7 @@ import { markOnboardingDone, seedChannels } from './onboarding.js';
3
3
  import { writeMcpConfig } from './mcp-config.js';
4
4
  import { currentRoute, permissionMode, routeModelEnv, systemPrompt } from './route.js';
5
5
  import { settingsConflicts, settingsFiles } from './claude-settings.js';
6
- import { localAgents, pickLocalAgent } from './local.js';
6
+ import { localAgent } from './local.js';
7
7
  import { PROVIDER_FLAGS } from './provider-flags.js';
8
8
  import { localPort, localUrl } from './runtime.js';
9
9
  const CHANNEL_FLAGS = ['--dangerously-load-development-channels', 'server:metro'];
@@ -76,27 +76,8 @@ async function daemonServing(base = localUrl()) {
76
76
  return false;
77
77
  }
78
78
  }
79
- export function agentKey(agents, wanted) {
80
- if (agents.length === 0)
81
- return { skip: 'no agent lives on this machine yet, so Claude Code talks to Anthropic directly' };
82
- try {
83
- return { key: pickLocalAgent(agents, wanted).key };
84
- }
85
- catch {
86
- return {
87
- skip: wanted === undefined || wanted === ''
88
- ? 'this box has no agent yet (or several old ones), so Claude Code talks to Anthropic directly for now'
89
- : `no agent named '${wanted}' lives here, so Claude Code talks to Anthropic directly`,
90
- };
91
- }
92
- }
93
- function localAgentList() {
94
- try {
95
- return localAgents();
96
- }
97
- catch {
98
- return [];
99
- }
79
+ export function agentKey(agent) {
80
+ return agent === null ? { skip: 'no agent lives on this machine yet, so Claude Code talks to Anthropic directly' } : { key: agent.key };
100
81
  }
101
82
  async function verdict() {
102
83
  const pinned = pinnedBy(process.env);
@@ -105,7 +86,7 @@ async function verdict() {
105
86
  const conflicts = settingsConflicts(settingsFiles());
106
87
  if (conflicts.length > 0)
107
88
  return { skip: `a settings file pins the provider (${conflicts.join(', ')}), so Claude Code keeps it` };
108
- const picked = agentKey(localAgentList(), undefined);
89
+ const picked = agentKey(localAgent());
109
90
  if ('skip' in picked)
110
91
  return picked;
111
92
  if (await daemonServing())
@@ -131,7 +112,7 @@ export function runClaude(args, env) {
131
112
  async function servedKey(decision) {
132
113
  if ('key' in decision)
133
114
  return decision.key;
134
- const picked = agentKey(localAgentList(), undefined);
115
+ const picked = agentKey(localAgent());
135
116
  if ('skip' in picked)
136
117
  return null;
137
118
  return (await daemonServing()) ? picked.key : null;
package/dist/cli.js CHANGED
@@ -22,7 +22,7 @@ const USAGE = `metro — run your agent on this machine
22
22
  starts at boot and after a crash, so the page can stop, start and
23
23
  restart it with no shell; metro service uninstall and status as well
24
24
  metro stop stop metro on this machine, metro serve included
25
- metro tail <agent-id>
25
+ metro tail [agent-id]
26
26
  follow this machine's inbound events, one JSON line each
27
27
  metro whoami print the agent this machine runs
28
28
  metro claude [args...]
@@ -53,8 +53,8 @@ async function stopDaemon() {
53
53
  const HELP = new Set([undefined, 'help', '--help', '-h']);
54
54
  const COMMANDS = {
55
55
  whoami: async () => {
56
- const { agent, where } = await whoisAuthorized(process.argv[3]);
57
- process.stdout.write(`agent '${agent}' on ${where}\n`);
56
+ const { agent, where } = await whoisAuthorized();
57
+ process.stdout.write(`agent ${agent} on ${where}\n`);
58
58
  return 0;
59
59
  },
60
60
  serve: () => serve(process.argv.slice(3)),
package/dist/local.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
1
+ import { readFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { localUrl } from './runtime.js';
@@ -7,64 +7,30 @@ export function agentsDir() {
7
7
  const explicit = process.env.METRO_AGENTS_DIR?.trim();
8
8
  return explicit !== undefined && explicit !== '' ? explicit : join(homedir(), '.metro', 'agents');
9
9
  }
10
- function agentFiles(dir) {
11
- const fixed = join(dir, 'agent.json');
12
- if (existsSync(fixed))
13
- return [fixed];
14
- if (!existsSync(dir))
15
- return [];
16
- return readdirSync(dir, { withFileTypes: true })
17
- .filter((entry) => entry.isDirectory())
18
- .map((entry) => join(dir, entry.name, 'agent.json'))
19
- .filter((path) => existsSync(path));
20
- }
21
- export function localAgents(dir = agentsDir()) {
22
- const out = [];
23
- for (const path of agentFiles(dir)) {
24
- try {
25
- const file = JSON.parse(readFileSync(path, 'utf8'));
26
- if (typeof file.id === 'string' && typeof file.key === 'string')
27
- out.push({ id: file.id, name: typeof file.name === 'string' ? file.name : 'agent', key: file.key });
28
- }
29
- catch {
30
- continue;
31
- }
10
+ function agentFile(dir) {
11
+ try {
12
+ return JSON.parse(readFileSync(join(dir, 'agent.json'), 'utf8'));
13
+ }
14
+ catch {
15
+ return null;
32
16
  }
33
- return out.sort((a, b) => a.name.localeCompare(b.name));
17
+ }
18
+ export function localAgent(dir = agentsDir()) {
19
+ const file = agentFile(dir);
20
+ return typeof file?.id === 'string' && typeof file.key === 'string' ? { id: file.id, key: file.key } : null;
34
21
  }
35
22
  export function localStations(dir = agentsDir()) {
23
+ const stations = agentFile(dir)?.stations;
24
+ if (!Array.isArray(stations))
25
+ return [];
36
26
  const out = new Set();
37
- for (const path of agentFiles(dir)) {
38
- try {
39
- const file = JSON.parse(readFileSync(path, 'utf8'));
40
- if (!Array.isArray(file.stations))
41
- continue;
42
- for (const s of file.stations) {
43
- const station = s.station;
44
- if (typeof station === 'string')
45
- out.add(station);
46
- }
47
- }
48
- catch {
49
- continue;
50
- }
27
+ for (const s of stations) {
28
+ const station = s.station;
29
+ if (typeof station === 'string')
30
+ out.add(station);
51
31
  }
52
32
  return [...out].sort();
53
33
  }
54
- export function pickLocalAgent(agents, wanted) {
55
- if (wanted !== undefined && wanted !== '') {
56
- const found = agents.find((a) => a.name === wanted || a.id === wanted);
57
- if (found === undefined)
58
- throw new Error(`no local agent named '${wanted}'`);
59
- return found;
60
- }
61
- const only = agents[0];
62
- if (only !== undefined && agents.length === 1)
63
- return only;
64
- throw new Error(agents.length === 0
65
- ? 'no agent on this machine yet'
66
- : `several agents on this machine — name one: ${agents.map((a) => a.name).join(', ')}`);
67
- }
68
34
  export async function localDaemonUp(base = localUrl()) {
69
35
  try {
70
36
  const res = await fetch(`${base}/api/mode`, { signal: AbortSignal.timeout(PROBE_MS) });
package/dist/serve.js CHANGED
@@ -8,7 +8,7 @@ import { findBun, localPort, spawnPlan } from './runtime.js';
8
8
  import { prepareRuntime } from './runtime-install.js';
9
9
  import { ensureNodeName } from './node-name.js';
10
10
  import { holdUntilStart } from './hold.js';
11
- const SCRUBBED = new Set(['METRO_RUN_TOKEN', 'METRO_AGENT', 'DATABASE_URL']);
11
+ const SCRUBBED = new Set(['DATABASE_URL']);
12
12
  const PORT_FLAG = /^--port=(.*)$/;
13
13
  const OWNER_FLAG = /^--owner=(.*)$/;
14
14
  const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
@@ -24,14 +24,14 @@ function portOf(raw) {
24
24
  const ORGANIZATION = /^org_[A-Za-z0-9]{10,64}$/;
25
25
  function ownerOf(raw) {
26
26
  if (raw !== undefined && ORGANIZATION.test(raw))
27
- return raw;
28
- if (raw === undefined || !ADDRESS.test(raw))
29
- throw new Error(`'${raw ?? ''}' is neither an organization id (org_…) nor an Ethereum address — ${USAGE}`);
30
- return raw.toLowerCase();
27
+ return { owner: raw };
28
+ if (raw !== undefined && ADDRESS.test(raw))
29
+ return { owner: null, ignoredOwner: raw };
30
+ throw new Error(`'${raw ?? ''}' is not an organization id (org_…) — ${USAGE}`);
31
31
  }
32
32
  export function parseServeArgs(argv) {
33
33
  let port = localPort();
34
- let owner = null;
34
+ let owner = { owner: null };
35
35
  for (let i = 0; i < argv.length; i += 1) {
36
36
  const arg = argv[i] ?? '';
37
37
  if (TUNNEL_FLAG.test(arg))
@@ -58,10 +58,14 @@ export function parseServeArgs(argv) {
58
58
  }
59
59
  throw new Error(`unknown argument '${arg}' — ${USAGE}`);
60
60
  }
61
- return { port, owner };
61
+ return { port, ...owner };
62
62
  }
63
- export function requireOwner(owner, dir = agentsDir()) {
64
- if (owner !== null || existsSync(join(dir, '.owner')))
63
+ export function requireOwner(args, dir = agentsDir()) {
64
+ if (args.ignoredOwner !== undefined) {
65
+ process.stderr.write(`--owner ${args.ignoredOwner} is a wallet address, which metro ignores: the organization id in ${join(dir, '.owner')} decides who signs in\n`);
66
+ return;
67
+ }
68
+ if (args.owner !== null || existsSync(join(dir, '.owner')))
65
69
  return;
66
70
  throw new Error('no owner is set for this machine, so nobody could sign in.\n' +
67
71
  'Pass the organization that owns it once; it is remembered in ' +
@@ -71,9 +75,7 @@ export function requireOwner(owner, dir = agentsDir()) {
71
75
  export function readOwner(dir = agentsDir()) {
72
76
  try {
73
77
  const raw = readFileSync(join(dir, '.owner'), 'utf8').trim();
74
- if (ORGANIZATION.test(raw))
75
- return raw;
76
- return ADDRESS.test(raw.toLowerCase()) ? raw.toLowerCase() : null;
78
+ return ORGANIZATION.test(raw) ? raw : null;
77
79
  }
78
80
  catch {
79
81
  return null;
@@ -142,12 +144,13 @@ export function servePlan(opts) {
142
144
  };
143
145
  }
144
146
  export function serve(argv) {
145
- const { port, owner } = parseServeArgs(argv);
147
+ const args = parseServeArgs(argv);
148
+ const { port, owner } = args;
146
149
  const running = serveLockedBy();
147
150
  if (running !== null)
148
151
  throw new Error(`a metro serve daemon is already running on this machine (pid ${String(running)}). ` +
149
152
  'Stop it first: metro stop');
150
- requireOwner(owner);
153
+ requireOwner(args);
151
154
  const tailscaleBin = findTailscale();
152
155
  const node = ensureNodeName(tailscaleBin);
153
156
  process.stderr.write(`Starting a metro daemon of your own on http://127.0.0.1:${String(port)}, published as ${node} on your tailnet\n`);
package/dist/service.js CHANGED
@@ -126,13 +126,13 @@ export function servicePlan(host, serveArgs) {
126
126
  throw new Error(`metro service supports Linux (systemd) and macOS (launchd), not ${host.platform}`);
127
127
  }
128
128
  function install(serveArgs, deps) {
129
- const { owner } = parseServeArgs(serveArgs);
129
+ const args = parseServeArgs(serveArgs);
130
130
  const plan = servicePlan(deps.host, serveArgs);
131
131
  if (deps.exists(plan.file)) {
132
132
  deps.out(`metro is already installed as a ${plan.kind} service (${plan.file}) and restarts on its own; metro service status says whether it is running. To change its arguments: metro service uninstall, then install again`);
133
133
  return 0;
134
134
  }
135
- deps.preflight(owner);
135
+ deps.preflight(args);
136
136
  const pid = deps.running();
137
137
  if (pid !== null)
138
138
  throw new Error(`a metro serve is running on this machine (pid ${String(pid)}). Stop it first (metro stop), then install: the service takes over from there`);
@@ -209,8 +209,8 @@ function realDeps() {
209
209
  return { status: code, output: `${result.stdout}${result.stderr}` };
210
210
  },
211
211
  running: serveLockedBy,
212
- preflight: (owner) => {
213
- requireOwner(owner);
212
+ preflight: (args) => {
213
+ requireOwner(args);
214
214
  findBun();
215
215
  findTailscale();
216
216
  },
package/dist/tail.js CHANGED
@@ -1,4 +1,4 @@
1
- import { localAgents } from './local.js';
1
+ import { localAgent } from './local.js';
2
2
  import { assertAgentId, localUrl } from './runtime.js';
3
3
  const BACKOFF_MS = [1_000, 2_000, 5_000];
4
4
  const STALL_MS = 90_000;
@@ -22,9 +22,9 @@ function keySource(agentId) {
22
22
  return { resolve: () => Promise.resolve(fromEnv), fixed: true };
23
23
  return {
24
24
  resolve: () => {
25
- const agent = localAgents().find((a) => a.id === agentId);
26
- if (agent === undefined)
27
- throw new Error(`no agent ${agentId} on this machine — create or restore it in the web UI of metro serve, or set METRO_AGENT_KEY`);
25
+ const agent = localAgent();
26
+ if (agent === null || (agentId !== undefined && agent.id !== agentId))
27
+ throw new Error(`no agent ${agentId === undefined ? '' : `${agentId} `}on this machine — start metro serve, or set METRO_AGENT_KEY`);
28
28
  return Promise.resolve(agent.key);
29
29
  },
30
30
  fixed: false,
@@ -89,7 +89,7 @@ function note(state, message) {
89
89
  process.stderr.write(`metro: ${message}\n`);
90
90
  }
91
91
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
92
- async function settle(end, source, key, agentId, state) {
92
+ async function settle(end, source, key, state) {
93
93
  if (end === 'unauthorized') {
94
94
  if (source.fixed)
95
95
  throw new Error('the key in METRO_AGENT_KEY was refused by the local daemon');
@@ -97,13 +97,13 @@ async function settle(end, source, key, agentId, state) {
97
97
  return source.resolve();
98
98
  }
99
99
  if (end === 'unreachable')
100
- note(state, `no metro daemon on ${localUrl()} — run 'metro serve' (agent ${agentId})`);
100
+ note(state, `no metro daemon on ${localUrl()} — run 'metro serve'`);
101
101
  else
102
102
  note(state, 'stream ended — reconnecting');
103
103
  return key;
104
104
  }
105
105
  export async function tailEvents(argv) {
106
- const agentId = assertAgentId(argv[0]);
106
+ const agentId = argv[0] === undefined ? undefined : assertAgentId(argv[0]);
107
107
  const source = keySource(agentId);
108
108
  let key = await source.resolve();
109
109
  const state = { last: '' };
@@ -112,7 +112,7 @@ export async function tailEvents(argv) {
112
112
  const end = await streamOnce(key);
113
113
  if (end === 'ended')
114
114
  attempt = 0;
115
- key = await settle(end, source, key, agentId, state);
115
+ key = await settle(end, source, key, state);
116
116
  await delay(backoffAt(attempt));
117
117
  attempt += 1;
118
118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.172",
3
+ "version": "0.1.0-beta.174",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metro",
3
- "version": "0.1.0-beta.172",
3
+ "version": "0.1.0-beta.174",
4
4
  "description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
5
5
  "author": {
6
6
  "name": "Bonustrack",
@@ -1,30 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { readFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
 
6
6
  const agentsDir = () => process.env.METRO_AGENTS_DIR || join(homedir(), '.metro', 'agents');
7
7
 
8
- function readAgent(path) {
8
+ function readAgent() {
9
+ const path = join(agentsDir(), 'agent.json');
10
+ let parsed;
9
11
  try {
10
- const parsed = JSON.parse(readFileSync(path, 'utf8'));
11
- return typeof parsed?.key === 'string' ? parsed : null;
12
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
12
13
  } catch {
13
- return null;
14
+ throw new Error(`no agent on this machine yet (${path})`);
14
15
  }
15
- }
16
-
17
- function pickAgent() {
18
- const dir = agentsDir();
19
- const fixed = readAgent(join(dir, 'agent.json'));
20
- if (fixed !== null) return fixed;
21
- if (!existsSync(dir)) throw new Error(`no agent on this machine yet (${dir})`);
22
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
23
- if (!entry.isDirectory()) continue;
24
- const found = readAgent(join(dir, entry.name, 'agent.json'));
25
- if (found !== null) return found;
26
- }
27
- throw new Error(`no agent on this machine yet (${dir})`);
16
+ if (typeof parsed?.key !== 'string') throw new Error(`no agent key in ${path}`);
17
+ return parsed;
28
18
  }
29
19
 
30
20
  if (process.argv[2] !== 'headers') {
@@ -33,7 +23,7 @@ if (process.argv[2] !== 'headers') {
33
23
  }
34
24
 
35
25
  try {
36
- process.stdout.write(`${JSON.stringify({ Authorization: `Bearer ${pickAgent().key}` })}\n`);
26
+ process.stdout.write(`${JSON.stringify({ Authorization: `Bearer ${readAgent().key}` })}\n`);
37
27
  } catch (err) {
38
28
  process.stderr.write(`metro: ${err.message}\n`);
39
29
  process.stdout.write('{}\n');
@@ -7,14 +7,13 @@
7
7
  "exports": {
8
8
  "./log": "./src/log.ts",
9
9
  "./events": "./src/events.ts",
10
- "./identity": "./src/identity.ts",
11
10
  "./is-record": "./src/is-record.ts",
12
11
  "./secure-fs": "./src/secure-fs.ts",
13
12
  "./version": "./src/version.ts",
14
13
  "./train-error": "./src/train-error.ts",
15
14
  "./trains/protocol": "./src/protocol.ts",
16
15
  "./ids": "./src/ids.ts",
17
- "./address": "./src/address.ts",
16
+ "./tickets": "./src/tickets.ts",
18
17
  "./station-names": "./src/station-names.ts",
19
18
  "./str": "./src/str.ts",
20
19
  "./lines": "./src/lines.ts",
@@ -25,7 +24,9 @@
25
24
  "./stations/attachments": "./src/stations/attachments.ts",
26
25
  "./stations/messaging-normalize": "./src/stations/messaging-normalize.ts",
27
26
  "./stations/profile": "./src/stations/profile.ts",
28
- "./stations/sender-profile": "./src/stations/sender-profile.ts"
27
+ "./stations/sender-profile": "./src/stations/sender-profile.ts",
28
+ "./stations/train-events": "./src/stations/train-events.ts",
29
+ "./stations/train-boot": "./src/stations/train-boot.ts"
29
30
  },
30
31
  "scripts": {
31
32
  "build": "tsc",
@@ -1,4 +1,3 @@
1
- import { randomBytes } from 'node:crypto';
2
1
  import { Line } from './lines.js';
3
2
 
4
3
  export { userSelf, daemonSelf } from './identity.js';
@@ -74,8 +73,7 @@ export function formatDisplay(e: MetroEvent): string {
74
73
  return `${headerFor('📩', [e.station, e.fromName ?? e.from, e.lineName])}\n> ${body}`;
75
74
  }
76
75
 
77
- export const mintId = (): string =>
78
- `msg_${randomBytes(6).toString('base64url')}`;
76
+ export { mintId } from './ids.js';
79
77
 
80
78
  export type BusListener = (event: MetroEvent, busSeq: number) => void;
81
79
 
@@ -5,6 +5,9 @@ const REROLLS = 16;
5
5
 
6
6
  export const ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{10}$/;
7
7
 
8
+ export const mintId = (): string =>
9
+ `msg_${randomBytes(6).toString('base64url')}`;
10
+
8
11
  export function newId(): string {
9
12
  for (let attempt = 0; attempt < REROLLS; attempt += 1) {
10
13
  const id = randomBytes(ID_BYTES).toString('base64url');
@@ -38,6 +38,23 @@ export function writeSecure(path: string, data: string): void {
38
38
  chmodIfExists(path, 0o600);
39
39
  }
40
40
 
41
+ function modeOf(path: string): number | null {
42
+ try {
43
+ return statSync(path).mode & 0o777;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ export function writeAtomic(path: string, data: string, mode?: number): void {
50
+ const chosen = mode ?? modeOf(path) ?? 0o644;
51
+ mkdirSync(dirname(path), { recursive: true });
52
+ const tmp = `${path}.metro-${String(process.pid)}.tmp`;
53
+ writeFileSync(tmp, data, { mode: chosen });
54
+ chmodSync(tmp, chosen);
55
+ renameSync(tmp, path);
56
+ }
57
+
41
58
  export function readJson<T>(
42
59
  path: string,
43
60
  fallback: T,
@@ -10,4 +10,3 @@ export const STATIONS = [
10
10
 
11
11
  export type StationName = (typeof STATIONS)[number];
12
12
 
13
- export type ConnectorTransport = 'http' | 'sse';
@@ -12,8 +12,7 @@ export const respond = (
12
12
  JSON.stringify({ op: 'response', id, ...body }) + '\n',
13
13
  );
14
14
 
15
- export const mintId = (): string =>
16
- `msg_${Math.random().toString(36).slice(2, 10)}`;
15
+ export { mintId } from '../ids.js';
17
16
 
18
17
  type Args = Record<string, unknown>;
19
18
 
@@ -29,13 +28,12 @@ export type StationHandler = (id: string, args: Args) => void | Promise<void>;
29
28
  export interface StationConfig {
30
29
  handlers: Record<string, StationHandler>;
31
30
  normalize: (action: string, args: Args) => Normalized;
32
- preDispatch?: (id: string, action: string) => boolean;
33
31
  }
34
32
 
35
33
  const lineTag = (args: Args): string =>
36
34
  typeof args.line === 'string' ? args.line : '?';
37
35
 
38
- export function makeStation({ handlers, normalize, preDispatch }: StationConfig) {
36
+ export function makeStation({ handlers, normalize }: StationConfig) {
39
37
  const known = Object.keys(handlers).join(', ');
40
38
  return async function handleCall(msg: CallMsg): Promise<void> {
41
39
  const { id } = msg;
@@ -44,7 +42,6 @@ export function makeStation({ handlers, normalize, preDispatch }: StationConfig)
44
42
  try {
45
43
  ({ action, args } = normalize(msg.action, msg.args));
46
44
  emit({ op: 'log', text: `call ${action} recv (line=${lineTag(args)})` });
47
- if (preDispatch?.(id, action)) return;
48
45
  const handler = handlers[action];
49
46
  if (!handler) {
50
47
  respond(id, { error: `unknown action '${action}' (have: ${known})` });
@@ -0,0 +1,42 @@
1
+ import { errMsg } from '../log.js';
2
+ import { readCalls } from '../protocol.js';
3
+ import { TrainError } from '../train-error.js';
4
+ import type { CallMsg } from './station-runtime.js';
5
+
6
+ export function announceAccounts(station: string, ids: Iterable<string>): void {
7
+ const list = [...ids];
8
+ if (list.length === 0) {
9
+ process.stderr.write(`${station}: no accounts booted, exiting\n`);
10
+ process.exit(2);
11
+ }
12
+ process.stderr.write(`${station} train ready — ${String(list.length)} account(s): ${list.join(', ')}\n`);
13
+ }
14
+
15
+ export interface ClientTrain<A extends { id: string }, C> {
16
+ station: string;
17
+ accounts: Map<string, A>;
18
+ loadAccounts: () => A[];
19
+ createClient: (account: A) => C;
20
+ startInbound: (client: C) => Promise<void>;
21
+ makeHandleCall: (clientFor: (accountId: string) => C) => (msg: CallMsg) => Promise<void>;
22
+ }
23
+
24
+ export function runClientTrain<A extends { id: string }, C>(train: ClientTrain<A, C>): void {
25
+ const clients = new Map<string, C>();
26
+ const clientFor = (accountId: string): C => {
27
+ const known = clients.get(accountId);
28
+ if (known !== undefined) return known;
29
+ const account = train.accounts.get(accountId);
30
+ if (!account) throw new TrainError('not_implemented', `unknown account '${accountId}'`);
31
+ const client = train.createClient(account);
32
+ clients.set(accountId, client);
33
+ return client;
34
+ };
35
+ readCalls(train.station, train.makeHandleCall(clientFor));
36
+ for (const cfg of train.loadAccounts()) train.accounts.set(cfg.id, cfg);
37
+ for (const id of train.accounts.keys())
38
+ train.startInbound(clientFor(id)).catch((err: unknown) => {
39
+ process.stderr.write(`${train.station}[${id}] inbound failed: ${errMsg(err)}\n`);
40
+ });
41
+ announceAccounts(train.station, train.accounts.keys());
42
+ }
@@ -0,0 +1,89 @@
1
+ import { mintId } from '../ids.js';
2
+ import { emit } from './station-runtime.js';
3
+
4
+ type Fields = Record<string, unknown>;
5
+ type Out = (e: unknown) => void;
6
+
7
+ export const selfUri = (station: string, account?: string): string =>
8
+ process.env.METRO_SELF_URI ??
9
+ (account === undefined ? `metro://${station}/self` : `metro://${station}/${account}/self`);
10
+
11
+ export function emitInbound(account: string, e: Fields, out: Out = emit): void {
12
+ out({
13
+ kind: 'inbound',
14
+ ...e,
15
+ account,
16
+ payload: { ...(e.payload as Fields | undefined), account },
17
+ });
18
+ }
19
+
20
+ export interface AttachmentAt {
21
+ station: string;
22
+ account: string;
23
+ line: string;
24
+ forId: string;
25
+ index: number;
26
+ from?: string;
27
+ }
28
+
29
+ export interface SavedFile {
30
+ path: string;
31
+ mime?: string;
32
+ name?: string;
33
+ }
34
+
35
+ function attachmentEvent(at: AttachmentAt, text: string, payload: Fields): Fields {
36
+ return {
37
+ kind: 'inbound',
38
+ id: mintId(),
39
+ ts: new Date().toISOString(),
40
+ station: at.station,
41
+ line: at.line,
42
+ from: at.from ?? selfUri(at.station, at.account),
43
+ text,
44
+ account: at.account,
45
+ payload: { account: at.account, attachmentFor: at.forId, index: at.index, ...payload },
46
+ };
47
+ }
48
+
49
+ export const attachmentSavedEvent = (at: AttachmentAt & { saved: SavedFile; extra?: Fields }): Fields =>
50
+ attachmentEvent(at, `📎 saved: ${at.saved.path}`, {
51
+ contentType: 'attachmentSaved',
52
+ ...at.extra,
53
+ attachmentPath: at.saved.path,
54
+ localPath: at.saved.path,
55
+ mime: at.saved.mime,
56
+ name: at.saved.name,
57
+ });
58
+
59
+ export const attachmentFailedEvent = (at: AttachmentAt & { reason: string; extra?: Fields }): Fields =>
60
+ attachmentEvent(at, `📎 not fetched: ${at.reason}`, {
61
+ contentType: 'attachmentFailed',
62
+ ...at.extra,
63
+ reason: at.reason,
64
+ });
65
+
66
+ const reasonOf = (err: unknown): string => (err instanceof Error ? err.message : String(err));
67
+
68
+ export function reportAttachment(
69
+ save: Promise<SavedFile>,
70
+ at: AttachmentAt,
71
+ extra: { saved?: Fields; failed?: Fields } = {},
72
+ out: Out = emit,
73
+ ): void {
74
+ const tag = `${at.station}[${at.account}]`;
75
+ save
76
+ .then(
77
+ (saved) => {
78
+ out(attachmentSavedEvent({ ...at, saved, extra: extra.saved }));
79
+ },
80
+ (err: unknown) => {
81
+ const reason = reasonOf(err);
82
+ process.stderr.write(`${tag} attachment save failed: ${reason}\n`);
83
+ out(attachmentFailedEvent({ ...at, reason, extra: extra.failed }));
84
+ },
85
+ )
86
+ .catch((err: unknown) => {
87
+ process.stderr.write(`${tag} attachment event not emitted: ${reasonOf(err)}\n`);
88
+ });
89
+ }