@roamcode.ai/server 1.0.0

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 (100) hide show
  1. package/LICENSE +21 -0
  2. package/dist/auth.d.ts +63 -0
  3. package/dist/auth.js +133 -0
  4. package/dist/claude-auth-service.d.ts +76 -0
  5. package/dist/claude-auth-service.js +217 -0
  6. package/dist/claude-latest-service.d.ts +34 -0
  7. package/dist/claude-latest-service.js +61 -0
  8. package/dist/config.d.ts +78 -0
  9. package/dist/config.js +59 -0
  10. package/dist/data-dir.d.ts +42 -0
  11. package/dist/data-dir.js +70 -0
  12. package/dist/diag.d.ts +43 -0
  13. package/dist/diag.js +83 -0
  14. package/dist/fs-service.d.ts +90 -0
  15. package/dist/fs-service.js +290 -0
  16. package/dist/index.d.ts +82 -0
  17. package/dist/index.js +44 -0
  18. package/dist/managed-runtime.d.ts +51 -0
  19. package/dist/managed-runtime.js +411 -0
  20. package/dist/managed-update-helper.d.ts +2 -0
  21. package/dist/managed-update-helper.js +34 -0
  22. package/dist/mcp-send.d.ts +33 -0
  23. package/dist/mcp-send.js +107 -0
  24. package/dist/origin-check.d.ts +37 -0
  25. package/dist/origin-check.js +101 -0
  26. package/dist/pane-status.d.ts +61 -0
  27. package/dist/pane-status.js +145 -0
  28. package/dist/providers/claude-metadata-service.d.ts +58 -0
  29. package/dist/providers/claude-metadata-service.js +352 -0
  30. package/dist/providers/claude-provider.d.ts +11 -0
  31. package/dist/providers/claude-provider.js +166 -0
  32. package/dist/providers/codex-activity.d.ts +21 -0
  33. package/dist/providers/codex-activity.js +122 -0
  34. package/dist/providers/codex-app-server-client.d.ts +90 -0
  35. package/dist/providers/codex-app-server-client.js +485 -0
  36. package/dist/providers/codex-latest-service.d.ts +50 -0
  37. package/dist/providers/codex-latest-service.js +174 -0
  38. package/dist/providers/codex-metadata-service.d.ts +161 -0
  39. package/dist/providers/codex-metadata-service.js +686 -0
  40. package/dist/providers/codex-profile-client.d.ts +16 -0
  41. package/dist/providers/codex-profile-client.js +52 -0
  42. package/dist/providers/codex-profile-security.d.ts +23 -0
  43. package/dist/providers/codex-profile-security.js +161 -0
  44. package/dist/providers/codex-provider.d.ts +15 -0
  45. package/dist/providers/codex-provider.js +174 -0
  46. package/dist/providers/codex-thread-coordinator.d.ts +18 -0
  47. package/dist/providers/codex-thread-coordinator.js +93 -0
  48. package/dist/providers/codex-thread-persistence.d.ts +9 -0
  49. package/dist/providers/codex-thread-persistence.js +45 -0
  50. package/dist/providers/codex-thread-resolver.d.ts +59 -0
  51. package/dist/providers/codex-thread-resolver.js +322 -0
  52. package/dist/providers/options.d.ts +7 -0
  53. package/dist/providers/options.js +155 -0
  54. package/dist/providers/provider-artifacts.d.ts +3 -0
  55. package/dist/providers/provider-artifacts.js +30 -0
  56. package/dist/providers/registry.d.ts +7 -0
  57. package/dist/providers/registry.js +23 -0
  58. package/dist/providers/types.d.ts +95 -0
  59. package/dist/providers/types.js +8 -0
  60. package/dist/push-dispatch.d.ts +81 -0
  61. package/dist/push-dispatch.js +100 -0
  62. package/dist/push-store.d.ts +25 -0
  63. package/dist/push-store.js +79 -0
  64. package/dist/rate-limit.d.ts +52 -0
  65. package/dist/rate-limit.js +72 -0
  66. package/dist/server-config.d.ts +60 -0
  67. package/dist/server-config.js +77 -0
  68. package/dist/service-install.d.ts +60 -0
  69. package/dist/service-install.js +221 -0
  70. package/dist/session-defaults.d.ts +26 -0
  71. package/dist/session-defaults.js +60 -0
  72. package/dist/session-store.d.ts +81 -0
  73. package/dist/session-store.js +654 -0
  74. package/dist/start.d.ts +31 -0
  75. package/dist/start.js +372 -0
  76. package/dist/static-routes.d.ts +101 -0
  77. package/dist/static-routes.js +188 -0
  78. package/dist/terminal-capability.d.ts +5 -0
  79. package/dist/terminal-capability.js +27 -0
  80. package/dist/terminal-manager.d.ts +224 -0
  81. package/dist/terminal-manager.js +917 -0
  82. package/dist/terminal-process.d.ts +85 -0
  83. package/dist/terminal-process.js +238 -0
  84. package/dist/terminal-shared.d.ts +36 -0
  85. package/dist/terminal-shared.js +43 -0
  86. package/dist/tmux-list.d.ts +11 -0
  87. package/dist/tmux-list.js +39 -0
  88. package/dist/transport.d.ts +123 -0
  89. package/dist/transport.js +1559 -0
  90. package/dist/updater.d.ts +161 -0
  91. package/dist/updater.js +451 -0
  92. package/dist/usage-service.d.ts +118 -0
  93. package/dist/usage-service.js +173 -0
  94. package/dist/vapid.d.ts +17 -0
  95. package/dist/vapid.js +31 -0
  96. package/dist/web-push-send.d.ts +20 -0
  97. package/dist/web-push-send.js +21 -0
  98. package/dist/ws-ticket.d.ts +47 -0
  99. package/dist/ws-ticket.js +62 -0
  100. package/package.json +55 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Origin / CSWSH (cross-site WebSocket hijacking) guard for the WS upgrade and state-changing HTTP.
