@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.
- package/LICENSE +21 -0
- package/dist/auth.d.ts +63 -0
- package/dist/auth.js +133 -0
- package/dist/claude-auth-service.d.ts +76 -0
- package/dist/claude-auth-service.js +217 -0
- package/dist/claude-latest-service.d.ts +34 -0
- package/dist/claude-latest-service.js +61 -0
- package/dist/config.d.ts +78 -0
- package/dist/config.js +59 -0
- package/dist/data-dir.d.ts +42 -0
- package/dist/data-dir.js +70 -0
- package/dist/diag.d.ts +43 -0
- package/dist/diag.js +83 -0
- package/dist/fs-service.d.ts +90 -0
- package/dist/fs-service.js +290 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +44 -0
- package/dist/managed-runtime.d.ts +51 -0
- package/dist/managed-runtime.js +411 -0
- package/dist/managed-update-helper.d.ts +2 -0
- package/dist/managed-update-helper.js +34 -0
- package/dist/mcp-send.d.ts +33 -0
- package/dist/mcp-send.js +107 -0
- package/dist/origin-check.d.ts +37 -0
- package/dist/origin-check.js +101 -0
- package/dist/pane-status.d.ts +61 -0
- package/dist/pane-status.js +145 -0
- package/dist/providers/claude-metadata-service.d.ts +58 -0
- package/dist/providers/claude-metadata-service.js +352 -0
- package/dist/providers/claude-provider.d.ts +11 -0
- package/dist/providers/claude-provider.js +166 -0
- package/dist/providers/codex-activity.d.ts +21 -0
- package/dist/providers/codex-activity.js +122 -0
- package/dist/providers/codex-app-server-client.d.ts +90 -0
- package/dist/providers/codex-app-server-client.js +485 -0
- package/dist/providers/codex-latest-service.d.ts +50 -0
- package/dist/providers/codex-latest-service.js +174 -0
- package/dist/providers/codex-metadata-service.d.ts +161 -0
- package/dist/providers/codex-metadata-service.js +686 -0
- package/dist/providers/codex-profile-client.d.ts +16 -0
- package/dist/providers/codex-profile-client.js +52 -0
- package/dist/providers/codex-profile-security.d.ts +23 -0
- package/dist/providers/codex-profile-security.js +161 -0
- package/dist/providers/codex-provider.d.ts +15 -0
- package/dist/providers/codex-provider.js +174 -0
- package/dist/providers/codex-thread-coordinator.d.ts +18 -0
- package/dist/providers/codex-thread-coordinator.js +93 -0
- package/dist/providers/codex-thread-persistence.d.ts +9 -0
- package/dist/providers/codex-thread-persistence.js +45 -0
- package/dist/providers/codex-thread-resolver.d.ts +59 -0
- package/dist/providers/codex-thread-resolver.js +322 -0
- package/dist/providers/options.d.ts +7 -0
- package/dist/providers/options.js +155 -0
- package/dist/providers/provider-artifacts.d.ts +3 -0
- package/dist/providers/provider-artifacts.js +30 -0
- package/dist/providers/registry.d.ts +7 -0
- package/dist/providers/registry.js +23 -0
- package/dist/providers/types.d.ts +95 -0
- package/dist/providers/types.js +8 -0
- package/dist/push-dispatch.d.ts +81 -0
- package/dist/push-dispatch.js +100 -0
- package/dist/push-store.d.ts +25 -0
- package/dist/push-store.js +79 -0
- package/dist/rate-limit.d.ts +52 -0
- package/dist/rate-limit.js +72 -0
- package/dist/server-config.d.ts +60 -0
- package/dist/server-config.js +77 -0
- package/dist/service-install.d.ts +60 -0
- package/dist/service-install.js +221 -0
- package/dist/session-defaults.d.ts +26 -0
- package/dist/session-defaults.js +60 -0
- package/dist/session-store.d.ts +81 -0
- package/dist/session-store.js +654 -0
- package/dist/start.d.ts +31 -0
- package/dist/start.js +372 -0
- package/dist/static-routes.d.ts +101 -0
- package/dist/static-routes.js +188 -0
- package/dist/terminal-capability.d.ts +5 -0
- package/dist/terminal-capability.js +27 -0
- package/dist/terminal-manager.d.ts +224 -0
- package/dist/terminal-manager.js +917 -0
- package/dist/terminal-process.d.ts +85 -0
- package/dist/terminal-process.js +238 -0
- package/dist/terminal-shared.d.ts +36 -0
- package/dist/terminal-shared.js +43 -0
- package/dist/tmux-list.d.ts +11 -0
- package/dist/tmux-list.js +39 -0
- package/dist/transport.d.ts +123 -0
- package/dist/transport.js +1559 -0
- package/dist/updater.d.ts +161 -0
- package/dist/updater.js +451 -0
- package/dist/usage-service.d.ts +118 -0
- package/dist/usage-service.js +173 -0
- package/dist/vapid.d.ts +17 -0
- package/dist/vapid.js +31 -0
- package/dist/web-push-send.d.ts +20 -0
- package/dist/web-push-send.js +21 -0
- package/dist/ws-ticket.d.ts +47 -0
- package/dist/ws-ticket.js +62 -0
- package/package.json +55 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { parseProviderOptions } from "./providers/options.js";
|
|
3
|
+
import { normalizeSessionDefaults, SessionDefaultsConflictError, } from "./session-defaults.js";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const concreteSessionStores = new WeakSet();
|
|
6
|
+
/** Internal trust check used by security-sensitive capabilities; only stores opened by this module are accepted. */
|
|
7
|
+
export function isConcreteSessionStore(store) {
|
|
8
|
+
return concreteSessionStores.has(store);
|
|
9
|
+
}
|
|
10
|
+
function brandSessionStore(store) {
|
|
11
|
+
concreteSessionStores.add(store);
|
|
12
|
+
return store;
|
|
13
|
+
}
|
|
14
|
+
/** Parse the stored spawn_args JSON back into a string[] — tolerant: a NULL, malformed, or non-array value
|
|
15
|
+
* (an ancient row, a hand-edited DB) yields undefined so the session simply respawns flag-less, never throws. */
|
|
16
|
+
function parseSpawnArgs(raw) {
|
|
17
|
+
if (typeof raw !== "string" || raw.length === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
try {
|
|
20
|
+
const v = JSON.parse(raw);
|
|
21
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string") ? v : undefined;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function legacyRowToSession(r) {
|
|
28
|
+
const spawnArgs = parseSpawnArgs(r.spawn_args);
|
|
29
|
+
return {
|
|
30
|
+
provider: "claude",
|
|
31
|
+
id: r.id,
|
|
32
|
+
cwd: r.cwd,
|
|
33
|
+
dangerouslySkip: r.dangerously_skip === 1,
|
|
34
|
+
status: r.status,
|
|
35
|
+
createdAt: r.created_at,
|
|
36
|
+
lastActivityAt: r.last_activity_at,
|
|
37
|
+
mode: "terminal",
|
|
38
|
+
// Only carry a REAL name — NULL/"" stays absent so consumers can `?? cwd` and toEqual-style tests
|
|
39
|
+
// of unnamed rows don't grow a noise field.
|
|
40
|
+
...(typeof r.name === "string" && r.name.length > 0 ? { name: r.name } : {}),
|
|
41
|
+
...(spawnArgs && spawnArgs.length > 0 ? { spawnArgs } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function parseCodexOptions(raw) {
|
|
45
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
46
|
+
throw new Error("Invalid Codex launch options");
|
|
47
|
+
}
|
|
48
|
+
const { provider, ...options } = raw;
|
|
49
|
+
if (provider !== "codex")
|
|
50
|
+
throw new Error("Invalid Codex launch options provider");
|
|
51
|
+
return parseProviderOptions("codex", options);
|
|
52
|
+
}
|
|
53
|
+
function validateCodexOptions(options) {
|
|
54
|
+
return parseCodexOptions(options);
|
|
55
|
+
}
|
|
56
|
+
function validateProviderSessionId(value) {
|
|
57
|
+
if (value === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
if (typeof value !== "string" ||
|
|
60
|
+
value.trim().length === 0 ||
|
|
61
|
+
value.length > 2048 ||
|
|
62
|
+
/[\p{Cc}\p{Zl}\p{Zp}]/u.test(value)) {
|
|
63
|
+
throw new Error("Invalid provider session id");
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function validateIntegrationStatus(value) {
|
|
68
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
69
|
+
throw new Error("Invalid integration status");
|
|
70
|
+
}
|
|
71
|
+
const record = value;
|
|
72
|
+
const keys = Object.keys(record);
|
|
73
|
+
if (keys.some((key) => !["attachments", "activity", "detail"].includes(key)) ||
|
|
74
|
+
(record.attachments !== "ready" && record.attachments !== "degraded") ||
|
|
75
|
+
(record.activity !== "ready" && record.activity !== "degraded") ||
|
|
76
|
+
(record.detail !== undefined && (typeof record.detail !== "string" || record.detail.length > 2048))) {
|
|
77
|
+
throw new Error("Invalid integration status");
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
attachments: record.attachments,
|
|
81
|
+
activity: record.activity,
|
|
82
|
+
...(typeof record.detail === "string" && record.detail.length > 0 ? { detail: record.detail } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function parseIntegrationStatus(raw) {
|
|
86
|
+
if (raw === null || raw.length === 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
return validateIntegrationStatus(JSON.parse(raw));
|
|
89
|
+
}
|
|
90
|
+
function providerRowToSession(r) {
|
|
91
|
+
try {
|
|
92
|
+
if (r.provider !== "codex")
|
|
93
|
+
return undefined;
|
|
94
|
+
const launchOptions = parseCodexOptions(JSON.parse(r.launch_options_json));
|
|
95
|
+
const integrationStatus = parseIntegrationStatus(r.integration_status_json);
|
|
96
|
+
return {
|
|
97
|
+
provider: "codex",
|
|
98
|
+
id: r.id,
|
|
99
|
+
cwd: r.cwd,
|
|
100
|
+
status: r.status,
|
|
101
|
+
createdAt: r.created_at,
|
|
102
|
+
lastActivityAt: r.last_activity_at,
|
|
103
|
+
mode: "terminal",
|
|
104
|
+
launchOptions,
|
|
105
|
+
...(typeof r.name === "string" && r.name.length > 0 ? { name: r.name } : {}),
|
|
106
|
+
...(typeof r.provider_session_id === "string" && r.provider_session_id.length > 0
|
|
107
|
+
? { providerSessionId: r.provider_session_id }
|
|
108
|
+
: {}),
|
|
109
|
+
...(integrationStatus ? { integrationStatus } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Keep corrupt rows in SQLite for diagnostics, but never surface them to a launcher.
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function compareSessions(a, b) {
|
|
118
|
+
return a.createdAt - b.createdAt || a.id.localeCompare(b.id);
|
|
119
|
+
}
|
|
120
|
+
function cloneSession(session) {
|
|
121
|
+
if (session.provider === "claude") {
|
|
122
|
+
return { ...session, ...(session.spawnArgs ? { spawnArgs: [...session.spawnArgs] } : {}) };
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
...session,
|
|
126
|
+
launchOptions: {
|
|
127
|
+
...session.launchOptions,
|
|
128
|
+
...(session.launchOptions.addDirs ? { addDirs: [...session.launchOptions.addDirs] } : {}),
|
|
129
|
+
},
|
|
130
|
+
...(session.integrationStatus ? { integrationStatus: { ...session.integrationStatus } } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function cloneStoredSessionDefaults(value) {
|
|
134
|
+
return {
|
|
135
|
+
defaults: normalizeSessionDefaults(value.defaults),
|
|
136
|
+
revision: value.revision,
|
|
137
|
+
updatedAt: value.updatedAt,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function appSettingRowToSessionDefaults(row) {
|
|
141
|
+
if (!row)
|
|
142
|
+
return undefined;
|
|
143
|
+
try {
|
|
144
|
+
return {
|
|
145
|
+
defaults: normalizeSessionDefaults(JSON.parse(row.value_json)),
|
|
146
|
+
revision: row.revision,
|
|
147
|
+
updatedAt: row.updated_at,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* In-memory fallback used when the native better-sqlite3 module cannot load
|
|
156
|
+
* (no toolchain / unsupported platform) so the server still boots. NOT durable
|
|
157
|
+
* across process restarts — surfaced as a diagnostic by the caller.
|
|
158
|
+
*/
|
|
159
|
+
function inMemoryStore() {
|
|
160
|
+
const map = new Map();
|
|
161
|
+
const provisionalProviderSessionIds = new Map();
|
|
162
|
+
let sessionDefaults;
|
|
163
|
+
const write = (s) => {
|
|
164
|
+
provisionalProviderSessionIds.delete(s.id);
|
|
165
|
+
if (s.provider === "codex") {
|
|
166
|
+
const launchOptions = validateCodexOptions(s.launchOptions);
|
|
167
|
+
const integrationStatus = s.integrationStatus ? validateIntegrationStatus(s.integrationStatus) : undefined;
|
|
168
|
+
const providerSessionId = validateProviderSessionId(s.providerSessionId);
|
|
169
|
+
map.set(s.id, cloneSession({
|
|
170
|
+
...s,
|
|
171
|
+
mode: "terminal",
|
|
172
|
+
launchOptions,
|
|
173
|
+
...(providerSessionId ? { providerSessionId } : {}),
|
|
174
|
+
...(integrationStatus ? { integrationStatus } : {}),
|
|
175
|
+
}));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (s.provider !== "claude")
|
|
179
|
+
throw new Error("Unknown session provider");
|
|
180
|
+
map.set(s.id, cloneSession({ ...s, mode: "terminal" }));
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
claimNew: (s) => {
|
|
184
|
+
if (map.has(s.id))
|
|
185
|
+
throw new Error(`Session id ${s.id} already exists`);
|
|
186
|
+
write(s);
|
|
187
|
+
},
|
|
188
|
+
upsert: (s) => {
|
|
189
|
+
const existing = map.get(s.id);
|
|
190
|
+
if (existing && existing.provider !== s.provider) {
|
|
191
|
+
throw new Error(`Session id ${s.id} already belongs to ${existing.provider}`);
|
|
192
|
+
}
|
|
193
|
+
write(s);
|
|
194
|
+
},
|
|
195
|
+
has: (id) => map.has(id),
|
|
196
|
+
get: (id) => {
|
|
197
|
+
const v = map.get(id);
|
|
198
|
+
return v ? cloneSession(v) : undefined;
|
|
199
|
+
},
|
|
200
|
+
list: () => [...map.values()].map(cloneSession).sort(compareSessions),
|
|
201
|
+
setStatus: (id, status) => {
|
|
202
|
+
const v = map.get(id);
|
|
203
|
+
if (v)
|
|
204
|
+
v.status = status;
|
|
205
|
+
},
|
|
206
|
+
touch: (id, at) => {
|
|
207
|
+
const v = map.get(id);
|
|
208
|
+
if (v)
|
|
209
|
+
v.lastActivityAt = at;
|
|
210
|
+
},
|
|
211
|
+
setName: (id, name) => {
|
|
212
|
+
const v = map.get(id);
|
|
213
|
+
if (!v)
|
|
214
|
+
return;
|
|
215
|
+
if (name === undefined)
|
|
216
|
+
delete v.name; // clear = the field goes back to absent, mirroring NULL
|
|
217
|
+
else
|
|
218
|
+
v.name = name;
|
|
219
|
+
},
|
|
220
|
+
setProviderSessionId: (id, value) => {
|
|
221
|
+
const v = map.get(id);
|
|
222
|
+
if (!v || v.provider !== "codex")
|
|
223
|
+
return;
|
|
224
|
+
const providerSessionId = validateProviderSessionId(value);
|
|
225
|
+
provisionalProviderSessionIds.delete(id);
|
|
226
|
+
if (providerSessionId === undefined)
|
|
227
|
+
delete v.providerSessionId;
|
|
228
|
+
else
|
|
229
|
+
v.providerSessionId = providerSessionId;
|
|
230
|
+
},
|
|
231
|
+
markProvisionalProviderSessionId: (id, value) => {
|
|
232
|
+
const v = map.get(id);
|
|
233
|
+
const providerSessionId = validateProviderSessionId(value);
|
|
234
|
+
if (!v || v.provider !== "codex" || v.providerSessionId !== undefined || provisionalProviderSessionIds.has(id)) {
|
|
235
|
+
throw new Error("Provisional provider identity unavailable");
|
|
236
|
+
}
|
|
237
|
+
provisionalProviderSessionIds.set(id, providerSessionId);
|
|
238
|
+
},
|
|
239
|
+
clearProvisionalProviderSessionId: (id, value) => {
|
|
240
|
+
if (provisionalProviderSessionIds.get(id) !== value)
|
|
241
|
+
throw new Error("Provisional provider identity changed");
|
|
242
|
+
provisionalProviderSessionIds.delete(id);
|
|
243
|
+
},
|
|
244
|
+
commitProvisionalProviderSessionId: (id, value) => {
|
|
245
|
+
const v = map.get(id);
|
|
246
|
+
if (!v ||
|
|
247
|
+
v.provider !== "codex" ||
|
|
248
|
+
v.providerSessionId !== undefined ||
|
|
249
|
+
provisionalProviderSessionIds.get(id) !== value) {
|
|
250
|
+
throw new Error("Provisional provider identity changed");
|
|
251
|
+
}
|
|
252
|
+
v.providerSessionId = value;
|
|
253
|
+
provisionalProviderSessionIds.delete(id);
|
|
254
|
+
},
|
|
255
|
+
getSessionDefaults: () => (sessionDefaults ? cloneStoredSessionDefaults(sessionDefaults) : undefined),
|
|
256
|
+
putSessionDefaults: (defaults, expectedRevision, updatedAt) => {
|
|
257
|
+
if ((sessionDefaults?.revision ?? 0) !== expectedRevision) {
|
|
258
|
+
throw new SessionDefaultsConflictError(sessionDefaults ? cloneStoredSessionDefaults(sessionDefaults) : undefined);
|
|
259
|
+
}
|
|
260
|
+
sessionDefaults = {
|
|
261
|
+
defaults: normalizeSessionDefaults(defaults),
|
|
262
|
+
revision: expectedRevision + 1,
|
|
263
|
+
updatedAt,
|
|
264
|
+
};
|
|
265
|
+
return cloneStoredSessionDefaults(sessionDefaults);
|
|
266
|
+
},
|
|
267
|
+
delete: (id) => {
|
|
268
|
+
provisionalProviderSessionIds.delete(id);
|
|
269
|
+
map.delete(id);
|
|
270
|
+
},
|
|
271
|
+
close: () => {
|
|
272
|
+
provisionalProviderSessionIds.clear();
|
|
273
|
+
map.clear();
|
|
274
|
+
sessionDefaults = undefined;
|
|
275
|
+
},
|
|
276
|
+
mode: "memory-fallback",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
export function openSessionStore(opts) {
|
|
280
|
+
let Database;
|
|
281
|
+
try {
|
|
282
|
+
// Dynamic require keeps the native dep out of the module graph until needed
|
|
283
|
+
// and lets us fall back gracefully if the build is missing. Injectable so tests force the fallback.
|
|
284
|
+
if (opts.loadDatabase) {
|
|
285
|
+
Database = opts.loadDatabase();
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
const mod = require("better-sqlite3");
|
|
289
|
+
Database = (mod.default ?? mod);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return brandSessionStore(inMemoryStore());
|
|
294
|
+
}
|
|
295
|
+
const db = new Database(opts.dbPath);
|
|
296
|
+
db.pragma("journal_mode = WAL");
|
|
297
|
+
db.pragma("busy_timeout = 5000");
|
|
298
|
+
db.exec(`
|
|
299
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
300
|
+
id TEXT PRIMARY KEY,
|
|
301
|
+
cwd TEXT NOT NULL,
|
|
302
|
+
dangerously_skip INTEGER NOT NULL DEFAULT 0,
|
|
303
|
+
status TEXT NOT NULL,
|
|
304
|
+
created_at INTEGER NOT NULL,
|
|
305
|
+
last_activity_at INTEGER NOT NULL,
|
|
306
|
+
mode TEXT NOT NULL DEFAULT 'terminal',
|
|
307
|
+
name TEXT
|
|
308
|
+
)
|
|
309
|
+
`);
|
|
310
|
+
// TERMINAL-ONLY MIGRATION: a chat-era DB carries dead columns (model, effort, permission_mode,
|
|
311
|
+
// display_name, context_window) and possibly stale mode!='terminal' rows the app never surfaces.
|
|
312
|
+
// Drop the dead columns and prune the stale rows so the schema matches the terminal-only model. All
|
|
313
|
+
// guarded (best-effort): on a FRESH DB the columns don't exist ("no such column"); on an ancient SQLite
|
|
314
|
+
// DROP COLUMN may be unsupported — either way we swallow and boot with the columns simply unused.
|
|
315
|
+
for (const col of ["model", "effort", "permission_mode", "display_name", "context_window"]) {
|
|
316
|
+
try {
|
|
317
|
+
db.exec(`ALTER TABLE sessions DROP COLUMN ${col}`);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// column already gone (fresh DB) or DROP COLUMN unsupported — harmless, leave it be
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// A pre-`mode` (Plan-5) DB lacks the column entirely; add it so the prune + queries below work.
|
|
324
|
+
try {
|
|
325
|
+
db.exec("ALTER TABLE sessions ADD COLUMN mode TEXT NOT NULL DEFAULT 'terminal'");
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// column already exists — nothing to do
|
|
329
|
+
}
|
|
330
|
+
// Session-name migration: a pre-name DB gains the nullable column; existing rows read back name-less
|
|
331
|
+
// (NULL → the field stays absent), so old sessions are simply "unnamed" — no backfill needed.
|
|
332
|
+
try {
|
|
333
|
+
db.exec("ALTER TABLE sessions ADD COLUMN name TEXT");
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// column already exists — nothing to do
|
|
337
|
+
}
|
|
338
|
+
// spawn-args migration: the user's chosen spawn flags (JSON), so a RESTART can respawn an ended session
|
|
339
|
+
// with the same model/effort/permission/danger/add-dir. A pre-spawn_args DB gains the nullable column;
|
|
340
|
+
// existing rows read back NULL → those sessions respawn flag-less (the old behavior), no backfill needed.
|
|
341
|
+
try {
|
|
342
|
+
db.exec("ALTER TABLE sessions ADD COLUMN spawn_args TEXT");
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// column already exists — nothing to do
|
|
346
|
+
}
|
|
347
|
+
// Prune leftover non-terminal (chat) rows — never adopted or surfaced by the terminal app, just cruft.
|
|
348
|
+
try {
|
|
349
|
+
db.exec("DELETE FROM sessions WHERE mode IS NOT NULL AND mode != 'terminal'");
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// best-effort — never block boot on the prune
|
|
353
|
+
}
|
|
354
|
+
// Rollback-safe provider migration: Codex rows live only in this additive table. The legacy `sessions`
|
|
355
|
+
// table remains byte-for-byte compatible with older builds, which continue to see Claude rows only.
|
|
356
|
+
db.exec(`
|
|
357
|
+
CREATE TABLE IF NOT EXISTS provider_sessions (
|
|
358
|
+
id TEXT PRIMARY KEY,
|
|
359
|
+
provider TEXT NOT NULL CHECK (provider = 'codex'),
|
|
360
|
+
cwd TEXT NOT NULL,
|
|
361
|
+
status TEXT NOT NULL,
|
|
362
|
+
created_at INTEGER NOT NULL,
|
|
363
|
+
last_activity_at INTEGER NOT NULL,
|
|
364
|
+
name TEXT,
|
|
365
|
+
provider_session_id TEXT,
|
|
366
|
+
provisional_provider_session_id TEXT,
|
|
367
|
+
launch_options_json TEXT NOT NULL,
|
|
368
|
+
integration_status_json TEXT
|
|
369
|
+
);
|
|
370
|
+
CREATE INDEX IF NOT EXISTS provider_sessions_activity_idx
|
|
371
|
+
ON provider_sessions(last_activity_at DESC);
|
|
372
|
+
|
|
373
|
+
CREATE TABLE IF NOT EXISTS app_settings (
|
|
374
|
+
key TEXT PRIMARY KEY,
|
|
375
|
+
value_json TEXT NOT NULL,
|
|
376
|
+
revision INTEGER NOT NULL CHECK (revision > 0),
|
|
377
|
+
updated_at INTEGER NOT NULL
|
|
378
|
+
);
|
|
379
|
+
`);
|
|
380
|
+
try {
|
|
381
|
+
db.exec("ALTER TABLE provider_sessions ADD COLUMN provisional_provider_session_id TEXT");
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// Column already exists on fresh/current databases.
|
|
385
|
+
}
|
|
386
|
+
// A previous process can only leave provisional state by terminating before cross-check/rollback.
|
|
387
|
+
// It is never resumable and is safe to discard before this process accepts work.
|
|
388
|
+
db.exec("UPDATE provider_sessions SET provisional_provider_session_id = NULL");
|
|
389
|
+
const legacyUpsertStmt = db.prepare(`
|
|
390
|
+
INSERT INTO sessions (id, cwd, dangerously_skip, status, created_at, last_activity_at, mode, name, spawn_args)
|
|
391
|
+
VALUES (@id, @cwd, @dangerously_skip, @status, @created_at, @last_activity_at, @mode, @name, @spawn_args)
|
|
392
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
393
|
+
cwd=excluded.cwd, dangerously_skip=excluded.dangerously_skip,
|
|
394
|
+
status=excluded.status, created_at=excluded.created_at, last_activity_at=excluded.last_activity_at,
|
|
395
|
+
mode=excluded.mode, name=excluded.name, spawn_args=excluded.spawn_args
|
|
396
|
+
`);
|
|
397
|
+
const providerUpsertStmt = db.prepare(`
|
|
398
|
+
INSERT INTO provider_sessions (
|
|
399
|
+
id, provider, cwd, status, created_at, last_activity_at, name,
|
|
400
|
+
provider_session_id, provisional_provider_session_id, launch_options_json, integration_status_json
|
|
401
|
+
) VALUES (
|
|
402
|
+
@id, @provider, @cwd, @status, @created_at, @last_activity_at, @name,
|
|
403
|
+
@provider_session_id, NULL, @launch_options_json, @integration_status_json
|
|
404
|
+
)
|
|
405
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
406
|
+
provider=excluded.provider, cwd=excluded.cwd, status=excluded.status,
|
|
407
|
+
created_at=excluded.created_at, last_activity_at=excluded.last_activity_at,
|
|
408
|
+
name=excluded.name, provider_session_id=excluded.provider_session_id,
|
|
409
|
+
provisional_provider_session_id=NULL, launch_options_json=excluded.launch_options_json,
|
|
410
|
+
integration_status_json=excluded.integration_status_json
|
|
411
|
+
`);
|
|
412
|
+
const legacyInsertStmt = db.prepare(`
|
|
413
|
+
INSERT INTO sessions (id, cwd, dangerously_skip, status, created_at, last_activity_at, mode, name, spawn_args)
|
|
414
|
+
VALUES (@id, @cwd, @dangerously_skip, @status, @created_at, @last_activity_at, @mode, @name, @spawn_args)
|
|
415
|
+
`);
|
|
416
|
+
const providerInsertStmt = db.prepare(`
|
|
417
|
+
INSERT INTO provider_sessions (
|
|
418
|
+
id, provider, cwd, status, created_at, last_activity_at, name,
|
|
419
|
+
provider_session_id, provisional_provider_session_id, launch_options_json, integration_status_json
|
|
420
|
+
) VALUES (
|
|
421
|
+
@id, @provider, @cwd, @status, @created_at, @last_activity_at, @name,
|
|
422
|
+
@provider_session_id, NULL, @launch_options_json, @integration_status_json
|
|
423
|
+
)
|
|
424
|
+
`);
|
|
425
|
+
const legacyGetStmt = db.prepare("SELECT * FROM sessions WHERE id = ?");
|
|
426
|
+
const providerGetStmt = db.prepare("SELECT * FROM provider_sessions WHERE id = ?");
|
|
427
|
+
const legacyListStmt = db.prepare("SELECT * FROM sessions");
|
|
428
|
+
const providerListStmt = db.prepare("SELECT * FROM provider_sessions");
|
|
429
|
+
const legacyStatusStmt = db.prepare("UPDATE sessions SET status = ? WHERE id = ?");
|
|
430
|
+
const providerStatusStmt = db.prepare("UPDATE provider_sessions SET status = ? WHERE id = ?");
|
|
431
|
+
const legacyTouchStmt = db.prepare("UPDATE sessions SET last_activity_at = ? WHERE id = ?");
|
|
432
|
+
const providerTouchStmt = db.prepare("UPDATE provider_sessions SET last_activity_at = ? WHERE id = ?");
|
|
433
|
+
const legacyNameStmt = db.prepare("UPDATE sessions SET name = ? WHERE id = ?");
|
|
434
|
+
const providerNameStmt = db.prepare("UPDATE provider_sessions SET name = ? WHERE id = ?");
|
|
435
|
+
const providerSessionIdStmt = db.prepare("UPDATE provider_sessions SET provider_session_id = ?, provisional_provider_session_id = NULL WHERE id = ?");
|
|
436
|
+
const providerMarkProvisionalStmt = db.prepare(`
|
|
437
|
+
UPDATE provider_sessions
|
|
438
|
+
SET provisional_provider_session_id = ?
|
|
439
|
+
WHERE id = ? AND provider_session_id IS NULL AND provisional_provider_session_id IS NULL
|
|
440
|
+
`);
|
|
441
|
+
const providerClearProvisionalStmt = db.prepare(`
|
|
442
|
+
UPDATE provider_sessions
|
|
443
|
+
SET provisional_provider_session_id = NULL
|
|
444
|
+
WHERE id = ? AND provider_session_id IS NULL AND provisional_provider_session_id = ?
|
|
445
|
+
`);
|
|
446
|
+
const providerCommitProvisionalStmt = db.prepare(`
|
|
447
|
+
UPDATE provider_sessions
|
|
448
|
+
SET provider_session_id = ?, provisional_provider_session_id = NULL
|
|
449
|
+
WHERE id = ? AND provider_session_id IS NULL AND provisional_provider_session_id = ?
|
|
450
|
+
`);
|
|
451
|
+
const legacyDeleteStmt = db.prepare("DELETE FROM sessions WHERE id = ?");
|
|
452
|
+
const providerDeleteStmt = db.prepare("DELETE FROM provider_sessions WHERE id = ?");
|
|
453
|
+
const sessionDefaultsGetStmt = db.prepare("SELECT * FROM app_settings WHERE key = 'session_defaults'");
|
|
454
|
+
const sessionDefaultsPutStmt = db.prepare(`
|
|
455
|
+
INSERT INTO app_settings (key, value_json, revision, updated_at)
|
|
456
|
+
VALUES ('session_defaults', ?, ?, ?)
|
|
457
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
458
|
+
value_json=excluded.value_json, revision=excluded.revision, updated_at=excluded.updated_at
|
|
459
|
+
`);
|
|
460
|
+
const ownerOf = (id) => {
|
|
461
|
+
const hasLegacy = legacyGetStmt.get(id) !== undefined;
|
|
462
|
+
const hasProvider = providerGetStmt.get(id) !== undefined;
|
|
463
|
+
// A cross-table id collision is ambiguous. Fail closed instead of mutating or launching the wrong owner.
|
|
464
|
+
if (hasLegacy === hasProvider)
|
|
465
|
+
return undefined;
|
|
466
|
+
return hasLegacy ? "claude" : "codex";
|
|
467
|
+
};
|
|
468
|
+
const upsertLegacyAtomically = db.transaction((s) => {
|
|
469
|
+
if (providerGetStmt.get(s.id) !== undefined) {
|
|
470
|
+
throw new Error(`Session id ${s.id} already belongs to codex`);
|
|
471
|
+
}
|
|
472
|
+
legacyUpsertStmt.run({
|
|
473
|
+
id: s.id,
|
|
474
|
+
cwd: s.cwd,
|
|
475
|
+
dangerously_skip: s.dangerouslySkip ? 1 : 0,
|
|
476
|
+
status: s.status,
|
|
477
|
+
created_at: s.createdAt,
|
|
478
|
+
last_activity_at: s.lastActivityAt,
|
|
479
|
+
mode: s.mode ?? "terminal",
|
|
480
|
+
name: s.name ?? null,
|
|
481
|
+
spawn_args: s.spawnArgs && s.spawnArgs.length > 0 ? JSON.stringify(s.spawnArgs) : null,
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
const upsertProviderAtomically = db.transaction((s, launchOptions, integrationStatus, providerSessionId) => {
|
|
485
|
+
if (legacyGetStmt.get(s.id) !== undefined) {
|
|
486
|
+
throw new Error(`Session id ${s.id} already belongs to claude`);
|
|
487
|
+
}
|
|
488
|
+
providerUpsertStmt.run({
|
|
489
|
+
id: s.id,
|
|
490
|
+
provider: "codex",
|
|
491
|
+
cwd: s.cwd,
|
|
492
|
+
status: s.status,
|
|
493
|
+
created_at: s.createdAt,
|
|
494
|
+
last_activity_at: s.lastActivityAt,
|
|
495
|
+
name: s.name ?? null,
|
|
496
|
+
provider_session_id: providerSessionId ?? null,
|
|
497
|
+
launch_options_json: JSON.stringify(launchOptions),
|
|
498
|
+
integration_status_json: integrationStatus ? JSON.stringify(integrationStatus) : null,
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
const claimLegacyAtomically = db.transaction((s) => {
|
|
502
|
+
if (legacyGetStmt.get(s.id) !== undefined || providerGetStmt.get(s.id) !== undefined) {
|
|
503
|
+
throw new Error(`Session id ${s.id} already exists`);
|
|
504
|
+
}
|
|
505
|
+
legacyInsertStmt.run({
|
|
506
|
+
id: s.id,
|
|
507
|
+
cwd: s.cwd,
|
|
508
|
+
dangerously_skip: s.dangerouslySkip ? 1 : 0,
|
|
509
|
+
status: s.status,
|
|
510
|
+
created_at: s.createdAt,
|
|
511
|
+
last_activity_at: s.lastActivityAt,
|
|
512
|
+
mode: s.mode ?? "terminal",
|
|
513
|
+
name: s.name ?? null,
|
|
514
|
+
spawn_args: s.spawnArgs && s.spawnArgs.length > 0 ? JSON.stringify(s.spawnArgs) : null,
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
const claimProviderAtomically = db.transaction((s, launchOptions, integrationStatus, providerSessionId) => {
|
|
518
|
+
if (legacyGetStmt.get(s.id) !== undefined || providerGetStmt.get(s.id) !== undefined) {
|
|
519
|
+
throw new Error(`Session id ${s.id} already exists`);
|
|
520
|
+
}
|
|
521
|
+
providerInsertStmt.run({
|
|
522
|
+
id: s.id,
|
|
523
|
+
provider: "codex",
|
|
524
|
+
cwd: s.cwd,
|
|
525
|
+
status: s.status,
|
|
526
|
+
created_at: s.createdAt,
|
|
527
|
+
last_activity_at: s.lastActivityAt,
|
|
528
|
+
name: s.name ?? null,
|
|
529
|
+
provider_session_id: providerSessionId ?? null,
|
|
530
|
+
launch_options_json: JSON.stringify(launchOptions),
|
|
531
|
+
integration_status_json: integrationStatus ? JSON.stringify(integrationStatus) : null,
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
const putSessionDefaultsAtomically = db.transaction((defaults, expectedRevision, updatedAt) => {
|
|
535
|
+
const row = sessionDefaultsGetStmt.get();
|
|
536
|
+
const current = appSettingRowToSessionDefaults(row);
|
|
537
|
+
if (row !== undefined && current === undefined) {
|
|
538
|
+
throw new SessionDefaultsConflictError(undefined);
|
|
539
|
+
}
|
|
540
|
+
if ((row?.revision ?? 0) !== expectedRevision) {
|
|
541
|
+
throw new SessionDefaultsConflictError(current ? cloneStoredSessionDefaults(current) : undefined);
|
|
542
|
+
}
|
|
543
|
+
const stored = {
|
|
544
|
+
defaults,
|
|
545
|
+
revision: (row?.revision ?? 0) + 1,
|
|
546
|
+
updatedAt,
|
|
547
|
+
};
|
|
548
|
+
sessionDefaultsPutStmt.run(JSON.stringify(defaults), stored.revision, updatedAt);
|
|
549
|
+
return cloneStoredSessionDefaults(stored);
|
|
550
|
+
});
|
|
551
|
+
return brandSessionStore({
|
|
552
|
+
claimNew: (s) => {
|
|
553
|
+
if (s.provider === "claude") {
|
|
554
|
+
claimLegacyAtomically.immediate(s);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (s.provider !== "codex")
|
|
558
|
+
throw new Error("Unknown session provider");
|
|
559
|
+
const launchOptions = validateCodexOptions(s.launchOptions);
|
|
560
|
+
const integrationStatus = s.integrationStatus ? validateIntegrationStatus(s.integrationStatus) : undefined;
|
|
561
|
+
const providerSessionId = validateProviderSessionId(s.providerSessionId);
|
|
562
|
+
claimProviderAtomically.immediate(s, launchOptions, integrationStatus, providerSessionId);
|
|
563
|
+
},
|
|
564
|
+
upsert: (s) => {
|
|
565
|
+
if (s.provider === "claude") {
|
|
566
|
+
upsertLegacyAtomically.immediate(s);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (s.provider !== "codex")
|
|
570
|
+
throw new Error("Unknown session provider");
|
|
571
|
+
const launchOptions = validateCodexOptions(s.launchOptions);
|
|
572
|
+
const integrationStatus = s.integrationStatus ? validateIntegrationStatus(s.integrationStatus) : undefined;
|
|
573
|
+
const providerSessionId = validateProviderSessionId(s.providerSessionId);
|
|
574
|
+
upsertProviderAtomically.immediate(s, launchOptions, integrationStatus, providerSessionId);
|
|
575
|
+
},
|
|
576
|
+
has: (id) => legacyGetStmt.get(id) !== undefined || providerGetStmt.get(id) !== undefined,
|
|
577
|
+
get: (id) => {
|
|
578
|
+
const owner = ownerOf(id);
|
|
579
|
+
if (owner === "claude") {
|
|
580
|
+
return legacyRowToSession(legacyGetStmt.get(id));
|
|
581
|
+
}
|
|
582
|
+
if (owner === "codex") {
|
|
583
|
+
return providerRowToSession(providerGetStmt.get(id));
|
|
584
|
+
}
|
|
585
|
+
return undefined;
|
|
586
|
+
},
|
|
587
|
+
list: () => {
|
|
588
|
+
const legacyRows = legacyListStmt.all();
|
|
589
|
+
const providerRows = providerListStmt.all();
|
|
590
|
+
const providerIds = new Set(providerRows.map((row) => row.id));
|
|
591
|
+
const ambiguousIds = new Set(legacyRows.filter((row) => providerIds.has(row.id)).map((row) => row.id));
|
|
592
|
+
const legacy = legacyRows.filter((row) => !ambiguousIds.has(row.id)).map(legacyRowToSession);
|
|
593
|
+
const provider = providerRows
|
|
594
|
+
.filter((row) => !ambiguousIds.has(row.id))
|
|
595
|
+
.map(providerRowToSession)
|
|
596
|
+
.filter((session) => session !== undefined);
|
|
597
|
+
return [...legacy, ...provider].sort(compareSessions);
|
|
598
|
+
},
|
|
599
|
+
setStatus: (id, status) => {
|
|
600
|
+
const owner = ownerOf(id);
|
|
601
|
+
if (owner === "claude")
|
|
602
|
+
legacyStatusStmt.run(status, id);
|
|
603
|
+
else if (owner === "codex")
|
|
604
|
+
providerStatusStmt.run(status, id);
|
|
605
|
+
},
|
|
606
|
+
touch: (id, at) => {
|
|
607
|
+
const owner = ownerOf(id);
|
|
608
|
+
if (owner === "claude")
|
|
609
|
+
legacyTouchStmt.run(at, id);
|
|
610
|
+
else if (owner === "codex")
|
|
611
|
+
providerTouchStmt.run(at, id);
|
|
612
|
+
},
|
|
613
|
+
setName: (id, name) => {
|
|
614
|
+
const owner = ownerOf(id);
|
|
615
|
+
if (owner === "claude")
|
|
616
|
+
legacyNameStmt.run(name ?? null, id);
|
|
617
|
+
else if (owner === "codex")
|
|
618
|
+
providerNameStmt.run(name ?? null, id);
|
|
619
|
+
},
|
|
620
|
+
setProviderSessionId: (id, value) => {
|
|
621
|
+
if (ownerOf(id) !== "codex")
|
|
622
|
+
return;
|
|
623
|
+
providerSessionIdStmt.run(validateProviderSessionId(value) ?? null, id);
|
|
624
|
+
},
|
|
625
|
+
markProvisionalProviderSessionId: (id, value) => {
|
|
626
|
+
const providerSessionId = validateProviderSessionId(value);
|
|
627
|
+
if (providerMarkProvisionalStmt.run(providerSessionId, id).changes !== 1) {
|
|
628
|
+
throw new Error("Provisional provider identity unavailable");
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
clearProvisionalProviderSessionId: (id, value) => {
|
|
632
|
+
if (providerClearProvisionalStmt.run(id, validateProviderSessionId(value)).changes !== 1) {
|
|
633
|
+
throw new Error("Provisional provider identity changed");
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
commitProvisionalProviderSessionId: (id, value) => {
|
|
637
|
+
const providerSessionId = validateProviderSessionId(value);
|
|
638
|
+
if (providerCommitProvisionalStmt.run(providerSessionId, id, providerSessionId).changes !== 1) {
|
|
639
|
+
throw new Error("Provisional provider identity changed");
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
getSessionDefaults: () => appSettingRowToSessionDefaults(sessionDefaultsGetStmt.get()),
|
|
643
|
+
putSessionDefaults: (defaults, expectedRevision, updatedAt) => putSessionDefaultsAtomically.immediate(normalizeSessionDefaults(defaults), expectedRevision, updatedAt),
|
|
644
|
+
delete: (id) => {
|
|
645
|
+
const owner = ownerOf(id);
|
|
646
|
+
if (owner === "claude")
|
|
647
|
+
legacyDeleteStmt.run(id);
|
|
648
|
+
else if (owner === "codex")
|
|
649
|
+
providerDeleteStmt.run(id);
|
|
650
|
+
},
|
|
651
|
+
close: () => db.close(),
|
|
652
|
+
mode: "sqlite",
|
|
653
|
+
});
|
|
654
|
+
}
|
package/dist/start.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ClaudeAvailability, ClaudeVersionProbe } from "./diag.js";
|
|
2
|
+
import type { CreateServerResult } from "./transport.js";
|
|
3
|
+
import type { ProviderAvailability } from "./providers/types.js";
|
|
4
|
+
export declare function providerPreflightWarning(name: string, availability: ProviderAvailability): string | undefined;
|
|
5
|
+
export declare function runProviderPreflight(providers: ReadonlyArray<{
|
|
6
|
+
name: string;
|
|
7
|
+
probe(): Promise<ProviderAvailability>;
|
|
8
|
+
}>, warn?: (msg: string) => void): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* STARTUP PREFLIGHT (#7): format a prominent, actionable boot warning when `claude --version` couldn't
|
|
11
|
+
* run. The server still boots (sessions just won't start until the operator fixes it) — this is purely
|
|
12
|
+
* to surface WHY in the logs immediately, like the better-sqlite3 fallback warning. Returns `undefined`
|
|
13
|
+
* when claude is available (no warning). PURE so it's unit-testable without spawning.
|
|
14
|
+
*/
|
|
15
|
+
export declare function claudePreflightWarning(availability: ClaudeAvailability): string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Run the startup preflight: best-effort probe `claude --version` and print {@link claudePreflightWarning}
|
|
18
|
+
* if it's missing/failing. NEVER throws and NEVER blocks boot — a hung/slow claude can't stall startup
|
|
19
|
+
* (the probe is short-timeout-guarded). Injectable probe + log sink so it's testable without a spawn.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runClaudePreflight(probe: ClaudeVersionProbe, warn?: (msg: string) => void): Promise<void>;
|
|
22
|
+
export declare function startServer(env?: NodeJS.ProcessEnv): Promise<CreateServerResult & {
|
|
23
|
+
url: string;
|
|
24
|
+
token?: string;
|
|
25
|
+
tokenGenerated: boolean;
|
|
26
|
+
}>;
|
|
27
|
+
/** Install process-wide crash guards so a stray unhandled rejection or a listener-less EventEmitter
|
|
28
|
+
* `error` (e.g. a write-after-teardown on a dying claude child, or a detached updater spawn failure)
|
|
29
|
+
* LOGS instead of taking the whole server down — for an always-on self-hosted server, staying up beats
|
|
30
|
+
* crashing. Install ONCE at a process entry (never in startServer, which tests call repeatedly). */
|
|
31
|
+
export declare function installCrashGuards(): void;
|