@nervmor/codexui 1.0.2 → 1.0.4

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/dist-cli/index.js CHANGED
@@ -3,10 +3,10 @@
3
3
  // src/cli/index.ts
4
4
  import { createServer as createServer2 } from "http";
5
5
  import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } from "fs";
6
- import { readFile as readFile4 } from "fs/promises";
7
- import { homedir as homedir3, networkInterfaces } from "os";
8
- import { join as join5 } from "path";
9
- import { spawn as spawn3, spawnSync } from "child_process";
6
+ import { readFile as readFile5, stat as stat6, writeFile as writeFile5 } from "fs/promises";
7
+ import { homedir as homedir4, networkInterfaces } from "os";
8
+ import { isAbsolute as isAbsolute3, join as join6, resolve as resolve2 } from "path";
9
+ import { spawn as spawn4, spawnSync } from "child_process";
10
10
  import { createInterface } from "readline/promises";
11
11
  import { fileURLToPath as fileURLToPath2 } from "url";
12
12
  import { dirname as dirname3 } from "path";
@@ -16,60 +16,897 @@ import qrcode from "qrcode-terminal";
16
16
 
17
17
  // src/server/httpServer.ts
18
18
  import { fileURLToPath } from "url";
19
- import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join4 } from "path";
19
+ import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join5 } from "path";
20
20
  import { existsSync as existsSync2 } from "fs";
21
- import { writeFile as writeFile3, stat as stat4 } from "fs/promises";
21
+ import { writeFile as writeFile4, stat as stat5 } from "fs/promises";
22
22
  import express from "express";
23
23
 
24
24
  // src/server/codexAppServerBridge.ts
25
- import { spawn as spawn2 } from "child_process";
25
+ import { spawn as spawn3 } from "child_process";
26
26
  import { randomBytes } from "crypto";
27
- import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as stat2 } from "fs/promises";
27
+ import { mkdtemp as mkdtemp3, readFile as readFile3, mkdir as mkdir3, stat as stat3 } from "fs/promises";
28
28
  import { request as httpsRequest } from "https";
29
- import { homedir as homedir2 } from "os";
30
- import { tmpdir as tmpdir2 } from "os";
31
- import { basename, isAbsolute, join as join2, resolve } from "path";
32
- import { writeFile as writeFile2 } from "fs/promises";
29
+ import { homedir as homedir3 } from "os";
30
+ import { tmpdir as tmpdir3 } from "os";
31
+ import { basename, isAbsolute, join as join3, resolve } from "path";
32
+ import { writeFile as writeFile3 } from "fs/promises";
33
33
 
34
- // src/server/skillsRoutes.ts
34
+ // src/server/accountRoutes.ts
35
35
  import { spawn } from "child_process";
36
- import { mkdtemp, readFile, readdir, rm, mkdir, stat, lstat, readlink, symlink } from "fs/promises";
37
- import { existsSync } from "fs";
36
+ import { createHash } from "crypto";
37
+ import { mkdtemp, mkdir, readFile, rm, stat, writeFile } from "fs/promises";
38
38
  import { homedir, tmpdir } from "os";
39
39
  import { join } from "path";
40
- import { writeFile } from "fs/promises";
40
+ var APP_SERVER_ARGS = [
41
+ "app-server",
42
+ "-c",
43
+ 'approval_policy="never"',
44
+ "-c",
45
+ 'sandbox_mode="danger-full-access"'
46
+ ];
47
+ var ACCOUNT_QUOTA_REFRESH_TTL_MS = 5 * 60 * 1e3;
48
+ var backgroundRefreshPromise = null;
41
49
  function asRecord(value) {
42
50
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
43
51
  }
