@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 +1 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +35 -11
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +5 -2
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +43 -13
- package/runtime/runtime.json +1 -1
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()}/#/
|
|
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.
|
|
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
|
-
|
|
179
|
-
.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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}'
|
|
16
|
-
'
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 };
|
package/runtime/runtime.json
CHANGED