codexapp 0.1.34 → 0.1.36
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-C62Rl1-Z.js +1444 -0
- package/dist/assets/index-Di9X0XLd.css +1 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +652 -601
- package/dist-cli/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/index-5eZebBiF.js +0 -1442
- package/dist/assets/index-zaSJxL5w.css +0 -1
package/dist-cli/index.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { createServer as createServer2 } from "http";
|
|
5
5
|
import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } from "fs";
|
|
6
|
-
import { readFile as
|
|
7
|
-
import { homedir as
|
|
8
|
-
import { join as
|
|
9
|
-
import { spawn as
|
|
6
|
+
import { readFile as 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
10
|
import { createInterface } from "readline/promises";
|
|
11
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12
12
|
import { dirname as dirname3 } from "path";
|
|
@@ -16,20 +16,27 @@ import qrcode from "qrcode-terminal";
|
|
|
16
16
|
|
|
17
17
|
// src/server/httpServer.ts
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
19
|
-
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as
|
|
19
|
+
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join4 } from "path";
|
|
20
20
|
import { existsSync as existsSync2 } from "fs";
|
|
21
|
-
import { writeFile as
|
|
21
|
+
import { writeFile as writeFile3, stat as stat4 } from "fs/promises";
|
|
22
22
|
import express from "express";
|
|
23
23
|
|
|
24
24
|
// src/server/codexAppServerBridge.ts
|
|
25
|
-
import { spawn } from "child_process";
|
|
25
|
+
import { spawn as spawn2 } from "child_process";
|
|
26
26
|
import { randomBytes } from "crypto";
|
|
27
|
+
import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as stat2 } from "fs/promises";
|
|
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
|
|
35
|
+
import { spawn } from "child_process";
|
|
27
36
|
import { mkdtemp, readFile, readdir, rm, mkdir, stat, lstat, readlink, symlink } from "fs/promises";
|
|
28
37
|
import { existsSync } from "fs";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { tmpdir } from "os";
|
|
32
|
-
import { basename, isAbsolute, join, resolve } from "path";
|
|
38
|
+
import { homedir, tmpdir } from "os";
|
|
39
|
+
import { join } from "path";
|
|
33
40
|
import { writeFile } from "fs/promises";
|
|
34
41
|
function asRecord(value) {
|
|
35
42
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -53,85 +60,6 @@ function setJson(res, statusCode, payload) {
|
|
|
53
60
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
54
61
|
res.end(JSON.stringify(payload));
|
|
55
62
|
}
|
|
56
|
-
function extractThreadMessageText(threadReadPayload) {
|
|
57
|
-
const payload = asRecord(threadReadPayload);
|
|
58
|
-
const thread = asRecord(payload?.thread);
|
|
59
|
-
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
60
|
-
const parts = [];
|
|
61
|
-
for (const turn of turns) {
|
|
62
|
-
const turnRecord = asRecord(turn);
|
|
63
|
-
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
64
|
-
for (const item of items) {
|
|
65
|
-
const itemRecord = asRecord(item);
|
|
66
|
-
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
67
|
-
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
68
|
-
parts.push(itemRecord.text.trim());
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (type === "userMessage") {
|
|
72
|
-
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
73
|
-
for (const block of content) {
|
|
74
|
-
const blockRecord = asRecord(block);
|
|
75
|
-
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
76
|
-
parts.push(blockRecord.text.trim());
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (type === "commandExecution") {
|
|
82
|
-
const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
|
|
83
|
-
const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
|
|
84
|
-
if (command) parts.push(command);
|
|
85
|
-
if (output) parts.push(output);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return parts.join("\n").trim();
|
|
90
|
-
}
|
|
91
|
-
function isExactPhraseMatch(query, doc) {
|
|
92
|
-
const q = query.trim().toLowerCase();
|
|
93
|
-
if (!q) return false;
|
|
94
|
-
return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
|
|
95
|
-
}
|
|
96
|
-
function scoreFileCandidate(path, query) {
|
|
97
|
-
if (!query) return 0;
|
|
98
|
-
const lowerPath = path.toLowerCase();
|
|
99
|
-
const lowerQuery = query.toLowerCase();
|
|
100
|
-
const baseName = lowerPath.slice(lowerPath.lastIndexOf("/") + 1);
|
|
101
|
-
if (baseName === lowerQuery) return 0;
|
|
102
|
-
if (baseName.startsWith(lowerQuery)) return 1;
|
|
103
|
-
if (baseName.includes(lowerQuery)) return 2;
|
|
104
|
-
if (lowerPath.includes(`/${lowerQuery}`)) return 3;
|
|
105
|
-
if (lowerPath.includes(lowerQuery)) return 4;
|
|
106
|
-
return 10;
|
|
107
|
-
}
|
|
108
|
-
async function listFilesWithRipgrep(cwd) {
|
|
109
|
-
return await new Promise((resolve2, reject) => {
|
|
110
|
-
const proc = spawn("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
|
|
111
|
-
cwd,
|
|
112
|
-
env: process.env,
|
|
113
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
114
|
-
});
|
|
115
|
-
let stdout = "";
|
|
116
|
-
let stderr = "";
|
|
117
|
-
proc.stdout.on("data", (chunk) => {
|
|
118
|
-
stdout += chunk.toString();
|
|
119
|
-
});
|
|
120
|
-
proc.stderr.on("data", (chunk) => {
|
|
121
|
-
stderr += chunk.toString();
|
|
122
|
-
});
|
|
123
|
-
proc.on("error", reject);
|
|
124
|
-
proc.on("close", (code) => {
|
|
125
|
-
if (code === 0) {
|
|
126
|
-
const rows = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
127
|
-
resolve2(rows);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
131
|
-
reject(new Error(details || "rg --files failed"));
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
63
|
function getCodexHomeDir() {
|
|
136
64
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
137
65
|
return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
|
|
@@ -166,55 +94,6 @@ async function runCommand(command, args, options = {}) {
|
|
|
166
94
|
});
|
|
167
95
|
});
|
|
168
96
|
}
|
|
169
|
-
function isMissingHeadError(error) {
|
|
170
|
-
const message = getErrorMessage(error, "").toLowerCase();
|
|
171
|
-
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
|
|
172
|
-
}
|
|
173
|
-
function isNotGitRepositoryError(error) {
|
|
174
|
-
const message = getErrorMessage(error, "").toLowerCase();
|
|
175
|
-
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
176
|
-
}
|
|
177
|
-
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
178
|
-
const agentsPath = join(repoRoot, "AGENTS.md");
|
|
179
|
-
try {
|
|
180
|
-
await stat(agentsPath);
|
|
181
|
-
} catch {
|
|
182
|
-
await writeFile(agentsPath, "", "utf8");
|
|
183
|
-
}
|
|
184
|
-
await runCommand("git", ["add", "AGENTS.md"], { cwd: repoRoot });
|
|
185
|
-
await runCommand(
|
|
186
|
-
"git",
|
|
187
|
-
["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
|
|
188
|
-
{ cwd: repoRoot }
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
async function runCommandCapture(command, args, options = {}) {
|
|
192
|
-
return await new Promise((resolve2, reject) => {
|
|
193
|
-
const proc = spawn(command, args, {
|
|
194
|
-
cwd: options.cwd,
|
|
195
|
-
env: process.env,
|
|
196
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
197
|
-
});
|
|
198
|
-
let stdout = "";
|
|
199
|
-
let stderr = "";
|
|
200
|
-
proc.stdout.on("data", (chunk) => {
|
|
201
|
-
stdout += chunk.toString();
|
|
202
|
-
});
|
|
203
|
-
proc.stderr.on("data", (chunk) => {
|
|
204
|
-
stderr += chunk.toString();
|
|
205
|
-
});
|
|
206
|
-
proc.on("error", reject);
|
|
207
|
-
proc.on("close", (code) => {
|
|
208
|
-
if (code === 0) {
|
|
209
|
-
resolve2(stdout.trim());
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
213
|
-
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
214
|
-
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
97
|
async function runCommandWithOutput(command, args, options = {}) {
|
|
219
98
|
return await new Promise((resolve2, reject) => {
|
|
220
99
|
const proc = spawn(command, args, {
|
|
@@ -325,7 +204,7 @@ async function fetchMetaBatch(entries) {
|
|
|
325
204
|
const toFetch = entries.filter((e) => !metaCache.has(`${e.owner}/${e.name}`));
|
|
326
205
|
if (toFetch.length === 0) return;
|
|
327
206
|
const batch = toFetch.slice(0, 50);
|
|
328
|
-
|
|
207
|
+
await Promise.allSettled(
|
|
329
208
|
batch.map(async (e) => {
|
|
330
209
|
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${e.owner}/${e.name}/_meta.json`;
|
|
331
210
|
const resp = await fetch(rawUrl);
|
|
@@ -338,7 +217,6 @@ async function fetchMetaBatch(entries) {
|
|
|
338
217
|
});
|
|
339
218
|
})
|
|
340
219
|
);
|
|
341
|
-
void results;
|
|
342
220
|
}
|
|
343
221
|
function buildHubEntry(e) {
|
|
344
222
|
const cached = metaCache.get(`${e.owner}/${e.name}`);
|
|
@@ -467,8 +345,7 @@ function isAndroidLikeRuntime() {
|
|
|
467
345
|
const prefix = process.env.PREFIX?.toLowerCase() ?? "";
|
|
468
346
|
if (prefix.includes("/com.termux/")) return true;
|
|
469
347
|
const proot = process.env.PROOT_TMP_DIR?.toLowerCase() ?? "";
|
|
470
|
-
|
|
471
|
-
return false;
|
|
348
|
+
return proot.length > 0;
|
|
472
349
|
}
|
|
473
350
|
function getPreferredSyncBranch() {
|
|
474
351
|
return isAndroidLikeRuntime() ? "android" : "main";
|
|
@@ -732,23 +609,21 @@ async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _inst
|
|
|
732
609
|
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
733
610
|
const branch = getPreferredSyncBranch();
|
|
734
611
|
const repoDir = await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
735
|
-
const addPaths = ["."];
|
|
736
612
|
void _installedMap;
|
|
737
613
|
await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: repoDir });
|
|
738
614
|
await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: repoDir });
|
|
739
|
-
await runCommand("git", ["add",
|
|
615
|
+
await runCommand("git", ["add", "."], { cwd: repoDir });
|
|
740
616
|
const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
|
|
741
617
|
if (!status) return;
|
|
742
618
|
await runCommand("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
|
|
743
619
|
await runCommand("git", ["push", "origin", `HEAD:${branch}`], { cwd: repoDir });
|
|
744
620
|
}
|
|
745
|
-
async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName
|
|
621
|
+
async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName) {
|
|
746
622
|
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
747
623
|
const branch = getPreferredSyncBranch();
|
|
748
624
|
await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
749
625
|
}
|
|
750
|
-
async function bootstrapSkillsFromUpstreamIntoLocal(
|
|
751
|
-
void _localSkillsDir;
|
|
626
|
+
async function bootstrapSkillsFromUpstreamIntoLocal() {
|
|
752
627
|
const repoUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
|
|
753
628
|
const branch = getPreferredSyncBranch();
|
|
754
629
|
await ensureSkillsWorkingTreeRepo(repoUrl, branch);
|
|
@@ -857,7 +732,6 @@ async function initializeSkillsSyncOnStartup(appServer) {
|
|
|
857
732
|
startupSyncStatus.branch = getPreferredSyncBranch();
|
|
858
733
|
try {
|
|
859
734
|
const state = await readSkillsSyncState();
|
|
860
|
-
const localSkillsDir = getSkillsInstallDir();
|
|
861
735
|
if (!state.githubToken) {
|
|
862
736
|
await ensureCodexAgentsSymlinkToSkillsAgents();
|
|
863
737
|
if (!isAndroidLikeRuntime()) {
|
|
@@ -868,7 +742,7 @@ async function initializeSkillsSyncOnStartup(appServer) {
|
|
|
868
742
|
}
|
|
869
743
|
startupSyncStatus.mode = "unauthenticated-bootstrap";
|
|
870
744
|
startupSyncStatus.lastAction = "pull-upstream";
|
|
871
|
-
await bootstrapSkillsFromUpstreamIntoLocal(
|
|
745
|
+
await bootstrapSkillsFromUpstreamIntoLocal();
|
|
872
746
|
try {
|
|
873
747
|
await appServer.rpc("skills/list", { forceReload: true });
|
|
874
748
|
} catch {
|
|
@@ -882,10 +756,9 @@ async function initializeSkillsSyncOnStartup(appServer) {
|
|
|
882
756
|
const username = state.githubUsername || await resolveGithubUsername(state.githubToken);
|
|
883
757
|
const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
|
|
884
758
|
await ensurePrivateForkFromUpstream(state.githubToken, username, repoName);
|
|
885
|
-
|
|
886
|
-
await writeSkillsSyncState(nextState);
|
|
759
|
+
await writeSkillsSyncState({ ...state, githubUsername: username, repoOwner: username, repoName });
|
|
887
760
|
startupSyncStatus.lastAction = "pull-private-fork";
|
|
888
|
-
await pullInstalledSkillsFolderFromRepo(state.githubToken, username, repoName
|
|
761
|
+
await pullInstalledSkillsFolderFromRepo(state.githubToken, username, repoName);
|
|
889
762
|
try {
|
|
890
763
|
await appServer.rpc("skills/list", { forceReload: true });
|
|
891
764
|
} catch {
|
|
@@ -905,15 +778,8 @@ async function finalizeGithubLoginAndSync(token, username, appServer) {
|
|
|
905
778
|
const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
|
|
906
779
|
await ensurePrivateForkFromUpstream(token, username, repoName);
|
|
907
780
|
const current = await readSkillsSyncState();
|
|
908
|
-
await writeSkillsSyncState({
|
|
909
|
-
|
|
910
|
-
githubToken: token,
|
|
911
|
-
githubUsername: username,
|
|
912
|
-
repoOwner: username,
|
|
913
|
-
repoName
|
|
914
|
-
});
|
|
915
|
-
const localDir = getSkillsInstallDir();
|
|
916
|
-
await pullInstalledSkillsFolderFromRepo(token, username, repoName, localDir);
|
|
781
|
+
await writeSkillsSyncState({ ...current, githubToken: token, githubUsername: username, repoOwner: username, repoName });
|
|
782
|
+
await pullInstalledSkillsFolderFromRepo(token, username, repoName);
|
|
917
783
|
try {
|
|
918
784
|
await appServer.rpc("skills/list", { forceReload: true });
|
|
919
785
|
} catch {
|
|
@@ -922,11 +788,10 @@ async function finalizeGithubLoginAndSync(token, username, appServer) {
|
|
|
922
788
|
}
|
|
923
789
|
async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
|
|
924
790
|
const q = query.toLowerCase().trim();
|
|
925
|
-
|
|
791
|
+
const filtered = q ? allEntries.filter((s) => {
|
|
926
792
|
if (s.name.toLowerCase().includes(q) || s.owner.toLowerCase().includes(q)) return true;
|
|
927
793
|
const cached = metaCache.get(`${s.owner}/${s.name}`);
|
|
928
|
-
|
|
929
|
-
return false;
|
|
794
|
+
return Boolean(cached?.displayName?.toLowerCase().includes(q));
|
|
930
795
|
}) : allEntries;
|
|
931
796
|
const page = filtered.slice(0, Math.min(limit * 2, 200));
|
|
932
797
|
await fetchMetaBatch(page);
|
|
@@ -946,6 +811,494 @@ async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
|
|
|
946
811
|
return local ? { ...s, installed: true, path: local.path, enabled: local.enabled } : s;
|
|
947
812
|
});
|
|
948
813
|
}
|
|
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") });
|
|
855
|
+
}
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
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;
|
|
879
|
+
}
|
|
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
|
+
}
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
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;
|
|
904
|
+
}
|
|
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;
|
|
920
|
+
}
|
|
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;
|
|
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") });
|
|
940
|
+
}
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
|
|
944
|
+
try {
|
|
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;
|
|
949
|
+
}
|
|
950
|
+
if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
|
|
951
|
+
setJson(res, 400, { error: "Refusing to push to upstream repository" });
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
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;
|
|
975
|
+
}
|
|
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
|
+
}
|
|
991
|
+
}
|
|
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
|
+
}
|
|
949
1302
|
function normalizeStringArray(value) {
|
|
950
1303
|
if (!Array.isArray(value)) return [];
|
|
951
1304
|
const normalized = [];
|
|
@@ -967,11 +1320,11 @@ function normalizeStringRecord(value) {
|
|
|
967
1320
|
return next;
|
|
968
1321
|
}
|
|
969
1322
|
function getCodexAuthPath() {
|
|
970
|
-
return
|
|
1323
|
+
return join2(getCodexHomeDir2(), "auth.json");
|
|
971
1324
|
}
|
|
972
1325
|
async function readCodexAuth() {
|
|
973
1326
|
try {
|
|
974
|
-
const raw = await
|
|
1327
|
+
const raw = await readFile2(getCodexAuthPath(), "utf8");
|
|
975
1328
|
const auth = JSON.parse(raw);
|
|
976
1329
|
const token = auth.tokens?.access_token;
|
|
977
1330
|
if (!token) return null;
|
|
@@ -981,13 +1334,13 @@ async function readCodexAuth() {
|
|
|
981
1334
|
}
|
|
982
1335
|
}
|
|
983
1336
|
function getCodexGlobalStatePath() {
|
|
984
|
-
return
|
|
1337
|
+
return join2(getCodexHomeDir2(), ".codex-global-state.json");
|
|
985
1338
|
}
|
|
986
1339
|
var MAX_THREAD_TITLES = 500;
|
|
987
1340
|
function normalizeThreadTitleCache(value) {
|
|
988
|
-
const record =
|
|
1341
|
+
const record = asRecord2(value);
|
|
989
1342
|
if (!record) return { titles: {}, order: [] };
|
|
990
|
-
const rawTitles =
|
|
1343
|
+
const rawTitles = asRecord2(record.titles);
|
|
991
1344
|
const titles = {};
|
|
992
1345
|
if (rawTitles) {
|
|
993
1346
|
for (const [k, v] of Object.entries(rawTitles)) {
|
|
@@ -1013,8 +1366,8 @@ function removeFromThreadTitleCache(cache, id) {
|
|
|
1013
1366
|
async function readThreadTitleCache() {
|
|
1014
1367
|
const statePath = getCodexGlobalStatePath();
|
|
1015
1368
|
try {
|
|
1016
|
-
const raw = await
|
|
1017
|
-
const payload =
|
|
1369
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1370
|
+
const payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
1018
1371
|
return normalizeThreadTitleCache(payload["thread-titles"]);
|
|
1019
1372
|
} catch {
|
|
1020
1373
|
return { titles: {}, order: [] };
|
|
@@ -1024,21 +1377,21 @@ async function writeThreadTitleCache(cache) {
|
|
|
1024
1377
|
const statePath = getCodexGlobalStatePath();
|
|
1025
1378
|
let payload = {};
|
|
1026
1379
|
try {
|
|
1027
|
-
const raw = await
|
|
1028
|
-
payload =
|
|
1380
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1381
|
+
payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
1029
1382
|
} catch {
|
|
1030
1383
|
payload = {};
|
|
1031
1384
|
}
|
|
1032
1385
|
payload["thread-titles"] = cache;
|
|
1033
|
-
await
|
|
1386
|
+
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
1034
1387
|
}
|
|
1035
1388
|
async function readWorkspaceRootsState() {
|
|
1036
1389
|
const statePath = getCodexGlobalStatePath();
|
|
1037
1390
|
let payload = {};
|
|
1038
1391
|
try {
|
|
1039
|
-
const raw = await
|
|
1392
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1040
1393
|
const parsed = JSON.parse(raw);
|
|
1041
|
-
payload =
|
|
1394
|
+
payload = asRecord2(parsed) ?? {};
|
|
1042
1395
|
} catch {
|
|
1043
1396
|
payload = {};
|
|
1044
1397
|
}
|
|
@@ -1052,15 +1405,15 @@ async function writeWorkspaceRootsState(nextState) {
|
|
|
1052
1405
|
const statePath = getCodexGlobalStatePath();
|
|
1053
1406
|
let payload = {};
|
|
1054
1407
|
try {
|
|
1055
|
-
const raw = await
|
|
1056
|
-
payload =
|
|
1408
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1409
|
+
payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
1057
1410
|
} catch {
|
|
1058
1411
|
payload = {};
|
|
1059
1412
|
}
|
|
1060
1413
|
payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
|
|
1061
1414
|
payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
|
|
1062
1415
|
payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
|
|
1063
|
-
await
|
|
1416
|
+
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
1064
1417
|
}
|
|
1065
1418
|
async function readJsonBody(req) {
|
|
1066
1419
|
const raw = await readRawBody(req);
|
|
@@ -1098,7 +1451,7 @@ function handleFileUpload(req, res) {
|
|
|
1098
1451
|
const contentType = req.headers["content-type"] ?? "";
|
|
1099
1452
|
const boundaryMatch = contentType.match(/boundary=(.+)/i);
|
|
1100
1453
|
if (!boundaryMatch) {
|
|
1101
|
-
|
|
1454
|
+
setJson2(res, 400, { error: "Missing multipart boundary" });
|
|
1102
1455
|
return;
|
|
1103
1456
|
}
|
|
1104
1457
|
const boundary = boundaryMatch[1];
|
|
@@ -1128,21 +1481,21 @@ function handleFileUpload(req, res) {
|
|
|
1128
1481
|
break;
|
|
1129
1482
|
}
|
|
1130
1483
|
if (!fileData) {
|
|
1131
|
-
|
|
1484
|
+
setJson2(res, 400, { error: "No file in request" });
|
|
1132
1485
|
return;
|
|
1133
1486
|
}
|
|
1134
|
-
const uploadDir =
|
|
1135
|
-
await
|
|
1136
|
-
const destDir = await
|
|
1137
|
-
const destPath =
|
|
1138
|
-
await
|
|
1139
|
-
|
|
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 });
|
|
1140
1493
|
} catch (err) {
|
|
1141
|
-
|
|
1494
|
+
setJson2(res, 500, { error: getErrorMessage2(err, "Upload failed") });
|
|
1142
1495
|
}
|
|
1143
1496
|
});
|
|
1144
1497
|
req.on("error", (err) => {
|
|
1145
|
-
|
|
1498
|
+
setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
|
|
1146
1499
|
});
|
|
1147
1500
|
}
|
|
1148
1501
|
async function proxyTranscribe(body, contentType, authToken, accountId) {
|
|
@@ -1194,7 +1547,7 @@ var AppServerProcess = class {
|
|
|
1194
1547
|
start() {
|
|
1195
1548
|
if (this.process) return;
|
|
1196
1549
|
this.stopping = false;
|
|
1197
|
-
const proc =
|
|
1550
|
+
const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
1198
1551
|
this.process = proc;
|
|
1199
1552
|
proc.stdout.setEncoding("utf8");
|
|
1200
1553
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -1288,7 +1641,7 @@ var AppServerProcess = class {
|
|
|
1288
1641
|
}
|
|
1289
1642
|
this.pendingServerRequests.delete(requestId);
|
|
1290
1643
|
this.sendServerRequestReply(requestId, reply);
|
|
1291
|
-
const requestParams =
|
|
1644
|
+
const requestParams = asRecord2(pendingRequest.params);
|
|
1292
1645
|
const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
|
|
1293
1646
|
this.emitNotification({
|
|
1294
1647
|
method: "server/request/resolved",
|
|
@@ -1357,7 +1710,7 @@ var AppServerProcess = class {
|
|
|
1357
1710
|
}
|
|
1358
1711
|
async respondToServerRequest(payload) {
|
|
1359
1712
|
await this.ensureInitialized();
|
|
1360
|
-
const body =
|
|
1713
|
+
const body = asRecord2(payload);
|
|
1361
1714
|
if (!body) {
|
|
1362
1715
|
throw new Error("Invalid response payload: expected object");
|
|
1363
1716
|
}
|
|
@@ -1365,7 +1718,7 @@ var AppServerProcess = class {
|
|
|
1365
1718
|
if (typeof id !== "number" || !Number.isInteger(id)) {
|
|
1366
1719
|
throw new Error('Invalid response payload: "id" must be an integer');
|
|
1367
1720
|
}
|
|
1368
|
-
const rawError =
|
|
1721
|
+
const rawError = asRecord2(body.error);
|
|
1369
1722
|
if (rawError) {
|
|
1370
1723
|
const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
|
|
1371
1724
|
const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
|
|
@@ -1420,7 +1773,7 @@ var MethodCatalog = class {
|
|
|
1420
1773
|
}
|
|
1421
1774
|
async runGenerateSchemaCommand(outDir) {
|
|
1422
1775
|
await new Promise((resolve2, reject) => {
|
|
1423
|
-
const process2 =
|
|
1776
|
+
const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
|
|
1424
1777
|
stdio: ["ignore", "ignore", "pipe"]
|
|
1425
1778
|
});
|
|
1426
1779
|
let stderr = "";
|
|
@@ -1439,13 +1792,13 @@ var MethodCatalog = class {
|
|
|
1439
1792
|
});
|
|
1440
1793
|
}
|
|
1441
1794
|
extractMethodsFromClientRequest(payload) {
|
|
1442
|
-
const root =
|
|
1795
|
+
const root = asRecord2(payload);
|
|
1443
1796
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
1444
1797
|
const methods = /* @__PURE__ */ new Set();
|
|
1445
1798
|
for (const entry of oneOf) {
|
|
1446
|
-
const row =
|
|
1447
|
-
const properties =
|
|
1448
|
-
const methodDef =
|
|
1799
|
+
const row = asRecord2(entry);
|
|
1800
|
+
const properties = asRecord2(row?.properties);
|
|
1801
|
+
const methodDef = asRecord2(properties?.method);
|
|
1449
1802
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
1450
1803
|
for (const item of methodEnum) {
|
|
1451
1804
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -1456,13 +1809,13 @@ var MethodCatalog = class {
|
|
|
1456
1809
|
return Array.from(methods).sort((a, b) => a.localeCompare(b));
|
|
1457
1810
|
}
|
|
1458
1811
|
extractMethodsFromServerNotification(payload) {
|
|
1459
|
-
const root =
|
|
1812
|
+
const root = asRecord2(payload);
|
|
1460
1813
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
1461
1814
|
const methods = /* @__PURE__ */ new Set();
|
|
1462
1815
|
for (const entry of oneOf) {
|
|
1463
|
-
const row =
|
|
1464
|
-
const properties =
|
|
1465
|
-
const methodDef =
|
|
1816
|
+
const row = asRecord2(entry);
|
|
1817
|
+
const properties = asRecord2(row?.properties);
|
|
1818
|
+
const methodDef = asRecord2(properties?.method);
|
|
1466
1819
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
1467
1820
|
for (const item of methodEnum) {
|
|
1468
1821
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -1476,10 +1829,10 @@ var MethodCatalog = class {
|
|
|
1476
1829
|
if (this.methodCache) {
|
|
1477
1830
|
return this.methodCache;
|
|
1478
1831
|
}
|
|
1479
|
-
const outDir = await
|
|
1832
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
1480
1833
|
await this.runGenerateSchemaCommand(outDir);
|
|
1481
|
-
const clientRequestPath =
|
|
1482
|
-
const raw = await
|
|
1834
|
+
const clientRequestPath = join2(outDir, "ClientRequest.json");
|
|
1835
|
+
const raw = await readFile2(clientRequestPath, "utf8");
|
|
1483
1836
|
const parsed = JSON.parse(raw);
|
|
1484
1837
|
const methods = this.extractMethodsFromClientRequest(parsed);
|
|
1485
1838
|
this.methodCache = methods;
|
|
@@ -1489,10 +1842,10 @@ var MethodCatalog = class {
|
|
|
1489
1842
|
if (this.notificationCache) {
|
|
1490
1843
|
return this.notificationCache;
|
|
1491
1844
|
}
|
|
1492
|
-
const outDir = await
|
|
1845
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
1493
1846
|
await this.runGenerateSchemaCommand(outDir);
|
|
1494
|
-
const serverNotificationPath =
|
|
1495
|
-
const raw = await
|
|
1847
|
+
const serverNotificationPath = join2(outDir, "ServerNotification.json");
|
|
1848
|
+
const raw = await readFile2(serverNotificationPath, "utf8");
|
|
1496
1849
|
const parsed = JSON.parse(raw);
|
|
1497
1850
|
const methods = this.extractMethodsFromServerNotification(parsed);
|
|
1498
1851
|
this.notificationCache = methods;
|
|
@@ -1515,7 +1868,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
1515
1868
|
const threads = [];
|
|
1516
1869
|
let cursor = null;
|
|
1517
1870
|
do {
|
|
1518
|
-
const response =
|
|
1871
|
+
const response = asRecord2(await appServer.rpc("thread/list", {
|
|
1519
1872
|
archived: false,
|
|
1520
1873
|
limit: 100,
|
|
1521
1874
|
sortKey: "updated_at",
|
|
@@ -1523,7 +1876,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
1523
1876
|
}));
|
|
1524
1877
|
const data = Array.isArray(response?.data) ? response.data : [];
|
|
1525
1878
|
for (const row of data) {
|
|
1526
|
-
const record =
|
|
1879
|
+
const record = asRecord2(row);
|
|
1527
1880
|
const id = typeof record?.id === "string" ? record.id : "";
|
|
1528
1881
|
if (!id) continue;
|
|
1529
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";
|
|
@@ -1595,25 +1948,28 @@ function createCodexBridgeMiddleware() {
|
|
|
1595
1948
|
return;
|
|
1596
1949
|
}
|
|
1597
1950
|
const url = new URL(req.url, "http://localhost");
|
|
1951
|
+
if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1598
1954
|
if (req.method === "POST" && url.pathname === "/codex-api/upload-file") {
|
|
1599
1955
|
handleFileUpload(req, res);
|
|
1600
1956
|
return;
|
|
1601
1957
|
}
|
|
1602
1958
|
if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
|
|
1603
1959
|
const payload = await readJsonBody(req);
|
|
1604
|
-
const body =
|
|
1960
|
+
const body = asRecord2(payload);
|
|
1605
1961
|
if (!body || typeof body.method !== "string" || body.method.length === 0) {
|
|
1606
|
-
|
|
1962
|
+
setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
|
|
1607
1963
|
return;
|
|
1608
1964
|
}
|
|
1609
1965
|
const result = await appServer.rpc(body.method, body.params ?? null);
|
|
1610
|
-
|
|
1966
|
+
setJson2(res, 200, { result });
|
|
1611
1967
|
return;
|
|
1612
1968
|
}
|
|
1613
1969
|
if (req.method === "POST" && url.pathname === "/codex-api/transcribe") {
|
|
1614
1970
|
const auth = await readCodexAuth();
|
|
1615
1971
|
if (!auth) {
|
|
1616
|
-
|
|
1972
|
+
setJson2(res, 401, { error: "No auth token available for transcription" });
|
|
1617
1973
|
return;
|
|
1618
1974
|
}
|
|
1619
1975
|
const rawBody = await readRawBody(req);
|
|
@@ -1627,48 +1983,48 @@ function createCodexBridgeMiddleware() {
|
|
|
1627
1983
|
if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
|
|
1628
1984
|
const payload = await readJsonBody(req);
|
|
1629
1985
|
await appServer.respondToServerRequest(payload);
|
|
1630
|
-
|
|
1986
|
+
setJson2(res, 200, { ok: true });
|
|
1631
1987
|
return;
|
|
1632
1988
|
}
|
|
1633
1989
|
if (req.method === "GET" && url.pathname === "/codex-api/server-requests/pending") {
|
|
1634
|
-
|
|
1990
|
+
setJson2(res, 200, { data: appServer.listPendingServerRequests() });
|
|
1635
1991
|
return;
|
|
1636
1992
|
}
|
|
1637
1993
|
if (req.method === "GET" && url.pathname === "/codex-api/meta/methods") {
|
|
1638
1994
|
const methods = await methodCatalog.listMethods();
|
|
1639
|
-
|
|
1995
|
+
setJson2(res, 200, { data: methods });
|
|
1640
1996
|
return;
|
|
1641
1997
|
}
|
|
1642
1998
|
if (req.method === "GET" && url.pathname === "/codex-api/meta/notifications") {
|
|
1643
1999
|
const methods = await methodCatalog.listNotificationMethods();
|
|
1644
|
-
|
|
2000
|
+
setJson2(res, 200, { data: methods });
|
|
1645
2001
|
return;
|
|
1646
2002
|
}
|
|
1647
2003
|
if (req.method === "GET" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1648
2004
|
const state = await readWorkspaceRootsState();
|
|
1649
|
-
|
|
2005
|
+
setJson2(res, 200, { data: state });
|
|
1650
2006
|
return;
|
|
1651
2007
|
}
|
|
1652
2008
|
if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
|
|
1653
|
-
|
|
2009
|
+
setJson2(res, 200, { data: { path: homedir2() } });
|
|
1654
2010
|
return;
|
|
1655
2011
|
}
|
|
1656
2012
|
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
1657
|
-
const payload =
|
|
2013
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1658
2014
|
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
1659
2015
|
if (!rawSourceCwd) {
|
|
1660
|
-
|
|
2016
|
+
setJson2(res, 400, { error: "Missing sourceCwd" });
|
|
1661
2017
|
return;
|
|
1662
2018
|
}
|
|
1663
2019
|
const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
|
|
1664
2020
|
try {
|
|
1665
|
-
const sourceInfo = await
|
|
2021
|
+
const sourceInfo = await stat2(sourceCwd);
|
|
1666
2022
|
if (!sourceInfo.isDirectory()) {
|
|
1667
|
-
|
|
2023
|
+
setJson2(res, 400, { error: "sourceCwd is not a directory" });
|
|
1668
2024
|
return;
|
|
1669
2025
|
}
|
|
1670
2026
|
} catch {
|
|
1671
|
-
|
|
2027
|
+
setJson2(res, 404, { error: "sourceCwd does not exist" });
|
|
1672
2028
|
return;
|
|
1673
2029
|
}
|
|
1674
2030
|
try {
|
|
@@ -1677,25 +2033,25 @@ function createCodexBridgeMiddleware() {
|
|
|
1677
2033
|
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1678
2034
|
} catch (error) {
|
|
1679
2035
|
if (!isNotGitRepositoryError(error)) throw error;
|
|
1680
|
-
await
|
|
2036
|
+
await runCommand2("git", ["init"], { cwd: sourceCwd });
|
|
1681
2037
|
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1682
2038
|
}
|
|
1683
2039
|
const repoName = basename(gitRoot) || "repo";
|
|
1684
|
-
const worktreesRoot =
|
|
1685
|
-
await
|
|
2040
|
+
const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
|
|
2041
|
+
await mkdir2(worktreesRoot, { recursive: true });
|
|
1686
2042
|
let worktreeId = "";
|
|
1687
2043
|
let worktreeParent = "";
|
|
1688
2044
|
let worktreeCwd = "";
|
|
1689
2045
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
1690
2046
|
const candidate = randomBytes(2).toString("hex");
|
|
1691
|
-
const parent =
|
|
2047
|
+
const parent = join2(worktreesRoot, candidate);
|
|
1692
2048
|
try {
|
|
1693
|
-
await
|
|
2049
|
+
await stat2(parent);
|
|
1694
2050
|
continue;
|
|
1695
2051
|
} catch {
|
|
1696
2052
|
worktreeId = candidate;
|
|
1697
2053
|
worktreeParent = parent;
|
|
1698
|
-
worktreeCwd =
|
|
2054
|
+
worktreeCwd = join2(parent, repoName);
|
|
1699
2055
|
break;
|
|
1700
2056
|
}
|
|
1701
2057
|
}
|
|
@@ -1703,15 +2059,15 @@ function createCodexBridgeMiddleware() {
|
|
|
1703
2059
|
throw new Error("Failed to allocate a unique worktree id");
|
|
1704
2060
|
}
|
|
1705
2061
|
const branch = `codex/${worktreeId}`;
|
|
1706
|
-
await
|
|
2062
|
+
await mkdir2(worktreeParent, { recursive: true });
|
|
1707
2063
|
try {
|
|
1708
|
-
await
|
|
2064
|
+
await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1709
2065
|
} catch (error) {
|
|
1710
2066
|
if (!isMissingHeadError(error)) throw error;
|
|
1711
2067
|
await ensureRepoHasInitialCommit(gitRoot);
|
|
1712
|
-
await
|
|
2068
|
+
await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1713
2069
|
}
|
|
1714
|
-
|
|
2070
|
+
setJson2(res, 200, {
|
|
1715
2071
|
data: {
|
|
1716
2072
|
cwd: worktreeCwd,
|
|
1717
2073
|
branch,
|
|
@@ -1719,15 +2075,15 @@ function createCodexBridgeMiddleware() {
|
|
|
1719
2075
|
}
|
|
1720
2076
|
});
|
|
1721
2077
|
} catch (error) {
|
|
1722
|
-
|
|
2078
|
+
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to create worktree") });
|
|
1723
2079
|
}
|
|
1724
2080
|
return;
|
|
1725
2081
|
}
|
|
1726
2082
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1727
2083
|
const payload = await readJsonBody(req);
|
|
1728
|
-
const record =
|
|
2084
|
+
const record = asRecord2(payload);
|
|
1729
2085
|
if (!record) {
|
|
1730
|
-
|
|
2086
|
+
setJson2(res, 400, { error: "Invalid body: expected object" });
|
|
1731
2087
|
return;
|
|
1732
2088
|
}
|
|
1733
2089
|
const nextState = {
|
|
@@ -1736,33 +2092,33 @@ function createCodexBridgeMiddleware() {
|
|
|
1736
2092
|
active: normalizeStringArray(record.active)
|
|
1737
2093
|
};
|
|
1738
2094
|
await writeWorkspaceRootsState(nextState);
|
|
1739
|
-
|
|
2095
|
+
setJson2(res, 200, { ok: true });
|
|
1740
2096
|
return;
|
|
1741
2097
|
}
|
|
1742
2098
|
if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
|
|
1743
|
-
const payload =
|
|
2099
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1744
2100
|
const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
|
1745
2101
|
const createIfMissing = payload?.createIfMissing === true;
|
|
1746
2102
|
const label = typeof payload?.label === "string" ? payload.label : "";
|
|
1747
2103
|
if (!rawPath) {
|
|
1748
|
-
|
|
2104
|
+
setJson2(res, 400, { error: "Missing path" });
|
|
1749
2105
|
return;
|
|
1750
2106
|
}
|
|
1751
2107
|
const normalizedPath = isAbsolute(rawPath) ? rawPath : resolve(rawPath);
|
|
1752
2108
|
let pathExists = true;
|
|
1753
2109
|
try {
|
|
1754
|
-
const info = await
|
|
2110
|
+
const info = await stat2(normalizedPath);
|
|
1755
2111
|
if (!info.isDirectory()) {
|
|
1756
|
-
|
|
2112
|
+
setJson2(res, 400, { error: "Path exists but is not a directory" });
|
|
1757
2113
|
return;
|
|
1758
2114
|
}
|
|
1759
2115
|
} catch {
|
|
1760
2116
|
pathExists = false;
|
|
1761
2117
|
}
|
|
1762
2118
|
if (!pathExists && createIfMissing) {
|
|
1763
|
-
await
|
|
2119
|
+
await mkdir2(normalizedPath, { recursive: true });
|
|
1764
2120
|
} else if (!pathExists) {
|
|
1765
|
-
|
|
2121
|
+
setJson2(res, 404, { error: "Directory does not exist" });
|
|
1766
2122
|
return;
|
|
1767
2123
|
}
|
|
1768
2124
|
const existingState = await readWorkspaceRootsState();
|
|
@@ -1777,408 +2133,103 @@ function createCodexBridgeMiddleware() {
|
|
|
1777
2133
|
labels: nextLabels,
|
|
1778
2134
|
active: nextActive
|
|
1779
2135
|
});
|
|
1780
|
-
|
|
2136
|
+
setJson2(res, 200, { data: { path: normalizedPath } });
|
|
1781
2137
|
return;
|
|
1782
2138
|
}
|
|
1783
2139
|
if (req.method === "GET" && url.pathname === "/codex-api/project-root-suggestion") {
|
|
1784
2140
|
const basePath = url.searchParams.get("basePath")?.trim() ?? "";
|
|
1785
2141
|
if (!basePath) {
|
|
1786
|
-
|
|
2142
|
+
setJson2(res, 400, { error: "Missing basePath" });
|
|
1787
2143
|
return;
|
|
1788
2144
|
}
|
|
1789
2145
|
const normalizedBasePath = isAbsolute(basePath) ? basePath : resolve(basePath);
|
|
1790
2146
|
try {
|
|
1791
|
-
const baseInfo = await
|
|
2147
|
+
const baseInfo = await stat2(normalizedBasePath);
|
|
1792
2148
|
if (!baseInfo.isDirectory()) {
|
|
1793
|
-
|
|
2149
|
+
setJson2(res, 400, { error: "basePath is not a directory" });
|
|
1794
2150
|
return;
|
|
1795
2151
|
}
|
|
1796
2152
|
} catch {
|
|
1797
|
-
|
|
2153
|
+
setJson2(res, 404, { error: "basePath does not exist" });
|
|
1798
2154
|
return;
|
|
1799
2155
|
}
|
|
1800
2156
|
let index = 1;
|
|
1801
2157
|
while (index < 1e5) {
|
|
1802
2158
|
const candidateName = `New Project (${String(index)})`;
|
|
1803
|
-
const candidatePath =
|
|
2159
|
+
const candidatePath = join2(normalizedBasePath, candidateName);
|
|
1804
2160
|
try {
|
|
1805
|
-
await
|
|
2161
|
+
await stat2(candidatePath);
|
|
1806
2162
|
index += 1;
|
|
1807
2163
|
continue;
|
|
1808
2164
|
} catch {
|
|
1809
|
-
|
|
2165
|
+
setJson2(res, 200, { data: { name: candidateName, path: candidatePath } });
|
|
1810
2166
|
return;
|
|
1811
2167
|
}
|
|
1812
2168
|
}
|
|
1813
|
-
|
|
2169
|
+
setJson2(res, 500, { error: "Failed to compute project name suggestion" });
|
|
1814
2170
|
return;
|
|
1815
2171
|
}
|
|
1816
2172
|
if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
|
|
1817
|
-
const payload =
|
|
2173
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1818
2174
|
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
1819
2175
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1820
2176
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
|
|
1821
2177
|
const limit = Math.max(1, Math.min(100, Math.floor(limitRaw)));
|
|
1822
2178
|
if (!rawCwd) {
|
|
1823
|
-
|
|
2179
|
+
setJson2(res, 400, { error: "Missing cwd" });
|
|
1824
2180
|
return;
|
|
1825
2181
|
}
|
|
1826
2182
|
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
1827
2183
|
try {
|
|
1828
|
-
const info = await
|
|
2184
|
+
const info = await stat2(cwd);
|
|
1829
2185
|
if (!info.isDirectory()) {
|
|
1830
|
-
|
|
2186
|
+
setJson2(res, 400, { error: "cwd is not a directory" });
|
|
1831
2187
|
return;
|
|
1832
2188
|
}
|
|
1833
2189
|
} catch {
|
|
1834
|
-
|
|
2190
|
+
setJson2(res, 404, { error: "cwd does not exist" });
|
|
1835
2191
|
return;
|
|
1836
2192
|
}
|
|
1837
2193
|
try {
|
|
1838
2194
|
const files = await listFilesWithRipgrep(cwd);
|
|
1839
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 }));
|
|
1840
|
-
|
|
2196
|
+
setJson2(res, 200, { data: scored });
|
|
1841
2197
|
} catch (error) {
|
|
1842
|
-
|
|
2198
|
+
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to search files") });
|
|
1843
2199
|
}
|
|
1844
2200
|
return;
|
|
1845
2201
|
}
|
|
1846
2202
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
|
|
1847
2203
|
const cache = await readThreadTitleCache();
|
|
1848
|
-
|
|
2204
|
+
setJson2(res, 200, { data: cache });
|
|
1849
2205
|
return;
|
|
1850
2206
|
}
|
|
1851
2207
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
1852
|
-
const payload =
|
|
2208
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1853
2209
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1854
2210
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
1855
2211
|
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
1856
2212
|
if (!query) {
|
|
1857
|
-
|
|
2213
|
+
setJson2(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
|
|
1858
2214
|
return;
|
|
1859
2215
|
}
|
|
1860
2216
|
const index = await getThreadSearchIndex();
|
|
1861
2217
|
const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
|
|
1862
|
-
|
|
2218
|
+
setJson2(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
|
|
1863
2219
|
return;
|
|
1864
2220
|
}
|
|
1865
2221
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
1866
|
-
const payload =
|
|
2222
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1867
2223
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
1868
2224
|
const title = typeof payload?.title === "string" ? payload.title : "";
|
|
1869
2225
|
if (!id) {
|
|
1870
|
-
|
|
2226
|
+
setJson2(res, 400, { error: "Missing id" });
|
|
1871
2227
|
return;
|
|
1872
2228
|
}
|
|
1873
2229
|
const cache = await readThreadTitleCache();
|
|
1874
2230
|
const next2 = title ? updateThreadTitleCache(cache, id, title) : removeFromThreadTitleCache(cache, id);
|
|
1875
2231
|
await writeThreadTitleCache(next2);
|
|
1876
|
-
|
|
1877
|
-
return;
|
|
1878
|
-
}
|
|
1879
|
-
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
|
|
1880
|
-
try {
|
|
1881
|
-
const q = url.searchParams.get("q") || "";
|
|
1882
|
-
const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
|
|
1883
|
-
const sort = url.searchParams.get("sort") || "date";
|
|
1884
|
-
const allEntries = await fetchSkillsTree();
|
|
1885
|
-
const installedMap = await scanInstalledSkillsFromDisk();
|
|
1886
|
-
try {
|
|
1887
|
-
const result = await appServer.rpc("skills/list", {});
|
|
1888
|
-
for (const entry of result.data ?? []) {
|
|
1889
|
-
for (const skill of entry.skills ?? []) {
|
|
1890
|
-
if (skill.name) {
|
|
1891
|
-
installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
} catch {
|
|
1896
|
-
}
|
|
1897
|
-
const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
|
|
1898
|
-
await fetchMetaBatch(installedHubEntries);
|
|
1899
|
-
const installed = [];
|
|
1900
|
-
for (const [, info] of installedMap) {
|
|
1901
|
-
const hubEntry = allEntries.find((e) => e.name === info.name);
|
|
1902
|
-
const base = hubEntry ? buildHubEntry(hubEntry) : {
|
|
1903
|
-
name: info.name,
|
|
1904
|
-
owner: "local",
|
|
1905
|
-
description: "",
|
|
1906
|
-
displayName: "",
|
|
1907
|
-
publishedAt: 0,
|
|
1908
|
-
avatarUrl: "",
|
|
1909
|
-
url: "",
|
|
1910
|
-
installed: false
|
|
1911
|
-
};
|
|
1912
|
-
installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
|
|
1913
|
-
}
|
|
1914
|
-
const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
|
|
1915
|
-
setJson(res, 200, { data: results, installed, total: allEntries.length });
|
|
1916
|
-
} catch (error) {
|
|
1917
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch skills hub") });
|
|
1918
|
-
}
|
|
1919
|
-
return;
|
|
1920
|
-
}
|
|
1921
|
-
if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
|
|
1922
|
-
const state = await readSkillsSyncState();
|
|
1923
|
-
setJson(res, 200, {
|
|
1924
|
-
data: {
|
|
1925
|
-
loggedIn: Boolean(state.githubToken),
|
|
1926
|
-
githubUsername: state.githubUsername ?? "",
|
|
1927
|
-
repoOwner: state.repoOwner ?? "",
|
|
1928
|
-
repoName: state.repoName ?? "",
|
|
1929
|
-
configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
|
|
1930
|
-
startup: {
|
|
1931
|
-
inProgress: startupSyncStatus.inProgress,
|
|
1932
|
-
mode: startupSyncStatus.mode,
|
|
1933
|
-
branch: startupSyncStatus.branch,
|
|
1934
|
-
lastAction: startupSyncStatus.lastAction,
|
|
1935
|
-
lastRunAtIso: startupSyncStatus.lastRunAtIso,
|
|
1936
|
-
lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
|
|
1937
|
-
lastError: startupSyncStatus.lastError
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
});
|
|
1941
|
-
return;
|
|
1942
|
-
}
|
|
1943
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
|
|
1944
|
-
try {
|
|
1945
|
-
const started = await startGithubDeviceLogin();
|
|
1946
|
-
setJson(res, 200, { data: started });
|
|
1947
|
-
} catch (error) {
|
|
1948
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to start GitHub login") });
|
|
1949
|
-
}
|
|
1950
|
-
return;
|
|
1951
|
-
}
|
|
1952
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
|
|
1953
|
-
try {
|
|
1954
|
-
const payload = asRecord(await readJsonBody(req));
|
|
1955
|
-
const token = typeof payload?.token === "string" ? payload.token.trim() : "";
|
|
1956
|
-
if (!token) {
|
|
1957
|
-
setJson(res, 400, { error: "Missing GitHub token" });
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
const username = await resolveGithubUsername(token);
|
|
1961
|
-
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
1962
|
-
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
1963
|
-
} catch (error) {
|
|
1964
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to login with GitHub token") });
|
|
1965
|
-
}
|
|
1966
|
-
return;
|
|
1967
|
-
}
|
|
1968
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
|
|
1969
|
-
try {
|
|
1970
|
-
const state = await readSkillsSyncState();
|
|
1971
|
-
await writeSkillsSyncState({
|
|
1972
|
-
...state,
|
|
1973
|
-
githubToken: void 0,
|
|
1974
|
-
githubUsername: void 0,
|
|
1975
|
-
repoOwner: void 0,
|
|
1976
|
-
repoName: void 0
|
|
1977
|
-
});
|
|
1978
|
-
setJson(res, 200, { ok: true });
|
|
1979
|
-
} catch (error) {
|
|
1980
|
-
setJson(res, 500, { error: getErrorMessage(error, "Failed to logout GitHub") });
|
|
1981
|
-
}
|
|
1982
|
-
return;
|
|
1983
|
-
}
|
|
1984
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
|
|
1985
|
-
try {
|
|
1986
|
-
const payload = asRecord(await readJsonBody(req));
|
|
1987
|
-
const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
|
|
1988
|
-
if (!deviceCode) {
|
|
1989
|
-
setJson(res, 400, { error: "Missing deviceCode" });
|
|
1990
|
-
return;
|
|
1991
|
-
}
|
|
1992
|
-
const result = await completeGithubDeviceLogin(deviceCode);
|
|
1993
|
-
if (!result.token) {
|
|
1994
|
-
setJson(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
|
|
1995
|
-
return;
|
|
1996
|
-
}
|
|
1997
|
-
const token = result.token;
|
|
1998
|
-
const username = await resolveGithubUsername(token);
|
|
1999
|
-
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
2000
|
-
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
2001
|
-
} catch (error) {
|
|
2002
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to complete GitHub login") });
|
|
2003
|
-
}
|
|
2004
|
-
return;
|
|
2005
|
-
}
|
|
2006
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
|
|
2007
|
-
try {
|
|
2008
|
-
const state = await readSkillsSyncState();
|
|
2009
|
-
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
2010
|
-
setJson(res, 400, { error: "Skills sync is not configured yet" });
|
|
2011
|
-
return;
|
|
2012
|
-
}
|
|
2013
|
-
if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
|
|
2014
|
-
setJson(res, 400, { error: "Refusing to push to upstream repository" });
|
|
2015
|
-
return;
|
|
2016
|
-
}
|
|
2017
|
-
const local = await collectLocalSyncedSkills(appServer);
|
|
2018
|
-
const installedMap = await scanInstalledSkillsFromDisk();
|
|
2019
|
-
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
2020
|
-
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
2021
|
-
setJson(res, 200, { ok: true, data: { synced: local.length } });
|
|
2022
|
-
} catch (error) {
|
|
2023
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to push synced skills") });
|
|
2024
|
-
}
|
|
2025
|
-
return;
|
|
2026
|
-
}
|
|
2027
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
|
|
2028
|
-
try {
|
|
2029
|
-
const state = await readSkillsSyncState();
|
|
2030
|
-
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
2031
|
-
const localDir2 = await detectUserSkillsDir(appServer);
|
|
2032
|
-
await bootstrapSkillsFromUpstreamIntoLocal(localDir2);
|
|
2033
|
-
try {
|
|
2034
|
-
await appServer.rpc("skills/list", { forceReload: true });
|
|
2035
|
-
} catch {
|
|
2036
|
-
}
|
|
2037
|
-
setJson(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
|
|
2038
|
-
return;
|
|
2039
|
-
}
|
|
2040
|
-
const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
|
|
2041
|
-
const tree = await fetchSkillsTree();
|
|
2042
|
-
const uniqueOwnerByName = /* @__PURE__ */ new Map();
|
|
2043
|
-
const ambiguousNames = /* @__PURE__ */ new Set();
|
|
2044
|
-
for (const entry of tree) {
|
|
2045
|
-
if (ambiguousNames.has(entry.name)) continue;
|
|
2046
|
-
const existingOwner = uniqueOwnerByName.get(entry.name);
|
|
2047
|
-
if (!existingOwner) {
|
|
2048
|
-
uniqueOwnerByName.set(entry.name, entry.owner);
|
|
2049
|
-
continue;
|
|
2050
|
-
}
|
|
2051
|
-
if (existingOwner !== entry.owner) {
|
|
2052
|
-
uniqueOwnerByName.delete(entry.name);
|
|
2053
|
-
ambiguousNames.add(entry.name);
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
const localDir = await detectUserSkillsDir(appServer);
|
|
2057
|
-
await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName, localDir);
|
|
2058
|
-
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
2059
|
-
const localSkills = await scanInstalledSkillsFromDisk();
|
|
2060
|
-
for (const skill of remote) {
|
|
2061
|
-
const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
|
|
2062
|
-
if (!owner) {
|
|
2063
|
-
continue;
|
|
2064
|
-
}
|
|
2065
|
-
if (!localSkills.has(skill.name)) {
|
|
2066
|
-
await runCommand("python3", [
|
|
2067
|
-
installerScript,
|
|
2068
|
-
"--repo",
|
|
2069
|
-
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
2070
|
-
"--path",
|
|
2071
|
-
`skills/${owner}/${skill.name}`,
|
|
2072
|
-
"--dest",
|
|
2073
|
-
localDir,
|
|
2074
|
-
"--method",
|
|
2075
|
-
"git"
|
|
2076
|
-
]);
|
|
2077
|
-
}
|
|
2078
|
-
const skillPath = join(localDir, skill.name);
|
|
2079
|
-
await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
|
|
2080
|
-
}
|
|
2081
|
-
const remoteNames = new Set(remote.map((row) => row.name));
|
|
2082
|
-
for (const [name, localInfo] of localSkills.entries()) {
|
|
2083
|
-
if (!remoteNames.has(name)) {
|
|
2084
|
-
await rm(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
|
-
const nextOwners = {};
|
|
2088
|
-
for (const item of remote) {
|
|
2089
|
-
const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
|
|
2090
|
-
if (owner) nextOwners[item.name] = owner;
|
|
2091
|
-
}
|
|
2092
|
-
await writeSkillsSyncState({ ...state, installedOwners: nextOwners });
|
|
2093
|
-
try {
|
|
2094
|
-
await appServer.rpc("skills/list", { forceReload: true });
|
|
2095
|
-
} catch {
|
|
2096
|
-
}
|
|
2097
|
-
setJson(res, 200, { ok: true, data: { synced: remote.length } });
|
|
2098
|
-
} catch (error) {
|
|
2099
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to pull synced skills") });
|
|
2100
|
-
}
|
|
2101
|
-
return;
|
|
2102
|
-
}
|
|
2103
|
-
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
|
|
2104
|
-
try {
|
|
2105
|
-
const owner = url.searchParams.get("owner") || "";
|
|
2106
|
-
const name = url.searchParams.get("name") || "";
|
|
2107
|
-
if (!owner || !name) {
|
|
2108
|
-
setJson(res, 400, { error: "Missing owner or name" });
|
|
2109
|
-
return;
|
|
2110
|
-
}
|
|
2111
|
-
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
|
|
2112
|
-
const resp = await fetch(rawUrl);
|
|
2113
|
-
if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
|
|
2114
|
-
const content = await resp.text();
|
|
2115
|
-
setJson(res, 200, { content });
|
|
2116
|
-
} catch (error) {
|
|
2117
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
|
|
2118
|
-
}
|
|
2119
|
-
return;
|
|
2120
|
-
}
|
|
2121
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
|
|
2122
|
-
try {
|
|
2123
|
-
const payload = asRecord(await readJsonBody(req));
|
|
2124
|
-
const owner = typeof payload?.owner === "string" ? payload.owner : "";
|
|
2125
|
-
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
2126
|
-
if (!owner || !name) {
|
|
2127
|
-
setJson(res, 400, { error: "Missing owner or name" });
|
|
2128
|
-
return;
|
|
2129
|
-
}
|
|
2130
|
-
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
2131
|
-
const installDest = await detectUserSkillsDir(appServer);
|
|
2132
|
-
const skillPathInRepo = `skills/${owner}/${name}`;
|
|
2133
|
-
await runCommand("python3", [
|
|
2134
|
-
installerScript,
|
|
2135
|
-
"--repo",
|
|
2136
|
-
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
2137
|
-
"--path",
|
|
2138
|
-
skillPathInRepo,
|
|
2139
|
-
"--dest",
|
|
2140
|
-
installDest,
|
|
2141
|
-
"--method",
|
|
2142
|
-
"git"
|
|
2143
|
-
]);
|
|
2144
|
-
const skillDir = join(installDest, name);
|
|
2145
|
-
await ensureInstalledSkillIsValid(appServer, skillDir);
|
|
2146
|
-
const syncState = await readSkillsSyncState();
|
|
2147
|
-
const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
|
|
2148
|
-
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
2149
|
-
await autoPushSyncedSkills(appServer);
|
|
2150
|
-
setJson(res, 200, { ok: true, path: skillDir });
|
|
2151
|
-
} catch (error) {
|
|
2152
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
|
|
2153
|
-
}
|
|
2154
|
-
return;
|
|
2155
|
-
}
|
|
2156
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
|
|
2157
|
-
try {
|
|
2158
|
-
const payload = asRecord(await readJsonBody(req));
|
|
2159
|
-
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
2160
|
-
const path = typeof payload?.path === "string" ? payload.path : "";
|
|
2161
|
-
const target = path || (name ? join(getSkillsInstallDir(), name) : "");
|
|
2162
|
-
if (!target) {
|
|
2163
|
-
setJson(res, 400, { error: "Missing name or path" });
|
|
2164
|
-
return;
|
|
2165
|
-
}
|
|
2166
|
-
await rm(target, { recursive: true, force: true });
|
|
2167
|
-
if (name) {
|
|
2168
|
-
const syncState = await readSkillsSyncState();
|
|
2169
|
-
const nextOwners = { ...syncState.installedOwners ?? {} };
|
|
2170
|
-
delete nextOwners[name];
|
|
2171
|
-
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
2172
|
-
}
|
|
2173
|
-
await autoPushSyncedSkills(appServer);
|
|
2174
|
-
try {
|
|
2175
|
-
await appServer.rpc("skills/list", { forceReload: true });
|
|
2176
|
-
} catch {
|
|
2177
|
-
}
|
|
2178
|
-
setJson(res, 200, { ok: true, deletedPath: target });
|
|
2179
|
-
} catch (error) {
|
|
2180
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to uninstall skill") });
|
|
2181
|
-
}
|
|
2232
|
+
setJson2(res, 200, { ok: true });
|
|
2182
2233
|
return;
|
|
2183
2234
|
}
|
|
2184
2235
|
if (req.method === "GET" && url.pathname === "/codex-api/events") {
|
|
@@ -2213,8 +2264,8 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
2213
2264
|
}
|
|
2214
2265
|
next();
|
|
2215
2266
|
} catch (error) {
|
|
2216
|
-
const message =
|
|
2217
|
-
|
|
2267
|
+
const message = getErrorMessage2(error, "Unknown bridge error");
|
|
2268
|
+
setJson2(res, 502, { error: message });
|
|
2218
2269
|
}
|
|
2219
2270
|
};
|
|
2220
2271
|
middleware.dispose = () => {
|
|
@@ -2351,8 +2402,8 @@ function createAuthSession(password) {
|
|
|
2351
2402
|
}
|
|
2352
2403
|
|
|
2353
2404
|
// src/server/localBrowseUi.ts
|
|
2354
|
-
import { dirname, extname, join as
|
|
2355
|
-
import { open, readFile as
|
|
2405
|
+
import { dirname, extname, join as join3 } from "path";
|
|
2406
|
+
import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
2356
2407
|
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2357
2408
|
".txt",
|
|
2358
2409
|
".md",
|
|
@@ -2467,7 +2518,7 @@ async function probeFileIsText(localPath) {
|
|
|
2467
2518
|
async function isTextEditableFile(localPath) {
|
|
2468
2519
|
if (isTextEditablePath(localPath)) return true;
|
|
2469
2520
|
try {
|
|
2470
|
-
const fileStat = await
|
|
2521
|
+
const fileStat = await stat3(localPath);
|
|
2471
2522
|
if (!fileStat.isFile()) return false;
|
|
2472
2523
|
return await probeFileIsText(localPath);
|
|
2473
2524
|
} catch {
|
|
@@ -2487,10 +2538,10 @@ function escapeForInlineScriptString(value) {
|
|
|
2487
2538
|
return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
|
|
2488
2539
|
}
|
|
2489
2540
|
async function getDirectoryItems(localPath) {
|
|
2490
|
-
const entries = await
|
|
2541
|
+
const entries = await readdir3(localPath, { withFileTypes: true });
|
|
2491
2542
|
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
2492
|
-
const entryPath =
|
|
2493
|
-
const entryStat = await
|
|
2543
|
+
const entryPath = join3(localPath, entry.name);
|
|
2544
|
+
const entryStat = await stat3(entryPath);
|
|
2494
2545
|
const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
|
|
2495
2546
|
return {
|
|
2496
2547
|
name: entry.name,
|
|
@@ -2548,7 +2599,7 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
2548
2599
|
</html>`;
|
|
2549
2600
|
}
|
|
2550
2601
|
async function createTextEditorHtml(localPath) {
|
|
2551
|
-
const content = await
|
|
2602
|
+
const content = await readFile3(localPath, "utf8");
|
|
2552
2603
|
const parentPath = dirname(localPath);
|
|
2553
2604
|
const language = languageForPath(localPath);
|
|
2554
2605
|
const safeContentLiteral = escapeForInlineScriptString(content);
|
|
@@ -2619,8 +2670,8 @@ async function createTextEditorHtml(localPath) {
|
|
|
2619
2670
|
// src/server/httpServer.ts
|
|
2620
2671
|
import { WebSocketServer } from "ws";
|
|
2621
2672
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
2622
|
-
var distDir =
|
|
2623
|
-
var spaEntryFile =
|
|
2673
|
+
var distDir = join4(__dirname, "..", "dist");
|
|
2674
|
+
var spaEntryFile = join4(distDir, "index.html");
|
|
2624
2675
|
var IMAGE_CONTENT_TYPES = {
|
|
2625
2676
|
".avif": "image/avif",
|
|
2626
2677
|
".bmp": "image/bmp",
|
|
@@ -2697,7 +2748,7 @@ function createServer(options = {}) {
|
|
|
2697
2748
|
return;
|
|
2698
2749
|
}
|
|
2699
2750
|
try {
|
|
2700
|
-
const fileStat = await
|
|
2751
|
+
const fileStat = await stat4(localPath);
|
|
2701
2752
|
res.setHeader("Cache-Control", "private, no-store");
|
|
2702
2753
|
if (fileStat.isDirectory()) {
|
|
2703
2754
|
const html = await createDirectoryListingHtml(localPath);
|
|
@@ -2720,7 +2771,7 @@ function createServer(options = {}) {
|
|
|
2720
2771
|
return;
|
|
2721
2772
|
}
|
|
2722
2773
|
try {
|
|
2723
|
-
const fileStat = await
|
|
2774
|
+
const fileStat = await stat4(localPath);
|
|
2724
2775
|
if (!fileStat.isFile()) {
|
|
2725
2776
|
res.status(400).json({ error: "Expected file path." });
|
|
2726
2777
|
return;
|
|
@@ -2744,7 +2795,7 @@ function createServer(options = {}) {
|
|
|
2744
2795
|
}
|
|
2745
2796
|
const body = typeof req.body === "string" ? req.body : "";
|
|
2746
2797
|
try {
|
|
2747
|
-
await
|
|
2798
|
+
await writeFile3(localPath, body, "utf8");
|
|
2748
2799
|
res.status(200).json({ ok: true });
|
|
2749
2800
|
} catch {
|
|
2750
2801
|
res.status(404).json({ error: "File not found." });
|
|
@@ -2824,8 +2875,8 @@ var program = new Command().name("codexui").description("Web interface for Codex
|
|
|
2824
2875
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
2825
2876
|
async function readCliVersion() {
|
|
2826
2877
|
try {
|
|
2827
|
-
const packageJsonPath =
|
|
2828
|
-
const raw = await
|
|
2878
|
+
const packageJsonPath = join5(__dirname2, "..", "package.json");
|
|
2879
|
+
const raw = await readFile4(packageJsonPath, "utf8");
|
|
2829
2880
|
const parsed = JSON.parse(raw);
|
|
2830
2881
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
2831
2882
|
} catch {
|
|
@@ -2850,13 +2901,13 @@ function runWithStatus(command, args) {
|
|
|
2850
2901
|
return result.status ?? -1;
|
|
2851
2902
|
}
|
|
2852
2903
|
function getUserNpmPrefix() {
|
|
2853
|
-
return
|
|
2904
|
+
return join5(homedir3(), ".npm-global");
|
|
2854
2905
|
}
|
|
2855
2906
|
function resolveCodexCommand() {
|
|
2856
2907
|
if (canRun("codex", ["--version"])) {
|
|
2857
2908
|
return "codex";
|
|
2858
2909
|
}
|
|
2859
|
-
const userCandidate =
|
|
2910
|
+
const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
|
|
2860
2911
|
if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
2861
2912
|
return userCandidate;
|
|
2862
2913
|
}
|
|
@@ -2864,7 +2915,7 @@ function resolveCodexCommand() {
|
|
|
2864
2915
|
if (!prefix) {
|
|
2865
2916
|
return null;
|
|
2866
2917
|
}
|
|
2867
|
-
const candidate =
|
|
2918
|
+
const candidate = join5(prefix, "bin", "codex");
|
|
2868
2919
|
if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
|
|
2869
2920
|
return candidate;
|
|
2870
2921
|
}
|
|
@@ -2874,7 +2925,7 @@ function resolveCloudflaredCommand() {
|
|
|
2874
2925
|
if (canRun("cloudflared", ["--version"])) {
|
|
2875
2926
|
return "cloudflared";
|
|
2876
2927
|
}
|
|
2877
|
-
const localCandidate =
|
|
2928
|
+
const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
|
|
2878
2929
|
if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
2879
2930
|
return localCandidate;
|
|
2880
2931
|
}
|
|
@@ -2928,9 +2979,9 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
2928
2979
|
if (!mappedArch) {
|
|
2929
2980
|
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
2930
2981
|
}
|
|
2931
|
-
const userBinDir =
|
|
2982
|
+
const userBinDir = join5(homedir3(), ".local", "bin");
|
|
2932
2983
|
mkdirSync(userBinDir, { recursive: true });
|
|
2933
|
-
const destination =
|
|
2984
|
+
const destination = join5(userBinDir, "cloudflared");
|
|
2934
2985
|
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
2935
2986
|
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
2936
2987
|
await downloadFile(downloadUrl, destination);
|
|
@@ -2969,8 +3020,8 @@ async function resolveCloudflaredForTunnel() {
|
|
|
2969
3020
|
return ensureCloudflaredInstalledLinux();
|
|
2970
3021
|
}
|
|
2971
3022
|
function hasCodexAuth() {
|
|
2972
|
-
const codexHome = process.env.CODEX_HOME?.trim() ||
|
|
2973
|
-
return existsSync3(
|
|
3023
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
|
|
3024
|
+
return existsSync3(join5(codexHome, "auth.json"));
|
|
2974
3025
|
}
|
|
2975
3026
|
function ensureCodexInstalled() {
|
|
2976
3027
|
let codexCommand = resolveCodexCommand();
|
|
@@ -2988,7 +3039,7 @@ function ensureCodexInstalled() {
|
|
|
2988
3039
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
2989
3040
|
`);
|
|
2990
3041
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
2991
|
-
process.env.PATH = `${
|
|
3042
|
+
process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
2992
3043
|
};
|
|
2993
3044
|
if (isTermuxRuntime()) {
|
|
2994
3045
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -3037,7 +3088,7 @@ function printTermuxKeepAlive(lines) {
|
|
|
3037
3088
|
}
|
|
3038
3089
|
function openBrowser(url) {
|
|
3039
3090
|
const command = process.platform === "darwin" ? { cmd: "open", args: [url] } : process.platform === "win32" ? { cmd: "cmd", args: ["/c", "start", "", url] } : { cmd: "xdg-open", args: [url] };
|
|
3040
|
-
const child =
|
|
3091
|
+
const child = spawn3(command.cmd, command.args, { detached: true, stdio: "ignore" });
|
|
3041
3092
|
child.on("error", () => {
|
|
3042
3093
|
});
|
|
3043
3094
|
child.unref();
|
|
@@ -3069,7 +3120,7 @@ function getAccessibleUrls(port) {
|
|
|
3069
3120
|
}
|
|
3070
3121
|
async function startCloudflaredTunnel(command, localPort) {
|
|
3071
3122
|
return new Promise((resolve2, reject) => {
|
|
3072
|
-
const child =
|
|
3123
|
+
const child = spawn3(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
|
|
3073
3124
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3074
3125
|
});
|
|
3075
3126
|
const timeout = setTimeout(() => {
|