52
+ function readString(value) {
53
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
54
+ }
55
+ function readNumber(value) {
56
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
57
+ }
58
+ function readBoolean(value) {
59
+ return typeof value === "boolean" ? value : null;
60
+ }
61
+ function normalizeAccountUnavailableReason(value) {
62
+ return value === "payment_required" ? value : null;
63
+ }
64
+ function setJson(res, statusCode, payload) {
65
+ res.statusCode = statusCode;
66
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
67
+ res.end(JSON.stringify(payload));
68
+ }
44
69
  function getErrorMessage(payload, fallback) {
45
70
  if (payload instanceof Error && payload.message.trim().length > 0) {
46
71
  return payload.message;
47
72
  }
48
73
  const record = asRecord(payload);
74
+ const error = record?.error;
75
+ if (typeof error === "string" && error.trim().length > 0) {
76
+ return error.trim();
77
+ }
78
+ if (typeof record?.message === "string" && record.message.trim().length > 0) {
79
+ return record.message.trim();
80
+ }
81
+ return fallback;
82
+ }
83
+ function isPaymentRequiredErrorMessage(value) {
84
+ if (!value) return false;
85
+ const normalized = value.toLowerCase();
86
+ return normalized.includes("payment required") || /\b402\b/.test(normalized);
87
+ }
88
+ function detectAccountUnavailableReason(error) {
89
+ return isPaymentRequiredErrorMessage(getErrorMessage(error, "")) ? "payment_required" : null;
90
+ }
91
+ function getCodexHomeDir() {
92
+ const codexHome = process.env.CODEX_HOME?.trim();
93
+ return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
94
+ }
95
+ function getActiveAuthPath() {
96
+ return join(getCodexHomeDir(), "auth.json");
97
+ }
98
+ function getAccountsStatePath() {
99
+ return join(getCodexHomeDir(), "accounts.json");
100
+ }
101
+ function getAccountsSnapshotRoot() {
102
+ return join(getCodexHomeDir(), "accounts");
103
+ }
104
+ function toStorageId(accountId) {
105
+ return createHash("sha256").update(accountId).digest("hex");
106
+ }
107
+ function normalizeRateLimitWindow(value) {
108
+ const record = asRecord(value);
109
+ if (!record) return null;
110
+ const usedPercent = readNumber(record.usedPercent ?? record.used_percent);
111
+ if (usedPercent === null) return null;
112
+ return {
113
+ usedPercent,
114
+ windowMinutes: readNumber(record.windowDurationMins ?? record.window_minutes),
115
+ resetsAt: readNumber(record.resetsAt ?? record.resets_at)
116
+ };
117
+ }
118
+ function normalizeCreditsSnapshot(value) {
119
+ const record = asRecord(value);
120
+ if (!record) return null;
121
+ const hasCredits = readBoolean(record.hasCredits ?? record.has_credits);
122
+ const unlimited = readBoolean(record.unlimited);
123
+ if (hasCredits === null || unlimited === null) return null;
124
+ return {
125
+ hasCredits,
126
+ unlimited,
127
+ balance: readString(record.balance)
128
+ };
129
+ }
130
+ function normalizeRateLimitSnapshot(value) {
131
+ const record = asRecord(value);
132
+ if (!record) return null;
133
+ const primary = normalizeRateLimitWindow(record.primary);
134
+ const secondary = normalizeRateLimitWindow(record.secondary);
135
+ const credits = normalizeCreditsSnapshot(record.credits);
136
+ if (!primary && !secondary && !credits) return null;
137
+ return {
138
+ limitId: readString(record.limitId ?? record.limit_id),
139
+ limitName: readString(record.limitName ?? record.limit_name),
140
+ primary,
141
+ secondary,
142
+ credits,
143
+ planType: readString(record.planType ?? record.plan_type)
144
+ };
145
+ }
146
+ function pickCodexRateLimitSnapshot(payload) {
147
+ const record = asRecord(payload);
148
+ if (!record) return null;
149
+ const rateLimitsByLimitId = asRecord(record.rateLimitsByLimitId ?? record.rate_limits_by_limit_id);
150
+ const codexBucket = normalizeRateLimitSnapshot(rateLimitsByLimitId?.codex);
151
+ if (codexBucket) return codexBucket;
152
+ return normalizeRateLimitSnapshot(record.rateLimits ?? record.rate_limits);
153
+ }
154
+ function normalizeStoredAccountEntry(value) {
155
+ const record = asRecord(value);
156
+ const accountId = readString(record?.accountId);
157
+ const storageId = readString(record?.storageId);
158
+ const lastRefreshedAtIso = readString(record?.lastRefreshedAtIso);
159
+ const quotaStatusRaw = readString(record?.quotaStatus);
160
+ const quotaStatus = quotaStatusRaw === "loading" || quotaStatusRaw === "ready" || quotaStatusRaw === "error" ? quotaStatusRaw : "idle";
161
+ if (!accountId || !storageId || !lastRefreshedAtIso) return null;
162
+ return {
163
+ accountId,
164
+ storageId,
165
+ authMode: readString(record?.authMode),
166
+ email: readString(record?.email),
167
+ planType: readString(record?.planType),
168
+ lastRefreshedAtIso,
169
+ lastActivatedAtIso: readString(record?.lastActivatedAtIso),
170
+ quotaSnapshot: normalizeRateLimitSnapshot(record?.quotaSnapshot),
171
+ quotaUpdatedAtIso: readString(record?.quotaUpdatedAtIso),
172
+ quotaStatus,
173
+ quotaError: readString(record?.quotaError),
174
+ unavailableReason: normalizeAccountUnavailableReason(record?.unavailableReason) ?? (isPaymentRequiredErrorMessage(readString(record?.quotaError)) ? "payment_required" : null)
175
+ };
176
+ }
177
+ async function readStoredAccountsState() {
178
+ try {
179
+ const raw = await readFile(getAccountsStatePath(), "utf8");
180
+ const parsed = asRecord(JSON.parse(raw));
181
+ const activeAccountId = readString(parsed?.activeAccountId);
182
+ const rawAccounts = Array.isArray(parsed?.accounts) ? parsed.accounts : [];
183
+ const accounts = rawAccounts.map((entry) => normalizeStoredAccountEntry(entry)).filter((entry) => entry !== null);
184
+ return { activeAccountId, accounts };
185
+ } catch {
186
+ return { activeAccountId: null, accounts: [] };
187
+ }
188
+ }
189
+ async function writeStoredAccountsState(state) {
190
+ await writeFile(getAccountsStatePath(), JSON.stringify(state, null, 2), { encoding: "utf8", mode: 384 });
191
+ }
192
+ function withUpsertedAccount(state, nextEntry) {
193
+ const rest = state.accounts.filter((entry) => entry.accountId !== nextEntry.accountId);
194
+ return {
195
+ activeAccountId: state.activeAccountId,
196
+ accounts: [nextEntry, ...rest]
197
+ };
198
+ }
199
+ function sortAccounts(accounts, activeAccountId) {
200
+ return [...accounts].sort((left, right) => {
201
+ const leftActive = left.accountId === activeAccountId ? 1 : 0;
202
+ const rightActive = right.accountId === activeAccountId ? 1 : 0;
203
+ if (leftActive !== rightActive) return rightActive - leftActive;
204
+ return right.lastRefreshedAtIso.localeCompare(left.lastRefreshedAtIso);
205
+ });
206
+ }
207
+ function toPublicAccountEntry(entry, activeAccountId) {
208
+ return {
209
+ ...entry,
210
+ isActive: entry.accountId === activeAccountId
211
+ };
212
+ }
213
+ function decodeBase64UrlJson(input) {
214
+ try {
215
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
216
+ const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
217
+ const raw = Buffer.from(`${normalized}${padding}`, "base64").toString("utf8");
218
+ const parsed = JSON.parse(raw);
219
+ return asRecord(parsed);
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
224
+ function extractTokenMetadata(accessToken) {
225
+ if (!accessToken || typeof accessToken !== "string") {
226
+ return { email: null, planType: null };
227
+ }
228
+ const parts = accessToken.split(".");
229
+ if (parts.length < 2) {
230
+ return { email: null, planType: null };
231
+ }
232
+ const payload = decodeBase64UrlJson(parts[1] ?? "");
233
+ const profile = asRecord(payload?.["https://api.openai.com/profile"]);
234
+ const auth = asRecord(payload?.["https://api.openai.com/auth"]);
235
+ return {
236
+ email: typeof profile?.email === "string" && profile.email.trim().length > 0 ? profile.email.trim() : null,
237
+ planType: typeof auth?.chatgpt_plan_type === "string" && auth.chatgpt_plan_type.trim().length > 0 ? auth.chatgpt_plan_type.trim() : null
238
+ };
239
+ }
240
+ async function readAuthFileFromPath(path) {
241
+ const raw = await readFile(path, "utf8");
242
+ const parsed = JSON.parse(raw);
243
+ const accountId = parsed.tokens?.account_id?.trim() ?? "";
244
+ if (!accountId) {
245
+ throw new Error("missing_account_id");
246
+ }
247
+ return {
248
+ raw,
249
+ parsed,
250
+ accountId,
251
+ authMode: typeof parsed.auth_mode === "string" && parsed.auth_mode.trim().length > 0 ? parsed.auth_mode.trim() : null,
252
+ metadata: extractTokenMetadata(parsed.tokens?.access_token)
253
+ };
254
+ }
255
+ function getSnapshotPath(storageId) {
256
+ return join(getAccountsSnapshotRoot(), storageId, "auth.json");
257
+ }
258
+ async function writeSnapshot(storageId, raw) {
259
+ const dir = join(getAccountsSnapshotRoot(), storageId);
260
+ await mkdir(dir, { recursive: true, mode: 448 });
261
+ await writeFile(getSnapshotPath(storageId), raw, { encoding: "utf8", mode: 384 });
262
+ }
263
+ async function removeSnapshot(storageId) {
264
+ await rm(join(getAccountsSnapshotRoot(), storageId), { recursive: true, force: true });
265
+ }
266
+ async function readRuntimeAccountMetadata(appServer) {
267
+ const payload = asRecord(await appServer.rpc("account/read", { refreshToken: false }));
268
+ const account = asRecord(payload?.account);
269
+ return {
270
+ email: typeof account?.email === "string" && account.email.trim().length > 0 ? account.email.trim() : null,
271
+ planType: typeof account?.planType === "string" && account.planType.trim().length > 0 ? account.planType.trim() : null
272
+ };
273
+ }
274
+ async function validateSwitchedAccount(appServer) {
275
+ const metadata = await readRuntimeAccountMetadata(appServer);
276
+ const quotaPayload = await appServer.rpc("account/rateLimits/read", null);
277
+ return {
278
+ metadata,
279
+ quotaSnapshot: pickCodexRateLimitSnapshot(quotaPayload)
280
+ };
281
+ }
282
+ async function restoreActiveAuth(raw) {
283
+ const path = getActiveAuthPath();
284
+ if (raw === null) {
285
+ await rm(path, { force: true });
286
+ return;
287
+ }
288
+ await writeFile(path, raw, { encoding: "utf8", mode: 384 });
289
+ }
290
+ async function fileExists(path) {
291
+ try {
292
+ await stat(path);
293
+ return true;
294
+ } catch {
295
+ return false;
296
+ }
297
+ }
298
+ async function withTemporaryCodexAppServer(authRaw, run) {
299
+ const tempCodexHome = await mkdtemp(join(tmpdir(), "codexui-account-"));
300
+ const authPath = join(tempCodexHome, "auth.json");
301
+ await writeFile(authPath, authRaw, { encoding: "utf8", mode: 384 });
302
+ const proc = spawn("codex", [...APP_SERVER_ARGS], {
303
+ env: { ...process.env, CODEX_HOME: tempCodexHome },
304
+ stdio: ["pipe", "pipe", "pipe"]
305
+ });
306
+ let disposed = false;
307
+ let initialized = false;
308
+ let initializePromise = null;
309
+ let readBuffer = "";
310
+ let nextId = 1;
311
+ const pending = /* @__PURE__ */ new Map();
312
+ const rejectAllPending = (error) => {
313
+ for (const request of pending.values()) {
314
+ request.reject(error);
315
+ }
316
+ pending.clear();
317
+ };
318
+ proc.stdout.setEncoding("utf8");
319
+ proc.stdout.on("data", (chunk) => {
320
+ readBuffer += chunk;
321
+ let lineEnd = readBuffer.indexOf("\n");
322
+ while (lineEnd !== -1) {
323
+ const line = readBuffer.slice(0, lineEnd).trim();
324
+ readBuffer = readBuffer.slice(lineEnd + 1);
325
+ if (line.length > 0) {
326
+ try {
327
+ const message = JSON.parse(line);
328
+ if (typeof message.id === "number" && pending.has(message.id)) {
329
+ const current = pending.get(message.id);
330
+ pending.delete(message.id);
331
+ if (!current) {
332
+ lineEnd = readBuffer.indexOf("\n");
333
+ continue;
334
+ }
335
+ if (message.error?.message) {
336
+ current.reject(new Error(message.error.message));
337
+ } else {
338
+ current.resolve(message.result);
339
+ }
340
+ }
341
+ } catch {
342
+ }
343
+ }
344
+ lineEnd = readBuffer.indexOf("\n");
345
+ }
346
+ });
347
+ proc.stderr.setEncoding("utf8");
348
+ proc.stderr.on("data", () => {
349
+ });
350
+ proc.on("error", (error) => {
351
+ rejectAllPending(error instanceof Error ? error : new Error("codex app-server failed to start"));
352
+ });
353
+ proc.on("exit", () => {
354
+ if (disposed) return;
355
+ rejectAllPending(new Error("codex app-server exited unexpectedly"));
356
+ });
357
+ const sendLine = (payload) => {
358
+ proc.stdin.write(`${JSON.stringify(payload)}
359
+ `);
360
+ };
361
+ const call = async (method, params) => {
362
+ const id = nextId++;
363
+ return await new Promise((resolve3, reject) => {
364
+ pending.set(id, { resolve: resolve3, reject });
365
+ sendLine({
366
+ jsonrpc: "2.0",
367
+ id,
368
+ method,
369
+ params
370
+ });
371
+ });
372
+ };
373
+ const ensureInitialized = async () => {
374
+ if (initialized) return;
375
+ if (initializePromise) {
376
+ await initializePromise;
377
+ return;
378
+ }
379
+ initializePromise = call("initialize", {
380
+ clientInfo: {
381
+ name: "codexui-account-refresh",
382
+ version: "0.1.0"
383
+ },
384
+ capabilities: {
385
+ experimentalApi: true
386
+ }
387
+ }).then(() => {
388
+ sendLine({
389
+ jsonrpc: "2.0",
390
+ method: "initialized"
391
+ });
392
+ initialized = true;
393
+ }).finally(() => {
394
+ initializePromise = null;
395
+ });
396
+ await initializePromise;
397
+ };
398
+ const dispose = async () => {
399
+ if (disposed) return;
400
+ disposed = true;
401
+ rejectAllPending(new Error("codex app-server stopped"));
402
+ try {
403
+ proc.stdin.end();
404
+ } catch {
405
+ }
406
+ try {
407
+ proc.kill("SIGTERM");
408
+ } catch {
409
+ }
410
+ await rm(tempCodexHome, { recursive: true, force: true });
411
+ };
412
+ try {
413
+ await ensureInitialized();
414
+ return await run(call);
415
+ } finally {
416
+ await dispose();
417
+ }
418
+ }
419
+ async function inspectStoredAccount(entry) {
420
+ const snapshotPath = getSnapshotPath(entry.storageId);
421
+ const authRaw = await readFile(snapshotPath, "utf8");
422
+ return await withTemporaryCodexAppServer(authRaw, async (rpc) => {
423
+ const accountPayload = asRecord(await rpc("account/read", { refreshToken: false }));
424
+ const account = asRecord(accountPayload?.account);
425
+ const quotaPayload = await rpc("account/rateLimits/read", null);
426
+ return {
427
+ metadata: {
428
+ email: typeof account?.email === "string" && account.email.trim().length > 0 ? account.email.trim() : entry.email,
429
+ planType: typeof account?.planType === "string" && account.planType.trim().length > 0 ? account.planType.trim() : entry.planType
430
+ },
431
+ quotaSnapshot: pickCodexRateLimitSnapshot(quotaPayload)
432
+ };
433
+ });
434
+ }
435
+ function shouldRefreshAccountQuota(entry) {
436
+ if (entry.quotaStatus === "loading") return false;
437
+ if (!entry.quotaUpdatedAtIso) return true;
438
+ const updatedAtMs = Date.parse(entry.quotaUpdatedAtIso);
439
+ if (!Number.isFinite(updatedAtMs)) return true;
440
+ return Date.now() - updatedAtMs >= ACCOUNT_QUOTA_REFRESH_TTL_MS;
441
+ }
442
+ async function replaceStoredAccount(nextEntry, activeAccountId) {
443
+ const state = await readStoredAccountsState();
444
+ const nextState = withUpsertedAccount({
445
+ activeAccountId,
446
+ accounts: state.accounts
447
+ }, nextEntry);
448
+ await writeStoredAccountsState({
449
+ activeAccountId,
450
+ accounts: nextState.accounts
451
+ });
452
+ }
453
+ async function pickReplacementActiveAccount(accounts) {
454
+ const sorted = sortAccounts(accounts, null);
455
+ for (const entry of sorted) {
456
+ if (entry.unavailableReason === "payment_required") continue;
457
+ if (await fileExists(getSnapshotPath(entry.storageId))) {
458
+ return entry;
459
+ }
460
+ }
461
+ return null;
462
+ }
463
+ async function refreshAccountsInBackground(accountIds, activeAccountId) {
464
+ for (const accountId of accountIds) {
465
+ const state = await readStoredAccountsState();
466
+ const entry = state.accounts.find((item) => item.accountId === accountId);
467
+ if (!entry) continue;
468
+ try {
469
+ const inspected = await inspectStoredAccount(entry);
470
+ await replaceStoredAccount({
471
+ ...entry,
472
+ email: inspected.metadata.email ?? entry.email,
473
+ planType: inspected.metadata.planType ?? entry.planType,
474
+ quotaSnapshot: inspected.quotaSnapshot ?? entry.quotaSnapshot,
475
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
476
+ quotaStatus: "ready",
477
+ quotaError: null,
478
+ unavailableReason: null
479
+ }, activeAccountId);
480
+ } catch (error) {
481
+ await replaceStoredAccount({
482
+ ...entry,
483
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
484
+ quotaStatus: "error",
485
+ quotaError: getErrorMessage(error, "Failed to refresh account quota"),
486
+ unavailableReason: detectAccountUnavailableReason(error)
487
+ }, activeAccountId);
488
+ }
489
+ }
490
+ }
491
+ async function scheduleAccountsBackgroundRefresh(options = {}) {
492
+ const state = await readStoredAccountsState();
493
+ if (state.accounts.length === 0) return state;
494
+ if (backgroundRefreshPromise) return state;
495
+ const allowedIds = options.accountIds ? new Set(options.accountIds) : null;
496
+ const candidates = state.accounts.filter((entry) => !allowedIds || allowedIds.has(entry.accountId)).filter((entry) => options.force === true || shouldRefreshAccountQuota(entry)).sort((left, right) => {
497
+ const prioritize = options.prioritizeAccountId ?? "";
498
+ const leftPriority = left.accountId === prioritize ? 1 : 0;
499
+ const rightPriority = right.accountId === prioritize ? 1 : 0;
500
+ if (leftPriority !== rightPriority) return rightPriority - leftPriority;
501
+ return 0;
502
+ });
503
+ if (candidates.length === 0) return state;
504
+ const candidateIds = new Set(candidates.map((entry) => entry.accountId));
505
+ const markedState = {
506
+ activeAccountId: state.activeAccountId,
507
+ accounts: state.accounts.map((entry) => candidateIds.has(entry.accountId) ? {
508
+ ...entry,
509
+ quotaStatus: "loading",
510
+ quotaError: null
511
+ } : entry)
512
+ };
513
+ await writeStoredAccountsState(markedState);
514
+ backgroundRefreshPromise = refreshAccountsInBackground(
515
+ candidates.map((entry) => entry.accountId),
516
+ markedState.activeAccountId
517
+ ).finally(() => {
518
+ backgroundRefreshPromise = null;
519
+ });
520
+ return markedState;
521
+ }
522
+ async function importAccountFromAuthPath(path) {
523
+ const imported = await readAuthFileFromPath(path);
524
+ const storageId = toStorageId(imported.accountId);
525
+ await writeSnapshot(storageId, imported.raw);
526
+ const state = await readStoredAccountsState();
527
+ const existing = state.accounts.find((entry) => entry.accountId === imported.accountId) ?? null;
528
+ const nextEntry = {
529
+ accountId: imported.accountId,
530
+ storageId,
531
+ authMode: imported.authMode,
532
+ email: imported.metadata.email ?? existing?.email ?? null,
533
+ planType: imported.metadata.planType ?? existing?.planType ?? null,
534
+ lastRefreshedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
535
+ lastActivatedAtIso: existing?.lastActivatedAtIso ?? null,
536
+ quotaSnapshot: existing?.quotaSnapshot ?? null,
537
+ quotaUpdatedAtIso: existing?.quotaUpdatedAtIso ?? null,
538
+ quotaStatus: existing?.quotaStatus ?? "idle",
539
+ quotaError: existing?.quotaError ?? null,
540
+ unavailableReason: existing?.unavailableReason ?? null
541
+ };
542
+ const nextState = withUpsertedAccount(state, nextEntry);
543
+ await writeStoredAccountsState(nextState);
544
+ return {
545
+ activeAccountId: nextState.activeAccountId,
546
+ importedAccountId: imported.accountId,
547
+ accounts: sortAccounts(nextState.accounts, nextState.activeAccountId).map((entry) => toPublicAccountEntry(entry, nextState.activeAccountId))
548
+ };
549
+ }
550
+ async function handleAccountRoutes(req, res, url, context) {
551
+ const { appServer } = context;
552
+ if (req.method === "GET" && url.pathname === "/codex-api/accounts") {
553
+ const state = await scheduleAccountsBackgroundRefresh();
554
+ setJson(res, 200, {
555
+ data: {
556
+ activeAccountId: state.activeAccountId,
557
+ accounts: sortAccounts(state.accounts, state.activeAccountId).map((entry) => toPublicAccountEntry(entry, state.activeAccountId))
558
+ }
559
+ });
560
+ return true;
561
+ }
562
+ if (req.method === "GET" && url.pathname === "/codex-api/accounts/active") {
563
+ const state = await readStoredAccountsState();
564
+ const active = state.activeAccountId ? state.accounts.find((entry) => entry.accountId === state.activeAccountId) ?? null : null;
565
+ setJson(res, 200, {
566
+ data: active ? toPublicAccountEntry(active, state.activeAccountId) : null
567
+ });
568
+ return true;
569
+ }
570
+ if (req.method === "POST" && url.pathname === "/codex-api/accounts/refresh") {
571
+ try {
572
+ const imported = await importAccountFromAuthPath(getActiveAuthPath());
573
+ try {
574
+ appServer.dispose();
575
+ const inspection = await validateSwitchedAccount(appServer);
576
+ const state = await readStoredAccountsState();
577
+ const importedAccountId = imported.importedAccountId;
578
+ const target = state.accounts.find((entry) => entry.accountId === importedAccountId) ?? null;
579
+ if (!target) {
580
+ throw new Error("account_not_found");
581
+ }
582
+ const nextEntry = {
583
+ ...target,
584
+ email: inspection.metadata.email ?? target.email,
585
+ planType: inspection.metadata.planType ?? target.planType,
586
+ lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
587
+ quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
588
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
589
+ quotaStatus: "ready",
590
+ quotaError: null,
591
+ unavailableReason: null
592
+ };
593
+ const nextState = withUpsertedAccount({
594
+ activeAccountId: importedAccountId,
595
+ accounts: state.accounts
596
+ }, nextEntry);
597
+ await writeStoredAccountsState({
598
+ activeAccountId: importedAccountId,
599
+ accounts: nextState.accounts
600
+ });
601
+ const backgroundState = await scheduleAccountsBackgroundRefresh({
602
+ force: true,
603
+ prioritizeAccountId: importedAccountId,
604
+ accountIds: nextState.accounts.filter((entry) => entry.accountId !== importedAccountId).map((entry) => entry.accountId)
605
+ });
606
+ setJson(res, 200, {
607
+ data: {
608
+ activeAccountId: importedAccountId,
609
+ importedAccountId,
610
+ accounts: sortAccounts(backgroundState.accounts, importedAccountId).map((entry) => toPublicAccountEntry(entry, importedAccountId))
611
+ }
612
+ });
613
+ } catch (error) {
614
+ setJson(res, 502, {
615
+ error: "account_refresh_failed",
616
+ message: getErrorMessage(error, "Failed to refresh account")
617
+ });
618
+ }
619
+ } catch (error) {
620
+ const message = getErrorMessage(error, "Failed to refresh account");
621
+ if (message === "missing_account_id") {
622
+ setJson(res, 400, { error: "missing_account_id", message: "Current auth.json is missing tokens.account_id." });
623
+ return true;
624
+ }
625
+ setJson(res, 400, { error: "invalid_auth_json", message: "Failed to parse the current auth.json file." });
626
+ }
627
+ return true;
628
+ }
629
+ if (req.method === "POST" && url.pathname === "/codex-api/accounts/switch") {
630
+ try {
631
+ if (appServer.listPendingServerRequests().length > 0) {
632
+ setJson(res, 409, {
633
+ error: "account_switch_blocked",
634
+ message: "Finish pending approval requests before switching accounts."
635
+ });
636
+ return true;
637
+ }
638
+ const rawBody = await new Promise((resolve3, reject) => {
639
+ let body = "";
640
+ req.setEncoding("utf8");
641
+ req.on("data", (chunk) => {
642
+ body += chunk;
643
+ });
644
+ req.on("end", () => resolve3(body));
645
+ req.on("error", reject);
646
+ });
647
+ const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
648
+ const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
649
+ if (!accountId) {
650
+ setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
651
+ return true;
652
+ }
653
+ const state = await readStoredAccountsState();
654
+ const target = state.accounts.find((entry) => entry.accountId === accountId) ?? null;
655
+ if (!target) {
656
+ setJson(res, 404, { error: "account_not_found", message: "The requested account was not found." });
657
+ return true;
658
+ }
659
+ const snapshotPath = getSnapshotPath(target.storageId);
660
+ if (!await fileExists(snapshotPath)) {
661
+ setJson(res, 404, { error: "account_not_found", message: "The requested account snapshot is missing." });
662
+ return true;
663
+ }
664
+ let previousRaw = null;
665
+ try {
666
+ previousRaw = await readFile(getActiveAuthPath(), "utf8");
667
+ } catch {
668
+ previousRaw = null;
669
+ }
670
+ const targetRaw = await readFile(snapshotPath, "utf8");
671
+ await writeFile(getActiveAuthPath(), targetRaw, { encoding: "utf8", mode: 384 });
672
+ try {
673
+ appServer.dispose();
674
+ const inspection = await validateSwitchedAccount(appServer);
675
+ const nextEntry = {
676
+ ...target,
677
+ email: inspection.metadata.email ?? target.email,
678
+ planType: inspection.metadata.planType ?? target.planType,
679
+ lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
680
+ quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
681
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
682
+ quotaStatus: "ready",
683
+ quotaError: null,
684
+ unavailableReason: null
685
+ };
686
+ const nextState = withUpsertedAccount({
687
+ activeAccountId: accountId,
688
+ accounts: state.accounts
689
+ }, nextEntry);
690
+ await writeStoredAccountsState({
691
+ activeAccountId: accountId,
692
+ accounts: nextState.accounts
693
+ });
694
+ void scheduleAccountsBackgroundRefresh({
695
+ force: true,
696
+ prioritizeAccountId: accountId,
697
+ accountIds: nextState.accounts.filter((entry) => entry.accountId !== accountId).map((entry) => entry.accountId)
698
+ });
699
+ setJson(res, 200, {
700
+ ok: true,
701
+ data: {
702
+ activeAccountId: accountId,
703
+ account: toPublicAccountEntry(nextEntry, accountId)
704
+ }
705
+ });
706
+ } catch (error) {
707
+ await restoreActiveAuth(previousRaw);
708
+ appServer.dispose();
709
+ await replaceStoredAccount({
710
+ ...target,
711
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
712
+ quotaStatus: "error",
713
+ quotaError: getErrorMessage(error, "Failed to switch account"),
714
+ unavailableReason: detectAccountUnavailableReason(error)
715
+ }, state.activeAccountId);
716
+ setJson(res, 502, {
717
+ error: "account_switch_failed",
718
+ message: getErrorMessage(error, "Failed to switch account")
719
+ });
720
+ }
721
+ } catch (error) {
722
+ setJson(res, 400, {
723
+ error: "invalid_auth_json",
724
+ message: getErrorMessage(error, "Failed to switch account")
725
+ });
726
+ }
727
+ return true;
728
+ }
729
+ if (req.method === "POST" && url.pathname === "/codex-api/accounts/remove") {
730
+ try {
731
+ const rawBody = await new Promise((resolve3, reject) => {
732
+ let body = "";
733
+ req.setEncoding("utf8");
734
+ req.on("data", (chunk) => {
735
+ body += chunk;
736
+ });
737
+ req.on("end", () => resolve3(body));
738
+ req.on("error", reject);
739
+ });
740
+ const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
741
+ const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
742
+ if (!accountId) {
743
+ setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
744
+ return true;
745
+ }
746
+ const state = await readStoredAccountsState();
747
+ const target = state.accounts.find((entry) => entry.accountId === accountId) ?? null;
748
+ if (!target) {
749
+ setJson(res, 404, { error: "account_not_found", message: "The requested account was not found." });
750
+ return true;
751
+ }
752
+ const remainingAccounts = state.accounts.filter((entry) => entry.accountId !== accountId);
753
+ if (state.activeAccountId !== accountId) {
754
+ await removeSnapshot(target.storageId);
755
+ await writeStoredAccountsState({
756
+ activeAccountId: state.activeAccountId,
757
+ accounts: remainingAccounts
758
+ });
759
+ setJson(res, 200, {
760
+ ok: true,
761
+ data: {
762
+ activeAccountId: state.activeAccountId,
763
+ accounts: sortAccounts(remainingAccounts, state.activeAccountId).map((entry) => toPublicAccountEntry(entry, state.activeAccountId))
764
+ }
765
+ });
766
+ return true;
767
+ }
768
+ if (appServer.listPendingServerRequests().length > 0) {
769
+ setJson(res, 409, {
770
+ error: "account_remove_blocked",
771
+ message: "Finish pending approval requests before removing the active account."
772
+ });
773
+ return true;
774
+ }
775
+ let previousRaw = null;
776
+ try {
777
+ previousRaw = await readFile(getActiveAuthPath(), "utf8");
778
+ } catch {
779
+ previousRaw = null;
780
+ }
781
+ const replacement = await pickReplacementActiveAccount(remainingAccounts);
782
+ if (!replacement) {
783
+ await restoreActiveAuth(null);
784
+ appServer.dispose();
785
+ await removeSnapshot(target.storageId);
786
+ await writeStoredAccountsState({
787
+ activeAccountId: null,
788
+ accounts: remainingAccounts
789
+ });
790
+ void scheduleAccountsBackgroundRefresh({
791
+ force: true,
792
+ accountIds: remainingAccounts.map((entry) => entry.accountId)
793
+ });
794
+ setJson(res, 200, {
795
+ ok: true,
796
+ data: {
797
+ activeAccountId: null,
798
+ accounts: sortAccounts(remainingAccounts, null).map((entry) => toPublicAccountEntry(entry, null))
799
+ }
800
+ });
801
+ return true;
802
+ }
803
+ const replacementSnapshotPath = getSnapshotPath(replacement.storageId);
804
+ if (!await fileExists(replacementSnapshotPath)) {
805
+ setJson(res, 404, {
806
+ error: "account_not_found",
807
+ message: "The replacement account snapshot is missing."
808
+ });
809
+ return true;
810
+ }
811
+ const replacementRaw = await readFile(replacementSnapshotPath, "utf8");
812
+ await writeFile(getActiveAuthPath(), replacementRaw, { encoding: "utf8", mode: 384 });
813
+ try {
814
+ appServer.dispose();
815
+ const inspection = await validateSwitchedAccount(appServer);
816
+ const activatedReplacement = {
817
+ ...replacement,
818
+ email: inspection.metadata.email ?? replacement.email,
819
+ planType: inspection.metadata.planType ?? replacement.planType,
820
+ lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
821
+ quotaSnapshot: inspection.quotaSnapshot ?? replacement.quotaSnapshot,
822
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
823
+ quotaStatus: "ready",
824
+ quotaError: null,
825
+ unavailableReason: null
826
+ };
827
+ const nextAccounts = remainingAccounts.map((entry) => entry.accountId === activatedReplacement.accountId ? activatedReplacement : entry);
828
+ await removeSnapshot(target.storageId);
829
+ await writeStoredAccountsState({
830
+ activeAccountId: activatedReplacement.accountId,
831
+ accounts: nextAccounts
832
+ });
833
+ void scheduleAccountsBackgroundRefresh({
834
+ force: true,
835
+ prioritizeAccountId: activatedReplacement.accountId,
836
+ accountIds: nextAccounts.filter((entry) => entry.accountId !== activatedReplacement.accountId).map((entry) => entry.accountId)
837
+ });
838
+ setJson(res, 200, {
839
+ ok: true,
840
+ data: {
841
+ activeAccountId: activatedReplacement.accountId,
842
+ accounts: sortAccounts(nextAccounts, activatedReplacement.accountId).map((entry) => toPublicAccountEntry(entry, activatedReplacement.accountId))
843
+ }
844
+ });
845
+ } catch (error) {
846
+ await restoreActiveAuth(previousRaw);
847
+ appServer.dispose();
848
+ await replaceStoredAccount({
849
+ ...replacement,
850
+ quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
851
+ quotaStatus: "error",
852
+ quotaError: getErrorMessage(error, "Failed to switch account"),
853
+ unavailableReason: detectAccountUnavailableReason(error)
854
+ }, state.activeAccountId);
855
+ setJson(res, 502, {
856
+ error: "account_remove_failed",
857
+ message: getErrorMessage(error, "Failed to remove account")
858
+ });
859
+ }
860
+ } catch (error) {
861
+ setJson(res, 400, {
862
+ error: "invalid_auth_json",
863
+ message: getErrorMessage(error, "Failed to remove account")
864
+ });
865
+ }
866
+ return true;
867
+ }
868
+ return false;
869
+ }
870
+
871
+ // src/server/skillsRoutes.ts
872
+ import { spawn as spawn2 } from "child_process";
873
+ import { mkdtemp as mkdtemp2, readFile as readFile2, readdir, rm as rm2, mkdir as mkdir2, stat as stat2, lstat, readlink, symlink } from "fs/promises";
874
+ import { existsSync } from "fs";
875
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
876
+ import { join as join2 } from "path";
877
+ import { writeFile as writeFile2 } from "fs/promises";
878
+ function asRecord2(value) {
879
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
880
+ }
881
+ function getErrorMessage2(payload, fallback) {
882
+ if (payload instanceof Error && payload.message.trim().length > 0) {
883
+ return payload.message;
884
+ }
885
+ const record = asRecord2(payload);
49
886
  if (!record) return fallback;
50
887
  const error = record.error;
51
888
  if (typeof error === "string" && error.length > 0) return error;
52
- const nestedError = asRecord(error);
889
+ const nestedError = asRecord2(error);
53
890
  if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
54
891
  return nestedError.message;
55
892
  }
56
893
  return fallback;
57
894
  }
58
- function setJson(res, statusCode, payload) {
895
+ function setJson2(res, statusCode, payload) {
59
896
  res.statusCode = statusCode;
60
897
  res.setHeader("Content-Type", "application/json; charset=utf-8");
61
898
  res.end(JSON.stringify(payload));
62
899
  }
63
- function getCodexHomeDir() {
900
+ function getCodexHomeDir2() {
64
901
  const codexHome = process.env.CODEX_HOME?.trim();
65
- return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
902
+ return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
66
903
  }
67
904
  function getSkillsInstallDir() {
68
- return join(getCodexHomeDir(), "skills");
905
+ return join2(getCodexHomeDir2(), "skills");
69
906
  }
70
907
  async function runCommand(command, args, options = {}) {
71
- await new Promise((resolve2, reject) => {
72
- const proc = spawn(command, args, {
908
+ await new Promise((resolve3, reject) => {
909
+ const proc = spawn2(command, args, {
73
910
  cwd: options.cwd,
74
911
  env: process.env,
75
912
  stdio: ["ignore", "pipe", "pipe"]
@@ -85,7 +922,7 @@ async function runCommand(command, args, options = {}) {
85
922
  proc.on("error", reject);
86
923
  proc.on("close", (code) => {
87
924
  if (code === 0) {
88
- resolve2();
925
+ resolve3();
89
926
  return;
90
927
  }
91
928
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -95,8 +932,8 @@ async function runCommand(command, args, options = {}) {
95
932
  });
96
933
  }
97
934
  async function runCommandWithOutput(command, args, options = {}) {
98
- return await new Promise((resolve2, reject) => {
99
- const proc = spawn(command, args, {
935
+ return await new Promise((resolve3, reject) => {
936
+ const proc = spawn2(command, args, {
100
937
  cwd: options.cwd,
101
938
  env: process.env,
102
939
  stdio: ["ignore", "pipe", "pipe"]
@@ -112,7 +949,7 @@ async function runCommandWithOutput(command, args, options = {}) {
112
949
  proc.on("error", reject);
113
950
  proc.on("close", (code) => {
114
951
  if (code === 0) {
115
- resolve2(stdout.trim());
952
+ resolve3(stdout.trim());
116
953
  return;
117
954
  }
118
955
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -152,14 +989,14 @@ var skillsTreeCache = null;
152
989
  var metaCache = /* @__PURE__ */ new Map();
153
990
  async function getGhToken() {
154
991
  try {
155
- const proc = spawn("gh", ["auth", "token"], { stdio: ["ignore", "pipe", "ignore"] });
992
+ const proc = spawn2("gh", ["auth", "token"], { stdio: ["ignore", "pipe", "ignore"] });
156
993
  let out = "";
157
994
  proc.stdout.on("data", (d) => {
158
995
  out += d.toString();
159
996
  });
160
- return new Promise((resolve2) => {
161
- proc.on("close", (code) => resolve2(code === 0 ? out.trim() : null));
162
- proc.on("error", () => resolve2(null));
997
+ return new Promise((resolve3) => {
998
+ proc.on("close", (code) => resolve3(code === 0 ? out.trim() : null));
999
+ proc.on("error", () => resolve3(null));
163
1000
  });
164
1001
  } catch {
165
1002
  return null;
@@ -255,9 +1092,9 @@ async function scanInstalledSkillsFromDisk() {
255
1092
  const entries = await readdir(skillsDir, { withFileTypes: true });
256
1093
  for (const entry of entries) {
257
1094
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
258
- const skillMd = join(skillsDir, entry.name, "SKILL.md");
1095
+ const skillMd = join2(skillsDir, entry.name, "SKILL.md");
259
1096
  try {
260
- await stat(skillMd);
1097
+ await stat2(skillMd);
261
1098
  map.set(entry.name, { name: entry.name, path: skillMd, enabled: true });
262
1099
  } catch {
263
1100
  }
@@ -267,11 +1104,11 @@ async function scanInstalledSkillsFromDisk() {
267
1104
  return map;
268
1105
  }
269
1106
  function getSkillsSyncStatePath() {
270
- return join(getCodexHomeDir(), "skills-sync.json");
1107
+ return join2(getCodexHomeDir2(), "skills-sync.json");
271
1108
  }
272
1109
  async function readSkillsSyncState() {
273
1110
  try {
274
- const raw = await readFile(getSkillsSyncStatePath(), "utf8");
1111
+ const raw = await readFile2(getSkillsSyncStatePath(), "utf8");
275
1112
  const parsed = JSON.parse(raw);
276
1113
  return parsed && typeof parsed === "object" ? parsed : {};
277
1114
  } catch {
@@ -279,7 +1116,7 @@ async function readSkillsSyncState() {
279
1116
  }
280
1117
  }
281
1118
  async function writeSkillsSyncState(state) {
282
- await writeFile(getSkillsSyncStatePath(), JSON.stringify(state), "utf8");
1119
+ await writeFile2(getSkillsSyncStatePath(), JSON.stringify(state), "utf8");
283
1120
  }
284
1121
  async function getGithubJson(url, token, method = "GET", body) {
285
1122
  const resp = await fetch(url, {
@@ -398,11 +1235,11 @@ async function ensurePrivateForkFromUpstream(token, username, repoName) {
398
1235
  ready = true;
399
1236
  break;
400
1237
  }
401
- await new Promise((resolve2) => setTimeout(resolve2, 1e3));
1238
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
402
1239
  }
403
1240
  if (!ready) throw new Error("Private mirror repo was created but is not available yet");
404
1241
  if (!created) return;
405
- const tmp = await mkdtemp(join(tmpdir(), "codex-skills-seed-"));
1242
+ const tmp = await mkdtemp2(join2(tmpdir2(), "codex-skills-seed-"));
406
1243
  try {
407
1244
  const upstreamUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
408
1245
  const branch = getPreferredSyncBranch();
@@ -419,7 +1256,7 @@ async function ensurePrivateForkFromUpstream(token, username, repoName) {
419
1256
  }
420
1257
  await runCommand("git", ["push", "-u", "origin", `HEAD:${branch}`], { cwd: tmp });
421
1258
  } finally {
422
- await rm(tmp, { recursive: true, force: true });
1259
+ await rm2(tmp, { recursive: true, force: true });
423
1260
  }
424
1261
  }
425
1262
  async function readRemoteSkillsManifest(token, repoOwner, repoName) {
@@ -440,7 +1277,7 @@ async function readRemoteSkillsManifest(token, repoOwner, repoName) {
440
1277
  if (!Array.isArray(parsed)) return [];
441
1278
  const skills = [];
442
1279
  for (const row of parsed) {
443
- const item = asRecord(row);
1280
+ const item = asRecord2(row);
444
1281
  const owner = typeof item?.owner === "string" ? item.owner : "";
445
1282
  const name = typeof item?.name === "string" ? item.name : "";
446
1283
  if (!name) continue;
@@ -475,11 +1312,11 @@ function toGitHubTokenRemote(repoOwner, repoName, token) {
475
1312
  }
476
1313
  async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
477
1314
  const localDir = getSkillsInstallDir();
478
- await mkdir(localDir, { recursive: true });
479
- const gitDir = join(localDir, ".git");
1315
+ await mkdir2(localDir, { recursive: true });
1316
+ const gitDir = join2(localDir, ".git");
480
1317
  let hasGitDir = false;
481
1318
  try {
482
- hasGitDir = (await stat(gitDir)).isDirectory();
1319
+ hasGitDir = (await stat2(gitDir)).isDirectory();
483
1320
  } catch {
484
1321
  hasGitDir = false;
485
1322
  }
@@ -591,7 +1428,7 @@ async function walkFileMtimes(rootDir, currentDir, out) {
591
1428
  for (const entry of entries) {
592
1429
  const entryName = String(entry.name);
593
1430
  if (entryName === ".git") continue;
594
- const absolutePath = join(currentDir, entryName);
1431
+ const absolutePath = join2(currentDir, entryName);
595
1432
  const relativePath = absolutePath.slice(rootDir.length + 1);
596
1433
  if (entry.isDirectory()) {
597
1434
  await walkFileMtimes(rootDir, absolutePath, out);
@@ -599,7 +1436,7 @@ async function walkFileMtimes(rootDir, currentDir, out) {
599
1436
  }
600
1437
  if (!entry.isFile()) continue;
601
1438
  try {
602
- const info = await stat(absolutePath);
1439
+ const info = await stat2(absolutePath);
603
1440
  out.set(relativePath, info.mtimeMs);
604
1441
  } catch {
605
1442
  }
@@ -684,41 +1521,41 @@ async function autoPushSyncedSkills(appServer) {
684
1521
  await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
685
1522
  }
686
1523
  async function ensureCodexAgentsSymlinkToSkillsAgents() {
687
- const codexHomeDir = getCodexHomeDir();
688
- const skillsAgentsPath = join(codexHomeDir, "skills", "AGENTS.md");
689
- const codexAgentsPath = join(codexHomeDir, "AGENTS.md");
690
- await mkdir(join(codexHomeDir, "skills"), { recursive: true });
1524
+ const codexHomeDir = getCodexHomeDir2();
1525
+ const skillsAgentsPath = join2(codexHomeDir, "skills", "AGENTS.md");
1526
+ const codexAgentsPath = join2(codexHomeDir, "AGENTS.md");
1527
+ await mkdir2(join2(codexHomeDir, "skills"), { recursive: true });
691
1528
  let copiedFromCodex = false;
692
1529
  try {
693
1530
  const codexAgentsStat = await lstat(codexAgentsPath);
694
1531
  if (codexAgentsStat.isFile() || codexAgentsStat.isSymbolicLink()) {
695
- const content = await readFile(codexAgentsPath, "utf8");
696
- await writeFile(skillsAgentsPath, content, "utf8");
1532
+ const content = await readFile2(codexAgentsPath, "utf8");
1533
+ await writeFile2(skillsAgentsPath, content, "utf8");
697
1534
  copiedFromCodex = true;
698
1535
  } else {
699
- await rm(codexAgentsPath, { force: true, recursive: true });
1536
+ await rm2(codexAgentsPath, { force: true, recursive: true });
700
1537
  }
701
1538
  } catch {
702
1539
  }
703
1540
  if (!copiedFromCodex) {
704
1541
  try {
705
- const skillsAgentsStat = await stat(skillsAgentsPath);
1542
+ const skillsAgentsStat = await stat2(skillsAgentsPath);
706
1543
  if (!skillsAgentsStat.isFile()) {
707
- await rm(skillsAgentsPath, { force: true, recursive: true });
708
- await writeFile(skillsAgentsPath, "", "utf8");
1544
+ await rm2(skillsAgentsPath, { force: true, recursive: true });
1545
+ await writeFile2(skillsAgentsPath, "", "utf8");
709
1546
  }
710
1547
  } catch {
711
- await writeFile(skillsAgentsPath, "", "utf8");
1548
+ await writeFile2(skillsAgentsPath, "", "utf8");
712
1549
  }
713
1550
  }
714
- const relativeTarget = join("skills", "AGENTS.md");
1551
+ const relativeTarget = join2("skills", "AGENTS.md");
715
1552
  try {
716
1553
  const current = await lstat(codexAgentsPath);
717
1554
  if (current.isSymbolicLink()) {
718
1555
  const existingTarget = await readlink(codexAgentsPath);
719
1556
  if (existingTarget === relativeTarget) return;
720
1557
  }
721
- await rm(codexAgentsPath, { force: true, recursive: true });
1558
+ await rm2(codexAgentsPath, { force: true, recursive: true });
722
1559
  } catch {
723
1560
  }
724
1561
  await symlink(relativeTarget, codexAgentsPath);
@@ -768,7 +1605,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
768
1605
  startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
769
1606
  startupSyncStatus.lastAction = "startup-sync-complete";
770
1607
  } catch (error) {
771
- startupSyncStatus.lastError = getErrorMessage(error, "startup-sync-failed");
1608
+ startupSyncStatus.lastError = getErrorMessage2(error, "startup-sync-failed");
772
1609
  startupSyncStatus.lastAction = "startup-sync-failed";
773
1610
  } finally {
774
1611
  startupSyncStatus.inProgress = false;
@@ -849,15 +1686,15 @@ async function handleSkillsRoutes(req, res, url, context) {
849
1686
  installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
850
1687
  }
851
1688
  const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
852
- setJson(res, 200, { data: results, installed, total: allEntries.length });
1689
+ setJson2(res, 200, { data: results, installed, total: allEntries.length });
853
1690
  } catch (error) {
854
- setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch skills hub") });
1691
+ setJson2(res, 502, { error: getErrorMessage2(error, "Failed to fetch skills hub") });
855
1692
  }
856
1693
  return true;
857
1694
  }
858
1695
  if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
859
1696
  const state = await readSkillsSyncState();
860
- setJson(res, 200, {
1697
+ setJson2(res, 200, {
861
1698
  data: {
862
1699
  loggedIn: Boolean(state.githubToken),
863
1700
  githubUsername: state.githubUsername ?? "",
@@ -880,25 +1717,25 @@ async function handleSkillsRoutes(req, res, url, context) {
880
1717
  if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
881
1718
  try {
882
1719
  const started = await startGithubDeviceLogin();
883
- setJson(res, 200, { data: started });
1720
+ setJson2(res, 200, { data: started });
884
1721
  } catch (error) {
885
- setJson(res, 502, { error: getErrorMessage(error, "Failed to start GitHub login") });
1722
+ setJson2(res, 502, { error: getErrorMessage2(error, "Failed to start GitHub login") });
886
1723
  }
887
1724
  return true;
888
1725
  }
889
1726
  if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
890
1727
  try {
891
- const payload = asRecord(await readJsonBody2(req));
1728
+ const payload = asRecord2(await readJsonBody2(req));
892
1729
  const token = typeof payload?.token === "string" ? payload.token.trim() : "";
893
1730
  if (!token) {
894
- setJson(res, 400, { error: "Missing GitHub token" });
1731
+ setJson2(res, 400, { error: "Missing GitHub token" });
895
1732
  return true;
896
1733
  }
897
1734
  const username = await resolveGithubUsername(token);
898
1735
  await finalizeGithubLoginAndSync(token, username, appServer);
899
- setJson(res, 200, { ok: true, data: { githubUsername: username } });
1736
+ setJson2(res, 200, { ok: true, data: { githubUsername: username } });
900
1737
  } catch (error) {
901
- setJson(res, 502, { error: getErrorMessage(error, "Failed to login with GitHub token") });
1738
+ setJson2(res, 502, { error: getErrorMessage2(error, "Failed to login with GitHub token") });
902
1739
  }
903
1740
  return true;
904
1741
  }
@@ -912,31 +1749,31 @@ async function handleSkillsRoutes(req, res, url, context) {
912
1749
  repoOwner: void 0,
913
1750
  repoName: void 0
914
1751
  });
915
- setJson(res, 200, { ok: true });
1752
+ setJson2(res, 200, { ok: true });
916
1753
  } catch (error) {
917
- setJson(res, 500, { error: getErrorMessage(error, "Failed to logout GitHub") });
1754
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to logout GitHub") });
918
1755
  }
919
1756
  return true;
920
1757
  }
921
1758
  if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
922
1759
  try {
923
- const payload = asRecord(await readJsonBody2(req));
1760
+ const payload = asRecord2(await readJsonBody2(req));
924
1761
  const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
925
1762
  if (!deviceCode) {
926
- setJson(res, 400, { error: "Missing deviceCode" });
1763
+ setJson2(res, 400, { error: "Missing deviceCode" });
927
1764
  return true;
928
1765
  }
929
1766
  const result = await completeGithubDeviceLogin(deviceCode);
930
1767
  if (!result.token) {
931
- setJson(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
1768
+ setJson2(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
932
1769
  return true;
933
1770
  }
934
1771
  const token = result.token;
935
1772
  const username = await resolveGithubUsername(token);
936
1773
  await finalizeGithubLoginAndSync(token, username, appServer);
937
- setJson(res, 200, { ok: true, data: { githubUsername: username } });
1774
+ setJson2(res, 200, { ok: true, data: { githubUsername: username } });
938
1775
  } catch (error) {
939
- setJson(res, 502, { error: getErrorMessage(error, "Failed to complete GitHub login") });
1776
+ setJson2(res, 502, { error: getErrorMessage2(error, "Failed to complete GitHub login") });
940
1777
  }
941
1778
  return true;
942
1779
  }
@@ -944,20 +1781,20 @@ async function handleSkillsRoutes(req, res, url, context) {
944
1781
  try {
945
1782
  const state = await readSkillsSyncState();
946
1783
  if (!state.githubToken || !state.repoOwner || !state.repoName) {
947
- setJson(res, 400, { error: "Skills sync is not configured yet" });
1784
+ setJson2(res, 400, { error: "Skills sync is not configured yet" });
948
1785
  return true;
949
1786
  }
950
1787
  if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
951
- setJson(res, 400, { error: "Refusing to push to upstream repository" });
1788
+ setJson2(res, 400, { error: "Refusing to push to upstream repository" });
952
1789
  return true;
953
1790
  }
954
1791
  const local = await collectLocalSyncedSkills(appServer);
955
1792
  const installedMap = await scanInstalledSkillsFromDisk();
956
1793
  await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
957
1794
  await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
958
- setJson(res, 200, { ok: true, data: { synced: local.length } });
1795
+ setJson2(res, 200, { ok: true, data: { synced: local.length } });
959
1796
  } catch (error) {
960
- setJson(res, 502, { error: getErrorMessage(error, "Failed to push synced skills") });
1797
+ setJson2(res, 502, { error: getErrorMessage2(error, "Failed to push synced skills") });
961
1798
  }
962
1799
  return true;
963
1800
  }
@@ -970,7 +1807,7 @@ async function handleSkillsRoutes(req, res, url, context) {
970
1807
  await appServer.rpc("skills/list", { forceReload: true });
971
1808
  } catch {
972
1809
  }
973
- setJson(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
1810
+ setJson2(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
974
1811
  return true;
975
1812
  }
976
1813
  const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
@@ -1009,13 +1846,13 @@ async function handleSkillsRoutes(req, res, url, context) {
1009
1846
  "git"
1010
1847
  ]);
1011
1848
  }
1012
- const skillPath = join(localDir, skill.name);
1849
+ const skillPath = join2(localDir, skill.name);
1013
1850
  await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
1014
1851
  }
1015
1852
  const remoteNames = new Set(remote.map((row) => row.name));
1016
1853
  for (const [name, localInfo] of localSkills.entries()) {
1017
1854
  if (!remoteNames.has(name)) {
1018
- await rm(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
1855
+ await rm2(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
1019
1856
  }
1020
1857
  }
1021
1858
  const nextOwners = {};
@@ -1028,9 +1865,9 @@ async function handleSkillsRoutes(req, res, url, context) {
1028
1865
  await appServer.rpc("skills/list", { forceReload: true });
1029
1866
  } catch {
1030
1867
  }
1031
- setJson(res, 200, { ok: true, data: { synced: remote.length } });
1868
+ setJson2(res, 200, { ok: true, data: { synced: remote.length } });
1032
1869
  } catch (error) {
1033
- setJson(res, 502, { error: getErrorMessage(error, "Failed to pull synced skills") });
1870
+ setJson2(res, 502, { error: getErrorMessage2(error, "Failed to pull synced skills") });
1034
1871
  }
1035
1872
  return true;
1036
1873
  }
@@ -1039,26 +1876,26 @@ async function handleSkillsRoutes(req, res, url, context) {
1039
1876
  const owner = url.searchParams.get("owner") || "";
1040
1877
  const name = url.searchParams.get("name") || "";
1041
1878
  if (!owner || !name) {
1042
- setJson(res, 400, { error: "Missing owner or name" });
1879
+ setJson2(res, 400, { error: "Missing owner or name" });
1043
1880
  return true;
1044
1881
  }
1045
1882
  const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
1046
1883
  const resp = await fetch(rawUrl);
1047
1884
  if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
1048
1885
  const content = await resp.text();
1049
- setJson(res, 200, { content });
1886
+ setJson2(res, 200, { content });
1050
1887
  } catch (error) {
1051
- setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
1888
+ setJson2(res, 502, { error: getErrorMessage2(error, "Failed to fetch SKILL.md") });
1052
1889
  }
1053
1890
  return true;
1054
1891
  }
1055
1892
  if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
1056
1893
  try {
1057
- const payload = asRecord(await readJsonBody2(req));
1894
+ const payload = asRecord2(await readJsonBody2(req));
1058
1895
  const owner = typeof payload?.owner === "string" ? payload.owner : "";
1059
1896
  const name = typeof payload?.name === "string" ? payload.name : "";
1060
1897
  if (!owner || !name) {
1061
- setJson(res, 400, { error: "Missing owner or name" });
1898
+ setJson2(res, 400, { error: "Missing owner or name" });
1062
1899
  return true;
1063
1900
  }
1064
1901
  const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
@@ -1074,29 +1911,29 @@ async function handleSkillsRoutes(req, res, url, context) {
1074
1911
  "--method",
1075
1912
  "git"
1076
1913
  ]);
1077
- const skillDir = join(installDest, name);
1914
+ const skillDir = join2(installDest, name);
1078
1915
  await ensureInstalledSkillIsValid(appServer, skillDir);
1079
1916
  const syncState = await readSkillsSyncState();
1080
1917
  const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
1081
1918
  await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1082
1919
  await autoPushSyncedSkills(appServer);
1083
- setJson(res, 200, { ok: true, path: skillDir });
1920
+ setJson2(res, 200, { ok: true, path: skillDir });
1084
1921
  } catch (error) {
1085
- setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
1922
+ setJson2(res, 502, { error: getErrorMessage2(error, "Failed to install skill") });
1086
1923
  }
1087
1924
  return true;
1088
1925
  }
1089
1926
  if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
1090
1927
  try {
1091
- const payload = asRecord(await readJsonBody2(req));
1928
+ const payload = asRecord2(await readJsonBody2(req));
1092
1929
  const name = typeof payload?.name === "string" ? payload.name : "";
1093
1930
  const path = typeof payload?.path === "string" ? payload.path : "";
1094
- const target = path || (name ? join(getSkillsInstallDir(), name) : "");
1931
+ const target = path || (name ? join2(getSkillsInstallDir(), name) : "");
1095
1932
  if (!target) {
1096
- setJson(res, 400, { error: "Missing name or path" });
1933
+ setJson2(res, 400, { error: "Missing name or path" });
1097
1934
  return true;
1098
1935
  }
1099
- await rm(target, { recursive: true, force: true });
1936
+ await rm2(target, { recursive: true, force: true });
1100
1937
  if (name) {
1101
1938
  const syncState = await readSkillsSyncState();
1102
1939
  const nextOwners = { ...syncState.installedOwners ?? {} };
@@ -1108,9 +1945,9 @@ async function handleSkillsRoutes(req, res, url, context) {
1108
1945
  await appServer.rpc("skills/list", { forceReload: true });
1109
1946
  } catch {
1110
1947
  }
1111
- setJson(res, 200, { ok: true, deletedPath: target });
1948
+ setJson2(res, 200, { ok: true, deletedPath: target });
1112
1949
  } catch (error) {
1113
- setJson(res, 502, { error: getErrorMessage(error, "Failed to uninstall skill") });
1950
+ setJson2(res, 502, { error: getErrorMessage2(error, "Failed to uninstall skill") });
1114
1951
  }
1115
1952
  return true;
1116
1953
  }
@@ -1118,38 +1955,38 @@ async function handleSkillsRoutes(req, res, url, context) {
1118
1955
  }
1119
1956
 
1120
1957
  // src/server/codexAppServerBridge.ts
1121
- function asRecord2(value) {
1958
+ function asRecord3(value) {
1122
1959
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
1123
1960
  }
1124
- function getErrorMessage2(payload, fallback) {
1961
+ function getErrorMessage3(payload, fallback) {
1125
1962
  if (payload instanceof Error && payload.message.trim().length > 0) {
1126
1963
  return payload.message;
1127
1964
  }
1128
- const record = asRecord2(payload);
1965
+ const record = asRecord3(payload);
1129
1966
  if (!record) return fallback;
1130
1967
  const error = record.error;
1131
1968
  if (typeof error === "string" && error.length > 0) return error;
1132
- const nestedError = asRecord2(error);
1969
+ const nestedError = asRecord3(error);
1133
1970
  if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
1134
1971
  return nestedError.message;
1135
1972
  }
1136
1973
  return fallback;
1137
1974
  }
1138
- function setJson2(res, statusCode, payload) {
1975
+ function setJson3(res, statusCode, payload) {
1139
1976
  res.statusCode = statusCode;
1140
1977
  res.setHeader("Content-Type", "application/json; charset=utf-8");
1141
1978
  res.end(JSON.stringify(payload));
1142
1979
  }
1143
1980
  function extractThreadMessageText(threadReadPayload) {
1144
- const payload = asRecord2(threadReadPayload);
1145
- const thread = asRecord2(payload?.thread);
1981
+ const payload = asRecord3(threadReadPayload);
1982
+ const thread = asRecord3(payload?.thread);
1146
1983
  const turns = Array.isArray(thread?.turns) ? thread.turns : [];
1147
1984
  const parts = [];
1148
1985
  for (const turn of turns) {
1149
- const turnRecord = asRecord2(turn);
1986
+ const turnRecord = asRecord3(turn);
1150
1987
  const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
1151
1988
  for (const item of items) {
1152
- const itemRecord = asRecord2(item);
1989
+ const itemRecord = asRecord3(item);
1153
1990
  const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
1154
1991
  if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
1155
1992
  parts.push(itemRecord.text.trim());
@@ -1158,7 +1995,7 @@ function extractThreadMessageText(threadReadPayload) {
1158
1995
  if (type === "userMessage") {
1159
1996
  const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
1160
1997
  for (const block of content) {
1161
- const blockRecord = asRecord2(block);
1998
+ const blockRecord = asRecord3(block);
1162
1999
  if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
1163
2000
  parts.push(blockRecord.text.trim());
1164
2001
  }
@@ -1193,8 +2030,8 @@ function scoreFileCandidate(path, query) {
1193
2030
  return 10;
1194
2031
  }
1195
2032
  async function listFilesWithRipgrep(cwd) {
1196
- return await new Promise((resolve2, reject) => {
1197
- const proc = spawn2("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
2033
+ return await new Promise((resolve3, reject) => {
2034
+ const proc = spawn3("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
1198
2035
  cwd,
1199
2036
  env: process.env,
1200
2037
  stdio: ["ignore", "pipe", "pipe"]
@@ -1211,7 +2048,7 @@ async function listFilesWithRipgrep(cwd) {
1211
2048
  proc.on("close", (code) => {
1212
2049
  if (code === 0) {
1213
2050
  const rows = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1214
- resolve2(rows);
2051
+ resolve3(rows);
1215
2052
  return;
1216
2053
  }
1217
2054
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -1219,13 +2056,13 @@ async function listFilesWithRipgrep(cwd) {
1219
2056
  });
1220
2057
  });
1221
2058
  }
1222
- function getCodexHomeDir2() {
2059
+ function getCodexHomeDir3() {
1223
2060
  const codexHome = process.env.CODEX_HOME?.trim();
1224
- return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
2061
+ return codexHome && codexHome.length > 0 ? codexHome : join3(homedir3(), ".codex");
1225
2062
  }
1226
2063
  async function runCommand2(command, args, options = {}) {
1227
- await new Promise((resolve2, reject) => {
1228
- const proc = spawn2(command, args, {
2064
+ await new Promise((resolve3, reject) => {
2065
+ const proc = spawn3(command, args, {
1229
2066
  cwd: options.cwd,
1230
2067
  env: process.env,
1231
2068
  stdio: ["ignore", "pipe", "pipe"]
@@ -1241,7 +2078,7 @@ async function runCommand2(command, args, options = {}) {
1241
2078
  proc.on("error", reject);
1242
2079
  proc.on("close", (code) => {
1243
2080
  if (code === 0) {
1244
- resolve2();
2081
+ resolve3();
1245
2082
  return;
1246
2083
  }
1247
2084
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -1251,19 +2088,19 @@ async function runCommand2(command, args, options = {}) {
1251
2088
  });
1252
2089
  }
1253
2090
  function isMissingHeadError(error) {
1254
- const message = getErrorMessage2(error, "").toLowerCase();
2091
+ const message = getErrorMessage3(error, "").toLowerCase();
1255
2092
  return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
1256
2093
  }
1257
2094
  function isNotGitRepositoryError(error) {
1258
- const message = getErrorMessage2(error, "").toLowerCase();
2095
+ const message = getErrorMessage3(error, "").toLowerCase();
1259
2096
  return message.includes("not a git repository") || message.includes("fatal: not a git repository");
1260
2097
  }
1261
2098
  async function ensureRepoHasInitialCommit(repoRoot) {
1262
- const agentsPath = join2(repoRoot, "AGENTS.md");
2099
+ const agentsPath = join3(repoRoot, "AGENTS.md");
1263
2100
  try {
1264
- await stat2(agentsPath);
2101
+ await stat3(agentsPath);
1265
2102
  } catch {
1266
- await writeFile2(agentsPath, "", "utf8");
2103
+ await writeFile3(agentsPath, "", "utf8");
1267
2104
  }
1268
2105
  await runCommand2("git", ["add", "AGENTS.md"], { cwd: repoRoot });
1269
2106
  await runCommand2(
@@ -1273,8 +2110,8 @@ async function ensureRepoHasInitialCommit(repoRoot) {
1273
2110
  );
1274
2111
  }
1275
2112
  async function runCommandCapture(command, args, options = {}) {
1276
- return await new Promise((resolve2, reject) => {
1277
- const proc = spawn2(command, args, {
2113
+ return await new Promise((resolve3, reject) => {
2114
+ const proc = spawn3(command, args, {
1278
2115
  cwd: options.cwd,
1279
2116
  env: process.env,
1280
2117
  stdio: ["ignore", "pipe", "pipe"]
@@ -1290,7 +2127,7 @@ async function runCommandCapture(command, args, options = {}) {
1290
2127
  proc.on("error", reject);
1291
2128
  proc.on("close", (code) => {
1292
2129
  if (code === 0) {
1293
- resolve2(stdout.trim());
2130
+ resolve3(stdout.trim());
1294
2131
  return;
1295
2132
  }
1296
2133
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -1320,11 +2157,11 @@ function normalizeStringRecord(value) {
1320
2157
  return next;
1321
2158
  }
1322
2159
  function getCodexAuthPath() {
1323
- return join2(getCodexHomeDir2(), "auth.json");
2160
+ return join3(getCodexHomeDir3(), "auth.json");
1324
2161
  }
1325
2162
  async function readCodexAuth() {
1326
2163
  try {
1327
- const raw = await readFile2(getCodexAuthPath(), "utf8");
2164
+ const raw = await readFile3(getCodexAuthPath(), "utf8");
1328
2165
  const auth = JSON.parse(raw);
1329
2166
  const token = auth.tokens?.access_token;
1330
2167
  if (!token) return null;
@@ -1334,13 +2171,16 @@ async function readCodexAuth() {
1334
2171
  }
1335
2172
  }
1336
2173
  function getCodexGlobalStatePath() {
1337
- return join2(getCodexHomeDir2(), ".codex-global-state.json");
2174
+ return join3(getCodexHomeDir3(), ".codex-global-state.json");
2175
+ }
2176
+ function getCodexSessionIndexPath() {
2177
+ return join3(getCodexHomeDir3(), "session_index.jsonl");
1338
2178
  }
1339
2179
  var MAX_THREAD_TITLES = 500;
1340
2180
  function normalizeThreadTitleCache(value) {
1341
- const record = asRecord2(value);
2181
+ const record = asRecord3(value);
1342
2182
  if (!record) return { titles: {}, order: [] };
1343
- const rawTitles = asRecord2(record.titles);
2183
+ const rawTitles = asRecord3(record.titles);
1344
2184
  const titles = {};
1345
2185
  if (rawTitles) {
1346
2186
  for (const [k, v] of Object.entries(rawTitles)) {
@@ -1363,11 +2203,52 @@ function removeFromThreadTitleCache(cache, id) {
1363
2203
  const { [id]: _, ...titles } = cache.titles;
1364
2204
  return { titles, order: cache.order.filter((o) => o !== id) };
1365
2205
  }
2206
+ function normalizeSessionIndexThreadTitle(value) {
2207
+ const record = asRecord3(value);
2208
+ if (!record) return null;
2209
+ const id = typeof record.id === "string" ? record.id.trim() : "";
2210
+ const title = typeof record.thread_name === "string" ? record.thread_name.trim() : "";
2211
+ const updatedAtIso = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
2212
+ const updatedAtMs = updatedAtIso ? Date.parse(updatedAtIso) : Number.NaN;
2213
+ if (!id || !title) return null;
2214
+ return {
2215
+ id,
2216
+ title,
2217
+ updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0
2218
+ };
2219
+ }
2220
+ function trimThreadTitleCache(cache) {
2221
+ const titles = { ...cache.titles };
2222
+ const order = cache.order.filter((id) => {
2223
+ if (!titles[id]) return false;
2224
+ return true;
2225
+ }).slice(0, MAX_THREAD_TITLES);
2226
+ for (const id of Object.keys(titles)) {
2227
+ if (!order.includes(id)) {
2228
+ delete titles[id];
2229
+ }
2230
+ }
2231
+ return { titles, order };
2232
+ }
2233
+ function mergeThreadTitleCaches(base, overlay) {
2234
+ const titles = { ...base.titles, ...overlay.titles };
2235
+ const order = [];
2236
+ for (const id of [...overlay.order, ...base.order]) {
2237
+ if (!titles[id] || order.includes(id)) continue;
2238
+ order.push(id);
2239
+ }
2240
+ for (const id of Object.keys(titles)) {
2241
+ if (!order.includes(id)) {
2242
+ order.push(id);
2243
+ }
2244
+ }
2245
+ return trimThreadTitleCache({ titles, order });
2246
+ }
1366
2247
  async function readThreadTitleCache() {
1367
2248
  const statePath = getCodexGlobalStatePath();
1368
2249
  try {
1369
- const raw = await readFile2(statePath, "utf8");
1370
- const payload = asRecord2(JSON.parse(raw)) ?? {};
2250
+ const raw = await readFile3(statePath, "utf8");
2251
+ const payload = asRecord3(JSON.parse(raw)) ?? {};
1371
2252
  return normalizeThreadTitleCache(payload["thread-titles"]);
1372
2253
  } catch {
1373
2254
  return { titles: {}, order: [] };
@@ -1377,21 +2258,57 @@ async function writeThreadTitleCache(cache) {
1377
2258
  const statePath = getCodexGlobalStatePath();
1378
2259
  let payload = {};
1379
2260
  try {
1380
- const raw = await readFile2(statePath, "utf8");
1381
- payload = asRecord2(JSON.parse(raw)) ?? {};
2261
+ const raw = await readFile3(statePath, "utf8");
2262
+ payload = asRecord3(JSON.parse(raw)) ?? {};
1382
2263
  } catch {
1383
2264
  payload = {};
1384
2265
  }
1385
2266
  payload["thread-titles"] = cache;
1386
- await writeFile2(statePath, JSON.stringify(payload), "utf8");
2267
+ await writeFile3(statePath, JSON.stringify(payload), "utf8");
2268
+ }
2269
+ async function readThreadTitlesFromSessionIndex() {
2270
+ try {
2271
+ const raw = await readFile3(getCodexSessionIndexPath(), "utf8");
2272
+ const latestById = /* @__PURE__ */ new Map();
2273
+ for (const line of raw.split(/\r?\n/u)) {
2274
+ const trimmed = line.trim();
2275
+ if (!trimmed) continue;
2276
+ try {
2277
+ const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed));
2278
+ if (!entry) continue;
2279
+ const previous = latestById.get(entry.id);
2280
+ if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
2281
+ latestById.set(entry.id, entry);
2282
+ }
2283
+ } catch {
2284
+ }
2285
+ }
2286
+ const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs);
2287
+ const titles = {};
2288
+ const order = [];
2289
+ for (const entry of entries) {
2290
+ titles[entry.id] = entry.title;
2291
+ order.push(entry.id);
2292
+ }
2293
+ return trimThreadTitleCache({ titles, order });
2294
+ } catch {
2295
+ return { titles: {}, order: [] };
2296
+ }
2297
+ }
2298
+ async function readMergedThreadTitleCache() {
2299
+ const [sessionIndexCache, persistedCache] = await Promise.all([
2300
+ readThreadTitlesFromSessionIndex(),
2301
+ readThreadTitleCache()
2302
+ ]);
2303
+ return mergeThreadTitleCaches(sessionIndexCache, persistedCache);
1387
2304
  }
1388
2305
  async function readWorkspaceRootsState() {
1389
2306
  const statePath = getCodexGlobalStatePath();
1390
2307
  let payload = {};
1391
2308
  try {
1392
- const raw = await readFile2(statePath, "utf8");
2309
+ const raw = await readFile3(statePath, "utf8");
1393
2310
  const parsed = JSON.parse(raw);
1394
- payload = asRecord2(parsed) ?? {};
2311
+ payload = asRecord3(parsed) ?? {};
1395
2312
  } catch {
1396
2313
  payload = {};
1397
2314
  }
@@ -1405,15 +2322,15 @@ async function writeWorkspaceRootsState(nextState) {
1405
2322
  const statePath = getCodexGlobalStatePath();
1406
2323
  let payload = {};
1407
2324
  try {
1408
- const raw = await readFile2(statePath, "utf8");
1409
- payload = asRecord2(JSON.parse(raw)) ?? {};
2325
+ const raw = await readFile3(statePath, "utf8");
2326
+ payload = asRecord3(JSON.parse(raw)) ?? {};
1410
2327
  } catch {
1411
2328
  payload = {};
1412
2329
  }
1413
2330
  payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
1414
2331
  payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
1415
2332
  payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
1416
- await writeFile2(statePath, JSON.stringify(payload), "utf8");
2333
+ await writeFile3(statePath, JSON.stringify(payload), "utf8");
1417
2334
  }
1418
2335
  async function readJsonBody(req) {
1419
2336
  const raw = await readRawBody(req);
@@ -1451,7 +2368,7 @@ function handleFileUpload(req, res) {
1451
2368
  const contentType = req.headers["content-type"] ?? "";
1452
2369
  const boundaryMatch = contentType.match(/boundary=(.+)/i);
1453
2370
  if (!boundaryMatch) {
1454
- setJson2(res, 400, { error: "Missing multipart boundary" });
2371
+ setJson3(res, 400, { error: "Missing multipart boundary" });
1455
2372
  return;
1456
2373
  }
1457
2374
  const boundary = boundaryMatch[1];
@@ -1481,21 +2398,21 @@ function handleFileUpload(req, res) {
1481
2398
  break;
1482
2399
  }
1483
2400
  if (!fileData) {
1484
- setJson2(res, 400, { error: "No file in request" });
2401
+ setJson3(res, 400, { error: "No file in request" });
1485
2402
  return;
1486
2403
  }
1487
- const uploadDir = join2(tmpdir2(), "codex-web-uploads");
1488
- await mkdir2(uploadDir, { recursive: true });
1489
- const destDir = await mkdtemp2(join2(uploadDir, "f-"));
1490
- const destPath = join2(destDir, fileName);
1491
- await writeFile2(destPath, fileData);
1492
- setJson2(res, 200, { path: destPath });
2404
+ const uploadDir = join3(tmpdir3(), "codex-web-uploads");
2405
+ await mkdir3(uploadDir, { recursive: true });
2406
+ const destDir = await mkdtemp3(join3(uploadDir, "f-"));
2407
+ const destPath = join3(destDir, fileName);
2408
+ await writeFile3(destPath, fileData);
2409
+ setJson3(res, 200, { path: destPath });
1493
2410
  } catch (err) {
1494
- setJson2(res, 500, { error: getErrorMessage2(err, "Upload failed") });
2411
+ setJson3(res, 500, { error: getErrorMessage3(err, "Upload failed") });
1495
2412
  }
1496
2413
  });
1497
2414
  req.on("error", (err) => {
1498
- setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
2415
+ setJson3(res, 500, { error: getErrorMessage3(err, "Upload stream error") });
1499
2416
  });
1500
2417
  }
1501
2418
  async function proxyTranscribe(body, contentType, authToken, accountId) {
@@ -1509,14 +2426,14 @@ async function proxyTranscribe(body, contentType, authToken, accountId) {
1509
2426
  if (accountId) {
1510
2427
  headers["ChatGPT-Account-Id"] = accountId;
1511
2428
  }
1512
- return new Promise((resolve2, reject) => {
2429
+ return new Promise((resolve3, reject) => {
1513
2430
  const req = httpsRequest(
1514
2431
  "https://chatgpt.com/backend-api/transcribe",
1515
2432
  { method: "POST", headers },
1516
2433
  (res) => {
1517
2434
  const chunks = [];
1518
2435
  res.on("data", (c) => chunks.push(c));
1519
- res.on("end", () => resolve2({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
2436
+ res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
1520
2437
  res.on("error", reject);
1521
2438
  }
1522
2439
  );
@@ -1547,7 +2464,7 @@ var AppServerProcess = class {
1547
2464
  start() {
1548
2465
  if (this.process) return;
1549
2466
  this.stopping = false;
1550
- const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
2467
+ const proc = spawn3("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
1551
2468
  this.process = proc;
1552
2469
  proc.stdout.setEncoding("utf8");
1553
2470
  proc.stdout.on("data", (chunk) => {
@@ -1566,6 +2483,9 @@ var AppServerProcess = class {
1566
2483
  proc.stderr.on("data", () => {
1567
2484
  });
1568
2485
  proc.on("exit", () => {
2486
+ if (this.process !== proc) {
2487
+ return;
2488
+ }
1569
2489
  const failure = new Error(this.stopping ? "codex app-server stopped" : "codex app-server exited unexpectedly");
1570
2490
  for (const request of this.pending.values()) {
1571
2491
  request.reject(failure);
@@ -1641,7 +2561,7 @@ var AppServerProcess = class {
1641
2561
  }
1642
2562
  this.pendingServerRequests.delete(requestId);
1643
2563
  this.sendServerRequestReply(requestId, reply);
1644
- const requestParams = asRecord2(pendingRequest.params);
2564
+ const requestParams = asRecord3(pendingRequest.params);
1645
2565
  const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
1646
2566
  this.emitNotification({
1647
2567
  method: "server/request/resolved",
@@ -1670,8 +2590,8 @@ var AppServerProcess = class {
1670
2590
  async call(method, params) {
1671
2591
  this.start();
1672
2592
  const id = this.nextId++;
1673
- return new Promise((resolve2, reject) => {
1674
- this.pending.set(id, { resolve: resolve2, reject });
2593
+ return new Promise((resolve3, reject) => {
2594
+ this.pending.set(id, { resolve: resolve3, reject });
1675
2595
  this.sendLine({
1676
2596
  jsonrpc: "2.0",
1677
2597
  id,
@@ -1717,7 +2637,7 @@ var AppServerProcess = class {
1717
2637
  }
1718
2638
  async respondToServerRequest(payload) {
1719
2639
  await this.ensureInitialized();
1720
- const body = asRecord2(payload);
2640
+ const body = asRecord3(payload);
1721
2641
  if (!body) {
1722
2642
  throw new Error("Invalid response payload: expected object");
1723
2643
  }
@@ -1725,7 +2645,7 @@ var AppServerProcess = class {
1725
2645
  if (typeof id !== "number" || !Number.isInteger(id)) {
1726
2646
  throw new Error('Invalid response payload: "id" must be an integer');
1727
2647
  }
1728
- const rawError = asRecord2(body.error);
2648
+ const rawError = asRecord3(body.error);
1729
2649
  if (rawError) {
1730
2650
  const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
1731
2651
  const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
@@ -1779,8 +2699,8 @@ var MethodCatalog = class {
1779
2699
  this.notificationCache = null;
1780
2700
  }
1781
2701
  async runGenerateSchemaCommand(outDir) {
1782
- await new Promise((resolve2, reject) => {
1783
- const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
2702
+ await new Promise((resolve3, reject) => {
2703
+ const process2 = spawn3("codex", ["app-server", "generate-json-schema", "--out", outDir], {
1784
2704
  stdio: ["ignore", "ignore", "pipe"]
1785
2705
  });
1786
2706
  let stderr = "";
@@ -1791,7 +2711,7 @@ var MethodCatalog = class {
1791
2711
  process2.on("error", reject);
1792
2712
  process2.on("exit", (code) => {
1793
2713
  if (code === 0) {
1794
- resolve2();
2714
+ resolve3();
1795
2715
  return;
1796
2716
  }
1797
2717
  reject(new Error(stderr.trim() || `generate-json-schema exited with code ${String(code)}`));
@@ -1799,13 +2719,13 @@ var MethodCatalog = class {
1799
2719
  });
1800
2720
  }
1801
2721
  extractMethodsFromClientRequest(payload) {
1802
- const root = asRecord2(payload);
2722
+ const root = asRecord3(payload);
1803
2723
  const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
1804
2724
  const methods = /* @__PURE__ */ new Set();
1805
2725
  for (const entry of oneOf) {
1806
- const row = asRecord2(entry);
1807
- const properties = asRecord2(row?.properties);
1808
- const methodDef = asRecord2(properties?.method);
2726
+ const row = asRecord3(entry);
2727
+ const properties = asRecord3(row?.properties);
2728
+ const methodDef = asRecord3(properties?.method);
1809
2729
  const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
1810
2730
  for (const item of methodEnum) {
1811
2731
  if (typeof item === "string" && item.length > 0) {
@@ -1816,13 +2736,13 @@ var MethodCatalog = class {
1816
2736
  return Array.from(methods).sort((a, b) => a.localeCompare(b));
1817
2737
  }
1818
2738
  extractMethodsFromServerNotification(payload) {
1819
- const root = asRecord2(payload);
2739
+ const root = asRecord3(payload);
1820
2740
  const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
1821
2741
  const methods = /* @__PURE__ */ new Set();
1822
2742
  for (const entry of oneOf) {
1823
- const row = asRecord2(entry);
1824
- const properties = asRecord2(row?.properties);
1825
- const methodDef = asRecord2(properties?.method);
2743
+ const row = asRecord3(entry);
2744
+ const properties = asRecord3(row?.properties);
2745
+ const methodDef = asRecord3(properties?.method);
1826
2746
  const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
1827
2747
  for (const item of methodEnum) {
1828
2748
  if (typeof item === "string" && item.length > 0) {
@@ -1836,10 +2756,10 @@ var MethodCatalog = class {
1836
2756
  if (this.methodCache) {
1837
2757
  return this.methodCache;
1838
2758
  }
1839
- const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
2759
+ const outDir = await mkdtemp3(join3(tmpdir3(), "codex-web-local-schema-"));
1840
2760
  await this.runGenerateSchemaCommand(outDir);
1841
- const clientRequestPath = join2(outDir, "ClientRequest.json");
1842
- const raw = await readFile2(clientRequestPath, "utf8");
2761
+ const clientRequestPath = join3(outDir, "ClientRequest.json");
2762
+ const raw = await readFile3(clientRequestPath, "utf8");
1843
2763
  const parsed = JSON.parse(raw);
1844
2764
  const methods = this.extractMethodsFromClientRequest(parsed);
1845
2765
  this.methodCache = methods;
@@ -1849,10 +2769,10 @@ var MethodCatalog = class {
1849
2769
  if (this.notificationCache) {
1850
2770
  return this.notificationCache;
1851
2771
  }
1852
- const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
2772
+ const outDir = await mkdtemp3(join3(tmpdir3(), "codex-web-local-schema-"));
1853
2773
  await this.runGenerateSchemaCommand(outDir);
1854
- const serverNotificationPath = join2(outDir, "ServerNotification.json");
1855
- const raw = await readFile2(serverNotificationPath, "utf8");
2774
+ const serverNotificationPath = join3(outDir, "ServerNotification.json");
2775
+ const raw = await readFile3(serverNotificationPath, "utf8");
1856
2776
  const parsed = JSON.parse(raw);
1857
2777
  const methods = this.extractMethodsFromServerNotification(parsed);
1858
2778
  this.notificationCache = methods;
@@ -1882,7 +2802,7 @@ async function loadAllThreadsForSearch(appServer) {
1882
2802
  const threads = [];
1883
2803
  let cursor = null;
1884
2804
  do {
1885
- const response = asRecord2(await appServer.rpc("thread/list", {
2805
+ const response = asRecord3(await appServer.rpc("thread/list", {
1886
2806
  archived: false,
1887
2807
  limit: 100,
1888
2808
  sortKey: "updated_at",
@@ -1890,7 +2810,7 @@ async function loadAllThreadsForSearch(appServer) {
1890
2810
  }));
1891
2811
  const data = Array.isArray(response?.data) ? response.data : [];
1892
2812
  for (const row of data) {
1893
- const record = asRecord2(row);
2813
+ const record = asRecord3(row);
1894
2814
  const id = typeof record?.id === "string" ? record.id : "";
1895
2815
  if (!id) continue;
1896
2816
  const title = typeof record?.name === "string" && record.name.trim().length > 0 ? record.name.trim() : typeof record?.preview === "string" && record.preview.trim().length > 0 ? record.preview.trim() : "Untitled thread";
@@ -1962,6 +2882,9 @@ function createCodexBridgeMiddleware() {
1962
2882
  return;
1963
2883
  }
1964
2884
  const url = new URL(req.url, "http://localhost");
2885
+ if (await handleAccountRoutes(req, res, url, { appServer })) {
2886
+ return;
2887
+ }
1965
2888
  if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
1966
2889
  return;
1967
2890
  }
@@ -1971,19 +2894,19 @@ function createCodexBridgeMiddleware() {
1971
2894
  }
1972
2895
  if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
1973
2896
  const payload = await readJsonBody(req);
1974
- const body = asRecord2(payload);
2897
+ const body = asRecord3(payload);
1975
2898
  if (!body || typeof body.method !== "string" || body.method.length === 0) {
1976
- setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
2899
+ setJson3(res, 400, { error: "Invalid body: expected { method, params? }" });
1977
2900
  return;
1978
2901
  }
1979
2902
  const result = await appServer.rpc(body.method, body.params ?? null);
1980
- setJson2(res, 200, { result });
2903
+ setJson3(res, 200, { result });
1981
2904
  return;
1982
2905
  }
1983
2906
  if (req.method === "POST" && url.pathname === "/codex-api/transcribe") {
1984
2907
  const auth = await readCodexAuth();
1985
2908
  if (!auth) {
1986
- setJson2(res, 401, { error: "No auth token available for transcription" });
2909
+ setJson3(res, 401, { error: "No auth token available for transcription" });
1987
2910
  return;
1988
2911
  }
1989
2912
  const rawBody = await readRawBody(req);
@@ -1997,48 +2920,48 @@ function createCodexBridgeMiddleware() {
1997
2920
  if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
1998
2921
  const payload = await readJsonBody(req);
1999
2922
  await appServer.respondToServerRequest(payload);
2000
- setJson2(res, 200, { ok: true });
2923
+ setJson3(res, 200, { ok: true });
2001
2924
  return;
2002
2925
  }
2003
2926
  if (req.method === "GET" && url.pathname === "/codex-api/server-requests/pending") {
2004
- setJson2(res, 200, { data: appServer.listPendingServerRequests() });
2927
+ setJson3(res, 200, { data: appServer.listPendingServerRequests() });
2005
2928
  return;
2006
2929
  }
2007
2930
  if (req.method === "GET" && url.pathname === "/codex-api/meta/methods") {
2008
2931
  const methods = await methodCatalog.listMethods();
2009
- setJson2(res, 200, { data: methods });
2932
+ setJson3(res, 200, { data: methods });
2010
2933
  return;
2011
2934
  }
2012
2935
  if (req.method === "GET" && url.pathname === "/codex-api/meta/notifications") {
2013
2936
  const methods = await methodCatalog.listNotificationMethods();
2014
- setJson2(res, 200, { data: methods });
2937
+ setJson3(res, 200, { data: methods });
2015
2938
  return;
2016
2939
  }
2017
2940
  if (req.method === "GET" && url.pathname === "/codex-api/workspace-roots-state") {
2018
2941
  const state = await readWorkspaceRootsState();
2019
- setJson2(res, 200, { data: state });
2942
+ setJson3(res, 200, { data: state });
2020
2943
  return;
2021
2944
  }
2022
2945
  if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
2023
- setJson2(res, 200, { data: { path: homedir2() } });
2946
+ setJson3(res, 200, { data: { path: homedir3() } });
2024
2947
  return;
2025
2948
  }
2026
2949
  if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
2027
- const payload = asRecord2(await readJsonBody(req));
2950
+ const payload = asRecord3(await readJsonBody(req));
2028
2951
  const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
2029
2952
  if (!rawSourceCwd) {
2030
- setJson2(res, 400, { error: "Missing sourceCwd" });
2953
+ setJson3(res, 400, { error: "Missing sourceCwd" });
2031
2954
  return;
2032
2955
  }
2033
2956
  const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
2034
2957
  try {
2035
- const sourceInfo = await stat2(sourceCwd);
2958
+ const sourceInfo = await stat3(sourceCwd);
2036
2959
  if (!sourceInfo.isDirectory()) {
2037
- setJson2(res, 400, { error: "sourceCwd is not a directory" });
2960
+ setJson3(res, 400, { error: "sourceCwd is not a directory" });
2038
2961
  return;
2039
2962
  }
2040
2963
  } catch {
2041
- setJson2(res, 404, { error: "sourceCwd does not exist" });
2964
+ setJson3(res, 404, { error: "sourceCwd does not exist" });
2042
2965
  return;
2043
2966
  }
2044
2967
  try {
@@ -2051,21 +2974,21 @@ function createCodexBridgeMiddleware() {
2051
2974
  gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
2052
2975
  }
2053
2976
  const repoName = basename(gitRoot) || "repo";
2054
- const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
2055
- await mkdir2(worktreesRoot, { recursive: true });
2977
+ const worktreesRoot = join3(getCodexHomeDir3(), "worktrees");
2978
+ await mkdir3(worktreesRoot, { recursive: true });
2056
2979
  let worktreeId = "";
2057
2980
  let worktreeParent = "";
2058
2981
  let worktreeCwd = "";
2059
2982
  for (let attempt = 0; attempt < 12; attempt += 1) {
2060
2983
  const candidate = randomBytes(2).toString("hex");
2061
- const parent = join2(worktreesRoot, candidate);
2984
+ const parent = join3(worktreesRoot, candidate);
2062
2985
  try {
2063
- await stat2(parent);
2986
+ await stat3(parent);
2064
2987
  continue;
2065
2988
  } catch {
2066
2989
  worktreeId = candidate;
2067
2990
  worktreeParent = parent;
2068
- worktreeCwd = join2(parent, repoName);
2991
+ worktreeCwd = join3(parent, repoName);
2069
2992
  break;
2070
2993
  }
2071
2994
  }
@@ -2073,7 +2996,7 @@ function createCodexBridgeMiddleware() {
2073
2996
  throw new Error("Failed to allocate a unique worktree id");
2074
2997
  }
2075
2998
  const branch = `codex/${worktreeId}`;
2076
- await mkdir2(worktreeParent, { recursive: true });
2999
+ await mkdir3(worktreeParent, { recursive: true });
2077
3000
  try {
2078
3001
  await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
2079
3002
  } catch (error) {
@@ -2081,7 +3004,7 @@ function createCodexBridgeMiddleware() {
2081
3004
  await ensureRepoHasInitialCommit(gitRoot);
2082
3005
  await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
2083
3006
  }
2084
- setJson2(res, 200, {
3007
+ setJson3(res, 200, {
2085
3008
  data: {
2086
3009
  cwd: worktreeCwd,
2087
3010
  branch,
@@ -2089,15 +3012,15 @@ function createCodexBridgeMiddleware() {
2089
3012
  }
2090
3013
  });
2091
3014
  } catch (error) {
2092
- setJson2(res, 500, { error: getErrorMessage2(error, "Failed to create worktree") });
3015
+ setJson3(res, 500, { error: getErrorMessage3(error, "Failed to create worktree") });
2093
3016
  }
2094
3017
  return;
2095
3018
  }
2096
3019
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
2097
3020
  const payload = await readJsonBody(req);
2098
- const record = asRecord2(payload);
3021
+ const record = asRecord3(payload);
2099
3022
  if (!record) {
2100
- setJson2(res, 400, { error: "Invalid body: expected object" });
3023
+ setJson3(res, 400, { error: "Invalid body: expected object" });
2101
3024
  return;
2102
3025
  }
2103
3026
  const nextState = {
@@ -2106,33 +3029,33 @@ function createCodexBridgeMiddleware() {
2106
3029
  active: normalizeStringArray(record.active)
2107
3030
  };
2108
3031
  await writeWorkspaceRootsState(nextState);
2109
- setJson2(res, 200, { ok: true });
3032
+ setJson3(res, 200, { ok: true });
2110
3033
  return;
2111
3034
  }
2112
3035
  if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
2113
- const payload = asRecord2(await readJsonBody(req));
3036
+ const payload = asRecord3(await readJsonBody(req));
2114
3037
  const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
2115
3038
  const createIfMissing = payload?.createIfMissing === true;
2116
3039
  const label = typeof payload?.label === "string" ? payload.label : "";
2117
3040
  if (!rawPath) {
2118
- setJson2(res, 400, { error: "Missing path" });
3041
+ setJson3(res, 400, { error: "Missing path" });
2119
3042
  return;
2120
3043
  }
2121
3044
  const normalizedPath = isAbsolute(rawPath) ? rawPath : resolve(rawPath);
2122
3045
  let pathExists = true;
2123
3046
  try {
2124
- const info = await stat2(normalizedPath);
3047
+ const info = await stat3(normalizedPath);
2125
3048
  if (!info.isDirectory()) {
2126
- setJson2(res, 400, { error: "Path exists but is not a directory" });
3049
+ setJson3(res, 400, { error: "Path exists but is not a directory" });
2127
3050
  return;
2128
3051
  }
2129
3052
  } catch {
2130
3053
  pathExists = false;
2131
3054
  }
2132
3055
  if (!pathExists && createIfMissing) {
2133
- await mkdir2(normalizedPath, { recursive: true });
3056
+ await mkdir3(normalizedPath, { recursive: true });
2134
3057
  } else if (!pathExists) {
2135
- setJson2(res, 404, { error: "Directory does not exist" });
3058
+ setJson3(res, 404, { error: "Directory does not exist" });
2136
3059
  return;
2137
3060
  }
2138
3061
  const existingState = await readWorkspaceRootsState();
@@ -2147,103 +3070,103 @@ function createCodexBridgeMiddleware() {
2147
3070
  labels: nextLabels,
2148
3071
  active: nextActive
2149
3072
  });
2150
- setJson2(res, 200, { data: { path: normalizedPath } });
3073
+ setJson3(res, 200, { data: { path: normalizedPath } });
2151
3074
  return;
2152
3075
  }
2153
3076
  if (req.method === "GET" && url.pathname === "/codex-api/project-root-suggestion") {
2154
3077
  const basePath = url.searchParams.get("basePath")?.trim() ?? "";
2155
3078
  if (!basePath) {
2156
- setJson2(res, 400, { error: "Missing basePath" });
3079
+ setJson3(res, 400, { error: "Missing basePath" });
2157
3080
  return;
2158
3081
  }
2159
3082
  const normalizedBasePath = isAbsolute(basePath) ? basePath : resolve(basePath);
2160
3083
  try {
2161
- const baseInfo = await stat2(normalizedBasePath);
3084
+ const baseInfo = await stat3(normalizedBasePath);
2162
3085
  if (!baseInfo.isDirectory()) {
2163
- setJson2(res, 400, { error: "basePath is not a directory" });
3086
+ setJson3(res, 400, { error: "basePath is not a directory" });
2164
3087
  return;
2165
3088
  }
2166
3089
  } catch {
2167
- setJson2(res, 404, { error: "basePath does not exist" });
3090
+ setJson3(res, 404, { error: "basePath does not exist" });
2168
3091
  return;
2169
3092
  }
2170
3093
  let index = 1;
2171
3094
  while (index < 1e5) {
2172
3095
  const candidateName = `New Project (${String(index)})`;
2173
- const candidatePath = join2(normalizedBasePath, candidateName);
3096
+ const candidatePath = join3(normalizedBasePath, candidateName);
2174
3097
  try {
2175
- await stat2(candidatePath);
3098
+ await stat3(candidatePath);
2176
3099
  index += 1;
2177
3100
  continue;
2178
3101
  } catch {
2179
- setJson2(res, 200, { data: { name: candidateName, path: candidatePath } });
3102
+ setJson3(res, 200, { data: { name: candidateName, path: candidatePath } });
2180
3103
  return;
2181
3104
  }
2182
3105
  }
2183
- setJson2(res, 500, { error: "Failed to compute project name suggestion" });
3106
+ setJson3(res, 500, { error: "Failed to compute project name suggestion" });
2184
3107
  return;
2185
3108
  }
2186
3109
  if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
2187
- const payload = asRecord2(await readJsonBody(req));
3110
+ const payload = asRecord3(await readJsonBody(req));
2188
3111
  const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
2189
3112
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
2190
3113
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
2191
3114
  const limit = Math.max(1, Math.min(100, Math.floor(limitRaw)));
2192
3115
  if (!rawCwd) {
2193
- setJson2(res, 400, { error: "Missing cwd" });
3116
+ setJson3(res, 400, { error: "Missing cwd" });
2194
3117
  return;
2195
3118
  }
2196
3119
  const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
2197
3120
  try {
2198
- const info = await stat2(cwd);
3121
+ const info = await stat3(cwd);
2199
3122
  if (!info.isDirectory()) {
2200
- setJson2(res, 400, { error: "cwd is not a directory" });
3123
+ setJson3(res, 400, { error: "cwd is not a directory" });
2201
3124
  return;
2202
3125
  }
2203
3126
  } catch {
2204
- setJson2(res, 404, { error: "cwd does not exist" });
3127
+ setJson3(res, 404, { error: "cwd does not exist" });
2205
3128
  return;
2206
3129
  }
2207
3130
  try {
2208
3131
  const files = await listFilesWithRipgrep(cwd);
2209
3132
  const scored = files.map((path) => ({ path, score: scoreFileCandidate(path, query) })).filter((row) => query.length === 0 || row.score < 10).sort((a, b) => a.score - b.score || a.path.localeCompare(b.path)).slice(0, limit).map((row) => ({ path: row.path }));
2210
- setJson2(res, 200, { data: scored });
3133
+ setJson3(res, 200, { data: scored });
2211
3134
  } catch (error) {
2212
- setJson2(res, 500, { error: getErrorMessage2(error, "Failed to search files") });
3135
+ setJson3(res, 500, { error: getErrorMessage3(error, "Failed to search files") });
2213
3136
  }
2214
3137
  return;
2215
3138
  }
2216
3139
  if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
2217
- const cache = await readThreadTitleCache();
2218
- setJson2(res, 200, { data: cache });
3140
+ const cache = await readMergedThreadTitleCache();
3141
+ setJson3(res, 200, { data: cache });
2219
3142
  return;
2220
3143
  }
2221
3144
  if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
2222
- const payload = asRecord2(await readJsonBody(req));
3145
+ const payload = asRecord3(await readJsonBody(req));
2223
3146
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
2224
3147
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
2225
3148
  const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
2226
3149
  if (!query) {
2227
- setJson2(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
3150
+ setJson3(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
2228
3151
  return;
2229
3152
  }
2230
3153
  const index = await getThreadSearchIndex();
2231
3154
  const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
2232
- setJson2(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
3155
+ setJson3(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
2233
3156
  return;
2234
3157
  }
2235
3158
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
2236
- const payload = asRecord2(await readJsonBody(req));
3159
+ const payload = asRecord3(await readJsonBody(req));
2237
3160
  const id = typeof payload?.id === "string" ? payload.id : "";
2238
3161
  const title = typeof payload?.title === "string" ? payload.title : "";
2239
3162
  if (!id) {
2240
- setJson2(res, 400, { error: "Missing id" });
3163
+ setJson3(res, 400, { error: "Missing id" });
2241
3164
  return;
2242
3165
  }
2243
3166
  const cache = await readThreadTitleCache();
2244
3167
  const next2 = title ? updateThreadTitleCache(cache, id, title) : removeFromThreadTitleCache(cache, id);
2245
3168
  await writeThreadTitleCache(next2);
2246
- setJson2(res, 200, { ok: true });
3169
+ setJson3(res, 200, { ok: true });
2247
3170
  return;
2248
3171
  }
2249
3172
  if (req.method === "GET" && url.pathname === "/codex-api/events") {
@@ -2278,8 +3201,8 @@ data: ${JSON.stringify({ ok: true })}
2278
3201
  }
2279
3202
  next();
2280
3203
  } catch (error) {
2281
- const message = getErrorMessage2(error, "Unknown bridge error");
2282
- setJson2(res, 502, { error: message });
3204
+ const message = getErrorMessage3(error, "Unknown bridge error");
3205
+ setJson3(res, 502, { error: message });
2283
3206
  }
2284
3207
  };
2285
3208
  middleware.dispose = () => {
@@ -2416,8 +3339,8 @@ function createAuthSession(password) {
2416
3339
  }
2417
3340
 
2418
3341
  // src/server/localBrowseUi.ts
2419
- import { dirname, extname, join as join3 } from "path";
2420
- import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
3342
+ import { dirname, extname, join as join4 } from "path";
3343
+ import { open, readFile as readFile4, readdir as readdir3, stat as stat4 } from "fs/promises";
2421
3344
  var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
2422
3345
  ".txt",
2423
3346
  ".md",
@@ -2532,7 +3455,7 @@ async function probeFileIsText(localPath) {
2532
3455
  async function isTextEditableFile(localPath) {
2533
3456
  if (isTextEditablePath(localPath)) return true;
2534
3457
  try {
2535
- const fileStat = await stat3(localPath);
3458
+ const fileStat = await stat4(localPath);
2536
3459
  if (!fileStat.isFile()) return false;
2537
3460
  return await probeFileIsText(localPath);
2538
3461
  } catch {
@@ -2554,8 +3477,8 @@ function escapeForInlineScriptString(value) {
2554
3477
  async function getDirectoryItems(localPath) {
2555
3478
  const entries = await readdir3(localPath, { withFileTypes: true });
2556
3479
  const withMeta = await Promise.all(entries.map(async (entry) => {
2557
- const entryPath = join3(localPath, entry.name);
2558
- const entryStat = await stat3(entryPath);
3480
+ const entryPath = join4(localPath, entry.name);
3481
+ const entryStat = await stat4(entryPath);
2559
3482
  const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
2560
3483
  return {
2561
3484
  name: entry.name,
@@ -2578,9 +3501,9 @@ async function createDirectoryListingHtml(localPath) {
2578
3501
  const rows = items.map((item) => {
2579
3502
  const suffix = item.isDirectory ? "/" : "";
2580
3503
  const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
2581
- return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
3504
+ return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a><span class="row-actions">${editAction}</span></li>`;
2582
3505
  }).join("\n");
2583
- const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
3506
+ const parentLink = localPath !== parentPath ? `<a href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : "";
2584
3507
  return `<!doctype html>
2585
3508
  <html lang="en">
2586
3509
  <head>
@@ -2594,8 +3517,27 @@ async function createDirectoryListingHtml(localPath) {
2594
3517
  ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
2595
3518
  .file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
2596
3519
  .file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
2597
- .icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; text-decoration: none; }
3520
+ .header-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
3521
+ .header-parent-link { color: #9ec8ff; font-size: 14px; padding: 8px 10px; border: 1px solid #2a4569; border-radius: 10px; background: #101f3a; }
3522
+ .header-parent-link:hover { text-decoration: none; filter: brightness(1.08); }
3523
+ .header-open-btn {
3524
+ height: 42px;
3525
+ padding: 0 14px;
3526
+ border: 1px solid #4f8de0;
3527
+ border-radius: 10px;
3528
+ background: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
3529
+ color: #eef6ff;
3530
+ font-weight: 700;
3531
+ letter-spacing: 0.01em;
3532
+ cursor: pointer;
3533
+ box-shadow: 0 6px 18px rgba(33, 90, 199, 0.35);
3534
+ }
3535
+ .header-open-btn:hover { filter: brightness(1.08); }
3536
+ .header-open-btn:disabled { opacity: 0.6; cursor: default; }
3537
+ .row-actions { display: inline-flex; align-items: center; gap: 8px; min-width: 42px; justify-content: flex-end; }
3538
+ .icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; color: #dbe6ff; text-decoration: none; cursor: pointer; }
2598
3539
  .icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
3540
+ .status { margin: 10px 0 0; color: #8cc2ff; min-height: 1.25em; }
2599
3541
  h1 { font-size: 18px; margin: 0; word-break: break-all; }
2600
3542
  @media (max-width: 640px) {
2601
3543
  body { margin: 12px; }
@@ -2607,13 +3549,51 @@ async function createDirectoryListingHtml(localPath) {
2607
3549
  </head>
2608
3550
  <body>
2609
3551
  <h1>Index of ${escapeHtml(localPath)}</h1>
2610
- ${parentLink}
3552
+ <div class="header-actions">
3553
+ ${parentLink ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : ""}
3554
+ <button class="header-open-btn open-folder-btn" type="button" aria-label="Open current folder in Codex" title="Open folder in Codex" data-path="${escapeHtml(localPath)}">Open folder in Codex</button>
3555
+ </div>
3556
+ <p id="status" class="status"></p>
2611
3557
  <ul>${rows}</ul>
3558
+ <script>
3559
+ const status = document.getElementById('status');
3560
+ document.addEventListener('click', async (event) => {
3561
+ const target = event.target;
3562
+ if (!(target instanceof Element)) return;
3563
+ const button = target.closest('.open-folder-btn');
3564
+ if (!(button instanceof HTMLButtonElement)) return;
3565
+
3566
+ const path = button.getAttribute('data-path') || '';
3567
+ if (!path) return;
3568
+ button.disabled = true;
3569
+ status.textContent = 'Opening folder in Codex...';
3570
+ try {
3571
+ const response = await fetch('/codex-api/project-root', {
3572
+ method: 'POST',
3573
+ headers: { 'Content-Type': 'application/json' },
3574
+ body: JSON.stringify({
3575
+ path,
3576
+ createIfMissing: false,
3577
+ label: '',
3578
+ }),
3579
+ });
3580
+ if (!response.ok) {
3581
+ status.textContent = 'Failed to open folder.';
3582
+ button.disabled = false;
3583
+ return;
3584
+ }
3585
+ window.location.assign('/#/');
3586
+ } catch {
3587
+ status.textContent = 'Failed to open folder.';
3588
+ button.disabled = false;
3589
+ }
3590
+ });
3591
+ </script>
2612
3592
  </body>
2613
3593
  </html>`;
2614
3594
  }
2615
3595
  async function createTextEditorHtml(localPath) {
2616
- const content = await readFile3(localPath, "utf8");
3596
+ const content = await readFile4(localPath, "utf8");
2617
3597
  const parentPath = dirname(localPath);
2618
3598
  const language = languageForPath(localPath);
2619
3599
  const safeContentLiteral = escapeForInlineScriptString(content);
@@ -2684,8 +3664,8 @@ async function createTextEditorHtml(localPath) {
2684
3664
  // src/server/httpServer.ts
2685
3665
  import { WebSocketServer } from "ws";
2686
3666
  var __dirname = dirname2(fileURLToPath(import.meta.url));
2687
- var distDir = join4(__dirname, "..", "dist");
2688
- var spaEntryFile = join4(distDir, "index.html");
3667
+ var distDir = join5(__dirname, "..", "dist");
3668
+ var spaEntryFile = join5(distDir, "index.html");
2689
3669
  var IMAGE_CONTENT_TYPES = {
2690
3670
  ".avif": "image/avif",
2691
3671
  ".bmp": "image/bmp",
@@ -2762,7 +3742,7 @@ function createServer(options = {}) {
2762
3742
  return;
2763
3743
  }
2764
3744
  try {
2765
- const fileStat = await stat4(localPath);
3745
+ const fileStat = await stat5(localPath);
2766
3746
  res.setHeader("Cache-Control", "private, no-store");
2767
3747
  if (fileStat.isDirectory()) {
2768
3748
  const html = await createDirectoryListingHtml(localPath);
@@ -2785,7 +3765,7 @@ function createServer(options = {}) {
2785
3765
  return;
2786
3766
  }
2787
3767
  try {
2788
- const fileStat = await stat4(localPath);
3768
+ const fileStat = await stat5(localPath);
2789
3769
  if (!fileStat.isFile()) {
2790
3770
  res.status(400).json({ error: "Expected file path." });
2791
3771
  return;
@@ -2809,7 +3789,7 @@ function createServer(options = {}) {
2809
3789
  }
2810
3790
  const body = typeof req.body === "string" ? req.body : "";
2811
3791
  try {
2812
- await writeFile3(localPath, body, "utf8");
3792
+ await writeFile4(localPath, body, "utf8");
2813
3793
  res.status(200).json({ ok: true });
2814
3794
  } catch {
2815
3795
  res.status(404).json({ error: "File not found." });
@@ -2887,10 +3867,26 @@ function generatePassword() {
2887
3867
  // src/cli/index.ts
2888
3868
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
2889
3869
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
3870
+ var hasPromptedCloudflaredInstall = false;
3871
+ function getCodexHomePath() {
3872
+ return process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
3873
+ }
3874
+ function getCloudflaredPromptMarkerPath() {
3875
+ return join6(getCodexHomePath(), ".cloudflared-install-prompted");
3876
+ }
3877
+ function hasPromptedCloudflaredInstallPersisted() {
3878
+ return existsSync3(getCloudflaredPromptMarkerPath());
3879
+ }
3880
+ async function persistCloudflaredInstallPrompted() {
3881
+ const codexHome = getCodexHomePath();
3882
+ mkdirSync(codexHome, { recursive: true });
3883
+ await writeFile5(getCloudflaredPromptMarkerPath(), `${Date.now()}
3884
+ `, "utf8");
3885
+ }
2890
3886
  async function readCliVersion() {
2891
3887
  try {
2892
- const packageJsonPath = join5(__dirname2, "..", "package.json");
2893
- const raw = await readFile4(packageJsonPath, "utf8");
3888
+ const packageJsonPath = join6(__dirname2, "..", "package.json");
3889
+ const raw = await readFile5(packageJsonPath, "utf8");
2894
3890
  const parsed = JSON.parse(raw);
2895
3891
  return typeof parsed.version === "string" ? parsed.version : "unknown";
2896
3892
  } catch {
@@ -2915,13 +3911,13 @@ function runWithStatus(command, args) {
2915
3911
  return result.status ?? -1;
2916
3912
  }
2917
3913
  function getUserNpmPrefix() {
2918
- return join5(homedir3(), ".npm-global");
3914
+ return join6(homedir4(), ".npm-global");
2919
3915
  }
2920
3916
  function resolveCodexCommand() {
2921
3917
  if (canRun("codex", ["--version"])) {
2922
3918
  return "codex";
2923
3919
  }
2924
- const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
3920
+ const userCandidate = join6(getUserNpmPrefix(), "bin", "codex");
2925
3921
  if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
2926
3922
  return userCandidate;
2927
3923
  }
@@ -2929,7 +3925,7 @@ function resolveCodexCommand() {
2929
3925
  if (!prefix) {
2930
3926
  return null;
2931
3927
  }
2932
- const candidate = join5(prefix, "bin", "codex");
3928
+ const candidate = join6(prefix, "bin", "codex");
2933
3929
  if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
2934
3930
  return candidate;
2935
3931
  }
@@ -2939,7 +3935,7 @@ function resolveCloudflaredCommand() {
2939
3935
  if (canRun("cloudflared", ["--version"])) {
2940
3936
  return "cloudflared";
2941
3937
  }
2942
- const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
3938
+ const localCandidate = join6(homedir4(), ".local", "bin", "cloudflared");
2943
3939
  if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
2944
3940
  return localCandidate;
2945
3941
  }
@@ -2955,7 +3951,7 @@ function mapCloudflaredLinuxArch(arch) {
2955
3951
  return null;
2956
3952
  }
2957
3953
  function downloadFile(url, destination) {
2958
- return new Promise((resolve2, reject) => {
3954
+ return new Promise((resolve3, reject) => {
2959
3955
  const request = (currentUrl) => {
2960
3956
  httpsGet(currentUrl, (response) => {
2961
3957
  const code = response.statusCode ?? 0;
@@ -2973,7 +3969,7 @@ function downloadFile(url, destination) {
2973
3969
  response.pipe(file);
2974
3970
  file.on("finish", () => {
2975
3971
  file.close();
2976
- resolve2();
3972
+ resolve3();
2977
3973
  });
2978
3974
  file.on("error", reject);
2979
3975
  }).on("error", reject);
@@ -2993,9 +3989,9 @@ async function ensureCloudflaredInstalledLinux() {
2993
3989
  if (!mappedArch) {
2994
3990
  throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
2995
3991
  }
2996
- const userBinDir = join5(homedir3(), ".local", "bin");
3992
+ const userBinDir = join6(homedir4(), ".local", "bin");
2997
3993
  mkdirSync(userBinDir, { recursive: true });
2998
- const destination = join5(userBinDir, "cloudflared");
3994
+ const destination = join6(userBinDir, "cloudflared");
2999
3995
  const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
3000
3996
  console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
3001
3997
  await downloadFile(downloadUrl, destination);
@@ -3009,6 +4005,14 @@ async function ensureCloudflaredInstalledLinux() {
3009
4005
  return installed;
3010
4006
  }
3011
4007
  async function shouldInstallCloudflaredInteractively() {
4008
+ if (hasPromptedCloudflaredInstall || hasPromptedCloudflaredInstallPersisted()) {
4009
+ return false;
4010
+ }
4011
+ hasPromptedCloudflaredInstall = true;
4012
+ await persistCloudflaredInstallPrompted();
4013
+ if (process.platform === "win32") {
4014
+ return false;
4015
+ }
3012
4016
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
3013
4017
  console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
3014
4018
  return false;
@@ -3027,6 +4031,9 @@ async function resolveCloudflaredForTunnel() {
3027
4031
  if (current) {
3028
4032
  return current;
3029
4033
  }
4034
+ if (process.platform === "win32") {
4035
+ return null;
4036
+ }
3030
4037
  const installApproved = await shouldInstallCloudflaredInteractively();
3031
4038
  if (!installApproved) {
3032
4039
  return null;
@@ -3034,8 +4041,8 @@ async function resolveCloudflaredForTunnel() {
3034
4041
  return ensureCloudflaredInstalledLinux();
3035
4042
  }
3036
4043
  function hasCodexAuth() {
3037
- const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3038
- return existsSync3(join5(codexHome, "auth.json"));
4044
+ const codexHome = getCodexHomePath();
4045
+ return existsSync3(join6(codexHome, "auth.json"));
3039
4046
  }
3040
4047
  function ensureCodexInstalled() {
3041
4048
  let codexCommand = resolveCodexCommand();
@@ -3053,7 +4060,7 @@ function ensureCodexInstalled() {
3053
4060
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
3054
4061
  `);
3055
4062
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
3056
- process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
4063
+ process.env.PATH = `${join6(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
3057
4064
  };
3058
4065
  if (isTermuxRuntime()) {
3059
4066
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -3102,7 +4109,7 @@ function printTermuxKeepAlive(lines) {
3102
4109
  }
3103
4110
  function openBrowser(url) {
3104
4111
  const command = process.platform === "darwin" ? { cmd: "open", args: [url] } : process.platform === "win32" ? { cmd: "cmd", args: ["/c", "start", "", url] } : { cmd: "xdg-open", args: [url] };
3105
- const child = spawn3(command.cmd, command.args, { detached: true, stdio: "ignore" });
4112
+ const child = spawn4(command.cmd, command.args, { detached: true, stdio: "ignore" });
3106
4113
  child.on("error", () => {
3107
4114
  });
3108
4115
  child.unref();
@@ -3133,8 +4140,8 @@ function getAccessibleUrls(port) {
3133
4140
  return Array.from(urls);
3134
4141
  }
3135
4142
  async function startCloudflaredTunnel(command, localPort) {
3136
- return new Promise((resolve2, reject) => {
3137
- const child = spawn3(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
4143
+ return new Promise((resolve3, reject) => {
4144
+ const child = spawn4(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
3138
4145
  stdio: ["ignore", "pipe", "pipe"]
3139
4146
  });
3140
4147
  const timeout = setTimeout(() => {
@@ -3150,7 +4157,7 @@ async function startCloudflaredTunnel(command, localPort) {
3150
4157
  clearTimeout(timeout);
3151
4158
  child.stdout?.off("data", handleData);
3152
4159
  child.stderr?.off("data", handleData);
3153
- resolve2({ process: child, url: parsedUrl });
4160
+ resolve3({ process: child, url: parsedUrl });
3154
4161
  };
3155
4162
  const onError = (error) => {
3156
4163
  clearTimeout(timeout);
@@ -3169,7 +4176,7 @@ async function startCloudflaredTunnel(command, localPort) {
3169
4176
  });
3170
4177
  }
3171
4178
  function listenWithFallback(server, startPort) {
3172
- return new Promise((resolve2, reject) => {
4179
+ return new Promise((resolve3, reject) => {
3173
4180
  const attempt = (port) => {
3174
4181
  const onError = (error) => {
3175
4182
  server.off("listening", onListening);
@@ -3181,7 +4188,7 @@ function listenWithFallback(server, startPort) {
3181
4188
  };
3182
4189
  const onListening = () => {
3183
4190
  server.off("error", onError);
3184
- resolve2(port);
4191
+ resolve3(port);
3185
4192
  };
3186
4193
  server.once("error", onError);
3187
4194
  server.once("listening", onListening);
@@ -3190,8 +4197,72 @@ function listenWithFallback(server, startPort) {
3190
4197
  attempt(startPort);
3191
4198
  });
3192
4199
  }
4200
+ function getCodexGlobalStatePath2() {
4201
+ const codexHome = getCodexHomePath();
4202
+ return join6(codexHome, ".codex-global-state.json");
4203
+ }
4204
+ function normalizeUniqueStrings(value) {
4205
+ if (!Array.isArray(value)) return [];
4206
+ const next = [];
4207
+ for (const item of value) {
4208
+ if (typeof item !== "string") continue;
4209
+ const trimmed = item.trim();
4210
+ if (!trimmed || next.includes(trimmed)) continue;
4211
+ next.push(trimmed);
4212
+ }
4213
+ return next;
4214
+ }
4215
+ async function persistLaunchProject(projectPath) {
4216
+ const trimmed = projectPath.trim();
4217
+ if (!trimmed) return;
4218
+ const normalizedPath = isAbsolute3(trimmed) ? trimmed : resolve2(trimmed);
4219
+ const directoryInfo = await stat6(normalizedPath);
4220
+ if (!directoryInfo.isDirectory()) {
4221
+ throw new Error(`Not a directory: ${normalizedPath}`);
4222
+ }
4223
+ const statePath = getCodexGlobalStatePath2();
4224
+ let payload = {};
4225
+ try {
4226
+ const raw = await readFile5(statePath, "utf8");
4227
+ const parsed = JSON.parse(raw);
4228
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
4229
+ payload = parsed;
4230
+ }
4231
+ } catch {
4232
+ payload = {};
4233
+ }
4234
+ const roots = normalizeUniqueStrings(payload["electron-saved-workspace-roots"]);
4235
+ const activeRoots = normalizeUniqueStrings(payload["active-workspace-roots"]);
4236
+ payload["electron-saved-workspace-roots"] = [
4237
+ normalizedPath,
4238
+ ...roots.filter((value) => value !== normalizedPath)
4239
+ ];
4240
+ payload["active-workspace-roots"] = [
4241
+ normalizedPath,
4242
+ ...activeRoots.filter((value) => value !== normalizedPath)
4243
+ ];
4244
+ await writeFile5(statePath, JSON.stringify(payload), "utf8");
4245
+ }
4246
+ async function addProjectOnly(projectPath) {
4247
+ const trimmed = projectPath.trim();
4248
+ if (!trimmed) {
4249
+ throw new Error("Missing project path");
4250
+ }
4251
+ await persistLaunchProject(trimmed);
4252
+ }
3193
4253
  async function startServer(options) {
3194
4254
  const version = await readCliVersion();
4255
+ const projectPath = options.projectPath?.trim() ?? "";
4256
+ if (projectPath.length > 0) {
4257
+ try {
4258
+ await persistLaunchProject(projectPath);
4259
+ } catch (error) {
4260
+ const message = error instanceof Error ? error.message : String(error);
4261
+ console.warn(`
4262
+ [project] Could not open launch project: ${message}
4263
+ `);
4264
+ }
4265
+ }
3195
4266
  const codexCommand = ensureCodexInstalled() ?? resolveCodexCommand();
3196
4267
  if (!hasCodexAuth() && codexCommand) {
3197
4268
  console.log("\nCodex is not logged in. Starting `codex login`...\n");
@@ -3275,8 +4346,20 @@ async function runLogin() {
3275
4346
  console.log("\nStarting `codex login`...\n");
3276
4347
  runOrFail(codexCommand, ["login"], "Codex login");
3277
4348
  }
3278
- program.option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").action(async (opts) => {
3279
- await startServer(opts);
4349
+ program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").action(async (projectPath, opts) => {
4350
+ const rawArgv = process.argv.slice(2);
4351
+ const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
4352
+ let openProjectOnly = (opts.openProject ?? "").trim();
4353
+ if (!openProjectOnly && openProjectFlagIndex >= 0 && projectPath?.trim()) {
4354
+ openProjectOnly = projectPath.trim();
4355
+ }
4356
+ if (openProjectOnly.length > 0) {
4357
+ await addProjectOnly(openProjectOnly);
4358
+ console.log(`Added project: ${openProjectOnly}`);
4359
+ return;
4360
+ }
4361
+ const launchProject = (projectPath ?? "").trim();
4362
+ await startServer({ ...opts, projectPath: launchProject });
3280
4363
  });
3281
4364
  program.command("login").description("Install/check Codex CLI and run `codex login`").action(runLogin);
3282
4365
  program.command("help").description("Show codexui command help").action(() => {