codexapp 0.1.33 → 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-Cffj8Ar1.css +1 -0
- package/dist/assets/index-DlCORPBL.js +1444 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +1444 -648
- package/dist-cli/index.js.map +1 -1
- package/package.json +1 -2
- package/dist/assets/index-CJZ_Yq9m.js +0 -1442
- package/dist/assets/index-PpE_h85K.css +0 -1
package/dist-cli/index.js
CHANGED
|
@@ -1,33 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import "dotenv/config";
|
|
5
4
|
import { createServer as createServer2 } from "http";
|
|
6
|
-
import { existsSync as existsSync3 } from "fs";
|
|
7
|
-
import { readFile as
|
|
8
|
-
import { homedir as
|
|
9
|
-
import { join as
|
|
10
|
-
import { spawn as
|
|
5
|
+
import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } from "fs";
|
|
6
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
7
|
+
import { homedir as homedir3, networkInterfaces } from "os";
|
|
8
|
+
import { join as join5 } from "path";
|
|
9
|
+
import { spawn as spawn3, spawnSync } from "child_process";
|
|
10
|
+
import { createInterface } from "readline/promises";
|
|
11
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12
|
-
import { dirname as
|
|
12
|
+
import { dirname as dirname3 } from "path";
|
|
13
|
+
import { get as httpsGet } from "https";
|
|
13
14
|
import { Command } from "commander";
|
|
14
15
|
import qrcode from "qrcode-terminal";
|
|
15
16
|
|
|
16
17
|
// src/server/httpServer.ts
|
|
17
18
|
import { fileURLToPath } from "url";
|
|
18
|
-
import { dirname, extname, isAbsolute as isAbsolute2, join as
|
|
19
|
+
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join4 } from "path";
|
|
19
20
|
import { existsSync as existsSync2 } from "fs";
|
|
21
|
+
import { writeFile as writeFile3, stat as stat4 } from "fs/promises";
|
|
20
22
|
import express from "express";
|
|
21
23
|
|
|
22
24
|
// src/server/codexAppServerBridge.ts
|
|
23
|
-
import "
|
|
25
|
+
import { spawn as spawn2 } from "child_process";
|
|
26
|
+
import { randomBytes } from "crypto";
|
|
27
|
+
import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as stat2 } from "fs/promises";
|
|
28
|
+
import { request as httpsRequest } from "https";
|
|
29
|
+
import { homedir as homedir2 } from "os";
|
|
30
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
31
|
+
import { basename, isAbsolute, join as join2, resolve } from "path";
|
|
32
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
33
|
+
|
|
34
|
+
// src/server/skillsRoutes.ts
|
|
24
35
|
import { spawn } from "child_process";
|
|
25
36
|
import { mkdtemp, readFile, readdir, rm, mkdir, stat, lstat, readlink, symlink } from "fs/promises";
|
|
26
37
|
import { existsSync } from "fs";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import { tmpdir } from "os";
|
|
30
|
-
import { isAbsolute, join, resolve } from "path";
|
|
38
|
+
import { homedir, tmpdir } from "os";
|
|
39
|
+
import { join } from "path";
|
|
31
40
|
import { writeFile } from "fs/promises";
|
|
32
41
|
function asRecord(value) {
|
|
33
42
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -51,45 +60,6 @@ function setJson(res, statusCode, payload) {
|
|
|
51
60
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
52
61
|
res.end(JSON.stringify(payload));
|
|
53
62
|
}
|
|
54
|
-
function scoreFileCandidate(path, query) {
|
|
55
|
-
if (!query) return 0;
|
|
56
|
-
const lowerPath = path.toLowerCase();
|
|
57
|
-
const lowerQuery = query.toLowerCase();
|
|
58
|
-
const baseName = lowerPath.slice(lowerPath.lastIndexOf("/") + 1);
|
|
59
|
-
if (baseName === lowerQuery) return 0;
|
|
60
|
-
if (baseName.startsWith(lowerQuery)) return 1;
|
|
61
|
-
if (baseName.includes(lowerQuery)) return 2;
|
|
62
|
-
if (lowerPath.includes(`/${lowerQuery}`)) return 3;
|
|
63
|
-
if (lowerPath.includes(lowerQuery)) return 4;
|
|
64
|
-
return 10;
|
|
65
|
-
}
|
|
66
|
-
async function listFilesWithRipgrep(cwd) {
|
|
67
|
-
return await new Promise((resolve2, reject) => {
|
|
68
|
-
const proc = spawn("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
|
|
69
|
-
cwd,
|
|
70
|
-
env: process.env,
|
|
71
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
72
|
-
});
|
|
73
|
-
let stdout = "";
|
|
74
|
-
let stderr = "";
|
|
75
|
-
proc.stdout.on("data", (chunk) => {
|
|
76
|
-
stdout += chunk.toString();
|
|
77
|
-
});
|
|
78
|
-
proc.stderr.on("data", (chunk) => {
|
|
79
|
-
stderr += chunk.toString();
|
|
80
|
-
});
|
|
81
|
-
proc.on("error", reject);
|
|
82
|
-
proc.on("close", (code) => {
|
|
83
|
-
if (code === 0) {
|
|
84
|
-
const rows = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
85
|
-
resolve2(rows);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
89
|
-
reject(new Error(details || "rg --files failed"));
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
63
|
function getCodexHomeDir() {
|
|
94
64
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
95
65
|
return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
|
|
@@ -142,7 +112,7 @@ async function runCommandWithOutput(command, args, options = {}) {
|
|
|
142
112
|
proc.on("error", reject);
|
|
143
113
|
proc.on("close", (code) => {
|
|
144
114
|
if (code === 0) {
|
|
145
|
-
resolve2(stdout);
|
|
115
|
+
resolve2(stdout.trim());
|
|
146
116
|
return;
|
|
147
117
|
}
|
|
148
118
|
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
@@ -234,7 +204,7 @@ async function fetchMetaBatch(entries) {
|
|
|
234
204
|
const toFetch = entries.filter((e) => !metaCache.has(`${e.owner}/${e.name}`));
|
|
235
205
|
if (toFetch.length === 0) return;
|
|
236
206
|
const batch = toFetch.slice(0, 50);
|
|
237
|
-
|
|
207
|
+
await Promise.allSettled(
|
|
238
208
|
batch.map(async (e) => {
|
|
239
209
|
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${e.owner}/${e.name}/_meta.json`;
|
|
240
210
|
const resp = await fetch(rawUrl);
|
|
@@ -247,7 +217,6 @@ async function fetchMetaBatch(entries) {
|
|
|
247
217
|
});
|
|
248
218
|
})
|
|
249
219
|
);
|
|
250
|
-
void results;
|
|
251
220
|
}
|
|
252
221
|
function buildHubEntry(e) {
|
|
253
222
|
const cached = metaCache.get(`${e.owner}/${e.name}`);
|
|
@@ -376,8 +345,7 @@ function isAndroidLikeRuntime() {
|
|
|
376
345
|
const prefix = process.env.PREFIX?.toLowerCase() ?? "";
|
|
377
346
|
if (prefix.includes("/com.termux/")) return true;
|
|
378
347
|
const proot = process.env.PROOT_TMP_DIR?.toLowerCase() ?? "";
|
|
379
|
-
|
|
380
|
-
return false;
|
|
348
|
+
return proot.length > 0;
|
|
381
349
|
}
|
|
382
350
|
function getPreferredSyncBranch() {
|
|
383
351
|
return isAndroidLikeRuntime() ? "android" : "main";
|
|
@@ -641,23 +609,21 @@ async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _inst
|
|
|
641
609
|
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
642
610
|
const branch = getPreferredSyncBranch();
|
|
643
611
|
const repoDir = await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
644
|
-
const addPaths = ["."];
|
|
645
612
|
void _installedMap;
|
|
646
613
|
await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: repoDir });
|
|
647
614
|
await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: repoDir });
|
|
648
|
-
await runCommand("git", ["add",
|
|
615
|
+
await runCommand("git", ["add", "."], { cwd: repoDir });
|
|
649
616
|
const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
|
|
650
617
|
if (!status) return;
|
|
651
618
|
await runCommand("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
|
|
652
619
|
await runCommand("git", ["push", "origin", `HEAD:${branch}`], { cwd: repoDir });
|
|
653
620
|
}
|
|
654
|
-
async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName
|
|
621
|
+
async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName) {
|
|
655
622
|
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
656
623
|
const branch = getPreferredSyncBranch();
|
|
657
624
|
await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
658
625
|
}
|
|
659
|
-
async function bootstrapSkillsFromUpstreamIntoLocal(
|
|
660
|
-
void _localSkillsDir;
|
|
626
|
+
async function bootstrapSkillsFromUpstreamIntoLocal() {
|
|
661
627
|
const repoUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
|
|
662
628
|
const branch = getPreferredSyncBranch();
|
|
663
629
|
await ensureSkillsWorkingTreeRepo(repoUrl, branch);
|
|
@@ -721,11 +687,29 @@ async function ensureCodexAgentsSymlinkToSkillsAgents() {
|
|
|
721
687
|
const codexHomeDir = getCodexHomeDir();
|
|
722
688
|
const skillsAgentsPath = join(codexHomeDir, "skills", "AGENTS.md");
|
|
723
689
|
const codexAgentsPath = join(codexHomeDir, "AGENTS.md");
|
|
690
|
+
await mkdir(join(codexHomeDir, "skills"), { recursive: true });
|
|
691
|
+
let copiedFromCodex = false;
|
|
724
692
|
try {
|
|
725
|
-
const
|
|
726
|
-
if (
|
|
693
|
+
const codexAgentsStat = await lstat(codexAgentsPath);
|
|
694
|
+
if (codexAgentsStat.isFile() || codexAgentsStat.isSymbolicLink()) {
|
|
695
|
+
const content = await readFile(codexAgentsPath, "utf8");
|
|
696
|
+
await writeFile(skillsAgentsPath, content, "utf8");
|
|
697
|
+
copiedFromCodex = true;
|
|
698
|
+
} else {
|
|
699
|
+
await rm(codexAgentsPath, { force: true, recursive: true });
|
|
700
|
+
}
|
|
727
701
|
} catch {
|
|
728
|
-
|
|
702
|
+
}
|
|
703
|
+
if (!copiedFromCodex) {
|
|
704
|
+
try {
|
|
705
|
+
const skillsAgentsStat = await stat(skillsAgentsPath);
|
|
706
|
+
if (!skillsAgentsStat.isFile()) {
|
|
707
|
+
await rm(skillsAgentsPath, { force: true, recursive: true });
|
|
708
|
+
await writeFile(skillsAgentsPath, "", "utf8");
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
await writeFile(skillsAgentsPath, "", "utf8");
|
|
712
|
+
}
|
|
729
713
|
}
|
|
730
714
|
const relativeTarget = join("skills", "AGENTS.md");
|
|
731
715
|
try {
|
|
@@ -748,7 +732,6 @@ async function initializeSkillsSyncOnStartup(appServer) {
|
|
|
748
732
|
startupSyncStatus.branch = getPreferredSyncBranch();
|
|
749
733
|
try {
|
|
750
734
|
const state = await readSkillsSyncState();
|
|
751
|
-
const localSkillsDir = getSkillsInstallDir();
|
|
752
735
|
if (!state.githubToken) {
|
|
753
736
|
await ensureCodexAgentsSymlinkToSkillsAgents();
|
|
754
737
|
if (!isAndroidLikeRuntime()) {
|
|
@@ -759,7 +742,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
|
|
|
759
742
|
}
|
|
760
743
|
startupSyncStatus.mode = "unauthenticated-bootstrap";
|
|
761
744
|
startupSyncStatus.lastAction = "pull-upstream";
|
|
762
|
-
await bootstrapSkillsFromUpstreamIntoLocal(
|
|
745
|
+
await bootstrapSkillsFromUpstreamIntoLocal();
|
|
763
746
|
try {
|
|
764
747
|
await appServer.rpc("skills/list", { forceReload: true });
|
|
765
748
|
} catch {
|
|
@@ -773,10 +756,9 @@ async function initializeSkillsSyncOnStartup(appServer) {
|
|
|
773
756
|
const username = state.githubUsername || await resolveGithubUsername(state.githubToken);
|
|
774
757
|
const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
|
|
775
758
|
await ensurePrivateForkFromUpstream(state.githubToken, username, repoName);
|
|
776
|
-
|
|
777
|
-
await writeSkillsSyncState(nextState);
|
|
759
|
+
await writeSkillsSyncState({ ...state, githubUsername: username, repoOwner: username, repoName });
|
|
778
760
|
startupSyncStatus.lastAction = "pull-private-fork";
|
|
779
|
-
await pullInstalledSkillsFolderFromRepo(state.githubToken, username, repoName
|
|
761
|
+
await pullInstalledSkillsFolderFromRepo(state.githubToken, username, repoName);
|
|
780
762
|
try {
|
|
781
763
|
await appServer.rpc("skills/list", { forceReload: true });
|
|
782
764
|
} catch {
|
|
@@ -796,15 +778,8 @@ async function finalizeGithubLoginAndSync(token, username, appServer) {
|
|
|
796
778
|
const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
|
|
797
779
|
await ensurePrivateForkFromUpstream(token, username, repoName);
|
|
798
780
|
const current = await readSkillsSyncState();
|
|
799
|
-
await writeSkillsSyncState({
|
|
800
|
-
|
|
801
|
-
githubToken: token,
|
|
802
|
-
githubUsername: username,
|
|
803
|
-
repoOwner: username,
|
|
804
|
-
repoName
|
|
805
|
-
});
|
|
806
|
-
const localDir = getSkillsInstallDir();
|
|
807
|
-
await pullInstalledSkillsFolderFromRepo(token, username, repoName, localDir);
|
|
781
|
+
await writeSkillsSyncState({ ...current, githubToken: token, githubUsername: username, repoOwner: username, repoName });
|
|
782
|
+
await pullInstalledSkillsFolderFromRepo(token, username, repoName);
|
|
808
783
|
try {
|
|
809
784
|
await appServer.rpc("skills/list", { forceReload: true });
|
|
810
785
|
} catch {
|
|
@@ -813,11 +788,10 @@ async function finalizeGithubLoginAndSync(token, username, appServer) {
|
|
|
813
788
|
}
|
|
814
789
|
async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
|
|
815
790
|
const q = query.toLowerCase().trim();
|
|
816
|
-
|
|
791
|
+
const filtered = q ? allEntries.filter((s) => {
|
|
817
792
|
if (s.name.toLowerCase().includes(q) || s.owner.toLowerCase().includes(q)) return true;
|
|
818
793
|
const cached = metaCache.get(`${s.owner}/${s.name}`);
|
|
819
|
-
|
|
820
|
-
return false;
|
|
794
|
+
return Boolean(cached?.displayName?.toLowerCase().includes(q));
|
|
821
795
|
}) : allEntries;
|
|
822
796
|
const page = filtered.slice(0, Math.min(limit * 2, 200));
|
|
823
797
|
await fetchMetaBatch(page);
|
|
@@ -837,204 +811,692 @@ async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
|
|
|
837
811
|
return local ? { ...s, installed: true, path: local.path, enabled: local.enabled } : s;
|
|
838
812
|
});
|
|
839
813
|
}
|
|
840
|
-
function
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
814
|
+
async function handleSkillsRoutes(req, res, url, context) {
|
|
815
|
+
const { appServer, readJsonBody: readJsonBody2 } = context;
|
|
816
|
+
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
|
|
817
|
+
try {
|
|
818
|
+
const q = url.searchParams.get("q") || "";
|
|
819
|
+
const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
|
|
820
|
+
const sort = url.searchParams.get("sort") || "date";
|
|
821
|
+
const allEntries = await fetchSkillsTree();
|
|
822
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
823
|
+
try {
|
|
824
|
+
const result = await appServer.rpc("skills/list", {});
|
|
825
|
+
for (const entry of result.data ?? []) {
|
|
826
|
+
for (const skill of entry.skills ?? []) {
|
|
827
|
+
if (skill.name) {
|
|
828
|
+
installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
} catch {
|
|
833
|
+
}
|
|
834
|
+
const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
|
|
835
|
+
await fetchMetaBatch(installedHubEntries);
|
|
836
|
+
const installed = [];
|
|
837
|
+
for (const [, info] of installedMap) {
|
|
838
|
+
const hubEntry = allEntries.find((e) => e.name === info.name);
|
|
839
|
+
const base = hubEntry ? buildHubEntry(hubEntry) : {
|
|
840
|
+
name: info.name,
|
|
841
|
+
owner: "local",
|
|
842
|
+
description: "",
|
|
843
|
+
displayName: "",
|
|
844
|
+
publishedAt: 0,
|
|
845
|
+
avatarUrl: "",
|
|
846
|
+
url: "",
|
|
847
|
+
installed: false
|
|
848
|
+
};
|
|
849
|
+
installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
|
|
850
|
+
}
|
|
851
|
+
const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
|
|
852
|
+
setJson(res, 200, { data: results, installed, total: allEntries.length });
|
|
853
|
+
} catch (error) {
|
|
854
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch skills hub") });
|
|
856
855
|
}
|
|
856
|
+
return true;
|
|
857
857
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
858
|
+
if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
|
|
859
|
+
const state = await readSkillsSyncState();
|
|
860
|
+
setJson(res, 200, {
|
|
861
|
+
data: {
|
|
862
|
+
loggedIn: Boolean(state.githubToken),
|
|
863
|
+
githubUsername: state.githubUsername ?? "",
|
|
864
|
+
repoOwner: state.repoOwner ?? "",
|
|
865
|
+
repoName: state.repoName ?? "",
|
|
866
|
+
configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
|
|
867
|
+
startup: {
|
|
868
|
+
inProgress: startupSyncStatus.inProgress,
|
|
869
|
+
mode: startupSyncStatus.mode,
|
|
870
|
+
branch: startupSyncStatus.branch,
|
|
871
|
+
lastAction: startupSyncStatus.lastAction,
|
|
872
|
+
lastRunAtIso: startupSyncStatus.lastRunAtIso,
|
|
873
|
+
lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
|
|
874
|
+
lastError: startupSyncStatus.lastError
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
return true;
|
|
872
879
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
const record = asRecord(value);
|
|
880
|
-
if (!record) return { titles: {}, order: [] };
|
|
881
|
-
const rawTitles = asRecord(record.titles);
|
|
882
|
-
const titles = {};
|
|
883
|
-
if (rawTitles) {
|
|
884
|
-
for (const [k, v] of Object.entries(rawTitles)) {
|
|
885
|
-
if (typeof v === "string" && v.length > 0) titles[k] = v;
|
|
880
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
|
|
881
|
+
try {
|
|
882
|
+
const started = await startGithubDeviceLogin();
|
|
883
|
+
setJson(res, 200, { data: started });
|
|
884
|
+
} catch (error) {
|
|
885
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to start GitHub login") });
|
|
886
886
|
}
|
|
887
|
+
return true;
|
|
887
888
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
async function readThreadTitleCache() {
|
|
905
|
-
const statePath = getCodexGlobalStatePath();
|
|
906
|
-
try {
|
|
907
|
-
const raw = await readFile(statePath, "utf8");
|
|
908
|
-
const payload = asRecord(JSON.parse(raw)) ?? {};
|
|
909
|
-
return normalizeThreadTitleCache(payload["thread-titles"]);
|
|
910
|
-
} catch {
|
|
911
|
-
return { titles: {}, order: [] };
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
async function writeThreadTitleCache(cache) {
|
|
915
|
-
const statePath = getCodexGlobalStatePath();
|
|
916
|
-
let payload = {};
|
|
917
|
-
try {
|
|
918
|
-
const raw = await readFile(statePath, "utf8");
|
|
919
|
-
payload = asRecord(JSON.parse(raw)) ?? {};
|
|
920
|
-
} catch {
|
|
921
|
-
payload = {};
|
|
922
|
-
}
|
|
923
|
-
payload["thread-titles"] = cache;
|
|
924
|
-
await writeFile(statePath, JSON.stringify(payload), "utf8");
|
|
925
|
-
}
|
|
926
|
-
async function readWorkspaceRootsState() {
|
|
927
|
-
const statePath = getCodexGlobalStatePath();
|
|
928
|
-
let payload = {};
|
|
929
|
-
try {
|
|
930
|
-
const raw = await readFile(statePath, "utf8");
|
|
931
|
-
const parsed = JSON.parse(raw);
|
|
932
|
-
payload = asRecord(parsed) ?? {};
|
|
933
|
-
} catch {
|
|
934
|
-
payload = {};
|
|
935
|
-
}
|
|
936
|
-
return {
|
|
937
|
-
order: normalizeStringArray(payload["electron-saved-workspace-roots"]),
|
|
938
|
-
labels: normalizeStringRecord(payload["electron-workspace-root-labels"]),
|
|
939
|
-
active: normalizeStringArray(payload["active-workspace-roots"])
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
async function writeWorkspaceRootsState(nextState) {
|
|
943
|
-
const statePath = getCodexGlobalStatePath();
|
|
944
|
-
let payload = {};
|
|
945
|
-
try {
|
|
946
|
-
const raw = await readFile(statePath, "utf8");
|
|
947
|
-
payload = asRecord(JSON.parse(raw)) ?? {};
|
|
948
|
-
} catch {
|
|
949
|
-
payload = {};
|
|
889
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
|
|
890
|
+
try {
|
|
891
|
+
const payload = asRecord(await readJsonBody2(req));
|
|
892
|
+
const token = typeof payload?.token === "string" ? payload.token.trim() : "";
|
|
893
|
+
if (!token) {
|
|
894
|
+
setJson(res, 400, { error: "Missing GitHub token" });
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
const username = await resolveGithubUsername(token);
|
|
898
|
+
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
899
|
+
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
900
|
+
} catch (error) {
|
|
901
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to login with GitHub token") });
|
|
902
|
+
}
|
|
903
|
+
return true;
|
|
950
904
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
905
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
|
|
906
|
+
try {
|
|
907
|
+
const state = await readSkillsSyncState();
|
|
908
|
+
await writeSkillsSyncState({
|
|
909
|
+
...state,
|
|
910
|
+
githubToken: void 0,
|
|
911
|
+
githubUsername: void 0,
|
|
912
|
+
repoOwner: void 0,
|
|
913
|
+
repoName: void 0
|
|
914
|
+
});
|
|
915
|
+
setJson(res, 200, { ok: true });
|
|
916
|
+
} catch (error) {
|
|
917
|
+
setJson(res, 500, { error: getErrorMessage(error, "Failed to logout GitHub") });
|
|
918
|
+
}
|
|
919
|
+
return true;
|
|
967
920
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
match = false;
|
|
976
|
-
break;
|
|
921
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
|
|
922
|
+
try {
|
|
923
|
+
const payload = asRecord(await readJsonBody2(req));
|
|
924
|
+
const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
|
|
925
|
+
if (!deviceCode) {
|
|
926
|
+
setJson(res, 400, { error: "Missing deviceCode" });
|
|
927
|
+
return true;
|
|
977
928
|
}
|
|
929
|
+
const result = await completeGithubDeviceLogin(deviceCode);
|
|
930
|
+
if (!result.token) {
|
|
931
|
+
setJson(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
const token = result.token;
|
|
935
|
+
const username = await resolveGithubUsername(token);
|
|
936
|
+
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
937
|
+
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
938
|
+
} catch (error) {
|
|
939
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to complete GitHub login") });
|
|
978
940
|
}
|
|
979
|
-
|
|
941
|
+
return true;
|
|
980
942
|
}
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
function handleFileUpload(req, res) {
|
|
984
|
-
const chunks = [];
|
|
985
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
986
|
-
req.on("end", async () => {
|
|
943
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
|
|
987
944
|
try {
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
setJson(res, 400, { error: "Missing multipart boundary" });
|
|
993
|
-
return;
|
|
945
|
+
const state = await readSkillsSyncState();
|
|
946
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
947
|
+
setJson(res, 400, { error: "Skills sync is not configured yet" });
|
|
948
|
+
return true;
|
|
994
949
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
let searchStart = 0;
|
|
999
|
-
while (searchStart < body.length) {
|
|
1000
|
-
const idx = body.indexOf(boundaryBuf, searchStart);
|
|
1001
|
-
if (idx < 0) break;
|
|
1002
|
-
if (searchStart > 0) parts.push(body.subarray(searchStart, idx));
|
|
1003
|
-
searchStart = idx + boundaryBuf.length;
|
|
1004
|
-
if (body[searchStart] === 13 && body[searchStart + 1] === 10) searchStart += 2;
|
|
950
|
+
if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
|
|
951
|
+
setJson(res, 400, { error: "Refusing to push to upstream repository" });
|
|
952
|
+
return true;
|
|
1005
953
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
954
|
+
const local = await collectLocalSyncedSkills(appServer);
|
|
955
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
956
|
+
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
957
|
+
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
958
|
+
setJson(res, 200, { ok: true, data: { synced: local.length } });
|
|
959
|
+
} catch (error) {
|
|
960
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to push synced skills") });
|
|
961
|
+
}
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
|
|
965
|
+
try {
|
|
966
|
+
const state = await readSkillsSyncState();
|
|
967
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
968
|
+
await bootstrapSkillsFromUpstreamIntoLocal();
|
|
969
|
+
try {
|
|
970
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
971
|
+
} catch {
|
|
972
|
+
}
|
|
973
|
+
setJson(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
|
|
974
|
+
return true;
|
|
1020
975
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
976
|
+
const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
|
|
977
|
+
const tree = await fetchSkillsTree();
|
|
978
|
+
const uniqueOwnerByName = /* @__PURE__ */ new Map();
|
|
979
|
+
const ambiguousNames = /* @__PURE__ */ new Set();
|
|
980
|
+
for (const entry of tree) {
|
|
981
|
+
if (ambiguousNames.has(entry.name)) continue;
|
|
982
|
+
const existingOwner = uniqueOwnerByName.get(entry.name);
|
|
983
|
+
if (!existingOwner) {
|
|
984
|
+
uniqueOwnerByName.set(entry.name, entry.owner);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (existingOwner !== entry.owner) {
|
|
988
|
+
uniqueOwnerByName.delete(entry.name);
|
|
989
|
+
ambiguousNames.add(entry.name);
|
|
990
|
+
}
|
|
1024
991
|
}
|
|
1025
|
-
const
|
|
1026
|
-
await
|
|
1027
|
-
const
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
992
|
+
const localDir = await detectUserSkillsDir(appServer);
|
|
993
|
+
await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
|
|
994
|
+
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
995
|
+
const localSkills = await scanInstalledSkillsFromDisk();
|
|
996
|
+
for (const skill of remote) {
|
|
997
|
+
const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
|
|
998
|
+
if (!owner) continue;
|
|
999
|
+
if (!localSkills.has(skill.name)) {
|
|
1000
|
+
await runCommand("python3", [
|
|
1001
|
+
installerScript,
|
|
1002
|
+
"--repo",
|
|
1003
|
+
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
1004
|
+
"--path",
|
|
1005
|
+
`skills/${owner}/${skill.name}`,
|
|
1006
|
+
"--dest",
|
|
1007
|
+
localDir,
|
|
1008
|
+
"--method",
|
|
1009
|
+
"git"
|
|
1010
|
+
]);
|
|
1011
|
+
}
|
|
1012
|
+
const skillPath = join(localDir, skill.name);
|
|
1013
|
+
await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
|
|
1014
|
+
}
|
|
1015
|
+
const remoteNames = new Set(remote.map((row) => row.name));
|
|
1016
|
+
for (const [name, localInfo] of localSkills.entries()) {
|
|
1017
|
+
if (!remoteNames.has(name)) {
|
|
1018
|
+
await rm(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
const nextOwners = {};
|
|
1022
|
+
for (const item of remote) {
|
|
1023
|
+
const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
|
|
1024
|
+
if (owner) nextOwners[item.name] = owner;
|
|
1025
|
+
}
|
|
1026
|
+
await writeSkillsSyncState({ ...state, installedOwners: nextOwners });
|
|
1027
|
+
try {
|
|
1028
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
1029
|
+
} catch {
|
|
1030
|
+
}
|
|
1031
|
+
setJson(res, 200, { ok: true, data: { synced: remote.length } });
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to pull synced skills") });
|
|
1034
|
+
}
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
|
|
1038
|
+
try {
|
|
1039
|
+
const owner = url.searchParams.get("owner") || "";
|
|
1040
|
+
const name = url.searchParams.get("name") || "";
|
|
1041
|
+
if (!owner || !name) {
|
|
1042
|
+
setJson(res, 400, { error: "Missing owner or name" });
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
|
|
1046
|
+
const resp = await fetch(rawUrl);
|
|
1047
|
+
if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
|
|
1048
|
+
const content = await resp.text();
|
|
1049
|
+
setJson(res, 200, { content });
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
|
|
1052
|
+
}
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
|
|
1056
|
+
try {
|
|
1057
|
+
const payload = asRecord(await readJsonBody2(req));
|
|
1058
|
+
const owner = typeof payload?.owner === "string" ? payload.owner : "";
|
|
1059
|
+
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1060
|
+
if (!owner || !name) {
|
|
1061
|
+
setJson(res, 400, { error: "Missing owner or name" });
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
1065
|
+
const installDest = await detectUserSkillsDir(appServer);
|
|
1066
|
+
await runCommand("python3", [
|
|
1067
|
+
installerScript,
|
|
1068
|
+
"--repo",
|
|
1069
|
+
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
1070
|
+
"--path",
|
|
1071
|
+
`skills/${owner}/${name}`,
|
|
1072
|
+
"--dest",
|
|
1073
|
+
installDest,
|
|
1074
|
+
"--method",
|
|
1075
|
+
"git"
|
|
1076
|
+
]);
|
|
1077
|
+
const skillDir = join(installDest, name);
|
|
1078
|
+
await ensureInstalledSkillIsValid(appServer, skillDir);
|
|
1079
|
+
const syncState = await readSkillsSyncState();
|
|
1080
|
+
const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
|
|
1081
|
+
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1082
|
+
await autoPushSyncedSkills(appServer);
|
|
1083
|
+
setJson(res, 200, { ok: true, path: skillDir });
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
|
|
1086
|
+
}
|
|
1087
|
+
return true;
|
|
1088
|
+
}
|
|
1089
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
|
|
1090
|
+
try {
|
|
1091
|
+
const payload = asRecord(await readJsonBody2(req));
|
|
1092
|
+
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1093
|
+
const path = typeof payload?.path === "string" ? payload.path : "";
|
|
1094
|
+
const target = path || (name ? join(getSkillsInstallDir(), name) : "");
|
|
1095
|
+
if (!target) {
|
|
1096
|
+
setJson(res, 400, { error: "Missing name or path" });
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
await rm(target, { recursive: true, force: true });
|
|
1100
|
+
if (name) {
|
|
1101
|
+
const syncState = await readSkillsSyncState();
|
|
1102
|
+
const nextOwners = { ...syncState.installedOwners ?? {} };
|
|
1103
|
+
delete nextOwners[name];
|
|
1104
|
+
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1105
|
+
}
|
|
1106
|
+
await autoPushSyncedSkills(appServer);
|
|
1107
|
+
try {
|
|
1108
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
1109
|
+
} catch {
|
|
1110
|
+
}
|
|
1111
|
+
setJson(res, 200, { ok: true, deletedPath: target });
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to uninstall skill") });
|
|
1114
|
+
}
|
|
1115
|
+
return true;
|
|
1116
|
+
}
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/server/codexAppServerBridge.ts
|
|
1121
|
+
function asRecord2(value) {
|
|
1122
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1123
|
+
}
|
|
1124
|
+
function getErrorMessage2(payload, fallback) {
|
|
1125
|
+
if (payload instanceof Error && payload.message.trim().length > 0) {
|
|
1126
|
+
return payload.message;
|
|
1127
|
+
}
|
|
1128
|
+
const record = asRecord2(payload);
|
|
1129
|
+
if (!record) return fallback;
|
|
1130
|
+
const error = record.error;
|
|
1131
|
+
if (typeof error === "string" && error.length > 0) return error;
|
|
1132
|
+
const nestedError = asRecord2(error);
|
|
1133
|
+
if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
|
|
1134
|
+
return nestedError.message;
|
|
1135
|
+
}
|
|
1136
|
+
return fallback;
|
|
1137
|
+
}
|
|
1138
|
+
function setJson2(res, statusCode, payload) {
|
|
1139
|
+
res.statusCode = statusCode;
|
|
1140
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
1141
|
+
res.end(JSON.stringify(payload));
|
|
1142
|
+
}
|
|
1143
|
+
function extractThreadMessageText(threadReadPayload) {
|
|
1144
|
+
const payload = asRecord2(threadReadPayload);
|
|
1145
|
+
const thread = asRecord2(payload?.thread);
|
|
1146
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
1147
|
+
const parts = [];
|
|
1148
|
+
for (const turn of turns) {
|
|
1149
|
+
const turnRecord = asRecord2(turn);
|
|
1150
|
+
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
1151
|
+
for (const item of items) {
|
|
1152
|
+
const itemRecord = asRecord2(item);
|
|
1153
|
+
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
1154
|
+
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
1155
|
+
parts.push(itemRecord.text.trim());
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (type === "userMessage") {
|
|
1159
|
+
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
1160
|
+
for (const block of content) {
|
|
1161
|
+
const blockRecord = asRecord2(block);
|
|
1162
|
+
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
1163
|
+
parts.push(blockRecord.text.trim());
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
if (type === "commandExecution") {
|
|
1169
|
+
const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
|
|
1170
|
+
const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
|
|
1171
|
+
if (command) parts.push(command);
|
|
1172
|
+
if (output) parts.push(output);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return parts.join("\n").trim();
|
|
1177
|
+
}
|
|
1178
|
+
function isExactPhraseMatch(query, doc) {
|
|
1179
|
+
const q = query.trim().toLowerCase();
|
|
1180
|
+
if (!q) return false;
|
|
1181
|
+
return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
|
|
1182
|
+
}
|
|
1183
|
+
function scoreFileCandidate(path, query) {
|
|
1184
|
+
if (!query) return 0;
|
|
1185
|
+
const lowerPath = path.toLowerCase();
|
|
1186
|
+
const lowerQuery = query.toLowerCase();
|
|
1187
|
+
const baseName = lowerPath.slice(lowerPath.lastIndexOf("/") + 1);
|
|
1188
|
+
if (baseName === lowerQuery) return 0;
|
|
1189
|
+
if (baseName.startsWith(lowerQuery)) return 1;
|
|
1190
|
+
if (baseName.includes(lowerQuery)) return 2;
|
|
1191
|
+
if (lowerPath.includes(`/${lowerQuery}`)) return 3;
|
|
1192
|
+
if (lowerPath.includes(lowerQuery)) return 4;
|
|
1193
|
+
return 10;
|
|
1194
|
+
}
|
|
1195
|
+
async function listFilesWithRipgrep(cwd) {
|
|
1196
|
+
return await new Promise((resolve2, reject) => {
|
|
1197
|
+
const proc = spawn2("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
|
|
1198
|
+
cwd,
|
|
1199
|
+
env: process.env,
|
|
1200
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1201
|
+
});
|
|
1202
|
+
let stdout = "";
|
|
1203
|
+
let stderr = "";
|
|
1204
|
+
proc.stdout.on("data", (chunk) => {
|
|
1205
|
+
stdout += chunk.toString();
|
|
1206
|
+
});
|
|
1207
|
+
proc.stderr.on("data", (chunk) => {
|
|
1208
|
+
stderr += chunk.toString();
|
|
1209
|
+
});
|
|
1210
|
+
proc.on("error", reject);
|
|
1211
|
+
proc.on("close", (code) => {
|
|
1212
|
+
if (code === 0) {
|
|
1213
|
+
const rows = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1214
|
+
resolve2(rows);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
1218
|
+
reject(new Error(details || "rg --files failed"));
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
function getCodexHomeDir2() {
|
|
1223
|
+
const codexHome = process.env.CODEX_HOME?.trim();
|
|
1224
|
+
return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
|
|
1225
|
+
}
|
|
1226
|
+
async function runCommand2(command, args, options = {}) {
|
|
1227
|
+
await new Promise((resolve2, reject) => {
|
|
1228
|
+
const proc = spawn2(command, args, {
|
|
1229
|
+
cwd: options.cwd,
|
|
1230
|
+
env: process.env,
|
|
1231
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1232
|
+
});
|
|
1233
|
+
let stdout = "";
|
|
1234
|
+
let stderr = "";
|
|
1235
|
+
proc.stdout.on("data", (chunk) => {
|
|
1236
|
+
stdout += chunk.toString();
|
|
1237
|
+
});
|
|
1238
|
+
proc.stderr.on("data", (chunk) => {
|
|
1239
|
+
stderr += chunk.toString();
|
|
1240
|
+
});
|
|
1241
|
+
proc.on("error", reject);
|
|
1242
|
+
proc.on("close", (code) => {
|
|
1243
|
+
if (code === 0) {
|
|
1244
|
+
resolve2();
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
1248
|
+
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
1249
|
+
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
function isMissingHeadError(error) {
|
|
1254
|
+
const message = getErrorMessage2(error, "").toLowerCase();
|
|
1255
|
+
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
|
|
1256
|
+
}
|
|
1257
|
+
function isNotGitRepositoryError(error) {
|
|
1258
|
+
const message = getErrorMessage2(error, "").toLowerCase();
|
|
1259
|
+
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
1260
|
+
}
|
|
1261
|
+
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
1262
|
+
const agentsPath = join2(repoRoot, "AGENTS.md");
|
|
1263
|
+
try {
|
|
1264
|
+
await stat2(agentsPath);
|
|
1265
|
+
} catch {
|
|
1266
|
+
await writeFile2(agentsPath, "", "utf8");
|
|
1267
|
+
}
|
|
1268
|
+
await runCommand2("git", ["add", "AGENTS.md"], { cwd: repoRoot });
|
|
1269
|
+
await runCommand2(
|
|
1270
|
+
"git",
|
|
1271
|
+
["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
|
|
1272
|
+
{ cwd: repoRoot }
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
async function runCommandCapture(command, args, options = {}) {
|
|
1276
|
+
return await new Promise((resolve2, reject) => {
|
|
1277
|
+
const proc = spawn2(command, args, {
|
|
1278
|
+
cwd: options.cwd,
|
|
1279
|
+
env: process.env,
|
|
1280
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1281
|
+
});
|
|
1282
|
+
let stdout = "";
|
|
1283
|
+
let stderr = "";
|
|
1284
|
+
proc.stdout.on("data", (chunk) => {
|
|
1285
|
+
stdout += chunk.toString();
|
|
1286
|
+
});
|
|
1287
|
+
proc.stderr.on("data", (chunk) => {
|
|
1288
|
+
stderr += chunk.toString();
|
|
1289
|
+
});
|
|
1290
|
+
proc.on("error", reject);
|
|
1291
|
+
proc.on("close", (code) => {
|
|
1292
|
+
if (code === 0) {
|
|
1293
|
+
resolve2(stdout.trim());
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
1297
|
+
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
1298
|
+
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
1299
|
+
});
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
function normalizeStringArray(value) {
|
|
1303
|
+
if (!Array.isArray(value)) return [];
|
|
1304
|
+
const normalized = [];
|
|
1305
|
+
for (const item of value) {
|
|
1306
|
+
if (typeof item === "string" && item.length > 0 && !normalized.includes(item)) {
|
|
1307
|
+
normalized.push(item);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return normalized;
|
|
1311
|
+
}
|
|
1312
|
+
function normalizeStringRecord(value) {
|
|
1313
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1314
|
+
const next = {};
|
|
1315
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1316
|
+
if (typeof key === "string" && key.length > 0 && typeof item === "string") {
|
|
1317
|
+
next[key] = item;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return next;
|
|
1321
|
+
}
|
|
1322
|
+
function getCodexAuthPath() {
|
|
1323
|
+
return join2(getCodexHomeDir2(), "auth.json");
|
|
1324
|
+
}
|
|
1325
|
+
async function readCodexAuth() {
|
|
1326
|
+
try {
|
|
1327
|
+
const raw = await readFile2(getCodexAuthPath(), "utf8");
|
|
1328
|
+
const auth = JSON.parse(raw);
|
|
1329
|
+
const token = auth.tokens?.access_token;
|
|
1330
|
+
if (!token) return null;
|
|
1331
|
+
return { accessToken: token, accountId: auth.tokens?.account_id ?? void 0 };
|
|
1332
|
+
} catch {
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
function getCodexGlobalStatePath() {
|
|
1337
|
+
return join2(getCodexHomeDir2(), ".codex-global-state.json");
|
|
1338
|
+
}
|
|
1339
|
+
var MAX_THREAD_TITLES = 500;
|
|
1340
|
+
function normalizeThreadTitleCache(value) {
|
|
1341
|
+
const record = asRecord2(value);
|
|
1342
|
+
if (!record) return { titles: {}, order: [] };
|
|
1343
|
+
const rawTitles = asRecord2(record.titles);
|
|
1344
|
+
const titles = {};
|
|
1345
|
+
if (rawTitles) {
|
|
1346
|
+
for (const [k, v] of Object.entries(rawTitles)) {
|
|
1347
|
+
if (typeof v === "string" && v.length > 0) titles[k] = v;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
const order = normalizeStringArray(record.order);
|
|
1351
|
+
return { titles, order };
|
|
1352
|
+
}
|
|
1353
|
+
function updateThreadTitleCache(cache, id, title) {
|
|
1354
|
+
const titles = { ...cache.titles, [id]: title };
|
|
1355
|
+
const order = [id, ...cache.order.filter((o) => o !== id)];
|
|
1356
|
+
while (order.length > MAX_THREAD_TITLES) {
|
|
1357
|
+
const removed = order.pop();
|
|
1358
|
+
if (removed) delete titles[removed];
|
|
1359
|
+
}
|
|
1360
|
+
return { titles, order };
|
|
1361
|
+
}
|
|
1362
|
+
function removeFromThreadTitleCache(cache, id) {
|
|
1363
|
+
const { [id]: _, ...titles } = cache.titles;
|
|
1364
|
+
return { titles, order: cache.order.filter((o) => o !== id) };
|
|
1365
|
+
}
|
|
1366
|
+
async function readThreadTitleCache() {
|
|
1367
|
+
const statePath = getCodexGlobalStatePath();
|
|
1368
|
+
try {
|
|
1369
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1370
|
+
const payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
1371
|
+
return normalizeThreadTitleCache(payload["thread-titles"]);
|
|
1372
|
+
} catch {
|
|
1373
|
+
return { titles: {}, order: [] };
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
async function writeThreadTitleCache(cache) {
|
|
1377
|
+
const statePath = getCodexGlobalStatePath();
|
|
1378
|
+
let payload = {};
|
|
1379
|
+
try {
|
|
1380
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1381
|
+
payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
1382
|
+
} catch {
|
|
1383
|
+
payload = {};
|
|
1384
|
+
}
|
|
1385
|
+
payload["thread-titles"] = cache;
|
|
1386
|
+
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
1387
|
+
}
|
|
1388
|
+
async function readWorkspaceRootsState() {
|
|
1389
|
+
const statePath = getCodexGlobalStatePath();
|
|
1390
|
+
let payload = {};
|
|
1391
|
+
try {
|
|
1392
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1393
|
+
const parsed = JSON.parse(raw);
|
|
1394
|
+
payload = asRecord2(parsed) ?? {};
|
|
1395
|
+
} catch {
|
|
1396
|
+
payload = {};
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
order: normalizeStringArray(payload["electron-saved-workspace-roots"]),
|
|
1400
|
+
labels: normalizeStringRecord(payload["electron-workspace-root-labels"]),
|
|
1401
|
+
active: normalizeStringArray(payload["active-workspace-roots"])
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
async function writeWorkspaceRootsState(nextState) {
|
|
1405
|
+
const statePath = getCodexGlobalStatePath();
|
|
1406
|
+
let payload = {};
|
|
1407
|
+
try {
|
|
1408
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1409
|
+
payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
1410
|
+
} catch {
|
|
1411
|
+
payload = {};
|
|
1412
|
+
}
|
|
1413
|
+
payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
|
|
1414
|
+
payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
|
|
1415
|
+
payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
|
|
1416
|
+
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
1417
|
+
}
|
|
1418
|
+
async function readJsonBody(req) {
|
|
1419
|
+
const raw = await readRawBody(req);
|
|
1420
|
+
if (raw.length === 0) return null;
|
|
1421
|
+
const text = raw.toString("utf8").trim();
|
|
1422
|
+
if (text.length === 0) return null;
|
|
1423
|
+
return JSON.parse(text);
|
|
1424
|
+
}
|
|
1425
|
+
async function readRawBody(req) {
|
|
1426
|
+
const chunks = [];
|
|
1427
|
+
for await (const chunk of req) {
|
|
1428
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
1429
|
+
}
|
|
1430
|
+
return Buffer.concat(chunks);
|
|
1431
|
+
}
|
|
1432
|
+
function bufferIndexOf(buf, needle, start = 0) {
|
|
1433
|
+
for (let i = start; i <= buf.length - needle.length; i++) {
|
|
1434
|
+
let match = true;
|
|
1435
|
+
for (let j = 0; j < needle.length; j++) {
|
|
1436
|
+
if (buf[i + j] !== needle[j]) {
|
|
1437
|
+
match = false;
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (match) return i;
|
|
1442
|
+
}
|
|
1443
|
+
return -1;
|
|
1444
|
+
}
|
|
1445
|
+
function handleFileUpload(req, res) {
|
|
1446
|
+
const chunks = [];
|
|
1447
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
1448
|
+
req.on("end", async () => {
|
|
1449
|
+
try {
|
|
1450
|
+
const body = Buffer.concat(chunks);
|
|
1451
|
+
const contentType = req.headers["content-type"] ?? "";
|
|
1452
|
+
const boundaryMatch = contentType.match(/boundary=(.+)/i);
|
|
1453
|
+
if (!boundaryMatch) {
|
|
1454
|
+
setJson2(res, 400, { error: "Missing multipart boundary" });
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
const boundary = boundaryMatch[1];
|
|
1458
|
+
const boundaryBuf = Buffer.from(`--${boundary}`);
|
|
1459
|
+
const parts = [];
|
|
1460
|
+
let searchStart = 0;
|
|
1461
|
+
while (searchStart < body.length) {
|
|
1462
|
+
const idx = body.indexOf(boundaryBuf, searchStart);
|
|
1463
|
+
if (idx < 0) break;
|
|
1464
|
+
if (searchStart > 0) parts.push(body.subarray(searchStart, idx));
|
|
1465
|
+
searchStart = idx + boundaryBuf.length;
|
|
1466
|
+
if (body[searchStart] === 13 && body[searchStart + 1] === 10) searchStart += 2;
|
|
1467
|
+
}
|
|
1468
|
+
let fileName = "uploaded-file";
|
|
1469
|
+
let fileData = null;
|
|
1470
|
+
const headerSep = Buffer.from("\r\n\r\n");
|
|
1471
|
+
for (const part of parts) {
|
|
1472
|
+
const headerEnd = bufferIndexOf(part, headerSep);
|
|
1473
|
+
if (headerEnd < 0) continue;
|
|
1474
|
+
const headers = part.subarray(0, headerEnd).toString("utf8");
|
|
1475
|
+
const fnMatch = headers.match(/filename="([^"]+)"/i);
|
|
1476
|
+
if (!fnMatch) continue;
|
|
1477
|
+
fileName = fnMatch[1].replace(/[/\\]/g, "_");
|
|
1478
|
+
let end = part.length;
|
|
1479
|
+
if (end >= 2 && part[end - 2] === 13 && part[end - 1] === 10) end -= 2;
|
|
1480
|
+
fileData = part.subarray(headerEnd + 4, end);
|
|
1481
|
+
break;
|
|
1482
|
+
}
|
|
1483
|
+
if (!fileData) {
|
|
1484
|
+
setJson2(res, 400, { error: "No file in request" });
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
const uploadDir = join2(tmpdir2(), "codex-web-uploads");
|
|
1488
|
+
await mkdir2(uploadDir, { recursive: true });
|
|
1489
|
+
const destDir = await mkdtemp2(join2(uploadDir, "f-"));
|
|
1490
|
+
const destPath = join2(destDir, fileName);
|
|
1491
|
+
await writeFile2(destPath, fileData);
|
|
1492
|
+
setJson2(res, 200, { path: destPath });
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
setJson2(res, 500, { error: getErrorMessage2(err, "Upload failed") });
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
req.on("error", (err) => {
|
|
1498
|
+
setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
|
|
1499
|
+
});
|
|
1038
1500
|
}
|
|
1039
1501
|
async function proxyTranscribe(body, contentType, authToken, accountId) {
|
|
1040
1502
|
const headers = {
|
|
@@ -1085,7 +1547,7 @@ var AppServerProcess = class {
|
|
|
1085
1547
|
start() {
|
|
1086
1548
|
if (this.process) return;
|
|
1087
1549
|
this.stopping = false;
|
|
1088
|
-
const proc =
|
|
1550
|
+
const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
1089
1551
|
this.process = proc;
|
|
1090
1552
|
proc.stdout.setEncoding("utf8");
|
|
1091
1553
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -1179,7 +1641,7 @@ var AppServerProcess = class {
|
|
|
1179
1641
|
}
|
|
1180
1642
|
this.pendingServerRequests.delete(requestId);
|
|
1181
1643
|
this.sendServerRequestReply(requestId, reply);
|
|
1182
|
-
const requestParams =
|
|
1644
|
+
const requestParams = asRecord2(pendingRequest.params);
|
|
1183
1645
|
const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
|
|
1184
1646
|
this.emitNotification({
|
|
1185
1647
|
method: "server/request/resolved",
|
|
@@ -1248,7 +1710,7 @@ var AppServerProcess = class {
|
|
|
1248
1710
|
}
|
|
1249
1711
|
async respondToServerRequest(payload) {
|
|
1250
1712
|
await this.ensureInitialized();
|
|
1251
|
-
const body =
|
|
1713
|
+
const body = asRecord2(payload);
|
|
1252
1714
|
if (!body) {
|
|
1253
1715
|
throw new Error("Invalid response payload: expected object");
|
|
1254
1716
|
}
|
|
@@ -1256,7 +1718,7 @@ var AppServerProcess = class {
|
|
|
1256
1718
|
if (typeof id !== "number" || !Number.isInteger(id)) {
|
|
1257
1719
|
throw new Error('Invalid response payload: "id" must be an integer');
|
|
1258
1720
|
}
|
|
1259
|
-
const rawError =
|
|
1721
|
+
const rawError = asRecord2(body.error);
|
|
1260
1722
|
if (rawError) {
|
|
1261
1723
|
const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
|
|
1262
1724
|
const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
|
|
@@ -1311,7 +1773,7 @@ var MethodCatalog = class {
|
|
|
1311
1773
|
}
|
|
1312
1774
|
async runGenerateSchemaCommand(outDir) {
|
|
1313
1775
|
await new Promise((resolve2, reject) => {
|
|
1314
|
-
const process2 =
|
|
1776
|
+
const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
|
|
1315
1777
|
stdio: ["ignore", "ignore", "pipe"]
|
|
1316
1778
|
});
|
|
1317
1779
|
let stderr = "";
|
|
@@ -1330,13 +1792,13 @@ var MethodCatalog = class {
|
|
|
1330
1792
|
});
|
|
1331
1793
|
}
|
|
1332
1794
|
extractMethodsFromClientRequest(payload) {
|
|
1333
|
-
const root =
|
|
1795
|
+
const root = asRecord2(payload);
|
|
1334
1796
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
1335
1797
|
const methods = /* @__PURE__ */ new Set();
|
|
1336
1798
|
for (const entry of oneOf) {
|
|
1337
|
-
const row =
|
|
1338
|
-
const properties =
|
|
1339
|
-
const methodDef =
|
|
1799
|
+
const row = asRecord2(entry);
|
|
1800
|
+
const properties = asRecord2(row?.properties);
|
|
1801
|
+
const methodDef = asRecord2(properties?.method);
|
|
1340
1802
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
1341
1803
|
for (const item of methodEnum) {
|
|
1342
1804
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -1347,13 +1809,13 @@ var MethodCatalog = class {
|
|
|
1347
1809
|
return Array.from(methods).sort((a, b) => a.localeCompare(b));
|
|
1348
1810
|
}
|
|
1349
1811
|
extractMethodsFromServerNotification(payload) {
|
|
1350
|
-
const root =
|
|
1812
|
+
const root = asRecord2(payload);
|
|
1351
1813
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
1352
1814
|
const methods = /* @__PURE__ */ new Set();
|
|
1353
1815
|
for (const entry of oneOf) {
|
|
1354
|
-
const row =
|
|
1355
|
-
const properties =
|
|
1356
|
-
const methodDef =
|
|
1816
|
+
const row = asRecord2(entry);
|
|
1817
|
+
const properties = asRecord2(row?.properties);
|
|
1818
|
+
const methodDef = asRecord2(properties?.method);
|
|
1357
1819
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
1358
1820
|
for (const item of methodEnum) {
|
|
1359
1821
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -1367,10 +1829,10 @@ var MethodCatalog = class {
|
|
|
1367
1829
|
if (this.methodCache) {
|
|
1368
1830
|
return this.methodCache;
|
|
1369
1831
|
}
|
|
1370
|
-
const outDir = await
|
|
1832
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
1371
1833
|
await this.runGenerateSchemaCommand(outDir);
|
|
1372
|
-
const clientRequestPath =
|
|
1373
|
-
const raw = await
|
|
1834
|
+
const clientRequestPath = join2(outDir, "ClientRequest.json");
|
|
1835
|
+
const raw = await readFile2(clientRequestPath, "utf8");
|
|
1374
1836
|
const parsed = JSON.parse(raw);
|
|
1375
1837
|
const methods = this.extractMethodsFromClientRequest(parsed);
|
|
1376
1838
|
this.methodCache = methods;
|
|
@@ -1380,10 +1842,10 @@ var MethodCatalog = class {
|
|
|
1380
1842
|
if (this.notificationCache) {
|
|
1381
1843
|
return this.notificationCache;
|
|
1382
1844
|
}
|
|
1383
|
-
const outDir = await
|
|
1845
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
1384
1846
|
await this.runGenerateSchemaCommand(outDir);
|
|
1385
|
-
const serverNotificationPath =
|
|
1386
|
-
const raw = await
|
|
1847
|
+
const serverNotificationPath = join2(outDir, "ServerNotification.json");
|
|
1848
|
+
const raw = await readFile2(serverNotificationPath, "utf8");
|
|
1387
1849
|
const parsed = JSON.parse(raw);
|
|
1388
1850
|
const methods = this.extractMethodsFromServerNotification(parsed);
|
|
1389
1851
|
this.notificationCache = methods;
|
|
@@ -1402,8 +1864,82 @@ function getSharedBridgeState() {
|
|
|
1402
1864
|
globalScope[SHARED_BRIDGE_KEY] = created;
|
|
1403
1865
|
return created;
|
|
1404
1866
|
}
|
|
1867
|
+
async function loadAllThreadsForSearch(appServer) {
|
|
1868
|
+
const threads = [];
|
|
1869
|
+
let cursor = null;
|
|
1870
|
+
do {
|
|
1871
|
+
const response = asRecord2(await appServer.rpc("thread/list", {
|
|
1872
|
+
archived: false,
|
|
1873
|
+
limit: 100,
|
|
1874
|
+
sortKey: "updated_at",
|
|
1875
|
+
cursor
|
|
1876
|
+
}));
|
|
1877
|
+
const data = Array.isArray(response?.data) ? response.data : [];
|
|
1878
|
+
for (const row of data) {
|
|
1879
|
+
const record = asRecord2(row);
|
|
1880
|
+
const id = typeof record?.id === "string" ? record.id : "";
|
|
1881
|
+
if (!id) continue;
|
|
1882
|
+
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";
|
|
1883
|
+
const preview = typeof record?.preview === "string" ? record.preview : "";
|
|
1884
|
+
threads.push({ id, title, preview });
|
|
1885
|
+
}
|
|
1886
|
+
cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
|
|
1887
|
+
} while (cursor);
|
|
1888
|
+
const docs = [];
|
|
1889
|
+
const concurrency = 4;
|
|
1890
|
+
for (let offset = 0; offset < threads.length; offset += concurrency) {
|
|
1891
|
+
const batch = threads.slice(offset, offset + concurrency);
|
|
1892
|
+
const loaded = await Promise.all(batch.map(async (thread) => {
|
|
1893
|
+
try {
|
|
1894
|
+
const readResponse = await appServer.rpc("thread/read", {
|
|
1895
|
+
threadId: thread.id,
|
|
1896
|
+
includeTurns: true
|
|
1897
|
+
});
|
|
1898
|
+
const messageText = extractThreadMessageText(readResponse);
|
|
1899
|
+
const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
|
|
1900
|
+
return {
|
|
1901
|
+
id: thread.id,
|
|
1902
|
+
title: thread.title,
|
|
1903
|
+
preview: thread.preview,
|
|
1904
|
+
messageText,
|
|
1905
|
+
searchableText
|
|
1906
|
+
};
|
|
1907
|
+
} catch {
|
|
1908
|
+
const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
|
|
1909
|
+
return {
|
|
1910
|
+
id: thread.id,
|
|
1911
|
+
title: thread.title,
|
|
1912
|
+
preview: thread.preview,
|
|
1913
|
+
messageText: "",
|
|
1914
|
+
searchableText
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
}));
|
|
1918
|
+
docs.push(...loaded);
|
|
1919
|
+
}
|
|
1920
|
+
return docs;
|
|
1921
|
+
}
|
|
1922
|
+
async function buildThreadSearchIndex(appServer) {
|
|
1923
|
+
const docs = await loadAllThreadsForSearch(appServer);
|
|
1924
|
+
const docsById = new Map(docs.map((doc) => [doc.id, doc]));
|
|
1925
|
+
return { docsById };
|
|
1926
|
+
}
|
|
1405
1927
|
function createCodexBridgeMiddleware() {
|
|
1406
1928
|
const { appServer, methodCatalog } = getSharedBridgeState();
|
|
1929
|
+
let threadSearchIndex = null;
|
|
1930
|
+
let threadSearchIndexPromise = null;
|
|
1931
|
+
async function getThreadSearchIndex() {
|
|
1932
|
+
if (threadSearchIndex) return threadSearchIndex;
|
|
1933
|
+
if (!threadSearchIndexPromise) {
|
|
1934
|
+
threadSearchIndexPromise = buildThreadSearchIndex(appServer).then((index) => {
|
|
1935
|
+
threadSearchIndex = index;
|
|
1936
|
+
return index;
|
|
1937
|
+
}).finally(() => {
|
|
1938
|
+
threadSearchIndexPromise = null;
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
return threadSearchIndexPromise;
|
|
1942
|
+
}
|
|
1407
1943
|
void initializeSkillsSyncOnStartup(appServer);
|
|
1408
1944
|
const middleware = async (req, res, next) => {
|
|
1409
1945
|
try {
|
|
@@ -1412,25 +1948,28 @@ function createCodexBridgeMiddleware() {
|
|
|
1412
1948
|
return;
|
|
1413
1949
|
}
|
|
1414
1950
|
const url = new URL(req.url, "http://localhost");
|
|
1951
|
+
if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1415
1954
|
if (req.method === "POST" && url.pathname === "/codex-api/upload-file") {
|
|
1416
1955
|
handleFileUpload(req, res);
|
|
1417
1956
|
return;
|
|
1418
1957
|
}
|
|
1419
1958
|
if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
|
|
1420
1959
|
const payload = await readJsonBody(req);
|
|
1421
|
-
const body =
|
|
1960
|
+
const body = asRecord2(payload);
|
|
1422
1961
|
if (!body || typeof body.method !== "string" || body.method.length === 0) {
|
|
1423
|
-
|
|
1962
|
+
setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
|
|
1424
1963
|
return;
|
|
1425
1964
|
}
|
|
1426
1965
|
const result = await appServer.rpc(body.method, body.params ?? null);
|
|
1427
|
-
|
|
1966
|
+
setJson2(res, 200, { result });
|
|
1428
1967
|
return;
|
|
1429
1968
|
}
|
|
1430
1969
|
if (req.method === "POST" && url.pathname === "/codex-api/transcribe") {
|
|
1431
1970
|
const auth = await readCodexAuth();
|
|
1432
1971
|
if (!auth) {
|
|
1433
|
-
|
|
1972
|
+
setJson2(res, 401, { error: "No auth token available for transcription" });
|
|
1434
1973
|
return;
|
|
1435
1974
|
}
|
|
1436
1975
|
const rawBody = await readRawBody(req);
|
|
@@ -1444,37 +1983,107 @@ function createCodexBridgeMiddleware() {
|
|
|
1444
1983
|
if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
|
|
1445
1984
|
const payload = await readJsonBody(req);
|
|
1446
1985
|
await appServer.respondToServerRequest(payload);
|
|
1447
|
-
|
|
1986
|
+
setJson2(res, 200, { ok: true });
|
|
1448
1987
|
return;
|
|
1449
1988
|
}
|
|
1450
1989
|
if (req.method === "GET" && url.pathname === "/codex-api/server-requests/pending") {
|
|
1451
|
-
|
|
1990
|
+
setJson2(res, 200, { data: appServer.listPendingServerRequests() });
|
|
1452
1991
|
return;
|
|
1453
1992
|
}
|
|
1454
1993
|
if (req.method === "GET" && url.pathname === "/codex-api/meta/methods") {
|
|
1455
1994
|
const methods = await methodCatalog.listMethods();
|
|
1456
|
-
|
|
1995
|
+
setJson2(res, 200, { data: methods });
|
|
1457
1996
|
return;
|
|
1458
1997
|
}
|
|
1459
1998
|
if (req.method === "GET" && url.pathname === "/codex-api/meta/notifications") {
|
|
1460
1999
|
const methods = await methodCatalog.listNotificationMethods();
|
|
1461
|
-
|
|
2000
|
+
setJson2(res, 200, { data: methods });
|
|
1462
2001
|
return;
|
|
1463
2002
|
}
|
|
1464
2003
|
if (req.method === "GET" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1465
2004
|
const state = await readWorkspaceRootsState();
|
|
1466
|
-
|
|
2005
|
+
setJson2(res, 200, { data: state });
|
|
1467
2006
|
return;
|
|
1468
2007
|
}
|
|
1469
2008
|
if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
|
|
1470
|
-
|
|
2009
|
+
setJson2(res, 200, { data: { path: homedir2() } });
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
2013
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
2014
|
+
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
2015
|
+
if (!rawSourceCwd) {
|
|
2016
|
+
setJson2(res, 400, { error: "Missing sourceCwd" });
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
|
|
2020
|
+
try {
|
|
2021
|
+
const sourceInfo = await stat2(sourceCwd);
|
|
2022
|
+
if (!sourceInfo.isDirectory()) {
|
|
2023
|
+
setJson2(res, 400, { error: "sourceCwd is not a directory" });
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
} catch {
|
|
2027
|
+
setJson2(res, 404, { error: "sourceCwd does not exist" });
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
try {
|
|
2031
|
+
let gitRoot = "";
|
|
2032
|
+
try {
|
|
2033
|
+
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
2034
|
+
} catch (error) {
|
|
2035
|
+
if (!isNotGitRepositoryError(error)) throw error;
|
|
2036
|
+
await runCommand2("git", ["init"], { cwd: sourceCwd });
|
|
2037
|
+
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
2038
|
+
}
|
|
2039
|
+
const repoName = basename(gitRoot) || "repo";
|
|
2040
|
+
const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
|
|
2041
|
+
await mkdir2(worktreesRoot, { recursive: true });
|
|
2042
|
+
let worktreeId = "";
|
|
2043
|
+
let worktreeParent = "";
|
|
2044
|
+
let worktreeCwd = "";
|
|
2045
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
2046
|
+
const candidate = randomBytes(2).toString("hex");
|
|
2047
|
+
const parent = join2(worktreesRoot, candidate);
|
|
2048
|
+
try {
|
|
2049
|
+
await stat2(parent);
|
|
2050
|
+
continue;
|
|
2051
|
+
} catch {
|
|
2052
|
+
worktreeId = candidate;
|
|
2053
|
+
worktreeParent = parent;
|
|
2054
|
+
worktreeCwd = join2(parent, repoName);
|
|
2055
|
+
break;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
if (!worktreeId || !worktreeParent || !worktreeCwd) {
|
|
2059
|
+
throw new Error("Failed to allocate a unique worktree id");
|
|
2060
|
+
}
|
|
2061
|
+
const branch = `codex/${worktreeId}`;
|
|
2062
|
+
await mkdir2(worktreeParent, { recursive: true });
|
|
2063
|
+
try {
|
|
2064
|
+
await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
if (!isMissingHeadError(error)) throw error;
|
|
2067
|
+
await ensureRepoHasInitialCommit(gitRoot);
|
|
2068
|
+
await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
2069
|
+
}
|
|
2070
|
+
setJson2(res, 200, {
|
|
2071
|
+
data: {
|
|
2072
|
+
cwd: worktreeCwd,
|
|
2073
|
+
branch,
|
|
2074
|
+
gitRoot
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to create worktree") });
|
|
2079
|
+
}
|
|
1471
2080
|
return;
|
|
1472
2081
|
}
|
|
1473
2082
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1474
2083
|
const payload = await readJsonBody(req);
|
|
1475
|
-
const record =
|
|
2084
|
+
const record = asRecord2(payload);
|
|
1476
2085
|
if (!record) {
|
|
1477
|
-
|
|
2086
|
+
setJson2(res, 400, { error: "Invalid body: expected object" });
|
|
1478
2087
|
return;
|
|
1479
2088
|
}
|
|
1480
2089
|
const nextState = {
|
|
@@ -1483,33 +2092,33 @@ function createCodexBridgeMiddleware() {
|
|
|
1483
2092
|
active: normalizeStringArray(record.active)
|
|
1484
2093
|
};
|
|
1485
2094
|
await writeWorkspaceRootsState(nextState);
|
|
1486
|
-
|
|
2095
|
+
setJson2(res, 200, { ok: true });
|
|
1487
2096
|
return;
|
|
1488
2097
|
}
|
|
1489
2098
|
if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
|
|
1490
|
-
const payload =
|
|
2099
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1491
2100
|
const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
|
1492
2101
|
const createIfMissing = payload?.createIfMissing === true;
|
|
1493
2102
|
const label = typeof payload?.label === "string" ? payload.label : "";
|
|
1494
2103
|
if (!rawPath) {
|
|
1495
|
-
|
|
2104
|
+
setJson2(res, 400, { error: "Missing path" });
|
|
1496
2105
|
return;
|
|
1497
2106
|
}
|
|
1498
2107
|
const normalizedPath = isAbsolute(rawPath) ? rawPath : resolve(rawPath);
|
|
1499
2108
|
let pathExists = true;
|
|
1500
2109
|
try {
|
|
1501
|
-
const info = await
|
|
2110
|
+
const info = await stat2(normalizedPath);
|
|
1502
2111
|
if (!info.isDirectory()) {
|
|
1503
|
-
|
|
2112
|
+
setJson2(res, 400, { error: "Path exists but is not a directory" });
|
|
1504
2113
|
return;
|
|
1505
2114
|
}
|
|
1506
2115
|
} catch {
|
|
1507
2116
|
pathExists = false;
|
|
1508
2117
|
}
|
|
1509
2118
|
if (!pathExists && createIfMissing) {
|
|
1510
|
-
await
|
|
2119
|
+
await mkdir2(normalizedPath, { recursive: true });
|
|
1511
2120
|
} else if (!pathExists) {
|
|
1512
|
-
|
|
2121
|
+
setJson2(res, 404, { error: "Directory does not exist" });
|
|
1513
2122
|
return;
|
|
1514
2123
|
}
|
|
1515
2124
|
const existingState = await readWorkspaceRootsState();
|
|
@@ -1524,388 +2133,103 @@ function createCodexBridgeMiddleware() {
|
|
|
1524
2133
|
labels: nextLabels,
|
|
1525
2134
|
active: nextActive
|
|
1526
2135
|
});
|
|
1527
|
-
|
|
2136
|
+
setJson2(res, 200, { data: { path: normalizedPath } });
|
|
1528
2137
|
return;
|
|
1529
2138
|
}
|
|
1530
2139
|
if (req.method === "GET" && url.pathname === "/codex-api/project-root-suggestion") {
|
|
1531
2140
|
const basePath = url.searchParams.get("basePath")?.trim() ?? "";
|
|
1532
2141
|
if (!basePath) {
|
|
1533
|
-
|
|
2142
|
+
setJson2(res, 400, { error: "Missing basePath" });
|
|
1534
2143
|
return;
|
|
1535
2144
|
}
|
|
1536
2145
|
const normalizedBasePath = isAbsolute(basePath) ? basePath : resolve(basePath);
|
|
1537
2146
|
try {
|
|
1538
|
-
const baseInfo = await
|
|
2147
|
+
const baseInfo = await stat2(normalizedBasePath);
|
|
1539
2148
|
if (!baseInfo.isDirectory()) {
|
|
1540
|
-
|
|
2149
|
+
setJson2(res, 400, { error: "basePath is not a directory" });
|
|
1541
2150
|
return;
|
|
1542
2151
|
}
|
|
1543
2152
|
} catch {
|
|
1544
|
-
|
|
2153
|
+
setJson2(res, 404, { error: "basePath does not exist" });
|
|
1545
2154
|
return;
|
|
1546
2155
|
}
|
|
1547
2156
|
let index = 1;
|
|
1548
2157
|
while (index < 1e5) {
|
|
1549
2158
|
const candidateName = `New Project (${String(index)})`;
|
|
1550
|
-
const candidatePath =
|
|
2159
|
+
const candidatePath = join2(normalizedBasePath, candidateName);
|
|
1551
2160
|
try {
|
|
1552
|
-
await
|
|
2161
|
+
await stat2(candidatePath);
|
|
1553
2162
|
index += 1;
|
|
1554
2163
|
continue;
|
|
1555
2164
|
} catch {
|
|
1556
|
-
|
|
2165
|
+
setJson2(res, 200, { data: { name: candidateName, path: candidatePath } });
|
|
1557
2166
|
return;
|
|
1558
2167
|
}
|
|
1559
2168
|
}
|
|
1560
|
-
|
|
2169
|
+
setJson2(res, 500, { error: "Failed to compute project name suggestion" });
|
|
1561
2170
|
return;
|
|
1562
2171
|
}
|
|
1563
2172
|
if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
|
|
1564
|
-
const payload =
|
|
2173
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1565
2174
|
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
1566
2175
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1567
2176
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
|
|
1568
2177
|
const limit = Math.max(1, Math.min(100, Math.floor(limitRaw)));
|
|
1569
2178
|
if (!rawCwd) {
|
|
1570
|
-
|
|
2179
|
+
setJson2(res, 400, { error: "Missing cwd" });
|
|
1571
2180
|
return;
|
|
1572
2181
|
}
|
|
1573
2182
|
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
1574
2183
|
try {
|
|
1575
|
-
const info = await
|
|
2184
|
+
const info = await stat2(cwd);
|
|
1576
2185
|
if (!info.isDirectory()) {
|
|
1577
|
-
|
|
2186
|
+
setJson2(res, 400, { error: "cwd is not a directory" });
|
|
1578
2187
|
return;
|
|
1579
2188
|
}
|
|
1580
2189
|
} catch {
|
|
1581
|
-
|
|
2190
|
+
setJson2(res, 404, { error: "cwd does not exist" });
|
|
1582
2191
|
return;
|
|
1583
2192
|
}
|
|
1584
2193
|
try {
|
|
1585
2194
|
const files = await listFilesWithRipgrep(cwd);
|
|
1586
2195
|
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 }));
|
|
1587
|
-
|
|
2196
|
+
setJson2(res, 200, { data: scored });
|
|
1588
2197
|
} catch (error) {
|
|
1589
|
-
|
|
2198
|
+
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to search files") });
|
|
1590
2199
|
}
|
|
1591
2200
|
return;
|
|
1592
2201
|
}
|
|
1593
2202
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
|
|
1594
2203
|
const cache = await readThreadTitleCache();
|
|
1595
|
-
|
|
2204
|
+
setJson2(res, 200, { data: cache });
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
2208
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
2209
|
+
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
2210
|
+
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
2211
|
+
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
2212
|
+
if (!query) {
|
|
2213
|
+
setJson2(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
const index = await getThreadSearchIndex();
|
|
2217
|
+
const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
|
|
2218
|
+
setJson2(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
|
|
1596
2219
|
return;
|
|
1597
2220
|
}
|
|
1598
2221
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
1599
|
-
const payload =
|
|
2222
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1600
2223
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
1601
2224
|
const title = typeof payload?.title === "string" ? payload.title : "";
|
|
1602
2225
|
if (!id) {
|
|
1603
|
-
|
|
2226
|
+
setJson2(res, 400, { error: "Missing id" });
|
|
1604
2227
|
return;
|
|
1605
2228
|
}
|
|
1606
2229
|
const cache = await readThreadTitleCache();
|
|
1607
2230
|
const next2 = title ? updateThreadTitleCache(cache, id, title) : removeFromThreadTitleCache(cache, id);
|
|
1608
2231
|
await writeThreadTitleCache(next2);
|
|
1609
|
-
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
|
|
1613
|
-
try {
|
|
1614
|
-
const q = url.searchParams.get("q") || "";
|
|
1615
|
-
const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
|
|
1616
|
-
const sort = url.searchParams.get("sort") || "date";
|
|
1617
|
-
const allEntries = await fetchSkillsTree();
|
|
1618
|
-
const installedMap = await scanInstalledSkillsFromDisk();
|
|
1619
|
-
try {
|
|
1620
|
-
const result = await appServer.rpc("skills/list", {});
|
|
1621
|
-
for (const entry of result.data ?? []) {
|
|
1622
|
-
for (const skill of entry.skills ?? []) {
|
|
1623
|
-
if (skill.name) {
|
|
1624
|
-
installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
} catch {
|
|
1629
|
-
}
|
|
1630
|
-
const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
|
|
1631
|
-
await fetchMetaBatch(installedHubEntries);
|
|
1632
|
-
const installed = [];
|
|
1633
|
-
for (const [, info] of installedMap) {
|
|
1634
|
-
const hubEntry = allEntries.find((e) => e.name === info.name);
|
|
1635
|
-
const base = hubEntry ? buildHubEntry(hubEntry) : {
|
|
1636
|
-
name: info.name,
|
|
1637
|
-
owner: "local",
|
|
1638
|
-
description: "",
|
|
1639
|
-
displayName: "",
|
|
1640
|
-
publishedAt: 0,
|
|
1641
|
-
avatarUrl: "",
|
|
1642
|
-
url: "",
|
|
1643
|
-
installed: false
|
|
1644
|
-
};
|
|
1645
|
-
installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
|
|
1646
|
-
}
|
|
1647
|
-
const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
|
|
1648
|
-
setJson(res, 200, { data: results, installed, total: allEntries.length });
|
|
1649
|
-
} catch (error) {
|
|
1650
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch skills hub") });
|
|
1651
|
-
}
|
|
1652
|
-
return;
|
|
1653
|
-
}
|
|
1654
|
-
if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
|
|
1655
|
-
const state = await readSkillsSyncState();
|
|
1656
|
-
setJson(res, 200, {
|
|
1657
|
-
data: {
|
|
1658
|
-
loggedIn: Boolean(state.githubToken),
|
|
1659
|
-
githubUsername: state.githubUsername ?? "",
|
|
1660
|
-
repoOwner: state.repoOwner ?? "",
|
|
1661
|
-
repoName: state.repoName ?? "",
|
|
1662
|
-
configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
|
|
1663
|
-
startup: {
|
|
1664
|
-
inProgress: startupSyncStatus.inProgress,
|
|
1665
|
-
mode: startupSyncStatus.mode,
|
|
1666
|
-
branch: startupSyncStatus.branch,
|
|
1667
|
-
lastAction: startupSyncStatus.lastAction,
|
|
1668
|
-
lastRunAtIso: startupSyncStatus.lastRunAtIso,
|
|
1669
|
-
lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
|
|
1670
|
-
lastError: startupSyncStatus.lastError
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
});
|
|
1674
|
-
return;
|
|
1675
|
-
}
|
|
1676
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
|
|
1677
|
-
try {
|
|
1678
|
-
const started = await startGithubDeviceLogin();
|
|
1679
|
-
setJson(res, 200, { data: started });
|
|
1680
|
-
} catch (error) {
|
|
1681
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to start GitHub login") });
|
|
1682
|
-
}
|
|
1683
|
-
return;
|
|
1684
|
-
}
|
|
1685
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
|
|
1686
|
-
try {
|
|
1687
|
-
const payload = asRecord(await readJsonBody(req));
|
|
1688
|
-
const token = typeof payload?.token === "string" ? payload.token.trim() : "";
|
|
1689
|
-
if (!token) {
|
|
1690
|
-
setJson(res, 400, { error: "Missing GitHub token" });
|
|
1691
|
-
return;
|
|
1692
|
-
}
|
|
1693
|
-
const username = await resolveGithubUsername(token);
|
|
1694
|
-
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
1695
|
-
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
1696
|
-
} catch (error) {
|
|
1697
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to login with GitHub token") });
|
|
1698
|
-
}
|
|
1699
|
-
return;
|
|
1700
|
-
}
|
|
1701
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
|
|
1702
|
-
try {
|
|
1703
|
-
const state = await readSkillsSyncState();
|
|
1704
|
-
await writeSkillsSyncState({
|
|
1705
|
-
...state,
|
|
1706
|
-
githubToken: void 0,
|
|
1707
|
-
githubUsername: void 0,
|
|
1708
|
-
repoOwner: void 0,
|
|
1709
|
-
repoName: void 0
|
|
1710
|
-
});
|
|
1711
|
-
setJson(res, 200, { ok: true });
|
|
1712
|
-
} catch (error) {
|
|
1713
|
-
setJson(res, 500, { error: getErrorMessage(error, "Failed to logout GitHub") });
|
|
1714
|
-
}
|
|
1715
|
-
return;
|
|
1716
|
-
}
|
|
1717
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
|
|
1718
|
-
try {
|
|
1719
|
-
const payload = asRecord(await readJsonBody(req));
|
|
1720
|
-
const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
|
|
1721
|
-
if (!deviceCode) {
|
|
1722
|
-
setJson(res, 400, { error: "Missing deviceCode" });
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
|
-
const result = await completeGithubDeviceLogin(deviceCode);
|
|
1726
|
-
if (!result.token) {
|
|
1727
|
-
setJson(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1730
|
-
const token = result.token;
|
|
1731
|
-
const username = await resolveGithubUsername(token);
|
|
1732
|
-
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
1733
|
-
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
1734
|
-
} catch (error) {
|
|
1735
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to complete GitHub login") });
|
|
1736
|
-
}
|
|
1737
|
-
return;
|
|
1738
|
-
}
|
|
1739
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
|
|
1740
|
-
try {
|
|
1741
|
-
const state = await readSkillsSyncState();
|
|
1742
|
-
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
1743
|
-
setJson(res, 400, { error: "Skills sync is not configured yet" });
|
|
1744
|
-
return;
|
|
1745
|
-
}
|
|
1746
|
-
if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
|
|
1747
|
-
setJson(res, 400, { error: "Refusing to push to upstream repository" });
|
|
1748
|
-
return;
|
|
1749
|
-
}
|
|
1750
|
-
const local = await collectLocalSyncedSkills(appServer);
|
|
1751
|
-
const installedMap = await scanInstalledSkillsFromDisk();
|
|
1752
|
-
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
1753
|
-
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
1754
|
-
setJson(res, 200, { ok: true, data: { synced: local.length } });
|
|
1755
|
-
} catch (error) {
|
|
1756
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to push synced skills") });
|
|
1757
|
-
}
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
|
|
1761
|
-
try {
|
|
1762
|
-
const state = await readSkillsSyncState();
|
|
1763
|
-
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
1764
|
-
setJson(res, 400, { error: "Skills sync is not configured yet" });
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
1767
|
-
const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
|
|
1768
|
-
const tree = await fetchSkillsTree();
|
|
1769
|
-
const uniqueOwnerByName = /* @__PURE__ */ new Map();
|
|
1770
|
-
const ambiguousNames = /* @__PURE__ */ new Set();
|
|
1771
|
-
for (const entry of tree) {
|
|
1772
|
-
if (ambiguousNames.has(entry.name)) continue;
|
|
1773
|
-
const existingOwner = uniqueOwnerByName.get(entry.name);
|
|
1774
|
-
if (!existingOwner) {
|
|
1775
|
-
uniqueOwnerByName.set(entry.name, entry.owner);
|
|
1776
|
-
continue;
|
|
1777
|
-
}
|
|
1778
|
-
if (existingOwner !== entry.owner) {
|
|
1779
|
-
uniqueOwnerByName.delete(entry.name);
|
|
1780
|
-
ambiguousNames.add(entry.name);
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
const localDir = await detectUserSkillsDir(appServer);
|
|
1784
|
-
await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName, localDir);
|
|
1785
|
-
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
1786
|
-
const localSkills = await scanInstalledSkillsFromDisk();
|
|
1787
|
-
for (const skill of remote) {
|
|
1788
|
-
const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
|
|
1789
|
-
if (!owner) {
|
|
1790
|
-
continue;
|
|
1791
|
-
}
|
|
1792
|
-
if (!localSkills.has(skill.name)) {
|
|
1793
|
-
await runCommand("python3", [
|
|
1794
|
-
installerScript,
|
|
1795
|
-
"--repo",
|
|
1796
|
-
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
1797
|
-
"--path",
|
|
1798
|
-
`skills/${owner}/${skill.name}`,
|
|
1799
|
-
"--dest",
|
|
1800
|
-
localDir,
|
|
1801
|
-
"--method",
|
|
1802
|
-
"git"
|
|
1803
|
-
]);
|
|
1804
|
-
}
|
|
1805
|
-
const skillPath = join(localDir, skill.name);
|
|
1806
|
-
await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
|
|
1807
|
-
}
|
|
1808
|
-
const remoteNames = new Set(remote.map((row) => row.name));
|
|
1809
|
-
for (const [name, localInfo] of localSkills.entries()) {
|
|
1810
|
-
if (!remoteNames.has(name)) {
|
|
1811
|
-
await rm(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
const nextOwners = {};
|
|
1815
|
-
for (const item of remote) {
|
|
1816
|
-
const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
|
|
1817
|
-
if (owner) nextOwners[item.name] = owner;
|
|
1818
|
-
}
|
|
1819
|
-
await writeSkillsSyncState({ ...state, installedOwners: nextOwners });
|
|
1820
|
-
try {
|
|
1821
|
-
await appServer.rpc("skills/list", { forceReload: true });
|
|
1822
|
-
} catch {
|
|
1823
|
-
}
|
|
1824
|
-
setJson(res, 200, { ok: true, data: { synced: remote.length } });
|
|
1825
|
-
} catch (error) {
|
|
1826
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to pull synced skills") });
|
|
1827
|
-
}
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
|
|
1831
|
-
try {
|
|
1832
|
-
const owner = url.searchParams.get("owner") || "";
|
|
1833
|
-
const name = url.searchParams.get("name") || "";
|
|
1834
|
-
if (!owner || !name) {
|
|
1835
|
-
setJson(res, 400, { error: "Missing owner or name" });
|
|
1836
|
-
return;
|
|
1837
|
-
}
|
|
1838
|
-
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
|
|
1839
|
-
const resp = await fetch(rawUrl);
|
|
1840
|
-
if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
|
|
1841
|
-
const content = await resp.text();
|
|
1842
|
-
setJson(res, 200, { content });
|
|
1843
|
-
} catch (error) {
|
|
1844
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
|
|
1845
|
-
}
|
|
1846
|
-
return;
|
|
1847
|
-
}
|
|
1848
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
|
|
1849
|
-
try {
|
|
1850
|
-
const payload = asRecord(await readJsonBody(req));
|
|
1851
|
-
const owner = typeof payload?.owner === "string" ? payload.owner : "";
|
|
1852
|
-
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1853
|
-
if (!owner || !name) {
|
|
1854
|
-
setJson(res, 400, { error: "Missing owner or name" });
|
|
1855
|
-
return;
|
|
1856
|
-
}
|
|
1857
|
-
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
1858
|
-
const installDest = await detectUserSkillsDir(appServer);
|
|
1859
|
-
const skillPathInRepo = `skills/${owner}/${name}`;
|
|
1860
|
-
await runCommand("python3", [
|
|
1861
|
-
installerScript,
|
|
1862
|
-
"--repo",
|
|
1863
|
-
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
1864
|
-
"--path",
|
|
1865
|
-
skillPathInRepo,
|
|
1866
|
-
"--dest",
|
|
1867
|
-
installDest,
|
|
1868
|
-
"--method",
|
|
1869
|
-
"git"
|
|
1870
|
-
]);
|
|
1871
|
-
const skillDir = join(installDest, name);
|
|
1872
|
-
await ensureInstalledSkillIsValid(appServer, skillDir);
|
|
1873
|
-
const syncState = await readSkillsSyncState();
|
|
1874
|
-
const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
|
|
1875
|
-
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1876
|
-
await autoPushSyncedSkills(appServer);
|
|
1877
|
-
setJson(res, 200, { ok: true, path: skillDir });
|
|
1878
|
-
} catch (error) {
|
|
1879
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
|
|
1880
|
-
}
|
|
1881
|
-
return;
|
|
1882
|
-
}
|
|
1883
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
|
|
1884
|
-
try {
|
|
1885
|
-
const payload = asRecord(await readJsonBody(req));
|
|
1886
|
-
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1887
|
-
const path = typeof payload?.path === "string" ? payload.path : "";
|
|
1888
|
-
const target = path || (name ? join(getSkillsInstallDir(), name) : "");
|
|
1889
|
-
if (!target) {
|
|
1890
|
-
setJson(res, 400, { error: "Missing name or path" });
|
|
1891
|
-
return;
|
|
1892
|
-
}
|
|
1893
|
-
await rm(target, { recursive: true, force: true });
|
|
1894
|
-
if (name) {
|
|
1895
|
-
const syncState = await readSkillsSyncState();
|
|
1896
|
-
const nextOwners = { ...syncState.installedOwners ?? {} };
|
|
1897
|
-
delete nextOwners[name];
|
|
1898
|
-
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1899
|
-
}
|
|
1900
|
-
await autoPushSyncedSkills(appServer);
|
|
1901
|
-
try {
|
|
1902
|
-
await appServer.rpc("skills/list", { forceReload: true });
|
|
1903
|
-
} catch {
|
|
1904
|
-
}
|
|
1905
|
-
setJson(res, 200, { ok: true, deletedPath: target });
|
|
1906
|
-
} catch (error) {
|
|
1907
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to uninstall skill") });
|
|
1908
|
-
}
|
|
2232
|
+
setJson2(res, 200, { ok: true });
|
|
1909
2233
|
return;
|
|
1910
2234
|
}
|
|
1911
2235
|
if (req.method === "GET" && url.pathname === "/codex-api/events") {
|
|
@@ -1940,11 +2264,12 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1940
2264
|
}
|
|
1941
2265
|
next();
|
|
1942
2266
|
} catch (error) {
|
|
1943
|
-
const message =
|
|
1944
|
-
|
|
2267
|
+
const message = getErrorMessage2(error, "Unknown bridge error");
|
|
2268
|
+
setJson2(res, 502, { error: message });
|
|
1945
2269
|
}
|
|
1946
2270
|
};
|
|
1947
2271
|
middleware.dispose = () => {
|
|
2272
|
+
threadSearchIndex = null;
|
|
1948
2273
|
appServer.dispose();
|
|
1949
2274
|
};
|
|
1950
2275
|
middleware.subscribeNotifications = (listener) => {
|
|
@@ -1959,7 +2284,7 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1959
2284
|
}
|
|
1960
2285
|
|
|
1961
2286
|
// src/server/authMiddleware.ts
|
|
1962
|
-
import { randomBytes, timingSafeEqual } from "crypto";
|
|
2287
|
+
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
1963
2288
|
var TOKEN_COOKIE = "codex_web_local_token";
|
|
1964
2289
|
function constantTimeCompare(a, b) {
|
|
1965
2290
|
const bufA = Buffer.from(a);
|
|
@@ -2057,7 +2382,7 @@ function createAuthSession(password) {
|
|
|
2057
2382
|
res.status(401).json({ error: "Invalid password" });
|
|
2058
2383
|
return;
|
|
2059
2384
|
}
|
|
2060
|
-
const token =
|
|
2385
|
+
const token = randomBytes2(32).toString("hex");
|
|
2061
2386
|
validTokens.add(token);
|
|
2062
2387
|
res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
|
|
2063
2388
|
res.json({ ok: true });
|
|
@@ -2076,11 +2401,277 @@ function createAuthSession(password) {
|
|
|
2076
2401
|
};
|
|
2077
2402
|
}
|
|
2078
2403
|
|
|
2404
|
+
// src/server/localBrowseUi.ts
|
|
2405
|
+
import { dirname, extname, join as join3 } from "path";
|
|
2406
|
+
import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
2407
|
+
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2408
|
+
".txt",
|
|
2409
|
+
".md",
|
|
2410
|
+
".json",
|
|
2411
|
+
".js",
|
|
2412
|
+
".ts",
|
|
2413
|
+
".tsx",
|
|
2414
|
+
".jsx",
|
|
2415
|
+
".css",
|
|
2416
|
+
".scss",
|
|
2417
|
+
".html",
|
|
2418
|
+
".htm",
|
|
2419
|
+
".xml",
|
|
2420
|
+
".yml",
|
|
2421
|
+
".yaml",
|
|
2422
|
+
".log",
|
|
2423
|
+
".csv",
|
|
2424
|
+
".env",
|
|
2425
|
+
".py",
|
|
2426
|
+
".sh",
|
|
2427
|
+
".toml",
|
|
2428
|
+
".ini",
|
|
2429
|
+
".conf",
|
|
2430
|
+
".sql",
|
|
2431
|
+
".bat",
|
|
2432
|
+
".cmd",
|
|
2433
|
+
".ps1"
|
|
2434
|
+
]);
|
|
2435
|
+
function languageForPath(pathValue) {
|
|
2436
|
+
const extension = extname(pathValue).toLowerCase();
|
|
2437
|
+
switch (extension) {
|
|
2438
|
+
case ".js":
|
|
2439
|
+
return "javascript";
|
|
2440
|
+
case ".ts":
|
|
2441
|
+
return "typescript";
|
|
2442
|
+
case ".jsx":
|
|
2443
|
+
return "javascript";
|
|
2444
|
+
case ".tsx":
|
|
2445
|
+
return "typescript";
|
|
2446
|
+
case ".py":
|
|
2447
|
+
return "python";
|
|
2448
|
+
case ".sh":
|
|
2449
|
+
return "sh";
|
|
2450
|
+
case ".css":
|
|
2451
|
+
case ".scss":
|
|
2452
|
+
return "css";
|
|
2453
|
+
case ".html":
|
|
2454
|
+
case ".htm":
|
|
2455
|
+
return "html";
|
|
2456
|
+
case ".json":
|
|
2457
|
+
return "json";
|
|
2458
|
+
case ".md":
|
|
2459
|
+
return "markdown";
|
|
2460
|
+
case ".yaml":
|
|
2461
|
+
case ".yml":
|
|
2462
|
+
return "yaml";
|
|
2463
|
+
case ".xml":
|
|
2464
|
+
return "xml";
|
|
2465
|
+
case ".sql":
|
|
2466
|
+
return "sql";
|
|
2467
|
+
case ".toml":
|
|
2468
|
+
return "ini";
|
|
2469
|
+
case ".ini":
|
|
2470
|
+
case ".conf":
|
|
2471
|
+
return "ini";
|
|
2472
|
+
default:
|
|
2473
|
+
return "plaintext";
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
function normalizeLocalPath(rawPath) {
|
|
2477
|
+
const trimmed = rawPath.trim();
|
|
2478
|
+
if (!trimmed) return "";
|
|
2479
|
+
if (trimmed.startsWith("file://")) {
|
|
2480
|
+
try {
|
|
2481
|
+
return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
|
|
2482
|
+
} catch {
|
|
2483
|
+
return trimmed.replace(/^file:\/\//u, "");
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
return trimmed;
|
|
2487
|
+
}
|
|
2488
|
+
function decodeBrowsePath(rawPath) {
|
|
2489
|
+
if (!rawPath) return "";
|
|
2490
|
+
try {
|
|
2491
|
+
return decodeURIComponent(rawPath);
|
|
2492
|
+
} catch {
|
|
2493
|
+
return rawPath;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
function isTextEditablePath(pathValue) {
|
|
2497
|
+
return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
|
|
2498
|
+
}
|
|
2499
|
+
function looksLikeTextBuffer(buffer) {
|
|
2500
|
+
if (buffer.length === 0) return true;
|
|
2501
|
+
for (const byte of buffer) {
|
|
2502
|
+
if (byte === 0) return false;
|
|
2503
|
+
}
|
|
2504
|
+
const decoded = buffer.toString("utf8");
|
|
2505
|
+
const replacementCount = (decoded.match(/\uFFFD/gu) ?? []).length;
|
|
2506
|
+
return replacementCount / decoded.length < 0.05;
|
|
2507
|
+
}
|
|
2508
|
+
async function probeFileIsText(localPath) {
|
|
2509
|
+
const handle = await open(localPath, "r");
|
|
2510
|
+
try {
|
|
2511
|
+
const sample = Buffer.allocUnsafe(4096);
|
|
2512
|
+
const { bytesRead } = await handle.read(sample, 0, sample.length, 0);
|
|
2513
|
+
return looksLikeTextBuffer(sample.subarray(0, bytesRead));
|
|
2514
|
+
} finally {
|
|
2515
|
+
await handle.close();
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
async function isTextEditableFile(localPath) {
|
|
2519
|
+
if (isTextEditablePath(localPath)) return true;
|
|
2520
|
+
try {
|
|
2521
|
+
const fileStat = await stat3(localPath);
|
|
2522
|
+
if (!fileStat.isFile()) return false;
|
|
2523
|
+
return await probeFileIsText(localPath);
|
|
2524
|
+
} catch {
|
|
2525
|
+
return false;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
function escapeHtml(value) {
|
|
2529
|
+
return value.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">").replace(/"/gu, """).replace(/'/gu, "'");
|
|
2530
|
+
}
|
|
2531
|
+
function toBrowseHref(pathValue) {
|
|
2532
|
+
return `/codex-local-browse${encodeURI(pathValue)}`;
|
|
2533
|
+
}
|
|
2534
|
+
function toEditHref(pathValue) {
|
|
2535
|
+
return `/codex-local-edit${encodeURI(pathValue)}`;
|
|
2536
|
+
}
|
|
2537
|
+
function escapeForInlineScriptString(value) {
|
|
2538
|
+
return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
|
|
2539
|
+
}
|
|
2540
|
+
async function getDirectoryItems(localPath) {
|
|
2541
|
+
const entries = await readdir3(localPath, { withFileTypes: true });
|
|
2542
|
+
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
2543
|
+
const entryPath = join3(localPath, entry.name);
|
|
2544
|
+
const entryStat = await stat3(entryPath);
|
|
2545
|
+
const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
|
|
2546
|
+
return {
|
|
2547
|
+
name: entry.name,
|
|
2548
|
+
path: entryPath,
|
|
2549
|
+
isDirectory: entry.isDirectory(),
|
|
2550
|
+
editable,
|
|
2551
|
+
mtimeMs: entryStat.mtimeMs
|
|
2552
|
+
};
|
|
2553
|
+
}));
|
|
2554
|
+
return withMeta.sort((a, b) => {
|
|
2555
|
+
if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs;
|
|
2556
|
+
if (a.isDirectory && !b.isDirectory) return -1;
|
|
2557
|
+
if (!a.isDirectory && b.isDirectory) return 1;
|
|
2558
|
+
return a.name.localeCompare(b.name);
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
async function createDirectoryListingHtml(localPath) {
|
|
2562
|
+
const items = await getDirectoryItems(localPath);
|
|
2563
|
+
const parentPath = dirname(localPath);
|
|
2564
|
+
const rows = items.map((item) => {
|
|
2565
|
+
const suffix = item.isDirectory ? "/" : "";
|
|
2566
|
+
const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
|
|
2567
|
+
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
|
|
2568
|
+
}).join("\n");
|
|
2569
|
+
const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
|
|
2570
|
+
return `<!doctype html>
|
|
2571
|
+
<html lang="en">
|
|
2572
|
+
<head>
|
|
2573
|
+
<meta charset="utf-8" />
|
|
2574
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2575
|
+
<title>Index of ${escapeHtml(localPath)}</title>
|
|
2576
|
+
<style>
|
|
2577
|
+
body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
|
|
2578
|
+
a { color: #8cc2ff; text-decoration: none; }
|
|
2579
|
+
a:hover { text-decoration: underline; }
|
|
2580
|
+
ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
|
|
2581
|
+
.file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
|
|
2582
|
+
.file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
|
|
2583
|
+
.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; text-decoration: none; }
|
|
2584
|
+
.icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
|
|
2585
|
+
h1 { font-size: 18px; margin: 0; word-break: break-all; }
|
|
2586
|
+
@media (max-width: 640px) {
|
|
2587
|
+
body { margin: 12px; }
|
|
2588
|
+
.file-row { gap: 8px; }
|
|
2589
|
+
.file-link { font-size: 15px; padding: 12px; }
|
|
2590
|
+
.icon-btn { width: 44px; height: 44px; }
|
|
2591
|
+
}
|
|
2592
|
+
</style>
|
|
2593
|
+
</head>
|
|
2594
|
+
<body>
|
|
2595
|
+
<h1>Index of ${escapeHtml(localPath)}</h1>
|
|
2596
|
+
${parentLink}
|
|
2597
|
+
<ul>${rows}</ul>
|
|
2598
|
+
</body>
|
|
2599
|
+
</html>`;
|
|
2600
|
+
}
|
|
2601
|
+
async function createTextEditorHtml(localPath) {
|
|
2602
|
+
const content = await readFile3(localPath, "utf8");
|
|
2603
|
+
const parentPath = dirname(localPath);
|
|
2604
|
+
const language = languageForPath(localPath);
|
|
2605
|
+
const safeContentLiteral = escapeForInlineScriptString(content);
|
|
2606
|
+
return `<!doctype html>
|
|
2607
|
+
<html lang="en">
|
|
2608
|
+
<head>
|
|
2609
|
+
<meta charset="utf-8" />
|
|
2610
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2611
|
+
<title>Edit ${escapeHtml(localPath)}</title>
|
|
2612
|
+
<style>
|
|
2613
|
+
html, body { width: 100%; height: 100%; margin: 0; }
|
|
2614
|
+
body { font-family: ui-monospace, Menlo, Monaco, monospace; background: #0b1020; color: #dbe6ff; display: flex; flex-direction: column; overflow: hidden; }
|
|
2615
|
+
.toolbar { position: sticky; top: 0; z-index: 10; display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; background: #0b1020; border-bottom: 1px solid #243a5a; }
|
|
2616
|
+
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
2617
|
+
button, a { background: #1b2a4a; color: #dbe6ff; border: 1px solid #345; padding: 6px 10px; border-radius: 6px; text-decoration: none; cursor: pointer; }
|
|
2618
|
+
button:hover, a:hover { filter: brightness(1.08); }
|
|
2619
|
+
#editor { flex: 1 1 auto; min-height: 0; width: 100%; border: none; overflow: hidden; }
|
|
2620
|
+
#status { margin-left: 8px; color: #8cc2ff; }
|
|
2621
|
+
.ace_editor { background: #07101f !important; color: #dbe6ff !important; width: 100% !important; height: 100% !important; }
|
|
2622
|
+
.ace_gutter { background: #07101f !important; color: #6f8eb5 !important; }
|
|
2623
|
+
.ace_marker-layer .ace_active-line { background: #10213c !important; }
|
|
2624
|
+
.ace_marker-layer .ace_selection { background: rgba(140, 194, 255, 0.3) !important; }
|
|
2625
|
+
.meta { opacity: 0.9; font-size: 12px; overflow-wrap: anywhere; }
|
|
2626
|
+
</style>
|
|
2627
|
+
</head>
|
|
2628
|
+
<body>
|
|
2629
|
+
<div class="toolbar">
|
|
2630
|
+
<div class="row">
|
|
2631
|
+
<a href="${escapeHtml(toBrowseHref(parentPath))}">Back</a>
|
|
2632
|
+
<button id="saveBtn" type="button">Save</button>
|
|
2633
|
+
<span id="status"></span>
|
|
2634
|
+
</div>
|
|
2635
|
+
<div class="meta">${escapeHtml(localPath)} \xB7 ${escapeHtml(language)}</div>
|
|
2636
|
+
</div>
|
|
2637
|
+
<div id="editor"></div>
|
|
2638
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js"></script>
|
|
2639
|
+
<script>
|
|
2640
|
+
const saveBtn = document.getElementById('saveBtn');
|
|
2641
|
+
const status = document.getElementById('status');
|
|
2642
|
+
const editor = ace.edit('editor');
|
|
2643
|
+
editor.setTheme('ace/theme/tomorrow_night');
|
|
2644
|
+
editor.session.setMode('ace/mode/${escapeHtml(language)}');
|
|
2645
|
+
editor.setValue(${safeContentLiteral}, -1);
|
|
2646
|
+
editor.setOptions({
|
|
2647
|
+
fontSize: '13px',
|
|
2648
|
+
wrap: true,
|
|
2649
|
+
showPrintMargin: false,
|
|
2650
|
+
useSoftTabs: true,
|
|
2651
|
+
tabSize: 2,
|
|
2652
|
+
behavioursEnabled: true,
|
|
2653
|
+
});
|
|
2654
|
+
editor.resize();
|
|
2655
|
+
|
|
2656
|
+
saveBtn.addEventListener('click', async () => {
|
|
2657
|
+
status.textContent = 'Saving...';
|
|
2658
|
+
const response = await fetch(location.pathname, {
|
|
2659
|
+
method: 'PUT',
|
|
2660
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
2661
|
+
body: editor.getValue(),
|
|
2662
|
+
});
|
|
2663
|
+
status.textContent = response.ok ? 'Saved' : 'Save failed';
|
|
2664
|
+
});
|
|
2665
|
+
</script>
|
|
2666
|
+
</body>
|
|
2667
|
+
</html>`;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2079
2670
|
// src/server/httpServer.ts
|
|
2080
2671
|
import { WebSocketServer } from "ws";
|
|
2081
|
-
var __dirname =
|
|
2082
|
-
var distDir =
|
|
2083
|
-
var spaEntryFile =
|
|
2672
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
2673
|
+
var distDir = join4(__dirname, "..", "dist");
|
|
2674
|
+
var spaEntryFile = join4(distDir, "index.html");
|
|
2084
2675
|
var IMAGE_CONTENT_TYPES = {
|
|
2085
2676
|
".avif": "image/avif",
|
|
2086
2677
|
".bmp": "image/bmp",
|
|
@@ -2103,6 +2694,11 @@ function normalizeLocalImagePath(rawPath) {
|
|
|
2103
2694
|
}
|
|
2104
2695
|
return trimmed;
|
|
2105
2696
|
}
|
|
2697
|
+
function readWildcardPathParam(value) {
|
|
2698
|
+
if (typeof value === "string") return value;
|
|
2699
|
+
if (Array.isArray(value)) return value.join("/");
|
|
2700
|
+
return "";
|
|
2701
|
+
}
|
|
2106
2702
|
function createServer(options = {}) {
|
|
2107
2703
|
const app = express();
|
|
2108
2704
|
const bridge = createCodexBridgeMiddleware();
|
|
@@ -2118,7 +2714,7 @@ function createServer(options = {}) {
|
|
|
2118
2714
|
res.status(400).json({ error: "Expected absolute local file path." });
|
|
2119
2715
|
return;
|
|
2120
2716
|
}
|
|
2121
|
-
const contentType = IMAGE_CONTENT_TYPES[
|
|
2717
|
+
const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
|
|
2122
2718
|
if (!contentType) {
|
|
2123
2719
|
res.status(415).json({ error: "Unsupported image type." });
|
|
2124
2720
|
return;
|
|
@@ -2130,6 +2726,81 @@ function createServer(options = {}) {
|
|
|
2130
2726
|
if (!res.headersSent) res.status(404).json({ error: "Image file not found." });
|
|
2131
2727
|
});
|
|
2132
2728
|
});
|
|
2729
|
+
app.get("/codex-local-file", (req, res) => {
|
|
2730
|
+
const rawPath = typeof req.query.path === "string" ? req.query.path : "";
|
|
2731
|
+
const localPath = normalizeLocalPath(rawPath);
|
|
2732
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
2733
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
2737
|
+
res.setHeader("Content-Disposition", "inline");
|
|
2738
|
+
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
2739
|
+
if (!error) return;
|
|
2740
|
+
if (!res.headersSent) res.status(404).json({ error: "File not found." });
|
|
2741
|
+
});
|
|
2742
|
+
});
|
|
2743
|
+
app.get("/codex-local-browse/*path", async (req, res) => {
|
|
2744
|
+
const rawPath = readWildcardPathParam(req.params.path);
|
|
2745
|
+
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
2746
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
2747
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
try {
|
|
2751
|
+
const fileStat = await stat4(localPath);
|
|
2752
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
2753
|
+
if (fileStat.isDirectory()) {
|
|
2754
|
+
const html = await createDirectoryListingHtml(localPath);
|
|
2755
|
+
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
|
|
2759
|
+
if (!error) return;
|
|
2760
|
+
if (!res.headersSent) res.status(404).json({ error: "File not found." });
|
|
2761
|
+
});
|
|
2762
|
+
} catch {
|
|
2763
|
+
res.status(404).json({ error: "File not found." });
|
|
2764
|
+
}
|
|
2765
|
+
});
|
|
2766
|
+
app.get("/codex-local-edit/*path", async (req, res) => {
|
|
2767
|
+
const rawPath = readWildcardPathParam(req.params.path);
|
|
2768
|
+
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
2769
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
2770
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
try {
|
|
2774
|
+
const fileStat = await stat4(localPath);
|
|
2775
|
+
if (!fileStat.isFile()) {
|
|
2776
|
+
res.status(400).json({ error: "Expected file path." });
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
const html = await createTextEditorHtml(localPath);
|
|
2780
|
+
res.status(200).type("text/html; charset=utf-8").send(html);
|
|
2781
|
+
} catch {
|
|
2782
|
+
res.status(404).json({ error: "File not found." });
|
|
2783
|
+
}
|
|
2784
|
+
});
|
|
2785
|
+
app.put("/codex-local-edit/*path", express.text({ type: "*/*", limit: "10mb" }), async (req, res) => {
|
|
2786
|
+
const rawPath = readWildcardPathParam(req.params.path);
|
|
2787
|
+
const localPath = decodeBrowsePath(`/${rawPath}`);
|
|
2788
|
+
if (!localPath || !isAbsolute2(localPath)) {
|
|
2789
|
+
res.status(400).json({ error: "Expected absolute local file path." });
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
if (!await isTextEditableFile(localPath)) {
|
|
2793
|
+
res.status(415).json({ error: "Only text-like files are editable." });
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
const body = typeof req.body === "string" ? req.body : "";
|
|
2797
|
+
try {
|
|
2798
|
+
await writeFile3(localPath, body, "utf8");
|
|
2799
|
+
res.status(200).json({ ok: true });
|
|
2800
|
+
} catch {
|
|
2801
|
+
res.status(404).json({ error: "File not found." });
|
|
2802
|
+
}
|
|
2803
|
+
});
|
|
2133
2804
|
const hasFrontendAssets = existsSync2(spaEntryFile);
|
|
2134
2805
|
if (hasFrontendAssets) {
|
|
2135
2806
|
app.use(express.static(distDir));
|
|
@@ -2201,11 +2872,11 @@ function generatePassword() {
|
|
|
2201
2872
|
|
|
2202
2873
|
// src/cli/index.ts
|
|
2203
2874
|
var program = new Command().name("codexui").description("Web interface for Codex app-server");
|
|
2204
|
-
var __dirname2 =
|
|
2875
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
2205
2876
|
async function readCliVersion() {
|
|
2206
2877
|
try {
|
|
2207
|
-
const packageJsonPath =
|
|
2208
|
-
const raw = await
|
|
2878
|
+
const packageJsonPath = join5(__dirname2, "..", "package.json");
|
|
2879
|
+
const raw = await readFile4(packageJsonPath, "utf8");
|
|
2209
2880
|
const parsed = JSON.parse(raw);
|
|
2210
2881
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
2211
2882
|
} catch {
|
|
@@ -2230,13 +2901,13 @@ function runWithStatus(command, args) {
|
|
|
2230
2901
|
return result.status ?? -1;
|
|
2231
2902
|
}
|
|
2232
2903
|
function getUserNpmPrefix() {
|
|
2233
|
-
return
|
|
2904
|
+
return join5(homedir3(), ".npm-global");
|
|
2234
2905
|
}
|
|
2235
2906
|
function resolveCodexCommand() {
|
|
2236
2907
|
if (canRun("codex", ["--version"])) {
|
|
2237
2908
|
return "codex";
|
|
2238
2909
|
}
|
|
2239
|
-
const userCandidate =
|
|
2910
|
+
const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
|
|
2240
2911
|
if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
2241
2912
|
return userCandidate;
|
|
2242
2913
|
}
|
|
@@ -2244,15 +2915,113 @@ function resolveCodexCommand() {
|
|
|
2244
2915
|
if (!prefix) {
|
|
2245
2916
|
return null;
|
|
2246
2917
|
}
|
|
2247
|
-
const candidate =
|
|
2918
|
+
const candidate = join5(prefix, "bin", "codex");
|
|
2248
2919
|
if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
|
|
2249
2920
|
return candidate;
|
|
2250
2921
|
}
|
|
2251
2922
|
return null;
|
|
2252
2923
|
}
|
|
2924
|
+
function resolveCloudflaredCommand() {
|
|
2925
|
+
if (canRun("cloudflared", ["--version"])) {
|
|
2926
|
+
return "cloudflared";
|
|
2927
|
+
}
|
|
2928
|
+
const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
|
|
2929
|
+
if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
2930
|
+
return localCandidate;
|
|
2931
|
+
}
|
|
2932
|
+
return null;
|
|
2933
|
+
}
|
|
2934
|
+
function mapCloudflaredLinuxArch(arch) {
|
|
2935
|
+
if (arch === "x64") {
|
|
2936
|
+
return "amd64";
|
|
2937
|
+
}
|
|
2938
|
+
if (arch === "arm64") {
|
|
2939
|
+
return "arm64";
|
|
2940
|
+
}
|
|
2941
|
+
return null;
|
|
2942
|
+
}
|
|
2943
|
+
function downloadFile(url, destination) {
|
|
2944
|
+
return new Promise((resolve2, reject) => {
|
|
2945
|
+
const request = (currentUrl) => {
|
|
2946
|
+
httpsGet(currentUrl, (response) => {
|
|
2947
|
+
const code = response.statusCode ?? 0;
|
|
2948
|
+
if (code >= 300 && code < 400 && response.headers.location) {
|
|
2949
|
+
response.resume();
|
|
2950
|
+
request(response.headers.location);
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
if (code !== 200) {
|
|
2954
|
+
response.resume();
|
|
2955
|
+
reject(new Error(`Download failed with HTTP status ${String(code)}`));
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
const file = createWriteStream(destination, { mode: 493 });
|
|
2959
|
+
response.pipe(file);
|
|
2960
|
+
file.on("finish", () => {
|
|
2961
|
+
file.close();
|
|
2962
|
+
resolve2();
|
|
2963
|
+
});
|
|
2964
|
+
file.on("error", reject);
|
|
2965
|
+
}).on("error", reject);
|
|
2966
|
+
};
|
|
2967
|
+
request(url);
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
async function ensureCloudflaredInstalledLinux() {
|
|
2971
|
+
const current = resolveCloudflaredCommand();
|
|
2972
|
+
if (current) {
|
|
2973
|
+
return current;
|
|
2974
|
+
}
|
|
2975
|
+
if (process.platform !== "linux") {
|
|
2976
|
+
return null;
|
|
2977
|
+
}
|
|
2978
|
+
const mappedArch = mapCloudflaredLinuxArch(process.arch);
|
|
2979
|
+
if (!mappedArch) {
|
|
2980
|
+
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
2981
|
+
}
|
|
2982
|
+
const userBinDir = join5(homedir3(), ".local", "bin");
|
|
2983
|
+
mkdirSync(userBinDir, { recursive: true });
|
|
2984
|
+
const destination = join5(userBinDir, "cloudflared");
|
|
2985
|
+
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
2986
|
+
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
2987
|
+
await downloadFile(downloadUrl, destination);
|
|
2988
|
+
chmodSync(destination, 493);
|
|
2989
|
+
process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
|
|
2990
|
+
const installed = resolveCloudflaredCommand();
|
|
2991
|
+
if (!installed) {
|
|
2992
|
+
throw new Error("cloudflared download completed but executable is still not available");
|
|
2993
|
+
}
|
|
2994
|
+
console.log("\ncloudflared installed.\n");
|
|
2995
|
+
return installed;
|
|
2996
|
+
}
|
|
2997
|
+
async function shouldInstallCloudflaredInteractively() {
|
|
2998
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2999
|
+
console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
|
|
3000
|
+
return false;
|
|
3001
|
+
}
|
|
3002
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
3003
|
+
try {
|
|
3004
|
+
const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
|
|
3005
|
+
const normalized = answer.trim().toLowerCase();
|
|
3006
|
+
return normalized === "y" || normalized === "yes";
|
|
3007
|
+
} finally {
|
|
3008
|
+
prompt.close();
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
async function resolveCloudflaredForTunnel() {
|
|
3012
|
+
const current = resolveCloudflaredCommand();
|
|
3013
|
+
if (current) {
|
|
3014
|
+
return current;
|
|
3015
|
+
}
|
|
3016
|
+
const installApproved = await shouldInstallCloudflaredInteractively();
|
|
3017
|
+
if (!installApproved) {
|
|
3018
|
+
return null;
|
|
3019
|
+
}
|
|
3020
|
+
return ensureCloudflaredInstalledLinux();
|
|
3021
|
+
}
|
|
2253
3022
|
function hasCodexAuth() {
|
|
2254
|
-
const codexHome = process.env.CODEX_HOME?.trim() ||
|
|
2255
|
-
return existsSync3(
|
|
3023
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
|
|
3024
|
+
return existsSync3(join5(codexHome, "auth.json"));
|
|
2256
3025
|
}
|
|
2257
3026
|
function ensureCodexInstalled() {
|
|
2258
3027
|
let codexCommand = resolveCodexCommand();
|
|
@@ -2270,7 +3039,7 @@ function ensureCodexInstalled() {
|
|
|
2270
3039
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
2271
3040
|
`);
|
|
2272
3041
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
2273
|
-
process.env.PATH = `${
|
|
3042
|
+
process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
2274
3043
|
};
|
|
2275
3044
|
if (isTermuxRuntime()) {
|
|
2276
3045
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -2319,7 +3088,7 @@ function printTermuxKeepAlive(lines) {
|
|
|
2319
3088
|
}
|
|
2320
3089
|
function openBrowser(url) {
|
|
2321
3090
|
const command = process.platform === "darwin" ? { cmd: "open", args: [url] } : process.platform === "win32" ? { cmd: "cmd", args: ["/c", "start", "", url] } : { cmd: "xdg-open", args: [url] };
|
|
2322
|
-
const child =
|
|
3091
|
+
const child = spawn3(command.cmd, command.args, { detached: true, stdio: "ignore" });
|
|
2323
3092
|
child.on("error", () => {
|
|
2324
3093
|
});
|
|
2325
3094
|
child.unref();
|
|
@@ -2331,9 +3100,27 @@ function parseCloudflaredUrl(chunk) {
|
|
|
2331
3100
|
}
|
|
2332
3101
|
return urlMatch[urlMatch.length - 1] ?? null;
|
|
2333
3102
|
}
|
|
2334
|
-
|
|
3103
|
+
function getAccessibleUrls(port) {
|
|
3104
|
+
const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
|
|
3105
|
+
const interfaces = networkInterfaces();
|
|
3106
|
+
for (const entries of Object.values(interfaces)) {
|
|
3107
|
+
if (!entries) {
|
|
3108
|
+
continue;
|
|
3109
|
+
}
|
|
3110
|
+
for (const entry of entries) {
|
|
3111
|
+
if (entry.internal) {
|
|
3112
|
+
continue;
|
|
3113
|
+
}
|
|
3114
|
+
if (entry.family === "IPv4") {
|
|
3115
|
+
urls.add(`http://${entry.address}:${String(port)}`);
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
return Array.from(urls);
|
|
3120
|
+
}
|
|
3121
|
+
async function startCloudflaredTunnel(command, localPort) {
|
|
2335
3122
|
return new Promise((resolve2, reject) => {
|
|
2336
|
-
const child =
|
|
3123
|
+
const child = spawn3(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
|
|
2337
3124
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2338
3125
|
});
|
|
2339
3126
|
const timeout = setTimeout(() => {
|
|
@@ -2384,7 +3171,7 @@ function listenWithFallback(server, startPort) {
|
|
|
2384
3171
|
};
|
|
2385
3172
|
server.once("error", onError);
|
|
2386
3173
|
server.once("listening", onListening);
|
|
2387
|
-
server.listen(port);
|
|
3174
|
+
server.listen(port, "0.0.0.0");
|
|
2388
3175
|
};
|
|
2389
3176
|
attempt(startPort);
|
|
2390
3177
|
});
|
|
@@ -2406,7 +3193,11 @@ async function startServer(options) {
|
|
|
2406
3193
|
let tunnelUrl = null;
|
|
2407
3194
|
if (options.tunnel) {
|
|
2408
3195
|
try {
|
|
2409
|
-
const
|
|
3196
|
+
const cloudflaredCommand = await resolveCloudflaredForTunnel();
|
|
3197
|
+
if (!cloudflaredCommand) {
|
|
3198
|
+
throw new Error("cloudflared is not installed");
|
|
3199
|
+
}
|
|
3200
|
+
const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
|
|
2410
3201
|
tunnelChild = tunnel.process;
|
|
2411
3202
|
tunnelUrl = tunnel.url;
|
|
2412
3203
|
} catch (error) {
|
|
@@ -2421,8 +3212,15 @@ async function startServer(options) {
|
|
|
2421
3212
|
` Version: ${version}`,
|
|
2422
3213
|
" GitHub: https://github.com/friuns2/codexui",
|
|
2423
3214
|
"",
|
|
2424
|
-
`
|
|
3215
|
+
` Bind: http://0.0.0.0:${String(port)}`
|
|
2425
3216
|
];
|
|
3217
|
+
const accessUrls = getAccessibleUrls(port);
|
|
3218
|
+
if (accessUrls.length > 0) {
|
|
3219
|
+
lines.push(` Local: ${accessUrls[0]}`);
|
|
3220
|
+
for (const accessUrl of accessUrls.slice(1)) {
|
|
3221
|
+
lines.push(` Network: ${accessUrl}`);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
2426
3224
|
if (port !== requestedPort) {
|
|
2427
3225
|
lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
|
|
2428
3226
|
}
|
|
@@ -2431,9 +3229,7 @@ async function startServer(options) {
|
|
|
2431
3229
|
}
|
|
2432
3230
|
if (tunnelUrl) {
|
|
2433
3231
|
lines.push(` Tunnel: ${tunnelUrl}`);
|
|
2434
|
-
lines.push("");
|
|
2435
|
-
lines.push(" Tunnel QR code:");
|
|
2436
|
-
lines.push(` URL: ${tunnelUrl}`);
|
|
3232
|
+
lines.push(" Tunnel QR code below");
|
|
2437
3233
|
}
|
|
2438
3234
|
printTermuxKeepAlive(lines);
|
|
2439
3235
|
lines.push("");
|