codexapp 0.1.45 → 0.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/dist/assets/index-DGYijDgu.css +1 -0
- package/dist/assets/index-dkC99kKB.js +1442 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +1629 -496
- package/dist-cli/index.js.map +1 -1
- package/package.json +3 -1
- package/dist/assets/index-B6BqKv1b.css +0 -1
- package/dist/assets/index-DFsz7W1m.js +0 -50
package/dist-cli/index.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { createServer as createServer2 } from "http";
|
|
5
|
-
import { chmodSync, createWriteStream, existsSync as
|
|
6
|
-
import { readFile as
|
|
7
|
-
import { homedir as
|
|
8
|
-
import { join as
|
|
9
|
-
import { spawn as
|
|
10
|
-
import { createInterface } from "readline/promises";
|
|
5
|
+
import { chmodSync, createWriteStream, existsSync as existsSync4, mkdirSync } from "fs";
|
|
6
|
+
import { readFile as readFile4, stat as stat5, writeFile as writeFile4 } from "fs/promises";
|
|
7
|
+
import { homedir as homedir3, networkInterfaces } from "os";
|
|
8
|
+
import { isAbsolute as isAbsolute3, join as join5, resolve as resolve2 } from "path";
|
|
9
|
+
import { spawn as spawn3, spawnSync } from "child_process";
|
|
10
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
11
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12
12
|
import { dirname as dirname3 } from "path";
|
|
13
13
|
import { get as httpsGet } from "https";
|
|
@@ -16,19 +16,30 @@ 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
|
|
20
|
-
import { existsSync } from "fs";
|
|
21
|
-
import { writeFile as
|
|
19
|
+
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join4 } from "path";
|
|
20
|
+
import { existsSync as existsSync3 } from "fs";
|
|
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, readFile
|
|
27
|
+
import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as stat2 } from "fs/promises";
|
|
28
|
+
import { createReadStream } from "fs";
|
|
29
|
+
import { request as httpRequest } from "http";
|
|
28
30
|
import { request as httpsRequest } from "https";
|
|
29
|
-
import { homedir } from "os";
|
|
30
|
-
import { tmpdir } from "os";
|
|
31
|
-
import { basename, isAbsolute, join, resolve } from "path";
|
|
31
|
+
import { homedir as homedir2 } from "os";
|
|
32
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
33
|
+
import { basename, isAbsolute, join as join2, resolve } from "path";
|
|
34
|
+
import { createInterface } from "readline";
|
|
35
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
36
|
+
|
|
37
|
+
// src/server/skillsRoutes.ts
|
|
38
|
+
import { spawn } from "child_process";
|
|
39
|
+
import { mkdtemp, readFile, readdir, rm, mkdir, stat, lstat, readlink, symlink } from "fs/promises";
|
|
40
|
+
import { existsSync } from "fs";
|
|
41
|
+
import { homedir, tmpdir } from "os";
|
|
42
|
+
import { join } from "path";
|
|
32
43
|
import { writeFile } from "fs/promises";
|
|
33
44
|
function asRecord(value) {
|
|
34
45
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
@@ -52,85 +63,6 @@ function setJson(res, statusCode, payload) {
|
|
|
52
63
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
53
64
|
res.end(JSON.stringify(payload));
|
|
54
65
|
}
|
|
55
|
-
function extractThreadMessageText(threadReadPayload) {
|
|
56
|
-
const payload = asRecord(threadReadPayload);
|
|
57
|
-
const thread = asRecord(payload?.thread);
|
|
58
|
-
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
59
|
-
const parts = [];
|
|
60
|
-
for (const turn of turns) {
|
|
61
|
-
const turnRecord = asRecord(turn);
|
|
62
|
-
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
63
|
-
for (const item of items) {
|
|
64
|
-
const itemRecord = asRecord(item);
|
|
65
|
-
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
66
|
-
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
67
|
-
parts.push(itemRecord.text.trim());
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
if (type === "userMessage") {
|
|
71
|
-
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
72
|
-
for (const block of content) {
|
|
73
|
-
const blockRecord = asRecord(block);
|
|
74
|
-
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
75
|
-
parts.push(blockRecord.text.trim());
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (type === "commandExecution") {
|
|
81
|
-
const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
|
|
82
|
-
const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
|
|
83
|
-
if (command) parts.push(command);
|
|
84
|
-
if (output) parts.push(output);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return parts.join("\n").trim();
|
|
89
|
-
}
|
|
90
|
-
function isExactPhraseMatch(query, doc) {
|
|
91
|
-
const q = query.trim().toLowerCase();
|
|
92
|
-
if (!q) return false;
|
|
93
|
-
return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
|
|
94
|
-
}
|
|
95
|
-
function scoreFileCandidate(path, query) {
|
|
96
|
-
if (!query) return 0;
|
|
97
|
-
const lowerPath = path.toLowerCase();
|
|
98
|
-
const lowerQuery = query.toLowerCase();
|
|
99
|
-
const baseName = lowerPath.slice(lowerPath.lastIndexOf("/") + 1);
|
|
100
|
-
if (baseName === lowerQuery) return 0;
|
|
101
|
-
if (baseName.startsWith(lowerQuery)) return 1;
|
|
102
|
-
if (baseName.includes(lowerQuery)) return 2;
|
|
103
|
-
if (lowerPath.includes(`/${lowerQuery}`)) return 3;
|
|
104
|
-
if (lowerPath.includes(lowerQuery)) return 4;
|
|
105
|
-
return 10;
|
|
106
|
-
}
|
|
107
|
-
async function listFilesWithRipgrep(cwd) {
|
|
108
|
-
return await new Promise((resolve2, reject) => {
|
|
109
|
-
const proc = spawn("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
|
|
110
|
-
cwd,
|
|
111
|
-
env: process.env,
|
|
112
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
113
|
-
});
|
|
114
|
-
let stdout = "";
|
|
115
|
-
let stderr = "";
|
|
116
|
-
proc.stdout.on("data", (chunk) => {
|
|
117
|
-
stdout += chunk.toString();
|
|
118
|
-
});
|
|
119
|
-
proc.stderr.on("data", (chunk) => {
|
|
120
|
-
stderr += chunk.toString();
|
|
121
|
-
});
|
|
122
|
-
proc.on("error", reject);
|
|
123
|
-
proc.on("close", (code) => {
|
|
124
|
-
if (code === 0) {
|
|
125
|
-
const rows = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
126
|
-
resolve2(rows);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
130
|
-
reject(new Error(details || "rg --files failed"));
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
66
|
function getCodexHomeDir() {
|
|
135
67
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
136
68
|
return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
|
|
@@ -139,7 +71,7 @@ function getSkillsInstallDir() {
|
|
|
139
71
|
return join(getCodexHomeDir(), "skills");
|
|
140
72
|
}
|
|
141
73
|
async function runCommand(command, args, options = {}) {
|
|
142
|
-
await new Promise((
|
|
74
|
+
await new Promise((resolve3, reject) => {
|
|
143
75
|
const proc = spawn(command, args, {
|
|
144
76
|
cwd: options.cwd,
|
|
145
77
|
env: process.env,
|
|
@@ -156,7 +88,7 @@ async function runCommand(command, args, options = {}) {
|
|
|
156
88
|
proc.on("error", reject);
|
|
157
89
|
proc.on("close", (code) => {
|
|
158
90
|
if (code === 0) {
|
|
159
|
-
|
|
91
|
+
resolve3();
|
|
160
92
|
return;
|
|
161
93
|
}
|
|
162
94
|
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
@@ -165,30 +97,8 @@ async function runCommand(command, args, options = {}) {
|
|
|
165
97
|
});
|
|
166
98
|
});
|
|
167
99
|
}
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
|
|
171
|
-
}
|
|
172
|
-
function isNotGitRepositoryError(error) {
|
|
173
|
-
const message = getErrorMessage(error, "").toLowerCase();
|
|
174
|
-
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
175
|
-
}
|
|
176
|
-
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
177
|
-
const agentsPath = join(repoRoot, "AGENTS.md");
|
|
178
|
-
try {
|
|
179
|
-
await stat(agentsPath);
|
|
180
|
-
} catch {
|
|
181
|
-
await writeFile(agentsPath, "", "utf8");
|
|
182
|
-
}
|
|
183
|
-
await runCommand("git", ["add", "AGENTS.md"], { cwd: repoRoot });
|
|
184
|
-
await runCommand(
|
|
185
|
-
"git",
|
|
186
|
-
["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
|
|
187
|
-
{ cwd: repoRoot }
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
async function runCommandCapture(command, args, options = {}) {
|
|
191
|
-
return await new Promise((resolveOutput, reject) => {
|
|
100
|
+
async function runCommandWithOutput(command, args, options = {}) {
|
|
101
|
+
return await new Promise((resolve3, reject) => {
|
|
192
102
|
const proc = spawn(command, args, {
|
|
193
103
|
cwd: options.cwd,
|
|
194
104
|
env: process.env,
|
|
@@ -205,7 +115,7 @@ async function runCommandCapture(command, args, options = {}) {
|
|
|
205
115
|
proc.on("error", reject);
|
|
206
116
|
proc.on("close", (code) => {
|
|
207
117
|
if (code === 0) {
|
|
208
|
-
|
|
118
|
+
resolve3(stdout.trim());
|
|
209
119
|
return;
|
|
210
120
|
}
|
|
211
121
|
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
@@ -250,9 +160,9 @@ async function getGhToken() {
|
|
|
250
160
|
proc.stdout.on("data", (d) => {
|
|
251
161
|
out += d.toString();
|
|
252
162
|
});
|
|
253
|
-
return new Promise((
|
|
254
|
-
proc.on("close", (code) =>
|
|
255
|
-
proc.on("error", () =>
|
|
163
|
+
return new Promise((resolve3) => {
|
|
164
|
+
proc.on("close", (code) => resolve3(code === 0 ? out.trim() : null));
|
|
165
|
+
proc.on("error", () => resolve3(null));
|
|
256
166
|
});
|
|
257
167
|
} catch {
|
|
258
168
|
return null;
|
|
@@ -271,7 +181,7 @@ async function fetchSkillsTree() {
|
|
|
271
181
|
if (skillsTreeCache && Date.now() - skillsTreeCache.fetchedAt < TREE_CACHE_TTL_MS) {
|
|
272
182
|
return skillsTreeCache.entries;
|
|
273
183
|
}
|
|
274
|
-
const resp = await ghFetch(
|
|
184
|
+
const resp = await ghFetch(`https://api.github.com/repos/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/git/trees/main?recursive=1`);
|
|
275
185
|
if (!resp.ok) throw new Error(`GitHub tree API returned ${resp.status}`);
|
|
276
186
|
const data = await resp.json();
|
|
277
187
|
const metaPattern = /^skills\/([^/]+)\/([^/]+)\/_meta\.json$/;
|
|
@@ -287,86 +197,1109 @@ async function fetchSkillsTree() {
|
|
|
287
197
|
entries.push({
|
|
288
198
|
name: skillName,
|
|
289
199
|
owner,
|
|
290
|
-
url: `https://github.com/
|
|
200
|
+
url: `https://github.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/tree/main/skills/${owner}/${skillName}`
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
skillsTreeCache = { entries, fetchedAt: Date.now() };
|
|
204
|
+
return entries;
|
|
205
|
+
}
|
|
206
|
+
async function fetchMetaBatch(entries) {
|
|
207
|
+
const toFetch = entries.filter((e) => !metaCache.has(`${e.owner}/${e.name}`));
|
|
208
|
+
if (toFetch.length === 0) return;
|
|
209
|
+
const batch = toFetch.slice(0, 50);
|
|
210
|
+
await Promise.allSettled(
|
|
211
|
+
batch.map(async (e) => {
|
|
212
|
+
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${e.owner}/${e.name}/_meta.json`;
|
|
213
|
+
const resp = await fetch(rawUrl);
|
|
214
|
+
if (!resp.ok) return;
|
|
215
|
+
const meta = await resp.json();
|
|
216
|
+
metaCache.set(`${e.owner}/${e.name}`, {
|
|
217
|
+
displayName: typeof meta.displayName === "string" ? meta.displayName : "",
|
|
218
|
+
description: typeof meta.displayName === "string" ? meta.displayName : "",
|
|
219
|
+
publishedAt: meta.latest?.publishedAt ?? 0
|
|
220
|
+
});
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
function buildHubEntry(e) {
|
|
225
|
+
const cached = metaCache.get(`${e.owner}/${e.name}`);
|
|
226
|
+
return {
|
|
227
|
+
name: e.name,
|
|
228
|
+
owner: e.owner,
|
|
229
|
+
description: cached?.description ?? "",
|
|
230
|
+
displayName: cached?.displayName ?? "",
|
|
231
|
+
publishedAt: cached?.publishedAt ?? 0,
|
|
232
|
+
avatarUrl: `https://github.com/${e.owner}.png?size=40`,
|
|
233
|
+
url: e.url,
|
|
234
|
+
installed: false
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
var GITHUB_DEVICE_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
238
|
+
var DEFAULT_SKILLS_SYNC_REPO_NAME = "codexskills";
|
|
239
|
+
var SKILLS_SYNC_MANIFEST_PATH = "installed-skills.json";
|
|
240
|
+
var SYNC_UPSTREAM_SKILLS_OWNER = "OpenClawAndroid";
|
|
241
|
+
var SYNC_UPSTREAM_SKILLS_REPO = "skills";
|
|
242
|
+
var HUB_SKILLS_OWNER = "openclaw";
|
|
243
|
+
var HUB_SKILLS_REPO = "skills";
|
|
244
|
+
var startupSkillsSyncInitialized = false;
|
|
245
|
+
var startupSyncStatus = {
|
|
246
|
+
inProgress: false,
|
|
247
|
+
mode: "idle",
|
|
248
|
+
branch: getPreferredSyncBranch(),
|
|
249
|
+
lastAction: "not-started",
|
|
250
|
+
lastRunAtIso: "",
|
|
251
|
+
lastSuccessAtIso: "",
|
|
252
|
+
lastError: ""
|
|
253
|
+
};
|
|
254
|
+
async function scanInstalledSkillsFromDisk() {
|
|
255
|
+
const map = /* @__PURE__ */ new Map();
|
|
256
|
+
const skillsDir = getSkillsInstallDir();
|
|
257
|
+
try {
|
|
258
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
259
|
+
for (const entry of entries) {
|
|
260
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
261
|
+
const skillMd = join(skillsDir, entry.name, "SKILL.md");
|
|
262
|
+
try {
|
|
263
|
+
await stat(skillMd);
|
|
264
|
+
map.set(entry.name, { name: entry.name, path: skillMd, enabled: true });
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
return map;
|
|
271
|
+
}
|
|
272
|
+
function getSkillsSyncStatePath() {
|
|
273
|
+
return join(getCodexHomeDir(), "skills-sync.json");
|
|
274
|
+
}
|
|
275
|
+
async function readSkillsSyncState() {
|
|
276
|
+
try {
|
|
277
|
+
const raw = await readFile(getSkillsSyncStatePath(), "utf8");
|
|
278
|
+
const parsed = JSON.parse(raw);
|
|
279
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
280
|
+
} catch {
|
|
281
|
+
return {};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async function writeSkillsSyncState(state) {
|
|
285
|
+
await writeFile(getSkillsSyncStatePath(), JSON.stringify(state), "utf8");
|
|
286
|
+
}
|
|
287
|
+
async function getGithubJson(url, token, method = "GET", body) {
|
|
288
|
+
const resp = await fetch(url, {
|
|
289
|
+
method,
|
|
290
|
+
headers: {
|
|
291
|
+
Accept: "application/vnd.github+json",
|
|
292
|
+
"Content-Type": "application/json",
|
|
293
|
+
Authorization: `Bearer ${token}`,
|
|
294
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
295
|
+
"User-Agent": "codex-web-local"
|
|
296
|
+
},
|
|
297
|
+
body: body ? JSON.stringify(body) : void 0
|
|
298
|
+
});
|
|
299
|
+
if (!resp.ok) {
|
|
300
|
+
const text = await resp.text();
|
|
301
|
+
throw new Error(`GitHub API ${method} ${url} failed (${resp.status}): ${text}`);
|
|
302
|
+
}
|
|
303
|
+
return await resp.json();
|
|
304
|
+
}
|
|
305
|
+
async function startGithubDeviceLogin() {
|
|
306
|
+
const resp = await fetch("https://github.com/login/device/code", {
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: {
|
|
309
|
+
Accept: "application/json",
|
|
310
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
311
|
+
"User-Agent": "codex-web-local"
|
|
312
|
+
},
|
|
313
|
+
body: new URLSearchParams({
|
|
314
|
+
client_id: GITHUB_DEVICE_CLIENT_ID,
|
|
315
|
+
scope: "repo read:user"
|
|
316
|
+
})
|
|
317
|
+
});
|
|
318
|
+
if (!resp.ok) {
|
|
319
|
+
throw new Error(`GitHub device flow init failed (${resp.status})`);
|
|
320
|
+
}
|
|
321
|
+
return await resp.json();
|
|
322
|
+
}
|
|
323
|
+
async function completeGithubDeviceLogin(deviceCode) {
|
|
324
|
+
const resp = await fetch("https://github.com/login/oauth/access_token", {
|
|
325
|
+
method: "POST",
|
|
326
|
+
headers: {
|
|
327
|
+
Accept: "application/json",
|
|
328
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
329
|
+
"User-Agent": "codex-web-local"
|
|
330
|
+
},
|
|
331
|
+
body: new URLSearchParams({
|
|
332
|
+
client_id: GITHUB_DEVICE_CLIENT_ID,
|
|
333
|
+
device_code: deviceCode,
|
|
334
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
335
|
+
})
|
|
336
|
+
});
|
|
337
|
+
if (!resp.ok) {
|
|
338
|
+
throw new Error(`GitHub token exchange failed (${resp.status})`);
|
|
339
|
+
}
|
|
340
|
+
const payload = await resp.json();
|
|
341
|
+
if (!payload.access_token) return { token: null, error: payload.error || "unknown_error" };
|
|
342
|
+
return { token: payload.access_token, error: null };
|
|
343
|
+
}
|
|
344
|
+
function isAndroidLikeRuntime() {
|
|
345
|
+
if (process.platform === "android") return true;
|
|
346
|
+
if (existsSync("/data/data/com.termux")) return true;
|
|
347
|
+
if (process.env.TERMUX_VERSION) return true;
|
|
348
|
+
const prefix = process.env.PREFIX?.toLowerCase() ?? "";
|
|
349
|
+
if (prefix.includes("/com.termux/")) return true;
|
|
350
|
+
const proot = process.env.PROOT_TMP_DIR?.toLowerCase() ?? "";
|
|
351
|
+
return proot.length > 0;
|
|
352
|
+
}
|
|
353
|
+
function getPreferredSyncBranch() {
|
|
354
|
+
return isAndroidLikeRuntime() ? "android" : "main";
|
|
355
|
+
}
|
|
356
|
+
function isUpstreamSkillsRepo(repoOwner, repoName) {
|
|
357
|
+
return repoOwner.toLowerCase() === SYNC_UPSTREAM_SKILLS_OWNER.toLowerCase() && repoName.toLowerCase() === SYNC_UPSTREAM_SKILLS_REPO.toLowerCase();
|
|
358
|
+
}
|
|
359
|
+
async function resolveGithubUsername(token) {
|
|
360
|
+
const user = await getGithubJson("https://api.github.com/user", token);
|
|
361
|
+
return user.login;
|
|
362
|
+
}
|
|
363
|
+
async function ensurePrivateForkFromUpstream(token, username, repoName) {
|
|
364
|
+
const repoUrl = `https://api.github.com/repos/${username}/${repoName}`;
|
|
365
|
+
let created = false;
|
|
366
|
+
const existing = await fetch(repoUrl, {
|
|
367
|
+
headers: {
|
|
368
|
+
Accept: "application/vnd.github+json",
|
|
369
|
+
Authorization: `Bearer ${token}`,
|
|
370
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
371
|
+
"User-Agent": "codex-web-local"
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
if (existing.ok) {
|
|
375
|
+
const details = await existing.json();
|
|
376
|
+
if (details.private === true) return;
|
|
377
|
+
await getGithubJson(repoUrl, token, "PATCH", { private: true });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (existing.status !== 404) {
|
|
381
|
+
throw new Error(`Failed to check personal repo existence (${existing.status})`);
|
|
382
|
+
}
|
|
383
|
+
await getGithubJson(
|
|
384
|
+
"https://api.github.com/user/repos",
|
|
385
|
+
token,
|
|
386
|
+
"POST",
|
|
387
|
+
{ name: repoName, private: true, auto_init: false, description: "Codex skills private mirror sync" }
|
|
388
|
+
);
|
|
389
|
+
created = true;
|
|
390
|
+
let ready = false;
|
|
391
|
+
for (let i = 0; i < 20; i++) {
|
|
392
|
+
const check = await fetch(repoUrl, {
|
|
393
|
+
headers: {
|
|
394
|
+
Accept: "application/vnd.github+json",
|
|
395
|
+
Authorization: `Bearer ${token}`,
|
|
396
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
397
|
+
"User-Agent": "codex-web-local"
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
if (check.ok) {
|
|
401
|
+
ready = true;
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
405
|
+
}
|
|
406
|
+
if (!ready) throw new Error("Private mirror repo was created but is not available yet");
|
|
407
|
+
if (!created) return;
|
|
408
|
+
const tmp = await mkdtemp(join(tmpdir(), "codex-skills-seed-"));
|
|
409
|
+
try {
|
|
410
|
+
const upstreamUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
|
|
411
|
+
const branch = getPreferredSyncBranch();
|
|
412
|
+
try {
|
|
413
|
+
await runCommand("git", ["clone", "--depth", "1", "--single-branch", "--branch", branch, upstreamUrl, tmp]);
|
|
414
|
+
} catch {
|
|
415
|
+
await runCommand("git", ["clone", "--depth", "1", upstreamUrl, tmp]);
|
|
416
|
+
}
|
|
417
|
+
const privateRemote = toGitHubTokenRemote(username, repoName, token);
|
|
418
|
+
await runCommand("git", ["remote", "set-url", "origin", privateRemote], { cwd: tmp });
|
|
419
|
+
try {
|
|
420
|
+
await runCommand("git", ["checkout", "-B", branch], { cwd: tmp });
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
await runCommand("git", ["push", "-u", "origin", `HEAD:${branch}`], { cwd: tmp });
|
|
424
|
+
} finally {
|
|
425
|
+
await rm(tmp, { recursive: true, force: true });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async function readRemoteSkillsManifest(token, repoOwner, repoName) {
|
|
429
|
+
const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
|
|
430
|
+
const resp = await fetch(url, {
|
|
431
|
+
headers: {
|
|
432
|
+
Accept: "application/vnd.github+json",
|
|
433
|
+
Authorization: `Bearer ${token}`,
|
|
434
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
435
|
+
"User-Agent": "codex-web-local"
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
if (resp.status === 404) return [];
|
|
439
|
+
if (!resp.ok) throw new Error(`Failed to read remote manifest (${resp.status})`);
|
|
440
|
+
const payload = await resp.json();
|
|
441
|
+
const content = payload.content ? Buffer.from(payload.content.replace(/\n/g, ""), "base64").toString("utf8") : "[]";
|
|
442
|
+
const parsed = JSON.parse(content);
|
|
443
|
+
if (!Array.isArray(parsed)) return [];
|
|
444
|
+
const skills = [];
|
|
445
|
+
for (const row of parsed) {
|
|
446
|
+
const item = asRecord(row);
|
|
447
|
+
const owner = typeof item?.owner === "string" ? item.owner : "";
|
|
448
|
+
const name = typeof item?.name === "string" ? item.name : "";
|
|
449
|
+
if (!name) continue;
|
|
450
|
+
skills.push({ ...owner ? { owner } : {}, name, enabled: item?.enabled !== false });
|
|
451
|
+
}
|
|
452
|
+
return skills;
|
|
453
|
+
}
|
|
454
|
+
async function writeRemoteSkillsManifest(token, repoOwner, repoName, skills) {
|
|
455
|
+
const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
|
|
456
|
+
let sha = "";
|
|
457
|
+
const existing = await fetch(url, {
|
|
458
|
+
headers: {
|
|
459
|
+
Accept: "application/vnd.github+json",
|
|
460
|
+
Authorization: `Bearer ${token}`,
|
|
461
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
462
|
+
"User-Agent": "codex-web-local"
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
if (existing.ok) {
|
|
466
|
+
const payload = await existing.json();
|
|
467
|
+
sha = payload.sha ?? "";
|
|
468
|
+
}
|
|
469
|
+
const content = Buffer.from(JSON.stringify(skills, null, 2), "utf8").toString("base64");
|
|
470
|
+
await getGithubJson(url, token, "PUT", {
|
|
471
|
+
message: "Update synced skills manifest",
|
|
472
|
+
content,
|
|
473
|
+
...sha ? { sha } : {}
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
function toGitHubTokenRemote(repoOwner, repoName, token) {
|
|
477
|
+
return `https://x-access-token:${encodeURIComponent(token)}@github.com/${repoOwner}/${repoName}.git`;
|
|
478
|
+
}
|
|
479
|
+
async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
|
|
480
|
+
const localDir = getSkillsInstallDir();
|
|
481
|
+
await mkdir(localDir, { recursive: true });
|
|
482
|
+
const gitDir = join(localDir, ".git");
|
|
483
|
+
let hasGitDir = false;
|
|
484
|
+
try {
|
|
485
|
+
hasGitDir = (await stat(gitDir)).isDirectory();
|
|
486
|
+
} catch {
|
|
487
|
+
hasGitDir = false;
|
|
488
|
+
}
|
|
489
|
+
if (!hasGitDir) {
|
|
490
|
+
await runCommand("git", ["init"], { cwd: localDir });
|
|
491
|
+
await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: localDir });
|
|
492
|
+
await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: localDir });
|
|
493
|
+
await runCommand("git", ["add", "-A"], { cwd: localDir });
|
|
494
|
+
try {
|
|
495
|
+
await runCommand("git", ["commit", "-m", "Local skills snapshot before sync"], { cwd: localDir });
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
await runCommand("git", ["branch", "-M", branch], { cwd: localDir });
|
|
499
|
+
try {
|
|
500
|
+
await runCommand("git", ["remote", "add", "origin", repoUrl], { cwd: localDir });
|
|
501
|
+
} catch {
|
|
502
|
+
await runCommand("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
|
|
503
|
+
}
|
|
504
|
+
await runCommand("git", ["fetch", "origin"], { cwd: localDir });
|
|
505
|
+
try {
|
|
506
|
+
await runCommand("git", ["merge", "--allow-unrelated-histories", "--no-edit", `origin/${branch}`], { cwd: localDir });
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
return localDir;
|
|
510
|
+
}
|
|
511
|
+
await runCommand("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
|
|
512
|
+
await runCommand("git", ["fetch", "origin"], { cwd: localDir });
|
|
513
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
514
|
+
try {
|
|
515
|
+
await runCommand("git", ["checkout", branch], { cwd: localDir });
|
|
516
|
+
} catch {
|
|
517
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
518
|
+
await runCommand("git", ["checkout", "-B", branch], { cwd: localDir });
|
|
519
|
+
}
|
|
520
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
521
|
+
const localMtimesBeforePull = await snapshotFileMtimes(localDir);
|
|
522
|
+
try {
|
|
523
|
+
await runCommand("git", ["stash", "push", "--include-untracked", "-m", "codex-skills-autostash"], { cwd: localDir });
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
let pulledMtimes = /* @__PURE__ */ new Map();
|
|
527
|
+
try {
|
|
528
|
+
await runCommand("git", ["pull", "--no-rebase", "origin", branch], { cwd: localDir });
|
|
529
|
+
pulledMtimes = await snapshotFileMtimes(localDir);
|
|
530
|
+
} catch {
|
|
531
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
532
|
+
pulledMtimes = await snapshotFileMtimes(localDir);
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
await runCommand("git", ["stash", "pop"], { cwd: localDir });
|
|
536
|
+
} catch {
|
|
537
|
+
await resolveStashPopConflictsByFileTime(localDir, localMtimesBeforePull, pulledMtimes);
|
|
538
|
+
}
|
|
539
|
+
return localDir;
|
|
540
|
+
}
|
|
541
|
+
async function resolveMergeConflictsByNewerCommit(repoDir, branch) {
|
|
542
|
+
const unmerged = (await runCommandWithOutput("git", ["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir })).split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
|
|
543
|
+
if (unmerged.length === 0) return;
|
|
544
|
+
for (const path of unmerged) {
|
|
545
|
+
const oursTime = await getCommitTime(repoDir, "HEAD", path);
|
|
546
|
+
const theirsTime = await getCommitTime(repoDir, `origin/${branch}`, path);
|
|
547
|
+
if (theirsTime > oursTime) {
|
|
548
|
+
await runCommand("git", ["checkout", "--theirs", "--", path], { cwd: repoDir });
|
|
549
|
+
} else {
|
|
550
|
+
await runCommand("git", ["checkout", "--ours", "--", path], { cwd: repoDir });
|
|
551
|
+
}
|
|
552
|
+
await runCommand("git", ["add", "--", path], { cwd: repoDir });
|
|
553
|
+
}
|
|
554
|
+
const mergeHead = (await runCommandWithOutput("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], { cwd: repoDir })).trim();
|
|
555
|
+
if (mergeHead) {
|
|
556
|
+
await runCommand("git", ["commit", "-m", "Auto-resolve skills merge by newer file"], { cwd: repoDir });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async function getCommitTime(repoDir, ref, path) {
|
|
560
|
+
try {
|
|
561
|
+
const output = (await runCommandWithOutput("git", ["log", "-1", "--format=%ct", ref, "--", path], { cwd: repoDir })).trim();
|
|
562
|
+
return output ? Number.parseInt(output, 10) : 0;
|
|
563
|
+
} catch {
|
|
564
|
+
return 0;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async function resolveStashPopConflictsByFileTime(repoDir, localMtimesBeforePull, pulledMtimes) {
|
|
568
|
+
const unmerged = (await runCommandWithOutput("git", ["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir })).split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
|
|
569
|
+
if (unmerged.length === 0) return;
|
|
570
|
+
for (const path of unmerged) {
|
|
571
|
+
const localMtime = localMtimesBeforePull.get(path) ?? 0;
|
|
572
|
+
const pulledMtime = pulledMtimes.get(path) ?? 0;
|
|
573
|
+
const side = localMtime >= pulledMtime ? "--theirs" : "--ours";
|
|
574
|
+
await runCommand("git", ["checkout", side, "--", path], { cwd: repoDir });
|
|
575
|
+
await runCommand("git", ["add", "--", path], { cwd: repoDir });
|
|
576
|
+
}
|
|
577
|
+
const mergeHead = (await runCommandWithOutput("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], { cwd: repoDir })).trim();
|
|
578
|
+
if (mergeHead) {
|
|
579
|
+
await runCommand("git", ["commit", "-m", "Auto-resolve stash-pop conflicts by file time"], { cwd: repoDir });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
async function snapshotFileMtimes(dir) {
|
|
583
|
+
const mtimes = /* @__PURE__ */ new Map();
|
|
584
|
+
await walkFileMtimes(dir, dir, mtimes);
|
|
585
|
+
return mtimes;
|
|
586
|
+
}
|
|
587
|
+
async function walkFileMtimes(rootDir, currentDir, out) {
|
|
588
|
+
let entries;
|
|
589
|
+
try {
|
|
590
|
+
entries = await readdir(currentDir, { withFileTypes: true });
|
|
591
|
+
} catch {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
for (const entry of entries) {
|
|
595
|
+
const entryName = String(entry.name);
|
|
596
|
+
if (entryName === ".git") continue;
|
|
597
|
+
const absolutePath = join(currentDir, entryName);
|
|
598
|
+
const relativePath = absolutePath.slice(rootDir.length + 1);
|
|
599
|
+
if (entry.isDirectory()) {
|
|
600
|
+
await walkFileMtimes(rootDir, absolutePath, out);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (!entry.isFile()) continue;
|
|
604
|
+
try {
|
|
605
|
+
const info = await stat(absolutePath);
|
|
606
|
+
out.set(relativePath, info.mtimeMs);
|
|
607
|
+
} catch {
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _installedMap) {
|
|
612
|
+
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
613
|
+
const branch = getPreferredSyncBranch();
|
|
614
|
+
const repoDir = await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
615
|
+
void _installedMap;
|
|
616
|
+
await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: repoDir });
|
|
617
|
+
await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: repoDir });
|
|
618
|
+
await runCommand("git", ["add", "."], { cwd: repoDir });
|
|
619
|
+
const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
|
|
620
|
+
if (!status) return;
|
|
621
|
+
await runCommand("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
|
|
622
|
+
await runCommand("git", ["push", "origin", `HEAD:${branch}`], { cwd: repoDir });
|
|
623
|
+
}
|
|
624
|
+
async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName) {
|
|
625
|
+
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
626
|
+
const branch = getPreferredSyncBranch();
|
|
627
|
+
await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
628
|
+
}
|
|
629
|
+
async function bootstrapSkillsFromUpstreamIntoLocal() {
|
|
630
|
+
const repoUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
|
|
631
|
+
const branch = getPreferredSyncBranch();
|
|
632
|
+
await ensureSkillsWorkingTreeRepo(repoUrl, branch);
|
|
633
|
+
}
|
|
634
|
+
async function collectLocalSyncedSkills(appServer) {
|
|
635
|
+
const state = await readSkillsSyncState();
|
|
636
|
+
const owners = { ...state.installedOwners ?? {} };
|
|
637
|
+
const tree = await fetchSkillsTree();
|
|
638
|
+
const uniqueOwnerByName = /* @__PURE__ */ new Map();
|
|
639
|
+
const ambiguousNames = /* @__PURE__ */ new Set();
|
|
640
|
+
for (const entry of tree) {
|
|
641
|
+
if (ambiguousNames.has(entry.name)) continue;
|
|
642
|
+
const existingOwner = uniqueOwnerByName.get(entry.name);
|
|
643
|
+
if (!existingOwner) {
|
|
644
|
+
uniqueOwnerByName.set(entry.name, entry.owner);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
if (existingOwner !== entry.owner) {
|
|
648
|
+
uniqueOwnerByName.delete(entry.name);
|
|
649
|
+
ambiguousNames.add(entry.name);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const skills = await appServer.rpc("skills/list", {});
|
|
653
|
+
const seen = /* @__PURE__ */ new Set();
|
|
654
|
+
const synced = [];
|
|
655
|
+
let ownersChanged = false;
|
|
656
|
+
for (const entry of skills.data ?? []) {
|
|
657
|
+
for (const skill of entry.skills ?? []) {
|
|
658
|
+
const name = typeof skill.name === "string" ? skill.name : "";
|
|
659
|
+
if (!name || seen.has(name)) continue;
|
|
660
|
+
seen.add(name);
|
|
661
|
+
let owner = owners[name];
|
|
662
|
+
if (!owner) {
|
|
663
|
+
owner = uniqueOwnerByName.get(name) ?? "";
|
|
664
|
+
if (owner) {
|
|
665
|
+
owners[name] = owner;
|
|
666
|
+
ownersChanged = true;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
synced.push({ ...owner ? { owner } : {}, name, enabled: skill.enabled !== false });
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (ownersChanged) {
|
|
673
|
+
await writeSkillsSyncState({ ...state, installedOwners: owners });
|
|
674
|
+
}
|
|
675
|
+
synced.sort((a, b) => `${a.owner ?? ""}/${a.name}`.localeCompare(`${b.owner ?? ""}/${b.name}`));
|
|
676
|
+
return synced;
|
|
677
|
+
}
|
|
678
|
+
async function autoPushSyncedSkills(appServer) {
|
|
679
|
+
const state = await readSkillsSyncState();
|
|
680
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) return;
|
|
681
|
+
if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
|
|
682
|
+
throw new Error("Refusing to push to upstream skills repository");
|
|
683
|
+
}
|
|
684
|
+
const local = await collectLocalSyncedSkills(appServer);
|
|
685
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
686
|
+
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
687
|
+
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
688
|
+
}
|
|
689
|
+
async function ensureCodexAgentsSymlinkToSkillsAgents() {
|
|
690
|
+
const codexHomeDir = getCodexHomeDir();
|
|
691
|
+
const skillsAgentsPath = join(codexHomeDir, "skills", "AGENTS.md");
|
|
692
|
+
const codexAgentsPath = join(codexHomeDir, "AGENTS.md");
|
|
693
|
+
await mkdir(join(codexHomeDir, "skills"), { recursive: true });
|
|
694
|
+
let copiedFromCodex = false;
|
|
695
|
+
try {
|
|
696
|
+
const codexAgentsStat = await lstat(codexAgentsPath);
|
|
697
|
+
if (codexAgentsStat.isFile() || codexAgentsStat.isSymbolicLink()) {
|
|
698
|
+
const content = await readFile(codexAgentsPath, "utf8");
|
|
699
|
+
await writeFile(skillsAgentsPath, content, "utf8");
|
|
700
|
+
copiedFromCodex = true;
|
|
701
|
+
} else {
|
|
702
|
+
await rm(codexAgentsPath, { force: true, recursive: true });
|
|
703
|
+
}
|
|
704
|
+
} catch {
|
|
705
|
+
}
|
|
706
|
+
if (!copiedFromCodex) {
|
|
707
|
+
try {
|
|
708
|
+
const skillsAgentsStat = await stat(skillsAgentsPath);
|
|
709
|
+
if (!skillsAgentsStat.isFile()) {
|
|
710
|
+
await rm(skillsAgentsPath, { force: true, recursive: true });
|
|
711
|
+
await writeFile(skillsAgentsPath, "", "utf8");
|
|
712
|
+
}
|
|
713
|
+
} catch {
|
|
714
|
+
await writeFile(skillsAgentsPath, "", "utf8");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const relativeTarget = join("skills", "AGENTS.md");
|
|
718
|
+
try {
|
|
719
|
+
const current = await lstat(codexAgentsPath);
|
|
720
|
+
if (current.isSymbolicLink()) {
|
|
721
|
+
const existingTarget = await readlink(codexAgentsPath);
|
|
722
|
+
if (existingTarget === relativeTarget) return;
|
|
723
|
+
}
|
|
724
|
+
await rm(codexAgentsPath, { force: true, recursive: true });
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
await symlink(relativeTarget, codexAgentsPath);
|
|
728
|
+
}
|
|
729
|
+
async function initializeSkillsSyncOnStartup(appServer) {
|
|
730
|
+
if (startupSkillsSyncInitialized) return;
|
|
731
|
+
startupSkillsSyncInitialized = true;
|
|
732
|
+
startupSyncStatus.inProgress = true;
|
|
733
|
+
startupSyncStatus.lastRunAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
734
|
+
startupSyncStatus.lastError = "";
|
|
735
|
+
startupSyncStatus.branch = getPreferredSyncBranch();
|
|
736
|
+
try {
|
|
737
|
+
const state = await readSkillsSyncState();
|
|
738
|
+
if (!state.githubToken) {
|
|
739
|
+
await ensureCodexAgentsSymlinkToSkillsAgents();
|
|
740
|
+
if (!isAndroidLikeRuntime()) {
|
|
741
|
+
startupSyncStatus.mode = "idle";
|
|
742
|
+
startupSyncStatus.lastAction = "skip-upstream-non-android";
|
|
743
|
+
startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
startupSyncStatus.mode = "unauthenticated-bootstrap";
|
|
747
|
+
startupSyncStatus.lastAction = "pull-upstream";
|
|
748
|
+
await bootstrapSkillsFromUpstreamIntoLocal();
|
|
749
|
+
try {
|
|
750
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
751
|
+
} catch {
|
|
752
|
+
}
|
|
753
|
+
startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
754
|
+
startupSyncStatus.lastAction = "pull-upstream-complete";
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
startupSyncStatus.mode = "authenticated-fork-sync";
|
|
758
|
+
startupSyncStatus.lastAction = "ensure-private-fork";
|
|
759
|
+
const username = state.githubUsername || await resolveGithubUsername(state.githubToken);
|
|
760
|
+
const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
|
|
761
|
+
await ensurePrivateForkFromUpstream(state.githubToken, username, repoName);
|
|
762
|
+
await writeSkillsSyncState({ ...state, githubUsername: username, repoOwner: username, repoName });
|
|
763
|
+
startupSyncStatus.lastAction = "pull-private-fork";
|
|
764
|
+
await pullInstalledSkillsFolderFromRepo(state.githubToken, username, repoName);
|
|
765
|
+
try {
|
|
766
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
767
|
+
} catch {
|
|
768
|
+
}
|
|
769
|
+
startupSyncStatus.lastAction = "push-private-fork";
|
|
770
|
+
await autoPushSyncedSkills(appServer);
|
|
771
|
+
startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
772
|
+
startupSyncStatus.lastAction = "startup-sync-complete";
|
|
773
|
+
} catch (error) {
|
|
774
|
+
startupSyncStatus.lastError = getErrorMessage(error, "startup-sync-failed");
|
|
775
|
+
startupSyncStatus.lastAction = "startup-sync-failed";
|
|
776
|
+
} finally {
|
|
777
|
+
startupSyncStatus.inProgress = false;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
async function finalizeGithubLoginAndSync(token, username, appServer) {
|
|
781
|
+
const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
|
|
782
|
+
await ensurePrivateForkFromUpstream(token, username, repoName);
|
|
783
|
+
const current = await readSkillsSyncState();
|
|
784
|
+
await writeSkillsSyncState({ ...current, githubToken: token, githubUsername: username, repoOwner: username, repoName });
|
|
785
|
+
await pullInstalledSkillsFolderFromRepo(token, username, repoName);
|
|
786
|
+
try {
|
|
787
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
788
|
+
} catch {
|
|
789
|
+
}
|
|
790
|
+
await autoPushSyncedSkills(appServer);
|
|
791
|
+
}
|
|
792
|
+
async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
|
|
793
|
+
const q = query.toLowerCase().trim();
|
|
794
|
+
const filtered = q ? allEntries.filter((s) => {
|
|
795
|
+
if (s.name.toLowerCase().includes(q) || s.owner.toLowerCase().includes(q)) return true;
|
|
796
|
+
const cached = metaCache.get(`${s.owner}/${s.name}`);
|
|
797
|
+
return Boolean(cached?.displayName?.toLowerCase().includes(q));
|
|
798
|
+
}) : allEntries;
|
|
799
|
+
const page = filtered.slice(0, Math.min(limit * 2, 200));
|
|
800
|
+
await fetchMetaBatch(page);
|
|
801
|
+
let results = page.map(buildHubEntry);
|
|
802
|
+
if (sort === "date") {
|
|
803
|
+
results.sort((a, b) => b.publishedAt - a.publishedAt);
|
|
804
|
+
} else if (q) {
|
|
805
|
+
results.sort((a, b) => {
|
|
806
|
+
const aExact = a.name.toLowerCase() === q ? 1 : 0;
|
|
807
|
+
const bExact = b.name.toLowerCase() === q ? 1 : 0;
|
|
808
|
+
if (aExact !== bExact) return bExact - aExact;
|
|
809
|
+
return b.publishedAt - a.publishedAt;
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
return results.slice(0, limit).map((s) => {
|
|
813
|
+
const local = installedMap.get(s.name);
|
|
814
|
+
return local ? { ...s, installed: true, path: local.path, enabled: local.enabled } : s;
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
async function handleSkillsRoutes(req, res, url, context) {
|
|
818
|
+
const { appServer, readJsonBody: readJsonBody2 } = context;
|
|
819
|
+
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
|
|
820
|
+
try {
|
|
821
|
+
const q = url.searchParams.get("q") || "";
|
|
822
|
+
const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
|
|
823
|
+
const sort = url.searchParams.get("sort") || "date";
|
|
824
|
+
const allEntries = await fetchSkillsTree();
|
|
825
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
826
|
+
try {
|
|
827
|
+
const result = await appServer.rpc("skills/list", {});
|
|
828
|
+
for (const entry of result.data ?? []) {
|
|
829
|
+
for (const skill of entry.skills ?? []) {
|
|
830
|
+
if (skill.name) {
|
|
831
|
+
installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
} catch {
|
|
836
|
+
}
|
|
837
|
+
const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
|
|
838
|
+
await fetchMetaBatch(installedHubEntries);
|
|
839
|
+
const installed = [];
|
|
840
|
+
for (const [, info] of installedMap) {
|
|
841
|
+
const hubEntry = allEntries.find((e) => e.name === info.name);
|
|
842
|
+
const base = hubEntry ? buildHubEntry(hubEntry) : {
|
|
843
|
+
name: info.name,
|
|
844
|
+
owner: "local",
|
|
845
|
+
description: "",
|
|
846
|
+
displayName: "",
|
|
847
|
+
publishedAt: 0,
|
|
848
|
+
avatarUrl: "",
|
|
849
|
+
url: "",
|
|
850
|
+
installed: false
|
|
851
|
+
};
|
|
852
|
+
installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
|
|
853
|
+
}
|
|
854
|
+
const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
|
|
855
|
+
setJson(res, 200, { data: results, installed, total: allEntries.length });
|
|
856
|
+
} catch (error) {
|
|
857
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch skills hub") });
|
|
858
|
+
}
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
|
|
862
|
+
const state = await readSkillsSyncState();
|
|
863
|
+
setJson(res, 200, {
|
|
864
|
+
data: {
|
|
865
|
+
loggedIn: Boolean(state.githubToken),
|
|
866
|
+
githubUsername: state.githubUsername ?? "",
|
|
867
|
+
repoOwner: state.repoOwner ?? "",
|
|
868
|
+
repoName: state.repoName ?? "",
|
|
869
|
+
configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
|
|
870
|
+
startup: {
|
|
871
|
+
inProgress: startupSyncStatus.inProgress,
|
|
872
|
+
mode: startupSyncStatus.mode,
|
|
873
|
+
branch: startupSyncStatus.branch,
|
|
874
|
+
lastAction: startupSyncStatus.lastAction,
|
|
875
|
+
lastRunAtIso: startupSyncStatus.lastRunAtIso,
|
|
876
|
+
lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
|
|
877
|
+
lastError: startupSyncStatus.lastError
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
|
|
884
|
+
try {
|
|
885
|
+
const started = await startGithubDeviceLogin();
|
|
886
|
+
setJson(res, 200, { data: started });
|
|
887
|
+
} catch (error) {
|
|
888
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to start GitHub login") });
|
|
889
|
+
}
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
|
|
893
|
+
try {
|
|
894
|
+
const payload = asRecord(await readJsonBody2(req));
|
|
895
|
+
const token = typeof payload?.token === "string" ? payload.token.trim() : "";
|
|
896
|
+
if (!token) {
|
|
897
|
+
setJson(res, 400, { error: "Missing GitHub token" });
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
const username = await resolveGithubUsername(token);
|
|
901
|
+
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
902
|
+
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
903
|
+
} catch (error) {
|
|
904
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to login with GitHub token") });
|
|
905
|
+
}
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
|
|
909
|
+
try {
|
|
910
|
+
const state = await readSkillsSyncState();
|
|
911
|
+
await writeSkillsSyncState({
|
|
912
|
+
...state,
|
|
913
|
+
githubToken: void 0,
|
|
914
|
+
githubUsername: void 0,
|
|
915
|
+
repoOwner: void 0,
|
|
916
|
+
repoName: void 0
|
|
917
|
+
});
|
|
918
|
+
setJson(res, 200, { ok: true });
|
|
919
|
+
} catch (error) {
|
|
920
|
+
setJson(res, 500, { error: getErrorMessage(error, "Failed to logout GitHub") });
|
|
921
|
+
}
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
|
|
925
|
+
try {
|
|
926
|
+
const payload = asRecord(await readJsonBody2(req));
|
|
927
|
+
const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
|
|
928
|
+
if (!deviceCode) {
|
|
929
|
+
setJson(res, 400, { error: "Missing deviceCode" });
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
const result = await completeGithubDeviceLogin(deviceCode);
|
|
933
|
+
if (!result.token) {
|
|
934
|
+
setJson(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
const token = result.token;
|
|
938
|
+
const username = await resolveGithubUsername(token);
|
|
939
|
+
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
940
|
+
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
941
|
+
} catch (error) {
|
|
942
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to complete GitHub login") });
|
|
943
|
+
}
|
|
944
|
+
return true;
|
|
945
|
+
}
|
|
946
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
|
|
947
|
+
try {
|
|
948
|
+
const state = await readSkillsSyncState();
|
|
949
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
950
|
+
setJson(res, 400, { error: "Skills sync is not configured yet" });
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
|
|
954
|
+
setJson(res, 400, { error: "Refusing to push to upstream repository" });
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
const local = await collectLocalSyncedSkills(appServer);
|
|
958
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
959
|
+
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
960
|
+
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
961
|
+
setJson(res, 200, { ok: true, data: { synced: local.length } });
|
|
962
|
+
} catch (error) {
|
|
963
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to push synced skills") });
|
|
964
|
+
}
|
|
965
|
+
return true;
|
|
966
|
+
}
|
|
967
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
|
|
968
|
+
try {
|
|
969
|
+
const state = await readSkillsSyncState();
|
|
970
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
971
|
+
await bootstrapSkillsFromUpstreamIntoLocal();
|
|
972
|
+
try {
|
|
973
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
974
|
+
} catch {
|
|
975
|
+
}
|
|
976
|
+
setJson(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
|
|
980
|
+
const tree = await fetchSkillsTree();
|
|
981
|
+
const uniqueOwnerByName = /* @__PURE__ */ new Map();
|
|
982
|
+
const ambiguousNames = /* @__PURE__ */ new Set();
|
|
983
|
+
for (const entry of tree) {
|
|
984
|
+
if (ambiguousNames.has(entry.name)) continue;
|
|
985
|
+
const existingOwner = uniqueOwnerByName.get(entry.name);
|
|
986
|
+
if (!existingOwner) {
|
|
987
|
+
uniqueOwnerByName.set(entry.name, entry.owner);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (existingOwner !== entry.owner) {
|
|
991
|
+
uniqueOwnerByName.delete(entry.name);
|
|
992
|
+
ambiguousNames.add(entry.name);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const localDir = await detectUserSkillsDir(appServer);
|
|
996
|
+
await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
|
|
997
|
+
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
998
|
+
const localSkills = await scanInstalledSkillsFromDisk();
|
|
999
|
+
for (const skill of remote) {
|
|
1000
|
+
const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
|
|
1001
|
+
if (!owner) continue;
|
|
1002
|
+
if (!localSkills.has(skill.name)) {
|
|
1003
|
+
await runCommand("python3", [
|
|
1004
|
+
installerScript,
|
|
1005
|
+
"--repo",
|
|
1006
|
+
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
1007
|
+
"--path",
|
|
1008
|
+
`skills/${owner}/${skill.name}`,
|
|
1009
|
+
"--dest",
|
|
1010
|
+
localDir,
|
|
1011
|
+
"--method",
|
|
1012
|
+
"git"
|
|
1013
|
+
]);
|
|
1014
|
+
}
|
|
1015
|
+
const skillPath = join(localDir, skill.name);
|
|
1016
|
+
await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
|
|
1017
|
+
}
|
|
1018
|
+
const remoteNames = new Set(remote.map((row) => row.name));
|
|
1019
|
+
for (const [name, localInfo] of localSkills.entries()) {
|
|
1020
|
+
if (!remoteNames.has(name)) {
|
|
1021
|
+
await rm(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const nextOwners = {};
|
|
1025
|
+
for (const item of remote) {
|
|
1026
|
+
const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
|
|
1027
|
+
if (owner) nextOwners[item.name] = owner;
|
|
1028
|
+
}
|
|
1029
|
+
await writeSkillsSyncState({ ...state, installedOwners: nextOwners });
|
|
1030
|
+
try {
|
|
1031
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
1032
|
+
} catch {
|
|
1033
|
+
}
|
|
1034
|
+
setJson(res, 200, { ok: true, data: { synced: remote.length } });
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to pull synced skills") });
|
|
1037
|
+
}
|
|
1038
|
+
return true;
|
|
1039
|
+
}
|
|
1040
|
+
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
|
|
1041
|
+
try {
|
|
1042
|
+
const owner = url.searchParams.get("owner") || "";
|
|
1043
|
+
const name = url.searchParams.get("name") || "";
|
|
1044
|
+
if (!owner || !name) {
|
|
1045
|
+
setJson(res, 400, { error: "Missing owner or name" });
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
|
|
1049
|
+
const resp = await fetch(rawUrl);
|
|
1050
|
+
if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
|
|
1051
|
+
const content = await resp.text();
|
|
1052
|
+
setJson(res, 200, { content });
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
|
|
1055
|
+
}
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
|
|
1059
|
+
try {
|
|
1060
|
+
const payload = asRecord(await readJsonBody2(req));
|
|
1061
|
+
const owner = typeof payload?.owner === "string" ? payload.owner : "";
|
|
1062
|
+
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1063
|
+
if (!owner || !name) {
|
|
1064
|
+
setJson(res, 400, { error: "Missing owner or name" });
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
1068
|
+
const installDest = await detectUserSkillsDir(appServer);
|
|
1069
|
+
await runCommand("python3", [
|
|
1070
|
+
installerScript,
|
|
1071
|
+
"--repo",
|
|
1072
|
+
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
1073
|
+
"--path",
|
|
1074
|
+
`skills/${owner}/${name}`,
|
|
1075
|
+
"--dest",
|
|
1076
|
+
installDest,
|
|
1077
|
+
"--method",
|
|
1078
|
+
"git"
|
|
1079
|
+
]);
|
|
1080
|
+
const skillDir = join(installDest, name);
|
|
1081
|
+
await ensureInstalledSkillIsValid(appServer, skillDir);
|
|
1082
|
+
const syncState = await readSkillsSyncState();
|
|
1083
|
+
const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
|
|
1084
|
+
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1085
|
+
await autoPushSyncedSkills(appServer);
|
|
1086
|
+
setJson(res, 200, { ok: true, path: skillDir });
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
|
|
1089
|
+
}
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
|
|
1093
|
+
try {
|
|
1094
|
+
const payload = asRecord(await readJsonBody2(req));
|
|
1095
|
+
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1096
|
+
const path = typeof payload?.path === "string" ? payload.path : "";
|
|
1097
|
+
const target = path || (name ? join(getSkillsInstallDir(), name) : "");
|
|
1098
|
+
if (!target) {
|
|
1099
|
+
setJson(res, 400, { error: "Missing name or path" });
|
|
1100
|
+
return true;
|
|
1101
|
+
}
|
|
1102
|
+
await rm(target, { recursive: true, force: true });
|
|
1103
|
+
if (name) {
|
|
1104
|
+
const syncState = await readSkillsSyncState();
|
|
1105
|
+
const nextOwners = { ...syncState.installedOwners ?? {} };
|
|
1106
|
+
delete nextOwners[name];
|
|
1107
|
+
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1108
|
+
}
|
|
1109
|
+
await autoPushSyncedSkills(appServer);
|
|
1110
|
+
try {
|
|
1111
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
1112
|
+
} catch {
|
|
1113
|
+
}
|
|
1114
|
+
setJson(res, 200, { ok: true, deletedPath: target });
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to uninstall skill") });
|
|
1117
|
+
}
|
|
1118
|
+
return true;
|
|
1119
|
+
}
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/server/codexAppServerBridge.ts
|
|
1124
|
+
function asRecord2(value) {
|
|
1125
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1126
|
+
}
|
|
1127
|
+
function getErrorMessage2(payload, fallback) {
|
|
1128
|
+
if (payload instanceof Error && payload.message.trim().length > 0) {
|
|
1129
|
+
return payload.message;
|
|
1130
|
+
}
|
|
1131
|
+
const record = asRecord2(payload);
|
|
1132
|
+
if (!record) return fallback;
|
|
1133
|
+
const error = record.error;
|
|
1134
|
+
if (typeof error === "string" && error.length > 0) return error;
|
|
1135
|
+
const nestedError = asRecord2(error);
|
|
1136
|
+
if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
|
|
1137
|
+
return nestedError.message;
|
|
1138
|
+
}
|
|
1139
|
+
return fallback;
|
|
1140
|
+
}
|
|
1141
|
+
function setJson2(res, statusCode, payload) {
|
|
1142
|
+
res.statusCode = statusCode;
|
|
1143
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
1144
|
+
res.end(JSON.stringify(payload));
|
|
1145
|
+
}
|
|
1146
|
+
function extractThreadMessageText(threadReadPayload) {
|
|
1147
|
+
const payload = asRecord2(threadReadPayload);
|
|
1148
|
+
const thread = asRecord2(payload?.thread);
|
|
1149
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
1150
|
+
const parts = [];
|
|
1151
|
+
for (const turn of turns) {
|
|
1152
|
+
const turnRecord = asRecord2(turn);
|
|
1153
|
+
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
1154
|
+
for (const item of items) {
|
|
1155
|
+
const itemRecord = asRecord2(item);
|
|
1156
|
+
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
1157
|
+
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
1158
|
+
parts.push(itemRecord.text.trim());
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
if (type === "userMessage") {
|
|
1162
|
+
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
1163
|
+
for (const block of content) {
|
|
1164
|
+
const blockRecord = asRecord2(block);
|
|
1165
|
+
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
1166
|
+
parts.push(blockRecord.text.trim());
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
if (type === "commandExecution") {
|
|
1172
|
+
const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
|
|
1173
|
+
const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
|
|
1174
|
+
if (command) parts.push(command);
|
|
1175
|
+
if (output) parts.push(output);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return parts.join("\n").trim();
|
|
1180
|
+
}
|
|
1181
|
+
function isExactPhraseMatch(query, doc) {
|
|
1182
|
+
const q = query.trim().toLowerCase();
|
|
1183
|
+
if (!q) return false;
|
|
1184
|
+
return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
|
|
1185
|
+
}
|
|
1186
|
+
function scoreFileCandidate(path, query) {
|
|
1187
|
+
if (!query) return 0;
|
|
1188
|
+
const lowerPath = path.toLowerCase();
|
|
1189
|
+
const lowerQuery = query.toLowerCase();
|
|
1190
|
+
const baseName = lowerPath.slice(lowerPath.lastIndexOf("/") + 1);
|
|
1191
|
+
if (baseName === lowerQuery) return 0;
|
|
1192
|
+
if (baseName.startsWith(lowerQuery)) return 1;
|
|
1193
|
+
if (baseName.includes(lowerQuery)) return 2;
|
|
1194
|
+
if (lowerPath.includes(`/${lowerQuery}`)) return 3;
|
|
1195
|
+
if (lowerPath.includes(lowerQuery)) return 4;
|
|
1196
|
+
return 10;
|
|
1197
|
+
}
|
|
1198
|
+
async function listFilesWithRipgrep(cwd) {
|
|
1199
|
+
return await new Promise((resolve3, reject) => {
|
|
1200
|
+
const proc = spawn2("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
|
|
1201
|
+
cwd,
|
|
1202
|
+
env: process.env,
|
|
1203
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1204
|
+
});
|
|
1205
|
+
let stdout = "";
|
|
1206
|
+
let stderr = "";
|
|
1207
|
+
proc.stdout.on("data", (chunk) => {
|
|
1208
|
+
stdout += chunk.toString();
|
|
1209
|
+
});
|
|
1210
|
+
proc.stderr.on("data", (chunk) => {
|
|
1211
|
+
stderr += chunk.toString();
|
|
1212
|
+
});
|
|
1213
|
+
proc.on("error", reject);
|
|
1214
|
+
proc.on("close", (code) => {
|
|
1215
|
+
if (code === 0) {
|
|
1216
|
+
const rows = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1217
|
+
resolve3(rows);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
1221
|
+
reject(new Error(details || "rg --files failed"));
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
function getCodexHomeDir2() {
|
|
1226
|
+
const codexHome = process.env.CODEX_HOME?.trim();
|
|
1227
|
+
return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
|
|
1228
|
+
}
|
|
1229
|
+
async function runCommand2(command, args, options = {}) {
|
|
1230
|
+
await new Promise((resolve3, reject) => {
|
|
1231
|
+
const proc = spawn2(command, args, {
|
|
1232
|
+
cwd: options.cwd,
|
|
1233
|
+
env: process.env,
|
|
1234
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1235
|
+
});
|
|
1236
|
+
let stdout = "";
|
|
1237
|
+
let stderr = "";
|
|
1238
|
+
proc.stdout.on("data", (chunk) => {
|
|
1239
|
+
stdout += chunk.toString();
|
|
1240
|
+
});
|
|
1241
|
+
proc.stderr.on("data", (chunk) => {
|
|
1242
|
+
stderr += chunk.toString();
|
|
1243
|
+
});
|
|
1244
|
+
proc.on("error", reject);
|
|
1245
|
+
proc.on("close", (code) => {
|
|
1246
|
+
if (code === 0) {
|
|
1247
|
+
resolve3();
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
1251
|
+
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
1252
|
+
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
291
1253
|
});
|
|
292
|
-
}
|
|
293
|
-
skillsTreeCache = { entries, fetchedAt: Date.now() };
|
|
294
|
-
return entries;
|
|
1254
|
+
});
|
|
295
1255
|
}
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
const batch = toFetch.slice(0, 50);
|
|
300
|
-
const results = await Promise.allSettled(
|
|
301
|
-
batch.map(async (e) => {
|
|
302
|
-
const rawUrl = `https://raw.githubusercontent.com/openclaw/skills/main/skills/${e.owner}/${e.name}/_meta.json`;
|
|
303
|
-
const resp = await fetch(rawUrl);
|
|
304
|
-
if (!resp.ok) return;
|
|
305
|
-
const meta = await resp.json();
|
|
306
|
-
metaCache.set(`${e.owner}/${e.name}`, {
|
|
307
|
-
displayName: typeof meta.displayName === "string" ? meta.displayName : "",
|
|
308
|
-
description: typeof meta.displayName === "string" ? meta.displayName : "",
|
|
309
|
-
publishedAt: meta.latest?.publishedAt ?? 0
|
|
310
|
-
});
|
|
311
|
-
})
|
|
312
|
-
);
|
|
313
|
-
void results;
|
|
1256
|
+
function isMissingHeadError(error) {
|
|
1257
|
+
const message = getErrorMessage2(error, "").toLowerCase();
|
|
1258
|
+
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
|
|
314
1259
|
}
|
|
315
|
-
function
|
|
316
|
-
const
|
|
317
|
-
return
|
|
318
|
-
name: e.name,
|
|
319
|
-
owner: e.owner,
|
|
320
|
-
description: cached?.description ?? "",
|
|
321
|
-
displayName: cached?.displayName ?? "",
|
|
322
|
-
publishedAt: cached?.publishedAt ?? 0,
|
|
323
|
-
avatarUrl: `https://github.com/${e.owner}.png?size=40`,
|
|
324
|
-
url: e.url,
|
|
325
|
-
installed: false
|
|
326
|
-
};
|
|
1260
|
+
function isNotGitRepositoryError(error) {
|
|
1261
|
+
const message = getErrorMessage2(error, "").toLowerCase();
|
|
1262
|
+
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
327
1263
|
}
|
|
328
|
-
async function
|
|
329
|
-
const
|
|
330
|
-
const skillsDir = getSkillsInstallDir();
|
|
1264
|
+
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
1265
|
+
const agentsPath = join2(repoRoot, "AGENTS.md");
|
|
331
1266
|
try {
|
|
332
|
-
|
|
333
|
-
for (const entry of entries) {
|
|
334
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
335
|
-
const skillMd = join(skillsDir, entry.name, "SKILL.md");
|
|
336
|
-
try {
|
|
337
|
-
await stat(skillMd);
|
|
338
|
-
map.set(entry.name, { name: entry.name, path: skillMd, enabled: true });
|
|
339
|
-
} catch {
|
|
340
|
-
}
|
|
341
|
-
}
|
|
1267
|
+
await stat2(agentsPath);
|
|
342
1268
|
} catch {
|
|
1269
|
+
await writeFile2(agentsPath, "", "utf8");
|
|
343
1270
|
}
|
|
344
|
-
|
|
1271
|
+
await runCommand2("git", ["add", "AGENTS.md"], { cwd: repoRoot });
|
|
1272
|
+
await runCommand2(
|
|
1273
|
+
"git",
|
|
1274
|
+
["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
|
|
1275
|
+
{ cwd: repoRoot }
|
|
1276
|
+
);
|
|
345
1277
|
}
|
|
346
|
-
async function
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (
|
|
364
|
-
|
|
1278
|
+
async function runCommandCapture(command, args, options = {}) {
|
|
1279
|
+
return await new Promise((resolve3, reject) => {
|
|
1280
|
+
const proc = spawn2(command, args, {
|
|
1281
|
+
cwd: options.cwd,
|
|
1282
|
+
env: process.env,
|
|
1283
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1284
|
+
});
|
|
1285
|
+
let stdout = "";
|
|
1286
|
+
let stderr = "";
|
|
1287
|
+
proc.stdout.on("data", (chunk) => {
|
|
1288
|
+
stdout += chunk.toString();
|
|
1289
|
+
});
|
|
1290
|
+
proc.stderr.on("data", (chunk) => {
|
|
1291
|
+
stderr += chunk.toString();
|
|
1292
|
+
});
|
|
1293
|
+
proc.on("error", reject);
|
|
1294
|
+
proc.on("close", (code) => {
|
|
1295
|
+
if (code === 0) {
|
|
1296
|
+
resolve3(stdout.trim());
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
1300
|
+
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
1301
|
+
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
365
1302
|
});
|
|
366
|
-
}
|
|
367
|
-
return results.slice(0, limit).map((s) => {
|
|
368
|
-
const local = installedMap.get(s.name);
|
|
369
|
-
return local ? { ...s, installed: true, path: local.path, enabled: local.enabled } : s;
|
|
370
1303
|
});
|
|
371
1304
|
}
|
|
372
1305
|
function normalizeStringArray(value) {
|
|
@@ -390,11 +1323,11 @@ function normalizeStringRecord(value) {
|
|
|
390
1323
|
return next;
|
|
391
1324
|
}
|
|
392
1325
|
function getCodexAuthPath() {
|
|
393
|
-
return
|
|
1326
|
+
return join2(getCodexHomeDir2(), "auth.json");
|
|
394
1327
|
}
|
|
395
1328
|
async function readCodexAuth() {
|
|
396
1329
|
try {
|
|
397
|
-
const raw = await
|
|
1330
|
+
const raw = await readFile2(getCodexAuthPath(), "utf8");
|
|
398
1331
|
const auth = JSON.parse(raw);
|
|
399
1332
|
const token = auth.tokens?.access_token;
|
|
400
1333
|
if (!token) return null;
|
|
@@ -404,13 +1337,21 @@ async function readCodexAuth() {
|
|
|
404
1337
|
}
|
|
405
1338
|
}
|
|
406
1339
|
function getCodexGlobalStatePath() {
|
|
407
|
-
return
|
|
1340
|
+
return join2(getCodexHomeDir2(), ".codex-global-state.json");
|
|
1341
|
+
}
|
|
1342
|
+
function getCodexSessionIndexPath() {
|
|
1343
|
+
return join2(getCodexHomeDir2(), "session_index.jsonl");
|
|
408
1344
|
}
|
|
409
1345
|
var MAX_THREAD_TITLES = 500;
|
|
1346
|
+
var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
|
|
1347
|
+
var sessionIndexThreadTitleCacheState = {
|
|
1348
|
+
fileSignature: null,
|
|
1349
|
+
cache: EMPTY_THREAD_TITLE_CACHE
|
|
1350
|
+
};
|
|
410
1351
|
function normalizeThreadTitleCache(value) {
|
|
411
|
-
const record =
|
|
412
|
-
if (!record) return
|
|
413
|
-
const rawTitles =
|
|
1352
|
+
const record = asRecord2(value);
|
|
1353
|
+
if (!record) return EMPTY_THREAD_TITLE_CACHE;
|
|
1354
|
+
const rawTitles = asRecord2(record.titles);
|
|
414
1355
|
const titles = {};
|
|
415
1356
|
if (rawTitles) {
|
|
416
1357
|
for (const [k, v] of Object.entries(rawTitles)) {
|
|
@@ -433,35 +1374,139 @@ function removeFromThreadTitleCache(cache, id) {
|
|
|
433
1374
|
const { [id]: _, ...titles } = cache.titles;
|
|
434
1375
|
return { titles, order: cache.order.filter((o) => o !== id) };
|
|
435
1376
|
}
|
|
1377
|
+
function normalizeSessionIndexThreadTitle(value) {
|
|
1378
|
+
const record = asRecord2(value);
|
|
1379
|
+
if (!record) return null;
|
|
1380
|
+
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
1381
|
+
const title = typeof record.thread_name === "string" ? record.thread_name.trim() : "";
|
|
1382
|
+
const updatedAtIso = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
|
|
1383
|
+
const updatedAtMs = updatedAtIso ? Date.parse(updatedAtIso) : Number.NaN;
|
|
1384
|
+
if (!id || !title) return null;
|
|
1385
|
+
return {
|
|
1386
|
+
id,
|
|
1387
|
+
title,
|
|
1388
|
+
updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
function trimThreadTitleCache(cache) {
|
|
1392
|
+
const titles = { ...cache.titles };
|
|
1393
|
+
const order = cache.order.filter((id) => {
|
|
1394
|
+
if (!titles[id]) return false;
|
|
1395
|
+
return true;
|
|
1396
|
+
}).slice(0, MAX_THREAD_TITLES);
|
|
1397
|
+
for (const id of Object.keys(titles)) {
|
|
1398
|
+
if (!order.includes(id)) {
|
|
1399
|
+
delete titles[id];
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return { titles, order };
|
|
1403
|
+
}
|
|
1404
|
+
function mergeThreadTitleCaches(base, overlay) {
|
|
1405
|
+
const titles = { ...base.titles, ...overlay.titles };
|
|
1406
|
+
const order = [];
|
|
1407
|
+
for (const id of [...overlay.order, ...base.order]) {
|
|
1408
|
+
if (!titles[id] || order.includes(id)) continue;
|
|
1409
|
+
order.push(id);
|
|
1410
|
+
}
|
|
1411
|
+
for (const id of Object.keys(titles)) {
|
|
1412
|
+
if (!order.includes(id)) {
|
|
1413
|
+
order.push(id);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return trimThreadTitleCache({ titles, order });
|
|
1417
|
+
}
|
|
436
1418
|
async function readThreadTitleCache() {
|
|
437
1419
|
const statePath = getCodexGlobalStatePath();
|
|
438
1420
|
try {
|
|
439
|
-
const raw = await
|
|
440
|
-
const payload =
|
|
1421
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1422
|
+
const payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
441
1423
|
return normalizeThreadTitleCache(payload["thread-titles"]);
|
|
442
1424
|
} catch {
|
|
443
|
-
return
|
|
1425
|
+
return EMPTY_THREAD_TITLE_CACHE;
|
|
444
1426
|
}
|
|
445
1427
|
}
|
|
446
1428
|
async function writeThreadTitleCache(cache) {
|
|
447
1429
|
const statePath = getCodexGlobalStatePath();
|
|
448
1430
|
let payload = {};
|
|
449
1431
|
try {
|
|
450
|
-
const raw = await
|
|
451
|
-
payload =
|
|
1432
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1433
|
+
payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
452
1434
|
} catch {
|
|
453
1435
|
payload = {};
|
|
454
1436
|
}
|
|
455
1437
|
payload["thread-titles"] = cache;
|
|
456
|
-
await
|
|
1438
|
+
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
1439
|
+
}
|
|
1440
|
+
function getSessionIndexFileSignature(stats) {
|
|
1441
|
+
return `${String(stats.mtimeMs)}:${String(stats.size)}`;
|
|
1442
|
+
}
|
|
1443
|
+
async function parseThreadTitlesFromSessionIndex(sessionIndexPath) {
|
|
1444
|
+
const latestById = /* @__PURE__ */ new Map();
|
|
1445
|
+
const input = createReadStream(sessionIndexPath, { encoding: "utf8" });
|
|
1446
|
+
const lines = createInterface({
|
|
1447
|
+
input,
|
|
1448
|
+
crlfDelay: Infinity
|
|
1449
|
+
});
|
|
1450
|
+
try {
|
|
1451
|
+
for await (const line of lines) {
|
|
1452
|
+
const trimmed = line.trim();
|
|
1453
|
+
if (!trimmed) continue;
|
|
1454
|
+
try {
|
|
1455
|
+
const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed));
|
|
1456
|
+
if (!entry) continue;
|
|
1457
|
+
const previous = latestById.get(entry.id);
|
|
1458
|
+
if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
|
|
1459
|
+
latestById.set(entry.id, entry);
|
|
1460
|
+
}
|
|
1461
|
+
} catch {
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
} finally {
|
|
1465
|
+
lines.close();
|
|
1466
|
+
input.close();
|
|
1467
|
+
}
|
|
1468
|
+
const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs);
|
|
1469
|
+
const titles = {};
|
|
1470
|
+
const order = [];
|
|
1471
|
+
for (const entry of entries) {
|
|
1472
|
+
titles[entry.id] = entry.title;
|
|
1473
|
+
order.push(entry.id);
|
|
1474
|
+
}
|
|
1475
|
+
return trimThreadTitleCache({ titles, order });
|
|
1476
|
+
}
|
|
1477
|
+
async function readThreadTitlesFromSessionIndex() {
|
|
1478
|
+
const sessionIndexPath = getCodexSessionIndexPath();
|
|
1479
|
+
try {
|
|
1480
|
+
const stats = await stat2(sessionIndexPath);
|
|
1481
|
+
const fileSignature = getSessionIndexFileSignature(stats);
|
|
1482
|
+
if (sessionIndexThreadTitleCacheState.fileSignature === fileSignature) {
|
|
1483
|
+
return sessionIndexThreadTitleCacheState.cache;
|
|
1484
|
+
}
|
|
1485
|
+
const cache = await parseThreadTitlesFromSessionIndex(sessionIndexPath);
|
|
1486
|
+
sessionIndexThreadTitleCacheState = { fileSignature, cache };
|
|
1487
|
+
return cache;
|
|
1488
|
+
} catch {
|
|
1489
|
+
sessionIndexThreadTitleCacheState = {
|
|
1490
|
+
fileSignature: "missing",
|
|
1491
|
+
cache: EMPTY_THREAD_TITLE_CACHE
|
|
1492
|
+
};
|
|
1493
|
+
return sessionIndexThreadTitleCacheState.cache;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
async function readMergedThreadTitleCache() {
|
|
1497
|
+
const [sessionIndexCache, persistedCache] = await Promise.all([
|
|
1498
|
+
readThreadTitlesFromSessionIndex(),
|
|
1499
|
+
readThreadTitleCache()
|
|
1500
|
+
]);
|
|
1501
|
+
return mergeThreadTitleCaches(persistedCache, sessionIndexCache);
|
|
457
1502
|
}
|
|
458
1503
|
async function readWorkspaceRootsState() {
|
|
459
1504
|
const statePath = getCodexGlobalStatePath();
|
|
460
1505
|
let payload = {};
|
|
461
1506
|
try {
|
|
462
|
-
const raw = await
|
|
1507
|
+
const raw = await readFile2(statePath, "utf8");
|
|
463
1508
|
const parsed = JSON.parse(raw);
|
|
464
|
-
payload =
|
|
1509
|
+
payload = asRecord2(parsed) ?? {};
|
|
465
1510
|
} catch {
|
|
466
1511
|
payload = {};
|
|
467
1512
|
}
|
|
@@ -475,15 +1520,15 @@ async function writeWorkspaceRootsState(nextState) {
|
|
|
475
1520
|
const statePath = getCodexGlobalStatePath();
|
|
476
1521
|
let payload = {};
|
|
477
1522
|
try {
|
|
478
|
-
const raw = await
|
|
479
|
-
payload =
|
|
1523
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1524
|
+
payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
480
1525
|
} catch {
|
|
481
1526
|
payload = {};
|
|
482
1527
|
}
|
|
483
1528
|
payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
|
|
484
1529
|
payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
|
|
485
1530
|
payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
|
|
486
|
-
await
|
|
1531
|
+
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
487
1532
|
}
|
|
488
1533
|
async function readJsonBody(req) {
|
|
489
1534
|
const raw = await readRawBody(req);
|
|
@@ -521,7 +1566,7 @@ function handleFileUpload(req, res) {
|
|
|
521
1566
|
const contentType = req.headers["content-type"] ?? "";
|
|
522
1567
|
const boundaryMatch = contentType.match(/boundary=(.+)/i);
|
|
523
1568
|
if (!boundaryMatch) {
|
|
524
|
-
|
|
1569
|
+
setJson2(res, 400, { error: "Missing multipart boundary" });
|
|
525
1570
|
return;
|
|
526
1571
|
}
|
|
527
1572
|
const boundary = boundaryMatch[1];
|
|
@@ -551,49 +1596,96 @@ function handleFileUpload(req, res) {
|
|
|
551
1596
|
break;
|
|
552
1597
|
}
|
|
553
1598
|
if (!fileData) {
|
|
554
|
-
|
|
1599
|
+
setJson2(res, 400, { error: "No file in request" });
|
|
555
1600
|
return;
|
|
556
1601
|
}
|
|
557
|
-
const uploadDir =
|
|
558
|
-
await
|
|
559
|
-
const destDir = await
|
|
560
|
-
const destPath =
|
|
561
|
-
await
|
|
562
|
-
|
|
1602
|
+
const uploadDir = join2(tmpdir2(), "codex-web-uploads");
|
|
1603
|
+
await mkdir2(uploadDir, { recursive: true });
|
|
1604
|
+
const destDir = await mkdtemp2(join2(uploadDir, "f-"));
|
|
1605
|
+
const destPath = join2(destDir, fileName);
|
|
1606
|
+
await writeFile2(destPath, fileData);
|
|
1607
|
+
setJson2(res, 200, { path: destPath });
|
|
563
1608
|
} catch (err) {
|
|
564
|
-
|
|
1609
|
+
setJson2(res, 500, { error: getErrorMessage2(err, "Upload failed") });
|
|
565
1610
|
}
|
|
566
1611
|
});
|
|
567
1612
|
req.on("error", (err) => {
|
|
568
|
-
|
|
1613
|
+
setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
function httpPost(url, headers, body) {
|
|
1617
|
+
const doRequest = url.startsWith("http://") ? httpRequest : httpsRequest;
|
|
1618
|
+
return new Promise((resolve3, reject) => {
|
|
1619
|
+
const req = doRequest(url, { method: "POST", headers }, (res) => {
|
|
1620
|
+
const chunks = [];
|
|
1621
|
+
res.on("data", (c) => chunks.push(c));
|
|
1622
|
+
res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
|
|
1623
|
+
res.on("error", reject);
|
|
1624
|
+
});
|
|
1625
|
+
req.on("error", reject);
|
|
1626
|
+
req.write(body);
|
|
1627
|
+
req.end();
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
var curlImpersonateAvailable = null;
|
|
1631
|
+
function curlImpersonatePost(url, headers, body) {
|
|
1632
|
+
return new Promise((resolve3, reject) => {
|
|
1633
|
+
const args = ["-s", "-w", "\n%{http_code}", "-X", "POST", url];
|
|
1634
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
1635
|
+
if (k.toLowerCase() === "content-length") continue;
|
|
1636
|
+
args.push("-H", `${k}: ${String(v)}`);
|
|
1637
|
+
}
|
|
1638
|
+
args.push("--data-binary", "@-");
|
|
1639
|
+
const proc = spawn2("curl-impersonate-chrome", args, {
|
|
1640
|
+
env: { ...process.env, CURL_IMPERSONATE: "chrome116" },
|
|
1641
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1642
|
+
});
|
|
1643
|
+
const chunks = [];
|
|
1644
|
+
proc.stdout.on("data", (c) => chunks.push(c));
|
|
1645
|
+
proc.on("error", (e) => {
|
|
1646
|
+
curlImpersonateAvailable = false;
|
|
1647
|
+
reject(e);
|
|
1648
|
+
});
|
|
1649
|
+
proc.on("close", (code) => {
|
|
1650
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1651
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
1652
|
+
const statusStr = lastNewline >= 0 ? raw.slice(lastNewline + 1).trim() : "";
|
|
1653
|
+
const responseBody = lastNewline >= 0 ? raw.slice(0, lastNewline) : raw;
|
|
1654
|
+
const status = parseInt(statusStr, 10) || (code === 0 ? 200 : 500);
|
|
1655
|
+
curlImpersonateAvailable = true;
|
|
1656
|
+
resolve3({ status, body: responseBody });
|
|
1657
|
+
});
|
|
1658
|
+
proc.stdin.write(body);
|
|
1659
|
+
proc.stdin.end();
|
|
569
1660
|
});
|
|
570
1661
|
}
|
|
571
1662
|
async function proxyTranscribe(body, contentType, authToken, accountId) {
|
|
572
|
-
const
|
|
1663
|
+
const chatgptHeaders = {
|
|
573
1664
|
"Content-Type": contentType,
|
|
574
1665
|
"Content-Length": body.length,
|
|
575
1666
|
Authorization: `Bearer ${authToken}`,
|
|
576
1667
|
originator: "Codex Desktop",
|
|
577
1668
|
"User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
|
|
578
1669
|
};
|
|
579
|
-
if (accountId)
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
1670
|
+
if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
|
|
1671
|
+
const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
|
|
1672
|
+
let result;
|
|
1673
|
+
try {
|
|
1674
|
+
result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
1675
|
+
} catch {
|
|
1676
|
+
result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
1677
|
+
}
|
|
1678
|
+
if (result.status === 403 && result.body.includes("cf_chl")) {
|
|
1679
|
+
if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
|
|
1680
|
+
try {
|
|
1681
|
+
const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
1682
|
+
if (ciResult.status !== 403) return ciResult;
|
|
1683
|
+
} catch {
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
|
|
1687
|
+
}
|
|
1688
|
+
return result;
|
|
597
1689
|
}
|
|
598
1690
|
var AppServerProcess = class {
|
|
599
1691
|
constructor() {
|
|
@@ -617,7 +1709,7 @@ var AppServerProcess = class {
|
|
|
617
1709
|
start() {
|
|
618
1710
|
if (this.process) return;
|
|
619
1711
|
this.stopping = false;
|
|
620
|
-
const proc =
|
|
1712
|
+
const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
621
1713
|
this.process = proc;
|
|
622
1714
|
proc.stdout.setEncoding("utf8");
|
|
623
1715
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -711,7 +1803,7 @@ var AppServerProcess = class {
|
|
|
711
1803
|
}
|
|
712
1804
|
this.pendingServerRequests.delete(requestId);
|
|
713
1805
|
this.sendServerRequestReply(requestId, reply);
|
|
714
|
-
const requestParams =
|
|
1806
|
+
const requestParams = asRecord2(pendingRequest.params);
|
|
715
1807
|
const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
|
|
716
1808
|
this.emitNotification({
|
|
717
1809
|
method: "server/request/resolved",
|
|
@@ -740,8 +1832,8 @@ var AppServerProcess = class {
|
|
|
740
1832
|
async call(method, params) {
|
|
741
1833
|
this.start();
|
|
742
1834
|
const id = this.nextId++;
|
|
743
|
-
return new Promise((
|
|
744
|
-
this.pending.set(id, { resolve:
|
|
1835
|
+
return new Promise((resolve3, reject) => {
|
|
1836
|
+
this.pending.set(id, { resolve: resolve3, reject });
|
|
745
1837
|
this.sendLine({
|
|
746
1838
|
jsonrpc: "2.0",
|
|
747
1839
|
id,
|
|
@@ -780,7 +1872,7 @@ var AppServerProcess = class {
|
|
|
780
1872
|
}
|
|
781
1873
|
async respondToServerRequest(payload) {
|
|
782
1874
|
await this.ensureInitialized();
|
|
783
|
-
const body =
|
|
1875
|
+
const body = asRecord2(payload);
|
|
784
1876
|
if (!body) {
|
|
785
1877
|
throw new Error("Invalid response payload: expected object");
|
|
786
1878
|
}
|
|
@@ -788,7 +1880,7 @@ var AppServerProcess = class {
|
|
|
788
1880
|
if (typeof id !== "number" || !Number.isInteger(id)) {
|
|
789
1881
|
throw new Error('Invalid response payload: "id" must be an integer');
|
|
790
1882
|
}
|
|
791
|
-
const rawError =
|
|
1883
|
+
const rawError = asRecord2(body.error);
|
|
792
1884
|
if (rawError) {
|
|
793
1885
|
const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
|
|
794
1886
|
const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
|
|
@@ -842,8 +1934,8 @@ var MethodCatalog = class {
|
|
|
842
1934
|
this.notificationCache = null;
|
|
843
1935
|
}
|
|
844
1936
|
async runGenerateSchemaCommand(outDir) {
|
|
845
|
-
await new Promise((
|
|
846
|
-
const process2 =
|
|
1937
|
+
await new Promise((resolve3, reject) => {
|
|
1938
|
+
const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
|
|
847
1939
|
stdio: ["ignore", "ignore", "pipe"]
|
|
848
1940
|
});
|
|
849
1941
|
let stderr = "";
|
|
@@ -854,7 +1946,7 @@ var MethodCatalog = class {
|
|
|
854
1946
|
process2.on("error", reject);
|
|
855
1947
|
process2.on("exit", (code) => {
|
|
856
1948
|
if (code === 0) {
|
|
857
|
-
|
|
1949
|
+
resolve3();
|
|
858
1950
|
return;
|
|
859
1951
|
}
|
|
860
1952
|
reject(new Error(stderr.trim() || `generate-json-schema exited with code ${String(code)}`));
|
|
@@ -862,13 +1954,13 @@ var MethodCatalog = class {
|
|
|
862
1954
|
});
|
|
863
1955
|
}
|
|
864
1956
|
extractMethodsFromClientRequest(payload) {
|
|
865
|
-
const root =
|
|
1957
|
+
const root = asRecord2(payload);
|
|
866
1958
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
867
1959
|
const methods = /* @__PURE__ */ new Set();
|
|
868
1960
|
for (const entry of oneOf) {
|
|
869
|
-
const row =
|
|
870
|
-
const properties =
|
|
871
|
-
const methodDef =
|
|
1961
|
+
const row = asRecord2(entry);
|
|
1962
|
+
const properties = asRecord2(row?.properties);
|
|
1963
|
+
const methodDef = asRecord2(properties?.method);
|
|
872
1964
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
873
1965
|
for (const item of methodEnum) {
|
|
874
1966
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -879,13 +1971,13 @@ var MethodCatalog = class {
|
|
|
879
1971
|
return Array.from(methods).sort((a, b) => a.localeCompare(b));
|
|
880
1972
|
}
|
|
881
1973
|
extractMethodsFromServerNotification(payload) {
|
|
882
|
-
const root =
|
|
1974
|
+
const root = asRecord2(payload);
|
|
883
1975
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
884
1976
|
const methods = /* @__PURE__ */ new Set();
|
|
885
1977
|
for (const entry of oneOf) {
|
|
886
|
-
const row =
|
|
887
|
-
const properties =
|
|
888
|
-
const methodDef =
|
|
1978
|
+
const row = asRecord2(entry);
|
|
1979
|
+
const properties = asRecord2(row?.properties);
|
|
1980
|
+
const methodDef = asRecord2(properties?.method);
|
|
889
1981
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
890
1982
|
for (const item of methodEnum) {
|
|
891
1983
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -899,10 +1991,10 @@ var MethodCatalog = class {
|
|
|
899
1991
|
if (this.methodCache) {
|
|
900
1992
|
return this.methodCache;
|
|
901
1993
|
}
|
|
902
|
-
const outDir = await
|
|
1994
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
903
1995
|
await this.runGenerateSchemaCommand(outDir);
|
|
904
|
-
const clientRequestPath =
|
|
905
|
-
const raw = await
|
|
1996
|
+
const clientRequestPath = join2(outDir, "ClientRequest.json");
|
|
1997
|
+
const raw = await readFile2(clientRequestPath, "utf8");
|
|
906
1998
|
const parsed = JSON.parse(raw);
|
|
907
1999
|
const methods = this.extractMethodsFromClientRequest(parsed);
|
|
908
2000
|
this.methodCache = methods;
|
|
@@ -912,10 +2004,10 @@ var MethodCatalog = class {
|
|
|
912
2004
|
if (this.notificationCache) {
|
|
913
2005
|
return this.notificationCache;
|
|
914
2006
|
}
|
|
915
|
-
const outDir = await
|
|
2007
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
916
2008
|
await this.runGenerateSchemaCommand(outDir);
|
|
917
|
-
const serverNotificationPath =
|
|
918
|
-
const raw = await
|
|
2009
|
+
const serverNotificationPath = join2(outDir, "ServerNotification.json");
|
|
2010
|
+
const raw = await readFile2(serverNotificationPath, "utf8");
|
|
919
2011
|
const parsed = JSON.parse(raw);
|
|
920
2012
|
const methods = this.extractMethodsFromServerNotification(parsed);
|
|
921
2013
|
this.notificationCache = methods;
|
|
@@ -938,7 +2030,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
938
2030
|
const threads = [];
|
|
939
2031
|
let cursor = null;
|
|
940
2032
|
do {
|
|
941
|
-
const response =
|
|
2033
|
+
const response = asRecord2(await appServer.rpc("thread/list", {
|
|
942
2034
|
archived: false,
|
|
943
2035
|
limit: 100,
|
|
944
2036
|
sortKey: "updated_at",
|
|
@@ -946,7 +2038,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
946
2038
|
}));
|
|
947
2039
|
const data = Array.isArray(response?.data) ? response.data : [];
|
|
948
2040
|
for (const row of data) {
|
|
949
|
-
const record =
|
|
2041
|
+
const record = asRecord2(row);
|
|
950
2042
|
const id = typeof record?.id === "string" ? record.id : "";
|
|
951
2043
|
if (!id) continue;
|
|
952
2044
|
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";
|
|
@@ -1010,6 +2102,7 @@ function createCodexBridgeMiddleware() {
|
|
|
1010
2102
|
}
|
|
1011
2103
|
return threadSearchIndexPromise;
|
|
1012
2104
|
}
|
|
2105
|
+
void initializeSkillsSyncOnStartup(appServer);
|
|
1013
2106
|
const middleware = async (req, res, next) => {
|
|
1014
2107
|
try {
|
|
1015
2108
|
if (!req.url) {
|
|
@@ -1017,25 +2110,28 @@ function createCodexBridgeMiddleware() {
|
|
|
1017
2110
|
return;
|
|
1018
2111
|
}
|
|
1019
2112
|
const url = new URL(req.url, "http://localhost");
|
|
2113
|
+
if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
1020
2116
|
if (req.method === "POST" && url.pathname === "/codex-api/upload-file") {
|
|
1021
2117
|
handleFileUpload(req, res);
|
|
1022
2118
|
return;
|
|
1023
2119
|
}
|
|
1024
2120
|
if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
|
|
1025
2121
|
const payload = await readJsonBody(req);
|
|
1026
|
-
const body =
|
|
2122
|
+
const body = asRecord2(payload);
|
|
1027
2123
|
if (!body || typeof body.method !== "string" || body.method.length === 0) {
|
|
1028
|
-
|
|
2124
|
+
setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
|
|
1029
2125
|
return;
|
|
1030
2126
|
}
|
|
1031
2127
|
const result = await appServer.rpc(body.method, body.params ?? null);
|
|
1032
|
-
|
|
2128
|
+
setJson2(res, 200, { result });
|
|
1033
2129
|
return;
|
|
1034
2130
|
}
|
|
1035
2131
|
if (req.method === "POST" && url.pathname === "/codex-api/transcribe") {
|
|
1036
2132
|
const auth = await readCodexAuth();
|
|
1037
2133
|
if (!auth) {
|
|
1038
|
-
|
|
2134
|
+
setJson2(res, 401, { error: "No auth token available for transcription" });
|
|
1039
2135
|
return;
|
|
1040
2136
|
}
|
|
1041
2137
|
const rawBody = await readRawBody(req);
|
|
@@ -1049,48 +2145,48 @@ function createCodexBridgeMiddleware() {
|
|
|
1049
2145
|
if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
|
|
1050
2146
|
const payload = await readJsonBody(req);
|
|
1051
2147
|
await appServer.respondToServerRequest(payload);
|
|
1052
|
-
|
|
2148
|
+
setJson2(res, 200, { ok: true });
|
|
1053
2149
|
return;
|
|
1054
2150
|
}
|
|
1055
2151
|
if (req.method === "GET" && url.pathname === "/codex-api/server-requests/pending") {
|
|
1056
|
-
|
|
2152
|
+
setJson2(res, 200, { data: appServer.listPendingServerRequests() });
|
|
1057
2153
|
return;
|
|
1058
2154
|
}
|
|
1059
2155
|
if (req.method === "GET" && url.pathname === "/codex-api/meta/methods") {
|
|
1060
2156
|
const methods = await methodCatalog.listMethods();
|
|
1061
|
-
|
|
2157
|
+
setJson2(res, 200, { data: methods });
|
|
1062
2158
|
return;
|
|
1063
2159
|
}
|
|
1064
2160
|
if (req.method === "GET" && url.pathname === "/codex-api/meta/notifications") {
|
|
1065
2161
|
const methods = await methodCatalog.listNotificationMethods();
|
|
1066
|
-
|
|
2162
|
+
setJson2(res, 200, { data: methods });
|
|
1067
2163
|
return;
|
|
1068
2164
|
}
|
|
1069
2165
|
if (req.method === "GET" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1070
2166
|
const state = await readWorkspaceRootsState();
|
|
1071
|
-
|
|
2167
|
+
setJson2(res, 200, { data: state });
|
|
1072
2168
|
return;
|
|
1073
2169
|
}
|
|
1074
2170
|
if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
|
|
1075
|
-
|
|
2171
|
+
setJson2(res, 200, { data: { path: homedir2() } });
|
|
1076
2172
|
return;
|
|
1077
2173
|
}
|
|
1078
2174
|
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
1079
|
-
const payload =
|
|
2175
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1080
2176
|
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
1081
2177
|
if (!rawSourceCwd) {
|
|
1082
|
-
|
|
2178
|
+
setJson2(res, 400, { error: "Missing sourceCwd" });
|
|
1083
2179
|
return;
|
|
1084
2180
|
}
|
|
1085
2181
|
const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
|
|
1086
2182
|
try {
|
|
1087
|
-
const sourceInfo = await
|
|
2183
|
+
const sourceInfo = await stat2(sourceCwd);
|
|
1088
2184
|
if (!sourceInfo.isDirectory()) {
|
|
1089
|
-
|
|
2185
|
+
setJson2(res, 400, { error: "sourceCwd is not a directory" });
|
|
1090
2186
|
return;
|
|
1091
2187
|
}
|
|
1092
2188
|
} catch {
|
|
1093
|
-
|
|
2189
|
+
setJson2(res, 404, { error: "sourceCwd does not exist" });
|
|
1094
2190
|
return;
|
|
1095
2191
|
}
|
|
1096
2192
|
try {
|
|
@@ -1099,25 +2195,25 @@ function createCodexBridgeMiddleware() {
|
|
|
1099
2195
|
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1100
2196
|
} catch (error) {
|
|
1101
2197
|
if (!isNotGitRepositoryError(error)) throw error;
|
|
1102
|
-
await
|
|
2198
|
+
await runCommand2("git", ["init"], { cwd: sourceCwd });
|
|
1103
2199
|
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1104
2200
|
}
|
|
1105
2201
|
const repoName = basename(gitRoot) || "repo";
|
|
1106
|
-
const worktreesRoot =
|
|
1107
|
-
await
|
|
2202
|
+
const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
|
|
2203
|
+
await mkdir2(worktreesRoot, { recursive: true });
|
|
1108
2204
|
let worktreeId = "";
|
|
1109
2205
|
let worktreeParent = "";
|
|
1110
2206
|
let worktreeCwd = "";
|
|
1111
2207
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
1112
2208
|
const candidate = randomBytes(2).toString("hex");
|
|
1113
|
-
const parent =
|
|
2209
|
+
const parent = join2(worktreesRoot, candidate);
|
|
1114
2210
|
try {
|
|
1115
|
-
await
|
|
2211
|
+
await stat2(parent);
|
|
1116
2212
|
continue;
|
|
1117
2213
|
} catch {
|
|
1118
2214
|
worktreeId = candidate;
|
|
1119
2215
|
worktreeParent = parent;
|
|
1120
|
-
worktreeCwd =
|
|
2216
|
+
worktreeCwd = join2(parent, repoName);
|
|
1121
2217
|
break;
|
|
1122
2218
|
}
|
|
1123
2219
|
}
|
|
@@ -1125,15 +2221,15 @@ function createCodexBridgeMiddleware() {
|
|
|
1125
2221
|
throw new Error("Failed to allocate a unique worktree id");
|
|
1126
2222
|
}
|
|
1127
2223
|
const branch = `codex/${worktreeId}`;
|
|
1128
|
-
await
|
|
2224
|
+
await mkdir2(worktreeParent, { recursive: true });
|
|
1129
2225
|
try {
|
|
1130
|
-
await
|
|
2226
|
+
await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1131
2227
|
} catch (error) {
|
|
1132
2228
|
if (!isMissingHeadError(error)) throw error;
|
|
1133
2229
|
await ensureRepoHasInitialCommit(gitRoot);
|
|
1134
|
-
await
|
|
2230
|
+
await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1135
2231
|
}
|
|
1136
|
-
|
|
2232
|
+
setJson2(res, 200, {
|
|
1137
2233
|
data: {
|
|
1138
2234
|
cwd: worktreeCwd,
|
|
1139
2235
|
branch,
|
|
@@ -1141,15 +2237,15 @@ function createCodexBridgeMiddleware() {
|
|
|
1141
2237
|
}
|
|
1142
2238
|
});
|
|
1143
2239
|
} catch (error) {
|
|
1144
|
-
|
|
2240
|
+
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to create worktree") });
|
|
1145
2241
|
}
|
|
1146
2242
|
return;
|
|
1147
2243
|
}
|
|
1148
2244
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1149
2245
|
const payload = await readJsonBody(req);
|
|
1150
|
-
const record =
|
|
2246
|
+
const record = asRecord2(payload);
|
|
1151
2247
|
if (!record) {
|
|
1152
|
-
|
|
2248
|
+
setJson2(res, 400, { error: "Invalid body: expected object" });
|
|
1153
2249
|
return;
|
|
1154
2250
|
}
|
|
1155
2251
|
const nextState = {
|
|
@@ -1158,33 +2254,33 @@ function createCodexBridgeMiddleware() {
|
|
|
1158
2254
|
active: normalizeStringArray(record.active)
|
|
1159
2255
|
};
|
|
1160
2256
|
await writeWorkspaceRootsState(nextState);
|
|
1161
|
-
|
|
2257
|
+
setJson2(res, 200, { ok: true });
|
|
1162
2258
|
return;
|
|
1163
2259
|
}
|
|
1164
2260
|
if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
|
|
1165
|
-
const payload =
|
|
2261
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1166
2262
|
const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
|
1167
2263
|
const createIfMissing = payload?.createIfMissing === true;
|
|
1168
2264
|
const label = typeof payload?.label === "string" ? payload.label : "";
|
|
1169
2265
|
if (!rawPath) {
|
|
1170
|
-
|
|
2266
|
+
setJson2(res, 400, { error: "Missing path" });
|
|
1171
2267
|
return;
|
|
1172
2268
|
}
|
|
1173
2269
|
const normalizedPath = isAbsolute(rawPath) ? rawPath : resolve(rawPath);
|
|
1174
2270
|
let pathExists = true;
|
|
1175
2271
|
try {
|
|
1176
|
-
const info = await
|
|
2272
|
+
const info = await stat2(normalizedPath);
|
|
1177
2273
|
if (!info.isDirectory()) {
|
|
1178
|
-
|
|
2274
|
+
setJson2(res, 400, { error: "Path exists but is not a directory" });
|
|
1179
2275
|
return;
|
|
1180
2276
|
}
|
|
1181
2277
|
} catch {
|
|
1182
2278
|
pathExists = false;
|
|
1183
2279
|
}
|
|
1184
2280
|
if (!pathExists && createIfMissing) {
|
|
1185
|
-
await
|
|
2281
|
+
await mkdir2(normalizedPath, { recursive: true });
|
|
1186
2282
|
} else if (!pathExists) {
|
|
1187
|
-
|
|
2283
|
+
setJson2(res, 404, { error: "Directory does not exist" });
|
|
1188
2284
|
return;
|
|
1189
2285
|
}
|
|
1190
2286
|
const existingState = await readWorkspaceRootsState();
|
|
@@ -1199,215 +2295,103 @@ function createCodexBridgeMiddleware() {
|
|
|
1199
2295
|
labels: nextLabels,
|
|
1200
2296
|
active: nextActive
|
|
1201
2297
|
});
|
|
1202
|
-
|
|
2298
|
+
setJson2(res, 200, { data: { path: normalizedPath } });
|
|
1203
2299
|
return;
|
|
1204
2300
|
}
|
|
1205
2301
|
if (req.method === "GET" && url.pathname === "/codex-api/project-root-suggestion") {
|
|
1206
2302
|
const basePath = url.searchParams.get("basePath")?.trim() ?? "";
|
|
1207
2303
|
if (!basePath) {
|
|
1208
|
-
|
|
2304
|
+
setJson2(res, 400, { error: "Missing basePath" });
|
|
1209
2305
|
return;
|
|
1210
2306
|
}
|
|
1211
2307
|
const normalizedBasePath = isAbsolute(basePath) ? basePath : resolve(basePath);
|
|
1212
2308
|
try {
|
|
1213
|
-
const baseInfo = await
|
|
2309
|
+
const baseInfo = await stat2(normalizedBasePath);
|
|
1214
2310
|
if (!baseInfo.isDirectory()) {
|
|
1215
|
-
|
|
2311
|
+
setJson2(res, 400, { error: "basePath is not a directory" });
|
|
1216
2312
|
return;
|
|
1217
2313
|
}
|
|
1218
2314
|
} catch {
|
|
1219
|
-
|
|
2315
|
+
setJson2(res, 404, { error: "basePath does not exist" });
|
|
1220
2316
|
return;
|
|
1221
2317
|
}
|
|
1222
2318
|
let index = 1;
|
|
1223
2319
|
while (index < 1e5) {
|
|
1224
2320
|
const candidateName = `New Project (${String(index)})`;
|
|
1225
|
-
const candidatePath =
|
|
2321
|
+
const candidatePath = join2(normalizedBasePath, candidateName);
|
|
1226
2322
|
try {
|
|
1227
|
-
await
|
|
2323
|
+
await stat2(candidatePath);
|
|
1228
2324
|
index += 1;
|
|
1229
2325
|
continue;
|
|
1230
2326
|
} catch {
|
|
1231
|
-
|
|
2327
|
+
setJson2(res, 200, { data: { name: candidateName, path: candidatePath } });
|
|
1232
2328
|
return;
|
|
1233
2329
|
}
|
|
1234
2330
|
}
|
|
1235
|
-
|
|
2331
|
+
setJson2(res, 500, { error: "Failed to compute project name suggestion" });
|
|
1236
2332
|
return;
|
|
1237
2333
|
}
|
|
1238
2334
|
if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
|
|
1239
|
-
const payload =
|
|
2335
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1240
2336
|
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
1241
2337
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1242
2338
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
|
|
1243
2339
|
const limit = Math.max(1, Math.min(100, Math.floor(limitRaw)));
|
|
1244
2340
|
if (!rawCwd) {
|
|
1245
|
-
|
|
2341
|
+
setJson2(res, 400, { error: "Missing cwd" });
|
|
1246
2342
|
return;
|
|
1247
2343
|
}
|
|
1248
2344
|
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
1249
2345
|
try {
|
|
1250
|
-
const info = await
|
|
2346
|
+
const info = await stat2(cwd);
|
|
1251
2347
|
if (!info.isDirectory()) {
|
|
1252
|
-
|
|
2348
|
+
setJson2(res, 400, { error: "cwd is not a directory" });
|
|
1253
2349
|
return;
|
|
1254
2350
|
}
|
|
1255
2351
|
} catch {
|
|
1256
|
-
|
|
2352
|
+
setJson2(res, 404, { error: "cwd does not exist" });
|
|
1257
2353
|
return;
|
|
1258
2354
|
}
|
|
1259
2355
|
try {
|
|
1260
2356
|
const files = await listFilesWithRipgrep(cwd);
|
|
1261
2357
|
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 }));
|
|
1262
|
-
|
|
2358
|
+
setJson2(res, 200, { data: scored });
|
|
1263
2359
|
} catch (error) {
|
|
1264
|
-
|
|
2360
|
+
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to search files") });
|
|
1265
2361
|
}
|
|
1266
2362
|
return;
|
|
1267
2363
|
}
|
|
1268
2364
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
|
|
1269
|
-
const cache = await
|
|
1270
|
-
|
|
2365
|
+
const cache = await readMergedThreadTitleCache();
|
|
2366
|
+
setJson2(res, 200, { data: cache });
|
|
1271
2367
|
return;
|
|
1272
2368
|
}
|
|
1273
2369
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
1274
|
-
const payload =
|
|
2370
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1275
2371
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1276
2372
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
1277
2373
|
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
1278
2374
|
if (!query) {
|
|
1279
|
-
|
|
2375
|
+
setJson2(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
|
|
1280
2376
|
return;
|
|
1281
2377
|
}
|
|
1282
2378
|
const index = await getThreadSearchIndex();
|
|
1283
2379
|
const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
|
|
1284
|
-
|
|
2380
|
+
setJson2(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
|
|
1285
2381
|
return;
|
|
1286
2382
|
}
|
|
1287
2383
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
1288
|
-
const payload =
|
|
2384
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1289
2385
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
1290
2386
|
const title = typeof payload?.title === "string" ? payload.title : "";
|
|
1291
2387
|
if (!id) {
|
|
1292
|
-
|
|
2388
|
+
setJson2(res, 400, { error: "Missing id" });
|
|
1293
2389
|
return;
|
|
1294
2390
|
}
|
|
1295
2391
|
const cache = await readThreadTitleCache();
|
|
1296
2392
|
const next2 = title ? updateThreadTitleCache(cache, id, title) : removeFromThreadTitleCache(cache, id);
|
|
1297
2393
|
await writeThreadTitleCache(next2);
|
|
1298
|
-
|
|
1299
|
-
return;
|
|
1300
|
-
}
|
|
1301
|
-
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
|
|
1302
|
-
try {
|
|
1303
|
-
const q = url.searchParams.get("q") || "";
|
|
1304
|
-
const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
|
|
1305
|
-
const sort = url.searchParams.get("sort") || "date";
|
|
1306
|
-
const allEntries = await fetchSkillsTree();
|
|
1307
|
-
const installedMap = await scanInstalledSkillsFromDisk();
|
|
1308
|
-
try {
|
|
1309
|
-
const result = await appServer.rpc("skills/list", {});
|
|
1310
|
-
for (const entry of result.data ?? []) {
|
|
1311
|
-
for (const skill of entry.skills ?? []) {
|
|
1312
|
-
if (skill.name) {
|
|
1313
|
-
installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
} catch {
|
|
1318
|
-
}
|
|
1319
|
-
const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
|
|
1320
|
-
await fetchMetaBatch(installedHubEntries);
|
|
1321
|
-
const installed = [];
|
|
1322
|
-
for (const [, info] of installedMap) {
|
|
1323
|
-
const hubEntry = allEntries.find((e) => e.name === info.name);
|
|
1324
|
-
const base = hubEntry ? buildHubEntry(hubEntry) : {
|
|
1325
|
-
name: info.name,
|
|
1326
|
-
owner: "local",
|
|
1327
|
-
description: "",
|
|
1328
|
-
displayName: "",
|
|
1329
|
-
publishedAt: 0,
|
|
1330
|
-
avatarUrl: "",
|
|
1331
|
-
url: "",
|
|
1332
|
-
installed: false
|
|
1333
|
-
};
|
|
1334
|
-
installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
|
|
1335
|
-
}
|
|
1336
|
-
const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
|
|
1337
|
-
setJson(res, 200, { data: results, installed, total: allEntries.length });
|
|
1338
|
-
} catch (error) {
|
|
1339
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch skills hub") });
|
|
1340
|
-
}
|
|
1341
|
-
return;
|
|
1342
|
-
}
|
|
1343
|
-
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
|
|
1344
|
-
try {
|
|
1345
|
-
const owner = url.searchParams.get("owner") || "";
|
|
1346
|
-
const name = url.searchParams.get("name") || "";
|
|
1347
|
-
if (!owner || !name) {
|
|
1348
|
-
setJson(res, 400, { error: "Missing owner or name" });
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
const rawUrl = `https://raw.githubusercontent.com/openclaw/skills/main/skills/${owner}/${name}/SKILL.md`;
|
|
1352
|
-
const resp = await fetch(rawUrl);
|
|
1353
|
-
if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
|
|
1354
|
-
const content = await resp.text();
|
|
1355
|
-
setJson(res, 200, { content });
|
|
1356
|
-
} catch (error) {
|
|
1357
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
|
|
1358
|
-
}
|
|
1359
|
-
return;
|
|
1360
|
-
}
|
|
1361
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
|
|
1362
|
-
try {
|
|
1363
|
-
const payload = asRecord(await readJsonBody(req));
|
|
1364
|
-
const owner = typeof payload?.owner === "string" ? payload.owner : "";
|
|
1365
|
-
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1366
|
-
if (!owner || !name) {
|
|
1367
|
-
setJson(res, 400, { error: "Missing owner or name" });
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
1371
|
-
const installDest = await detectUserSkillsDir(appServer);
|
|
1372
|
-
const skillPathInRepo = `skills/${owner}/${name}`;
|
|
1373
|
-
await runCommand("python3", [
|
|
1374
|
-
installerScript,
|
|
1375
|
-
"--repo",
|
|
1376
|
-
"openclaw/skills",
|
|
1377
|
-
"--path",
|
|
1378
|
-
skillPathInRepo,
|
|
1379
|
-
"--dest",
|
|
1380
|
-
installDest,
|
|
1381
|
-
"--method",
|
|
1382
|
-
"git"
|
|
1383
|
-
]);
|
|
1384
|
-
const skillDir = join(installDest, name);
|
|
1385
|
-
await ensureInstalledSkillIsValid(appServer, skillDir);
|
|
1386
|
-
setJson(res, 200, { ok: true, path: skillDir });
|
|
1387
|
-
} catch (error) {
|
|
1388
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
|
|
1389
|
-
}
|
|
1390
|
-
return;
|
|
1391
|
-
}
|
|
1392
|
-
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
|
|
1393
|
-
try {
|
|
1394
|
-
const payload = asRecord(await readJsonBody(req));
|
|
1395
|
-
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1396
|
-
const path = typeof payload?.path === "string" ? payload.path : "";
|
|
1397
|
-
const target = path || (name ? join(getSkillsInstallDir(), name) : "");
|
|
1398
|
-
if (!target) {
|
|
1399
|
-
setJson(res, 400, { error: "Missing name or path" });
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
await rm(target, { recursive: true, force: true });
|
|
1403
|
-
try {
|
|
1404
|
-
await appServer.rpc("skills/list", { forceReload: true });
|
|
1405
|
-
} catch {
|
|
1406
|
-
}
|
|
1407
|
-
setJson(res, 200, { ok: true, deletedPath: target });
|
|
1408
|
-
} catch (error) {
|
|
1409
|
-
setJson(res, 502, { error: getErrorMessage(error, "Failed to uninstall skill") });
|
|
1410
|
-
}
|
|
2394
|
+
setJson2(res, 200, { ok: true });
|
|
1411
2395
|
return;
|
|
1412
2396
|
}
|
|
1413
2397
|
if (req.method === "GET" && url.pathname === "/codex-api/events") {
|
|
@@ -1442,8 +2426,8 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1442
2426
|
}
|
|
1443
2427
|
next();
|
|
1444
2428
|
} catch (error) {
|
|
1445
|
-
const message =
|
|
1446
|
-
|
|
2429
|
+
const message = getErrorMessage2(error, "Unknown bridge error");
|
|
2430
|
+
setJson2(res, 502, { error: message });
|
|
1447
2431
|
}
|
|
1448
2432
|
};
|
|
1449
2433
|
middleware.dispose = () => {
|
|
@@ -1580,8 +2564,8 @@ function createAuthSession(password) {
|
|
|
1580
2564
|
}
|
|
1581
2565
|
|
|
1582
2566
|
// src/server/localBrowseUi.ts
|
|
1583
|
-
import { dirname, extname, join as
|
|
1584
|
-
import { open, readFile as
|
|
2567
|
+
import { dirname, extname, join as join3 } from "path";
|
|
2568
|
+
import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
1585
2569
|
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1586
2570
|
".txt",
|
|
1587
2571
|
".md",
|
|
@@ -1696,7 +2680,7 @@ async function probeFileIsText(localPath) {
|
|
|
1696
2680
|
async function isTextEditableFile(localPath) {
|
|
1697
2681
|
if (isTextEditablePath(localPath)) return true;
|
|
1698
2682
|
try {
|
|
1699
|
-
const fileStat = await
|
|
2683
|
+
const fileStat = await stat3(localPath);
|
|
1700
2684
|
if (!fileStat.isFile()) return false;
|
|
1701
2685
|
return await probeFileIsText(localPath);
|
|
1702
2686
|
} catch {
|
|
@@ -1716,10 +2700,10 @@ function escapeForInlineScriptString(value) {
|
|
|
1716
2700
|
return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
|
|
1717
2701
|
}
|
|
1718
2702
|
async function getDirectoryItems(localPath) {
|
|
1719
|
-
const entries = await
|
|
2703
|
+
const entries = await readdir3(localPath, { withFileTypes: true });
|
|
1720
2704
|
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
1721
|
-
const entryPath =
|
|
1722
|
-
const entryStat = await
|
|
2705
|
+
const entryPath = join3(localPath, entry.name);
|
|
2706
|
+
const entryStat = await stat3(entryPath);
|
|
1723
2707
|
const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
|
|
1724
2708
|
return {
|
|
1725
2709
|
name: entry.name,
|
|
@@ -1742,9 +2726,9 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
1742
2726
|
const rows = items.map((item) => {
|
|
1743
2727
|
const suffix = item.isDirectory ? "/" : "";
|
|
1744
2728
|
const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
|
|
1745
|
-
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
|
|
2729
|
+
return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a><span class="row-actions">${editAction}</span></li>`;
|
|
1746
2730
|
}).join("\n");
|
|
1747
|
-
const parentLink = localPath !== parentPath ? `<
|
|
2731
|
+
const parentLink = localPath !== parentPath ? `<a href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : "";
|
|
1748
2732
|
return `<!doctype html>
|
|
1749
2733
|
<html lang="en">
|
|
1750
2734
|
<head>
|
|
@@ -1758,8 +2742,27 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
1758
2742
|
ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
|
|
1759
2743
|
.file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
|
|
1760
2744
|
.file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
|
|
1761
|
-
.
|
|
2745
|
+
.header-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
|
|
2746
|
+
.header-parent-link { color: #9ec8ff; font-size: 14px; padding: 8px 10px; border: 1px solid #2a4569; border-radius: 10px; background: #101f3a; }
|
|
2747
|
+
.header-parent-link:hover { text-decoration: none; filter: brightness(1.08); }
|
|
2748
|
+
.header-open-btn {
|
|
2749
|
+
height: 42px;
|
|
2750
|
+
padding: 0 14px;
|
|
2751
|
+
border: 1px solid #4f8de0;
|
|
2752
|
+
border-radius: 10px;
|
|
2753
|
+
background: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
|
|
2754
|
+
color: #eef6ff;
|
|
2755
|
+
font-weight: 700;
|
|
2756
|
+
letter-spacing: 0.01em;
|
|
2757
|
+
cursor: pointer;
|
|
2758
|
+
box-shadow: 0 6px 18px rgba(33, 90, 199, 0.35);
|
|
2759
|
+
}
|
|
2760
|
+
.header-open-btn:hover { filter: brightness(1.08); }
|
|
2761
|
+
.header-open-btn:disabled { opacity: 0.6; cursor: default; }
|
|
2762
|
+
.row-actions { display: inline-flex; align-items: center; gap: 8px; min-width: 42px; justify-content: flex-end; }
|
|
2763
|
+
.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; color: #dbe6ff; text-decoration: none; cursor: pointer; }
|
|
1762
2764
|
.icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
|
|
2765
|
+
.status { margin: 10px 0 0; color: #8cc2ff; min-height: 1.25em; }
|
|
1763
2766
|
h1 { font-size: 18px; margin: 0; word-break: break-all; }
|
|
1764
2767
|
@media (max-width: 640px) {
|
|
1765
2768
|
body { margin: 12px; }
|
|
@@ -1771,13 +2774,51 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
1771
2774
|
</head>
|
|
1772
2775
|
<body>
|
|
1773
2776
|
<h1>Index of ${escapeHtml(localPath)}</h1>
|
|
1774
|
-
|
|
2777
|
+
<div class="header-actions">
|
|
2778
|
+
${parentLink ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : ""}
|
|
2779
|
+
<button class="header-open-btn open-folder-btn" type="button" aria-label="Open current folder in Codex" title="Open folder in Codex" data-path="${escapeHtml(localPath)}">Open folder in Codex</button>
|
|
2780
|
+
</div>
|
|
2781
|
+
<p id="status" class="status"></p>
|
|
1775
2782
|
<ul>${rows}</ul>
|
|
2783
|
+
<script>
|
|
2784
|
+
const status = document.getElementById('status');
|
|
2785
|
+
document.addEventListener('click', async (event) => {
|
|
2786
|
+
const target = event.target;
|
|
2787
|
+
if (!(target instanceof Element)) return;
|
|
2788
|
+
const button = target.closest('.open-folder-btn');
|
|
2789
|
+
if (!(button instanceof HTMLButtonElement)) return;
|
|
2790
|
+
|
|
2791
|
+
const path = button.getAttribute('data-path') || '';
|
|
2792
|
+
if (!path) return;
|
|
2793
|
+
button.disabled = true;
|
|
2794
|
+
status.textContent = 'Opening folder in Codex...';
|
|
2795
|
+
try {
|
|
2796
|
+
const response = await fetch('/codex-api/project-root', {
|
|
2797
|
+
method: 'POST',
|
|
2798
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2799
|
+
body: JSON.stringify({
|
|
2800
|
+
path,
|
|
2801
|
+
createIfMissing: false,
|
|
2802
|
+
label: '',
|
|
2803
|
+
}),
|
|
2804
|
+
});
|
|
2805
|
+
if (!response.ok) {
|
|
2806
|
+
status.textContent = 'Failed to open folder.';
|
|
2807
|
+
button.disabled = false;
|
|
2808
|
+
return;
|
|
2809
|
+
}
|
|
2810
|
+
window.location.assign('/#/');
|
|
2811
|
+
} catch {
|
|
2812
|
+
status.textContent = 'Failed to open folder.';
|
|
2813
|
+
button.disabled = false;
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
</script>
|
|
1776
2817
|
</body>
|
|
1777
2818
|
</html>`;
|
|
1778
2819
|
}
|
|
1779
2820
|
async function createTextEditorHtml(localPath) {
|
|
1780
|
-
const content = await
|
|
2821
|
+
const content = await readFile3(localPath, "utf8");
|
|
1781
2822
|
const parentPath = dirname(localPath);
|
|
1782
2823
|
const language = languageForPath(localPath);
|
|
1783
2824
|
const safeContentLiteral = escapeForInlineScriptString(content);
|
|
@@ -1848,8 +2889,8 @@ async function createTextEditorHtml(localPath) {
|
|
|
1848
2889
|
// src/server/httpServer.ts
|
|
1849
2890
|
import { WebSocketServer } from "ws";
|
|
1850
2891
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
1851
|
-
var distDir =
|
|
1852
|
-
var spaEntryFile =
|
|
2892
|
+
var distDir = join4(__dirname, "..", "dist");
|
|
2893
|
+
var spaEntryFile = join4(distDir, "index.html");
|
|
1853
2894
|
var IMAGE_CONTENT_TYPES = {
|
|
1854
2895
|
".avif": "image/avif",
|
|
1855
2896
|
".bmp": "image/bmp",
|
|
@@ -1860,6 +2901,20 @@ var IMAGE_CONTENT_TYPES = {
|
|
|
1860
2901
|
".svg": "image/svg+xml",
|
|
1861
2902
|
".webp": "image/webp"
|
|
1862
2903
|
};
|
|
2904
|
+
function renderFrontendMissingHtml(message, details) {
|
|
2905
|
+
const lines = details && details.length > 0 ? `<pre>${details.join("\n")}</pre>` : "";
|
|
2906
|
+
return [
|
|
2907
|
+
"<!doctype html>",
|
|
2908
|
+
'<html lang="en">',
|
|
2909
|
+
'<head><meta charset="utf-8"><title>Codex Web UI Error</title></head>',
|
|
2910
|
+
"<body>",
|
|
2911
|
+
`<h1>${message}</h1>`,
|
|
2912
|
+
lines,
|
|
2913
|
+
'<p><a href="/">Back to chat</a></p>',
|
|
2914
|
+
"</body>",
|
|
2915
|
+
"</html>"
|
|
2916
|
+
].join("");
|
|
2917
|
+
}
|
|
1863
2918
|
function normalizeLocalImagePath(rawPath) {
|
|
1864
2919
|
const trimmed = rawPath.trim();
|
|
1865
2920
|
if (!trimmed) return "";
|
|
@@ -1926,7 +2981,7 @@ function createServer(options = {}) {
|
|
|
1926
2981
|
return;
|
|
1927
2982
|
}
|
|
1928
2983
|
try {
|
|
1929
|
-
const fileStat = await
|
|
2984
|
+
const fileStat = await stat4(localPath);
|
|
1930
2985
|
res.setHeader("Cache-Control", "private, no-store");
|
|
1931
2986
|
if (fileStat.isDirectory()) {
|
|
1932
2987
|
const html = await createDirectoryListingHtml(localPath);
|
|
@@ -1949,7 +3004,7 @@ function createServer(options = {}) {
|
|
|
1949
3004
|
return;
|
|
1950
3005
|
}
|
|
1951
3006
|
try {
|
|
1952
|
-
const fileStat = await
|
|
3007
|
+
const fileStat = await stat4(localPath);
|
|
1953
3008
|
if (!fileStat.isFile()) {
|
|
1954
3009
|
res.status(400).json({ error: "Expected file path." });
|
|
1955
3010
|
return;
|
|
@@ -1973,32 +3028,31 @@ function createServer(options = {}) {
|
|
|
1973
3028
|
}
|
|
1974
3029
|
const body = typeof req.body === "string" ? req.body : "";
|
|
1975
3030
|
try {
|
|
1976
|
-
await
|
|
3031
|
+
await writeFile3(localPath, body, "utf8");
|
|
1977
3032
|
res.status(200).json({ ok: true });
|
|
1978
3033
|
} catch {
|
|
1979
3034
|
res.status(404).json({ error: "File not found." });
|
|
1980
3035
|
}
|
|
1981
3036
|
});
|
|
1982
|
-
const hasFrontendAssets =
|
|
3037
|
+
const hasFrontendAssets = existsSync3(spaEntryFile);
|
|
1983
3038
|
if (hasFrontendAssets) {
|
|
1984
3039
|
app.use(express.static(distDir));
|
|
1985
3040
|
}
|
|
1986
3041
|
app.use((_req, res) => {
|
|
1987
3042
|
if (!hasFrontendAssets) {
|
|
1988
|
-
res.status(503).type("text/
|
|
1989
|
-
[
|
|
1990
|
-
"Codex web UI assets are missing.",
|
|
3043
|
+
res.status(503).type("text/html; charset=utf-8").send(
|
|
3044
|
+
renderFrontendMissingHtml("Codex web UI assets are missing.", [
|
|
1991
3045
|
`Expected: ${spaEntryFile}`,
|
|
1992
3046
|
"If running from source, build frontend assets with: npm run build:frontend",
|
|
1993
3047
|
"If running with npx, clear the npx cache and reinstall codexapp."
|
|
1994
|
-
]
|
|
3048
|
+
])
|
|
1995
3049
|
);
|
|
1996
3050
|
return;
|
|
1997
3051
|
}
|
|
1998
3052
|
res.sendFile(spaEntryFile, (error) => {
|
|
1999
3053
|
if (!error) return;
|
|
2000
3054
|
if (!res.headersSent) {
|
|
2001
|
-
res.status(404).type("text/
|
|
3055
|
+
res.status(404).type("text/html; charset=utf-8").send(renderFrontendMissingHtml("Frontend entry file not found."));
|
|
2002
3056
|
}
|
|
2003
3057
|
});
|
|
2004
3058
|
});
|
|
@@ -2053,8 +3107,8 @@ var program = new Command().name("codexui").description("Web interface for Codex
|
|
|
2053
3107
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
2054
3108
|
async function readCliVersion() {
|
|
2055
3109
|
try {
|
|
2056
|
-
const packageJsonPath =
|
|
2057
|
-
const raw = await
|
|
3110
|
+
const packageJsonPath = join5(__dirname2, "..", "package.json");
|
|
3111
|
+
const raw = await readFile4(packageJsonPath, "utf8");
|
|
2058
3112
|
const parsed = JSON.parse(raw);
|
|
2059
3113
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
2060
3114
|
} catch {
|
|
@@ -2079,22 +3133,22 @@ function runWithStatus(command, args) {
|
|
|
2079
3133
|
return result.status ?? -1;
|
|
2080
3134
|
}
|
|
2081
3135
|
function getUserNpmPrefix() {
|
|
2082
|
-
return
|
|
3136
|
+
return join5(homedir3(), ".npm-global");
|
|
2083
3137
|
}
|
|
2084
3138
|
function resolveCodexCommand() {
|
|
2085
3139
|
if (canRun("codex", ["--version"])) {
|
|
2086
3140
|
return "codex";
|
|
2087
3141
|
}
|
|
2088
|
-
const userCandidate =
|
|
2089
|
-
if (
|
|
3142
|
+
const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
|
|
3143
|
+
if (existsSync4(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
2090
3144
|
return userCandidate;
|
|
2091
3145
|
}
|
|
2092
3146
|
const prefix = process.env.PREFIX?.trim();
|
|
2093
3147
|
if (!prefix) {
|
|
2094
3148
|
return null;
|
|
2095
3149
|
}
|
|
2096
|
-
const candidate =
|
|
2097
|
-
if (
|
|
3150
|
+
const candidate = join5(prefix, "bin", "codex");
|
|
3151
|
+
if (existsSync4(candidate) && canRun(candidate, ["--version"])) {
|
|
2098
3152
|
return candidate;
|
|
2099
3153
|
}
|
|
2100
3154
|
return null;
|
|
@@ -2103,8 +3157,8 @@ function resolveCloudflaredCommand() {
|
|
|
2103
3157
|
if (canRun("cloudflared", ["--version"])) {
|
|
2104
3158
|
return "cloudflared";
|
|
2105
3159
|
}
|
|
2106
|
-
const localCandidate =
|
|
2107
|
-
if (
|
|
3160
|
+
const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
|
|
3161
|
+
if (existsSync4(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
2108
3162
|
return localCandidate;
|
|
2109
3163
|
}
|
|
2110
3164
|
return null;
|
|
@@ -2119,7 +3173,7 @@ function mapCloudflaredLinuxArch(arch) {
|
|
|
2119
3173
|
return null;
|
|
2120
3174
|
}
|
|
2121
3175
|
function downloadFile(url, destination) {
|
|
2122
|
-
return new Promise((
|
|
3176
|
+
return new Promise((resolve3, reject) => {
|
|
2123
3177
|
const request = (currentUrl) => {
|
|
2124
3178
|
httpsGet(currentUrl, (response) => {
|
|
2125
3179
|
const code = response.statusCode ?? 0;
|
|
@@ -2137,7 +3191,7 @@ function downloadFile(url, destination) {
|
|
|
2137
3191
|
response.pipe(file);
|
|
2138
3192
|
file.on("finish", () => {
|
|
2139
3193
|
file.close();
|
|
2140
|
-
|
|
3194
|
+
resolve3();
|
|
2141
3195
|
});
|
|
2142
3196
|
file.on("error", reject);
|
|
2143
3197
|
}).on("error", reject);
|
|
@@ -2157,9 +3211,9 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
2157
3211
|
if (!mappedArch) {
|
|
2158
3212
|
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
2159
3213
|
}
|
|
2160
|
-
const userBinDir =
|
|
3214
|
+
const userBinDir = join5(homedir3(), ".local", "bin");
|
|
2161
3215
|
mkdirSync(userBinDir, { recursive: true });
|
|
2162
|
-
const destination =
|
|
3216
|
+
const destination = join5(userBinDir, "cloudflared");
|
|
2163
3217
|
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
2164
3218
|
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
2165
3219
|
await downloadFile(downloadUrl, destination);
|
|
@@ -2177,7 +3231,7 @@ async function shouldInstallCloudflaredInteractively() {
|
|
|
2177
3231
|
console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
|
|
2178
3232
|
return false;
|
|
2179
3233
|
}
|
|
2180
|
-
const prompt =
|
|
3234
|
+
const prompt = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2181
3235
|
try {
|
|
2182
3236
|
const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
|
|
2183
3237
|
const normalized = answer.trim().toLowerCase();
|
|
@@ -2198,8 +3252,8 @@ async function resolveCloudflaredForTunnel() {
|
|
|
2198
3252
|
return ensureCloudflaredInstalledLinux();
|
|
2199
3253
|
}
|
|
2200
3254
|
function hasCodexAuth() {
|
|
2201
|
-
const codexHome = process.env.CODEX_HOME?.trim() ||
|
|
2202
|
-
return
|
|
3255
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
|
|
3256
|
+
return existsSync4(join5(codexHome, "auth.json"));
|
|
2203
3257
|
}
|
|
2204
3258
|
function ensureCodexInstalled() {
|
|
2205
3259
|
let codexCommand = resolveCodexCommand();
|
|
@@ -2217,7 +3271,7 @@ function ensureCodexInstalled() {
|
|
|
2217
3271
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
2218
3272
|
`);
|
|
2219
3273
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
2220
|
-
process.env.PATH = `${
|
|
3274
|
+
process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
2221
3275
|
};
|
|
2222
3276
|
if (isTermuxRuntime()) {
|
|
2223
3277
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -2266,7 +3320,7 @@ function printTermuxKeepAlive(lines) {
|
|
|
2266
3320
|
}
|
|
2267
3321
|
function openBrowser(url) {
|
|
2268
3322
|
const command = process.platform === "darwin" ? { cmd: "open", args: [url] } : process.platform === "win32" ? { cmd: "cmd", args: ["/c", "start", "", url] } : { cmd: "xdg-open", args: [url] };
|
|
2269
|
-
const child =
|
|
3323
|
+
const child = spawn3(command.cmd, command.args, { detached: true, stdio: "ignore" });
|
|
2270
3324
|
child.on("error", () => {
|
|
2271
3325
|
});
|
|
2272
3326
|
child.unref();
|
|
@@ -2280,25 +3334,28 @@ function parseCloudflaredUrl(chunk) {
|
|
|
2280
3334
|
}
|
|
2281
3335
|
function getAccessibleUrls(port) {
|
|
2282
3336
|
const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
}
|
|
2288
|
-
for (const entry of entries) {
|
|
2289
|
-
if (entry.internal) {
|
|
3337
|
+
try {
|
|
3338
|
+
const interfaces = networkInterfaces();
|
|
3339
|
+
for (const entries of Object.values(interfaces)) {
|
|
3340
|
+
if (!entries) {
|
|
2290
3341
|
continue;
|
|
2291
3342
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
3343
|
+
for (const entry of entries) {
|
|
3344
|
+
if (entry.internal) {
|
|
3345
|
+
continue;
|
|
3346
|
+
}
|
|
3347
|
+
if (entry.family === "IPv4") {
|
|
3348
|
+
urls.add(`http://${entry.address}:${String(port)}`);
|
|
3349
|
+
}
|
|
2294
3350
|
}
|
|
2295
3351
|
}
|
|
3352
|
+
} catch {
|
|
2296
3353
|
}
|
|
2297
3354
|
return Array.from(urls);
|
|
2298
3355
|
}
|
|
2299
3356
|
async function startCloudflaredTunnel(command, localPort) {
|
|
2300
|
-
return new Promise((
|
|
2301
|
-
const child =
|
|
3357
|
+
return new Promise((resolve3, reject) => {
|
|
3358
|
+
const child = spawn3(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
|
|
2302
3359
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2303
3360
|
});
|
|
2304
3361
|
const timeout = setTimeout(() => {
|
|
@@ -2314,7 +3371,7 @@ async function startCloudflaredTunnel(command, localPort) {
|
|
|
2314
3371
|
clearTimeout(timeout);
|
|
2315
3372
|
child.stdout?.off("data", handleData);
|
|
2316
3373
|
child.stderr?.off("data", handleData);
|
|
2317
|
-
|
|
3374
|
+
resolve3({ process: child, url: parsedUrl });
|
|
2318
3375
|
};
|
|
2319
3376
|
const onError = (error) => {
|
|
2320
3377
|
clearTimeout(timeout);
|
|
@@ -2333,7 +3390,7 @@ async function startCloudflaredTunnel(command, localPort) {
|
|
|
2333
3390
|
});
|
|
2334
3391
|
}
|
|
2335
3392
|
function listenWithFallback(server, startPort) {
|
|
2336
|
-
return new Promise((
|
|
3393
|
+
return new Promise((resolve3, reject) => {
|
|
2337
3394
|
const attempt = (port) => {
|
|
2338
3395
|
const onError = (error) => {
|
|
2339
3396
|
server.off("listening", onListening);
|
|
@@ -2345,7 +3402,7 @@ function listenWithFallback(server, startPort) {
|
|
|
2345
3402
|
};
|
|
2346
3403
|
const onListening = () => {
|
|
2347
3404
|
server.off("error", onError);
|
|
2348
|
-
|
|
3405
|
+
resolve3(port);
|
|
2349
3406
|
};
|
|
2350
3407
|
server.once("error", onError);
|
|
2351
3408
|
server.once("listening", onListening);
|
|
@@ -2354,8 +3411,72 @@ function listenWithFallback(server, startPort) {
|
|
|
2354
3411
|
attempt(startPort);
|
|
2355
3412
|
});
|
|
2356
3413
|
}
|
|
3414
|
+
function getCodexGlobalStatePath2() {
|
|
3415
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
|
|
3416
|
+
return join5(codexHome, ".codex-global-state.json");
|
|
3417
|
+
}
|
|
3418
|
+
function normalizeUniqueStrings(value) {
|
|
3419
|
+
if (!Array.isArray(value)) return [];
|
|
3420
|
+
const next = [];
|
|
3421
|
+
for (const item of value) {
|
|
3422
|
+
if (typeof item !== "string") continue;
|
|
3423
|
+
const trimmed = item.trim();
|
|
3424
|
+
if (!trimmed || next.includes(trimmed)) continue;
|
|
3425
|
+
next.push(trimmed);
|
|
3426
|
+
}
|
|
3427
|
+
return next;
|
|
3428
|
+
}
|
|
3429
|
+
async function persistLaunchProject(projectPath) {
|
|
3430
|
+
const trimmed = projectPath.trim();
|
|
3431
|
+
if (!trimmed) return;
|
|
3432
|
+
const normalizedPath = isAbsolute3(trimmed) ? trimmed : resolve2(trimmed);
|
|
3433
|
+
const directoryInfo = await stat5(normalizedPath);
|
|
3434
|
+
if (!directoryInfo.isDirectory()) {
|
|
3435
|
+
throw new Error(`Not a directory: ${normalizedPath}`);
|
|
3436
|
+
}
|
|
3437
|
+
const statePath = getCodexGlobalStatePath2();
|
|
3438
|
+
let payload = {};
|
|
3439
|
+
try {
|
|
3440
|
+
const raw = await readFile4(statePath, "utf8");
|
|
3441
|
+
const parsed = JSON.parse(raw);
|
|
3442
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3443
|
+
payload = parsed;
|
|
3444
|
+
}
|
|
3445
|
+
} catch {
|
|
3446
|
+
payload = {};
|
|
3447
|
+
}
|
|
3448
|
+
const roots = normalizeUniqueStrings(payload["electron-saved-workspace-roots"]);
|
|
3449
|
+
const activeRoots = normalizeUniqueStrings(payload["active-workspace-roots"]);
|
|
3450
|
+
payload["electron-saved-workspace-roots"] = [
|
|
3451
|
+
normalizedPath,
|
|
3452
|
+
...roots.filter((value) => value !== normalizedPath)
|
|
3453
|
+
];
|
|
3454
|
+
payload["active-workspace-roots"] = [
|
|
3455
|
+
normalizedPath,
|
|
3456
|
+
...activeRoots.filter((value) => value !== normalizedPath)
|
|
3457
|
+
];
|
|
3458
|
+
await writeFile4(statePath, JSON.stringify(payload), "utf8");
|
|
3459
|
+
}
|
|
3460
|
+
async function addProjectOnly(projectPath) {
|
|
3461
|
+
const trimmed = projectPath.trim();
|
|
3462
|
+
if (!trimmed) {
|
|
3463
|
+
throw new Error("Missing project path");
|
|
3464
|
+
}
|
|
3465
|
+
await persistLaunchProject(trimmed);
|
|
3466
|
+
}
|
|
2357
3467
|
async function startServer(options) {
|
|
2358
3468
|
const version = await readCliVersion();
|
|
3469
|
+
const projectPath = options.projectPath?.trim() ?? "";
|
|
3470
|
+
if (projectPath.length > 0) {
|
|
3471
|
+
try {
|
|
3472
|
+
await persistLaunchProject(projectPath);
|
|
3473
|
+
} catch (error) {
|
|
3474
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3475
|
+
console.warn(`
|
|
3476
|
+
[project] Could not open launch project: ${message}
|
|
3477
|
+
`);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
2359
3480
|
const codexCommand = ensureCodexInstalled() ?? resolveCodexCommand();
|
|
2360
3481
|
if (!hasCodexAuth() && codexCommand) {
|
|
2361
3482
|
console.log("\nCodex is not logged in. Starting `codex login`...\n");
|
|
@@ -2416,7 +3537,7 @@ async function startServer(options) {
|
|
|
2416
3537
|
qrcode.generate(tunnelUrl, { small: true });
|
|
2417
3538
|
console.log("");
|
|
2418
3539
|
}
|
|
2419
|
-
openBrowser(`http://localhost:${String(port)}`);
|
|
3540
|
+
if (options.open) openBrowser(`http://localhost:${String(port)}`);
|
|
2420
3541
|
function shutdown() {
|
|
2421
3542
|
console.log("\nShutting down...");
|
|
2422
3543
|
if (tunnelChild && !tunnelChild.killed) {
|
|
@@ -2439,8 +3560,20 @@ async function runLogin() {
|
|
|
2439
3560
|
console.log("\nStarting `codex login`...\n");
|
|
2440
3561
|
runOrFail(codexCommand, ["login"], "Codex login");
|
|
2441
3562
|
}
|
|
2442
|
-
program.option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").action(async (opts) => {
|
|
2443
|
-
|
|
3563
|
+
program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").option("--open", "open browser on startup", true).option("--no-open", "do not open browser on startup").action(async (projectPath, opts) => {
|
|
3564
|
+
const rawArgv = process.argv.slice(2);
|
|
3565
|
+
const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
|
|
3566
|
+
let openProjectOnly = (opts.openProject ?? "").trim();
|
|
3567
|
+
if (!openProjectOnly && openProjectFlagIndex >= 0 && projectPath?.trim()) {
|
|
3568
|
+
openProjectOnly = projectPath.trim();
|
|
3569
|
+
}
|
|
3570
|
+
if (openProjectOnly.length > 0) {
|
|
3571
|
+
await addProjectOnly(openProjectOnly);
|
|
3572
|
+
console.log(`Added project: ${openProjectOnly}`);
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
const launchProject = (projectPath ?? "").trim();
|
|
3576
|
+
await startServer({ ...opts, projectPath: launchProject });
|
|
2444
3577
|
});
|
|
2445
3578
|
program.command("login").description("Install/check Codex CLI and run `codex login`").action(runLogin);
|
|
2446
3579
|
program.command("help").description("Show codexui command help").action(() => {
|