@roamcode.ai/server 1.0.0 → 1.0.1
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/index.d.ts +2406 -82
- package/dist/index.js +8314 -44
- package/dist/managed-update-helper.js +621 -26
- package/dist/mcp-send.js +76 -90
- package/dist/start.d.ts +591 -9
- package/dist/start.js +7629 -353
- package/package.json +3 -3
- package/dist/auth.d.ts +0 -63
- package/dist/auth.js +0 -133
- package/dist/claude-auth-service.d.ts +0 -76
- package/dist/claude-auth-service.js +0 -217
- package/dist/claude-latest-service.d.ts +0 -34
- package/dist/claude-latest-service.js +0 -61
- package/dist/config.d.ts +0 -78
- package/dist/config.js +0 -59
- package/dist/data-dir.d.ts +0 -42
- package/dist/data-dir.js +0 -70
- package/dist/diag.d.ts +0 -43
- package/dist/diag.js +0 -83
- package/dist/fs-service.d.ts +0 -90
- package/dist/fs-service.js +0 -290
- package/dist/managed-runtime.d.ts +0 -51
- package/dist/managed-runtime.js +0 -411
- package/dist/managed-update-helper.d.ts +0 -2
- package/dist/mcp-send.d.ts +0 -33
- package/dist/origin-check.d.ts +0 -37
- package/dist/origin-check.js +0 -101
- package/dist/pane-status.d.ts +0 -61
- package/dist/pane-status.js +0 -145
- package/dist/providers/claude-metadata-service.d.ts +0 -58
- package/dist/providers/claude-metadata-service.js +0 -352
- package/dist/providers/claude-provider.d.ts +0 -11
- package/dist/providers/claude-provider.js +0 -166
- package/dist/providers/codex-activity.d.ts +0 -21
- package/dist/providers/codex-activity.js +0 -122
- package/dist/providers/codex-app-server-client.d.ts +0 -90
- package/dist/providers/codex-app-server-client.js +0 -485
- package/dist/providers/codex-latest-service.d.ts +0 -50
- package/dist/providers/codex-latest-service.js +0 -174
- package/dist/providers/codex-metadata-service.d.ts +0 -161
- package/dist/providers/codex-metadata-service.js +0 -686
- package/dist/providers/codex-profile-client.d.ts +0 -16
- package/dist/providers/codex-profile-client.js +0 -52
- package/dist/providers/codex-profile-security.d.ts +0 -23
- package/dist/providers/codex-profile-security.js +0 -161
- package/dist/providers/codex-provider.d.ts +0 -15
- package/dist/providers/codex-provider.js +0 -174
- package/dist/providers/codex-thread-coordinator.d.ts +0 -18
- package/dist/providers/codex-thread-coordinator.js +0 -93
- package/dist/providers/codex-thread-persistence.d.ts +0 -9
- package/dist/providers/codex-thread-persistence.js +0 -45
- package/dist/providers/codex-thread-resolver.d.ts +0 -59
- package/dist/providers/codex-thread-resolver.js +0 -322
- package/dist/providers/options.d.ts +0 -7
- package/dist/providers/options.js +0 -155
- package/dist/providers/provider-artifacts.d.ts +0 -3
- package/dist/providers/provider-artifacts.js +0 -30
- package/dist/providers/registry.d.ts +0 -7
- package/dist/providers/registry.js +0 -23
- package/dist/providers/types.d.ts +0 -95
- package/dist/providers/types.js +0 -8
- package/dist/push-dispatch.d.ts +0 -81
- package/dist/push-dispatch.js +0 -100
- package/dist/push-store.d.ts +0 -25
- package/dist/push-store.js +0 -79
- package/dist/rate-limit.d.ts +0 -52
- package/dist/rate-limit.js +0 -72
- package/dist/server-config.d.ts +0 -60
- package/dist/server-config.js +0 -77
- package/dist/service-install.d.ts +0 -60
- package/dist/service-install.js +0 -221
- package/dist/session-defaults.d.ts +0 -26
- package/dist/session-defaults.js +0 -60
- package/dist/session-store.d.ts +0 -81
- package/dist/session-store.js +0 -654
- package/dist/static-routes.d.ts +0 -101
- package/dist/static-routes.js +0 -188
- package/dist/terminal-capability.d.ts +0 -5
- package/dist/terminal-capability.js +0 -27
- package/dist/terminal-manager.d.ts +0 -224
- package/dist/terminal-manager.js +0 -917
- package/dist/terminal-process.d.ts +0 -85
- package/dist/terminal-process.js +0 -238
- package/dist/terminal-shared.d.ts +0 -36
- package/dist/terminal-shared.js +0 -43
- package/dist/tmux-list.d.ts +0 -11
- package/dist/tmux-list.js +0 -39
- package/dist/transport.d.ts +0 -123
- package/dist/transport.js +0 -1559
- package/dist/updater.d.ts +0 -161
- package/dist/updater.js +0 -451
- package/dist/usage-service.d.ts +0 -118
- package/dist/usage-service.js +0 -173
- package/dist/vapid.d.ts +0 -17
- package/dist/vapid.js +0 -31
- package/dist/web-push-send.d.ts +0 -20
- package/dist/web-push-send.js +0 -21
- package/dist/ws-ticket.d.ts +0 -47
- package/dist/ws-ticket.js +0 -62
package/dist/pane-status.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
/**
|
|
3
|
-
* The newest Claude Code MAJOR.MINOR the classifier's markers below were VERIFIED against on a live box.
|
|
4
|
-
* The whole classifier is tied to Claude Code's ENGLISH TUI strings — the load-bearing markers are:
|
|
5
|
-
* - "Do you want to proceed" / "Would you like to proceed" (blocked — permission/plan prompts)
|
|
6
|
-
* - the gerund ellipsis "…" + "↓ <n>k tokens" live counter (working — main spinner + live agents)
|
|
7
|
-
* - a "…(<n>s"-style parenthesised spinner timer (working — pre-token-flow window)
|
|
8
|
-
* - "Waiting for … to finish" / "esc to interrupt" (working)
|
|
9
|
-
* A NEWER claude may reword any of these and silently degrade every rail status to "idle", so boot logs a
|
|
10
|
-
* one-time warning (see start.ts) when the installed claude's MAJOR.MINOR exceeds this. Bump it after
|
|
11
|
-
* re-verifying the markers against a newer claude.
|
|
12
|
-
*/
|
|
13
|
-
export const CLASSIFIER_TESTED_UP_TO = "2.1";
|
|
14
|
-
/**
|
|
15
|
-
* True iff `current`'s MAJOR.MINOR is strictly NEWER than `testedUpTo`'s. Patch versions are ignored — a
|
|
16
|
-
* patch release doesn't reword the TUI. Unparseable input (either side) → false, so a weird version string
|
|
17
|
-
* can never produce a spurious warning. Pure.
|
|
18
|
-
*/
|
|
19
|
-
export function isNewerMajorMinor(current, testedUpTo) {
|
|
20
|
-
const parse = (v) => {
|
|
21
|
-
const m = /(\d+)\.(\d+)/.exec(v);
|
|
22
|
-
return m ? [Number(m[1]), Number(m[2])] : undefined;
|
|
23
|
-
};
|
|
24
|
-
const cur = parse(current);
|
|
25
|
-
const tested = parse(testedUpTo);
|
|
26
|
-
if (!cur || !tested)
|
|
27
|
-
return false; // can't decide → never warn spuriously
|
|
28
|
-
if (cur[0] !== tested[0])
|
|
29
|
-
return cur[0] > tested[0];
|
|
30
|
-
return cur[1] > tested[1];
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* The one-line boot warning when the RUNNING claude is newer than the classifier was verified against —
|
|
34
|
-
* or undefined when there is nothing to warn about (older/equal/unknown version). Pure so it's testable;
|
|
35
|
-
* start.ts logs it once via console.warn and NEVER throws (a version bump must not affect boot).
|
|
36
|
-
*/
|
|
37
|
-
export function classifierVersionWarning(claudeVersion) {
|
|
38
|
-
if (!claudeVersion || !isNewerMajorMinor(claudeVersion, CLASSIFIER_TESTED_UP_TO))
|
|
39
|
-
return undefined;
|
|
40
|
-
return (`pane-status markers were verified against claude <=${CLASSIFIER_TESTED_UP_TO}; ` +
|
|
41
|
-
`current is ${claudeVersion} — verify rail statuses after this upgrade`);
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Classify a session's live ACTIVITY from its RENDERED tmux pane (`capture-pane -p` — the CURRENT screen, not
|
|
45
|
-
* scrollback). UNIVERSAL: works for any running session regardless of how claude was spawned (no per-session
|
|
46
|
-
* hooks needed), and works while the browser is DETACHED (it reads the tmux session directly). Grounded in
|
|
47
|
-
* Claude Code's real output — see pane-status.test.ts for captured samples.
|
|
48
|
-
*
|
|
49
|
-
* working → something is actively generating. The strongest tell is a LIVE token-flow counter "↓ 216.5k
|
|
50
|
-
* tokens" — it appears on the MAIN spinner ("Schlepping… (1m 17s · ↓ 2.1k tokens)") AND on an
|
|
51
|
-
* ACTIVE background agent ("⏺ general-purpose Listing f… 24m 23s · ↓ 216.5k tokens"), and is GONE
|
|
52
|
-
* the moment a turn finishes ("Baked for 23m 15s"). So a session whose MAIN loop is at the prompt
|
|
53
|
-
* but has agents still developing in the background reads "working" — NOT idle (the user's
|
|
54
|
-
* explicit correction). Also: a spinner's parenthesised timer, "Waiting for … to finish", or
|
|
55
|
-
* "esc to interrupt".
|
|
56
|
-
* blocked → claude is WAITING ON A DECISION FROM YOU: a permission prompt ("Do you want to proceed?"), a
|
|
57
|
-
* plan-mode approval ("Would you like to proceed?"). This is the ONLY state that earns the loud
|
|
58
|
-
* "needs you" — so it must stay RARE + real; that's why it's just the explicit-prompt phrasings.
|
|
59
|
-
* idle → none of the above: claude finished a turn and is sitting at an empty prompt with nothing running
|
|
60
|
-
* and nothing to decide. A calm "your turn whenever" — NOT a loud alert.
|
|
61
|
-
*
|
|
62
|
-
* Order matters: blocked is checked first (a decision prompt overrides any stale spinner text), then working,
|
|
63
|
-
* else idle.
|
|
64
|
-
*/
|
|
65
|
-
export function classifyPaneStatus(pane) {
|
|
66
|
-
// Only look at the BOTTOM of the pane. Claude Code pins its live chrome — the spinner, the input box, the
|
|
67
|
-
// status line, and the background-agent block — to the last ~dozen rows; everything above is CONVERSATION
|
|
68
|
-
// SCROLLBACK. Restricting to the tail means a session whose scrollback happens to show these very marker
|
|
69
|
-
// strings (e.g. one editing roamcode's own source, or a diff mentioning "↓ … tokens" / "proceed") can't
|
|
70
|
-
// be misread — only claude's actual status chrome is classified.
|
|
71
|
-
const tail = pane.split("\n").slice(-22).join("\n");
|
|
72
|
-
// BLOCKED — an explicit decision prompt. Kept to Claude Code's exact permission/plan phrasings so the loud
|
|
73
|
-
// "needs you" it drives stays rare + trustworthy.
|
|
74
|
-
if (/\bDo you want to proceed\b/i.test(tail))
|
|
75
|
-
return "blocked";
|
|
76
|
-
if (/\bWould you like to proceed\b/i.test(tail))
|
|
77
|
-
return "blocked";
|
|
78
|
-
// WORKING — a LIVE token-flow counter "↓ <n>k tokens" is the reliable "actively generating" signal, present
|
|
79
|
-
// for BOTH the main spinner ("Schlepping… (1m 17s · ↓ 2.1k tokens)") and an active background agent
|
|
80
|
-
// ("⏺ general-purpose Listing f… 24m 23s · ↓ 216.5k tokens"). We require a GERUND ellipsis "…" on the SAME
|
|
81
|
-
// line so a FINISHED thing's past-tense summary ("Done · ↓ 12k tokens") can't read as working — an active
|
|
82
|
-
// worker always shows "<Gerund>… <timer> · ↓ tokens".
|
|
83
|
-
if (/…[^\n]*↓\s*[\d.]+\s*[kKmM]?\s*tokens?\b/.test(tail))
|
|
84
|
-
return "working";
|
|
85
|
-
// A spinner's parenthesised elapsed timer (covers the brief window at a turn's start before tokens flow).
|
|
86
|
-
if (/…\s*\(\s*\d+\s*[ms]\b/.test(tail))
|
|
87
|
-
return "working";
|
|
88
|
-
// Main loop blocked on a foreground agent/tool.
|
|
89
|
-
if (/\bWaiting for\b[\s\S]{0,80}?\bto finish\b/i.test(tail))
|
|
90
|
-
return "working";
|
|
91
|
-
// Interruptible generation (may be truncated to "e…" on a narrow phone pane, so it's a bonus, not the only tell).
|
|
92
|
-
if (/\besc to interrupt\b/i.test(tail))
|
|
93
|
-
return "working";
|
|
94
|
-
return "idle";
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Capture a tmux session's CURRENT pane as plain text (`capture-pane -p`, no escape sequences). READ-ONLY —
|
|
98
|
-
* it never sends input or resizes, so it can NEVER disturb a live session. Best-effort: resolves "" on any
|
|
99
|
-
* error/timeout and never throws. Async (non-blocking) so the activity monitor doesn't stall the event loop.
|
|
100
|
-
*/
|
|
101
|
-
export function capturePane(opts) {
|
|
102
|
-
return new Promise((resolve) => {
|
|
103
|
-
let out = "";
|
|
104
|
-
let done = false;
|
|
105
|
-
const finish = (s) => {
|
|
106
|
-
if (done)
|
|
107
|
-
return;
|
|
108
|
-
done = true;
|
|
109
|
-
resolve(s);
|
|
110
|
-
};
|
|
111
|
-
try {
|
|
112
|
-
const p = spawn(opts.tmuxBin ?? "tmux", ["-L", opts.socket, "capture-pane", "-p", "-t", opts.sessionName], {
|
|
113
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
114
|
-
});
|
|
115
|
-
p.stdout?.on("data", (d) => {
|
|
116
|
-
out += d.toString("utf8");
|
|
117
|
-
if (out.length > 65536) {
|
|
118
|
-
try {
|
|
119
|
-
p.kill();
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
/* already gone */
|
|
123
|
-
}
|
|
124
|
-
finish(out);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
p.on("error", () => finish("")); // tmux missing / spawn failed → treat as "no data"
|
|
128
|
-
p.on("close", () => finish(out));
|
|
129
|
-
const t = setTimeout(() => {
|
|
130
|
-
try {
|
|
131
|
-
p.kill();
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
/* already gone */
|
|
135
|
-
}
|
|
136
|
-
finish(out);
|
|
137
|
-
}, opts.timeoutMs ?? 2000);
|
|
138
|
-
if (typeof t.unref === "function")
|
|
139
|
-
t.unref();
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
finish("");
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { type SpawnOptions } from "node:child_process";
|
|
2
|
-
export interface ClaudeModelCatalogItem {
|
|
3
|
-
value: string;
|
|
4
|
-
displayName: string;
|
|
5
|
-
description?: string;
|
|
6
|
-
supportedEffortLevels: string[];
|
|
7
|
-
isDefault: boolean;
|
|
8
|
-
}
|
|
9
|
-
export interface ClaudeMetadataRunner {
|
|
10
|
-
run(): Promise<unknown>;
|
|
11
|
-
/** Claude 2.1.207+ no longer includes models in initialize; `/model` is the catalog fallback. */
|
|
12
|
-
runModelList?(): Promise<unknown>;
|
|
13
|
-
dispose?(): void | Promise<void>;
|
|
14
|
-
}
|
|
15
|
-
interface ClaudeMetadataChildProcess {
|
|
16
|
-
readonly stdin: {
|
|
17
|
-
end(chunk: string): unknown;
|
|
18
|
-
on(event: "error", listener: (error: Error) => void): unknown;
|
|
19
|
-
off(event: "error", listener: (error: Error) => void): unknown;
|
|
20
|
-
};
|
|
21
|
-
readonly stdout: NodeJS.ReadableStream;
|
|
22
|
-
readonly stderr: NodeJS.ReadableStream;
|
|
23
|
-
on(event: "error", listener: (error: Error) => void): this;
|
|
24
|
-
on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
|
25
|
-
off(event: "error", listener: (error: Error) => void): this;
|
|
26
|
-
off(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
|
27
|
-
kill(): boolean;
|
|
28
|
-
}
|
|
29
|
-
type ClaudeMetadataSpawnProcess = (command: string, args: readonly string[], options: SpawnOptions) => ClaudeMetadataChildProcess;
|
|
30
|
-
interface CreateClaudeMetadataRunnerOptions {
|
|
31
|
-
claudeBin: string;
|
|
32
|
-
cwd: string;
|
|
33
|
-
env: NodeJS.ProcessEnv;
|
|
34
|
-
timeoutMs?: number;
|
|
35
|
-
maxOutputBytes?: number;
|
|
36
|
-
/** Process boundary injected by lifecycle tests. */
|
|
37
|
-
spawnProcess?: ClaudeMetadataSpawnProcess;
|
|
38
|
-
/** `/model` process boundary injected by fallback tests. */
|
|
39
|
-
modelListRun?: () => Promise<unknown>;
|
|
40
|
-
}
|
|
41
|
-
export declare class ClaudeMetadataService {
|
|
42
|
-
private readonly runner;
|
|
43
|
-
private readonly now;
|
|
44
|
-
private readonly ttlMs;
|
|
45
|
-
private cache?;
|
|
46
|
-
private inFlight?;
|
|
47
|
-
private disposed;
|
|
48
|
-
constructor(runner: ClaudeMetadataRunner, options?: {
|
|
49
|
-
now?: () => number;
|
|
50
|
-
ttlMs?: number;
|
|
51
|
-
});
|
|
52
|
-
getModels(force?: boolean): Promise<ClaudeModelCatalogItem[]>;
|
|
53
|
-
validateModelSelection(model: string, effort?: string): Promise<void>;
|
|
54
|
-
dispose(): void | Promise<void>;
|
|
55
|
-
private loadModels;
|
|
56
|
-
}
|
|
57
|
-
export declare function createClaudeMetadataRunner(options: CreateClaudeMetadataRunnerOptions): ClaudeMetadataRunner;
|
|
58
|
-
export {};
|
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
import { execFile as nodeExecFile, spawn as nodeSpawn } from "node:child_process";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { StringDecoder } from "node:string_decoder";
|
|
4
|
-
import { ProviderError } from "./types.js";
|
|
5
|
-
const SAFE_VALUE = /^[A-Za-z0-9][A-Za-z0-9._:/\u005b\u005d-]*$/;
|
|
6
|
-
const MAX_MODELS = 64;
|
|
7
|
-
const MAX_EFFORTS = 32;
|
|
8
|
-
const MAX_TOKEN = 128;
|
|
9
|
-
const MAX_STREAM_OBJECTS = 256;
|
|
10
|
-
const CLAUDE_BASELINE_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
11
|
-
const DEFAULT_TTL_MS = 5 * 60_000;
|
|
12
|
-
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
13
|
-
const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
14
|
-
const CLAUDE_METADATA_ARGS = [
|
|
15
|
-
"--print",
|
|
16
|
-
"--input-format",
|
|
17
|
-
"stream-json",
|
|
18
|
-
"--output-format",
|
|
19
|
-
"stream-json",
|
|
20
|
-
"--verbose",
|
|
21
|
-
"--no-session-persistence",
|
|
22
|
-
"--permission-mode",
|
|
23
|
-
"plan",
|
|
24
|
-
];
|
|
25
|
-
function metadataUnavailable() {
|
|
26
|
-
const error = new Error("Claude model metadata is unavailable");
|
|
27
|
-
error.name = "ClaudeMetadataError";
|
|
28
|
-
return error;
|
|
29
|
-
}
|
|
30
|
-
function isRecord(value) {
|
|
31
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
32
|
-
}
|
|
33
|
-
function isSafeToken(value) {
|
|
34
|
-
return typeof value === "string" && value.length <= MAX_TOKEN && SAFE_VALUE.test(value);
|
|
35
|
-
}
|
|
36
|
-
function normalizeCatalog(envelope) {
|
|
37
|
-
if (!isRecord(envelope) || !isRecord(envelope.response) || !isRecord(envelope.response.response)) {
|
|
38
|
-
throw metadataUnavailable();
|
|
39
|
-
}
|
|
40
|
-
const models = envelope.response.response.models;
|
|
41
|
-
if (!Array.isArray(models) || models.length === 0 || models.length > MAX_MODELS) {
|
|
42
|
-
throw metadataUnavailable();
|
|
43
|
-
}
|
|
44
|
-
const seenModels = new Set();
|
|
45
|
-
return models.map((candidate) => {
|
|
46
|
-
if (!isRecord(candidate) || !isSafeToken(candidate.value) || seenModels.has(candidate.value)) {
|
|
47
|
-
throw metadataUnavailable();
|
|
48
|
-
}
|
|
49
|
-
seenModels.add(candidate.value);
|
|
50
|
-
if (typeof candidate.displayName !== "string" ||
|
|
51
|
-
candidate.displayName.length === 0 ||
|
|
52
|
-
candidate.displayName.length > 512 ||
|
|
53
|
-
(candidate.description !== undefined &&
|
|
54
|
-
(typeof candidate.description !== "string" || candidate.description.length > 4_096)) ||
|
|
55
|
-
!Array.isArray(candidate.supportedEffortLevels) ||
|
|
56
|
-
candidate.supportedEffortLevels.length > MAX_EFFORTS ||
|
|
57
|
-
typeof candidate.isDefault !== "boolean") {
|
|
58
|
-
throw metadataUnavailable();
|
|
59
|
-
}
|
|
60
|
-
const seenEfforts = new Set();
|
|
61
|
-
const supportedEffortLevels = candidate.supportedEffortLevels.map((effort) => {
|
|
62
|
-
if (!isSafeToken(effort) || seenEfforts.has(effort))
|
|
63
|
-
throw metadataUnavailable();
|
|
64
|
-
seenEfforts.add(effort);
|
|
65
|
-
return effort;
|
|
66
|
-
});
|
|
67
|
-
return {
|
|
68
|
-
value: candidate.value,
|
|
69
|
-
displayName: candidate.displayName,
|
|
70
|
-
...(candidate.description !== undefined ? { description: candidate.description } : {}),
|
|
71
|
-
supportedEffortLevels,
|
|
72
|
-
isDefault: candidate.isDefault,
|
|
73
|
-
};
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
const MODEL_LIST_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
77
|
-
const MODEL_DISPLAY_NAMES = {
|
|
78
|
-
sonnet: "Sonnet",
|
|
79
|
-
opus: "Opus",
|
|
80
|
-
haiku: "Haiku",
|
|
81
|
-
fable: "Fable",
|
|
82
|
-
best: "Best available",
|
|
83
|
-
opusplan: "Opus Plan",
|
|
84
|
-
};
|
|
85
|
-
/** Parse Claude's stable human-facing `/model` result, e.g.
|
|
86
|
-
* `Available: sonnet, opus, fable[1m], default, or a full model ID.` The provider-default option already
|
|
87
|
-
* exists as the picker's empty value, so the literal `default` alias is intentionally omitted here. */
|
|
88
|
-
function normalizeModelList(result) {
|
|
89
|
-
const text = typeof result === "string" ? result : isRecord(result) && typeof result.result === "string" ? result.result : "";
|
|
90
|
-
const available = /Available:\s*(.*?)\s*,?\s*or a full model ID\b/is.exec(text)?.[1];
|
|
91
|
-
if (!available)
|
|
92
|
-
throw metadataUnavailable();
|
|
93
|
-
const values = available
|
|
94
|
-
.split(",")
|
|
95
|
-
.map((value) => value.trim())
|
|
96
|
-
.filter((value) => value.length > 0 && value !== "default");
|
|
97
|
-
if (values.length === 0 || values.length > MAX_MODELS)
|
|
98
|
-
throw metadataUnavailable();
|
|
99
|
-
const seen = new Set();
|
|
100
|
-
return values.map((value) => {
|
|
101
|
-
if (!isSafeToken(value) || seen.has(value))
|
|
102
|
-
throw metadataUnavailable();
|
|
103
|
-
seen.add(value);
|
|
104
|
-
const context = /\[1m\]$/i.test(value);
|
|
105
|
-
const base = value.replace(/\[1m\]$/i, "");
|
|
106
|
-
const displayName = `${MODEL_DISPLAY_NAMES[base] ?? base}${context ? " · 1M context" : ""}`;
|
|
107
|
-
return {
|
|
108
|
-
value,
|
|
109
|
-
displayName,
|
|
110
|
-
supportedEffortLevels: [...MODEL_LIST_EFFORTS],
|
|
111
|
-
isDefault: false,
|
|
112
|
-
};
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
function cloneCatalog(models) {
|
|
116
|
-
return models.map((model) => ({ ...model, supportedEffortLevels: [...model.supportedEffortLevels] }));
|
|
117
|
-
}
|
|
118
|
-
export class ClaudeMetadataService {
|
|
119
|
-
runner;
|
|
120
|
-
now;
|
|
121
|
-
ttlMs;
|
|
122
|
-
cache;
|
|
123
|
-
inFlight;
|
|
124
|
-
disposed = false;
|
|
125
|
-
constructor(runner, options = {}) {
|
|
126
|
-
this.runner = runner;
|
|
127
|
-
this.now = options.now ?? (() => Date.now());
|
|
128
|
-
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
129
|
-
}
|
|
130
|
-
async getModels(force = false) {
|
|
131
|
-
if (!force && this.cache && this.now() - this.cache.at < this.ttlMs) {
|
|
132
|
-
return cloneCatalog(this.cache.models);
|
|
133
|
-
}
|
|
134
|
-
if (this.inFlight)
|
|
135
|
-
return cloneCatalog(await this.inFlight);
|
|
136
|
-
const request = this.loadModels();
|
|
137
|
-
this.inFlight = request;
|
|
138
|
-
try {
|
|
139
|
-
const models = await request;
|
|
140
|
-
this.cache = { at: this.now(), models: cloneCatalog(models) };
|
|
141
|
-
return cloneCatalog(models);
|
|
142
|
-
}
|
|
143
|
-
finally {
|
|
144
|
-
if (this.inFlight === request)
|
|
145
|
-
this.inFlight = undefined;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
async validateModelSelection(model, effort) {
|
|
149
|
-
const models = await this.getModels();
|
|
150
|
-
const selected = models.find((candidate) => candidate.value === model);
|
|
151
|
-
if (!selected) {
|
|
152
|
-
if (effort === undefined || CLAUDE_BASELINE_EFFORTS.has(effort))
|
|
153
|
-
return;
|
|
154
|
-
throw new ProviderError("INVALID_PROVIDER_OPTIONS", "Invalid Claude custom model effort selection");
|
|
155
|
-
}
|
|
156
|
-
if (effort === undefined || selected.supportedEffortLevels.includes(effort))
|
|
157
|
-
return;
|
|
158
|
-
throw new ProviderError("INVALID_PROVIDER_OPTIONS", "Invalid Claude model and effort selection");
|
|
159
|
-
}
|
|
160
|
-
dispose() {
|
|
161
|
-
if (this.disposed)
|
|
162
|
-
return;
|
|
163
|
-
this.disposed = true;
|
|
164
|
-
return this.runner.dispose?.();
|
|
165
|
-
}
|
|
166
|
-
async loadModels() {
|
|
167
|
-
try {
|
|
168
|
-
return normalizeCatalog(await this.runner.run());
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
if (!this.runner.runModelList)
|
|
172
|
-
throw metadataUnavailable();
|
|
173
|
-
try {
|
|
174
|
-
return normalizeModelList(await this.runner.runModelList());
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
throw metadataUnavailable();
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
export function createClaudeMetadataRunner(options) {
|
|
183
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
184
|
-
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
185
|
-
const spawnProcess = options.spawnProcess ?? nodeSpawn;
|
|
186
|
-
const activeRuns = new Set();
|
|
187
|
-
let disposed = false;
|
|
188
|
-
return {
|
|
189
|
-
runModelList() {
|
|
190
|
-
if (disposed)
|
|
191
|
-
return Promise.reject(metadataUnavailable());
|
|
192
|
-
if (options.modelListRun)
|
|
193
|
-
return options.modelListRun();
|
|
194
|
-
const env = { ...options.env };
|
|
195
|
-
delete env.ANTHROPIC_API_KEY;
|
|
196
|
-
return new Promise((resolve, reject) => {
|
|
197
|
-
nodeExecFile(options.claudeBin, ["-p", "/model", "--output-format", "json", "--dangerously-skip-permissions"], {
|
|
198
|
-
cwd: options.cwd,
|
|
199
|
-
env,
|
|
200
|
-
timeout: timeoutMs,
|
|
201
|
-
maxBuffer: maxOutputBytes,
|
|
202
|
-
windowsHide: true,
|
|
203
|
-
}, (error, stdout) => {
|
|
204
|
-
if (error) {
|
|
205
|
-
reject(metadataUnavailable());
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
try {
|
|
209
|
-
const parsed = JSON.parse(stdout);
|
|
210
|
-
if (typeof parsed.result !== "string")
|
|
211
|
-
throw metadataUnavailable();
|
|
212
|
-
resolve(parsed.result);
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
reject(metadataUnavailable());
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
},
|
|
220
|
-
run() {
|
|
221
|
-
if (disposed)
|
|
222
|
-
return Promise.reject(metadataUnavailable());
|
|
223
|
-
return new Promise((resolve, reject) => {
|
|
224
|
-
const env = { ...options.env };
|
|
225
|
-
delete env.ANTHROPIC_API_KEY;
|
|
226
|
-
let child;
|
|
227
|
-
try {
|
|
228
|
-
child = spawnProcess(options.claudeBin, CLAUDE_METADATA_ARGS, {
|
|
229
|
-
cwd: options.cwd,
|
|
230
|
-
env,
|
|
231
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
reject(metadataUnavailable());
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
const requestId = `roamcode-models-${randomUUID()}`;
|
|
239
|
-
const decoder = new StringDecoder("utf8");
|
|
240
|
-
let stdoutBuffer = "";
|
|
241
|
-
let outputBytes = 0;
|
|
242
|
-
let streamObjects = 0;
|
|
243
|
-
let settled = false;
|
|
244
|
-
let timer = undefined;
|
|
245
|
-
const cleanup = () => {
|
|
246
|
-
if (timer !== undefined)
|
|
247
|
-
clearTimeout(timer);
|
|
248
|
-
child.stdout.off("data", onStdout);
|
|
249
|
-
child.stderr.off("data", onStderr);
|
|
250
|
-
child.stdin.off("error", onStdinError);
|
|
251
|
-
child.off("error", onError);
|
|
252
|
-
child.off("exit", onExit);
|
|
253
|
-
activeRuns.delete(cancel);
|
|
254
|
-
try {
|
|
255
|
-
child.kill();
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
// Cleanup is best-effort and must not replace the original settlement.
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
const settle = (value) => {
|
|
262
|
-
if (settled)
|
|
263
|
-
return;
|
|
264
|
-
settled = true;
|
|
265
|
-
cleanup();
|
|
266
|
-
if (value === undefined)
|
|
267
|
-
reject(metadataUnavailable());
|
|
268
|
-
else
|
|
269
|
-
resolve(value);
|
|
270
|
-
};
|
|
271
|
-
const addBytes = (chunk) => {
|
|
272
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
273
|
-
outputBytes += buffer.byteLength;
|
|
274
|
-
if (outputBytes > maxOutputBytes) {
|
|
275
|
-
settle();
|
|
276
|
-
return undefined;
|
|
277
|
-
}
|
|
278
|
-
return buffer;
|
|
279
|
-
};
|
|
280
|
-
const onStdout = (chunk) => {
|
|
281
|
-
const buffer = addBytes(chunk);
|
|
282
|
-
if (!buffer || settled)
|
|
283
|
-
return;
|
|
284
|
-
stdoutBuffer += decoder.write(buffer);
|
|
285
|
-
while (!settled) {
|
|
286
|
-
const newline = stdoutBuffer.indexOf("\n");
|
|
287
|
-
if (newline < 0)
|
|
288
|
-
return;
|
|
289
|
-
const line = stdoutBuffer.slice(0, newline).trim();
|
|
290
|
-
stdoutBuffer = stdoutBuffer.slice(newline + 1);
|
|
291
|
-
if (line.length === 0)
|
|
292
|
-
continue;
|
|
293
|
-
streamObjects += 1;
|
|
294
|
-
if (streamObjects > MAX_STREAM_OBJECTS) {
|
|
295
|
-
settle();
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
let message;
|
|
299
|
-
try {
|
|
300
|
-
message = JSON.parse(line);
|
|
301
|
-
}
|
|
302
|
-
catch {
|
|
303
|
-
settle();
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (isRecord(message) &&
|
|
307
|
-
message.type === "control_response" &&
|
|
308
|
-
isRecord(message.response) &&
|
|
309
|
-
message.response.request_id === requestId) {
|
|
310
|
-
settle(message);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
const onStderr = (chunk) => {
|
|
315
|
-
addBytes(chunk);
|
|
316
|
-
};
|
|
317
|
-
const onStdinError = () => settle();
|
|
318
|
-
const onError = () => settle();
|
|
319
|
-
const onExit = () => settle();
|
|
320
|
-
const cancel = () => settle();
|
|
321
|
-
child.stdout.on("data", onStdout);
|
|
322
|
-
child.stderr.on("data", onStderr);
|
|
323
|
-
child.stdin.on("error", onStdinError);
|
|
324
|
-
child.on("error", onError);
|
|
325
|
-
child.on("exit", onExit);
|
|
326
|
-
activeRuns.add(cancel);
|
|
327
|
-
timer = setTimeout(cancel, timeoutMs);
|
|
328
|
-
const request = {
|
|
329
|
-
type: "control_request",
|
|
330
|
-
request_id: requestId,
|
|
331
|
-
request: {
|
|
332
|
-
subtype: "initialize",
|
|
333
|
-
hooks: { PreToolUse: [{ matcher: "", hookCallbackIds: ["roamcode-metadata"] }] },
|
|
334
|
-
},
|
|
335
|
-
};
|
|
336
|
-
try {
|
|
337
|
-
child.stdin.end(`${JSON.stringify(request)}\n`);
|
|
338
|
-
}
|
|
339
|
-
catch {
|
|
340
|
-
settle();
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
|
-
},
|
|
344
|
-
dispose() {
|
|
345
|
-
if (disposed)
|
|
346
|
-
return;
|
|
347
|
-
disposed = true;
|
|
348
|
-
for (const cancel of [...activeRuns])
|
|
349
|
-
cancel();
|
|
350
|
-
},
|
|
351
|
-
};
|
|
352
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { type AttachSpawnOptions } from "../config.js";
|
|
2
|
-
import { type AgentProvider } from "./types.js";
|
|
3
|
-
import type { ProviderAvailability } from "./types.js";
|
|
4
|
-
export interface CreateClaudeProviderOptions {
|
|
5
|
-
claudeBin: string;
|
|
6
|
-
env?: NodeJS.ProcessEnv;
|
|
7
|
-
attach?: AttachSpawnOptions;
|
|
8
|
-
getAttach?: () => AttachSpawnOptions | undefined;
|
|
9
|
-
probe?: () => Promise<ProviderAvailability>;
|
|
10
|
-
}
|
|
11
|
-
export declare function createClaudeProvider(options: CreateClaudeProviderOptions): AgentProvider;
|