@stage-labs/metro 0.1.0-beta.270 → 0.1.0-beta.271

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.270",
3
+ "version": "0.1.0-beta.271",
4
4
  "description": "The metro command line: run your agent on this machine, relaying its chat channels and connectors to Claude Code.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metro",
3
- "version": "0.1.0-beta.270",
3
+ "version": "0.1.0-beta.271",
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",
@@ -26,6 +26,7 @@ import { setupAnswer, type SetupApiDeps } from './setup-api.js';
26
26
  import {
27
27
  autostartEnabled,
28
28
  ensureSession,
29
+ sessionSnapshot,
29
30
  sessionStatus,
30
31
  setAutostart,
31
32
  startSession,
@@ -270,6 +271,11 @@ export function handleClaudeRequest(
270
271
  apiSession(req)
271
272
  .then(async (session) => {
272
273
  if (!session) throw new ApiError('unauthorized', 401);
274
+ if (path === `${PREFIX}/session/snapshot`) {
275
+ if (req.method !== 'GET') throw new ApiError('method not allowed', 405);
276
+ sendJson(req, res, 200, sessionSnapshot(deps.session));
277
+ return;
278
+ }
273
279
  if (req.method !== 'GET' && ADMIN_ONLY.test(path)) requireAdmin(session);
274
280
  if (await streamed(req, res, path, search, (deps.dir ?? claudeDir)())) return;
275
281
  sendJson(req, res, 200, await routed(req, path, search, deps, session));
@@ -61,6 +61,13 @@ export interface SessionStatus {
61
61
  lastError: string | null;
62
62
  }
63
63
 
64
+ export interface SessionSnapshot {
65
+ running: boolean;
66
+ runner: HarnessRunner | null;
67
+ activity: RunnerActivity | null;
68
+ lastStartedAt: string | null;
69
+ }
70
+
64
71
  interface Memory {
65
72
  lastStartedAt: number | null;
66
73
  lastError: string | null;
@@ -267,17 +274,27 @@ export function stopSession(deps: SessionDeps = {}): SessionStatus {
267
274
  export const sessionLive = (deps: SessionDeps = {}): boolean =>
268
275
  sessionRunning(deps.tmux ?? 'tmux') || sdkAlive(readAgentActivity(sessionHome(deps)));
269
276
 
270
- export function sessionStatus(deps: SessionDeps = {}): SessionStatus {
277
+ export function sessionSnapshot(deps: SessionDeps = {}): SessionSnapshot {
271
278
  const activity = readAgentActivity(sessionHome(deps));
272
279
  const sdk = sdkAlive(activity);
280
+ const tmux = sessionRunning(deps.tmux ?? 'tmux');
281
+ const runner = sdk ? 'sdk' : tmux ? readState(deps.agents ?? agentsDir()).runner : null;
273
282
  return {
274
- name: SESSION_NAME,
275
- running: sessionRunning(deps.tmux ?? 'tmux') || sdk,
276
- runner: sdk ? 'sdk' : sessionRunner(deps),
283
+ running: tmux || sdk,
284
+ runner: isHarnessRunner(runner) ? runner : null,
277
285
  activity,
286
+ lastStartedAt: memory.lastStartedAt === null ? null : new Date(memory.lastStartedAt).toISOString(),
287
+ };
288
+ }
289
+
290
+ export function sessionStatus(deps: SessionDeps = {}): SessionStatus {
291
+ const snapshot = sessionSnapshot(deps);
292
+ return {
293
+ name: SESSION_NAME,
294
+ ...snapshot,
295
+ runner: snapshot.runner ?? sessionRunner(deps),
278
296
  autostart: autostartEnabled(deps.agents ?? agentsDir()),
279
297
  blocked: sessionBlocked(deps),
280
- lastStartedAt: memory.lastStartedAt === null ? null : new Date(memory.lastStartedAt).toISOString(),
281
298
  lastError: memory.lastError,
282
299
  };
283
300
  }
@@ -14,6 +14,7 @@ import {
14
14
  connectionOf,
15
15
  parseModelConfig,
16
16
  readModelConfig,
17
+ readModelSnapshot,
17
18
  removeConnection,
18
19
  setFallbacks,
19
20
  setRoute,
@@ -28,6 +29,7 @@ import { refreshUsage } from './usage-refresh.js';
28
29
  import { handleOpenRouterSignIn } from './openrouter-signin.js';
29
30
 
30
31
  const PATH = '/api/model';
32
+ const SNAPSHOT = '/api/model/snapshot';
31
33
  const CONNECTIONS = '/api/model/connections';
32
34
  const FALLBACKS = '/api/model/fallbacks';
33
35
  const CODEX = '/api/model/codex/';
@@ -201,12 +203,13 @@ const TABLES: [string, Record<string, Route>][] = [
201
203
  [BEDROCK, BEDROCK_ROUTES],
202
204
  [GEMINI, GEMINI_ROUTES],
203
205
  ];
204
- const EXACT = new Set([PATH, FALLBACKS, BUNDLE, RESTORE, CONNECTIONS]);
206
+ const EXACT = new Set([PATH, SNAPSHOT, FALLBACKS, BUNDLE, RESTORE, CONNECTIONS]);
205
207
  const PREFIXES = [`${CONNECTIONS}/`, CODEX, ...TABLES.map(([prefix]) => prefix)];
206
208
 
207
209
  const mine = (path: string): boolean => EXACT.has(path) || PREFIXES.some((prefix) => path.startsWith(prefix));
208
210
 
209
211
  function routeFor(path: string, method: string | undefined): Route | number {
212
+ if (path === SNAPSHOT) return method === 'GET' ? { method: 'GET', run: (_req, _deps, store) => Promise.resolve().then(() => settingsBody(store.read())).catch(asApiError) } : 405;
210
213
  if (path === PATH || path === FALLBACKS) return settingsRoute(path, method);
211
214
  if (path === BUNDLE || path === RESTORE) return bundleRoute(path, method);
212
215
  if (path === CONNECTIONS || path.startsWith(`${CONNECTIONS}/`)) return connectionsRoute(path, method);
@@ -232,7 +235,7 @@ export function handleModelRequest(req: IncomingMessage, res: ServerResponse, de
232
235
  return true;
233
236
  }
234
237
  const store: Store = {
235
- read: deps.read ?? readModelConfig,
238
+ read: deps.read ?? (path === SNAPSHOT ? readModelSnapshot : readModelConfig),
236
239
  write: (cfg) => {
237
240
  checkRunnerRoute(store.read(), cfg, deps);
238
241
  (deps.write ?? writeModelConfig)(cfg);
@@ -158,13 +158,22 @@ export function parseModelConfig(raw: unknown): ModelConfig {
158
158
  const needsIds = (raw: unknown): boolean =>
159
159
  isRecord(raw) && Array.isArray(raw.connections) && raw.connections.some((c: unknown) => isRecord(c) && text(c.id) === '');
160
160
 
161
+ const readStoredModel = (dir: string): unknown =>
162
+ readJson<unknown>(join(dir, MODEL_FILE), null, { warn: 'model-config: model.json is unreadable, so every request goes to Anthropic until it is fixed' });
163
+
161
164
  export function readModelConfig(dir = agentsDir()): ModelConfig {
162
- const raw = readJson<unknown>(join(dir, MODEL_FILE), null, { warn: 'model-config: model.json is unreadable, so every request goes to Anthropic until it is fixed' });
165
+ const raw = readStoredModel(dir);
163
166
  const cfg = parseModelConfig(raw);
164
167
  if (needsIds(raw)) writeModelConfig(cfg, dir);
165
168
  return cfg;
166
169
  }
167
170
 
171
+ export function readModelSnapshot(dir = agentsDir()): ModelConfig {
172
+ const raw = readStoredModel(dir);
173
+ if (needsIds(raw)) throw new ModelConfigError('model snapshot is unavailable until every connection has a saved id');
174
+ return parseModelConfig(raw);
175
+ }
176
+
168
177
  export function writeModelConfig(cfg: ModelConfig, dir = agentsDir()): void {
169
178
  writeSecure(join(dir, MODEL_FILE), JSON.stringify(cfg, null, 2));
170
179
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.270"
2
+ "version": "0.1.0-beta.271"
3
3
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-beta.270",
2
+ "version": "0.1.0-beta.271",
3
3
  "dependencies": {
4
4
  "@anthropic-ai/claude-agent-sdk": "0.3.287",
5
5
  "@modelcontextprotocol/sdk": "^1.29.0",