@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/README.md +9 -6
- package/dist/assets/index-CiKOQNSF.css +1 -0
- package/dist/assets/index-DmGuhOhZ.js +1443 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +1353 -270
- package/dist-cli/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/assets/index-BmK4cun7.js +0 -1440
- package/dist/assets/index-IKfGXpjw.css +0 -1
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
|
|
7
|
-
import { homedir as
|
|
8
|
-
import { join as
|
|
9
|
-
import { spawn as
|
|
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
|
|
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
|
|
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
|
|
25
|
+
import { spawn as spawn3 } from "child_process";
|
|
26
26
|
import { randomBytes } from "crypto";
|
|
27
|
-
import { mkdtemp as
|
|
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
|
|
30
|
-
import { tmpdir as
|
|
31
|
-
import { basename, isAbsolute, join as
|
|
32
|
-
import { writeFile as
|
|
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/
|
|
34
|
+
// src/server/accountRoutes.ts
|
|
35
35
|
import { spawn } from "child_process";
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
900
|
+
function getCodexHomeDir2() {
|
|
64
901
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
65
|
-
return codexHome && codexHome.length > 0 ? codexHome :
|
|
902
|
+
return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
|
|
66
903
|
}
|
|
67
904
|
function getSkillsInstallDir() {
|
|
68
|
-
return
|
|
905
|
+
return join2(getCodexHomeDir2(), "skills");
|
|
69
906
|
}
|
|
70
907
|
async function runCommand(command, args, options = {}) {
|
|
71
|
-
await new Promise((
|
|
72
|
-
const proc =
|
|
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
|
-
|
|
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((
|
|
99
|
-
const proc =
|
|
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
|
-
|
|
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 =
|
|
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((
|
|
161
|
-
proc.on("close", (code) =>
|
|
162
|
-
proc.on("error", () =>
|
|
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 =
|
|
1095
|
+
const skillMd = join2(skillsDir, entry.name, "SKILL.md");
|
|
259
1096
|
try {
|
|
260
|
-
await
|
|
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
|
|
1107
|
+
return join2(getCodexHomeDir2(), "skills-sync.json");
|
|
271
1108
|
}
|
|
272
1109
|
async function readSkillsSyncState() {
|
|
273
1110
|
try {
|
|
274
|
-
const raw = await
|
|
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
|
|
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((
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
479
|
-
const gitDir =
|
|
1315
|
+
await mkdir2(localDir, { recursive: true });
|
|
1316
|
+
const gitDir = join2(localDir, ".git");
|
|
480
1317
|
let hasGitDir = false;
|
|
481
1318
|
try {
|
|
482
|
-
hasGitDir = (await
|
|
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 =
|
|
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
|
|
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 =
|
|
688
|
-
const skillsAgentsPath =
|
|
689
|
-
const codexAgentsPath =
|
|
690
|
-
await
|
|
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
|
|
696
|
-
await
|
|
1532
|
+
const content = await readFile2(codexAgentsPath, "utf8");
|
|
1533
|
+
await writeFile2(skillsAgentsPath, content, "utf8");
|
|
697
1534
|
copiedFromCodex = true;
|
|
698
1535
|
} else {
|
|
699
|
-
await
|
|
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
|
|
1542
|
+
const skillsAgentsStat = await stat2(skillsAgentsPath);
|
|
706
1543
|
if (!skillsAgentsStat.isFile()) {
|
|
707
|
-
await
|
|
708
|
-
await
|
|
1544
|
+
await rm2(skillsAgentsPath, { force: true, recursive: true });
|
|
1545
|
+
await writeFile2(skillsAgentsPath, "", "utf8");
|
|
709
1546
|
}
|
|
710
1547
|
} catch {
|
|
711
|
-
await
|
|
1548
|
+
await writeFile2(skillsAgentsPath, "", "utf8");
|
|
712
1549
|
}
|
|
713
1550
|
}
|
|
714
|
-
const relativeTarget =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1689
|
+
setJson2(res, 200, { data: results, installed, total: allEntries.length });
|
|
853
1690
|
} catch (error) {
|
|
854
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1720
|
+
setJson2(res, 200, { data: started });
|
|
884
1721
|
} catch (error) {
|
|
885
|
-
|
|
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 =
|
|
1728
|
+
const payload = asRecord2(await readJsonBody2(req));
|
|
892
1729
|
const token = typeof payload?.token === "string" ? payload.token.trim() : "";
|
|
893
1730
|
if (!token) {
|
|
894
|
-
|
|
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
|
-
|
|
1736
|
+
setJson2(res, 200, { ok: true, data: { githubUsername: username } });
|
|
900
1737
|
} catch (error) {
|
|
901
|
-
|
|
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
|
-
|
|
1752
|
+
setJson2(res, 200, { ok: true });
|
|
916
1753
|
} catch (error) {
|
|
917
|
-
|
|
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 =
|
|
1760
|
+
const payload = asRecord2(await readJsonBody2(req));
|
|
924
1761
|
const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
|
|
925
1762
|
if (!deviceCode) {
|
|
926
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1774
|
+
setJson2(res, 200, { ok: true, data: { githubUsername: username } });
|
|
938
1775
|
} catch (error) {
|
|
939
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1795
|
+
setJson2(res, 200, { ok: true, data: { synced: local.length } });
|
|
959
1796
|
} catch (error) {
|
|
960
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
1868
|
+
setJson2(res, 200, { ok: true, data: { synced: remote.length } });
|
|
1032
1869
|
} catch (error) {
|
|
1033
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1886
|
+
setJson2(res, 200, { content });
|
|
1050
1887
|
} catch (error) {
|
|
1051
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1920
|
+
setJson2(res, 200, { ok: true, path: skillDir });
|
|
1084
1921
|
} catch (error) {
|
|
1085
|
-
|
|
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 =
|
|
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 ?
|
|
1931
|
+
const target = path || (name ? join2(getSkillsInstallDir(), name) : "");
|
|
1095
1932
|
if (!target) {
|
|
1096
|
-
|
|
1933
|
+
setJson2(res, 400, { error: "Missing name or path" });
|
|
1097
1934
|
return true;
|
|
1098
1935
|
}
|
|
1099
|
-
await
|
|
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
|
-
|
|
1948
|
+
setJson2(res, 200, { ok: true, deletedPath: target });
|
|
1112
1949
|
} catch (error) {
|
|
1113
|
-
|
|
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
|
|
1958
|
+
function asRecord3(value) {
|
|
1122
1959
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1123
1960
|
}
|
|
1124
|
-
function
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
1145
|
-
const 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 =
|
|
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 =
|
|
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 =
|
|
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((
|
|
1197
|
-
const proc =
|
|
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
|
-
|
|
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
|
|
2059
|
+
function getCodexHomeDir3() {
|
|
1223
2060
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
1224
|
-
return codexHome && codexHome.length > 0 ? codexHome :
|
|
2061
|
+
return codexHome && codexHome.length > 0 ? codexHome : join3(homedir3(), ".codex");
|
|
1225
2062
|
}
|
|
1226
2063
|
async function runCommand2(command, args, options = {}) {
|
|
1227
|
-
await new Promise((
|
|
1228
|
-
const proc =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2099
|
+
const agentsPath = join3(repoRoot, "AGENTS.md");
|
|
1263
2100
|
try {
|
|
1264
|
-
await
|
|
2101
|
+
await stat3(agentsPath);
|
|
1265
2102
|
} catch {
|
|
1266
|
-
await
|
|
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((
|
|
1277
|
-
const proc =
|
|
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
|
-
|
|
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
|
|
2160
|
+
return join3(getCodexHomeDir3(), "auth.json");
|
|
1324
2161
|
}
|
|
1325
2162
|
async function readCodexAuth() {
|
|
1326
2163
|
try {
|
|
1327
|
-
const raw = await
|
|
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
|
|
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 =
|
|
2181
|
+
const record = asRecord3(value);
|
|
1342
2182
|
if (!record) return { titles: {}, order: [] };
|
|
1343
|
-
const rawTitles =
|
|
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
|
|
1370
|
-
const payload =
|
|
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
|
|
1381
|
-
payload =
|
|
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
|
|
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
|
|
2309
|
+
const raw = await readFile3(statePath, "utf8");
|
|
1393
2310
|
const parsed = JSON.parse(raw);
|
|
1394
|
-
payload =
|
|
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
|
|
1409
|
-
payload =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2401
|
+
setJson3(res, 400, { error: "No file in request" });
|
|
1485
2402
|
return;
|
|
1486
2403
|
}
|
|
1487
|
-
const uploadDir =
|
|
1488
|
-
await
|
|
1489
|
-
const destDir = await
|
|
1490
|
-
const destPath =
|
|
1491
|
-
await
|
|
1492
|
-
|
|
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
|
-
|
|
2411
|
+
setJson3(res, 500, { error: getErrorMessage3(err, "Upload failed") });
|
|
1495
2412
|
}
|
|
1496
2413
|
});
|
|
1497
2414
|
req.on("error", (err) => {
|
|
1498
|
-
|
|
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((
|
|
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", () =>
|
|
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 =
|
|
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 =
|
|
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((
|
|
1674
|
-
this.pending.set(id, { resolve:
|
|
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 =
|
|
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 =
|
|
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((
|
|
1783
|
-
const process2 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
1807
|
-
const properties =
|
|
1808
|
-
const methodDef =
|
|
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 =
|
|
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 =
|
|
1824
|
-
const properties =
|
|
1825
|
-
const methodDef =
|
|
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
|
|
2759
|
+
const outDir = await mkdtemp3(join3(tmpdir3(), "codex-web-local-schema-"));
|
|
1840
2760
|
await this.runGenerateSchemaCommand(outDir);
|
|
1841
|
-
const clientRequestPath =
|
|
1842
|
-
const raw = await
|
|
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
|
|
2772
|
+
const outDir = await mkdtemp3(join3(tmpdir3(), "codex-web-local-schema-"));
|
|
1853
2773
|
await this.runGenerateSchemaCommand(outDir);
|
|
1854
|
-
const serverNotificationPath =
|
|
1855
|
-
const raw = await
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2897
|
+
const body = asRecord3(payload);
|
|
1975
2898
|
if (!body || typeof body.method !== "string" || body.method.length === 0) {
|
|
1976
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2942
|
+
setJson3(res, 200, { data: state });
|
|
2020
2943
|
return;
|
|
2021
2944
|
}
|
|
2022
2945
|
if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
|
|
2023
|
-
|
|
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 =
|
|
2950
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2028
2951
|
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
2029
2952
|
if (!rawSourceCwd) {
|
|
2030
|
-
|
|
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
|
|
2958
|
+
const sourceInfo = await stat3(sourceCwd);
|
|
2036
2959
|
if (!sourceInfo.isDirectory()) {
|
|
2037
|
-
|
|
2960
|
+
setJson3(res, 400, { error: "sourceCwd is not a directory" });
|
|
2038
2961
|
return;
|
|
2039
2962
|
}
|
|
2040
2963
|
} catch {
|
|
2041
|
-
|
|
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 =
|
|
2055
|
-
await
|
|
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 =
|
|
2984
|
+
const parent = join3(worktreesRoot, candidate);
|
|
2062
2985
|
try {
|
|
2063
|
-
await
|
|
2986
|
+
await stat3(parent);
|
|
2064
2987
|
continue;
|
|
2065
2988
|
} catch {
|
|
2066
2989
|
worktreeId = candidate;
|
|
2067
2990
|
worktreeParent = parent;
|
|
2068
|
-
worktreeCwd =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
3021
|
+
const record = asRecord3(payload);
|
|
2099
3022
|
if (!record) {
|
|
2100
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
3047
|
+
const info = await stat3(normalizedPath);
|
|
2125
3048
|
if (!info.isDirectory()) {
|
|
2126
|
-
|
|
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
|
|
3056
|
+
await mkdir3(normalizedPath, { recursive: true });
|
|
2134
3057
|
} else if (!pathExists) {
|
|
2135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3084
|
+
const baseInfo = await stat3(normalizedBasePath);
|
|
2162
3085
|
if (!baseInfo.isDirectory()) {
|
|
2163
|
-
|
|
3086
|
+
setJson3(res, 400, { error: "basePath is not a directory" });
|
|
2164
3087
|
return;
|
|
2165
3088
|
}
|
|
2166
3089
|
} catch {
|
|
2167
|
-
|
|
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 =
|
|
3096
|
+
const candidatePath = join3(normalizedBasePath, candidateName);
|
|
2174
3097
|
try {
|
|
2175
|
-
await
|
|
3098
|
+
await stat3(candidatePath);
|
|
2176
3099
|
index += 1;
|
|
2177
3100
|
continue;
|
|
2178
3101
|
} catch {
|
|
2179
|
-
|
|
3102
|
+
setJson3(res, 200, { data: { name: candidateName, path: candidatePath } });
|
|
2180
3103
|
return;
|
|
2181
3104
|
}
|
|
2182
3105
|
}
|
|
2183
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
3121
|
+
const info = await stat3(cwd);
|
|
2199
3122
|
if (!info.isDirectory()) {
|
|
2200
|
-
|
|
3123
|
+
setJson3(res, 400, { error: "cwd is not a directory" });
|
|
2201
3124
|
return;
|
|
2202
3125
|
}
|
|
2203
3126
|
} catch {
|
|
2204
|
-
|
|
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
|
-
|
|
3133
|
+
setJson3(res, 200, { data: scored });
|
|
2211
3134
|
} catch (error) {
|
|
2212
|
-
|
|
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
|
|
2218
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
2282
|
-
|
|
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
|
|
2420
|
-
import { open, readFile as
|
|
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
|
|
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 =
|
|
2558
|
-
const entryStat = await
|
|
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 ? `<
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
2688
|
-
var spaEntryFile =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2893
|
-
const raw = await
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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((
|
|
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
|
-
|
|
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 =
|
|
3992
|
+
const userBinDir = join6(homedir4(), ".local", "bin");
|
|
2997
3993
|
mkdirSync(userBinDir, { recursive: true });
|
|
2998
|
-
const destination =
|
|
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 =
|
|
3038
|
-
return existsSync3(
|
|
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 = `${
|
|
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 =
|
|
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((
|
|
3137
|
-
const child =
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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(() => {
|