@stage-labs/metro 0.1.0-beta.40 → 0.1.0-beta.42

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.40",
3
+ "version": "0.1.0-beta.42",
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": {
@@ -0,0 +1,95 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { errMsg, log } from './log.js';
3
+ import { apiFailure, apiSession, cors, sendJson } from './api-http.js';
4
+ import { ApiError } from './api-error.js';
5
+ import {
6
+ claudeDir,
7
+ listClaudeProjects,
8
+ listClaudeSessions,
9
+ listMemory,
10
+ readMemoryFile,
11
+ readTranscript,
12
+ } from './claude-files.js';
13
+
14
+ const PREFIX = '/api/claude';
15
+ const PAGE = 100;
16
+ const PAGE_MAX = 500;
17
+
18
+ export interface ClaudeApiDeps {
19
+ authorize: (subject: string) => void;
20
+ dir?: () => string;
21
+ }
22
+
23
+ function projectOf(query: URLSearchParams): string {
24
+ const project = query.get('project');
25
+ if (project === null || project === '') throw new ApiError('project is required', 400);
26
+ return project;
27
+ }
28
+
29
+ function pageOf(query: URLSearchParams): { offset: number; limit: number } {
30
+ const offset = Math.max(0, Math.floor(Number(query.get('offset') ?? '0')) || 0);
31
+ const limit = Math.min(PAGE_MAX, Math.max(1, Math.floor(Number(query.get('limit') ?? String(PAGE))) || PAGE));
32
+ return { offset, limit };
33
+ }
34
+
35
+ type Handler = (query: URLSearchParams, dir: string, item: string) => unknown;
36
+
37
+ const COLLECTIONS: Record<string, Handler> = {
38
+ projects: (_query, dir) => ({ projects: listClaudeProjects(dir) }),
39
+ sessions: (query, dir) => ({ sessions: listClaudeSessions(projectOf(query), dir) }),
40
+ memory: (query, dir) => listMemory(projectOf(query), dir),
41
+ };
42
+
43
+ const ITEMS: Record<string, Handler> = {
44
+ sessions: (query, dir, id) => {
45
+ const { offset, limit } = pageOf(query);
46
+ return readTranscript(projectOf(query), id, offset, limit, dir);
47
+ },
48
+ memory: (query, dir, name) => ({ name, content: readMemoryFile(projectOf(query), name, dir) }),
49
+ };
50
+
51
+ function answer(path: string, query: URLSearchParams, dir: string): unknown {
52
+ const rest = path.slice(PREFIX.length + 1).split('/').filter(Boolean);
53
+ const [head = '', item] = rest;
54
+ const handler = rest.length === 1 ? COLLECTIONS[head] : rest.length === 2 ? ITEMS[head] : undefined;
55
+ if (handler === undefined) throw new ApiError('no such route', 404);
56
+ return handler(query, dir, item ?? '');
57
+ }
58
+
59
+ export function handleClaudeRequest(
60
+ req: IncomingMessage,
61
+ res: ServerResponse,
62
+ deps: ClaudeApiDeps,
63
+ ): boolean {
64
+ const [path = '', search = ''] = (req.url ?? '').split('?');
65
+ if (path !== PREFIX && !path.startsWith(`${PREFIX}/`)) return false;
66
+ if (req.method === 'OPTIONS') {
67
+ res.writeHead(204, cors(req)).end();
68
+ return true;
69
+ }
70
+ if (req.method !== 'GET') {
71
+ sendJson(req, res, 405, { error: 'method not allowed' });
72
+ return true;
73
+ }
74
+ const session = apiSession(req);
75
+ if (!session) {
76
+ sendJson(req, res, 401, { error: 'unauthorized' });
77
+ return true;
78
+ }
79
+ Promise.resolve()
80
+ .then(() => {
81
+ deps.authorize(session.subject);
82
+ return answer(path, new URLSearchParams(search), (deps.dir ?? claudeDir)());
83
+ })
84
+ .then((body) => {
85
+ sendJson(req, res, 200, body);
86
+ })
87
+ .catch((err: unknown) => {
88
+ if (err instanceof ApiError) apiFailure(req, res, err, 'claude-api');
89
+ else {
90
+ log.warn({ err: errMsg(err) }, 'claude-api: request failed');
91
+ if (!res.headersSent) sendJson(req, res, 500, { error: 'claude-api failed' });
92
+ }
93
+ });
94
+ return true;
95
+ }
@@ -0,0 +1,321 @@
1
+ import {
2
+ closeSync,
3
+ createReadStream,
4
+ existsSync,
5
+ openSync,
6
+ readdirSync,
7
+ readSync,
8
+ statSync,
9
+ } from 'node:fs';
10
+ import { homedir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { createInterface } from 'node:readline';
13
+ import { ApiError } from './api-error.js';
14
+
15
+ const PROJECT_RE = /^[A-Za-z0-9._-]{1,200}$/;
16
+ const SESSION_RE = /^[A-Za-z0-9][A-Za-z0-9-]{7,63}$/;
17
+ const MEMORY_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,119}\.md$/;
18
+ const EDGE_BYTES = 64 * 1024;
19
+ const TEXT_CAP = 20_000;
20
+ const RESULT_CAP = 8_000;
21
+ const INPUT_CAP = 4_000;
22
+ const FILE_CAP = 1_000_000;
23
+ const NOISE = ['<task-notification>', '<system-reminder>', '<local-command', '<command-name>'];
24
+
25
+ export function claudeDir(): string {
26
+ const explicit = process.env.METRO_CLAUDE_DIR?.trim();
27
+ if (explicit !== undefined && explicit !== '') return explicit;
28
+ const cfg = process.env.CLAUDE_CONFIG_DIR?.trim();
29
+ if (cfg !== undefined && cfg !== '') return cfg;
30
+ return join(homedir(), '.claude');
31
+ }
32
+
33
+ const isRecord = (v: unknown): v is Record<string, unknown> =>
34
+ typeof v === 'object' && v !== null && !Array.isArray(v);
35
+
36
+ const str = (v: unknown): string | null => (typeof v === 'string' && v !== '' ? v : null);
37
+
38
+ function safeName(value: string, re: RegExp, what: string): string {
39
+ if (!re.test(value) || value === '.' || value === '..')
40
+ throw new ApiError(`that is not a ${what}`, 400);
41
+ return value;
42
+ }
43
+
44
+ function projectDir(project: string, dir: string): string {
45
+ const path = join(dir, 'projects', safeName(project, PROJECT_RE, 'Claude project id'));
46
+ if (!existsSync(path)) throw new ApiError('no such Claude project', 404);
47
+ return path;
48
+ }
49
+
50
+ function parseLines(text: string): Record<string, unknown>[] {
51
+ const out: Record<string, unknown>[] = [];
52
+ for (const line of text.split('\n')) {
53
+ if (line.trim() === '') continue;
54
+ try {
55
+ const parsed: unknown = JSON.parse(line);
56
+ if (isRecord(parsed)) out.push(parsed);
57
+ } catch {
58
+ continue;
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+
64
+ function readEdge(path: string, fromEnd: boolean): Record<string, unknown>[] {
65
+ const size = statSync(path).size;
66
+ const length = Math.min(EDGE_BYTES, size);
67
+ const fd = openSync(path, 'r');
68
+ try {
69
+ const buffer = Buffer.alloc(length);
70
+ readSync(fd, buffer, 0, length, fromEnd ? size - length : 0);
71
+ const text = buffer.toString('utf8');
72
+ const cut = fromEnd && length < size ? text.indexOf('\n') + 1 : 0;
73
+ return parseLines(text.slice(cut));
74
+ } finally {
75
+ closeSync(fd);
76
+ }
77
+ }
78
+
79
+ export interface ClaudeProject {
80
+ id: string;
81
+ cwd: string | null;
82
+ sessions: number;
83
+ lastActiveAt: string | null;
84
+ hasMemory: boolean;
85
+ }
86
+
87
+ function sessionFiles(path: string): { name: string; mtime: number; size: number }[] {
88
+ return readdirSync(path)
89
+ .filter((name) => name.endsWith('.jsonl'))
90
+ .map((name) => {
91
+ const s = statSync(join(path, name));
92
+ return { name, mtime: s.mtimeMs, size: s.size };
93
+ })
94
+ .sort((a, b) => b.mtime - a.mtime);
95
+ }
96
+
97
+ function cwdOf(path: string): string | null {
98
+ for (const line of readEdge(path, false)) {
99
+ const cwd = str(line.cwd);
100
+ if (cwd !== null) return cwd;
101
+ }
102
+ return null;
103
+ }
104
+
105
+ export function listClaudeProjects(dir = claudeDir()): ClaudeProject[] {
106
+ const root = join(dir, 'projects');
107
+ if (!existsSync(root)) return [];
108
+ return readdirSync(root, { withFileTypes: true })
109
+ .filter((entry) => entry.isDirectory() && PROJECT_RE.test(entry.name))
110
+ .map((entry) => {
111
+ const path = join(root, entry.name);
112
+ const files = sessionFiles(path);
113
+ const newest = files[0];
114
+ return {
115
+ id: entry.name,
116
+ cwd: newest === undefined ? null : cwdOf(join(path, newest.name)),
117
+ sessions: files.length,
118
+ lastActiveAt: newest === undefined ? null : new Date(newest.mtime).toISOString(),
119
+ hasMemory: existsSync(join(path, 'memory')),
120
+ };
121
+ })
122
+ .sort((a, b) => (b.lastActiveAt ?? '').localeCompare(a.lastActiveAt ?? ''));
123
+ }
124
+
125
+ export interface ClaudeSession {
126
+ id: string;
127
+ title: string;
128
+ startedAt: string | null;
129
+ lastAt: string;
130
+ bytes: number;
131
+ gitBranch: string | null;
132
+ version: string | null;
133
+ }
134
+
135
+ function firstPrompt(lines: Record<string, unknown>[]): string | null {
136
+ for (const line of lines) {
137
+ if (line.type !== 'user' || line.isSidechain === true) continue;
138
+ const message = isRecord(line.message) ? line.message : {};
139
+ const text = promptText(message.content);
140
+ if (text !== null && !NOISE.some((n) => text.startsWith(n))) return text.slice(0, 120);
141
+ }
142
+ return null;
143
+ }
144
+
145
+ function promptText(content: unknown): string | null {
146
+ if (typeof content === 'string') return content.trim() === '' ? null : content.trim();
147
+ if (!Array.isArray(content)) return null;
148
+ for (const block of content)
149
+ if (isRecord(block) && block.type === 'text') {
150
+ const text = str(block.text);
151
+ if (text !== null) return text.trim();
152
+ }
153
+ return null;
154
+ }
155
+
156
+ function sessionOf(path: string, id: string, mtime: number, size: number): ClaudeSession {
157
+ const head = readEdge(path, false);
158
+ const tail = readEdge(path, true);
159
+ const titled = [...tail].reverse().find((l) => l.type === 'ai-title' && str(l.aiTitle) !== null);
160
+ const meta = head.find((l) => l.type === 'user' || l.type === 'assistant') ?? {};
161
+ return {
162
+ id,
163
+ title: (titled === undefined ? null : str(titled.aiTitle)) ?? firstPrompt(head) ?? 'Untitled session',
164
+ startedAt: str(head.find((l) => str(l.timestamp) !== null)?.timestamp) ?? null,
165
+ lastAt: new Date(mtime).toISOString(),
166
+ bytes: size,
167
+ gitBranch: str(meta.gitBranch),
168
+ version: str(meta.version),
169
+ };
170
+ }
171
+
172
+ export function listClaudeSessions(project: string, dir = claudeDir()): ClaudeSession[] {
173
+ const path = projectDir(project, dir);
174
+ return sessionFiles(path).map((f) =>
175
+ sessionOf(join(path, f.name), f.name.slice(0, -'.jsonl'.length), f.mtime, f.size),
176
+ );
177
+ }
178
+
179
+ export type Block =
180
+ | { kind: 'text'; text: string }
181
+ | { kind: 'tool_use'; name: string; input: string }
182
+ | { kind: 'tool_result'; text: string; isError: boolean }
183
+ | { kind: 'thinking' }
184
+ | { kind: 'image' };
185
+
186
+ export interface TranscriptEntry {
187
+ uuid: string;
188
+ at: string | null;
189
+ role: 'user' | 'assistant';
190
+ blocks: Block[];
191
+ }
192
+
193
+ export interface TranscriptPage {
194
+ entries: TranscriptEntry[];
195
+ total: number;
196
+ next: number | null;
197
+ }
198
+
199
+ const cap = (text: string, max: number): string =>
200
+ text.length > max ? `${text.slice(0, max)}\n… (${String(text.length - max)} more characters)` : text;
201
+
202
+ function resultText(content: unknown): string {
203
+ if (typeof content === 'string') return content;
204
+ if (!Array.isArray(content)) return '';
205
+ return content
206
+ .map((b) => (isRecord(b) && b.type === 'text' ? (str(b.text) ?? '') : ''))
207
+ .filter((t) => t !== '')
208
+ .join('\n');
209
+ }
210
+
211
+ function textBlock(raw: Record<string, unknown>): Block | null {
212
+ const text = str(raw.text);
213
+ if (text === null || NOISE.some((n) => text.startsWith(n))) return null;
214
+ return { kind: 'text', text: cap(text, TEXT_CAP) };
215
+ }
216
+
217
+ const BLOCKS: Record<string, (raw: Record<string, unknown>) => Block | null> = {
218
+ text: textBlock,
219
+ tool_use: (raw) => ({
220
+ kind: 'tool_use',
221
+ name: str(raw.name) ?? 'tool',
222
+ input: cap(JSON.stringify(raw.input ?? {}, null, 1), INPUT_CAP),
223
+ }),
224
+ tool_result: (raw) => ({
225
+ kind: 'tool_result',
226
+ text: cap(resultText(raw.content), RESULT_CAP),
227
+ isError: raw.is_error === true,
228
+ }),
229
+ thinking: () => ({ kind: 'thinking' }),
230
+ image: () => ({ kind: 'image' }),
231
+ };
232
+
233
+ function blockOf(raw: unknown): Block | null {
234
+ if (!isRecord(raw) || typeof raw.type !== 'string') return null;
235
+ const make = BLOCKS[raw.type];
236
+ return make === undefined ? null : make(raw);
237
+ }
238
+
239
+ function entryOf(line: Record<string, unknown>): TranscriptEntry | null {
240
+ if ((line.type !== 'user' && line.type !== 'assistant') || line.isSidechain === true) return null;
241
+ const message = isRecord(line.message) ? line.message : {};
242
+ const content = message.content;
243
+ const blocks =
244
+ typeof content === 'string'
245
+ ? [blockOf({ type: 'text', text: content })]
246
+ : Array.isArray(content)
247
+ ? content.map(blockOf)
248
+ : [];
249
+ const kept = blocks.filter((b): b is Block => b !== null);
250
+ if (kept.length === 0) return null;
251
+ return { uuid: str(line.uuid) ?? '', at: str(line.timestamp), role: line.type, blocks: kept };
252
+ }
253
+
254
+ export async function readTranscript(
255
+ project: string,
256
+ session: string,
257
+ offset: number,
258
+ limit: number,
259
+ dir = claudeDir(),
260
+ ): Promise<TranscriptPage> {
261
+ const path = join(projectDir(project, dir), `${safeName(session, SESSION_RE, 'session id')}.jsonl`);
262
+ if (!existsSync(path)) throw new ApiError('no such session', 404);
263
+ const entries: TranscriptEntry[] = [];
264
+ let total = 0;
265
+ const lines = createInterface({ input: createReadStream(path, { encoding: 'utf8' }), crlfDelay: Infinity });
266
+ for await (const line of lines) {
267
+ const parsed = parseLines(line)[0];
268
+ const entry = parsed === undefined ? null : entryOf(parsed);
269
+ if (entry === null) continue;
270
+ if (total >= offset && entries.length < limit) entries.push(entry);
271
+ total += 1;
272
+ }
273
+ return { entries, total, next: offset + limit < total ? offset + limit : null };
274
+ }
275
+
276
+ export interface MemoryFile {
277
+ name: string;
278
+ bytes: number;
279
+ modifiedAt: string;
280
+ }
281
+
282
+ export interface MemoryListing {
283
+ files: MemoryFile[];
284
+ index: string | null;
285
+ }
286
+
287
+ function memoryDir(project: string, dir: string): string {
288
+ return join(projectDir(project, dir), 'memory');
289
+ }
290
+
291
+ function readCapped(path: string): string {
292
+ const size = statSync(path).size;
293
+ const fd = openSync(path, 'r');
294
+ try {
295
+ const buffer = Buffer.alloc(Math.min(size, FILE_CAP));
296
+ readSync(fd, buffer, 0, buffer.length, 0);
297
+ return size > FILE_CAP ? `${buffer.toString('utf8')}\n\n… truncated` : buffer.toString('utf8');
298
+ } finally {
299
+ closeSync(fd);
300
+ }
301
+ }
302
+
303
+ export function listMemory(project: string, dir = claudeDir()): MemoryListing {
304
+ const path = memoryDir(project, dir);
305
+ if (!existsSync(path)) return { files: [], index: null };
306
+ const files = readdirSync(path)
307
+ .filter((name) => MEMORY_RE.test(name) && name !== 'MEMORY.md')
308
+ .map((name) => {
309
+ const s = statSync(join(path, name));
310
+ return { name, bytes: s.size, modifiedAt: s.mtime.toISOString() };
311
+ })
312
+ .sort((a, b) => a.name.localeCompare(b.name));
313
+ const index = join(path, 'MEMORY.md');
314
+ return { files, index: existsSync(index) ? readCapped(index) : null };
315
+ }
316
+
317
+ export function readMemoryFile(project: string, name: string, dir = claudeDir()): string {
318
+ const path = join(memoryDir(project, dir), safeName(name, MEMORY_RE, 'memory file name'));
319
+ if (!existsSync(path)) throw new ApiError('no such memory file', 404);
320
+ return readCapped(path);
321
+ }
@@ -45,11 +45,16 @@ function credential(file: Record<string, unknown> | null): HostedCredential | nu
45
45
  return { token: file.token, url: typeof file.url === 'string' && file.url !== '' ? file.url : metroBaseUrl() };
46
46
  }
47
47
 
48
- export function hostedCredentialFor(agentId: string, dir = configDir()): HostedCredential | null {
49
- const runtime = join(dir, `runtime-${agentId}.json`);
50
- if (existsSync(runtime)) return credential(readJsonFile(runtime));
48
+ export function hostedCredentialsFor(agentId: string, dir = configDir()): HostedCredential[] {
49
+ const out: HostedCredential[] = [];
51
50
  const login = credential(readJsonFile(join(dir, 'credentials.json')));
52
- return login !== null && agentOfToken(login.token) === agentId ? login : null;
51
+ if (login !== null && agentOfToken(login.token) === agentId) out.push(login);
52
+ const runtime = join(dir, `runtime-${agentId}.json`);
53
+ if (existsSync(runtime)) {
54
+ const cred = credential(readJsonFile(runtime));
55
+ if (cred !== null) out.push(cred);
56
+ }
57
+ return out;
53
58
  }
54
59
 
55
60
  const isConnector = (v: unknown): v is HostedConnector =>
@@ -58,26 +63,48 @@ const isConnector = (v: unknown): v is HostedConnector =>
58
63
  typeof (v as { id?: unknown }).id === 'string' &&
59
64
  typeof (v as { name?: unknown }).name === 'string';
60
65
 
66
+ class Refused extends Error {}
67
+
61
68
  async function fetchHosted(cred: HostedCredential): Promise<HostedConnector[]> {
62
69
  const res = await fetch(`${cred.url}/api/cli/connectors`, {
63
70
  headers: { authorization: `Bearer ${cred.token}` },
64
71
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
65
72
  redirect: 'manual',
66
73
  });
74
+ if (res.status === 401 || res.status === 403 || res.status === 409) {
75
+ await res.arrayBuffer().catch(() => undefined);
76
+ throw new Refused(`metro no longer accepts this credential (${String(res.status)})`);
77
+ }
67
78
  if (!res.ok) throw new Error(`metro answered ${String(res.status)} for the connectors`);
68
79
  const body = (await res.json()) as { connectors?: unknown };
69
80
  return Array.isArray(body.connectors) ? body.connectors.filter(isConnector) : [];
70
81
  }
71
82
 
83
+ async function firstAccepted(agentId: string, creds: HostedCredential[]): Promise<HostedConnector[] | null> {
84
+ for (const cred of creds) {
85
+ try {
86
+ return await fetchHosted(cred);
87
+ } catch (err) {
88
+ if (!(err instanceof Refused)) throw err;
89
+ log.warn({ agent: agentId, reason: errMsg(err) }, 'hosted connectors: credential refused, trying the next');
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+
72
95
  const cache = new Map<string, { at: number; list: HostedConnector[] }>();
73
96
 
74
97
  export async function hostedConnectorsFor(agentId: string, dir = configDir()): Promise<HostedConnector[]> {
75
98
  const hit = cache.get(agentId);
76
99
  if (hit !== undefined && Date.now() - hit.at < CACHE_MS) return hit.list;
77
- const cred = hostedCredentialFor(agentId, dir);
78
- if (cred === null) return [];
100
+ const creds = hostedCredentialsFor(agentId, dir);
101
+ if (creds.length === 0) return [];
79
102
  try {
80
- const list = await fetchHosted(cred);
103
+ const list = await firstAccepted(agentId, creds);
104
+ if (list === null) {
105
+ log.warn({ agent: agentId }, 'hosted connectors: every stored credential is stale; metro login again');
106
+ return [];
107
+ }
81
108
  cache.set(agentId, { at: Date.now(), list });
82
109
  return list;
83
110
  } catch (err) {
@@ -180,6 +180,7 @@ export function localSessionApis(deps: LocalModeDeps): SessionApis {
180
180
  agentConnectorApi,
181
181
  importApi: importApi(deps),
182
182
  localConnectors: { listConnectors: connectorsOfOwner },
183
+ claudeApi: { authorize: (subject) => { assertLocalOwner(subject); } },
183
184
  projectApi,
184
185
  siwe: { ensureUser: claimLocalOwner },
185
186
  mode: localModeInfo,
@@ -22,6 +22,7 @@ import {
22
22
  handleLocalConnectorsRequest,
23
23
  type LocalConnectorsDeps,
24
24
  } from './local-connectors-api.js';
25
+ import { handleClaudeRequest, type ClaudeApiDeps } from './claude-api.js';
25
26
  import type { ModeInfo } from './mode-api.js';
26
27
 
27
28
  export interface SessionApis {
@@ -29,6 +30,7 @@ export interface SessionApis {
29
30
  agentConnectorApi?: AgentConnectorApiDeps;
30
31
  importApi?: ImportApiDeps;
31
32
  localConnectors?: LocalConnectorsDeps;
33
+ claudeApi?: ClaudeApiDeps;
32
34
  connectorApi?: ConnectorApiDeps;
33
35
  projectApi?: ProjectApiDeps;
34
36
  runApi?: RunApiDeps;
@@ -43,7 +45,7 @@ export function handleSessionApis(
43
45
  apis: SessionApis,
44
46
  ): boolean {
45
47
  const { agentApi, agentConnectorApi, connectorApi, projectApi, runApi, importApi } = apis;
46
- const { localConnectors } = apis;
48
+ const { localConnectors, claudeApi } = apis;
47
49
  const routes: (() => boolean)[] = [() => handleSessionApiRequest(req, res)];
48
50
  if (runApi) routes.push(() => handleRunApiRequest(req, res, runApi));
49
51
  if (projectApi)
@@ -56,6 +58,7 @@ export function handleSessionApis(
56
58
  if (importApi) routes.push(() => handleImportRequest(req, res, importApi));
57
59
  if (localConnectors)
58
60
  routes.push(() => handleLocalConnectorsRequest(req, res, localConnectors));
61
+ if (claudeApi) routes.push(() => handleClaudeRequest(req, res, claudeApi));
59
62
  if (agentConnectorApi)
60
63
  routes.push(() => handleAgentConnectorRequest(req, res, agentConnectorApi));
61
64
  if (agentApi) routes.push(() => handleAgentApiRequest(req, res, agentApi));
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.40"
2
+ "version": "0.1.0-beta.42"
3
3
  }