@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,352 @@
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
+ }
@@ -0,0 +1,11 @@
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;
@@ -0,0 +1,166 @@
1
+ import { buildHooksSettingsDocument, buildMcpConfigDocument, hookAuthFileContent, hookAuthPathFor, hooksSettingsPathFor, mcpConfigPathFor, } from "../config.js";
2
+ import { classifyPaneStatus } from "../pane-status.js";
3
+ import { cleanupProviderArtifacts, writeProviderArtifact0600 } from "./provider-artifacts.js";
4
+ import { ProviderError } from "./types.js";
5
+ const OWNED_VALUE_FLAGS = new Map([
6
+ ["--resume", "optional"],
7
+ ["--session-id", "one"],
8
+ ["--model", "one"],
9
+ ["--effort", "one"],
10
+ ["--permission-mode", "one"],
11
+ ["--add-dir", "variadic"],
12
+ ["--mcp-config", "variadic"],
13
+ ["--settings", "one"],
14
+ ]);
15
+ const OWNED_BOOLEAN_FLAGS = ["--continue", "--dangerously-skip-permissions"];
16
+ function invalidLegacyArg(message) {
17
+ return new ProviderError("INVALID_PROVIDER_OPTIONS", `Invalid provider options: ${message}`);
18
+ }
19
+ function requireLegacyValue(args, index, flag) {
20
+ const value = args[index + 1];
21
+ if (value === undefined || value.startsWith("-"))
22
+ throw invalidLegacyArg(`${flag} requires a value`);
23
+ return index + 1;
24
+ }
25
+ function sanitizeLegacyArgs(legacyArgs) {
26
+ const preserved = [];
27
+ for (let index = 0; index < legacyArgs.length; index += 1) {
28
+ const arg = legacyArgs[index];
29
+ if (arg === "--") {
30
+ preserved.push(...legacyArgs.slice(index));
31
+ break;
32
+ }
33
+ if (arg === "-c")
34
+ continue;
35
+ if (arg.startsWith("-c")) {
36
+ throw invalidLegacyArg("-c has an ambiguous attached value");
37
+ }
38
+ if (arg === "-r") {
39
+ const value = legacyArgs[index + 1];
40
+ if (value !== undefined && !value.startsWith("-"))
41
+ index += 1;
42
+ continue;
43
+ }
44
+ if (arg.startsWith("-r")) {
45
+ if (arg === "-r=" || arg.length === 2)
46
+ throw invalidLegacyArg("-r has an ambiguous empty value");
47
+ continue;
48
+ }
49
+ let handled = false;
50
+ for (const flag of OWNED_BOOLEAN_FLAGS) {
51
+ if (arg === flag) {
52
+ handled = true;
53
+ break;
54
+ }
55
+ if (arg.startsWith(`${flag}=`)) {
56
+ if (arg.length === flag.length + 1)
57
+ throw invalidLegacyArg(`${flag} has an ambiguous empty value`);
58
+ handled = true;
59
+ break;
60
+ }
61
+ }
62
+ if (handled)
63
+ continue;
64
+ for (const [flag, arity] of OWNED_VALUE_FLAGS) {
65
+ if (arg.startsWith(`${flag}=`)) {
66
+ if (arg.length === flag.length + 1) {
67
+ const detail = arity === "optional" ? "has an ambiguous empty value" : "requires a value";
68
+ throw invalidLegacyArg(`${flag} ${detail}`);
69
+ }
70
+ handled = true;
71
+ break;
72
+ }
73
+ if (arg !== flag)
74
+ continue;
75
+ handled = true;
76
+ if (arity === "optional") {
77
+ const value = legacyArgs[index + 1];
78
+ if (value !== undefined && !value.startsWith("-"))
79
+ index += 1;
80
+ }
81
+ else {
82
+ index = requireLegacyValue(legacyArgs, index, flag);
83
+ if (arity === "variadic") {
84
+ while (legacyArgs[index + 1] !== undefined && !legacyArgs[index + 1].startsWith("-"))
85
+ index += 1;
86
+ }
87
+ }
88
+ break;
89
+ }
90
+ if (!handled)
91
+ preserved.push(arg);
92
+ }
93
+ return preserved;
94
+ }
95
+ function insertBeforeSeparator(args, ownedArgs) {
96
+ const separatorIndex = args.indexOf("--");
97
+ args.splice(separatorIndex === -1 ? args.length : separatorIndex, 0, ...ownedArgs);
98
+ }
99
+ function claudeArgs(options) {
100
+ const args = [];
101
+ if (options.model)
102
+ args.push("--model", options.model);
103
+ if (options.effort)
104
+ args.push("--effort", options.effort);
105
+ if (options.dangerouslySkip)
106
+ args.push("--dangerously-skip-permissions");
107
+ else if (options.permissionMode)
108
+ args.push("--permission-mode", options.permissionMode);
109
+ for (const dir of options.addDirs ?? [])
110
+ args.push("--add-dir", dir);
111
+ // Legacy arguments exist only for rollback-readable Claude rows. Provider-owned model, safety, session,
112
+ // and attachment flags are rebuilt from typed options so historical argv cannot override adapter policy.
113
+ args.push(...sanitizeLegacyArgs(options.legacyArgs ?? []));
114
+ return args;
115
+ }
116
+ export function createClaudeProvider(options) {
117
+ return {
118
+ id: "claude",
119
+ displayName: "Claude Code",
120
+ resumeIdentity: "optional",
121
+ probe: options.probe ?? (() => Promise.resolve({ terminalAvailable: true, metadataAvailable: true })),
122
+ buildProcess: async (context) => {
123
+ if (context.options.provider !== "claude") {
124
+ throw new ProviderError("INVALID_PROVIDER_OPTIONS", "Claude provider received non-Claude options");
125
+ }
126
+ const args = claudeArgs(context.options);
127
+ const ownedPaths = [];
128
+ const attach = context.attach ?? options.getAttach?.() ?? options.attach;
129
+ if (attach) {
130
+ try {
131
+ const mcpPath = mcpConfigPathFor(attach.dataDir, context.roamSessionId);
132
+ if (writeProviderArtifact0600(mcpPath, JSON.stringify(buildMcpConfigDocument(context.roamSessionId, attach)), context, ownedPaths)) {
133
+ insertBeforeSeparator(args, ["--mcp-config", mcpPath]);
134
+ }
135
+ const authPath = hookAuthPathFor(attach.dataDir, context.roamSessionId);
136
+ const settingsPath = hooksSettingsPathFor(attach.dataDir, context.roamSessionId);
137
+ if (writeProviderArtifact0600(authPath, hookAuthFileContent(attach.token), context, ownedPaths)) {
138
+ if (writeProviderArtifact0600(settingsPath, JSON.stringify(buildHooksSettingsDocument(context.roamSessionId, attach, authPath)), context, ownedPaths)) {
139
+ insertBeforeSeparator(args, ["--settings", settingsPath]);
140
+ }
141
+ else {
142
+ cleanupProviderArtifacts([authPath]);
143
+ }
144
+ }
145
+ }
146
+ catch (error) {
147
+ cleanupProviderArtifacts(ownedPaths);
148
+ throw error;
149
+ }
150
+ }
151
+ if (context.intent === "resume")
152
+ insertBeforeSeparator(args, ["--continue"]);
153
+ const env = { ...(options.env ?? process.env) };
154
+ delete env.ANTHROPIC_API_KEY;
155
+ return {
156
+ executable: options.claudeBin,
157
+ args,
158
+ env,
159
+ cleanupPaths: ownedPaths,
160
+ };
161
+ },
162
+ runtimeSignals: () => [],
163
+ classifyPane: classifyPaneStatus,
164
+ cleanup: cleanupProviderArtifacts,
165
+ };
166
+ }
@@ -0,0 +1,21 @@
1
+ import { type PaneStatus } from "../pane-status.js";
2
+ import type { ProviderRuntimeMetadata, ProviderRuntimeSignal, ProviderRuntimeSignalParser } from "./types.js";
3
+ export declare const CODEX_OSC_MAX_CARRY: number;
4
+ export declare const CODEX_CLASSIFIER_TESTED_UP_TO = "0.144";
5
+ export interface CodexOscParser extends ProviderRuntimeSignalParser {
6
+ readonly bufferedLength: number;
7
+ }
8
+ export declare function createCodexOscParser(): CodexOscParser;
9
+ export declare function parseCodexOscNotifications(input: string): ProviderRuntimeSignal[];
10
+ export declare function classifyCodexPane(pane: string): PaneStatus;
11
+ /**
12
+ * Read Codex's LIVE model + reasoning level from the pinned bottom status row. Unlike launch options, this
13
+ * changes immediately when the user switches reasoning inside the TUI. Only the final non-empty row is
14
+ * considered, so conversation text that happens to mention a model/effort pair cannot rewrite metadata.
15
+ *
16
+ * Supported real Codex layouts:
17
+ * gpt-5.6-sol xhigh · ~/Developer/remote-coder
18
+ * gpt-5.6 · high · 91% left
19
+ */
20
+ export declare function parseCodexRuntimeMetadata(pane: string): ProviderRuntimeMetadata | undefined;
21
+ export declare function codexClassifierVersionWarning(codexVersion: string | undefined): string | undefined;
@@ -0,0 +1,122 @@
1
+ import { isNewerMajorMinor } from "../pane-status.js";
2
+ export const CODEX_OSC_MAX_CARRY = 8 * 1024;
3
+ export const CODEX_CLASSIFIER_TESTED_UP_TO = "0.144";
4
+ const OSC_START = "\u001b]";
5
+ const OSC_9_START = "\u001b]9;";
6
+ const ST = "\u001b\\";
7
+ function signalForPayload(payload) {
8
+ if (payload.startsWith("Approval requested: ") ||
9
+ payload.startsWith("Codex wants to edit ") ||
10
+ payload.startsWith("Approval requested by ") ||
11
+ payload.startsWith("Plan mode prompt: ")) {
12
+ return { type: "blocked" };
13
+ }
14
+ return payload.length > 0 ? { type: "idle" } : undefined;
15
+ }
16
+ function terminatorAt(input, from) {
17
+ const bel = input.indexOf("\u0007", from);
18
+ const st = input.indexOf(ST, from);
19
+ if (bel === -1 && st === -1)
20
+ return undefined;
21
+ if (bel !== -1 && (st === -1 || bel < st))
22
+ return { index: bel, width: 1 };
23
+ return { index: st, width: 2 };
24
+ }
25
+ export function createCodexOscParser() {
26
+ let carry = "";
27
+ return {
28
+ get bufferedLength() {
29
+ return carry.length;
30
+ },
31
+ push(chunk) {
32
+ const input = carry + chunk;
33
+ carry = "";
34
+ const signals = [];
35
+ let cursor = 0;
36
+ while (cursor < input.length) {
37
+ const start = input.indexOf("\u001b", cursor);
38
+ if (start === -1)
39
+ break;
40
+ if (start === input.length - 1) {
41
+ carry = "\u001b";
42
+ break;
43
+ }
44
+ if (!input.startsWith(OSC_START, start)) {
45
+ cursor = start + 1;
46
+ continue;
47
+ }
48
+ if (input.length < start + OSC_9_START.length) {
49
+ carry = input.slice(start);
50
+ break;
51
+ }
52
+ if (!input.startsWith(OSC_9_START, start)) {
53
+ cursor = start + OSC_START.length;
54
+ continue;
55
+ }
56
+ const payloadStart = start + OSC_9_START.length;
57
+ const terminator = terminatorAt(input, payloadStart);
58
+ if (!terminator) {
59
+ const pending = input.slice(start);
60
+ carry = pending.length <= CODEX_OSC_MAX_CARRY ? pending : "";
61
+ break;
62
+ }
63
+ const frameLength = terminator.index + terminator.width - start;
64
+ if (frameLength <= CODEX_OSC_MAX_CARRY) {
65
+ const signal = signalForPayload(input.slice(payloadStart, terminator.index));
66
+ if (signal)
67
+ signals.push(signal);
68
+ }
69
+ cursor = terminator.index + terminator.width;
70
+ }
71
+ return signals;
72
+ },
73
+ };
74
+ }
75
+ export function parseCodexOscNotifications(input) {
76
+ return createCodexOscParser().push(input);
77
+ }
78
+ export function classifyCodexPane(pane) {
79
+ const tail = pane.split("\n").slice(-24).join("\n");
80
+ if (/\bWould you like to run the following command\b/i.test(tail))
81
+ return "blocked";
82
+ if (/\bDo you want to allow\b/i.test(tail))
83
+ return "blocked";
84
+ if (/\bPress enter to confirm or esc to cancel\b/i.test(tail))
85
+ return "blocked";
86
+ if (/\besc to interrupt\b/i.test(tail))
87
+ return "working";
88
+ if (/[•●]\s*(?:Working|Thinking|Running)\s*\(\s*\d+\s*[ms]\b/i.test(tail))
89
+ return "working";
90
+ return "idle";
91
+ }
92
+ const RUNTIME_TOKEN = "[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}";
93
+ const EFFORT_TOKEN = "[A-Za-z0-9][A-Za-z0-9._-]{0,127}";
94
+ /**
95
+ * Read Codex's LIVE model + reasoning level from the pinned bottom status row. Unlike launch options, this
96
+ * changes immediately when the user switches reasoning inside the TUI. Only the final non-empty row is
97
+ * considered, so conversation text that happens to mention a model/effort pair cannot rewrite metadata.
98
+ *
99
+ * Supported real Codex layouts:
100
+ * gpt-5.6-sol xhigh · ~/Developer/remote-coder
101
+ * gpt-5.6 · high · 91% left
102
+ */
103
+ export function parseCodexRuntimeMetadata(pane) {
104
+ const line = pane
105
+ .split(/\r?\n/)
106
+ .map((value) => value.trim())
107
+ .filter(Boolean)
108
+ .at(-1);
109
+ if (!line)
110
+ return undefined;
111
+ const current = new RegExp(`^(${RUNTIME_TOKEN})\\s+(${EFFORT_TOKEN})\\s+·\\s+(?:~/|/).+$`).exec(line);
112
+ if (current)
113
+ return { model: current[1], effort: current[2] };
114
+ const legacy = new RegExp(`^(${RUNTIME_TOKEN})\\s+·\\s+(${EFFORT_TOKEN})\\s+·\\s+.+$`).exec(line);
115
+ return legacy ? { model: legacy[1], effort: legacy[2] } : undefined;
116
+ }
117
+ export function codexClassifierVersionWarning(codexVersion) {
118
+ if (!codexVersion || !isNewerMajorMinor(codexVersion, CODEX_CLASSIFIER_TESTED_UP_TO))
119
+ return undefined;
120
+ return (`Codex pane-status markers were verified against Codex <=${CODEX_CLASSIFIER_TESTED_UP_TO}; ` +
121
+ `current is ${codexVersion} — verify rail statuses after this upgrade`);
122
+ }