@nervmor/codexui 1.0.1 → 1.0.3
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-BRHjQaZ8.css +1 -0
- package/dist/assets/index-C2tfimEi.js +1441 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +904 -237
- package/dist-cli/index.js.map +1 -1
- package/package.json +1 -2
- package/dist/assets/index-BUh0F7vz.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 } from "fs/promises";
|
|
7
|
+
import { homedir as homedir4, networkInterfaces } from "os";
|
|
8
|
+
import { join as join6 } 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,721 @@ 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 setJson(res, statusCode, payload) {
|
|
62
|
+
res.statusCode = statusCode;
|
|
63
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
64
|
+
res.end(JSON.stringify(payload));
|
|
65
|
+
}
|
|
44
66
|
function getErrorMessage(payload, fallback) {
|
|
45
67
|
if (payload instanceof Error && payload.message.trim().length > 0) {
|
|
46
68
|
return payload.message;
|
|
47
69
|
}
|
|
48
70
|
const record = asRecord(payload);
|
|
71
|
+
const error = record?.error;
|
|
72
|
+
if (typeof error === "string" && error.trim().length > 0) {
|
|
73
|
+
return error.trim();
|
|
74
|
+
}
|
|
75
|
+
if (typeof record?.message === "string" && record.message.trim().length > 0) {
|
|
76
|
+
return record.message.trim();
|
|
77
|
+
}
|
|
78
|
+
return fallback;
|
|
79
|
+
}
|
|
80
|
+
function getCodexHomeDir() {
|
|
81
|
+
const codexHome = process.env.CODEX_HOME?.trim();
|
|
82
|
+
return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
|
|
83
|
+
}
|
|
84
|
+
function getActiveAuthPath() {
|
|
85
|
+
return join(getCodexHomeDir(), "auth.json");
|
|
86
|
+
}
|
|
87
|
+
function getAccountsStatePath() {
|
|
88
|
+
return join(getCodexHomeDir(), "accounts.json");
|
|
89
|
+
}
|
|
90
|
+
function getAccountsSnapshotRoot() {
|
|
91
|
+
return join(getCodexHomeDir(), "accounts");
|
|
92
|
+
}
|
|
93
|
+
function toStorageId(accountId) {
|
|
94
|
+
return createHash("sha256").update(accountId).digest("hex");
|
|
95
|
+
}
|
|
96
|
+
function normalizeRateLimitWindow(value) {
|
|
97
|
+
const record = asRecord(value);
|
|
98
|
+
if (!record) return null;
|
|
99
|
+
const usedPercent = readNumber(record.usedPercent ?? record.used_percent);
|
|
100
|
+
if (usedPercent === null) return null;
|
|
101
|
+
return {
|
|
102
|
+
usedPercent,
|
|
103
|
+
windowMinutes: readNumber(record.windowDurationMins ?? record.window_minutes),
|
|
104
|
+
resetsAt: readNumber(record.resetsAt ?? record.resets_at)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function normalizeCreditsSnapshot(value) {
|
|
108
|
+
const record = asRecord(value);
|
|
109
|
+
if (!record) return null;
|
|
110
|
+
const hasCredits = readBoolean(record.hasCredits ?? record.has_credits);
|
|
111
|
+
const unlimited = readBoolean(record.unlimited);
|
|
112
|
+
if (hasCredits === null || unlimited === null) return null;
|
|
113
|
+
return {
|
|
114
|
+
hasCredits,
|
|
115
|
+
unlimited,
|
|
116
|
+
balance: readString(record.balance)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function normalizeRateLimitSnapshot(value) {
|
|
120
|
+
const record = asRecord(value);
|
|
121
|
+
if (!record) return null;
|
|
122
|
+
const primary = normalizeRateLimitWindow(record.primary);
|
|
123
|
+
const secondary = normalizeRateLimitWindow(record.secondary);
|
|
124
|
+
const credits = normalizeCreditsSnapshot(record.credits);
|
|
125
|
+
if (!primary && !secondary && !credits) return null;
|
|
126
|
+
return {
|
|
127
|
+
limitId: readString(record.limitId ?? record.limit_id),
|
|
128
|
+
limitName: readString(record.limitName ?? record.limit_name),
|
|
129
|
+
primary,
|
|
130
|
+
secondary,
|
|
131
|
+
credits,
|
|
132
|
+
planType: readString(record.planType ?? record.plan_type)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function pickCodexRateLimitSnapshot(payload) {
|
|
136
|
+
const record = asRecord(payload);
|
|
137
|
+
if (!record) return null;
|
|
138
|
+
const rateLimitsByLimitId = asRecord(record.rateLimitsByLimitId ?? record.rate_limits_by_limit_id);
|
|
139
|
+
const codexBucket = normalizeRateLimitSnapshot(rateLimitsByLimitId?.codex);
|
|
140
|
+
if (codexBucket) return codexBucket;
|
|
141
|
+
return normalizeRateLimitSnapshot(record.rateLimits ?? record.rate_limits);
|
|
142
|
+
}
|
|
143
|
+
function normalizeStoredAccountEntry(value) {
|
|
144
|
+
const record = asRecord(value);
|
|
145
|
+
const accountId = readString(record?.accountId);
|
|
146
|
+
const storageId = readString(record?.storageId);
|
|
147
|
+
const lastRefreshedAtIso = readString(record?.lastRefreshedAtIso);
|
|
148
|
+
const quotaStatusRaw = readString(record?.quotaStatus);
|
|
149
|
+
const quotaStatus = quotaStatusRaw === "loading" || quotaStatusRaw === "ready" || quotaStatusRaw === "error" ? quotaStatusRaw : "idle";
|
|
150
|
+
if (!accountId || !storageId || !lastRefreshedAtIso) return null;
|
|
151
|
+
return {
|
|
152
|
+
accountId,
|
|
153
|
+
storageId,
|
|
154
|
+
authMode: readString(record?.authMode),
|
|
155
|
+
email: readString(record?.email),
|
|
156
|
+
planType: readString(record?.planType),
|
|
157
|
+
lastRefreshedAtIso,
|
|
158
|
+
lastActivatedAtIso: readString(record?.lastActivatedAtIso),
|
|
159
|
+
quotaSnapshot: normalizeRateLimitSnapshot(record?.quotaSnapshot),
|
|
160
|
+
quotaUpdatedAtIso: readString(record?.quotaUpdatedAtIso),
|
|
161
|
+
quotaStatus,
|
|
162
|
+
quotaError: readString(record?.quotaError)
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async function readStoredAccountsState() {
|
|
166
|
+
try {
|
|
167
|
+
const raw = await readFile(getAccountsStatePath(), "utf8");
|
|
168
|
+
const parsed = asRecord(JSON.parse(raw));
|
|
169
|
+
const activeAccountId = readString(parsed?.activeAccountId);
|
|
170
|
+
const rawAccounts = Array.isArray(parsed?.accounts) ? parsed.accounts : [];
|
|
171
|
+
const accounts = rawAccounts.map((entry) => normalizeStoredAccountEntry(entry)).filter((entry) => entry !== null);
|
|
172
|
+
return { activeAccountId, accounts };
|
|
173
|
+
} catch {
|
|
174
|
+
return { activeAccountId: null, accounts: [] };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function writeStoredAccountsState(state) {
|
|
178
|
+
await writeFile(getAccountsStatePath(), JSON.stringify(state, null, 2), { encoding: "utf8", mode: 384 });
|
|
179
|
+
}
|
|
180
|
+
function withUpsertedAccount(state, nextEntry) {
|
|
181
|
+
const rest = state.accounts.filter((entry) => entry.accountId !== nextEntry.accountId);
|
|
182
|
+
return {
|
|
183
|
+
activeAccountId: state.activeAccountId,
|
|
184
|
+
accounts: [nextEntry, ...rest]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function sortAccounts(accounts, activeAccountId) {
|
|
188
|
+
return [...accounts].sort((left, right) => {
|
|
189
|
+
const leftActive = left.accountId === activeAccountId ? 1 : 0;
|
|
190
|
+
const rightActive = right.accountId === activeAccountId ? 1 : 0;
|
|
191
|
+
if (leftActive !== rightActive) return rightActive - leftActive;
|
|
192
|
+
return right.lastRefreshedAtIso.localeCompare(left.lastRefreshedAtIso);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function toPublicAccountEntry(entry, activeAccountId) {
|
|
196
|
+
return {
|
|
197
|
+
...entry,
|
|
198
|
+
isActive: entry.accountId === activeAccountId
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function decodeBase64UrlJson(input) {
|
|
202
|
+
try {
|
|
203
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
204
|
+
const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
205
|
+
const raw = Buffer.from(`${normalized}${padding}`, "base64").toString("utf8");
|
|
206
|
+
const parsed = JSON.parse(raw);
|
|
207
|
+
return asRecord(parsed);
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function extractTokenMetadata(accessToken) {
|
|
213
|
+
if (!accessToken || typeof accessToken !== "string") {
|
|
214
|
+
return { email: null, planType: null };
|
|
215
|
+
}
|
|
216
|
+
const parts = accessToken.split(".");
|
|
217
|
+
if (parts.length < 2) {
|
|
218
|
+
return { email: null, planType: null };
|
|
219
|
+
}
|
|
220
|
+
const payload = decodeBase64UrlJson(parts[1] ?? "");
|
|
221
|
+
const profile = asRecord(payload?.["https://api.openai.com/profile"]);
|
|
222
|
+
const auth = asRecord(payload?.["https://api.openai.com/auth"]);
|
|
223
|
+
return {
|
|
224
|
+
email: typeof profile?.email === "string" && profile.email.trim().length > 0 ? profile.email.trim() : null,
|
|
225
|
+
planType: typeof auth?.chatgpt_plan_type === "string" && auth.chatgpt_plan_type.trim().length > 0 ? auth.chatgpt_plan_type.trim() : null
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function readAuthFileFromPath(path) {
|
|
229
|
+
const raw = await readFile(path, "utf8");
|
|
230
|
+
const parsed = JSON.parse(raw);
|
|
231
|
+
const accountId = parsed.tokens?.account_id?.trim() ?? "";
|
|
232
|
+
if (!accountId) {
|
|
233
|
+
throw new Error("missing_account_id");
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
raw,
|
|
237
|
+
parsed,
|
|
238
|
+
accountId,
|
|
239
|
+
authMode: typeof parsed.auth_mode === "string" && parsed.auth_mode.trim().length > 0 ? parsed.auth_mode.trim() : null,
|
|
240
|
+
metadata: extractTokenMetadata(parsed.tokens?.access_token)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function getSnapshotPath(storageId) {
|
|
244
|
+
return join(getAccountsSnapshotRoot(), storageId, "auth.json");
|
|
245
|
+
}
|
|
246
|
+
async function writeSnapshot(storageId, raw) {
|
|
247
|
+
const dir = join(getAccountsSnapshotRoot(), storageId);
|
|
248
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
249
|
+
await writeFile(getSnapshotPath(storageId), raw, { encoding: "utf8", mode: 384 });
|
|
250
|
+
}
|
|
251
|
+
async function readRuntimeAccountMetadata(appServer) {
|
|
252
|
+
const payload = asRecord(await appServer.rpc("account/read", { refreshToken: false }));
|
|
253
|
+
const account = asRecord(payload?.account);
|
|
254
|
+
return {
|
|
255
|
+
email: typeof account?.email === "string" && account.email.trim().length > 0 ? account.email.trim() : null,
|
|
256
|
+
planType: typeof account?.planType === "string" && account.planType.trim().length > 0 ? account.planType.trim() : null
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
async function validateSwitchedAccount(appServer) {
|
|
260
|
+
const metadata = await readRuntimeAccountMetadata(appServer);
|
|
261
|
+
const quotaPayload = await appServer.rpc("account/rateLimits/read", null);
|
|
262
|
+
return {
|
|
263
|
+
metadata,
|
|
264
|
+
quotaSnapshot: pickCodexRateLimitSnapshot(quotaPayload)
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function restoreActiveAuth(raw) {
|
|
268
|
+
const path = getActiveAuthPath();
|
|
269
|
+
if (raw === null) {
|
|
270
|
+
await rm(path, { force: true });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
await writeFile(path, raw, { encoding: "utf8", mode: 384 });
|
|
274
|
+
}
|
|
275
|
+
async function fileExists(path) {
|
|
276
|
+
try {
|
|
277
|
+
await stat(path);
|
|
278
|
+
return true;
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function withTemporaryCodexAppServer(authRaw, run) {
|
|
284
|
+
const tempCodexHome = await mkdtemp(join(tmpdir(), "codexui-account-"));
|
|
285
|
+
const authPath = join(tempCodexHome, "auth.json");
|
|
286
|
+
await writeFile(authPath, authRaw, { encoding: "utf8", mode: 384 });
|
|
287
|
+
const proc = spawn("codex", [...APP_SERVER_ARGS], {
|
|
288
|
+
env: { ...process.env, CODEX_HOME: tempCodexHome },
|
|
289
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
290
|
+
});
|
|
291
|
+
let disposed = false;
|
|
292
|
+
let initialized = false;
|
|
293
|
+
let initializePromise = null;
|
|
294
|
+
let readBuffer = "";
|
|
295
|
+
let nextId = 1;
|
|
296
|
+
const pending = /* @__PURE__ */ new Map();
|
|
297
|
+
const rejectAllPending = (error) => {
|
|
298
|
+
for (const request of pending.values()) {
|
|
299
|
+
request.reject(error);
|
|
300
|
+
}
|
|
301
|
+
pending.clear();
|
|
302
|
+
};
|
|
303
|
+
proc.stdout.setEncoding("utf8");
|
|
304
|
+
proc.stdout.on("data", (chunk) => {
|
|
305
|
+
readBuffer += chunk;
|
|
306
|
+
let lineEnd = readBuffer.indexOf("\n");
|
|
307
|
+
while (lineEnd !== -1) {
|
|
308
|
+
const line = readBuffer.slice(0, lineEnd).trim();
|
|
309
|
+
readBuffer = readBuffer.slice(lineEnd + 1);
|
|
310
|
+
if (line.length > 0) {
|
|
311
|
+
try {
|
|
312
|
+
const message = JSON.parse(line);
|
|
313
|
+
if (typeof message.id === "number" && pending.has(message.id)) {
|
|
314
|
+
const current = pending.get(message.id);
|
|
315
|
+
pending.delete(message.id);
|
|
316
|
+
if (!current) {
|
|
317
|
+
lineEnd = readBuffer.indexOf("\n");
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (message.error?.message) {
|
|
321
|
+
current.reject(new Error(message.error.message));
|
|
322
|
+
} else {
|
|
323
|
+
current.resolve(message.result);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
lineEnd = readBuffer.indexOf("\n");
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
proc.stderr.setEncoding("utf8");
|
|
333
|
+
proc.stderr.on("data", () => {
|
|
334
|
+
});
|
|
335
|
+
proc.on("error", (error) => {
|
|
336
|
+
rejectAllPending(error instanceof Error ? error : new Error("codex app-server failed to start"));
|
|
337
|
+
});
|
|
338
|
+
proc.on("exit", () => {
|
|
339
|
+
if (disposed) return;
|
|
340
|
+
rejectAllPending(new Error("codex app-server exited unexpectedly"));
|
|
341
|
+
});
|
|
342
|
+
const sendLine = (payload) => {
|
|
343
|
+
proc.stdin.write(`${JSON.stringify(payload)}
|
|
344
|
+
`);
|
|
345
|
+
};
|
|
346
|
+
const call = async (method, params) => {
|
|
347
|
+
const id = nextId++;
|
|
348
|
+
return await new Promise((resolve2, reject) => {
|
|
349
|
+
pending.set(id, { resolve: resolve2, reject });
|
|
350
|
+
sendLine({
|
|
351
|
+
jsonrpc: "2.0",
|
|
352
|
+
id,
|
|
353
|
+
method,
|
|
354
|
+
params
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
const ensureInitialized = async () => {
|
|
359
|
+
if (initialized) return;
|
|
360
|
+
if (initializePromise) {
|
|
361
|
+
await initializePromise;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
initializePromise = call("initialize", {
|
|
365
|
+
clientInfo: {
|
|
366
|
+
name: "codexui-account-refresh",
|
|
367
|
+
version: "0.1.0"
|
|
368
|
+
},
|
|
369
|
+
capabilities: {
|
|
370
|
+
experimentalApi: true
|
|
371
|
+
}
|
|
372
|
+
}).then(() => {
|
|
373
|
+
sendLine({
|
|
374
|
+
jsonrpc: "2.0",
|
|
375
|
+
method: "initialized"
|
|
376
|
+
});
|
|
377
|
+
initialized = true;
|
|
378
|
+
}).finally(() => {
|
|
379
|
+
initializePromise = null;
|
|
380
|
+
});
|
|
381
|
+
await initializePromise;
|
|
382
|
+
};
|
|
383
|
+
const dispose = async () => {
|
|
384
|
+
if (disposed) return;
|
|
385
|
+
disposed = true;
|
|
386
|
+
rejectAllPending(new Error("codex app-server stopped"));
|
|
387
|
+
try {
|
|
388
|
+
proc.stdin.end();
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
proc.kill("SIGTERM");
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
await rm(tempCodexHome, { recursive: true, force: true });
|
|
396
|
+
};
|
|
397
|
+
try {
|
|
398
|
+
await ensureInitialized();
|
|
399
|
+
return await run(call);
|
|
400
|
+
} finally {
|
|
401
|
+
await dispose();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async function inspectStoredAccount(entry) {
|
|
405
|
+
const snapshotPath = getSnapshotPath(entry.storageId);
|
|
406
|
+
const authRaw = await readFile(snapshotPath, "utf8");
|
|
407
|
+
return await withTemporaryCodexAppServer(authRaw, async (rpc) => {
|
|
408
|
+
const accountPayload = asRecord(await rpc("account/read", { refreshToken: false }));
|
|
409
|
+
const account = asRecord(accountPayload?.account);
|
|
410
|
+
const quotaPayload = await rpc("account/rateLimits/read", null);
|
|
411
|
+
return {
|
|
412
|
+
metadata: {
|
|
413
|
+
email: typeof account?.email === "string" && account.email.trim().length > 0 ? account.email.trim() : entry.email,
|
|
414
|
+
planType: typeof account?.planType === "string" && account.planType.trim().length > 0 ? account.planType.trim() : entry.planType
|
|
415
|
+
},
|
|
416
|
+
quotaSnapshot: pickCodexRateLimitSnapshot(quotaPayload)
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
function shouldRefreshAccountQuota(entry) {
|
|
421
|
+
if (entry.quotaStatus === "loading") return false;
|
|
422
|
+
if (!entry.quotaUpdatedAtIso) return true;
|
|
423
|
+
const updatedAtMs = Date.parse(entry.quotaUpdatedAtIso);
|
|
424
|
+
if (!Number.isFinite(updatedAtMs)) return true;
|
|
425
|
+
return Date.now() - updatedAtMs >= ACCOUNT_QUOTA_REFRESH_TTL_MS;
|
|
426
|
+
}
|
|
427
|
+
async function replaceStoredAccount(nextEntry, activeAccountId) {
|
|
428
|
+
const state = await readStoredAccountsState();
|
|
429
|
+
const nextState = withUpsertedAccount({
|
|
430
|
+
activeAccountId,
|
|
431
|
+
accounts: state.accounts
|
|
432
|
+
}, nextEntry);
|
|
433
|
+
await writeStoredAccountsState({
|
|
434
|
+
activeAccountId,
|
|
435
|
+
accounts: nextState.accounts
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
async function refreshAccountsInBackground(accountIds, activeAccountId) {
|
|
439
|
+
for (const accountId of accountIds) {
|
|
440
|
+
const state = await readStoredAccountsState();
|
|
441
|
+
const entry = state.accounts.find((item) => item.accountId === accountId);
|
|
442
|
+
if (!entry) continue;
|
|
443
|
+
try {
|
|
444
|
+
const inspected = await inspectStoredAccount(entry);
|
|
445
|
+
await replaceStoredAccount({
|
|
446
|
+
...entry,
|
|
447
|
+
email: inspected.metadata.email ?? entry.email,
|
|
448
|
+
planType: inspected.metadata.planType ?? entry.planType,
|
|
449
|
+
quotaSnapshot: inspected.quotaSnapshot ?? entry.quotaSnapshot,
|
|
450
|
+
quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
451
|
+
quotaStatus: "ready",
|
|
452
|
+
quotaError: null
|
|
453
|
+
}, activeAccountId);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
await replaceStoredAccount({
|
|
456
|
+
...entry,
|
|
457
|
+
quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
458
|
+
quotaStatus: "error",
|
|
459
|
+
quotaError: getErrorMessage(error, "Failed to refresh account quota")
|
|
460
|
+
}, activeAccountId);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function scheduleAccountsBackgroundRefresh(options = {}) {
|
|
465
|
+
const state = await readStoredAccountsState();
|
|
466
|
+
if (state.accounts.length === 0) return state;
|
|
467
|
+
if (backgroundRefreshPromise) return state;
|
|
468
|
+
const allowedIds = options.accountIds ? new Set(options.accountIds) : null;
|
|
469
|
+
const candidates = state.accounts.filter((entry) => !allowedIds || allowedIds.has(entry.accountId)).filter((entry) => options.force === true || shouldRefreshAccountQuota(entry)).sort((left, right) => {
|
|
470
|
+
const prioritize = options.prioritizeAccountId ?? "";
|
|
471
|
+
const leftPriority = left.accountId === prioritize ? 1 : 0;
|
|
472
|
+
const rightPriority = right.accountId === prioritize ? 1 : 0;
|
|
473
|
+
if (leftPriority !== rightPriority) return rightPriority - leftPriority;
|
|
474
|
+
return 0;
|
|
475
|
+
});
|
|
476
|
+
if (candidates.length === 0) return state;
|
|
477
|
+
const candidateIds = new Set(candidates.map((entry) => entry.accountId));
|
|
478
|
+
const markedState = {
|
|
479
|
+
activeAccountId: state.activeAccountId,
|
|
480
|
+
accounts: state.accounts.map((entry) => candidateIds.has(entry.accountId) ? {
|
|
481
|
+
...entry,
|
|
482
|
+
quotaStatus: "loading",
|
|
483
|
+
quotaError: null
|
|
484
|
+
} : entry)
|
|
485
|
+
};
|
|
486
|
+
await writeStoredAccountsState(markedState);
|
|
487
|
+
backgroundRefreshPromise = refreshAccountsInBackground(
|
|
488
|
+
candidates.map((entry) => entry.accountId),
|
|
489
|
+
markedState.activeAccountId
|
|
490
|
+
).finally(() => {
|
|
491
|
+
backgroundRefreshPromise = null;
|
|
492
|
+
});
|
|
493
|
+
return markedState;
|
|
494
|
+
}
|
|
495
|
+
async function importAccountFromAuthPath(path) {
|
|
496
|
+
const imported = await readAuthFileFromPath(path);
|
|
497
|
+
const storageId = toStorageId(imported.accountId);
|
|
498
|
+
await writeSnapshot(storageId, imported.raw);
|
|
499
|
+
const state = await readStoredAccountsState();
|
|
500
|
+
const existing = state.accounts.find((entry) => entry.accountId === imported.accountId) ?? null;
|
|
501
|
+
const nextEntry = {
|
|
502
|
+
accountId: imported.accountId,
|
|
503
|
+
storageId,
|
|
504
|
+
authMode: imported.authMode,
|
|
505
|
+
email: imported.metadata.email ?? existing?.email ?? null,
|
|
506
|
+
planType: imported.metadata.planType ?? existing?.planType ?? null,
|
|
507
|
+
lastRefreshedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
508
|
+
lastActivatedAtIso: existing?.lastActivatedAtIso ?? null,
|
|
509
|
+
quotaSnapshot: existing?.quotaSnapshot ?? null,
|
|
510
|
+
quotaUpdatedAtIso: existing?.quotaUpdatedAtIso ?? null,
|
|
511
|
+
quotaStatus: existing?.quotaStatus ?? "idle",
|
|
512
|
+
quotaError: existing?.quotaError ?? null
|
|
513
|
+
};
|
|
514
|
+
const nextState = withUpsertedAccount(state, nextEntry);
|
|
515
|
+
await writeStoredAccountsState(nextState);
|
|
516
|
+
return {
|
|
517
|
+
activeAccountId: nextState.activeAccountId,
|
|
518
|
+
importedAccountId: imported.accountId,
|
|
519
|
+
accounts: sortAccounts(nextState.accounts, nextState.activeAccountId).map((entry) => toPublicAccountEntry(entry, nextState.activeAccountId))
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
async function handleAccountRoutes(req, res, url, context) {
|
|
523
|
+
const { appServer } = context;
|
|
524
|
+
if (req.method === "GET" && url.pathname === "/codex-api/accounts") {
|
|
525
|
+
const state = await scheduleAccountsBackgroundRefresh();
|
|
526
|
+
setJson(res, 200, {
|
|
527
|
+
data: {
|
|
528
|
+
activeAccountId: state.activeAccountId,
|
|
529
|
+
accounts: sortAccounts(state.accounts, state.activeAccountId).map((entry) => toPublicAccountEntry(entry, state.activeAccountId))
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
if (req.method === "GET" && url.pathname === "/codex-api/accounts/active") {
|
|
535
|
+
const state = await readStoredAccountsState();
|
|
536
|
+
const active = state.activeAccountId ? state.accounts.find((entry) => entry.accountId === state.activeAccountId) ?? null : null;
|
|
537
|
+
setJson(res, 200, {
|
|
538
|
+
data: active ? toPublicAccountEntry(active, state.activeAccountId) : null
|
|
539
|
+
});
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
if (req.method === "POST" && url.pathname === "/codex-api/accounts/refresh") {
|
|
543
|
+
try {
|
|
544
|
+
const imported = await importAccountFromAuthPath(getActiveAuthPath());
|
|
545
|
+
try {
|
|
546
|
+
appServer.dispose();
|
|
547
|
+
const inspection = await validateSwitchedAccount(appServer);
|
|
548
|
+
const state = await readStoredAccountsState();
|
|
549
|
+
const importedAccountId = imported.importedAccountId;
|
|
550
|
+
const target = state.accounts.find((entry) => entry.accountId === importedAccountId) ?? null;
|
|
551
|
+
if (!target) {
|
|
552
|
+
throw new Error("account_not_found");
|
|
553
|
+
}
|
|
554
|
+
const nextEntry = {
|
|
555
|
+
...target,
|
|
556
|
+
email: inspection.metadata.email ?? target.email,
|
|
557
|
+
planType: inspection.metadata.planType ?? target.planType,
|
|
558
|
+
lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
559
|
+
quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
|
|
560
|
+
quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
561
|
+
quotaStatus: "ready",
|
|
562
|
+
quotaError: null
|
|
563
|
+
};
|
|
564
|
+
const nextState = withUpsertedAccount({
|
|
565
|
+
activeAccountId: importedAccountId,
|
|
566
|
+
accounts: state.accounts
|
|
567
|
+
}, nextEntry);
|
|
568
|
+
await writeStoredAccountsState({
|
|
569
|
+
activeAccountId: importedAccountId,
|
|
570
|
+
accounts: nextState.accounts
|
|
571
|
+
});
|
|
572
|
+
const backgroundState = await scheduleAccountsBackgroundRefresh({
|
|
573
|
+
force: true,
|
|
574
|
+
prioritizeAccountId: importedAccountId,
|
|
575
|
+
accountIds: nextState.accounts.filter((entry) => entry.accountId !== importedAccountId).map((entry) => entry.accountId)
|
|
576
|
+
});
|
|
577
|
+
setJson(res, 200, {
|
|
578
|
+
data: {
|
|
579
|
+
activeAccountId: importedAccountId,
|
|
580
|
+
importedAccountId,
|
|
581
|
+
accounts: sortAccounts(backgroundState.accounts, importedAccountId).map((entry) => toPublicAccountEntry(entry, importedAccountId))
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
} catch (error) {
|
|
585
|
+
setJson(res, 502, {
|
|
586
|
+
error: "account_refresh_failed",
|
|
587
|
+
message: getErrorMessage(error, "Failed to refresh account")
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
} catch (error) {
|
|
591
|
+
const message = getErrorMessage(error, "Failed to refresh account");
|
|
592
|
+
if (message === "missing_account_id") {
|
|
593
|
+
setJson(res, 400, { error: "missing_account_id", message: "Current auth.json is missing tokens.account_id." });
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
setJson(res, 400, { error: "invalid_auth_json", message: "Failed to parse the current auth.json file." });
|
|
597
|
+
}
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
if (req.method === "POST" && url.pathname === "/codex-api/accounts/switch") {
|
|
601
|
+
try {
|
|
602
|
+
if (appServer.listPendingServerRequests().length > 0) {
|
|
603
|
+
setJson(res, 409, {
|
|
604
|
+
error: "account_switch_blocked",
|
|
605
|
+
message: "Finish pending approval requests before switching accounts."
|
|
606
|
+
});
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
const rawBody = await new Promise((resolve2, reject) => {
|
|
610
|
+
let body = "";
|
|
611
|
+
req.setEncoding("utf8");
|
|
612
|
+
req.on("data", (chunk) => {
|
|
613
|
+
body += chunk;
|
|
614
|
+
});
|
|
615
|
+
req.on("end", () => resolve2(body));
|
|
616
|
+
req.on("error", reject);
|
|
617
|
+
});
|
|
618
|
+
const payload = asRecord(rawBody.length > 0 ? JSON.parse(rawBody) : {});
|
|
619
|
+
const accountId = typeof payload?.accountId === "string" ? payload.accountId.trim() : "";
|
|
620
|
+
if (!accountId) {
|
|
621
|
+
setJson(res, 400, { error: "account_not_found", message: "Missing accountId." });
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
const state = await readStoredAccountsState();
|
|
625
|
+
const target = state.accounts.find((entry) => entry.accountId === accountId) ?? null;
|
|
626
|
+
if (!target) {
|
|
627
|
+
setJson(res, 404, { error: "account_not_found", message: "The requested account was not found." });
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
const snapshotPath = getSnapshotPath(target.storageId);
|
|
631
|
+
if (!await fileExists(snapshotPath)) {
|
|
632
|
+
setJson(res, 404, { error: "account_not_found", message: "The requested account snapshot is missing." });
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
let previousRaw = null;
|
|
636
|
+
try {
|
|
637
|
+
previousRaw = await readFile(getActiveAuthPath(), "utf8");
|
|
638
|
+
} catch {
|
|
639
|
+
previousRaw = null;
|
|
640
|
+
}
|
|
641
|
+
const targetRaw = await readFile(snapshotPath, "utf8");
|
|
642
|
+
await writeFile(getActiveAuthPath(), targetRaw, { encoding: "utf8", mode: 384 });
|
|
643
|
+
try {
|
|
644
|
+
appServer.dispose();
|
|
645
|
+
const inspection = await validateSwitchedAccount(appServer);
|
|
646
|
+
const nextEntry = {
|
|
647
|
+
...target,
|
|
648
|
+
email: inspection.metadata.email ?? target.email,
|
|
649
|
+
planType: inspection.metadata.planType ?? target.planType,
|
|
650
|
+
lastActivatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
651
|
+
quotaSnapshot: inspection.quotaSnapshot ?? target.quotaSnapshot,
|
|
652
|
+
quotaUpdatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
653
|
+
quotaStatus: "ready",
|
|
654
|
+
quotaError: null
|
|
655
|
+
};
|
|
656
|
+
const nextState = withUpsertedAccount({
|
|
657
|
+
activeAccountId: accountId,
|
|
658
|
+
accounts: state.accounts
|
|
659
|
+
}, nextEntry);
|
|
660
|
+
await writeStoredAccountsState({
|
|
661
|
+
activeAccountId: accountId,
|
|
662
|
+
accounts: nextState.accounts
|
|
663
|
+
});
|
|
664
|
+
void scheduleAccountsBackgroundRefresh({
|
|
665
|
+
force: true,
|
|
666
|
+
prioritizeAccountId: accountId,
|
|
667
|
+
accountIds: nextState.accounts.filter((entry) => entry.accountId !== accountId).map((entry) => entry.accountId)
|
|
668
|
+
});
|
|
669
|
+
setJson(res, 200, {
|
|
670
|
+
ok: true,
|
|
671
|
+
data: {
|
|
672
|
+
activeAccountId: accountId,
|
|
673
|
+
account: toPublicAccountEntry(nextEntry, accountId)
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
} catch (error) {
|
|
677
|
+
await restoreActiveAuth(previousRaw);
|
|
678
|
+
appServer.dispose();
|
|
679
|
+
setJson(res, 502, {
|
|
680
|
+
error: "account_switch_failed",
|
|
681
|
+
message: getErrorMessage(error, "Failed to switch account")
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
} catch (error) {
|
|
685
|
+
setJson(res, 400, {
|
|
686
|
+
error: "invalid_auth_json",
|
|
687
|
+
message: getErrorMessage(error, "Failed to switch account")
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/server/skillsRoutes.ts
|
|
696
|
+
import { spawn as spawn2 } from "child_process";
|
|
697
|
+
import { mkdtemp as mkdtemp2, readFile as readFile2, readdir, rm as rm2, mkdir as mkdir2, stat as stat2, lstat, readlink, symlink } from "fs/promises";
|
|
698
|
+
import { existsSync } from "fs";
|
|
699
|
+
import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
|
|
700
|
+
import { join as join2 } from "path";
|
|
701
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
702
|
+
function asRecord2(value) {
|
|
703
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
704
|
+
}
|
|
705
|
+
function getErrorMessage2(payload, fallback) {
|
|
706
|
+
if (payload instanceof Error && payload.message.trim().length > 0) {
|
|
707
|
+
return payload.message;
|
|
708
|
+
}
|
|
709
|
+
const record = asRecord2(payload);
|
|
49
710
|
if (!record) return fallback;
|
|
50
711
|
const error = record.error;
|
|
51
712
|
if (typeof error === "string" && error.length > 0) return error;
|
|
52
|
-
const nestedError =
|
|
713
|
+
const nestedError = asRecord2(error);
|
|
53
714
|
if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
|
|
54
715
|
return nestedError.message;
|
|
55
716
|
}
|
|
56
717
|
return fallback;
|
|
57
718
|
}
|
|
58
|
-
function
|
|
719
|
+
function setJson2(res, statusCode, payload) {
|
|
59
720
|
res.statusCode = statusCode;
|
|
60
721
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
61
722
|
res.end(JSON.stringify(payload));
|
|
62
723
|
}
|
|
63
|
-
function
|
|
724
|
+
function getCodexHomeDir2() {
|
|
64
725
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
65
|
-
return codexHome && codexHome.length > 0 ? codexHome :
|
|
726
|
+
return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
|
|
66
727
|
}
|
|
67
728
|
function getSkillsInstallDir() {
|
|
68
|
-
return
|
|
729
|
+
return join2(getCodexHomeDir2(), "skills");
|
|
69
730
|
}
|
|
70
731
|
async function runCommand(command, args, options = {}) {
|
|
71
732
|
await new Promise((resolve2, reject) => {
|
|
72
|
-
const proc =
|
|
733
|
+
const proc = spawn2(command, args, {
|
|
73
734
|
cwd: options.cwd,
|
|
74
735
|
env: process.env,
|
|
75
736
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -96,7 +757,7 @@ async function runCommand(command, args, options = {}) {
|
|
|
96
757
|
}
|
|
97
758
|
async function runCommandWithOutput(command, args, options = {}) {
|
|
98
759
|
return await new Promise((resolve2, reject) => {
|
|
99
|
-
const proc =
|
|
760
|
+
const proc = spawn2(command, args, {
|
|
100
761
|
cwd: options.cwd,
|
|
101
762
|
env: process.env,
|
|
102
763
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -152,7 +813,7 @@ var skillsTreeCache = null;
|
|
|
152
813
|
var metaCache = /* @__PURE__ */ new Map();
|
|
153
814
|
async function getGhToken() {
|
|
154
815
|
try {
|
|
155
|
-
const proc =
|
|
816
|
+
const proc = spawn2("gh", ["auth", "token"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
156
817
|
let out = "";
|
|
157
818
|
proc.stdout.on("data", (d) => {
|
|
158
819
|
out += d.toString();
|
|
@@ -255,9 +916,9 @@ async function scanInstalledSkillsFromDisk() {
|
|
|
255
916
|
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
256
917
|
for (const entry of entries) {
|
|
257
918
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
258
|
-
const skillMd =
|
|
919
|
+
const skillMd = join2(skillsDir, entry.name, "SKILL.md");
|
|
259
920
|
try {
|
|
260
|
-
await
|
|
921
|
+
await stat2(skillMd);
|
|
261
922
|
map.set(entry.name, { name: entry.name, path: skillMd, enabled: true });
|
|
262
923
|
} catch {
|
|
263
924
|
}
|
|
@@ -267,11 +928,11 @@ async function scanInstalledSkillsFromDisk() {
|
|
|
267
928
|
return map;
|
|
268
929
|
}
|
|
269
930
|
function getSkillsSyncStatePath() {
|
|
270
|
-
return
|
|
931
|
+
return join2(getCodexHomeDir2(), "skills-sync.json");
|
|
271
932
|
}
|
|
272
933
|
async function readSkillsSyncState() {
|
|
273
934
|
try {
|
|
274
|
-
const raw = await
|
|
935
|
+
const raw = await readFile2(getSkillsSyncStatePath(), "utf8");
|
|
275
936
|
const parsed = JSON.parse(raw);
|
|
276
937
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
277
938
|
} catch {
|
|
@@ -279,7 +940,7 @@ async function readSkillsSyncState() {
|
|
|
279
940
|
}
|
|
280
941
|
}
|
|
281
942
|
async function writeSkillsSyncState(state) {
|
|
282
|
-
await
|
|
943
|
+
await writeFile2(getSkillsSyncStatePath(), JSON.stringify(state), "utf8");
|
|
283
944
|
}
|
|
284
945
|
async function getGithubJson(url, token, method = "GET", body) {
|
|
285
946
|
const resp = await fetch(url, {
|
|
@@ -402,7 +1063,7 @@ async function ensurePrivateForkFromUpstream(token, username, repoName) {
|
|
|
402
1063
|
}
|
|
403
1064
|
if (!ready) throw new Error("Private mirror repo was created but is not available yet");
|
|
404
1065
|
if (!created) return;
|
|
405
|
-
const tmp = await
|
|
1066
|
+
const tmp = await mkdtemp2(join2(tmpdir2(), "codex-skills-seed-"));
|
|
406
1067
|
try {
|
|
407
1068
|
const upstreamUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
|
|
408
1069
|
const branch = getPreferredSyncBranch();
|
|
@@ -419,7 +1080,7 @@ async function ensurePrivateForkFromUpstream(token, username, repoName) {
|
|
|
419
1080
|
}
|
|
420
1081
|
await runCommand("git", ["push", "-u", "origin", `HEAD:${branch}`], { cwd: tmp });
|
|
421
1082
|
} finally {
|
|
422
|
-
await
|
|
1083
|
+
await rm2(tmp, { recursive: true, force: true });
|
|
423
1084
|
}
|
|
424
1085
|
}
|
|
425
1086
|
async function readRemoteSkillsManifest(token, repoOwner, repoName) {
|
|
@@ -440,7 +1101,7 @@ async function readRemoteSkillsManifest(token, repoOwner, repoName) {
|
|
|
440
1101
|
if (!Array.isArray(parsed)) return [];
|
|
441
1102
|
const skills = [];
|
|
442
1103
|
for (const row of parsed) {
|
|
443
|
-
const item =
|
|
1104
|
+
const item = asRecord2(row);
|
|
444
1105
|
const owner = typeof item?.owner === "string" ? item.owner : "";
|
|
445
1106
|
const name = typeof item?.name === "string" ? item.name : "";
|
|
446
1107
|
if (!name) continue;
|
|
@@ -475,11 +1136,11 @@ function toGitHubTokenRemote(repoOwner, repoName, token) {
|
|
|
475
1136
|
}
|
|
476
1137
|
async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
|
|
477
1138
|
const localDir = getSkillsInstallDir();
|
|
478
|
-
await
|
|
479
|
-
const gitDir =
|
|
1139
|
+
await mkdir2(localDir, { recursive: true });
|
|
1140
|
+
const gitDir = join2(localDir, ".git");
|
|
480
1141
|
let hasGitDir = false;
|
|
481
1142
|
try {
|
|
482
|
-
hasGitDir = (await
|
|
1143
|
+
hasGitDir = (await stat2(gitDir)).isDirectory();
|
|
483
1144
|
} catch {
|
|
484
1145
|
hasGitDir = false;
|
|
485
1146
|
}
|
|
@@ -591,7 +1252,7 @@ async function walkFileMtimes(rootDir, currentDir, out) {
|
|
|
591
1252
|
for (const entry of entries) {
|
|
592
1253
|
const entryName = String(entry.name);
|
|
593
1254
|
if (entryName === ".git") continue;
|
|
594
|
-
const absolutePath =
|
|
1255
|
+
const absolutePath = join2(currentDir, entryName);
|
|
595
1256
|
const relativePath = absolutePath.slice(rootDir.length + 1);
|
|
596
1257
|
if (entry.isDirectory()) {
|
|
597
1258
|
await walkFileMtimes(rootDir, absolutePath, out);
|
|
@@ -599,7 +1260,7 @@ async function walkFileMtimes(rootDir, currentDir, out) {
|
|
|
599
1260
|
}
|
|
600
1261
|
if (!entry.isFile()) continue;
|
|
601
1262
|
try {
|
|
602
|
-
const info = await
|
|
1263
|
+
const info = await stat2(absolutePath);
|
|
603
1264
|
out.set(relativePath, info.mtimeMs);
|
|
604
1265
|
} catch {
|
|
605
1266
|
}
|
|
@@ -684,41 +1345,41 @@ async function autoPushSyncedSkills(appServer) {
|
|
|
684
1345
|
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
685
1346
|
}
|
|
686
1347
|
async function ensureCodexAgentsSymlinkToSkillsAgents() {
|
|
687
|
-
const codexHomeDir =
|
|
688
|
-
const skillsAgentsPath =
|
|
689
|
-
const codexAgentsPath =
|
|
690
|
-
await
|
|
1348
|
+
const codexHomeDir = getCodexHomeDir2();
|
|
1349
|
+
const skillsAgentsPath = join2(codexHomeDir, "skills", "AGENTS.md");
|
|
1350
|
+
const codexAgentsPath = join2(codexHomeDir, "AGENTS.md");
|
|
1351
|
+
await mkdir2(join2(codexHomeDir, "skills"), { recursive: true });
|
|
691
1352
|
let copiedFromCodex = false;
|
|
692
1353
|
try {
|
|
693
1354
|
const codexAgentsStat = await lstat(codexAgentsPath);
|
|
694
1355
|
if (codexAgentsStat.isFile() || codexAgentsStat.isSymbolicLink()) {
|
|
695
|
-
const content = await
|
|
696
|
-
await
|
|
1356
|
+
const content = await readFile2(codexAgentsPath, "utf8");
|
|
1357
|
+
await writeFile2(skillsAgentsPath, content, "utf8");
|
|
697
1358
|
copiedFromCodex = true;
|
|
698
1359
|
} else {
|
|
699
|
-
await
|
|
1360
|
+
await rm2(codexAgentsPath, { force: true, recursive: true });
|
|
700
1361
|
}
|
|
701
1362
|
} catch {
|
|
702
1363
|
}
|
|
703
1364
|
if (!copiedFromCodex) {
|
|
704
1365
|
try {
|
|
705
|
-
const skillsAgentsStat = await
|
|
1366
|
+
const skillsAgentsStat = await stat2(skillsAgentsPath);
|
|
706
1367
|
if (!skillsAgentsStat.isFile()) {
|
|
707
|
-
await
|
|
708
|
-
await
|
|
1368
|
+
await rm2(skillsAgentsPath, { force: true, recursive: true });
|
|
1369
|
+
await writeFile2(skillsAgentsPath, "", "utf8");
|
|
709
1370
|
}
|
|
710
1371
|
} catch {
|
|
711
|
-
await
|
|
1372
|
+
await writeFile2(skillsAgentsPath, "", "utf8");
|
|
712
1373
|
}
|
|
713
1374
|
}
|
|
714
|
-
const relativeTarget =
|
|
1375
|
+
const relativeTarget = join2("skills", "AGENTS.md");
|
|
715
1376
|
try {
|
|
716
1377
|
const current = await lstat(codexAgentsPath);
|
|
717
1378
|
if (current.isSymbolicLink()) {
|
|
718
1379
|
const existingTarget = await readlink(codexAgentsPath);
|
|
719
1380
|
if (existingTarget === relativeTarget) return;
|
|
720
1381
|
}
|
|
721
|
-
await
|
|
1382
|
+
await rm2(codexAgentsPath, { force: true, recursive: true });
|
|
722
1383
|
} catch {
|
|
723
1384
|
}
|
|
724
1385
|
await symlink(relativeTarget, codexAgentsPath);
|
|
@@ -768,7 +1429,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
|
|
|
768
1429
|
startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
769
1430
|
startupSyncStatus.lastAction = "startup-sync-complete";
|
|
770
1431
|
} catch (error) {
|
|
771
|
-
startupSyncStatus.lastError =
|
|
1432
|
+
startupSyncStatus.lastError = getErrorMessage2(error, "startup-sync-failed");
|
|
772
1433
|
startupSyncStatus.lastAction = "startup-sync-failed";
|
|
773
1434
|
} finally {
|
|
774
1435
|
startupSyncStatus.inProgress = false;
|
|
@@ -849,15 +1510,15 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
849
1510
|
installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
|
|
850
1511
|
}
|
|
851
1512
|
const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
|
|
852
|
-
|
|
1513
|
+
setJson2(res, 200, { data: results, installed, total: allEntries.length });
|
|
853
1514
|
} catch (error) {
|
|
854
|
-
|
|
1515
|
+
setJson2(res, 502, { error: getErrorMessage2(error, "Failed to fetch skills hub") });
|
|
855
1516
|
}
|
|
856
1517
|
return true;
|
|
857
1518
|
}
|
|
858
1519
|
if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
|
|
859
1520
|
const state = await readSkillsSyncState();
|
|
860
|
-
|
|
1521
|
+
setJson2(res, 200, {
|
|
861
1522
|
data: {
|
|
862
1523
|
loggedIn: Boolean(state.githubToken),
|
|
863
1524
|
githubUsername: state.githubUsername ?? "",
|
|
@@ -880,25 +1541,25 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
880
1541
|
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
|
|
881
1542
|
try {
|
|
882
1543
|
const started = await startGithubDeviceLogin();
|
|
883
|
-
|
|
1544
|
+
setJson2(res, 200, { data: started });
|
|
884
1545
|
} catch (error) {
|
|
885
|
-
|
|
1546
|
+
setJson2(res, 502, { error: getErrorMessage2(error, "Failed to start GitHub login") });
|
|
886
1547
|
}
|
|
887
1548
|
return true;
|
|
888
1549
|
}
|
|
889
1550
|
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
|
|
890
1551
|
try {
|
|
891
|
-
const payload =
|
|
1552
|
+
const payload = asRecord2(await readJsonBody2(req));
|
|
892
1553
|
const token = typeof payload?.token === "string" ? payload.token.trim() : "";
|
|
893
1554
|
if (!token) {
|
|
894
|
-
|
|
1555
|
+
setJson2(res, 400, { error: "Missing GitHub token" });
|
|
895
1556
|
return true;
|
|
896
1557
|
}
|
|
897
1558
|
const username = await resolveGithubUsername(token);
|
|
898
1559
|
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
899
|
-
|
|
1560
|
+
setJson2(res, 200, { ok: true, data: { githubUsername: username } });
|
|
900
1561
|
} catch (error) {
|
|
901
|
-
|
|
1562
|
+
setJson2(res, 502, { error: getErrorMessage2(error, "Failed to login with GitHub token") });
|
|
902
1563
|
}
|
|
903
1564
|
return true;
|
|
904
1565
|
}
|
|
@@ -912,31 +1573,31 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
912
1573
|
repoOwner: void 0,
|
|
913
1574
|
repoName: void 0
|
|
914
1575
|
});
|
|
915
|
-
|
|
1576
|
+
setJson2(res, 200, { ok: true });
|
|
916
1577
|
} catch (error) {
|
|
917
|
-
|
|
1578
|
+
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to logout GitHub") });
|
|
918
1579
|
}
|
|
919
1580
|
return true;
|
|
920
1581
|
}
|
|
921
1582
|
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
|
|
922
1583
|
try {
|
|
923
|
-
const payload =
|
|
1584
|
+
const payload = asRecord2(await readJsonBody2(req));
|
|
924
1585
|
const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
|
|
925
1586
|
if (!deviceCode) {
|
|
926
|
-
|
|
1587
|
+
setJson2(res, 400, { error: "Missing deviceCode" });
|
|
927
1588
|
return true;
|
|
928
1589
|
}
|
|
929
1590
|
const result = await completeGithubDeviceLogin(deviceCode);
|
|
930
1591
|
if (!result.token) {
|
|
931
|
-
|
|
1592
|
+
setJson2(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
|
|
932
1593
|
return true;
|
|
933
1594
|
}
|
|
934
1595
|
const token = result.token;
|
|
935
1596
|
const username = await resolveGithubUsername(token);
|
|
936
1597
|
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
937
|
-
|
|
1598
|
+
setJson2(res, 200, { ok: true, data: { githubUsername: username } });
|
|
938
1599
|
} catch (error) {
|
|
939
|
-
|
|
1600
|
+
setJson2(res, 502, { error: getErrorMessage2(error, "Failed to complete GitHub login") });
|
|
940
1601
|
}
|
|
941
1602
|
return true;
|
|
942
1603
|
}
|
|
@@ -944,20 +1605,20 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
944
1605
|
try {
|
|
945
1606
|
const state = await readSkillsSyncState();
|
|
946
1607
|
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
947
|
-
|
|
1608
|
+
setJson2(res, 400, { error: "Skills sync is not configured yet" });
|
|
948
1609
|
return true;
|
|
949
1610
|
}
|
|
950
1611
|
if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
|
|
951
|
-
|
|
1612
|
+
setJson2(res, 400, { error: "Refusing to push to upstream repository" });
|
|
952
1613
|
return true;
|
|
953
1614
|
}
|
|
954
1615
|
const local = await collectLocalSyncedSkills(appServer);
|
|
955
1616
|
const installedMap = await scanInstalledSkillsFromDisk();
|
|
956
1617
|
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
957
1618
|
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
958
|
-
|
|
1619
|
+
setJson2(res, 200, { ok: true, data: { synced: local.length } });
|
|
959
1620
|
} catch (error) {
|
|
960
|
-
|
|
1621
|
+
setJson2(res, 502, { error: getErrorMessage2(error, "Failed to push synced skills") });
|
|
961
1622
|
}
|
|
962
1623
|
return true;
|
|
963
1624
|
}
|
|
@@ -970,7 +1631,7 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
970
1631
|
await appServer.rpc("skills/list", { forceReload: true });
|
|
971
1632
|
} catch {
|
|
972
1633
|
}
|
|
973
|
-
|
|
1634
|
+
setJson2(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
|
|
974
1635
|
return true;
|
|
975
1636
|
}
|
|
976
1637
|
const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
|
|
@@ -1009,13 +1670,13 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1009
1670
|
"git"
|
|
1010
1671
|
]);
|
|
1011
1672
|
}
|
|
1012
|
-
const skillPath =
|
|
1673
|
+
const skillPath = join2(localDir, skill.name);
|
|
1013
1674
|
await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
|
|
1014
1675
|
}
|
|
1015
1676
|
const remoteNames = new Set(remote.map((row) => row.name));
|
|
1016
1677
|
for (const [name, localInfo] of localSkills.entries()) {
|
|
1017
1678
|
if (!remoteNames.has(name)) {
|
|
1018
|
-
await
|
|
1679
|
+
await rm2(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
|
|
1019
1680
|
}
|
|
1020
1681
|
}
|
|
1021
1682
|
const nextOwners = {};
|
|
@@ -1028,9 +1689,9 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1028
1689
|
await appServer.rpc("skills/list", { forceReload: true });
|
|
1029
1690
|
} catch {
|
|
1030
1691
|
}
|
|
1031
|
-
|
|
1692
|
+
setJson2(res, 200, { ok: true, data: { synced: remote.length } });
|
|
1032
1693
|
} catch (error) {
|
|
1033
|
-
|
|
1694
|
+
setJson2(res, 502, { error: getErrorMessage2(error, "Failed to pull synced skills") });
|
|
1034
1695
|
}
|
|
1035
1696
|
return true;
|
|
1036
1697
|
}
|
|
@@ -1039,26 +1700,26 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1039
1700
|
const owner = url.searchParams.get("owner") || "";
|
|
1040
1701
|
const name = url.searchParams.get("name") || "";
|
|
1041
1702
|
if (!owner || !name) {
|
|
1042
|
-
|
|
1703
|
+
setJson2(res, 400, { error: "Missing owner or name" });
|
|
1043
1704
|
return true;
|
|
1044
1705
|
}
|
|
1045
1706
|
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
|
|
1046
1707
|
const resp = await fetch(rawUrl);
|
|
1047
1708
|
if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
|
|
1048
1709
|
const content = await resp.text();
|
|
1049
|
-
|
|
1710
|
+
setJson2(res, 200, { content });
|
|
1050
1711
|
} catch (error) {
|
|
1051
|
-
|
|
1712
|
+
setJson2(res, 502, { error: getErrorMessage2(error, "Failed to fetch SKILL.md") });
|
|
1052
1713
|
}
|
|
1053
1714
|
return true;
|
|
1054
1715
|
}
|
|
1055
1716
|
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
|
|
1056
1717
|
try {
|
|
1057
|
-
const payload =
|
|
1718
|
+
const payload = asRecord2(await readJsonBody2(req));
|
|
1058
1719
|
const owner = typeof payload?.owner === "string" ? payload.owner : "";
|
|
1059
1720
|
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1060
1721
|
if (!owner || !name) {
|
|
1061
|
-
|
|
1722
|
+
setJson2(res, 400, { error: "Missing owner or name" });
|
|
1062
1723
|
return true;
|
|
1063
1724
|
}
|
|
1064
1725
|
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
@@ -1074,29 +1735,29 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1074
1735
|
"--method",
|
|
1075
1736
|
"git"
|
|
1076
1737
|
]);
|
|
1077
|
-
const skillDir =
|
|
1738
|
+
const skillDir = join2(installDest, name);
|
|
1078
1739
|
await ensureInstalledSkillIsValid(appServer, skillDir);
|
|
1079
1740
|
const syncState = await readSkillsSyncState();
|
|
1080
1741
|
const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
|
|
1081
1742
|
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1082
1743
|
await autoPushSyncedSkills(appServer);
|
|
1083
|
-
|
|
1744
|
+
setJson2(res, 200, { ok: true, path: skillDir });
|
|
1084
1745
|
} catch (error) {
|
|
1085
|
-
|
|
1746
|
+
setJson2(res, 502, { error: getErrorMessage2(error, "Failed to install skill") });
|
|
1086
1747
|
}
|
|
1087
1748
|
return true;
|
|
1088
1749
|
}
|
|
1089
1750
|
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
|
|
1090
1751
|
try {
|
|
1091
|
-
const payload =
|
|
1752
|
+
const payload = asRecord2(await readJsonBody2(req));
|
|
1092
1753
|
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1093
1754
|
const path = typeof payload?.path === "string" ? payload.path : "";
|
|
1094
|
-
const target = path || (name ?
|
|
1755
|
+
const target = path || (name ? join2(getSkillsInstallDir(), name) : "");
|
|
1095
1756
|
if (!target) {
|
|
1096
|
-
|
|
1757
|
+
setJson2(res, 400, { error: "Missing name or path" });
|
|
1097
1758
|
return true;
|
|
1098
1759
|
}
|
|
1099
|
-
await
|
|
1760
|
+
await rm2(target, { recursive: true, force: true });
|
|
1100
1761
|
if (name) {
|
|
1101
1762
|
const syncState = await readSkillsSyncState();
|
|
1102
1763
|
const nextOwners = { ...syncState.installedOwners ?? {} };
|
|
@@ -1108,9 +1769,9 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1108
1769
|
await appServer.rpc("skills/list", { forceReload: true });
|
|
1109
1770
|
} catch {
|
|
1110
1771
|
}
|
|
1111
|
-
|
|
1772
|
+
setJson2(res, 200, { ok: true, deletedPath: target });
|
|
1112
1773
|
} catch (error) {
|
|
1113
|
-
|
|
1774
|
+
setJson2(res, 502, { error: getErrorMessage2(error, "Failed to uninstall skill") });
|
|
1114
1775
|
}
|
|
1115
1776
|
return true;
|
|
1116
1777
|
}
|
|
@@ -1118,38 +1779,38 @@ async function handleSkillsRoutes(req, res, url, context) {
|
|
|
1118
1779
|
}
|
|
1119
1780
|
|
|
1120
1781
|
// src/server/codexAppServerBridge.ts
|
|
1121
|
-
function
|
|
1782
|
+
function asRecord3(value) {
|
|
1122
1783
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1123
1784
|
}
|
|
1124
|
-
function
|
|
1785
|
+
function getErrorMessage3(payload, fallback) {
|
|
1125
1786
|
if (payload instanceof Error && payload.message.trim().length > 0) {
|
|
1126
1787
|
return payload.message;
|
|
1127
1788
|
}
|
|
1128
|
-
const record =
|
|
1789
|
+
const record = asRecord3(payload);
|
|
1129
1790
|
if (!record) return fallback;
|
|
1130
1791
|
const error = record.error;
|
|
1131
1792
|
if (typeof error === "string" && error.length > 0) return error;
|
|
1132
|
-
const nestedError =
|
|
1793
|
+
const nestedError = asRecord3(error);
|
|
1133
1794
|
if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
|
|
1134
1795
|
return nestedError.message;
|
|
1135
1796
|
}
|
|
1136
1797
|
return fallback;
|
|
1137
1798
|
}
|
|
1138
|
-
function
|
|
1799
|
+
function setJson3(res, statusCode, payload) {
|
|
1139
1800
|
res.statusCode = statusCode;
|
|
1140
1801
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
1141
1802
|
res.end(JSON.stringify(payload));
|
|
1142
1803
|
}
|
|
1143
1804
|
function extractThreadMessageText(threadReadPayload) {
|
|
1144
|
-
const payload =
|
|
1145
|
-
const thread =
|
|
1805
|
+
const payload = asRecord3(threadReadPayload);
|
|
1806
|
+
const thread = asRecord3(payload?.thread);
|
|
1146
1807
|
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
1147
1808
|
const parts = [];
|
|
1148
1809
|
for (const turn of turns) {
|
|
1149
|
-
const turnRecord =
|
|
1810
|
+
const turnRecord = asRecord3(turn);
|
|
1150
1811
|
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
1151
1812
|
for (const item of items) {
|
|
1152
|
-
const itemRecord =
|
|
1813
|
+
const itemRecord = asRecord3(item);
|
|
1153
1814
|
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
1154
1815
|
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
1155
1816
|
parts.push(itemRecord.text.trim());
|
|
@@ -1158,7 +1819,7 @@ function extractThreadMessageText(threadReadPayload) {
|
|
|
1158
1819
|
if (type === "userMessage") {
|
|
1159
1820
|
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
1160
1821
|
for (const block of content) {
|
|
1161
|
-
const blockRecord =
|
|
1822
|
+
const blockRecord = asRecord3(block);
|
|
1162
1823
|
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
1163
1824
|
parts.push(blockRecord.text.trim());
|
|
1164
1825
|
}
|
|
@@ -1194,7 +1855,7 @@ function scoreFileCandidate(path, query) {
|
|
|
1194
1855
|
}
|
|
1195
1856
|
async function listFilesWithRipgrep(cwd) {
|
|
1196
1857
|
return await new Promise((resolve2, reject) => {
|
|
1197
|
-
const proc =
|
|
1858
|
+
const proc = spawn3("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
|
|
1198
1859
|
cwd,
|
|
1199
1860
|
env: process.env,
|
|
1200
1861
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1219,13 +1880,13 @@ async function listFilesWithRipgrep(cwd) {
|
|
|
1219
1880
|
});
|
|
1220
1881
|
});
|
|
1221
1882
|
}
|
|
1222
|
-
function
|
|
1883
|
+
function getCodexHomeDir3() {
|
|
1223
1884
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
1224
|
-
return codexHome && codexHome.length > 0 ? codexHome :
|
|
1885
|
+
return codexHome && codexHome.length > 0 ? codexHome : join3(homedir3(), ".codex");
|
|
1225
1886
|
}
|
|
1226
1887
|
async function runCommand2(command, args, options = {}) {
|
|
1227
1888
|
await new Promise((resolve2, reject) => {
|
|
1228
|
-
const proc =
|
|
1889
|
+
const proc = spawn3(command, args, {
|
|
1229
1890
|
cwd: options.cwd,
|
|
1230
1891
|
env: process.env,
|
|
1231
1892
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1251,19 +1912,19 @@ async function runCommand2(command, args, options = {}) {
|
|
|
1251
1912
|
});
|
|
1252
1913
|
}
|
|
1253
1914
|
function isMissingHeadError(error) {
|
|
1254
|
-
const message =
|
|
1915
|
+
const message = getErrorMessage3(error, "").toLowerCase();
|
|
1255
1916
|
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
|
|
1256
1917
|
}
|
|
1257
1918
|
function isNotGitRepositoryError(error) {
|
|
1258
|
-
const message =
|
|
1919
|
+
const message = getErrorMessage3(error, "").toLowerCase();
|
|
1259
1920
|
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
1260
1921
|
}
|
|
1261
1922
|
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
1262
|
-
const agentsPath =
|
|
1923
|
+
const agentsPath = join3(repoRoot, "AGENTS.md");
|
|
1263
1924
|
try {
|
|
1264
|
-
await
|
|
1925
|
+
await stat3(agentsPath);
|
|
1265
1926
|
} catch {
|
|
1266
|
-
await
|
|
1927
|
+
await writeFile3(agentsPath, "", "utf8");
|
|
1267
1928
|
}
|
|
1268
1929
|
await runCommand2("git", ["add", "AGENTS.md"], { cwd: repoRoot });
|
|
1269
1930
|
await runCommand2(
|
|
@@ -1274,7 +1935,7 @@ async function ensureRepoHasInitialCommit(repoRoot) {
|
|
|
1274
1935
|
}
|
|
1275
1936
|
async function runCommandCapture(command, args, options = {}) {
|
|
1276
1937
|
return await new Promise((resolve2, reject) => {
|
|
1277
|
-
const proc =
|
|
1938
|
+
const proc = spawn3(command, args, {
|
|
1278
1939
|
cwd: options.cwd,
|
|
1279
1940
|
env: process.env,
|
|
1280
1941
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1320,11 +1981,11 @@ function normalizeStringRecord(value) {
|
|
|
1320
1981
|
return next;
|
|
1321
1982
|
}
|
|
1322
1983
|
function getCodexAuthPath() {
|
|
1323
|
-
return
|
|
1984
|
+
return join3(getCodexHomeDir3(), "auth.json");
|
|
1324
1985
|
}
|
|
1325
1986
|
async function readCodexAuth() {
|
|
1326
1987
|
try {
|
|
1327
|
-
const raw = await
|
|
1988
|
+
const raw = await readFile3(getCodexAuthPath(), "utf8");
|
|
1328
1989
|
const auth = JSON.parse(raw);
|
|
1329
1990
|
const token = auth.tokens?.access_token;
|
|
1330
1991
|
if (!token) return null;
|
|
@@ -1334,13 +1995,13 @@ async function readCodexAuth() {
|
|
|
1334
1995
|
}
|
|
1335
1996
|
}
|
|
1336
1997
|
function getCodexGlobalStatePath() {
|
|
1337
|
-
return
|
|
1998
|
+
return join3(getCodexHomeDir3(), ".codex-global-state.json");
|
|
1338
1999
|
}
|
|
1339
2000
|
var MAX_THREAD_TITLES = 500;
|
|
1340
2001
|
function normalizeThreadTitleCache(value) {
|
|
1341
|
-
const record =
|
|
2002
|
+
const record = asRecord3(value);
|
|
1342
2003
|
if (!record) return { titles: {}, order: [] };
|
|
1343
|
-
const rawTitles =
|
|
2004
|
+
const rawTitles = asRecord3(record.titles);
|
|
1344
2005
|
const titles = {};
|
|
1345
2006
|
if (rawTitles) {
|
|
1346
2007
|
for (const [k, v] of Object.entries(rawTitles)) {
|
|
@@ -1366,8 +2027,8 @@ function removeFromThreadTitleCache(cache, id) {
|
|
|
1366
2027
|
async function readThreadTitleCache() {
|
|
1367
2028
|
const statePath = getCodexGlobalStatePath();
|
|
1368
2029
|
try {
|
|
1369
|
-
const raw = await
|
|
1370
|
-
const payload =
|
|
2030
|
+
const raw = await readFile3(statePath, "utf8");
|
|
2031
|
+
const payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
1371
2032
|
return normalizeThreadTitleCache(payload["thread-titles"]);
|
|
1372
2033
|
} catch {
|
|
1373
2034
|
return { titles: {}, order: [] };
|
|
@@ -1377,21 +2038,21 @@ async function writeThreadTitleCache(cache) {
|
|
|
1377
2038
|
const statePath = getCodexGlobalStatePath();
|
|
1378
2039
|
let payload = {};
|
|
1379
2040
|
try {
|
|
1380
|
-
const raw = await
|
|
1381
|
-
payload =
|
|
2041
|
+
const raw = await readFile3(statePath, "utf8");
|
|
2042
|
+
payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
1382
2043
|
} catch {
|
|
1383
2044
|
payload = {};
|
|
1384
2045
|
}
|
|
1385
2046
|
payload["thread-titles"] = cache;
|
|
1386
|
-
await
|
|
2047
|
+
await writeFile3(statePath, JSON.stringify(payload), "utf8");
|
|
1387
2048
|
}
|
|
1388
2049
|
async function readWorkspaceRootsState() {
|
|
1389
2050
|
const statePath = getCodexGlobalStatePath();
|
|
1390
2051
|
let payload = {};
|
|
1391
2052
|
try {
|
|
1392
|
-
const raw = await
|
|
2053
|
+
const raw = await readFile3(statePath, "utf8");
|
|
1393
2054
|
const parsed = JSON.parse(raw);
|
|
1394
|
-
payload =
|
|
2055
|
+
payload = asRecord3(parsed) ?? {};
|
|
1395
2056
|
} catch {
|
|
1396
2057
|
payload = {};
|
|
1397
2058
|
}
|
|
@@ -1405,15 +2066,15 @@ async function writeWorkspaceRootsState(nextState) {
|
|
|
1405
2066
|
const statePath = getCodexGlobalStatePath();
|
|
1406
2067
|
let payload = {};
|
|
1407
2068
|
try {
|
|
1408
|
-
const raw = await
|
|
1409
|
-
payload =
|
|
2069
|
+
const raw = await readFile3(statePath, "utf8");
|
|
2070
|
+
payload = asRecord3(JSON.parse(raw)) ?? {};
|
|
1410
2071
|
} catch {
|
|
1411
2072
|
payload = {};
|
|
1412
2073
|
}
|
|
1413
2074
|
payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
|
|
1414
2075
|
payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
|
|
1415
2076
|
payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
|
|
1416
|
-
await
|
|
2077
|
+
await writeFile3(statePath, JSON.stringify(payload), "utf8");
|
|
1417
2078
|
}
|
|
1418
2079
|
async function readJsonBody(req) {
|
|
1419
2080
|
const raw = await readRawBody(req);
|
|
@@ -1451,7 +2112,7 @@ function handleFileUpload(req, res) {
|
|
|
1451
2112
|
const contentType = req.headers["content-type"] ?? "";
|
|
1452
2113
|
const boundaryMatch = contentType.match(/boundary=(.+)/i);
|
|
1453
2114
|
if (!boundaryMatch) {
|
|
1454
|
-
|
|
2115
|
+
setJson3(res, 400, { error: "Missing multipart boundary" });
|
|
1455
2116
|
return;
|
|
1456
2117
|
}
|
|
1457
2118
|
const boundary = boundaryMatch[1];
|
|
@@ -1481,21 +2142,21 @@ function handleFileUpload(req, res) {
|
|
|
1481
2142
|
break;
|
|
1482
2143
|
}
|
|
1483
2144
|
if (!fileData) {
|
|
1484
|
-
|
|
2145
|
+
setJson3(res, 400, { error: "No file in request" });
|
|
1485
2146
|
return;
|
|
1486
2147
|
}
|
|
1487
|
-
const uploadDir =
|
|
1488
|
-
await
|
|
1489
|
-
const destDir = await
|
|
1490
|
-
const destPath =
|
|
1491
|
-
await
|
|
1492
|
-
|
|
2148
|
+
const uploadDir = join3(tmpdir3(), "codex-web-uploads");
|
|
2149
|
+
await mkdir3(uploadDir, { recursive: true });
|
|
2150
|
+
const destDir = await mkdtemp3(join3(uploadDir, "f-"));
|
|
2151
|
+
const destPath = join3(destDir, fileName);
|
|
2152
|
+
await writeFile3(destPath, fileData);
|
|
2153
|
+
setJson3(res, 200, { path: destPath });
|
|
1493
2154
|
} catch (err) {
|
|
1494
|
-
|
|
2155
|
+
setJson3(res, 500, { error: getErrorMessage3(err, "Upload failed") });
|
|
1495
2156
|
}
|
|
1496
2157
|
});
|
|
1497
2158
|
req.on("error", (err) => {
|
|
1498
|
-
|
|
2159
|
+
setJson3(res, 500, { error: getErrorMessage3(err, "Upload stream error") });
|
|
1499
2160
|
});
|
|
1500
2161
|
}
|
|
1501
2162
|
async function proxyTranscribe(body, contentType, authToken, accountId) {
|
|
@@ -1547,7 +2208,7 @@ var AppServerProcess = class {
|
|
|
1547
2208
|
start() {
|
|
1548
2209
|
if (this.process) return;
|
|
1549
2210
|
this.stopping = false;
|
|
1550
|
-
const proc =
|
|
2211
|
+
const proc = spawn3("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
1551
2212
|
this.process = proc;
|
|
1552
2213
|
proc.stdout.setEncoding("utf8");
|
|
1553
2214
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -1566,6 +2227,9 @@ var AppServerProcess = class {
|
|
|
1566
2227
|
proc.stderr.on("data", () => {
|
|
1567
2228
|
});
|
|
1568
2229
|
proc.on("exit", () => {
|
|
2230
|
+
if (this.process !== proc) {
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
1569
2233
|
const failure = new Error(this.stopping ? "codex app-server stopped" : "codex app-server exited unexpectedly");
|
|
1570
2234
|
for (const request of this.pending.values()) {
|
|
1571
2235
|
request.reject(failure);
|
|
@@ -1641,7 +2305,7 @@ var AppServerProcess = class {
|
|
|
1641
2305
|
}
|
|
1642
2306
|
this.pendingServerRequests.delete(requestId);
|
|
1643
2307
|
this.sendServerRequestReply(requestId, reply);
|
|
1644
|
-
const requestParams =
|
|
2308
|
+
const requestParams = asRecord3(pendingRequest.params);
|
|
1645
2309
|
const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
|
|
1646
2310
|
this.emitNotification({
|
|
1647
2311
|
method: "server/request/resolved",
|
|
@@ -1717,7 +2381,7 @@ var AppServerProcess = class {
|
|
|
1717
2381
|
}
|
|
1718
2382
|
async respondToServerRequest(payload) {
|
|
1719
2383
|
await this.ensureInitialized();
|
|
1720
|
-
const body =
|
|
2384
|
+
const body = asRecord3(payload);
|
|
1721
2385
|
if (!body) {
|
|
1722
2386
|
throw new Error("Invalid response payload: expected object");
|
|
1723
2387
|
}
|
|
@@ -1725,7 +2389,7 @@ var AppServerProcess = class {
|
|
|
1725
2389
|
if (typeof id !== "number" || !Number.isInteger(id)) {
|
|
1726
2390
|
throw new Error('Invalid response payload: "id" must be an integer');
|
|
1727
2391
|
}
|
|
1728
|
-
const rawError =
|
|
2392
|
+
const rawError = asRecord3(body.error);
|
|
1729
2393
|
if (rawError) {
|
|
1730
2394
|
const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
|
|
1731
2395
|
const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
|
|
@@ -1780,7 +2444,7 @@ var MethodCatalog = class {
|
|
|
1780
2444
|
}
|
|
1781
2445
|
async runGenerateSchemaCommand(outDir) {
|
|
1782
2446
|
await new Promise((resolve2, reject) => {
|
|
1783
|
-
const process2 =
|
|
2447
|
+
const process2 = spawn3("codex", ["app-server", "generate-json-schema", "--out", outDir], {
|
|
1784
2448
|
stdio: ["ignore", "ignore", "pipe"]
|
|
1785
2449
|
});
|
|
1786
2450
|
let stderr = "";
|
|
@@ -1799,13 +2463,13 @@ var MethodCatalog = class {
|
|
|
1799
2463
|
});
|
|
1800
2464
|
}
|
|
1801
2465
|
extractMethodsFromClientRequest(payload) {
|
|
1802
|
-
const root =
|
|
2466
|
+
const root = asRecord3(payload);
|
|
1803
2467
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
1804
2468
|
const methods = /* @__PURE__ */ new Set();
|
|
1805
2469
|
for (const entry of oneOf) {
|
|
1806
|
-
const row =
|
|
1807
|
-
const properties =
|
|
1808
|
-
const methodDef =
|
|
2470
|
+
const row = asRecord3(entry);
|
|
2471
|
+
const properties = asRecord3(row?.properties);
|
|
2472
|
+
const methodDef = asRecord3(properties?.method);
|
|
1809
2473
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
1810
2474
|
for (const item of methodEnum) {
|
|
1811
2475
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -1816,13 +2480,13 @@ var MethodCatalog = class {
|
|
|
1816
2480
|
return Array.from(methods).sort((a, b) => a.localeCompare(b));
|
|
1817
2481
|
}
|
|
1818
2482
|
extractMethodsFromServerNotification(payload) {
|
|
1819
|
-
const root =
|
|
2483
|
+
const root = asRecord3(payload);
|
|
1820
2484
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
1821
2485
|
const methods = /* @__PURE__ */ new Set();
|
|
1822
2486
|
for (const entry of oneOf) {
|
|
1823
|
-
const row =
|
|
1824
|
-
const properties =
|
|
1825
|
-
const methodDef =
|
|
2487
|
+
const row = asRecord3(entry);
|
|
2488
|
+
const properties = asRecord3(row?.properties);
|
|
2489
|
+
const methodDef = asRecord3(properties?.method);
|
|
1826
2490
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
1827
2491
|
for (const item of methodEnum) {
|
|
1828
2492
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -1836,10 +2500,10 @@ var MethodCatalog = class {
|
|
|
1836
2500
|
if (this.methodCache) {
|
|
1837
2501
|
return this.methodCache;
|
|
1838
2502
|
}
|
|
1839
|
-
const outDir = await
|
|
2503
|
+
const outDir = await mkdtemp3(join3(tmpdir3(), "codex-web-local-schema-"));
|
|
1840
2504
|
await this.runGenerateSchemaCommand(outDir);
|
|
1841
|
-
const clientRequestPath =
|
|
1842
|
-
const raw = await
|
|
2505
|
+
const clientRequestPath = join3(outDir, "ClientRequest.json");
|
|
2506
|
+
const raw = await readFile3(clientRequestPath, "utf8");
|
|
1843
2507
|
const parsed = JSON.parse(raw);
|
|
1844
2508
|
const methods = this.extractMethodsFromClientRequest(parsed);
|
|
1845
2509
|
this.methodCache = methods;
|
|
@@ -1849,10 +2513,10 @@ var MethodCatalog = class {
|
|
|
1849
2513
|
if (this.notificationCache) {
|
|
1850
2514
|
return this.notificationCache;
|
|
1851
2515
|
}
|
|
1852
|
-
const outDir = await
|
|
2516
|
+
const outDir = await mkdtemp3(join3(tmpdir3(), "codex-web-local-schema-"));
|
|
1853
2517
|
await this.runGenerateSchemaCommand(outDir);
|
|
1854
|
-
const serverNotificationPath =
|
|
1855
|
-
const raw = await
|
|
2518
|
+
const serverNotificationPath = join3(outDir, "ServerNotification.json");
|
|
2519
|
+
const raw = await readFile3(serverNotificationPath, "utf8");
|
|
1856
2520
|
const parsed = JSON.parse(raw);
|
|
1857
2521
|
const methods = this.extractMethodsFromServerNotification(parsed);
|
|
1858
2522
|
this.notificationCache = methods;
|
|
@@ -1882,7 +2546,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
1882
2546
|
const threads = [];
|
|
1883
2547
|
let cursor = null;
|
|
1884
2548
|
do {
|
|
1885
|
-
const response =
|
|
2549
|
+
const response = asRecord3(await appServer.rpc("thread/list", {
|
|
1886
2550
|
archived: false,
|
|
1887
2551
|
limit: 100,
|
|
1888
2552
|
sortKey: "updated_at",
|
|
@@ -1890,7 +2554,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
1890
2554
|
}));
|
|
1891
2555
|
const data = Array.isArray(response?.data) ? response.data : [];
|
|
1892
2556
|
for (const row of data) {
|
|
1893
|
-
const record =
|
|
2557
|
+
const record = asRecord3(row);
|
|
1894
2558
|
const id = typeof record?.id === "string" ? record.id : "";
|
|
1895
2559
|
if (!id) continue;
|
|
1896
2560
|
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 +2626,9 @@ function createCodexBridgeMiddleware() {
|
|
|
1962
2626
|
return;
|
|
1963
2627
|
}
|
|
1964
2628
|
const url = new URL(req.url, "http://localhost");
|
|
2629
|
+
if (await handleAccountRoutes(req, res, url, { appServer })) {
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
1965
2632
|
if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
|
|
1966
2633
|
return;
|
|
1967
2634
|
}
|
|
@@ -1971,19 +2638,19 @@ function createCodexBridgeMiddleware() {
|
|
|
1971
2638
|
}
|
|
1972
2639
|
if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
|
|
1973
2640
|
const payload = await readJsonBody(req);
|
|
1974
|
-
const body =
|
|
2641
|
+
const body = asRecord3(payload);
|
|
1975
2642
|
if (!body || typeof body.method !== "string" || body.method.length === 0) {
|
|
1976
|
-
|
|
2643
|
+
setJson3(res, 400, { error: "Invalid body: expected { method, params? }" });
|
|
1977
2644
|
return;
|
|
1978
2645
|
}
|
|
1979
2646
|
const result = await appServer.rpc(body.method, body.params ?? null);
|
|
1980
|
-
|
|
2647
|
+
setJson3(res, 200, { result });
|
|
1981
2648
|
return;
|
|
1982
2649
|
}
|
|
1983
2650
|
if (req.method === "POST" && url.pathname === "/codex-api/transcribe") {
|
|
1984
2651
|
const auth = await readCodexAuth();
|
|
1985
2652
|
if (!auth) {
|
|
1986
|
-
|
|
2653
|
+
setJson3(res, 401, { error: "No auth token available for transcription" });
|
|
1987
2654
|
return;
|
|
1988
2655
|
}
|
|
1989
2656
|
const rawBody = await readRawBody(req);
|
|
@@ -1997,48 +2664,48 @@ function createCodexBridgeMiddleware() {
|
|
|
1997
2664
|
if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
|
|
1998
2665
|
const payload = await readJsonBody(req);
|
|
1999
2666
|
await appServer.respondToServerRequest(payload);
|
|
2000
|
-
|
|
2667
|
+
setJson3(res, 200, { ok: true });
|
|
2001
2668
|
return;
|
|
2002
2669
|
}
|
|
2003
2670
|
if (req.method === "GET" && url.pathname === "/codex-api/server-requests/pending") {
|
|
2004
|
-
|
|
2671
|
+
setJson3(res, 200, { data: appServer.listPendingServerRequests() });
|
|
2005
2672
|
return;
|
|
2006
2673
|
}
|
|
2007
2674
|
if (req.method === "GET" && url.pathname === "/codex-api/meta/methods") {
|
|
2008
2675
|
const methods = await methodCatalog.listMethods();
|
|
2009
|
-
|
|
2676
|
+
setJson3(res, 200, { data: methods });
|
|
2010
2677
|
return;
|
|
2011
2678
|
}
|
|
2012
2679
|
if (req.method === "GET" && url.pathname === "/codex-api/meta/notifications") {
|
|
2013
2680
|
const methods = await methodCatalog.listNotificationMethods();
|
|
2014
|
-
|
|
2681
|
+
setJson3(res, 200, { data: methods });
|
|
2015
2682
|
return;
|
|
2016
2683
|
}
|
|
2017
2684
|
if (req.method === "GET" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
2018
2685
|
const state = await readWorkspaceRootsState();
|
|
2019
|
-
|
|
2686
|
+
setJson3(res, 200, { data: state });
|
|
2020
2687
|
return;
|
|
2021
2688
|
}
|
|
2022
2689
|
if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
|
|
2023
|
-
|
|
2690
|
+
setJson3(res, 200, { data: { path: homedir3() } });
|
|
2024
2691
|
return;
|
|
2025
2692
|
}
|
|
2026
2693
|
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
2027
|
-
const payload =
|
|
2694
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2028
2695
|
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
2029
2696
|
if (!rawSourceCwd) {
|
|
2030
|
-
|
|
2697
|
+
setJson3(res, 400, { error: "Missing sourceCwd" });
|
|
2031
2698
|
return;
|
|
2032
2699
|
}
|
|
2033
2700
|
const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
|
|
2034
2701
|
try {
|
|
2035
|
-
const sourceInfo = await
|
|
2702
|
+
const sourceInfo = await stat3(sourceCwd);
|
|
2036
2703
|
if (!sourceInfo.isDirectory()) {
|
|
2037
|
-
|
|
2704
|
+
setJson3(res, 400, { error: "sourceCwd is not a directory" });
|
|
2038
2705
|
return;
|
|
2039
2706
|
}
|
|
2040
2707
|
} catch {
|
|
2041
|
-
|
|
2708
|
+
setJson3(res, 404, { error: "sourceCwd does not exist" });
|
|
2042
2709
|
return;
|
|
2043
2710
|
}
|
|
2044
2711
|
try {
|
|
@@ -2051,21 +2718,21 @@ function createCodexBridgeMiddleware() {
|
|
|
2051
2718
|
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
2052
2719
|
}
|
|
2053
2720
|
const repoName = basename(gitRoot) || "repo";
|
|
2054
|
-
const worktreesRoot =
|
|
2055
|
-
await
|
|
2721
|
+
const worktreesRoot = join3(getCodexHomeDir3(), "worktrees");
|
|
2722
|
+
await mkdir3(worktreesRoot, { recursive: true });
|
|
2056
2723
|
let worktreeId = "";
|
|
2057
2724
|
let worktreeParent = "";
|
|
2058
2725
|
let worktreeCwd = "";
|
|
2059
2726
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
2060
2727
|
const candidate = randomBytes(2).toString("hex");
|
|
2061
|
-
const parent =
|
|
2728
|
+
const parent = join3(worktreesRoot, candidate);
|
|
2062
2729
|
try {
|
|
2063
|
-
await
|
|
2730
|
+
await stat3(parent);
|
|
2064
2731
|
continue;
|
|
2065
2732
|
} catch {
|
|
2066
2733
|
worktreeId = candidate;
|
|
2067
2734
|
worktreeParent = parent;
|
|
2068
|
-
worktreeCwd =
|
|
2735
|
+
worktreeCwd = join3(parent, repoName);
|
|
2069
2736
|
break;
|
|
2070
2737
|
}
|
|
2071
2738
|
}
|
|
@@ -2073,7 +2740,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2073
2740
|
throw new Error("Failed to allocate a unique worktree id");
|
|
2074
2741
|
}
|
|
2075
2742
|
const branch = `codex/${worktreeId}`;
|
|
2076
|
-
await
|
|
2743
|
+
await mkdir3(worktreeParent, { recursive: true });
|
|
2077
2744
|
try {
|
|
2078
2745
|
await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
2079
2746
|
} catch (error) {
|
|
@@ -2081,7 +2748,7 @@ function createCodexBridgeMiddleware() {
|
|
|
2081
2748
|
await ensureRepoHasInitialCommit(gitRoot);
|
|
2082
2749
|
await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
2083
2750
|
}
|
|
2084
|
-
|
|
2751
|
+
setJson3(res, 200, {
|
|
2085
2752
|
data: {
|
|
2086
2753
|
cwd: worktreeCwd,
|
|
2087
2754
|
branch,
|
|
@@ -2089,15 +2756,15 @@ function createCodexBridgeMiddleware() {
|
|
|
2089
2756
|
}
|
|
2090
2757
|
});
|
|
2091
2758
|
} catch (error) {
|
|
2092
|
-
|
|
2759
|
+
setJson3(res, 500, { error: getErrorMessage3(error, "Failed to create worktree") });
|
|
2093
2760
|
}
|
|
2094
2761
|
return;
|
|
2095
2762
|
}
|
|
2096
2763
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
2097
2764
|
const payload = await readJsonBody(req);
|
|
2098
|
-
const record =
|
|
2765
|
+
const record = asRecord3(payload);
|
|
2099
2766
|
if (!record) {
|
|
2100
|
-
|
|
2767
|
+
setJson3(res, 400, { error: "Invalid body: expected object" });
|
|
2101
2768
|
return;
|
|
2102
2769
|
}
|
|
2103
2770
|
const nextState = {
|
|
@@ -2106,33 +2773,33 @@ function createCodexBridgeMiddleware() {
|
|
|
2106
2773
|
active: normalizeStringArray(record.active)
|
|
2107
2774
|
};
|
|
2108
2775
|
await writeWorkspaceRootsState(nextState);
|
|
2109
|
-
|
|
2776
|
+
setJson3(res, 200, { ok: true });
|
|
2110
2777
|
return;
|
|
2111
2778
|
}
|
|
2112
2779
|
if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
|
|
2113
|
-
const payload =
|
|
2780
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2114
2781
|
const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
|
2115
2782
|
const createIfMissing = payload?.createIfMissing === true;
|
|
2116
2783
|
const label = typeof payload?.label === "string" ? payload.label : "";
|
|
2117
2784
|
if (!rawPath) {
|
|
2118
|
-
|
|
2785
|
+
setJson3(res, 400, { error: "Missing path" });
|
|
2119
2786
|
return;
|
|
2120
2787
|
}
|
|
2121
2788
|
const normalizedPath = isAbsolute(rawPath) ? rawPath : resolve(rawPath);
|
|
2122
2789
|
let pathExists = true;
|
|
2123
2790
|
try {
|
|
2124
|
-
const info = await
|
|
2791
|
+
const info = await stat3(normalizedPath);
|
|
2125
2792
|
if (!info.isDirectory()) {
|
|
2126
|
-
|
|
2793
|
+
setJson3(res, 400, { error: "Path exists but is not a directory" });
|
|
2127
2794
|
return;
|
|
2128
2795
|
}
|
|
2129
2796
|
} catch {
|
|
2130
2797
|
pathExists = false;
|
|
2131
2798
|
}
|
|
2132
2799
|
if (!pathExists && createIfMissing) {
|
|
2133
|
-
await
|
|
2800
|
+
await mkdir3(normalizedPath, { recursive: true });
|
|
2134
2801
|
} else if (!pathExists) {
|
|
2135
|
-
|
|
2802
|
+
setJson3(res, 404, { error: "Directory does not exist" });
|
|
2136
2803
|
return;
|
|
2137
2804
|
}
|
|
2138
2805
|
const existingState = await readWorkspaceRootsState();
|
|
@@ -2147,103 +2814,103 @@ function createCodexBridgeMiddleware() {
|
|
|
2147
2814
|
labels: nextLabels,
|
|
2148
2815
|
active: nextActive
|
|
2149
2816
|
});
|
|
2150
|
-
|
|
2817
|
+
setJson3(res, 200, { data: { path: normalizedPath } });
|
|
2151
2818
|
return;
|
|
2152
2819
|
}
|
|
2153
2820
|
if (req.method === "GET" && url.pathname === "/codex-api/project-root-suggestion") {
|
|
2154
2821
|
const basePath = url.searchParams.get("basePath")?.trim() ?? "";
|
|
2155
2822
|
if (!basePath) {
|
|
2156
|
-
|
|
2823
|
+
setJson3(res, 400, { error: "Missing basePath" });
|
|
2157
2824
|
return;
|
|
2158
2825
|
}
|
|
2159
2826
|
const normalizedBasePath = isAbsolute(basePath) ? basePath : resolve(basePath);
|
|
2160
2827
|
try {
|
|
2161
|
-
const baseInfo = await
|
|
2828
|
+
const baseInfo = await stat3(normalizedBasePath);
|
|
2162
2829
|
if (!baseInfo.isDirectory()) {
|
|
2163
|
-
|
|
2830
|
+
setJson3(res, 400, { error: "basePath is not a directory" });
|
|
2164
2831
|
return;
|
|
2165
2832
|
}
|
|
2166
2833
|
} catch {
|
|
2167
|
-
|
|
2834
|
+
setJson3(res, 404, { error: "basePath does not exist" });
|
|
2168
2835
|
return;
|
|
2169
2836
|
}
|
|
2170
2837
|
let index = 1;
|
|
2171
2838
|
while (index < 1e5) {
|
|
2172
2839
|
const candidateName = `New Project (${String(index)})`;
|
|
2173
|
-
const candidatePath =
|
|
2840
|
+
const candidatePath = join3(normalizedBasePath, candidateName);
|
|
2174
2841
|
try {
|
|
2175
|
-
await
|
|
2842
|
+
await stat3(candidatePath);
|
|
2176
2843
|
index += 1;
|
|
2177
2844
|
continue;
|
|
2178
2845
|
} catch {
|
|
2179
|
-
|
|
2846
|
+
setJson3(res, 200, { data: { name: candidateName, path: candidatePath } });
|
|
2180
2847
|
return;
|
|
2181
2848
|
}
|
|
2182
2849
|
}
|
|
2183
|
-
|
|
2850
|
+
setJson3(res, 500, { error: "Failed to compute project name suggestion" });
|
|
2184
2851
|
return;
|
|
2185
2852
|
}
|
|
2186
2853
|
if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
|
|
2187
|
-
const payload =
|
|
2854
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2188
2855
|
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
2189
2856
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
2190
2857
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
|
|
2191
2858
|
const limit = Math.max(1, Math.min(100, Math.floor(limitRaw)));
|
|
2192
2859
|
if (!rawCwd) {
|
|
2193
|
-
|
|
2860
|
+
setJson3(res, 400, { error: "Missing cwd" });
|
|
2194
2861
|
return;
|
|
2195
2862
|
}
|
|
2196
2863
|
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
2197
2864
|
try {
|
|
2198
|
-
const info = await
|
|
2865
|
+
const info = await stat3(cwd);
|
|
2199
2866
|
if (!info.isDirectory()) {
|
|
2200
|
-
|
|
2867
|
+
setJson3(res, 400, { error: "cwd is not a directory" });
|
|
2201
2868
|
return;
|
|
2202
2869
|
}
|
|
2203
2870
|
} catch {
|
|
2204
|
-
|
|
2871
|
+
setJson3(res, 404, { error: "cwd does not exist" });
|
|
2205
2872
|
return;
|
|
2206
2873
|
}
|
|
2207
2874
|
try {
|
|
2208
2875
|
const files = await listFilesWithRipgrep(cwd);
|
|
2209
2876
|
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
|
-
|
|
2877
|
+
setJson3(res, 200, { data: scored });
|
|
2211
2878
|
} catch (error) {
|
|
2212
|
-
|
|
2879
|
+
setJson3(res, 500, { error: getErrorMessage3(error, "Failed to search files") });
|
|
2213
2880
|
}
|
|
2214
2881
|
return;
|
|
2215
2882
|
}
|
|
2216
2883
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
|
|
2217
2884
|
const cache = await readThreadTitleCache();
|
|
2218
|
-
|
|
2885
|
+
setJson3(res, 200, { data: cache });
|
|
2219
2886
|
return;
|
|
2220
2887
|
}
|
|
2221
2888
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
2222
|
-
const payload =
|
|
2889
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2223
2890
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
2224
2891
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
2225
2892
|
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
2226
2893
|
if (!query) {
|
|
2227
|
-
|
|
2894
|
+
setJson3(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
|
|
2228
2895
|
return;
|
|
2229
2896
|
}
|
|
2230
2897
|
const index = await getThreadSearchIndex();
|
|
2231
2898
|
const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
|
|
2232
|
-
|
|
2899
|
+
setJson3(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
|
|
2233
2900
|
return;
|
|
2234
2901
|
}
|
|
2235
2902
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
2236
|
-
const payload =
|
|
2903
|
+
const payload = asRecord3(await readJsonBody(req));
|
|
2237
2904
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
2238
2905
|
const title = typeof payload?.title === "string" ? payload.title : "";
|
|
2239
2906
|
if (!id) {
|
|
2240
|
-
|
|
2907
|
+
setJson3(res, 400, { error: "Missing id" });
|
|
2241
2908
|
return;
|
|
2242
2909
|
}
|
|
2243
2910
|
const cache = await readThreadTitleCache();
|
|
2244
2911
|
const next2 = title ? updateThreadTitleCache(cache, id, title) : removeFromThreadTitleCache(cache, id);
|
|
2245
2912
|
await writeThreadTitleCache(next2);
|
|
2246
|
-
|
|
2913
|
+
setJson3(res, 200, { ok: true });
|
|
2247
2914
|
return;
|
|
2248
2915
|
}
|
|
2249
2916
|
if (req.method === "GET" && url.pathname === "/codex-api/events") {
|
|
@@ -2278,8 +2945,8 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
2278
2945
|
}
|
|
2279
2946
|
next();
|
|
2280
2947
|
} catch (error) {
|
|
2281
|
-
const message =
|
|
2282
|
-
|
|
2948
|
+
const message = getErrorMessage3(error, "Unknown bridge error");
|
|
2949
|
+
setJson3(res, 502, { error: message });
|
|
2283
2950
|
}
|
|
2284
2951
|
};
|
|
2285
2952
|
middleware.dispose = () => {
|
|
@@ -2416,8 +3083,8 @@ function createAuthSession(password) {
|
|
|
2416
3083
|
}
|
|
2417
3084
|
|
|
2418
3085
|
// src/server/localBrowseUi.ts
|
|
2419
|
-
import { dirname, extname, join as
|
|
2420
|
-
import { open, readFile as
|
|
3086
|
+
import { dirname, extname, join as join4 } from "path";
|
|
3087
|
+
import { open, readFile as readFile4, readdir as readdir3, stat as stat4 } from "fs/promises";
|
|
2421
3088
|
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2422
3089
|
".txt",
|
|
2423
3090
|
".md",
|
|
@@ -2532,7 +3199,7 @@ async function probeFileIsText(localPath) {
|
|
|
2532
3199
|
async function isTextEditableFile(localPath) {
|
|
2533
3200
|
if (isTextEditablePath(localPath)) return true;
|
|
2534
3201
|
try {
|
|
2535
|
-
const fileStat = await
|
|
3202
|
+
const fileStat = await stat4(localPath);
|
|
2536
3203
|
if (!fileStat.isFile()) return false;
|
|
2537
3204
|
return await probeFileIsText(localPath);
|
|
2538
3205
|
} catch {
|
|
@@ -2554,8 +3221,8 @@ function escapeForInlineScriptString(value) {
|
|
|
2554
3221
|
async function getDirectoryItems(localPath) {
|
|
2555
3222
|
const entries = await readdir3(localPath, { withFileTypes: true });
|
|
2556
3223
|
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
2557
|
-
const entryPath =
|
|
2558
|
-
const entryStat = await
|
|
3224
|
+
const entryPath = join4(localPath, entry.name);
|
|
3225
|
+
const entryStat = await stat4(entryPath);
|
|
2559
3226
|
const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
|
|
2560
3227
|
return {
|
|
2561
3228
|
name: entry.name,
|
|
@@ -2613,7 +3280,7 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
2613
3280
|
</html>`;
|
|
2614
3281
|
}
|
|
2615
3282
|
async function createTextEditorHtml(localPath) {
|
|
2616
|
-
const content = await
|
|
3283
|
+
const content = await readFile4(localPath, "utf8");
|
|
2617
3284
|
const parentPath = dirname(localPath);
|
|
2618
3285
|
const language = languageForPath(localPath);
|
|
2619
3286
|
const safeContentLiteral = escapeForInlineScriptString(content);
|
|
@@ -2684,8 +3351,8 @@ async function createTextEditorHtml(localPath) {
|
|
|
2684
3351
|
// src/server/httpServer.ts
|
|
2685
3352
|
import { WebSocketServer } from "ws";
|
|
2686
3353
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
2687
|
-
var distDir =
|
|
2688
|
-
var spaEntryFile =
|
|
3354
|
+
var distDir = join5(__dirname, "..", "dist");
|
|
3355
|
+
var spaEntryFile = join5(distDir, "index.html");
|
|
2689
3356
|
var IMAGE_CONTENT_TYPES = {
|
|
2690
3357
|
".avif": "image/avif",
|
|
2691
3358
|
".bmp": "image/bmp",
|
|
@@ -2762,7 +3429,7 @@ function createServer(options = {}) {
|
|
|
2762
3429
|
return;
|
|
2763
3430
|
}
|
|
2764
3431
|
try {
|
|
2765
|
-
const fileStat = await
|
|
3432
|
+
const fileStat = await stat5(localPath);
|
|
2766
3433
|
res.setHeader("Cache-Control", "private, no-store");
|
|
2767
3434
|
if (fileStat.isDirectory()) {
|
|
2768
3435
|
const html = await createDirectoryListingHtml(localPath);
|
|
@@ -2785,7 +3452,7 @@ function createServer(options = {}) {
|
|
|
2785
3452
|
return;
|
|
2786
3453
|
}
|
|
2787
3454
|
try {
|
|
2788
|
-
const fileStat = await
|
|
3455
|
+
const fileStat = await stat5(localPath);
|
|
2789
3456
|
if (!fileStat.isFile()) {
|
|
2790
3457
|
res.status(400).json({ error: "Expected file path." });
|
|
2791
3458
|
return;
|
|
@@ -2809,7 +3476,7 @@ function createServer(options = {}) {
|
|
|
2809
3476
|
}
|
|
2810
3477
|
const body = typeof req.body === "string" ? req.body : "";
|
|
2811
3478
|
try {
|
|
2812
|
-
await
|
|
3479
|
+
await writeFile4(localPath, body, "utf8");
|
|
2813
3480
|
res.status(200).json({ ok: true });
|
|
2814
3481
|
} catch {
|
|
2815
3482
|
res.status(404).json({ error: "File not found." });
|
|
@@ -2889,8 +3556,8 @@ var program = new Command().name("codexui").description("Web interface for Codex
|
|
|
2889
3556
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
2890
3557
|
async function readCliVersion() {
|
|
2891
3558
|
try {
|
|
2892
|
-
const packageJsonPath =
|
|
2893
|
-
const raw = await
|
|
3559
|
+
const packageJsonPath = join6(__dirname2, "..", "package.json");
|
|
3560
|
+
const raw = await readFile5(packageJsonPath, "utf8");
|
|
2894
3561
|
const parsed = JSON.parse(raw);
|
|
2895
3562
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
2896
3563
|
} catch {
|
|
@@ -2915,13 +3582,13 @@ function runWithStatus(command, args) {
|
|
|
2915
3582
|
return result.status ?? -1;
|
|
2916
3583
|
}
|
|
2917
3584
|
function getUserNpmPrefix() {
|
|
2918
|
-
return
|
|
3585
|
+
return join6(homedir4(), ".npm-global");
|
|
2919
3586
|
}
|
|
2920
3587
|
function resolveCodexCommand() {
|
|
2921
3588
|
if (canRun("codex", ["--version"])) {
|
|
2922
3589
|
return "codex";
|
|
2923
3590
|
}
|
|
2924
|
-
const userCandidate =
|
|
3591
|
+
const userCandidate = join6(getUserNpmPrefix(), "bin", "codex");
|
|
2925
3592
|
if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
2926
3593
|
return userCandidate;
|
|
2927
3594
|
}
|
|
@@ -2929,7 +3596,7 @@ function resolveCodexCommand() {
|
|
|
2929
3596
|
if (!prefix) {
|
|
2930
3597
|
return null;
|
|
2931
3598
|
}
|
|
2932
|
-
const candidate =
|
|
3599
|
+
const candidate = join6(prefix, "bin", "codex");
|
|
2933
3600
|
if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
|
|
2934
3601
|
return candidate;
|
|
2935
3602
|
}
|
|
@@ -2939,7 +3606,7 @@ function resolveCloudflaredCommand() {
|
|
|
2939
3606
|
if (canRun("cloudflared", ["--version"])) {
|
|
2940
3607
|
return "cloudflared";
|
|
2941
3608
|
}
|
|
2942
|
-
const localCandidate =
|
|
3609
|
+
const localCandidate = join6(homedir4(), ".local", "bin", "cloudflared");
|
|
2943
3610
|
if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
2944
3611
|
return localCandidate;
|
|
2945
3612
|
}
|
|
@@ -2993,9 +3660,9 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
2993
3660
|
if (!mappedArch) {
|
|
2994
3661
|
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
2995
3662
|
}
|
|
2996
|
-
const userBinDir =
|
|
3663
|
+
const userBinDir = join6(homedir4(), ".local", "bin");
|
|
2997
3664
|
mkdirSync(userBinDir, { recursive: true });
|
|
2998
|
-
const destination =
|
|
3665
|
+
const destination = join6(userBinDir, "cloudflared");
|
|
2999
3666
|
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
3000
3667
|
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
3001
3668
|
await downloadFile(downloadUrl, destination);
|
|
@@ -3034,8 +3701,8 @@ async function resolveCloudflaredForTunnel() {
|
|
|
3034
3701
|
return ensureCloudflaredInstalledLinux();
|
|
3035
3702
|
}
|
|
3036
3703
|
function hasCodexAuth() {
|
|
3037
|
-
const codexHome = process.env.CODEX_HOME?.trim() ||
|
|
3038
|
-
return existsSync3(
|
|
3704
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join6(homedir4(), ".codex");
|
|
3705
|
+
return existsSync3(join6(codexHome, "auth.json"));
|
|
3039
3706
|
}
|
|
3040
3707
|
function ensureCodexInstalled() {
|
|
3041
3708
|
let codexCommand = resolveCodexCommand();
|
|
@@ -3053,7 +3720,7 @@ function ensureCodexInstalled() {
|
|
|
3053
3720
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
3054
3721
|
`);
|
|
3055
3722
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
3056
|
-
process.env.PATH = `${
|
|
3723
|
+
process.env.PATH = `${join6(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
3057
3724
|
};
|
|
3058
3725
|
if (isTermuxRuntime()) {
|
|
3059
3726
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -3102,7 +3769,7 @@ function printTermuxKeepAlive(lines) {
|
|
|
3102
3769
|
}
|
|
3103
3770
|
function openBrowser(url) {
|
|
3104
3771
|
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 =
|
|
3772
|
+
const child = spawn4(command.cmd, command.args, { detached: true, stdio: "ignore" });
|
|
3106
3773
|
child.on("error", () => {
|
|
3107
3774
|
});
|
|
3108
3775
|
child.unref();
|
|
@@ -3134,7 +3801,7 @@ function getAccessibleUrls(port) {
|
|
|
3134
3801
|
}
|
|
3135
3802
|
async function startCloudflaredTunnel(command, localPort) {
|
|
3136
3803
|
return new Promise((resolve2, reject) => {
|
|
3137
|
-
const child =
|
|
3804
|
+
const child = spawn4(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
|
|
3138
3805
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3139
3806
|
});
|
|
3140
3807
|
const timeout = setTimeout(() => {
|