@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.
Files changed (99) hide show
  1. package/dist/index.d.ts +2406 -82
  2. package/dist/index.js +8314 -44
  3. package/dist/managed-update-helper.js +621 -26
  4. package/dist/mcp-send.js +76 -90
  5. package/dist/start.d.ts +591 -9
  6. package/dist/start.js +7629 -353
  7. package/package.json +3 -3
  8. package/dist/auth.d.ts +0 -63
  9. package/dist/auth.js +0 -133
  10. package/dist/claude-auth-service.d.ts +0 -76
  11. package/dist/claude-auth-service.js +0 -217
  12. package/dist/claude-latest-service.d.ts +0 -34
  13. package/dist/claude-latest-service.js +0 -61
  14. package/dist/config.d.ts +0 -78
  15. package/dist/config.js +0 -59
  16. package/dist/data-dir.d.ts +0 -42
  17. package/dist/data-dir.js +0 -70
  18. package/dist/diag.d.ts +0 -43
  19. package/dist/diag.js +0 -83
  20. package/dist/fs-service.d.ts +0 -90
  21. package/dist/fs-service.js +0 -290
  22. package/dist/managed-runtime.d.ts +0 -51
  23. package/dist/managed-runtime.js +0 -411
  24. package/dist/managed-update-helper.d.ts +0 -2
  25. package/dist/mcp-send.d.ts +0 -33
  26. package/dist/origin-check.d.ts +0 -37
  27. package/dist/origin-check.js +0 -101
  28. package/dist/pane-status.d.ts +0 -61
  29. package/dist/pane-status.js +0 -145
  30. package/dist/providers/claude-metadata-service.d.ts +0 -58
  31. package/dist/providers/claude-metadata-service.js +0 -352
  32. package/dist/providers/claude-provider.d.ts +0 -11
  33. package/dist/providers/claude-provider.js +0 -166
  34. package/dist/providers/codex-activity.d.ts +0 -21
  35. package/dist/providers/codex-activity.js +0 -122
  36. package/dist/providers/codex-app-server-client.d.ts +0 -90
  37. package/dist/providers/codex-app-server-client.js +0 -485
  38. package/dist/providers/codex-latest-service.d.ts +0 -50
  39. package/dist/providers/codex-latest-service.js +0 -174
  40. package/dist/providers/codex-metadata-service.d.ts +0 -161
  41. package/dist/providers/codex-metadata-service.js +0 -686
  42. package/dist/providers/codex-profile-client.d.ts +0 -16
  43. package/dist/providers/codex-profile-client.js +0 -52
  44. package/dist/providers/codex-profile-security.d.ts +0 -23
  45. package/dist/providers/codex-profile-security.js +0 -161
  46. package/dist/providers/codex-provider.d.ts +0 -15
  47. package/dist/providers/codex-provider.js +0 -174
  48. package/dist/providers/codex-thread-coordinator.d.ts +0 -18
  49. package/dist/providers/codex-thread-coordinator.js +0 -93
  50. package/dist/providers/codex-thread-persistence.d.ts +0 -9
  51. package/dist/providers/codex-thread-persistence.js +0 -45
  52. package/dist/providers/codex-thread-resolver.d.ts +0 -59
  53. package/dist/providers/codex-thread-resolver.js +0 -322
  54. package/dist/providers/options.d.ts +0 -7
  55. package/dist/providers/options.js +0 -155
  56. package/dist/providers/provider-artifacts.d.ts +0 -3
  57. package/dist/providers/provider-artifacts.js +0 -30
  58. package/dist/providers/registry.d.ts +0 -7
  59. package/dist/providers/registry.js +0 -23
  60. package/dist/providers/types.d.ts +0 -95
  61. package/dist/providers/types.js +0 -8
  62. package/dist/push-dispatch.d.ts +0 -81
  63. package/dist/push-dispatch.js +0 -100
  64. package/dist/push-store.d.ts +0 -25
  65. package/dist/push-store.js +0 -79
  66. package/dist/rate-limit.d.ts +0 -52
  67. package/dist/rate-limit.js +0 -72
  68. package/dist/server-config.d.ts +0 -60
  69. package/dist/server-config.js +0 -77
  70. package/dist/service-install.d.ts +0 -60
  71. package/dist/service-install.js +0 -221
  72. package/dist/session-defaults.d.ts +0 -26
  73. package/dist/session-defaults.js +0 -60
  74. package/dist/session-store.d.ts +0 -81
  75. package/dist/session-store.js +0 -654
  76. package/dist/static-routes.d.ts +0 -101
  77. package/dist/static-routes.js +0 -188
  78. package/dist/terminal-capability.d.ts +0 -5
  79. package/dist/terminal-capability.js +0 -27
  80. package/dist/terminal-manager.d.ts +0 -224
  81. package/dist/terminal-manager.js +0 -917
  82. package/dist/terminal-process.d.ts +0 -85
  83. package/dist/terminal-process.js +0 -238
  84. package/dist/terminal-shared.d.ts +0 -36
  85. package/dist/terminal-shared.js +0 -43
  86. package/dist/tmux-list.d.ts +0 -11
  87. package/dist/tmux-list.js +0 -39
  88. package/dist/transport.d.ts +0 -123
  89. package/dist/transport.js +0 -1559
  90. package/dist/updater.d.ts +0 -161
  91. package/dist/updater.js +0 -451
  92. package/dist/usage-service.d.ts +0 -118
  93. package/dist/usage-service.js +0 -173
  94. package/dist/vapid.d.ts +0 -17
  95. package/dist/vapid.js +0 -31
  96. package/dist/web-push-send.d.ts +0 -20
  97. package/dist/web-push-send.js +0 -21
  98. package/dist/ws-ticket.d.ts +0 -47
  99. package/dist/ws-ticket.js +0 -62
@@ -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;