@stage-labs/metro 0.1.0-beta.179 → 0.1.0-beta.180
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.
|
|
3
|
+
"version": "0.1.0-beta.180",
|
|
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.
|
|
3
|
+
"version": "0.1.0-beta.180",
|
|
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",
|
|
@@ -99,7 +99,10 @@ const ITEMS: Record<string, Handler> = {
|
|
|
99
99
|
const { offset, limit } = pageOf(query);
|
|
100
100
|
return readTranscript(projectOf(query), id, offset, limit, dir);
|
|
101
101
|
},
|
|
102
|
-
memory: (query, dir,
|
|
102
|
+
memory: (query, dir, raw) => {
|
|
103
|
+
const name = decodeURIComponent(raw);
|
|
104
|
+
return { name, content: readMemoryFile(projectOf(query), name, dir) };
|
|
105
|
+
},
|
|
103
106
|
skills: (_query, dir, id) => readClaudeSkill(decodeURIComponent(id), dir),
|
|
104
107
|
};
|
|
105
108
|
|
|
@@ -19,6 +19,9 @@ import { writeAtomic } from '@metro-labs/core/secure-fs';
|
|
|
19
19
|
export const PROJECT_RE = /^[A-Za-z0-9._-]{1,200}$/;
|
|
20
20
|
export const SESSION_RE = /^[A-Za-z0-9][A-Za-z0-9-]{7,63}$/;
|
|
21
21
|
const MEMORY_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,119}\.md$/;
|
|
22
|
+
const FOLDER_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,119}$/;
|
|
23
|
+
const MEMORY_DEPTH = 6;
|
|
24
|
+
const MEMORY_FILES_MAX = 5000;
|
|
22
25
|
const EDGE_BYTES = 64 * 1024;
|
|
23
26
|
const TEXT_CAP = 20_000;
|
|
24
27
|
const RESULT_CAP = 8_000;
|
|
@@ -312,16 +315,37 @@ function readCapped(path: string): string {
|
|
|
312
315
|
}
|
|
313
316
|
}
|
|
314
317
|
|
|
318
|
+
function memoryName(name: string): string[] {
|
|
319
|
+
const segments = name.split('/');
|
|
320
|
+
const file = segments.at(-1) ?? '';
|
|
321
|
+
const folders = segments.slice(0, -1);
|
|
322
|
+
if (folders.length >= MEMORY_DEPTH || !MEMORY_RE.test(file) || folders.some((f) => !FOLDER_RE.test(f)))
|
|
323
|
+
throw new ApiError('that is not a memory file name', 400);
|
|
324
|
+
return segments;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function memoryPath(project: string, name: string, dir: string): string {
|
|
328
|
+
return join(memoryDir(project, dir), ...memoryName(name));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function walkMemory(root: string, folders: string[], out: MemoryFile[]): void {
|
|
332
|
+
for (const entry of readdirSync(join(root, ...folders), { withFileTypes: true })) {
|
|
333
|
+
if (out.length >= MEMORY_FILES_MAX) return;
|
|
334
|
+
const inner = [...folders, entry.name];
|
|
335
|
+
if (entry.isDirectory() && FOLDER_RE.test(entry.name) && inner.length < MEMORY_DEPTH) walkMemory(root, inner, out);
|
|
336
|
+
else if (entry.isFile() && MEMORY_RE.test(entry.name) && !(folders.length === 0 && entry.name === 'MEMORY.md')) {
|
|
337
|
+
const s = statSync(join(root, ...inner));
|
|
338
|
+
out.push({ name: inner.join('/'), bytes: s.size, modifiedAt: s.mtime.toISOString() });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
315
343
|
export function listMemory(project: string, dir = claudeDir()): MemoryListing {
|
|
316
344
|
const path = memoryDir(project, dir);
|
|
317
345
|
if (!existsSync(path)) return { files: [], index: null };
|
|
318
|
-
const files =
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const s = statSync(join(path, name));
|
|
322
|
-
return { name, bytes: s.size, modifiedAt: s.mtime.toISOString() };
|
|
323
|
-
})
|
|
324
|
-
.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt) || a.name.localeCompare(b.name));
|
|
346
|
+
const files: MemoryFile[] = [];
|
|
347
|
+
walkMemory(path, [], files);
|
|
348
|
+
files.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt) || a.name.localeCompare(b.name));
|
|
325
349
|
const index = join(path, 'MEMORY.md');
|
|
326
350
|
return { files, index: existsSync(index) ? readCapped(index) : null };
|
|
327
351
|
}
|
|
@@ -336,26 +360,23 @@ export function writeMemoryFile(project: string, name: string, text: string, dir
|
|
|
336
360
|
if (text.trim() === '') throw new ApiError('a memory file cannot be empty', 400);
|
|
337
361
|
if (Buffer.byteLength(text, 'utf8') > MEMORY_MAX)
|
|
338
362
|
throw new ApiError(`a memory file is at most ${String(MEMORY_MAX)} bytes`, 400);
|
|
339
|
-
const
|
|
340
|
-
const file = safeName(name, MEMORY_RE, 'memory file name');
|
|
341
|
-
const path = join(folder, file);
|
|
363
|
+
const path = memoryPath(project, name, dir);
|
|
342
364
|
writeAtomic(path, text, MEMORY_MODE);
|
|
343
365
|
const stamp = stampOf(modifiedAt);
|
|
344
366
|
if (stamp !== null) utimesSync(path, stamp, stamp);
|
|
345
367
|
const stat = statSync(path);
|
|
346
|
-
return { name
|
|
368
|
+
return { name, bytes: stat.size, modifiedAt: stat.mtime.toISOString() };
|
|
347
369
|
}
|
|
348
370
|
|
|
349
371
|
export function deleteMemoryFile(project: string, name: string, dir = claudeDir()): string {
|
|
350
|
-
const
|
|
351
|
-
const path = join(memoryDir(project, dir), file);
|
|
372
|
+
const path = memoryPath(project, name, dir);
|
|
352
373
|
if (!existsSync(path)) throw new ApiError('no such memory file', 404);
|
|
353
374
|
rmSync(path);
|
|
354
|
-
return
|
|
375
|
+
return name;
|
|
355
376
|
}
|
|
356
377
|
|
|
357
378
|
export function readMemoryFile(project: string, name: string, dir = claudeDir()): string {
|
|
358
|
-
const path =
|
|
379
|
+
const path = memoryPath(project, name, dir);
|
|
359
380
|
if (!existsSync(path)) throw new ApiError('no such memory file', 404);
|
|
360
381
|
return readCapped(path);
|
|
361
382
|
}
|
package/runtime/runtime.json
CHANGED