@stage-labs/metro 0.1.0-beta.123 → 0.1.0-beta.125
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 +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +25 -6
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +44 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +109 -0
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +21 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +2 -2
- package/runtime/runtime.json +1 -1
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.125",
|
|
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.125",
|
|
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, the standing rules loaded at each session start, and a refresh skill for when the daemon could not keep the server list current. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
stopSession,
|
|
32
32
|
type SessionDeps,
|
|
33
33
|
} from './session.js';
|
|
34
|
+
import { claudeVersion, updateClaude, type VersionDeps } from './version.js';
|
|
35
|
+
import { readSessionFile, SESSION_FILE_MAX, writeSessionFile } from './session-files.js';
|
|
34
36
|
import {
|
|
35
37
|
claudeDir,
|
|
36
38
|
deleteClaudeSession,
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
|
|
45
47
|
const PREFIX = '/api/claude';
|
|
46
48
|
const BODY_MAX = SETTINGS_MAX + 4096;
|
|
49
|
+
const SESSION_BODY_MAX = SESSION_FILE_MAX + 4096;
|
|
47
50
|
const WRITABLE = new Set(['GET', 'DELETE', 'PUT', 'POST']);
|
|
48
51
|
const PAGE = 100;
|
|
49
52
|
const PAGE_MAX = 500;
|
|
@@ -54,6 +57,7 @@ export interface ClaudeApiDeps {
|
|
|
54
57
|
login?: LoginDeps;
|
|
55
58
|
session?: SessionDeps;
|
|
56
59
|
setup?: SetupDeps;
|
|
60
|
+
version?: VersionDeps;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
function projectOf(query: URLSearchParams): string {
|
|
@@ -83,6 +87,7 @@ const COLLECTIONS: Record<string, Handler> = {
|
|
|
83
87
|
|
|
84
88
|
const ITEMS: Record<string, Handler> = {
|
|
85
89
|
sessions: (query, dir, id) => {
|
|
90
|
+
if (query.get('raw') === '1') return { id, text: readSessionFile(projectOf(query), id, dir) };
|
|
86
91
|
const { offset, limit } = pageOf(query);
|
|
87
92
|
return readTranscript(projectOf(query), id, offset, limit, dir);
|
|
88
93
|
},
|
|
@@ -95,17 +100,21 @@ const parts = (path: string): string[] => path.slice(PREFIX.length + 1).split('/
|
|
|
95
100
|
const seenIn = (body: Record<string, unknown>): string | null | undefined =>
|
|
96
101
|
'seenAt' in body ? (typeof body.seenAt === 'string' ? body.seenAt : null) : undefined;
|
|
97
102
|
|
|
98
|
-
const WRITE_HEADS = new Set(['settings', 'skills', 'memory']);
|
|
103
|
+
const WRITE_HEADS = new Set(['settings', 'skills', 'memory', 'sessions']);
|
|
104
|
+
|
|
105
|
+
function writeItem(head: string, item: string, text: string, seen: string | null | undefined, search: string, dir: string): unknown {
|
|
106
|
+
if (head === 'skills') return writeClaudeSkill(decodeURIComponent(item), text, seen, dir);
|
|
107
|
+
if (head === 'sessions') return writeSessionFile(projectOf(new URLSearchParams(search)), item, text, dir);
|
|
108
|
+
if (head === 'memory') return writeMemoryFile(projectOf(new URLSearchParams(search)), decodeURIComponent(item), text, dir);
|
|
109
|
+
return writeClaudeSettings(item, text, seen, dir);
|
|
110
|
+
}
|
|
99
111
|
|
|
100
112
|
async function writeAnswer(req: IncomingMessage, path: string, search: string, dir: string): Promise<unknown> {
|
|
101
113
|
const [head = '', item = ''] = parts(path);
|
|
102
114
|
if (!WRITE_HEADS.has(head) || item === '') throw new ApiError('method not allowed', 405);
|
|
103
|
-
const body = await readJsonBody(req, BODY_MAX);
|
|
115
|
+
const body = await readJsonBody(req, head === 'sessions' ? SESSION_BODY_MAX : BODY_MAX);
|
|
104
116
|
if (!isRecord(body) || typeof body.text !== 'string') throw new ApiError('text is required', 400);
|
|
105
|
-
|
|
106
|
-
if (head === 'memory')
|
|
107
|
-
return writeMemoryFile(projectOf(new URLSearchParams(search)), decodeURIComponent(item), body.text, dir);
|
|
108
|
-
return writeClaudeSettings(item, body.text, seenIn(body), dir);
|
|
117
|
+
return writeItem(head, item, body.text, seenIn(body), search, dir);
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
async function created(req: IncomingMessage, path: string, dir: string): Promise<unknown> {
|
|
@@ -120,6 +129,7 @@ async function created(req: IncomingMessage, path: string, dir: string): Promise
|
|
|
120
129
|
const LOGIN = 'login';
|
|
121
130
|
const SESSION = 'session';
|
|
122
131
|
const SETUP = 'setup';
|
|
132
|
+
const VERSION = 'version';
|
|
123
133
|
|
|
124
134
|
interface SetupChange {
|
|
125
135
|
privacy?: boolean;
|
|
@@ -211,9 +221,18 @@ function answer(method: string, path: string, query: URLSearchParams, dir: strin
|
|
|
211
221
|
return handler(query, dir, item);
|
|
212
222
|
}
|
|
213
223
|
|
|
224
|
+
function versionAnswer(req: IncomingMessage, deps: ClaudeApiDeps): Promise<unknown> {
|
|
225
|
+
const version: VersionDeps = { ...deps.version, session: deps.version?.session ?? deps.session };
|
|
226
|
+
const method = req.method ?? 'GET';
|
|
227
|
+
if (method === 'GET') return claudeVersion(version);
|
|
228
|
+
if (method !== 'POST') throw new ApiError('method not allowed', 405);
|
|
229
|
+
return updateClaude(version);
|
|
230
|
+
}
|
|
231
|
+
|
|
214
232
|
const SINGLETONS: Record<string, (req: IncomingMessage, deps: ClaudeApiDeps) => Promise<unknown>> = {
|
|
215
233
|
[SESSION]: sessionAnswer,
|
|
216
234
|
[SETUP]: setupAnswer,
|
|
235
|
+
[VERSION]: versionAnswer,
|
|
217
236
|
};
|
|
218
237
|
|
|
219
238
|
function routed(req: IncomingMessage, path: string, search: string, deps: ClaudeApiDeps): unknown {
|
|
@@ -17,8 +17,8 @@ import { createInterface } from 'node:readline';
|
|
|
17
17
|
import { ApiError } from '@metro-labs/http/api-error';
|
|
18
18
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
19
19
|
|
|
20
|
-
const PROJECT_RE = /^[A-Za-z0-9._-]{1,200}$/;
|
|
21
|
-
const SESSION_RE = /^[A-Za-z0-9][A-Za-z0-9-]{7,63}$/;
|
|
20
|
+
export const PROJECT_RE = /^[A-Za-z0-9._-]{1,200}$/;
|
|
21
|
+
export const SESSION_RE = /^[A-Za-z0-9][A-Za-z0-9-]{7,63}$/;
|
|
22
22
|
const MEMORY_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,119}\.md$/;
|
|
23
23
|
const EDGE_BYTES = 64 * 1024;
|
|
24
24
|
const TEXT_CAP = 20_000;
|
|
@@ -39,7 +39,7 @@ export function claudeDir(): string {
|
|
|
39
39
|
|
|
40
40
|
const str = (v: unknown): string | null => (typeof v === 'string' && v !== '' ? v : null);
|
|
41
41
|
|
|
42
|
-
function safeName(value: string, re: RegExp, what: string): string {
|
|
42
|
+
export function safeName(value: string, re: RegExp, what: string): string {
|
|
43
43
|
if (!re.test(value) || value === '.' || value === '..')
|
|
44
44
|
throw new ApiError(`that is not a ${what}`, 400);
|
|
45
45
|
return value;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ApiError } from '@metro-labs/http/api-error';
|
|
4
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
5
|
+
import { claudeDir, PROJECT_RE, safeName, SESSION_RE } from './files.js';
|
|
6
|
+
|
|
7
|
+
export const SESSION_FILE_MAX = 64 * 1024 * 1024;
|
|
8
|
+
const FILE_MODE = 0o644;
|
|
9
|
+
|
|
10
|
+
export interface SessionFile {
|
|
11
|
+
id: string;
|
|
12
|
+
bytes: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const transcriptPath = (project: string, session: string, dir: string): string =>
|
|
16
|
+
join(dir, 'projects', safeName(project, PROJECT_RE, 'Claude project id'), `${safeName(session, SESSION_RE, 'session id')}.jsonl`);
|
|
17
|
+
|
|
18
|
+
export function readSessionFile(project: string, session: string, dir = claudeDir()): string {
|
|
19
|
+
const path = transcriptPath(project, session, dir);
|
|
20
|
+
if (!existsSync(path)) throw new ApiError('no such session', 404);
|
|
21
|
+
if (statSync(path).size > SESSION_FILE_MAX) throw new ApiError(`a session file is at most ${String(SESSION_FILE_MAX)} bytes`, 400);
|
|
22
|
+
return readFileSync(path, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function firstLineIsJson(text: string): boolean {
|
|
26
|
+
const first = text.split('\n').find((line) => line.trim() !== '');
|
|
27
|
+
if (first === undefined) return false;
|
|
28
|
+
try {
|
|
29
|
+
return isRecord(JSON.parse(first));
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function writeSessionFile(project: string, session: string, text: string, dir = claudeDir()): SessionFile {
|
|
36
|
+
if (!firstLineIsJson(text)) throw new ApiError('a session file is JSON lines, one Claude Code entry per line', 400);
|
|
37
|
+
if (Buffer.byteLength(text, 'utf8') > SESSION_FILE_MAX) throw new ApiError(`a session file is at most ${String(SESSION_FILE_MAX)} bytes`, 400);
|
|
38
|
+
const path = transcriptPath(project, session, dir);
|
|
39
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
40
|
+
const tmp = `${path}.metro-${String(process.pid)}`;
|
|
41
|
+
writeFileSync(tmp, text, { mode: FILE_MODE });
|
|
42
|
+
renameSync(tmp, path);
|
|
43
|
+
return { id: safeName(session, SESSION_RE, 'session id'), bytes: statSync(path).size };
|
|
44
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { ApiError } from '@metro-labs/http/api-error';
|
|
5
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
6
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
7
|
+
import { sessionRunning, stopSession, type SessionDeps } from './session.js';
|
|
8
|
+
|
|
9
|
+
const LATEST_URL = 'https://registry.npmjs.org/@anthropic-ai/claude-code/latest';
|
|
10
|
+
const VERSION_RE = /(\d+)\.(\d+)\.(\d+)/;
|
|
11
|
+
const CACHE_MS = 10 * 60_000;
|
|
12
|
+
const INSTALL_TIMEOUT_MS = 5 * 60_000;
|
|
13
|
+
const TAIL = 400;
|
|
14
|
+
|
|
15
|
+
export interface VersionDeps {
|
|
16
|
+
claude?: string;
|
|
17
|
+
fetchImpl?: typeof fetch;
|
|
18
|
+
session?: SessionDeps;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ClaudeVersion {
|
|
23
|
+
installed: string | null;
|
|
24
|
+
latest: string | null;
|
|
25
|
+
newer: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let cached: { latest: string | null; at: number } | null = null;
|
|
29
|
+
|
|
30
|
+
const candidates = (deps: VersionDeps): string[] => (deps.claude === undefined ? ['claude', join(homedir(), '.local', 'bin', 'claude')] : [deps.claude]);
|
|
31
|
+
|
|
32
|
+
const parse = (text: string): string | null => {
|
|
33
|
+
const m = VERSION_RE.exec(text);
|
|
34
|
+
return m === null ? null : `${m[1] ?? ''}.${m[2] ?? ''}.${m[3] ?? ''}`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function probe(bin: string): string | null {
|
|
38
|
+
const run = spawnSync(bin, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
39
|
+
return run.error === undefined && run.status === 0 && typeof run.stdout === 'string' ? parse(run.stdout) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function located(deps: VersionDeps): { bin: string; installed: string } | null {
|
|
43
|
+
for (const bin of candidates(deps)) {
|
|
44
|
+
const installed = probe(bin);
|
|
45
|
+
if (installed !== null) return { bin, installed };
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function newerThan(a: string, b: string): boolean {
|
|
51
|
+
const left = a.split('.').map(Number);
|
|
52
|
+
const right = b.split('.').map(Number);
|
|
53
|
+
for (let i = 0; i < 3; i += 1) {
|
|
54
|
+
const d = (left[i] ?? 0) - (right[i] ?? 0);
|
|
55
|
+
if (d !== 0) return d > 0;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function latestClaudeVersion(deps: VersionDeps = {}): Promise<string | null> {
|
|
61
|
+
const now = (deps.now ?? Date.now)();
|
|
62
|
+
if (cached !== null && now - cached.at < CACHE_MS) return cached.latest;
|
|
63
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
64
|
+
let latest: string | null = null;
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetchImpl(LATEST_URL, { headers: { accept: 'application/json' }, redirect: 'manual' });
|
|
67
|
+
const body: unknown = res.ok ? await res.json() : null;
|
|
68
|
+
latest = isRecord(body) && typeof body.version === 'string' ? parse(body.version) : null;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log.warn({ err: errMsg(err) }, 'claude-version: could not read the latest version from npm');
|
|
71
|
+
}
|
|
72
|
+
cached = { latest, at: now };
|
|
73
|
+
return latest;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function forgetClaudeVersion(): void {
|
|
77
|
+
cached = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function claudeVersion(deps: VersionDeps = {}): Promise<ClaudeVersion> {
|
|
81
|
+
const installed = located(deps)?.installed ?? null;
|
|
82
|
+
const latest = await latestClaudeVersion(deps);
|
|
83
|
+
return { installed, latest, newer: installed !== null && latest !== null && newerThan(latest, installed) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function install(bin: string): void {
|
|
87
|
+
const run = spawnSync(bin, ['install', 'latest'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: INSTALL_TIMEOUT_MS });
|
|
88
|
+
if (run.error !== undefined) throw new ApiError(`claude install latest failed: ${errMsg(run.error)}`, 502);
|
|
89
|
+
if (run.status === 0) return;
|
|
90
|
+
const said = `${run.stderr ?? ''}${run.stdout ?? ''}`.trim().slice(-TAIL);
|
|
91
|
+
throw new ApiError(`claude install latest failed: ${said}`, 502);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function restartIfChanged(changed: boolean, session: SessionDeps): boolean {
|
|
95
|
+
if (!changed || !sessionRunning(session.tmux ?? 'tmux')) return false;
|
|
96
|
+
stopSession(session);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function updateClaude(deps: VersionDeps = {}): Promise<ClaudeVersion & { restarted: boolean }> {
|
|
101
|
+
const found = located(deps);
|
|
102
|
+
if (found === null) throw new ApiError('Claude Code is not installed on this machine', 404);
|
|
103
|
+
install(found.bin);
|
|
104
|
+
forgetClaudeVersion();
|
|
105
|
+
const after = await claudeVersion(deps);
|
|
106
|
+
const restarted = restartIfChanged(after.installed !== null && after.installed !== found.installed, deps.session ?? {});
|
|
107
|
+
log.info({ from: found.installed, to: after.installed, restarted }, 'claude-version: Claude Code updated');
|
|
108
|
+
return { ...after, restarted };
|
|
109
|
+
}
|
|
@@ -16,6 +16,7 @@ import { GatewayError } from './forward.js';
|
|
|
16
16
|
import {
|
|
17
17
|
applyModelUpdate,
|
|
18
18
|
ModelConfigError,
|
|
19
|
+
parseModelConfig,
|
|
19
20
|
publicModelConfig,
|
|
20
21
|
readModelConfig,
|
|
21
22
|
setCodexAuth,
|
|
@@ -28,6 +29,9 @@ const CODEX = '/api/model/codex/';
|
|
|
28
29
|
const OPENROUTER = '/api/model/openrouter/';
|
|
29
30
|
const ANTHROPIC = '/api/model/anthropic/';
|
|
30
31
|
const BEDROCK = '/api/model/bedrock/';
|
|
32
|
+
const BUNDLE = '/api/model/bundle';
|
|
33
|
+
const RESTORE = '/api/model/restore';
|
|
34
|
+
const RESTORE_MAX = 64 * 1024;
|
|
31
35
|
const BODY_MAX = 16 * 1024;
|
|
32
36
|
const DEVICE_PREFIX = 'device/';
|
|
33
37
|
const DEVICE_ID_RE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
@@ -199,6 +203,21 @@ const named = (table: Record<string, Route>, name: string, method: string | unde
|
|
|
199
203
|
return route.method === method ? route : 405;
|
|
200
204
|
};
|
|
201
205
|
|
|
206
|
+
async function restore(req: IncomingMessage, store: Store, deps: ModelApiDeps): Promise<unknown> {
|
|
207
|
+
const body = await readJsonBody(req, RESTORE_MAX);
|
|
208
|
+
if (!isRecord(body)) throw new ApiError('body must be a JSON object', 400);
|
|
209
|
+
const next = parseModelConfig(body);
|
|
210
|
+
store.write(next);
|
|
211
|
+
syncAvailableModelsQuietly(deps.setup ?? {}, next);
|
|
212
|
+
log.info({ provider: next.provider }, 'model-api: model setup restored from a file');
|
|
213
|
+
return settingsBody(next);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function bundleRoute(path: string, method: string | undefined): Route | number {
|
|
217
|
+
if (path === BUNDLE) return method === 'GET' ? { method: 'GET', run: (_req, _deps, store) => Promise.resolve(store.read()) } : 405;
|
|
218
|
+
return method === 'POST' ? { method: 'POST', run: (req, deps, store) => restore(req, store, deps) } : 405;
|
|
219
|
+
}
|
|
220
|
+
|
|
202
221
|
function settingsRoute(method: string | undefined): Route | number {
|
|
203
222
|
if (method === 'GET') return { method: 'GET', run: (_req, deps, store) => settingsWithUsage(store.read(), deps) };
|
|
204
223
|
if (method === 'PUT') return { method: 'POST', run: (req, deps, store) => update(req, store, deps) };
|
|
@@ -213,10 +232,11 @@ function codexRoute(rest: string, method: string | undefined): Route | number {
|
|
|
213
232
|
}
|
|
214
233
|
|
|
215
234
|
const mine = (path: string): boolean =>
|
|
216
|
-
path === PATH || path.startsWith(CODEX) || path.startsWith(OPENROUTER) || path.startsWith(ANTHROPIC) || path.startsWith(BEDROCK);
|
|
235
|
+
path === PATH || path === BUNDLE || path === RESTORE || path.startsWith(CODEX) || path.startsWith(OPENROUTER) || path.startsWith(ANTHROPIC) || path.startsWith(BEDROCK);
|
|
217
236
|
|
|
218
237
|
function routeFor(path: string, method: string | undefined): Route | number {
|
|
219
238
|
if (path === PATH) return settingsRoute(method);
|
|
239
|
+
if (path === BUNDLE || path === RESTORE) return bundleRoute(path, method);
|
|
220
240
|
if (path.startsWith(OPENROUTER)) return named(OPENROUTER_ROUTES, path.slice(OPENROUTER.length), method);
|
|
221
241
|
if (path.startsWith(ANTHROPIC)) return named(ANTHROPIC_ROUTES, path.slice(ANTHROPIC.length), method);
|
|
222
242
|
if (path.startsWith(BEDROCK)) return named(BEDROCK_ROUTES, path.slice(BEDROCK.length), method);
|
|
@@ -82,7 +82,7 @@ function tokensFromDisk(raw: unknown): CodexTokens | null {
|
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
function
|
|
85
|
+
export function parseModelConfig(raw: unknown): ModelConfig {
|
|
86
86
|
const base = empty();
|
|
87
87
|
if (!isRecord(raw)) return base;
|
|
88
88
|
const anthropic = isRecord(raw.anthropic) ? raw.anthropic : {};
|
|
@@ -100,7 +100,7 @@ function fromDisk(raw: unknown): ModelConfig {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
export function readModelConfig(dir = agentsDir()): ModelConfig {
|
|
103
|
-
return
|
|
103
|
+
return parseModelConfig(readJson<unknown>(join(dir, MODEL_FILE), null, { warn: 'model-config: model.json is unreadable, so every request goes to Anthropic until it is fixed' }));
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
export function writeModelConfig(cfg: ModelConfig, dir = agentsDir()): void {
|
package/runtime/runtime.json
CHANGED