3
+ *
4
+ * The token is the only auth boundary, and it can leak into a URL (`?token=` connect link, push deep
5
+ * link, browser history). The WS handshake (and any browser-driven HTTP) carries an `Origin` header the
6
+ * page CANNOT forge — so a malicious cross-origin page that somehow holds the token still can't make the
7
+ * BROWSER attach to our WS / drive our API, because the browser stamps its OWN origin on the request and
8
+ * we reject it. Without this check, a leaked-in-URL token is enough for a foreign page to puppet the host.
9
+ *
10
+ * SAFE DEFAULT (must never reject the real app): a request is allowed when
11
+ * - the `Origin` header is ABSENT (native/non-browser clients, same-origin navigations + most
12
+ * same-origin GETs omit it — the PWA's own fetches are same-origin), OR
13
+ * - the Origin's host:port equals the request `Host` (same-origin — the PWA always is), OR
14
+ * - the Origin matches the configured public URL (ROAMCODE_PUBLIC_URL), OR
15
+ * - the Origin is a loopback/localhost origin (local dev), OR
16
+ * - the Origin is in the explicit ROAMCODE_ALLOWED_ORIGINS allow-list.
17
+ * Only a PRESENT, cross-origin, non-allow-listed Origin is rejected (403).
18
+ */
19
+ /** Normalize a URL/origin string to a comparable `scheme://host[:port]` origin (lowercased). Returns
20
+ * undefined for anything that isn't a parseable absolute URL (so it can never match by accident). */
21
+ export function normalizeOrigin(value) {
22
+ if (!value)
23
+ return undefined;
24
+ const trimmed = value.trim();
25
+ if (!trimmed || trimmed === "null")
26
+ return undefined; // a sandboxed/opaque origin serializes to "null"
27
+ try {
28
+ const u = new URL(trimmed);
29
+ // `URL.origin` is `scheme://host[:port]` with the default port elided — exactly the comparison we want.
30
+ return u.origin.toLowerCase();
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ }
36
+ /** True if a host (no scheme) is a loopback/localhost name. Accepts a bare host or host:port. */
37
+ function isLoopbackHost(host) {
38
+ const h = host.toLowerCase().replace(/:\d+$/, ""); // drop a :port suffix
39
+ if (h === "localhost" || h === "::1" || h === "[::1]")
40
+ return true;
41
+ return /^127(?:\.\d{1,3}){3}$/.test(h);
42
+ }
43
+ /** True if a normalized origin (`scheme://host[:port]`) is loopback. */
44
+ function isLoopbackOrigin(origin) {
45
+ try {
46
+ return isLoopbackHost(new URL(origin).host);
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ /**
53
+ * Decide whether a request's Origin is allowed. PURE / I/O-free so it is unit-testable; the transport
54
+ * preHandler passes `origin` (the header) and `host` (the request Host header).
55
+ *
56
+ * @param origin the request's `Origin` header (may be undefined)
57
+ * @param host the request's `Host` header (e.g. `remotecode.example.com` or `127.0.0.1:4280`)
58
+ */
59
+ export function isOriginAllowed(origin, host, opts = {}) {
60
+ // Absent Origin → allow (native clients, same-origin navigations + most same-origin GETs omit it). A
61
+ // present-but-unparseable / opaque ("null") Origin normalizes to undefined and is treated the same way
62
+ // (it cannot be a real cross-origin browser page driving us — those send a concrete origin).
63
+ const reqOrigin = normalizeOrigin(origin);
64
+ if (reqOrigin === undefined)
65
+ return true;
66
+ // Same-origin: the Origin's host:port equals the request Host. The PWA (served same-origin) always is.
67
+ if (host) {
68
+ const sameOriginHost = (() => {
69
+ try {
70
+ return new URL(reqOrigin).host.toLowerCase() === host.trim().toLowerCase();
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ })();
76
+ if (sameOriginHost)
77
+ return true;
78
+ }
79
+ // Loopback origin (local dev directly against 127.0.0.1/localhost).
80
+ if (isLoopbackOrigin(reqOrigin))
81
+ return true;
82
+ // Configured public URL (the tunnel/user-facing origin the PWA is actually installed under).
83
+ const publicOrigin = normalizeOrigin(opts.publicUrl);
84
+ if (publicOrigin !== undefined && reqOrigin === publicOrigin)
85
+ return true;
86
+ // Explicit allow-list extension.
87
+ for (const extra of opts.allowedOrigins ?? []) {
88
+ if (normalizeOrigin(extra) === reqOrigin)
89
+ return true;
90
+ }
91
+ return false;
92
+ }
93
+ /** Parse a comma-separated ROAMCODE_ALLOWED_ORIGINS value into a trimmed, non-empty list. */
94
+ export function parseAllowedOrigins(raw) {
95
+ if (!raw)
96
+ return [];
97
+ return raw
98
+ .split(",")
99
+ .map((s) => s.trim())
100
+ .filter((s) => s.length > 0);
101
+ }
@@ -0,0 +1,61 @@
1
+ export type PaneStatus = "working" | "blocked" | "idle";
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 declare 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 declare function isNewerMajorMinor(current: string, testedUpTo: string): boolean;
20
+ /**
21
+ * The one-line boot warning when the RUNNING claude is newer than the classifier was verified against —
22
+ * or undefined when there is nothing to warn about (older/equal/unknown version). Pure so it's testable;
23
+ * start.ts logs it once via console.warn and NEVER throws (a version bump must not affect boot).
24
+ */
25
+ export declare function classifierVersionWarning(claudeVersion: string | undefined): string | undefined;
26
+ /**
27
+ * Classify a session's live ACTIVITY from its RENDERED tmux pane (`capture-pane -p` — the CURRENT screen, not
28
+ * scrollback). UNIVERSAL: works for any running session regardless of how claude was spawned (no per-session
29
+ * hooks needed), and works while the browser is DETACHED (it reads the tmux session directly). Grounded in
30
+ * Claude Code's real output — see pane-status.test.ts for captured samples.
31
+ *
32
+ * working → something is actively generating. The strongest tell is a LIVE token-flow counter "↓ 216.5k
33
+ * tokens" — it appears on the MAIN spinner ("Schlepping… (1m 17s · ↓ 2.1k tokens)") AND on an
34
+ * ACTIVE background agent ("⏺ general-purpose Listing f… 24m 23s · ↓ 216.5k tokens"), and is GONE
35
+ * the moment a turn finishes ("Baked for 23m 15s"). So a session whose MAIN loop is at the prompt
36
+ * but has agents still developing in the background reads "working" — NOT idle (the user's
37
+ * explicit correction). Also: a spinner's parenthesised timer, "Waiting for … to finish", or
38
+ * "esc to interrupt".
39
+ * blocked → claude is WAITING ON A DECISION FROM YOU: a permission prompt ("Do you want to proceed?"), a
40
+ * plan-mode approval ("Would you like to proceed?"). This is the ONLY state that earns the loud
41
+ * "needs you" — so it must stay RARE + real; that's why it's just the explicit-prompt phrasings.
42
+ * idle → none of the above: claude finished a turn and is sitting at an empty prompt with nothing running
43
+ * and nothing to decide. A calm "your turn whenever" — NOT a loud alert.
44
+ *
45
+ * Order matters: blocked is checked first (a decision prompt overrides any stale spinner text), then working,
46
+ * else idle.
47
+ */
48
+ export declare function classifyPaneStatus(pane: string): PaneStatus;
49
+ /** How capturePane locates a session's tmux pane. */
50
+ export interface CaptureOptions {
51
+ tmuxBin?: string;
52
+ socket: string;
53
+ sessionName: string;
54
+ timeoutMs?: number;
55
+ }
56
+ /**
57
+ * Capture a tmux session's CURRENT pane as plain text (`capture-pane -p`, no escape sequences). READ-ONLY —
58
+ * it never sends input or resizes, so it can NEVER disturb a live session. Best-effort: resolves "" on any
59
+ * error/timeout and never throws. Async (non-blocking) so the activity monitor doesn't stall the event loop.
60
+ */
61
+ export declare function capturePane(opts: CaptureOptions): Promise<string>;
@@ -0,0 +1,145 @@
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
+ }
@@ -0,0 +1,58 @@
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 {};