@stage-labs/metro 0.1.0-beta.20 → 0.1.0-beta.21

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 CHANGED
@@ -39,7 +39,7 @@ function hostLabel() {
39
39
  return name === '' ? 'unnamed machine' : name;
40
40
  }
41
41
  async function authorizeRuntime(agentId) {
42
- process.stderr.write(`Authorize this machine at ${metroWebUrl()}/#/agent/${agentId}\n`);
42
+ process.stderr.write(`Authorize this machine at ${metroWebUrl()}/#/authorize/${agentId}\n`);
43
43
  const code = (await askLine('Paste the code: ')).trim();
44
44
  if (code === '')
45
45
  throw new Error('no code given');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.20",
3
+ "version": "0.1.0-beta.21",
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": {
@@ -108,6 +108,24 @@ export function unmovableStations(list: LoadedAccount[]): StationName[] {
108
108
  ];
109
109
  }
110
110
 
111
+ export interface StationRecord {
112
+ id: string;
113
+ agentId: string;
114
+ station: StationName;
115
+ }
116
+
117
+ export async function listAllStations(): Promise<StationRecord[]> {
118
+ if (!databaseUrl()) return [];
119
+ const rows = await getDb()
120
+ .select({
121
+ id: stations.id,
122
+ agentId: stations.agentId,
123
+ station: stations.station,
124
+ })
125
+ .from(stations);
126
+ return rows;
127
+ }
128
+
111
129
  export async function loadAllStationsFor(
112
130
  agentId: string,
113
131
  ): Promise<LoadedAccount[]> {
@@ -174,14 +192,12 @@ async function loadAgents(): Promise<LoadedAgent[]> {
174
192
  out.push({
175
193
  id: a.id,
176
194
  name: a.name,
177
- accounts: acctRows
178
- .filter((r) => !MOVABLE_STATIONS.has(r.station))
179
- .map((r) => ({
180
- station: r.station,
181
- id: r.id,
182
- allowlist: r.allowlist,
183
- config: r.config as Record<string, unknown>,
184
- })),
195
+ accounts: acctRows.map((r) => ({
196
+ station: r.station,
197
+ id: r.id,
198
+ allowlist: r.allowlist,
199
+ config: r.config as Record<string, unknown>,
200
+ })),
185
201
  key: a.key,
186
202
  });
187
203
  }
@@ -214,6 +230,12 @@ export function writeIfChanged(path: string, content: string): boolean {
214
230
  return true;
215
231
  }
216
232
 
233
+ export const isLocalRuntime = (): boolean =>
234
+ (process.env.METRO_RUN_TOKEN?.trim() ?? '') !== '';
235
+
236
+ export const stationRunsHere = (station: StationName): boolean =>
237
+ isLocalRuntime() || !MOVABLE_STATIONS.has(station);
238
+
217
239
  function writeStations(list: LoadedAgent[]): Map<StationName, number> {
218
240
  mkdirSync(METRO_DIR, { recursive: true });
219
241
  mkdirSync(TRAINS_DIR, { recursive: true });
@@ -225,11 +247,13 @@ function writeStations(list: LoadedAgent[]): Map<StationName, number> {
225
247
  for (const agent of list) {
226
248
  names[agent.id] = agent.name;
227
249
  for (const a of agent.accounts) {
228
- const cur = byStation.get(a.station);
229
- if (cur) cur.push(a);
230
- else byStation.set(a.station, [a]);
231
250
  map[`${a.station}/${a.id}`] = agent.id;
232
251
  if (a.allowlist) allow[`${a.station}/${a.id}`] = a.allowlist;
252
+ if (stationRunsHere(a.station)) {
253
+ const cur = byStation.get(a.station);
254
+ if (cur) cur.push(a);
255
+ else byStation.set(a.station, [a]);
256
+ }
233
257
  }
234
258
  }
235
259
  setAgentMap(map, names);
@@ -12,8 +12,11 @@ export async function mintRuntimeCodeForEmail(
12
12
  const blocked = unmovableStations(await loadAllStationsFor(agent.id));
13
13
  if (blocked.length > 0)
14
14
  throw new ApiError(
15
- `'${agent.name}' cannot run locally while it holds ${blocked.join(', ')} — ` +
16
- 'those stations only run on metro. Move them to another agent first.',
15
+ `'${agent.name}' holds ${blocked.join(', ')}, which only runs on metro ` +
16
+ 'because a webhook url has to be publicly reachable. Its deliveries ' +
17
+ 'would arrive at metro while this agent listens on your machine, so ' +
18
+ 'they would be dropped. Detach it, or attach it to a second agent that ' +
19
+ 'stays on metro, then authorize this one.',
17
20
  409,
18
21
  );
19
22
  return mintRunCode({ email, agentId: agent.id });
@@ -6,6 +6,12 @@ import {
6
6
  import { listEndpoints } from '../daemon/tunnel.js';
7
7
  import { hookUrl } from '../stations/attach.js';
8
8
  import { agentIdForAccount } from '../db/agent-map.js';
9
+ import {
10
+ listAllStations,
11
+ stationRunsHere,
12
+ type StationRecord,
13
+ } from '../db/materialize.js';
14
+ import { type StationName } from '../db/schema.js';
9
15
 
10
16
  const accountId = (acc: unknown): string | undefined => {
11
17
  const id = (acc as { id?: unknown }).id;
@@ -74,24 +80,48 @@ export interface ScopedAccounts {
74
80
  unavailable: string[];
75
81
  }
76
82
 
83
+ async function liveAccounts(
84
+ station: string,
85
+ ): Promise<{ rows: unknown[]; reachable: boolean }> {
86
+ if (!hasTrain(station))
87
+ return { rows: inCoreAccounts(station), reachable: true };
88
+ if (!stationRunsHere(station as StationName))
89
+ return { rows: [], reachable: true };
90
+ try {
91
+ const resp = await forwardTrainCall(station, 'accounts', {});
92
+ const list = (resp.result as { accounts?: unknown[] } | undefined)?.accounts;
93
+ return { rows: Array.isArray(list) ? list : [], reachable: true };
94
+ } catch {
95
+ return { rows: [], reachable: false };
96
+ }
97
+ }
98
+
99
+ function mergeKnown(
100
+ station: string,
101
+ live: unknown[],
102
+ known: StationRecord[],
103
+ ): unknown[] {
104
+ const seen = new Set(
105
+ live.flatMap((row) => {
106
+ const id = accountId(row);
107
+ return id === undefined ? [] : [id];
108
+ }),
109
+ );
110
+ const missing = known
111
+ .filter((r) => r.station === station && !seen.has(r.id))
112
+ .map((r) => ({ id: r.id }));
113
+ return [...live, ...missing];
114
+ }
115
+
77
116
  async function loadStations(): Promise<ScopedAccounts> {
78
117
  const unavailable: string[] = [];
79
118
  const accounts: Record<string, unknown[]> = {};
119
+ const known = await listAllStations();
80
120
  await Promise.all(
81
121
  accountStationNames().map(async (station) => {
82
- if (!hasTrain(station)) {
83
- accounts[station] = inCoreAccounts(station);
84
- return;
85
- }
86
- try {
87
- const resp = await forwardTrainCall(station, 'accounts', {});
88
- const list = (resp.result as { accounts?: unknown[] } | undefined)
89
- ?.accounts;
90
- accounts[station] = Array.isArray(list) ? list : [];
91
- } catch {
92
- accounts[station] = [];
93
- unavailable.push(station);
94
- }
122
+ const { rows, reachable } = await liveAccounts(station);
123
+ if (!reachable) unavailable.push(station);
124
+ accounts[station] = mergeKnown(station, rows, known);
95
125
  }),
96
126
  );
97
127
  return { accounts, unavailable };
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.20"
2
+ "version": "0.1.0-beta.21"
3
3
  }