codexapp 0.1.4 → 0.1.9
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 +54 -54
- package/dist/assets/index-BIm_B5t3.css +1 -0
- package/dist/assets/index-DrAmX48U.js +48 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +202 -14
- package/dist-cli/index.js.map +1 -1
- package/package.json +5 -4
- package/dist/assets/index-BBAMY_B8.css +0 -1
- package/dist/assets/index-Crq14xIZ.js +0 -38
package/dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Codex Web Local</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DrAmX48U.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BIm_B5t3.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body class="bg-slate-950">
|
|
11
11
|
<div id="app"></div>
|
package/dist-cli/index.js
CHANGED
|
@@ -46,6 +46,45 @@ function setJson(res, statusCode, payload) {
|
|
|
46
46
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
47
47
|
res.end(JSON.stringify(payload));
|
|
48
48
|
}
|
|
49
|
+
function scoreFileCandidate(path, query) {
|
|
50
|
+
if (!query) return 0;
|
|
51
|
+
const lowerPath = path.toLowerCase();
|
|
52
|
+
const lowerQuery = query.toLowerCase();
|
|
53
|
+
const baseName = lowerPath.slice(lowerPath.lastIndexOf("/") + 1);
|
|
54
|
+
if (baseName === lowerQuery) return 0;
|
|
55
|
+
if (baseName.startsWith(lowerQuery)) return 1;
|
|
56
|
+
if (baseName.includes(lowerQuery)) return 2;
|
|
57
|
+
if (lowerPath.includes(`/${lowerQuery}`)) return 3;
|
|
58
|
+
if (lowerPath.includes(lowerQuery)) return 4;
|
|
59
|
+
return 10;
|
|
60
|
+
}
|
|
61
|
+
async function listFilesWithRipgrep(cwd) {
|
|
62
|
+
return await new Promise((resolve2, reject) => {
|
|
63
|
+
const proc = spawn("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
|
|
64
|
+
cwd,
|
|
65
|
+
env: process.env,
|
|
66
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
67
|
+
});
|
|
68
|
+
let stdout = "";
|
|
69
|
+
let stderr = "";
|
|
70
|
+
proc.stdout.on("data", (chunk) => {
|
|
71
|
+
stdout += chunk.toString();
|
|
72
|
+
});
|
|
73
|
+
proc.stderr.on("data", (chunk) => {
|
|
74
|
+
stderr += chunk.toString();
|
|
75
|
+
});
|
|
76
|
+
proc.on("error", reject);
|
|
77
|
+
proc.on("close", (code) => {
|
|
78
|
+
if (code === 0) {
|
|
79
|
+
const rows = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
80
|
+
resolve2(rows);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
84
|
+
reject(new Error(details || "rg --files failed"));
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
49
88
|
function getCodexHomeDir() {
|
|
50
89
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
51
90
|
return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
|
|
@@ -365,6 +404,75 @@ async function readRawBody(req) {
|
|
|
365
404
|
}
|
|
366
405
|
return Buffer.concat(chunks);
|
|
367
406
|
}
|
|
407
|
+
function bufferIndexOf(buf, needle, start = 0) {
|
|
408
|
+
for (let i = start; i <= buf.length - needle.length; i++) {
|
|
409
|
+
let match = true;
|
|
410
|
+
for (let j = 0; j < needle.length; j++) {
|
|
411
|
+
if (buf[i + j] !== needle[j]) {
|
|
412
|
+
match = false;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (match) return i;
|
|
417
|
+
}
|
|
418
|
+
return -1;
|
|
419
|
+
}
|
|
420
|
+
function handleFileUpload(req, res) {
|
|
421
|
+
const chunks = [];
|
|
422
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
423
|
+
req.on("end", async () => {
|
|
424
|
+
try {
|
|
425
|
+
const body = Buffer.concat(chunks);
|
|
426
|
+
const contentType = req.headers["content-type"] ?? "";
|
|
427
|
+
const boundaryMatch = contentType.match(/boundary=(.+)/i);
|
|
428
|
+
if (!boundaryMatch) {
|
|
429
|
+
setJson(res, 400, { error: "Missing multipart boundary" });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const boundary = boundaryMatch[1];
|
|
433
|
+
const boundaryBuf = Buffer.from(`--${boundary}`);
|
|
434
|
+
const parts = [];
|
|
435
|
+
let searchStart = 0;
|
|
436
|
+
while (searchStart < body.length) {
|
|
437
|
+
const idx = body.indexOf(boundaryBuf, searchStart);
|
|
438
|
+
if (idx < 0) break;
|
|
439
|
+
if (searchStart > 0) parts.push(body.subarray(searchStart, idx));
|
|
440
|
+
searchStart = idx + boundaryBuf.length;
|
|
441
|
+
if (body[searchStart] === 13 && body[searchStart + 1] === 10) searchStart += 2;
|
|
442
|
+
}
|
|
443
|
+
let fileName = "uploaded-file";
|
|
444
|
+
let fileData = null;
|
|
445
|
+
const headerSep = Buffer.from("\r\n\r\n");
|
|
446
|
+
for (const part of parts) {
|
|
447
|
+
const headerEnd = bufferIndexOf(part, headerSep);
|
|
448
|
+
if (headerEnd < 0) continue;
|
|
449
|
+
const headers = part.subarray(0, headerEnd).toString("utf8");
|
|
450
|
+
const fnMatch = headers.match(/filename="([^"]+)"/i);
|
|
451
|
+
if (!fnMatch) continue;
|
|
452
|
+
fileName = fnMatch[1].replace(/[/\\]/g, "_");
|
|
453
|
+
let end = part.length;
|
|
454
|
+
if (end >= 2 && part[end - 2] === 13 && part[end - 1] === 10) end -= 2;
|
|
455
|
+
fileData = part.subarray(headerEnd + 4, end);
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
if (!fileData) {
|
|
459
|
+
setJson(res, 400, { error: "No file in request" });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const uploadDir = join(tmpdir(), "codex-web-uploads");
|
|
463
|
+
await mkdir(uploadDir, { recursive: true });
|
|
464
|
+
const destDir = await mkdtemp(join(uploadDir, "f-"));
|
|
465
|
+
const destPath = join(destDir, fileName);
|
|
466
|
+
await writeFile(destPath, fileData);
|
|
467
|
+
setJson(res, 200, { path: destPath });
|
|
468
|
+
} catch (err) {
|
|
469
|
+
setJson(res, 500, { error: getErrorMessage(err, "Upload failed") });
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
req.on("error", (err) => {
|
|
473
|
+
setJson(res, 500, { error: getErrorMessage(err, "Upload stream error") });
|
|
474
|
+
});
|
|
475
|
+
}
|
|
368
476
|
async function proxyTranscribe(body, contentType, authToken, accountId) {
|
|
369
477
|
const headers = {
|
|
370
478
|
"Content-Type": contentType,
|
|
@@ -403,11 +511,18 @@ var AppServerProcess = class {
|
|
|
403
511
|
this.pending = /* @__PURE__ */ new Map();
|
|
404
512
|
this.notificationListeners = /* @__PURE__ */ new Set();
|
|
405
513
|
this.pendingServerRequests = /* @__PURE__ */ new Map();
|
|
514
|
+
this.appServerArgs = [
|
|
515
|
+
"app-server",
|
|
516
|
+
"-c",
|
|
517
|
+
'approval_policy="never"',
|
|
518
|
+
"-c",
|
|
519
|
+
'sandbox_mode="danger-full-access"'
|
|
520
|
+
];
|
|
406
521
|
}
|
|
407
522
|
start() {
|
|
408
523
|
if (this.process) return;
|
|
409
524
|
this.stopping = false;
|
|
410
|
-
const proc = spawn("codex",
|
|
525
|
+
const proc = spawn("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
411
526
|
this.process = proc;
|
|
412
527
|
proc.stdout.setEncoding("utf8");
|
|
413
528
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -733,6 +848,10 @@ function createCodexBridgeMiddleware() {
|
|
|
733
848
|
return;
|
|
734
849
|
}
|
|
735
850
|
const url = new URL(req.url, "http://localhost");
|
|
851
|
+
if (req.method === "POST" && url.pathname === "/codex-api/upload-file") {
|
|
852
|
+
handleFileUpload(req, res);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
736
855
|
if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
|
|
737
856
|
const payload = await readJsonBody(req);
|
|
738
857
|
const body = asRecord(payload);
|
|
@@ -783,6 +902,10 @@ function createCodexBridgeMiddleware() {
|
|
|
783
902
|
setJson(res, 200, { data: state });
|
|
784
903
|
return;
|
|
785
904
|
}
|
|
905
|
+
if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
|
|
906
|
+
setJson(res, 200, { data: { path: homedir() } });
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
786
909
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
787
910
|
const payload = await readJsonBody(req);
|
|
788
911
|
const record = asRecord(payload);
|
|
@@ -873,6 +996,36 @@ function createCodexBridgeMiddleware() {
|
|
|
873
996
|
setJson(res, 500, { error: "Failed to compute project name suggestion" });
|
|
874
997
|
return;
|
|
875
998
|
}
|
|
999
|
+
if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
|
|
1000
|
+
const payload = asRecord(await readJsonBody(req));
|
|
1001
|
+
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
1002
|
+
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1003
|
+
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
|
|
1004
|
+
const limit = Math.max(1, Math.min(100, Math.floor(limitRaw)));
|
|
1005
|
+
if (!rawCwd) {
|
|
1006
|
+
setJson(res, 400, { error: "Missing cwd" });
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
1010
|
+
try {
|
|
1011
|
+
const info = await stat(cwd);
|
|
1012
|
+
if (!info.isDirectory()) {
|
|
1013
|
+
setJson(res, 400, { error: "cwd is not a directory" });
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
} catch {
|
|
1017
|
+
setJson(res, 404, { error: "cwd does not exist" });
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
const files = await listFilesWithRipgrep(cwd);
|
|
1022
|
+
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 }));
|
|
1023
|
+
setJson(res, 200, { data: scored });
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
setJson(res, 500, { error: getErrorMessage(error, "Failed to search files") });
|
|
1026
|
+
}
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
876
1029
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
|
|
877
1030
|
const cache = await readThreadTitleCache();
|
|
878
1031
|
setJson(res, 200, { data: cache });
|
|
@@ -1263,10 +1416,21 @@ function runOrFail(command, args, label) {
|
|
|
1263
1416
|
throw new Error(`${label} failed with exit code ${String(result.status ?? -1)}`);
|
|
1264
1417
|
}
|
|
1265
1418
|
}
|
|
1419
|
+
function runWithStatus(command, args) {
|
|
1420
|
+
const result = spawnSync(command, args, { stdio: "inherit" });
|
|
1421
|
+
return result.status ?? -1;
|
|
1422
|
+
}
|
|
1423
|
+
function getUserNpmPrefix() {
|
|
1424
|
+
return join3(homedir2(), ".npm-global");
|
|
1425
|
+
}
|
|
1266
1426
|
function resolveCodexCommand() {
|
|
1267
1427
|
if (canRun("codex", ["--version"])) {
|
|
1268
1428
|
return "codex";
|
|
1269
1429
|
}
|
|
1430
|
+
const userCandidate = join3(getUserNpmPrefix(), "bin", "codex");
|
|
1431
|
+
if (existsSync(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
1432
|
+
return userCandidate;
|
|
1433
|
+
}
|
|
1270
1434
|
const prefix = process.env.PREFIX?.trim();
|
|
1271
1435
|
if (!prefix) {
|
|
1272
1436
|
return null;
|
|
@@ -1281,18 +1445,41 @@ function hasCodexAuth() {
|
|
|
1281
1445
|
const codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex");
|
|
1282
1446
|
return existsSync(join3(codexHome, "auth.json"));
|
|
1283
1447
|
}
|
|
1284
|
-
function
|
|
1285
|
-
if (!isTermuxRuntime()) {
|
|
1286
|
-
return resolveCodexCommand();
|
|
1287
|
-
}
|
|
1448
|
+
function ensureCodexInstalled() {
|
|
1288
1449
|
let codexCommand = resolveCodexCommand();
|
|
1289
1450
|
if (!codexCommand) {
|
|
1290
|
-
|
|
1291
|
-
|
|
1451
|
+
const installWithFallback = (pkg, label) => {
|
|
1452
|
+
const status = runWithStatus("npm", ["install", "-g", pkg]);
|
|
1453
|
+
if (status === 0) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
if (isTermuxRuntime()) {
|
|
1457
|
+
throw new Error(`${label} failed with exit code ${String(status)}`);
|
|
1458
|
+
}
|
|
1459
|
+
const userPrefix = getUserNpmPrefix();
|
|
1460
|
+
console.log(`
|
|
1461
|
+
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
1462
|
+
`);
|
|
1463
|
+
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
1464
|
+
process.env.PATH = `${join3(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
1465
|
+
};
|
|
1466
|
+
if (isTermuxRuntime()) {
|
|
1467
|
+
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
1468
|
+
installWithFallback("@mmmbuto/codex-cli-termux", "Codex CLI install");
|
|
1469
|
+
codexCommand = resolveCodexCommand();
|
|
1470
|
+
if (!codexCommand) {
|
|
1471
|
+
console.log("\nTermux npm package did not expose `codex`. Installing official CLI fallback...\n");
|
|
1472
|
+
installWithFallback("@openai/codex", "Codex CLI fallback install");
|
|
1473
|
+
}
|
|
1474
|
+
} else {
|
|
1475
|
+
console.log("\nCodex CLI not found. Installing official Codex CLI from npm...\n");
|
|
1476
|
+
installWithFallback("@openai/codex", "Codex CLI install");
|
|
1477
|
+
}
|
|
1292
1478
|
codexCommand = resolveCodexCommand();
|
|
1293
|
-
if (!codexCommand) {
|
|
1294
|
-
|
|
1295
|
-
|
|
1479
|
+
if (!codexCommand && !isTermuxRuntime()) {
|
|
1480
|
+
throw new Error("Official Codex CLI install completed but binary is still not available in PATH");
|
|
1481
|
+
}
|
|
1482
|
+
if (!codexCommand && isTermuxRuntime()) {
|
|
1296
1483
|
codexCommand = resolveCodexCommand();
|
|
1297
1484
|
}
|
|
1298
1485
|
if (!codexCommand) {
|
|
@@ -1352,7 +1539,7 @@ function listenWithFallback(server, startPort) {
|
|
|
1352
1539
|
}
|
|
1353
1540
|
async function startServer(options) {
|
|
1354
1541
|
const version = await readCliVersion();
|
|
1355
|
-
const codexCommand =
|
|
1542
|
+
const codexCommand = ensureCodexInstalled() ?? resolveCodexCommand();
|
|
1356
1543
|
if (!hasCodexAuth() && codexCommand) {
|
|
1357
1544
|
console.log("\nCodex is not logged in. Starting `codex login`...\n");
|
|
1358
1545
|
runOrFail(codexCommand, ["login"], "Codex login");
|
|
@@ -1366,6 +1553,7 @@ async function startServer(options) {
|
|
|
1366
1553
|
"",
|
|
1367
1554
|
"Codex Web Local is running!",
|
|
1368
1555
|
` Version: ${version}`,
|
|
1556
|
+
" GitHub: https://github.com/friuns2/codexui",
|
|
1369
1557
|
"",
|
|
1370
1558
|
` Local: http://localhost:${String(port)}`
|
|
1371
1559
|
];
|
|
@@ -1394,14 +1582,14 @@ async function startServer(options) {
|
|
|
1394
1582
|
process.on("SIGTERM", shutdown);
|
|
1395
1583
|
}
|
|
1396
1584
|
async function runLogin() {
|
|
1397
|
-
const codexCommand =
|
|
1585
|
+
const codexCommand = ensureCodexInstalled() ?? "codex";
|
|
1398
1586
|
console.log("\nStarting `codex login`...\n");
|
|
1399
1587
|
runOrFail(codexCommand, ["login"], "Codex login");
|
|
1400
1588
|
}
|
|
1401
|
-
program.option("-p, --port <port>", "port to listen on", "
|
|
1589
|
+
program.option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").action(async (opts) => {
|
|
1402
1590
|
await startServer(opts);
|
|
1403
1591
|
});
|
|
1404
|
-
program.command("login").description("Install/check Codex CLI
|
|
1592
|
+
program.command("login").description("Install/check Codex CLI and run `codex login`").action(runLogin);
|
|
1405
1593
|
program.command("help").description("Show codexui command help").action(() => {
|
|
1406
1594
|
program.outputHelp();
|
|
1407
1595
|
});
|