@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,322 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { z } from "zod";
3
+ import { codexThreadResolutionCoordinator } from "./codex-thread-coordinator.js";
4
+ import { isCodexThreadPersistence } from "./codex-thread-persistence.js";
5
+ import { ProviderError } from "./types.js";
6
+ const DEFAULT_DEADLINE_MS = 5_000;
7
+ const DEFAULT_POLL_INTERVAL_MS = 100;
8
+ const DEFAULT_MAX_INVENTORY_ITEMS = 1_000;
9
+ const DEFAULT_CREATION_SKEW_MS = 5_000;
10
+ const MAX_THREAD_ID = 2_048;
11
+ const DEFAULT_CANCELLATION_ACK_MS = 1_000;
12
+ const ThreadSourceSchema = z.union([
13
+ z.enum(["cli", "vscode", "exec", "appServer", "unknown"]),
14
+ z.object({ custom: z.string().max(256) }),
15
+ z.object({ subAgent: z.unknown() }),
16
+ ]);
17
+ const ThreadSchema = z.object({
18
+ id: z.string().min(1).max(MAX_THREAD_ID),
19
+ cwd: z.string().min(1).max(4_096),
20
+ source: ThreadSourceSchema,
21
+ createdAt: z.number().int().safe().nonnegative(),
22
+ });
23
+ const ThreadListResponseSchema = z.object({
24
+ data: z.array(ThreadSchema).max(500),
25
+ nextCursor: z.string().min(1).max(2_048).nullable().optional(),
26
+ });
27
+ function unavailable() {
28
+ return new ProviderError("RESUME_IDENTITY_UNAVAILABLE", "Codex resume identity is unavailable");
29
+ }
30
+ function positiveInteger(value, fallback) {
31
+ return value !== undefined && Number.isSafeInteger(value) && value > 0 ? value : fallback;
32
+ }
33
+ function validThreadId(id) {
34
+ return (id.trim().length > 0 &&
35
+ id.length <= MAX_THREAD_ID &&
36
+ !/[\p{Cc}\p{Zl}\p{Zp}]/u.test(id) &&
37
+ !id.trimStart().startsWith("-"));
38
+ }
39
+ function normalizeProtocolTimestamp(value) {
40
+ const milliseconds = value < 1_000_000_000_000 ? value * 1_000 : value;
41
+ return Number.isSafeInteger(milliseconds) && milliseconds >= 0 ? milliseconds : undefined;
42
+ }
43
+ function defaultSleep(milliseconds, signal) {
44
+ return new Promise((resolve, reject) => {
45
+ if (signal?.aborted) {
46
+ reject(unavailable());
47
+ return;
48
+ }
49
+ const timer = setTimeout(done, milliseconds);
50
+ function done() {
51
+ signal?.removeEventListener("abort", aborted);
52
+ resolve();
53
+ }
54
+ function aborted() {
55
+ clearTimeout(timer);
56
+ signal?.removeEventListener("abort", aborted);
57
+ reject(unavailable());
58
+ }
59
+ signal?.addEventListener("abort", aborted, { once: true });
60
+ });
61
+ }
62
+ export function resetCodexThreadResolutionCoordinatorForTests() {
63
+ codexThreadResolutionCoordinator.resetForTests();
64
+ }
65
+ class ResolverDeadline {
66
+ callerSignal;
67
+ controller = new AbortController();
68
+ timer;
69
+ abortFromCaller;
70
+ constructor(milliseconds, callerSignal) {
71
+ this.callerSignal = callerSignal;
72
+ if (callerSignal?.aborted) {
73
+ this.controller.abort();
74
+ }
75
+ else if (callerSignal) {
76
+ this.abortFromCaller = () => this.controller.abort();
77
+ callerSignal.addEventListener("abort", this.abortFromCaller, { once: true });
78
+ }
79
+ this.timer = setTimeout(() => this.controller.abort(), milliseconds);
80
+ }
81
+ get signal() {
82
+ return this.controller.signal;
83
+ }
84
+ run(operation) {
85
+ if (this.signal.aborted)
86
+ return Promise.reject(unavailable());
87
+ return new Promise((resolve, reject) => {
88
+ let settled = false;
89
+ const finish = (callback) => {
90
+ if (settled)
91
+ return;
92
+ settled = true;
93
+ this.signal.removeEventListener("abort", aborted);
94
+ callback();
95
+ };
96
+ const aborted = () => finish(() => reject(unavailable()));
97
+ this.signal.addEventListener("abort", aborted, { once: true });
98
+ try {
99
+ Promise.resolve(operation()).then((value) => finish(() => resolve(value)), () => finish(() => reject(unavailable())));
100
+ }
101
+ catch {
102
+ finish(() => reject(unavailable()));
103
+ }
104
+ });
105
+ }
106
+ dispose() {
107
+ clearTimeout(this.timer);
108
+ if (this.abortFromCaller)
109
+ this.callerSignal?.removeEventListener("abort", this.abortFromCaller);
110
+ }
111
+ }
112
+ /** Shared assertion used by the provider's generated resume argv regression. */
113
+ export function assertExactCodexResumeArgs(args) {
114
+ if (args.some((arg) => arg === "--last" || arg.startsWith("--last="))) {
115
+ throw new ProviderError("RESUME_IDENTITY_UNAVAILABLE", "Codex resume requires an exact safe session id");
116
+ }
117
+ }
118
+ /** Creates the bounded current-protocol `thread/list` reader used by resolver snapshots and polls. */
119
+ export function createCodexThreadInventory(rpc, options) {
120
+ const maxPages = positiveInteger(options.maxPages, 20);
121
+ const maxItems = positiveInteger(options.maxItems, DEFAULT_MAX_INVENTORY_ITEMS);
122
+ return async () => {
123
+ if (!isAbsolute(options.cwd) || options.cwd.length > 4_096)
124
+ throw unavailable();
125
+ const entries = [];
126
+ const seenCursors = new Set();
127
+ let cursor = null;
128
+ try {
129
+ for (let page = 0; page < maxPages; page += 1) {
130
+ const response = await rpc.request("thread/list", {
131
+ cursor,
132
+ limit: 100,
133
+ archived: false,
134
+ cwd: options.cwd,
135
+ sourceKinds: ["cli"],
136
+ sortKey: "created_at",
137
+ sortDirection: "desc",
138
+ }, ThreadListResponseSchema);
139
+ if (entries.length + response.data.length > maxItems)
140
+ throw unavailable();
141
+ entries.push(...response.data);
142
+ const next = response.nextCursor ?? null;
143
+ if (next === null)
144
+ return entries;
145
+ if (seenCursors.has(next))
146
+ throw unavailable();
147
+ seenCursors.add(next);
148
+ cursor = next;
149
+ }
150
+ }
151
+ catch {
152
+ throw unavailable();
153
+ }
154
+ throw unavailable();
155
+ };
156
+ }
157
+ export class CodexThreadResolver {
158
+ inventory;
159
+ now;
160
+ sleep;
161
+ deadlineMs;
162
+ pollIntervalMs;
163
+ maxInventoryItems;
164
+ creationSkewMs;
165
+ cancellationAckMs;
166
+ maxPolls;
167
+ constructor(options) {
168
+ this.inventory = options.inventory;
169
+ this.now = options.now ?? (() => Date.now());
170
+ this.sleep = options.sleep ?? defaultSleep;
171
+ this.deadlineMs = positiveInteger(options.deadlineMs, DEFAULT_DEADLINE_MS);
172
+ this.pollIntervalMs = positiveInteger(options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS);
173
+ this.maxInventoryItems = positiveInteger(options.maxInventoryItems, DEFAULT_MAX_INVENTORY_ITEMS);
174
+ this.creationSkewMs = positiveInteger(options.creationSkewMs, DEFAULT_CREATION_SKEW_MS);
175
+ this.cancellationAckMs = positiveInteger(options.cancellationAckMs, DEFAULT_CANCELLATION_ACK_MS);
176
+ this.maxPolls = Math.min(10_000, Math.ceil(this.deadlineMs / this.pollIntervalMs) + 2);
177
+ }
178
+ async resolveAfterSpawn(options) {
179
+ if (!isAbsolute(options.cwd) ||
180
+ options.cwd.length > 4_096 ||
181
+ /[\p{Cc}\p{Zl}\p{Zp}]/u.test(options.cwd) ||
182
+ typeof options.spawn !== "function" ||
183
+ !isCodexThreadPersistence(options.persistence)) {
184
+ throw unavailable();
185
+ }
186
+ const deadlineAt = this.now() + this.deadlineMs;
187
+ const deadline = new ResolverDeadline(this.deadlineMs, options.signal);
188
+ let release;
189
+ try {
190
+ release = await codexThreadResolutionCoordinator.acquire(deadline.signal);
191
+ return await this.resolveLocked(options, deadline, deadlineAt);
192
+ }
193
+ catch {
194
+ throw unavailable();
195
+ }
196
+ finally {
197
+ release?.();
198
+ deadline.dispose();
199
+ }
200
+ }
201
+ async resolveLocked(options, deadline, deadlineAt) {
202
+ this.throwIfCancelled(deadline.signal);
203
+ const before = await this.readInventory(deadline);
204
+ const beforeIds = new Set(before.map((thread) => thread.id));
205
+ const startedAt = this.now();
206
+ let lease;
207
+ let spawnAttempted = false;
208
+ let completed = false;
209
+ try {
210
+ spawnAttempted = true;
211
+ lease = options.spawn(deadline.signal);
212
+ if (!lease || !(lease.started instanceof Promise) || typeof lease.cancel !== "function")
213
+ throw unavailable();
214
+ await deadline.run(() => lease.started);
215
+ for (let poll = 0; poll < this.maxPolls; poll += 1) {
216
+ this.throwIfCancelled(deadline.signal);
217
+ const current = await this.readInventory(deadline);
218
+ const observedAt = this.now();
219
+ if (observedAt > deadlineAt)
220
+ throw unavailable();
221
+ const candidates = this.candidates(current, beforeIds, options.cwd, startedAt, observedAt);
222
+ if (candidates.length > 1)
223
+ throw unavailable();
224
+ const candidate = candidates[0];
225
+ if (candidate) {
226
+ try {
227
+ options.persistence.markProvisional(candidate.id);
228
+ this.throwIfCancelled(deadline.signal);
229
+ if (this.now() > deadlineAt)
230
+ throw unavailable();
231
+ const fresh = await this.readInventory(deadline);
232
+ const crossCheckedAt = this.now();
233
+ if (crossCheckedAt > deadlineAt)
234
+ throw unavailable();
235
+ const crossChecked = this.candidates(fresh, beforeIds, options.cwd, startedAt, crossCheckedAt);
236
+ if (crossChecked.length !== 1 || !this.sameIdentity(candidate, crossChecked[0]))
237
+ throw unavailable();
238
+ options.persistence.commit(candidate.id);
239
+ completed = true;
240
+ return candidate.id;
241
+ }
242
+ catch {
243
+ try {
244
+ options.persistence.clear(candidate.id);
245
+ }
246
+ catch {
247
+ // The resolver still fails closed; the capability contract requires storage to mark the id unusable.
248
+ }
249
+ throw unavailable();
250
+ }
251
+ }
252
+ const now = this.now();
253
+ if (now >= deadlineAt)
254
+ throw unavailable();
255
+ try {
256
+ await deadline.run(() => this.sleep(Math.min(this.pollIntervalMs, deadlineAt - now), deadline.signal));
257
+ }
258
+ catch {
259
+ throw unavailable();
260
+ }
261
+ }
262
+ throw unavailable();
263
+ }
264
+ finally {
265
+ if (lease && !completed) {
266
+ await codexThreadResolutionCoordinator.acknowledgeCancellation(lease, this.cancellationAckMs);
267
+ }
268
+ else if (spawnAttempted && !completed) {
269
+ codexThreadResolutionCoordinator.poisonUnknownSpawnOutcome();
270
+ }
271
+ }
272
+ }
273
+ async readInventory(deadline) {
274
+ let raw;
275
+ try {
276
+ raw = await deadline.run(() => this.inventory());
277
+ }
278
+ catch {
279
+ throw unavailable();
280
+ }
281
+ if (!Array.isArray(raw) || raw.length > this.maxInventoryItems)
282
+ throw unavailable();
283
+ const normalized = [];
284
+ const ids = new Set();
285
+ for (const value of raw) {
286
+ const parsed = ThreadSchema.safeParse(value);
287
+ if (!parsed.success || !validThreadId(parsed.data.id) || ids.has(parsed.data.id))
288
+ throw unavailable();
289
+ const createdAtMs = normalizeProtocolTimestamp(parsed.data.createdAt);
290
+ if (createdAtMs === undefined)
291
+ throw unavailable();
292
+ ids.add(parsed.data.id);
293
+ normalized.push({
294
+ id: parsed.data.id,
295
+ cwd: parsed.data.cwd,
296
+ source: parsed.data.source,
297
+ createdAtMs,
298
+ });
299
+ }
300
+ return normalized;
301
+ }
302
+ candidates(inventory, beforeIds, cwd, startedAt, observedAt) {
303
+ const earliest = startedAt - this.creationSkewMs;
304
+ const latest = Math.min(startedAt + this.deadlineMs + this.creationSkewMs, observedAt + this.creationSkewMs);
305
+ return inventory.filter((thread) => !beforeIds.has(thread.id) &&
306
+ validThreadId(thread.id) &&
307
+ thread.cwd === cwd &&
308
+ thread.source === "cli" &&
309
+ thread.createdAtMs >= earliest &&
310
+ thread.createdAtMs <= latest);
311
+ }
312
+ sameIdentity(left, right) {
313
+ return (left.id === right.id &&
314
+ left.cwd === right.cwd &&
315
+ left.source === right.source &&
316
+ left.createdAtMs === right.createdAtMs);
317
+ }
318
+ throwIfCancelled(signal) {
319
+ if (signal?.aborted)
320
+ throw unavailable();
321
+ }
322
+ }
@@ -0,0 +1,7 @@
1
+ import { ProviderError } from "./types.js";
2
+ import type { ClaudeSessionOptions, ProviderId, ProviderSessionOptions } from "./types.js";
3
+ export declare class ProviderOptionsError extends ProviderError {
4
+ constructor(message: string);
5
+ }
6
+ export declare function parseProviderOptions(provider: ProviderId, raw: unknown): ProviderSessionOptions;
7
+ export declare function parseLegacyClaudeArgs(args: readonly string[]): ClaudeSessionOptions;
@@ -0,0 +1,155 @@
1
+ import { z } from "zod";
2
+ import { ProviderError } from "./types.js";
3
+ const MAX_MODEL_LENGTH = 128;
4
+ const MAX_PROFILE_LENGTH = 128;
5
+ const MAX_PATH_LENGTH = 4096;
6
+ const MAX_ADD_DIRS = 32;
7
+ const MAX_LEGACY_ARGS = 128;
8
+ const MAX_LEGACY_ARG_LENGTH = 4096;
9
+ const modelToken = z
10
+ .string()
11
+ .min(1)
12
+ .max(MAX_MODEL_LENGTH)
13
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._:/\u005b\u005d-]*$/, "must be a safe model token");
14
+ const profileToken = z
15
+ .string()
16
+ .min(1)
17
+ .max(MAX_PROFILE_LENGTH)
18
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "must be a safe profile token");
19
+ const effortToken = z
20
+ .string()
21
+ .min(1)
22
+ .max(128)
23
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "must be a safe effort token");
24
+ const pathToken = z
25
+ .string()
26
+ .min(1)
27
+ .max(MAX_PATH_LENGTH)
28
+ .regex(/^\/[^\x00-\x1f\x7f]*$/, "must be an absolute path without control characters");
29
+ const addDirs = z.array(pathToken).max(MAX_ADD_DIRS);
30
+ const claudeOptionsSchema = z
31
+ .object({
32
+ model: modelToken.optional(),
33
+ effort: effortToken.optional(),
34
+ permissionMode: z.enum(["default", "acceptEdits", "plan", "bypassPermissions"]).optional(),
35
+ dangerouslySkip: z.boolean().optional(),
36
+ addDirs: addDirs.optional(),
37
+ })
38
+ .strict()
39
+ .superRefine((options, context) => {
40
+ if (options.dangerouslySkip === true && options.permissionMode !== undefined) {
41
+ context.addIssue({
42
+ code: "custom",
43
+ message: "dangerouslySkip and permissionMode are mutually exclusive",
44
+ });
45
+ }
46
+ });
47
+ const codexOptionsSchema = z
48
+ .object({
49
+ model: modelToken.optional(),
50
+ reasoningEffort: effortToken.optional(),
51
+ sandbox: z.enum(["read-only", "workspace-write", "danger-full-access"]).optional(),
52
+ approvalPolicy: z.enum(["untrusted", "on-request", "never"]).optional(),
53
+ profile: profileToken.optional(),
54
+ webSearch: z.boolean().optional(),
55
+ dangerouslyBypassApprovalsAndSandbox: z.boolean().optional(),
56
+ addDirs: addDirs.optional(),
57
+ })
58
+ .strict()
59
+ .superRefine((options, context) => {
60
+ if (options.dangerouslyBypassApprovalsAndSandbox === true &&
61
+ (options.sandbox !== undefined || options.approvalPolicy !== undefined)) {
62
+ context.addIssue({
63
+ code: "custom",
64
+ message: "dangerouslyBypassApprovalsAndSandbox is mutually exclusive with sandbox and approvalPolicy",
65
+ });
66
+ }
67
+ });
68
+ const legacyArgSchema = z
69
+ .string()
70
+ .max(MAX_LEGACY_ARG_LENGTH)
71
+ .refine((arg) => !arg.includes("\0"), {
72
+ message: "must not contain a null byte",
73
+ });
74
+ const legacyArgsSchema = z.array(legacyArgSchema).max(MAX_LEGACY_ARGS);
75
+ export class ProviderOptionsError extends ProviderError {
76
+ constructor(message) {
77
+ super("INVALID_PROVIDER_OPTIONS", message);
78
+ this.name = "ProviderOptionsError";
79
+ }
80
+ }
81
+ function invalidOptions(error) {
82
+ const detail = error.issues.map((issue) => issue.message).join("; ");
83
+ return new ProviderOptionsError(`Invalid provider options: ${detail}`);
84
+ }
85
+ export function parseProviderOptions(provider, raw) {
86
+ try {
87
+ if (provider === "claude") {
88
+ return { provider, ...claudeOptionsSchema.parse(raw) };
89
+ }
90
+ if (provider === "codex") {
91
+ return { provider, ...codexOptionsSchema.parse(raw) };
92
+ }
93
+ }
94
+ catch (error) {
95
+ if (error instanceof z.ZodError)
96
+ throw invalidOptions(error);
97
+ throw error;
98
+ }
99
+ throw new ProviderOptionsError(`Invalid provider options: unknown provider ${String(provider)}`);
100
+ }
101
+ function requireValue(args, index, flag) {
102
+ const value = args[index + 1];
103
+ if (value === undefined || value.startsWith("--")) {
104
+ throw new ProviderOptionsError(`Invalid provider options: ${flag} requires a value`);
105
+ }
106
+ return value;
107
+ }
108
+ export function parseLegacyClaudeArgs(args) {
109
+ let parsedArgs;
110
+ try {
111
+ parsedArgs = legacyArgsSchema.parse(args);
112
+ }
113
+ catch (error) {
114
+ if (error instanceof z.ZodError)
115
+ throw invalidOptions(error);
116
+ throw error;
117
+ }
118
+ const raw = {};
119
+ const parsedAddDirs = [];
120
+ const legacyArgs = [];
121
+ for (let index = 0; index < parsedArgs.length; index += 1) {
122
+ const arg = parsedArgs[index];
123
+ if (arg === "--") {
124
+ legacyArgs.push(...parsedArgs.slice(index));
125
+ break;
126
+ }
127
+ switch (arg) {
128
+ case "--model":
129
+ raw.model = requireValue(parsedArgs, index, arg);
130
+ index += 1;
131
+ break;
132
+ case "--effort":
133
+ raw.effort = requireValue(parsedArgs, index, arg);
134
+ index += 1;
135
+ break;
136
+ case "--permission-mode":
137
+ raw.permissionMode = requireValue(parsedArgs, index, arg);
138
+ index += 1;
139
+ break;
140
+ case "--dangerously-skip-permissions":
141
+ raw.dangerouslySkip = true;
142
+ break;
143
+ case "--add-dir":
144
+ parsedAddDirs.push(requireValue(parsedArgs, index, arg));
145
+ index += 1;
146
+ break;
147
+ default:
148
+ legacyArgs.push(arg);
149
+ }
150
+ }
151
+ if (parsedAddDirs.length > 0)
152
+ raw.addDirs = parsedAddDirs;
153
+ const parsed = parseProviderOptions("claude", raw);
154
+ return legacyArgs.length > 0 ? { ...parsed, legacyArgs } : parsed;
155
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProviderProcessContext } from "./types.js";
2
+ export declare function cleanupProviderArtifacts(paths: readonly string[]): void;
3
+ export declare function writeProviderArtifact0600(path: string, content: string, context: ProviderProcessContext, ownedPaths: string[]): boolean;
@@ -0,0 +1,30 @@
1
+ import { chmodSync, unlinkSync, writeFileSync } from "node:fs";
2
+ export function cleanupProviderArtifacts(paths) {
3
+ for (const path of paths) {
4
+ try {
5
+ unlinkSync(path);
6
+ }
7
+ catch {
8
+ /* already gone */
9
+ }
10
+ }
11
+ }
12
+ export function writeProviderArtifact0600(path, content, context, ownedPaths) {
13
+ try {
14
+ context.registerCleanupPaths?.([path]);
15
+ }
16
+ catch (error) {
17
+ cleanupProviderArtifacts([path]);
18
+ throw error;
19
+ }
20
+ ownedPaths.push(path);
21
+ try {
22
+ writeFileSync(path, content, { mode: 0o600 });
23
+ chmodSync(path, 0o600);
24
+ return true;
25
+ }
26
+ catch {
27
+ cleanupProviderArtifacts([path]);
28
+ return false;
29
+ }
30
+ }
@@ -0,0 +1,7 @@
1
+ import { type AgentProvider, type ProviderId } from "./types.js";
2
+ export declare class ProviderRegistry {
3
+ private readonly byId;
4
+ constructor(providers: readonly AgentProvider[]);
5
+ get(id: ProviderId): AgentProvider;
6
+ list(): AgentProvider[];
7
+ }
@@ -0,0 +1,23 @@
1
+ import { ProviderError } from "./types.js";
2
+ export class ProviderRegistry {
3
+ byId;
4
+ constructor(providers) {
5
+ const byId = new Map();
6
+ for (const provider of providers) {
7
+ if (byId.has(provider.id)) {
8
+ throw new ProviderError("PROVIDER_UNAVAILABLE", `duplicate provider id: ${provider.id}`);
9
+ }
10
+ byId.set(provider.id, provider);
11
+ }
12
+ this.byId = byId;
13
+ }
14
+ get(id) {
15
+ const provider = this.byId.get(id);
16
+ if (!provider)
17
+ throw new ProviderError("PROVIDER_UNAVAILABLE", `provider unavailable: ${id}`);
18
+ return provider;
19
+ }
20
+ list() {
21
+ return [...this.byId.values()];
22
+ }
23
+ }
@@ -0,0 +1,95 @@
1
+ import type { AttachSpawnOptions } from "../config.js";
2
+ export type ProviderId = "claude" | "codex";
3
+ export interface ProviderAvailability {
4
+ terminalAvailable: boolean;
5
+ metadataAvailable: boolean;
6
+ version?: string;
7
+ detail?: string;
8
+ }
9
+ export declare class ProviderError extends Error {
10
+ readonly code: "PROVIDER_UNAVAILABLE" | "INVALID_PROVIDER_OPTIONS" | "RESUME_IDENTITY_UNAVAILABLE" | "OSS_PROVIDER_DEFERRED";
11
+ constructor(code: "PROVIDER_UNAVAILABLE" | "INVALID_PROVIDER_OPTIONS" | "RESUME_IDENTITY_UNAVAILABLE" | "OSS_PROVIDER_DEFERRED", message: string);
12
+ }
13
+ export type ClaudeSessionOptions = {
14
+ provider: "claude";
15
+ model?: string;
16
+ effort?: string;
17
+ permissionMode?: "default" | "acceptEdits" | "plan" | "bypassPermissions";
18
+ dangerouslySkip?: boolean;
19
+ addDirs?: string[];
20
+ legacyArgs?: string[];
21
+ };
22
+ export type CodexSessionOptions = {
23
+ provider: "codex";
24
+ model?: string;
25
+ reasoningEffort?: string;
26
+ sandbox?: "read-only" | "workspace-write" | "danger-full-access";
27
+ approvalPolicy?: "untrusted" | "on-request" | "never";
28
+ profile?: string;
29
+ webSearch?: boolean;
30
+ dangerouslyBypassApprovalsAndSandbox?: boolean;
31
+ addDirs?: string[];
32
+ };
33
+ export type ProviderSessionOptions = ClaudeSessionOptions | CodexSessionOptions;
34
+ export type LaunchIntent = "fresh" | "resume";
35
+ export interface ProcessSpec {
36
+ executable: string;
37
+ args: string[];
38
+ env: NodeJS.ProcessEnv;
39
+ cleanupPaths: string[];
40
+ /** Revalidates ephemeral launch proof immediately before the terminal process is spawned. */
41
+ preSpawnCheck?: () => void | Promise<void>;
42
+ integration?: {
43
+ attachments: "ready" | "degraded";
44
+ activity: "ready" | "degraded";
45
+ detail?: string;
46
+ };
47
+ }
48
+ export interface CodexProfileLaunchProof {
49
+ readonly profile: string;
50
+ readonly codexHome: string;
51
+ readonly assertUnchanged: () => Promise<void>;
52
+ }
53
+ export interface ProviderProcessContext {
54
+ roamSessionId: string;
55
+ cwd: string;
56
+ intent: LaunchIntent;
57
+ options: ProviderSessionOptions;
58
+ providerSessionId?: string;
59
+ /** Late-bound after listen so both providers share the real loopback MCP callback configuration. */
60
+ attach?: AttachSpawnOptions;
61
+ /** Transfer temporary-artifact ownership as soon as paths exist, so a later build rejection is cleanable. */
62
+ registerCleanupPaths?(paths: readonly string[]): void;
63
+ }
64
+ export type ProviderRuntimeSignal = {
65
+ type: "working";
66
+ } | {
67
+ type: "blocked";
68
+ } | {
69
+ type: "idle";
70
+ } | {
71
+ type: "provider-session-id";
72
+ id: string;
73
+ };
74
+ export interface ProviderRuntimeSignalParser {
75
+ push(chunk: string): ProviderRuntimeSignal[];
76
+ }
77
+ export interface ProviderRuntimeMetadata {
78
+ model?: string;
79
+ effort?: string;
80
+ }
81
+ export interface AgentProvider {
82
+ readonly id: ProviderId;
83
+ readonly displayName: string;
84
+ readonly resumeIdentity: "optional" | "required";
85
+ probe(): Promise<ProviderAvailability>;
86
+ buildProcess(context: ProviderProcessContext): Promise<ProcessSpec>;
87
+ /** Stateful parsers must be created per spawned process; providers are registry singletons. */
88
+ createRuntimeSignalParser?(): ProviderRuntimeSignalParser;
89
+ runtimeSignals(chunk: string): ProviderRuntimeSignal[];
90
+ classifyPane(pane: string): "working" | "blocked" | "idle";
91
+ /** Read provider-owned live model/effort chrome from a captured pane. Optional because not every TUI
92
+ * exposes these values. A missing/invalid result must leave launch metadata untouched. */
93
+ runtimeMetadata?(pane: string): ProviderRuntimeMetadata | undefined;
94
+ cleanup(paths: readonly string[]): void;
95
+ }
@@ -0,0 +1,8 @@
1
+ export class ProviderError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "ProviderError";
7
+ }
8
+ }