codexapp 0.1.45 → 0.1.46
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 +4 -0
- package/dist/assets/index-D2tyBrlG.css +1 -0
- package/dist/assets/index-ol-gi5Ys.js +1428 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +1588 -497
- package/dist-cli/index.js.map +1 -1
- package/package.json +5 -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();
|
|
291
1243
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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}`));
|
|
1253
|
+
});
|
|
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,27 +1323,36 @@ 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);
|
|
1332
|
+
const apiKey = auth.OPENAI_API_KEY || process.env.OPENAI_API_KEY || void 0;
|
|
399
1333
|
const token = auth.tokens?.access_token;
|
|
400
|
-
if (!token) return null;
|
|
401
|
-
return { accessToken: token, accountId: auth.tokens?.account_id ?? void 0 };
|
|
1334
|
+
if (!token && !apiKey) return null;
|
|
1335
|
+
return { accessToken: token ?? "", accountId: auth.tokens?.account_id ?? void 0, apiKey };
|
|
402
1336
|
} catch {
|
|
403
1337
|
return null;
|
|
404
1338
|
}
|
|
405
1339
|
}
|
|
406
1340
|
function getCodexGlobalStatePath() {
|
|
407
|
-
return
|
|
1341
|
+
return join2(getCodexHomeDir2(), ".codex-global-state.json");
|
|
1342
|
+
}
|
|
1343
|
+
function getCodexSessionIndexPath() {
|
|
1344
|
+
return join2(getCodexHomeDir2(), "session_index.jsonl");
|
|
408
1345
|
}
|
|
409
1346
|
var MAX_THREAD_TITLES = 500;
|
|
1347
|
+
var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
|
|
1348
|
+
var sessionIndexThreadTitleCacheState = {
|
|
1349
|
+
fileSignature: null,
|
|
1350
|
+
cache: EMPTY_THREAD_TITLE_CACHE
|
|
1351
|
+
};
|
|
410
1352
|
function normalizeThreadTitleCache(value) {
|
|
411
|
-
const record =
|
|
412
|
-
if (!record) return
|
|
413
|
-
const rawTitles =
|
|
1353
|
+
const record = asRecord2(value);
|
|
1354
|
+
if (!record) return EMPTY_THREAD_TITLE_CACHE;
|
|
1355
|
+
const rawTitles = asRecord2(record.titles);
|
|
414
1356
|
const titles = {};
|
|
415
1357
|
if (rawTitles) {
|
|
416
1358
|
for (const [k, v] of Object.entries(rawTitles)) {
|
|
@@ -433,35 +1375,139 @@ function removeFromThreadTitleCache(cache, id) {
|
|
|
433
1375
|
const { [id]: _, ...titles } = cache.titles;
|
|
434
1376
|
return { titles, order: cache.order.filter((o) => o !== id) };
|
|
435
1377
|
}
|
|
1378
|
+
function normalizeSessionIndexThreadTitle(value) {
|
|
1379
|
+
const record = asRecord2(value);
|
|
1380
|
+
if (!record) return null;
|
|
1381
|
+
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
1382
|
+
const title = typeof record.thread_name === "string" ? record.thread_name.trim() : "";
|
|
1383
|
+
const updatedAtIso = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
|
|
1384
|
+
const updatedAtMs = updatedAtIso ? Date.parse(updatedAtIso) : Number.NaN;
|
|
1385
|
+
if (!id || !title) return null;
|
|
1386
|
+
return {
|
|
1387
|
+
id,
|
|
1388
|
+
title,
|
|
1389
|
+
updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
function trimThreadTitleCache(cache) {
|
|
1393
|
+
const titles = { ...cache.titles };
|
|
1394
|
+
const order = cache.order.filter((id) => {
|
|
1395
|
+
if (!titles[id]) return false;
|
|
1396
|
+
return true;
|
|
1397
|
+
}).slice(0, MAX_THREAD_TITLES);
|
|
1398
|
+
for (const id of Object.keys(titles)) {
|
|
1399
|
+
if (!order.includes(id)) {
|
|
1400
|
+
delete titles[id];
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return { titles, order };
|
|
1404
|
+
}
|
|
1405
|
+
function mergeThreadTitleCaches(base, overlay) {
|
|
1406
|
+
const titles = { ...base.titles, ...overlay.titles };
|
|
1407
|
+
const order = [];
|
|
1408
|
+
for (const id of [...overlay.order, ...base.order]) {
|
|
1409
|
+
if (!titles[id] || order.includes(id)) continue;
|
|
1410
|
+
order.push(id);
|
|
1411
|
+
}
|
|
1412
|
+
for (const id of Object.keys(titles)) {
|
|
1413
|
+
if (!order.includes(id)) {
|
|
1414
|
+
order.push(id);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return trimThreadTitleCache({ titles, order });
|
|
1418
|
+
}
|
|
436
1419
|
async function readThreadTitleCache() {
|
|
437
1420
|
const statePath = getCodexGlobalStatePath();
|
|
438
1421
|
try {
|
|
439
|
-
const raw = await
|
|
440
|
-
const payload =
|
|
1422
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1423
|
+
const payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
441
1424
|
return normalizeThreadTitleCache(payload["thread-titles"]);
|
|
442
1425
|
} catch {
|
|
443
|
-
return
|
|
1426
|
+
return EMPTY_THREAD_TITLE_CACHE;
|
|
444
1427
|
}
|
|
445
1428
|
}
|
|
446
1429
|
async function writeThreadTitleCache(cache) {
|
|
447
1430
|
const statePath = getCodexGlobalStatePath();
|
|
448
1431
|
let payload = {};
|
|
449
1432
|
try {
|
|
450
|
-
const raw = await
|
|
451
|
-
payload =
|
|
1433
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1434
|
+
payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
452
1435
|
} catch {
|
|
453
1436
|
payload = {};
|
|
454
1437
|
}
|
|
455
1438
|
payload["thread-titles"] = cache;
|
|
456
|
-
await
|
|
1439
|
+
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
1440
|
+
}
|
|
1441
|
+
function getSessionIndexFileSignature(stats) {
|
|
1442
|
+
return `${String(stats.mtimeMs)}:${String(stats.size)}`;
|
|
1443
|
+
}
|
|
1444
|
+
async function parseThreadTitlesFromSessionIndex(sessionIndexPath) {
|
|
1445
|
+
const latestById = /* @__PURE__ */ new Map();
|
|
1446
|
+
const input = createReadStream(sessionIndexPath, { encoding: "utf8" });
|
|
1447
|
+
const lines = createInterface({
|
|
1448
|
+
input,
|
|
1449
|
+
crlfDelay: Infinity
|
|
1450
|
+
});
|
|
1451
|
+
try {
|
|
1452
|
+
for await (const line of lines) {
|
|
1453
|
+
const trimmed = line.trim();
|
|
1454
|
+
if (!trimmed) continue;
|
|
1455
|
+
try {
|
|
1456
|
+
const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed));
|
|
1457
|
+
if (!entry) continue;
|
|
1458
|
+
const previous = latestById.get(entry.id);
|
|
1459
|
+
if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
|
|
1460
|
+
latestById.set(entry.id, entry);
|
|
1461
|
+
}
|
|
1462
|
+
} catch {
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
} finally {
|
|
1466
|
+
lines.close();
|
|
1467
|
+
input.close();
|
|
1468
|
+
}
|
|
1469
|
+
const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs);
|
|
1470
|
+
const titles = {};
|
|
1471
|
+
const order = [];
|
|
1472
|
+
for (const entry of entries) {
|
|
1473
|
+
titles[entry.id] = entry.title;
|
|
1474
|
+
order.push(entry.id);
|
|
1475
|
+
}
|
|
1476
|
+
return trimThreadTitleCache({ titles, order });
|
|
1477
|
+
}
|
|
1478
|
+
async function readThreadTitlesFromSessionIndex() {
|
|
1479
|
+
const sessionIndexPath = getCodexSessionIndexPath();
|
|
1480
|
+
try {
|
|
1481
|
+
const stats = await stat2(sessionIndexPath);
|
|
1482
|
+
const fileSignature = getSessionIndexFileSignature(stats);
|
|
1483
|
+
if (sessionIndexThreadTitleCacheState.fileSignature === fileSignature) {
|
|
1484
|
+
return sessionIndexThreadTitleCacheState.cache;
|
|
1485
|
+
}
|
|
1486
|
+
const cache = await parseThreadTitlesFromSessionIndex(sessionIndexPath);
|
|
1487
|
+
sessionIndexThreadTitleCacheState = { fileSignature, cache };
|
|
1488
|
+
return cache;
|
|
1489
|
+
} catch {
|
|
1490
|
+
sessionIndexThreadTitleCacheState = {
|
|
1491
|
+
fileSignature: "missing",
|
|
1492
|
+
cache: EMPTY_THREAD_TITLE_CACHE
|
|
1493
|
+
};
|
|
1494
|
+
return sessionIndexThreadTitleCacheState.cache;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
async function readMergedThreadTitleCache() {
|
|
1498
|
+
const [sessionIndexCache, persistedCache] = await Promise.all([
|
|
1499
|
+
readThreadTitlesFromSessionIndex(),
|
|
1500
|
+
readThreadTitleCache()
|
|
1501
|
+
]);
|
|
1502
|
+
return mergeThreadTitleCaches(persistedCache, sessionIndexCache);
|
|
457
1503
|
}
|
|
458
1504
|
async function readWorkspaceRootsState() {
|
|
459
1505
|
const statePath = getCodexGlobalStatePath();
|
|
460
1506
|
let payload = {};
|
|
461
1507
|
try {
|
|
462
|
-
const raw = await
|
|
1508
|
+
const raw = await readFile2(statePath, "utf8");
|
|
463
1509
|
const parsed = JSON.parse(raw);
|
|
464
|
-
payload =
|
|
1510
|
+
payload = asRecord2(parsed) ?? {};
|
|
465
1511
|
} catch {
|
|
466
1512
|
payload = {};
|
|
467
1513
|
}
|
|
@@ -475,15 +1521,15 @@ async function writeWorkspaceRootsState(nextState) {
|
|
|
475
1521
|
const statePath = getCodexGlobalStatePath();
|
|
476
1522
|
let payload = {};
|
|
477
1523
|
try {
|
|
478
|
-
const raw = await
|
|
479
|
-
payload =
|
|
1524
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1525
|
+
payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
480
1526
|
} catch {
|
|
481
1527
|
payload = {};
|
|
482
1528
|
}
|
|
483
1529
|
payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
|
|
484
1530
|
payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
|
|
485
1531
|
payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
|
|
486
|
-
await
|
|
1532
|
+
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
487
1533
|
}
|
|
488
1534
|
async function readJsonBody(req) {
|
|
489
1535
|
const raw = await readRawBody(req);
|
|
@@ -521,7 +1567,7 @@ function handleFileUpload(req, res) {
|
|
|
521
1567
|
const contentType = req.headers["content-type"] ?? "";
|
|
522
1568
|
const boundaryMatch = contentType.match(/boundary=(.+)/i);
|
|
523
1569
|
if (!boundaryMatch) {
|
|
524
|
-
|
|
1570
|
+
setJson2(res, 400, { error: "Missing multipart boundary" });
|
|
525
1571
|
return;
|
|
526
1572
|
}
|
|
527
1573
|
const boundary = boundaryMatch[1];
|
|
@@ -551,49 +1597,110 @@ function handleFileUpload(req, res) {
|
|
|
551
1597
|
break;
|
|
552
1598
|
}
|
|
553
1599
|
if (!fileData) {
|
|
554
|
-
|
|
1600
|
+
setJson2(res, 400, { error: "No file in request" });
|
|
555
1601
|
return;
|
|
556
1602
|
}
|
|
557
|
-
const uploadDir =
|
|
558
|
-
await
|
|
559
|
-
const destDir = await
|
|
560
|
-
const destPath =
|
|
561
|
-
await
|
|
562
|
-
|
|
1603
|
+
const uploadDir = join2(tmpdir2(), "codex-web-uploads");
|
|
1604
|
+
await mkdir2(uploadDir, { recursive: true });
|
|
1605
|
+
const destDir = await mkdtemp2(join2(uploadDir, "f-"));
|
|
1606
|
+
const destPath = join2(destDir, fileName);
|
|
1607
|
+
await writeFile2(destPath, fileData);
|
|
1608
|
+
setJson2(res, 200, { path: destPath });
|
|
563
1609
|
} catch (err) {
|
|
564
|
-
|
|
1610
|
+
setJson2(res, 500, { error: getErrorMessage2(err, "Upload failed") });
|
|
565
1611
|
}
|
|
566
1612
|
});
|
|
567
1613
|
req.on("error", (err) => {
|
|
568
|
-
|
|
1614
|
+
setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
|
|
569
1615
|
});
|
|
570
1616
|
}
|
|
571
|
-
|
|
572
|
-
const
|
|
1617
|
+
function httpPost(url, headers, body) {
|
|
1618
|
+
const doRequest = url.startsWith("http://") ? httpRequest : httpsRequest;
|
|
1619
|
+
return new Promise((resolve3, reject) => {
|
|
1620
|
+
const req = doRequest(url, { method: "POST", headers }, (res) => {
|
|
1621
|
+
const chunks = [];
|
|
1622
|
+
res.on("data", (c) => chunks.push(c));
|
|
1623
|
+
res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
|
|
1624
|
+
res.on("error", reject);
|
|
1625
|
+
});
|
|
1626
|
+
req.on("error", reject);
|
|
1627
|
+
req.write(body);
|
|
1628
|
+
req.end();
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
var curlImpersonateAvailable = null;
|
|
1632
|
+
function curlImpersonatePost(url, headers, body) {
|
|
1633
|
+
return new Promise((resolve3, reject) => {
|
|
1634
|
+
const args = ["-s", "-w", "\n%{http_code}", "-X", "POST", url];
|
|
1635
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
1636
|
+
if (k.toLowerCase() === "content-length") continue;
|
|
1637
|
+
args.push("-H", `${k}: ${String(v)}`);
|
|
1638
|
+
}
|
|
1639
|
+
args.push("--data-binary", "@-");
|
|
1640
|
+
const proc = spawn2("curl-impersonate-chrome", args, {
|
|
1641
|
+
env: { ...process.env, CURL_IMPERSONATE: "chrome116" },
|
|
1642
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1643
|
+
});
|
|
1644
|
+
const chunks = [];
|
|
1645
|
+
proc.stdout.on("data", (c) => chunks.push(c));
|
|
1646
|
+
proc.on("error", (e) => {
|
|
1647
|
+
curlImpersonateAvailable = false;
|
|
1648
|
+
reject(e);
|
|
1649
|
+
});
|
|
1650
|
+
proc.on("close", (code) => {
|
|
1651
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1652
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
1653
|
+
const statusStr = lastNewline >= 0 ? raw.slice(lastNewline + 1).trim() : "";
|
|
1654
|
+
const responseBody = lastNewline >= 0 ? raw.slice(0, lastNewline) : raw;
|
|
1655
|
+
const status = parseInt(statusStr, 10) || (code === 0 ? 200 : 500);
|
|
1656
|
+
curlImpersonateAvailable = true;
|
|
1657
|
+
resolve3({ status, body: responseBody });
|
|
1658
|
+
});
|
|
1659
|
+
proc.stdin.write(body);
|
|
1660
|
+
proc.stdin.end();
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
var TRANSCRIBE_RELAY_URL = process.env.TRANSCRIBE_RELAY_URL || "http://127.0.0.1:1090/relay-transcribe";
|
|
1664
|
+
async function tryRelay(headers, body) {
|
|
1665
|
+
try {
|
|
1666
|
+
const resp = await httpPost(TRANSCRIBE_RELAY_URL, headers, body);
|
|
1667
|
+
if (resp.status !== 0) return resp;
|
|
1668
|
+
} catch {
|
|
1669
|
+
}
|
|
1670
|
+
return null;
|
|
1671
|
+
}
|
|
1672
|
+
async function proxyTranscribe(body, contentType, authToken, accountId, apiKey) {
|
|
1673
|
+
const chatgptHeaders = {
|
|
573
1674
|
"Content-Type": contentType,
|
|
574
1675
|
"Content-Length": body.length,
|
|
575
|
-
Authorization: `Bearer ${authToken}`,
|
|
1676
|
+
Authorization: `Bearer ${authToken || apiKey || ""}`,
|
|
576
1677
|
originator: "Codex Desktop",
|
|
577
1678
|
"User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
|
|
578
1679
|
};
|
|
579
|
-
if (accountId)
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1680
|
+
if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
|
|
1681
|
+
const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
|
|
1682
|
+
let result;
|
|
1683
|
+
try {
|
|
1684
|
+
result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
1685
|
+
} catch {
|
|
1686
|
+
result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
1687
|
+
}
|
|
1688
|
+
if (result.status === 403 && result.body.includes("cf_chl")) {
|
|
1689
|
+
if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
|
|
1690
|
+
try {
|
|
1691
|
+
const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
|
|
1692
|
+
if (ciResult.status !== 403) return ciResult;
|
|
1693
|
+
} catch {
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
const relayed = await tryRelay(chatgptHeaders, body);
|
|
1697
|
+
if (relayed && relayed.status !== 403) return relayed;
|
|
1698
|
+
if (apiKey) {
|
|
1699
|
+
return httpPost("https://api.openai.com/v1/audio/transcriptions", { ...chatgptHeaders, Authorization: `Bearer ${apiKey}` }, body);
|
|
1700
|
+
}
|
|
1701
|
+
return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome, start relay, or set OPENAI_API_KEY." }) };
|
|
1702
|
+
}
|
|
1703
|
+
return result;
|
|
597
1704
|
}
|
|
598
1705
|
var AppServerProcess = class {
|
|
599
1706
|
constructor() {
|
|
@@ -617,7 +1724,7 @@ var AppServerProcess = class {
|
|
|
617
1724
|
start() {
|
|
618
1725
|
if (this.process) return;
|
|
619
1726
|
this.stopping = false;
|
|
620
|
-
const proc =
|
|
1727
|
+
const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
621
1728
|
this.process = proc;
|
|
622
1729
|
proc.stdout.setEncoding("utf8");
|
|
623
1730
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -711,7 +1818,7 @@ var AppServerProcess = class {
|
|
|
711
1818
|
}
|
|
712
1819
|
this.pendingServerRequests.delete(requestId);
|
|
713
1820
|
this.sendServerRequestReply(requestId, reply);
|
|
714
|
-
const requestParams =
|
|
1821
|
+
const requestParams = asRecord2(pendingRequest.params);
|
|
715
1822
|
const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
|
|
716
1823
|
this.emitNotification({
|
|
717
1824
|
method: "server/request/resolved",
|
|
@@ -740,8 +1847,8 @@ var AppServerProcess = class {
|
|
|
740
1847
|
async call(method, params) {
|
|
741
1848
|
this.start();
|
|
742
1849
|
const id = this.nextId++;
|
|
743
|
-
return new Promise((
|
|
744
|
-
this.pending.set(id, { resolve:
|
|
1850
|
+
return new Promise((resolve3, reject) => {
|
|
1851
|
+
this.pending.set(id, { resolve: resolve3, reject });
|
|
745
1852
|
this.sendLine({
|
|
746
1853
|
jsonrpc: "2.0",
|
|
747
1854
|
id,
|
|
@@ -780,7 +1887,7 @@ var AppServerProcess = class {
|
|
|
780
1887
|
}
|
|
781
1888
|
async respondToServerRequest(payload) {
|
|
782
1889
|
await this.ensureInitialized();
|
|
783
|
-
const body =
|
|
1890
|
+
const body = asRecord2(payload);
|
|
784
1891
|
if (!body) {
|
|
785
1892
|
throw new Error("Invalid response payload: expected object");
|
|
786
1893
|
}
|
|
@@ -788,7 +1895,7 @@ var AppServerProcess = class {
|
|
|
788
1895
|
if (typeof id !== "number" || !Number.isInteger(id)) {
|
|
789
1896
|
throw new Error('Invalid response payload: "id" must be an integer');
|
|
790
1897
|
}
|
|
791
|
-
const rawError =
|
|
1898
|
+
const rawError = asRecord2(body.error);
|
|
792
1899
|
if (rawError) {
|
|
793
1900
|
const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
|
|
794
1901
|
const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
|
|
@@ -842,8 +1949,8 @@ var MethodCatalog = class {
|
|
|
842
1949
|
this.notificationCache = null;
|
|
843
1950
|
}
|
|
844
1951
|
async runGenerateSchemaCommand(outDir) {
|
|
845
|
-
await new Promise((
|
|
846
|
-
const process2 =
|
|
1952
|
+
await new Promise((resolve3, reject) => {
|
|
1953
|
+
const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
|
|
847
1954
|
stdio: ["ignore", "ignore", "pipe"]
|
|
848
1955
|
});
|
|
849
1956
|
let stderr = "";
|
|
@@ -854,7 +1961,7 @@ var MethodCatalog = class {
|
|
|
854
1961
|
process2.on("error", reject);
|
|
855
1962
|
process2.on("exit", (code) => {
|
|
856
1963
|
if (code === 0) {
|
|
857
|
-
|
|
1964
|
+
resolve3();
|
|
858
1965
|
return;
|
|
859
1966
|
}
|
|
860
1967
|
reject(new Error(stderr.trim() || `generate-json-schema exited with code ${String(code)}`));
|
|
@@ -862,13 +1969,13 @@ var MethodCatalog = class {
|
|
|
862
1969
|
});
|
|
863
1970
|
}
|
|
864
1971
|
extractMethodsFromClientRequest(payload) {
|
|
865
|
-
const root =
|
|
1972
|
+
const root = asRecord2(payload);
|
|
866
1973
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
867
1974
|
const methods = /* @__PURE__ */ new Set();
|
|
868
1975
|
for (const entry of oneOf) {
|
|
869
|
-
const row =
|
|
870
|
-
const properties =
|
|
871
|
-
const methodDef =
|
|
1976
|
+
const row = asRecord2(entry);
|
|
1977
|
+
const properties = asRecord2(row?.properties);
|
|
1978
|
+
const methodDef = asRecord2(properties?.method);
|
|
872
1979
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
873
1980
|
for (const item of methodEnum) {
|
|
874
1981
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -879,13 +1986,13 @@ var MethodCatalog = class {
|
|
|
879
1986
|
return Array.from(methods).sort((a, b) => a.localeCompare(b));
|
|
880
1987
|
}
|
|
881
1988
|
extractMethodsFromServerNotification(payload) {
|
|
882
|
-
const root =
|
|
1989
|
+
const root = asRecord2(payload);
|
|
883
1990
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
884
1991
|
const methods = /* @__PURE__ */ new Set();
|
|
885
1992
|
for (const entry of oneOf) {
|
|
886
|
-
const row =
|
|
887
|
-
const properties =
|
|
888
|
-
const methodDef =
|
|
1993
|
+
const row = asRecord2(entry);
|
|
1994
|
+
const properties = asRecord2(row?.properties);
|
|
1995
|
+
const methodDef = asRecord2(properties?.method);
|
|
889
1996
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
890
1997
|
for (const item of methodEnum) {
|
|
891
1998
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -899,10 +2006,10 @@ var MethodCatalog = class {
|
|
|
899
2006
|
if (this.methodCache) {
|
|
900
2007
|
return this.methodCache;
|
|
901
2008
|
}
|
|
902
|
-
const outDir = await
|
|
2009
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
903
2010
|
await this.runGenerateSchemaCommand(outDir);
|
|
904
|
-
const clientRequestPath =
|
|
905
|
-
const raw = await
|
|
2011
|
+
const clientRequestPath = join2(outDir, "ClientRequest.json");
|
|
2012
|
+
const raw = await readFile2(clientRequestPath, "utf8");
|
|
906
2013
|
const parsed = JSON.parse(raw);
|
|
907
2014
|
const methods = this.extractMethodsFromClientRequest(parsed);
|
|
908
2015
|
this.methodCache = methods;
|
|
@@ -912,10 +2019,10 @@ var MethodCatalog = class {
|
|
|
912
2019
|
if (this.notificationCache) {
|
|
913
2020
|
return this.notificationCache;
|
|
914
2021
|
}
|
|
915
|
-
const outDir = await
|
|
2022
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
916
2023
|
await this.runGenerateSchemaCommand(outDir);
|
|
917
|
-
const serverNotificationPath =
|
|
918
|
-
const raw = await
|
|
2024
|
+
const serverNotificationPath = join2(outDir, "ServerNotification.json");
|
|
2025
|
+
const raw = await readFile2(serverNotificationPath, "utf8");
|
|
919
2026
|
const parsed = JSON.parse(raw);
|
|
920
2027
|
const methods = this.extractMethodsFromServerNotification(parsed);
|
|
921
2028
|
this.notificationCache = methods;
|
|
@@ -938,7 +2045,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
938
2045
|
const threads = [];
|
|
939
2046
|
let cursor = null;
|
|
940
2047
|
do {
|
|
941
|
-
const response =
|
|
2048
|
+
const response = asRecord2(await appServer.rpc("thread/list", {
|
|
942
2049
|
archived: false,
|
|
943
2050
|
limit: 100,
|
|
944
2051
|
sortKey: "updated_at",
|
|
@@ -946,7 +2053,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
946
2053
|
}));
|
|
947
2054
|
const data = Array.isArray(response?.data) ? response.data : [];
|
|
948
2055
|
for (const row of data) {
|
|
949
|
-
const record =
|
|
2056
|
+
const record = asRecord2(row);
|
|
950
2057
|
const id = typeof record?.id === "string" ? record.id : "";
|
|
951
2058
|
if (!id) continue;
|
|
952
2059
|
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 +2117,7 @@ function createCodexBridgeMiddleware() {
|
|
|
1010
2117
|
}
|
|
1011
2118
|
return threadSearchIndexPromise;
|
|
1012
2119
|
}
|
|
2120
|
+
void initializeSkillsSyncOnStartup(appServer);
|
|
1013
2121
|
const middleware = async (req, res, next) => {
|
|
1014
2122
|
try {
|
|
1015
2123
|
if (!req.url) {
|
|
@@ -1017,30 +2125,33 @@ function createCodexBridgeMiddleware() {
|
|
|
1017
2125
|
return;
|
|
1018
2126
|
}
|
|
1019
2127
|
const url = new URL(req.url, "http://localhost");
|
|
2128
|
+
if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
1020
2131
|
if (req.method === "POST" && url.pathname === "/codex-api/upload-file") {
|
|
1021
2132
|
handleFileUpload(req, res);
|
|
1022
2133
|
return;
|
|
1023
2134
|
}
|
|
1024
2135
|
if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
|
|
1025
2136
|
const payload = await readJsonBody(req);
|
|
1026
|
-
const body =
|
|
2137
|
+
const body = asRecord2(payload);
|
|
1027
2138
|
if (!body || typeof body.method !== "string" || body.method.length === 0) {
|
|
1028
|
-
|
|
2139
|
+
setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
|
|
1029
2140
|
return;
|
|
1030
2141
|
}
|
|
1031
2142
|
const result = await appServer.rpc(body.method, body.params ?? null);
|
|
1032
|
-
|
|
2143
|
+
setJson2(res, 200, { result });
|
|
1033
2144
|
return;
|
|
1034
2145
|
}
|
|
1035
2146
|
if (req.method === "POST" && url.pathname === "/codex-api/transcribe") {
|
|
1036
2147
|
const auth = await readCodexAuth();
|
|
1037
2148
|
if (!auth) {
|
|
1038
|
-
|
|
2149
|
+
setJson2(res, 401, { error: "No auth token available for transcription" });
|
|
1039
2150
|
return;
|
|
1040
2151
|
}
|
|
1041
2152
|
const rawBody = await readRawBody(req);
|
|
1042
2153
|
const incomingCt = req.headers["content-type"] ?? "application/octet-stream";
|
|
1043
|
-
const upstream = await proxyTranscribe(rawBody, incomingCt, auth.accessToken, auth.accountId);
|
|
2154
|
+
const upstream = await proxyTranscribe(rawBody, incomingCt, auth.accessToken, auth.accountId, auth.apiKey);
|
|
1044
2155
|
res.statusCode = upstream.status;
|
|
1045
2156
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
1046
2157
|
res.end(upstream.body);
|
|
@@ -1049,48 +2160,48 @@ function createCodexBridgeMiddleware() {
|
|
|
1049
2160
|
if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
|
|
1050
2161
|
const payload = await readJsonBody(req);
|
|
1051
2162
|
await appServer.respondToServerRequest(payload);
|
|
1052
|
-
|
|
2163
|
+
setJson2(res, 200, { ok: true });
|
|
1053
2164
|
return;
|
|
1054
2165
|
}
|
|
1055
2166
|
if (req.method === "GET" && url.pathname === "/codex-api/server-requests/pending") {
|
|
1056
|
-
|
|
2167
|
+
setJson2(res, 200, { data: appServer.listPendingServerRequests() });
|
|
1057
2168
|
return;
|
|
1058
2169
|
}
|
|
1059
2170
|
if (req.method === "GET" && url.pathname === "/codex-api/meta/methods") {
|
|
1060
2171
|
const methods = await methodCatalog.listMethods();
|
|
1061
|
-
|
|
2172
|
+
setJson2(res, 200, { data: methods });
|
|
1062
2173
|
return;
|
|
1063
2174
|
}
|
|
1064
2175
|
if (req.method === "GET" && url.pathname === "/codex-api/meta/notifications") {
|
|
1065
2176
|
const methods = await methodCatalog.listNotificationMethods();
|
|
1066
|
-
|
|
2177
|
+
setJson2(res, 200, { data: methods });
|
|
1067
2178
|
return;
|
|
1068
2179
|
}
|
|
1069
2180
|
if (req.method === "GET" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1070
2181
|
const state = await readWorkspaceRootsState();
|
|
1071
|
-
|
|
2182
|
+
setJson2(res, 200, { data: state });
|
|
1072
2183
|
return;
|
|
1073
2184
|
}
|
|
1074
2185
|
if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
|
|
1075
|
-
|
|
2186
|
+
setJson2(res, 200, { data: { path: homedir2() } });
|
|
1076
2187
|
return;
|
|
1077
2188
|
}
|
|
1078
2189
|
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
1079
|
-
const payload =
|
|
2190
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1080
2191
|
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
1081
2192
|
if (!rawSourceCwd) {
|
|
1082
|
-
|
|
2193
|
+
setJson2(res, 400, { error: "Missing sourceCwd" });
|
|
1083
2194
|
return;
|
|
1084
2195
|
}
|
|
1085
2196
|
const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
|
|
1086
2197
|
try {
|
|
1087
|
-
const sourceInfo = await
|
|
2198
|
+
const sourceInfo = await stat2(sourceCwd);
|
|
1088
2199
|
if (!sourceInfo.isDirectory()) {
|
|
1089
|
-
|
|
2200
|
+
setJson2(res, 400, { error: "sourceCwd is not a directory" });
|
|
1090
2201
|
return;
|
|
1091
2202
|
}
|
|
1092
2203
|
} catch {
|
|
1093
|
-
|
|
2204
|
+
setJson2(res, 404, { error: "sourceCwd does not exist" });
|
|
1094
2205
|
return;
|
|
1095
2206
|
}
|
|
1096
2207
|
try {
|
|
@@ -1099,25 +2210,25 @@ function createCodexBridgeMiddleware() {
|
|
|
1099
2210
|
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1100
2211
|
} catch (error) {
|
|
1101
2212
|
if (!isNotGitRepositoryError(error)) throw error;
|
|
1102
|
-
await
|
|
2213
|
+
await runCommand2("git", ["init"], { cwd: sourceCwd });
|
|
1103
2214
|
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1104
2215
|
}
|
|
1105
2216
|
const repoName = basename(gitRoot) || "repo";
|
|
1106
|
-
const worktreesRoot =
|
|
1107
|
-
await
|
|
2217
|
+
const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
|
|
2218
|
+
await mkdir2(worktreesRoot, { recursive: true });
|
|
1108
2219
|
let worktreeId = "";
|
|
1109
2220
|
let worktreeParent = "";
|
|
1110
2221
|
let worktreeCwd = "";
|
|
1111
2222
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
1112
2223
|
const candidate = randomBytes(2).toString("hex");
|
|
1113
|
-
const parent =
|
|
2224
|
+
const parent = join2(worktreesRoot, candidate);
|
|
1114
2225
|
try {
|
|
1115
|
-
await
|
|
2226
|
+
await stat2(parent);
|
|
1116
2227
|
continue;
|
|
1117
2228
|
} catch {
|
|
1118
2229
|
worktreeId = candidate;
|
|
1119
2230
|
worktreeParent = parent;
|
|
1120
|
-
worktreeCwd =
|
|
2231
|
+
worktreeCwd = join2(parent, repoName);
|
|
1121
2232
|
break;
|
|
1122
2233
|
}
|
|
1123
2234
|
}
|
|
@@ -1125,15 +2236,15 @@ function createCodexBridgeMiddleware() {
|
|
|
1125
2236
|
throw new Error("Failed to allocate a unique worktree id");
|
|
1126
2237
|
}
|
|
1127
2238
|
const branch = `codex/${worktreeId}`;
|
|
1128
|
-
await
|
|
2239
|
+
await mkdir2(worktreeParent, { recursive: true });
|
|
1129
2240
|
try {
|
|
1130
|
-
await
|
|
2241
|
+
await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1131
2242
|
} catch (error) {
|
|
1132
2243
|
if (!isMissingHeadError(error)) throw error;
|
|
1133
2244
|
await ensureRepoHasInitialCommit(gitRoot);
|
|
1134
|
-
await
|
|
2245
|
+
await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1135
2246
|
}
|
|
1136
|
-
|
|
2247
|
+
setJson2(res, 200, {
|
|
1137
2248
|
data: {
|
|
1138
2249
|
cwd: worktreeCwd,
|
|
1139
2250
|
branch,
|
|
@@ -1141,15 +2252,15 @@ function createCodexBridgeMiddleware() {
|
|
|
1141
2252
|
}
|
|
1142
2253
|
});
|
|
1143
2254
|
} catch (error) {
|
|
1144
|
-
|
|
2255
|
+
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to create worktree") });
|
|
1145
2256
|
}
|
|
1146
2257
|
return;
|
|
1147
2258
|
}
|
|
1148
2259
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1149
2260
|
const payload = await readJsonBody(req);
|
|
1150
|
-
const record =
|
|
2261
|
+
const record = asRecord2(payload);
|
|
1151
2262
|
if (!record) {
|
|
1152
|
-
|
|
2263
|
+
setJson2(res, 400, { error: "Invalid body: expected object" });
|
|
1153
2264
|
return;
|
|
1154
2265
|
}
|
|
1155
2266
|
const nextState = {
|
|
@@ -1158,33 +2269,33 @@ function createCodexBridgeMiddleware() {
|
|
|
1158
2269
|
active: normalizeStringArray(record.active)
|
|
1159
2270
|
};
|
|
1160
2271
|
await writeWorkspaceRootsState(nextState);
|
|
1161
|
-
|
|
2272
|
+
setJson2(res, 200, { ok: true });
|
|
1162
2273
|
return;
|
|
1163
2274
|
}
|
|
1164
2275
|
if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
|
|
1165
|
-
const payload =
|
|
2276
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1166
2277
|
const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
|
1167
2278
|
const createIfMissing = payload?.createIfMissing === true;
|
|
1168
2279
|
const label = typeof payload?.label === "string" ? payload.label : "";
|
|
1169
2280
|
if (!rawPath) {
|
|
1170
|
-
|
|
2281
|
+
setJson2(res, 400, { error: "Missing path" });
|
|
1171
2282
|
return;
|
|
1172
2283
|
}
|
|
1173
2284
|
const normalizedPath = isAbsolute(rawPath) ? rawPath : resolve(rawPath);
|
|
1174
2285
|
let pathExists = true;
|
|
1175
2286
|
try {
|
|
1176
|
-
const info = await
|
|
2287
|
+
const info = await stat2(normalizedPath);
|
|
1177
2288
|
if (!info.isDirectory()) {
|
|
1178
|
-
|
|
2289
|
+
setJson2(res, 400, { error: "Path exists but is not a directory" });
|
|
1179
2290
|
return;
|
|
1180
2291
|
}
|
|
1181
2292
|
} catch {
|
|
1182
2293
|
pathExists = false;
|
|
1183
2294
|
}
|
|
1184
2295
|
if (!pathExists && createIfMissing) {
|
|
1185
|
-
await
|
|
2296
|
+
await mkdir2(normalizedPath, { recursive: true });
|
|
1186
2297
|
} else if (!pathExists) {
|
|
1187
|
-
|
|
2298
|
+
setJson2(res, 404, { error: "Directory does not exist" });
|
|
1188
2299
|
return;
|
|
1189
2300
|
}
|
|
1190
2301
|
const existingState = await readWorkspaceRootsState();
|
|
@@ -1199,215 +2310,103 @@ function createCodexBridgeMiddleware() {
|
|
|
1199
2310
|
labels: nextLabels,
|
|
1200
2311
|
active: nextActive
|
|
1201
2312
|
});
|
|
1202
|
-
|
|
2313
|
+
setJson2(res, 200, { data: { path: normalizedPath } });
|
|
1203
2314
|
return;
|
|
1204
2315
|
}
|
|
1205
2316
|
if (req.method === "GET" && url.pathname === "/codex-api/project-root-suggestion") {
|
|
1206
2317
|
const basePath = url.searchParams.get("basePath")?.trim() ?? "";
|
|
1207
2318
|
if (!basePath) {
|
|
1208
|
-
|
|
2319
|
+
setJson2(res, 400, { error: "Missing basePath" });
|
|
1209
2320
|
return;
|
|
1210
2321
|
}
|
|
1211
2322
|
const normalizedBasePath = isAbsolute(basePath) ? basePath : resolve(basePath);
|
|
1212
2323
|
try {
|
|
1213
|
-
const baseInfo = await
|
|
2324
|
+
const baseInfo = await stat2(normalizedBasePath);
|
|
1214
2325
|
if (!baseInfo.isDirectory()) {
|
|
1215
|
-
|
|
2326
|
+
setJson2(res, 400, { error: "basePath is not a directory" });
|
|
1216
2327
|
return;
|
|
1217
2328
|
}
|
|
1218
2329
|
} catch {
|
|
1219
|
-
|
|
2330
|
+
setJson2(res, 404, { error: "basePath does not exist" });
|
|
1220
2331
|
return;
|
|
1221
2332
|
}
|
|
1222
2333
|
let index = 1;
|
|
1223
2334
|
while (index < 1e5) {
|
|
1224
2335
|
const candidateName = `New Project (${String(index)})`;
|
|
1225
|
-
const candidatePath =
|
|
2336
|
+
const candidatePath = join2(normalizedBasePath, candidateName);
|
|
1226
2337
|
try {
|
|
1227
|
-
await
|
|
2338
|
+
await stat2(candidatePath);
|
|
1228
2339
|
index += 1;
|
|
1229
2340
|
continue;
|
|
1230
2341
|
} catch {
|
|
1231
|
-
|
|
2342
|
+
setJson2(res, 200, { data: { name: candidateName, path: candidatePath } });
|
|
1232
2343
|
return;
|
|
1233
2344
|
}
|
|
1234
2345
|
}
|
|
1235
|
-
|
|
2346
|
+
setJson2(res, 500, { error: "Failed to compute project name suggestion" });
|
|
1236
2347
|
return;
|
|
1237
2348
|
}
|
|
1238
2349
|
if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
|
|
1239
|
-
const payload =
|
|
2350
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1240
2351
|
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
1241
2352
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1242
2353
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
|
|
1243
2354
|
const limit = Math.max(1, Math.min(100, Math.floor(limitRaw)));
|
|
1244
2355
|
if (!rawCwd) {
|
|
1245
|
-
|
|
2356
|
+
setJson2(res, 400, { error: "Missing cwd" });
|
|
1246
2357
|
return;
|
|
1247
2358
|
}
|
|
1248
2359
|
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
1249
2360
|
try {
|
|
1250
|
-
const info = await
|
|
2361
|
+
const info = await stat2(cwd);
|
|
1251
2362
|
if (!info.isDirectory()) {
|
|
1252
|
-
|
|
2363
|
+
setJson2(res, 400, { error: "cwd is not a directory" });
|
|
1253
2364
|
return;
|
|
1254
2365
|
}
|
|
1255
2366
|
} catch {
|
|
1256
|
-
|
|
2367
|
+
setJson2(res, 404, { error: "cwd does not exist" });
|
|
1257
2368
|
return;
|
|
1258
2369
|
}
|
|
1259
2370
|
try {
|
|
1260
2371
|
const files = await listFilesWithRipgrep(cwd);
|
|
1261
2372
|
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
|
-
|
|
2373
|
+
setJson2(res, 200, { data: scored });
|
|
1263
2374
|
} catch (error) {
|
|
1264
|
-
|
|
2375
|
+
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to search files") });
|
|
1265
2376
|
}
|
|
1266
2377
|
return;
|
|
1267
2378
|
}
|
|
1268
2379
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
|
|
1269
|
-
const cache = await
|
|
1270
|
-
|
|
2380
|
+
const cache = await readMergedThreadTitleCache();
|
|
2381
|
+
setJson2(res, 200, { data: cache });
|
|
1271
2382
|
return;
|
|
1272
2383
|
}
|
|
1273
2384
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
1274
|
-
const payload =
|
|
2385
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1275
2386
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1276
2387
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
1277
2388
|
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
1278
2389
|
if (!query) {
|
|
1279
|
-
|
|
2390
|
+
setJson2(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
|
|
1280
2391
|
return;
|
|
1281
2392
|
}
|
|
1282
2393
|
const index = await getThreadSearchIndex();
|
|
1283
2394
|
const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
|
|
1284
|
-
|
|
2395
|
+
setJson2(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
|
|
1285
2396
|
return;
|
|
1286
2397
|
}
|
|
1287
2398
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
1288
|
-
const payload =
|
|
2399
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1289
2400
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
1290
2401
|
const title = typeof payload?.title === "string" ? payload.title : "";
|
|
1291
2402
|
if (!id) {
|
|
1292
|
-
|
|
2403
|
+
setJson2(res, 400, { error: "Missing id" });
|
|
1293
2404
|
return;
|
|
1294
2405
|
}
|
|
1295
2406
|
const cache = await readThreadTitleCache();
|
|
1296
2407
|
const next2 = title ? updateThreadTitleCache(cache, id, title) : removeFromThreadTitleCache(cache, id);
|
|
1297
2408
|
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
|
-
}
|
|
2409
|
+
setJson2(res, 200, { ok: true });
|
|
1411
2410
|
return;
|
|
1412
2411
|
}
|
|
1413
2412
|
if (req.method === "GET" && url.pathname === "/codex-api/events") {
|
|
@@ -1442,8 +2441,8 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1442
2441
|
}
|
|
1443
2442
|
next();
|
|
1444
2443
|
} catch (error) {
|
|
1445
|
-
const message =
|
|
1446
|
-
|
|
2444
|
+
const message = getErrorMessage2(error, "Unknown bridge error");
|
|
2445
|
+
setJson2(res, 502, { error: message });
|
|
1447
2446
|
}
|
|
1448
2447
|
};
|
|
1449
2448
|
middleware.dispose = () => {
|
|
@@ -1580,8 +2579,8 @@ function createAuthSession(password) {
|
|
|
1580
2579
|
}
|
|
1581
2580
|
|
|
1582
2581
|
// src/server/localBrowseUi.ts
|
|
1583
|
-
import { dirname, extname, join as
|
|
1584
|
-
import { open, readFile as
|
|
2582
|
+
import { dirname, extname, join as join3 } from "path";
|
|
2583
|
+
import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
1585
2584
|
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1586
2585
|
".txt",
|
|
1587
2586
|
".md",
|
|
@@ -1696,7 +2695,7 @@ async function probeFileIsText(localPath) {
|
|
|
1696
2695
|
async function isTextEditableFile(localPath) {
|
|
1697
2696
|
if (isTextEditablePath(localPath)) return true;
|
|
1698
2697
|
try {
|
|
1699
|
-
const fileStat = await
|
|
2698
|
+
const fileStat = await stat3(localPath);
|
|
1700
2699
|
if (!fileStat.isFile()) return false;
|
|
1701
2700
|
return await probeFileIsText(localPath);
|
|
1702
2701
|
} catch {
|
|
@@ -1716,10 +2715,10 @@ function escapeForInlineScriptString(value) {
|
|
|
1716
2715
|
return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
|
|
1717
2716
|
}
|
|
1718
2717
|
async function getDirectoryItems(localPath) {
|
|
1719
|
-
const entries = await
|
|
2718
|
+
const entries = await readdir3(localPath, { withFileTypes: true });
|
|
1720
2719
|
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
1721
|
-
const entryPath =
|
|
1722
|
-
const entryStat = await
|
|
2720
|
+
const entryPath = join3(localPath, entry.name);
|
|
2721
|
+
const entryStat = await stat3(entryPath);
|
|
1723
2722
|
const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
|
|
1724
2723
|
return {
|
|
1725
2724
|
name: entry.name,
|
|
@@ -1777,7 +2776,7 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
1777
2776
|
</html>`;
|
|
1778
2777
|
}
|
|
1779
2778
|
async function createTextEditorHtml(localPath) {
|
|
1780
|
-
const content = await
|
|
2779
|
+
const content = await readFile3(localPath, "utf8");
|
|
1781
2780
|
const parentPath = dirname(localPath);
|
|
1782
2781
|
const language = languageForPath(localPath);
|
|
1783
2782
|
const safeContentLiteral = escapeForInlineScriptString(content);
|
|
@@ -1848,8 +2847,8 @@ async function createTextEditorHtml(localPath) {
|
|
|
1848
2847
|
// src/server/httpServer.ts
|
|
1849
2848
|
import { WebSocketServer } from "ws";
|
|
1850
2849
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
1851
|
-
var distDir =
|
|
1852
|
-
var spaEntryFile =
|
|
2850
|
+
var distDir = join4(__dirname, "..", "dist");
|
|
2851
|
+
var spaEntryFile = join4(distDir, "index.html");
|
|
1853
2852
|
var IMAGE_CONTENT_TYPES = {
|
|
1854
2853
|
".avif": "image/avif",
|
|
1855
2854
|
".bmp": "image/bmp",
|
|
@@ -1860,6 +2859,20 @@ var IMAGE_CONTENT_TYPES = {
|
|
|
1860
2859
|
".svg": "image/svg+xml",
|
|
1861
2860
|
".webp": "image/webp"
|
|
1862
2861
|
};
|
|
2862
|
+
function renderFrontendMissingHtml(message, details) {
|
|
2863
|
+
const lines = details && details.length > 0 ? `<pre>${details.join("\n")}</pre>` : "";
|
|
2864
|
+
return [
|
|
2865
|
+
"<!doctype html>",
|
|
2866
|
+
'<html lang="en">',
|
|
2867
|
+
'<head><meta charset="utf-8"><title>Codex Web UI Error</title></head>',
|
|
2868
|
+
"<body>",
|
|
2869
|
+
`<h1>${message}</h1>`,
|
|
2870
|
+
lines,
|
|
2871
|
+
'<p><a href="/">Back to chat</a></p>',
|
|
2872
|
+
"</body>",
|
|
2873
|
+
"</html>"
|
|
2874
|
+
].join("");
|
|
2875
|
+
}
|
|
1863
2876
|
function normalizeLocalImagePath(rawPath) {
|
|
1864
2877
|
const trimmed = rawPath.trim();
|
|
1865
2878
|
if (!trimmed) return "";
|
|
@@ -1926,7 +2939,7 @@ function createServer(options = {}) {
|
|
|
1926
2939
|
return;
|
|
1927
2940
|
}
|
|
1928
2941
|
try {
|
|
1929
|
-
const fileStat = await
|
|
2942
|
+
const fileStat = await stat4(localPath);
|
|
1930
2943
|
res.setHeader("Cache-Control", "private, no-store");
|
|
1931
2944
|
if (fileStat.isDirectory()) {
|
|
1932
2945
|
const html = await createDirectoryListingHtml(localPath);
|
|
@@ -1949,7 +2962,7 @@ function createServer(options = {}) {
|
|
|
1949
2962
|
return;
|
|
1950
2963
|
}
|
|
1951
2964
|
try {
|
|
1952
|
-
const fileStat = await
|
|
2965
|
+
const fileStat = await stat4(localPath);
|
|
1953
2966
|
if (!fileStat.isFile()) {
|
|
1954
2967
|
res.status(400).json({ error: "Expected file path." });
|
|
1955
2968
|
return;
|
|
@@ -1973,32 +2986,31 @@ function createServer(options = {}) {
|
|
|
1973
2986
|
}
|
|
1974
2987
|
const body = typeof req.body === "string" ? req.body : "";
|
|
1975
2988
|
try {
|
|
1976
|
-
await
|
|
2989
|
+
await writeFile3(localPath, body, "utf8");
|
|
1977
2990
|
res.status(200).json({ ok: true });
|
|
1978
2991
|
} catch {
|
|
1979
2992
|
res.status(404).json({ error: "File not found." });
|
|
1980
2993
|
}
|
|
1981
2994
|
});
|
|
1982
|
-
const hasFrontendAssets =
|
|
2995
|
+
const hasFrontendAssets = existsSync3(spaEntryFile);
|
|
1983
2996
|
if (hasFrontendAssets) {
|
|
1984
2997
|
app.use(express.static(distDir));
|
|
1985
2998
|
}
|
|
1986
2999
|
app.use((_req, res) => {
|
|
1987
3000
|
if (!hasFrontendAssets) {
|
|
1988
|
-
res.status(503).type("text/
|
|
1989
|
-
[
|
|
1990
|
-
"Codex web UI assets are missing.",
|
|
3001
|
+
res.status(503).type("text/html; charset=utf-8").send(
|
|
3002
|
+
renderFrontendMissingHtml("Codex web UI assets are missing.", [
|
|
1991
3003
|
`Expected: ${spaEntryFile}`,
|
|
1992
3004
|
"If running from source, build frontend assets with: npm run build:frontend",
|
|
1993
3005
|
"If running with npx, clear the npx cache and reinstall codexapp."
|
|
1994
|
-
]
|
|
3006
|
+
])
|
|
1995
3007
|
);
|
|
1996
3008
|
return;
|
|
1997
3009
|
}
|
|
1998
3010
|
res.sendFile(spaEntryFile, (error) => {
|
|
1999
3011
|
if (!error) return;
|
|
2000
3012
|
if (!res.headersSent) {
|
|
2001
|
-
res.status(404).type("text/
|
|
3013
|
+
res.status(404).type("text/html; charset=utf-8").send(renderFrontendMissingHtml("Frontend entry file not found."));
|
|
2002
3014
|
}
|
|
2003
3015
|
});
|
|
2004
3016
|
});
|
|
@@ -2053,8 +3065,8 @@ var program = new Command().name("codexui").description("Web interface for Codex
|
|
|
2053
3065
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
2054
3066
|
async function readCliVersion() {
|
|
2055
3067
|
try {
|
|
2056
|
-
const packageJsonPath =
|
|
2057
|
-
const raw = await
|
|
3068
|
+
const packageJsonPath = join5(__dirname2, "..", "package.json");
|
|
3069
|
+
const raw = await readFile4(packageJsonPath, "utf8");
|
|
2058
3070
|
const parsed = JSON.parse(raw);
|
|
2059
3071
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
2060
3072
|
} catch {
|
|
@@ -2079,22 +3091,22 @@ function runWithStatus(command, args) {
|
|
|
2079
3091
|
return result.status ?? -1;
|
|
2080
3092
|
}
|
|
2081
3093
|
function getUserNpmPrefix() {
|
|
2082
|
-
return
|
|
3094
|
+
return join5(homedir3(), ".npm-global");
|
|
2083
3095
|
}
|
|
2084
3096
|
function resolveCodexCommand() {
|
|
2085
3097
|
if (canRun("codex", ["--version"])) {
|
|
2086
3098
|
return "codex";
|
|
2087
3099
|
}
|
|
2088
|
-
const userCandidate =
|
|
2089
|
-
if (
|
|
3100
|
+
const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
|
|
3101
|
+
if (existsSync4(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
2090
3102
|
return userCandidate;
|
|
2091
3103
|
}
|
|
2092
3104
|
const prefix = process.env.PREFIX?.trim();
|
|
2093
3105
|
if (!prefix) {
|
|
2094
3106
|
return null;
|
|
2095
3107
|
}
|
|
2096
|
-
const candidate =
|
|
2097
|
-
if (
|
|
3108
|
+
const candidate = join5(prefix, "bin", "codex");
|
|
3109
|
+
if (existsSync4(candidate) && canRun(candidate, ["--version"])) {
|
|
2098
3110
|
return candidate;
|
|
2099
3111
|
}
|
|
2100
3112
|
return null;
|
|
@@ -2103,8 +3115,8 @@ function resolveCloudflaredCommand() {
|
|
|
2103
3115
|
if (canRun("cloudflared", ["--version"])) {
|
|
2104
3116
|
return "cloudflared";
|
|
2105
3117
|
}
|
|
2106
|
-
const localCandidate =
|
|
2107
|
-
if (
|
|
3118
|
+
const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
|
|
3119
|
+
if (existsSync4(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
2108
3120
|
return localCandidate;
|
|
2109
3121
|
}
|
|
2110
3122
|
return null;
|
|
@@ -2119,7 +3131,7 @@ function mapCloudflaredLinuxArch(arch) {
|
|
|
2119
3131
|
return null;
|
|
2120
3132
|
}
|
|
2121
3133
|
function downloadFile(url, destination) {
|
|
2122
|
-
return new Promise((
|
|
3134
|
+
return new Promise((resolve3, reject) => {
|
|
2123
3135
|
const request = (currentUrl) => {
|
|
2124
3136
|
httpsGet(currentUrl, (response) => {
|
|
2125
3137
|
const code = response.statusCode ?? 0;
|
|
@@ -2137,7 +3149,7 @@ function downloadFile(url, destination) {
|
|
|
2137
3149
|
response.pipe(file);
|
|
2138
3150
|
file.on("finish", () => {
|
|
2139
3151
|
file.close();
|
|
2140
|
-
|
|
3152
|
+
resolve3();
|
|
2141
3153
|
});
|
|
2142
3154
|
file.on("error", reject);
|
|
2143
3155
|
}).on("error", reject);
|
|
@@ -2157,9 +3169,9 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
2157
3169
|
if (!mappedArch) {
|
|
2158
3170
|
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
2159
3171
|
}
|
|
2160
|
-
const userBinDir =
|
|
3172
|
+
const userBinDir = join5(homedir3(), ".local", "bin");
|
|
2161
3173
|
mkdirSync(userBinDir, { recursive: true });
|
|
2162
|
-
const destination =
|
|
3174
|
+
const destination = join5(userBinDir, "cloudflared");
|
|
2163
3175
|
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
2164
3176
|
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
2165
3177
|
await downloadFile(downloadUrl, destination);
|
|
@@ -2177,7 +3189,7 @@ async function shouldInstallCloudflaredInteractively() {
|
|
|
2177
3189
|
console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
|
|
2178
3190
|
return false;
|
|
2179
3191
|
}
|
|
2180
|
-
const prompt =
|
|
3192
|
+
const prompt = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2181
3193
|
try {
|
|
2182
3194
|
const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
|
|
2183
3195
|
const normalized = answer.trim().toLowerCase();
|
|
@@ -2198,8 +3210,8 @@ async function resolveCloudflaredForTunnel() {
|
|
|
2198
3210
|
return ensureCloudflaredInstalledLinux();
|
|
2199
3211
|
}
|
|
2200
3212
|
function hasCodexAuth() {
|
|
2201
|
-
const codexHome = process.env.CODEX_HOME?.trim() ||
|
|
2202
|
-
return
|
|
3213
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
|
|
3214
|
+
return existsSync4(join5(codexHome, "auth.json"));
|
|
2203
3215
|
}
|
|
2204
3216
|
function ensureCodexInstalled() {
|
|
2205
3217
|
let codexCommand = resolveCodexCommand();
|
|
@@ -2217,7 +3229,7 @@ function ensureCodexInstalled() {
|
|
|
2217
3229
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
2218
3230
|
`);
|
|
2219
3231
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
2220
|
-
process.env.PATH = `${
|
|
3232
|
+
process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
2221
3233
|
};
|
|
2222
3234
|
if (isTermuxRuntime()) {
|
|
2223
3235
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -2266,7 +3278,7 @@ function printTermuxKeepAlive(lines) {
|
|
|
2266
3278
|
}
|
|
2267
3279
|
function openBrowser(url) {
|
|
2268
3280
|
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 =
|
|
3281
|
+
const child = spawn3(command.cmd, command.args, { detached: true, stdio: "ignore" });
|
|
2270
3282
|
child.on("error", () => {
|
|
2271
3283
|
});
|
|
2272
3284
|
child.unref();
|
|
@@ -2280,25 +3292,28 @@ function parseCloudflaredUrl(chunk) {
|
|
|
2280
3292
|
}
|
|
2281
3293
|
function getAccessibleUrls(port) {
|
|
2282
3294
|
const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
}
|
|
2288
|
-
for (const entry of entries) {
|
|
2289
|
-
if (entry.internal) {
|
|
3295
|
+
try {
|
|
3296
|
+
const interfaces = networkInterfaces();
|
|
3297
|
+
for (const entries of Object.values(interfaces)) {
|
|
3298
|
+
if (!entries) {
|
|
2290
3299
|
continue;
|
|
2291
3300
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
3301
|
+
for (const entry of entries) {
|
|
3302
|
+
if (entry.internal) {
|
|
3303
|
+
continue;
|
|
3304
|
+
}
|
|
3305
|
+
if (entry.family === "IPv4") {
|
|
3306
|
+
urls.add(`http://${entry.address}:${String(port)}`);
|
|
3307
|
+
}
|
|
2294
3308
|
}
|
|
2295
3309
|
}
|
|
3310
|
+
} catch {
|
|
2296
3311
|
}
|
|
2297
3312
|
return Array.from(urls);
|
|
2298
3313
|
}
|
|
2299
3314
|
async function startCloudflaredTunnel(command, localPort) {
|
|
2300
|
-
return new Promise((
|
|
2301
|
-
const child =
|
|
3315
|
+
return new Promise((resolve3, reject) => {
|
|
3316
|
+
const child = spawn3(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
|
|
2302
3317
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2303
3318
|
});
|
|
2304
3319
|
const timeout = setTimeout(() => {
|
|
@@ -2314,7 +3329,7 @@ async function startCloudflaredTunnel(command, localPort) {
|
|
|
2314
3329
|
clearTimeout(timeout);
|
|
2315
3330
|
child.stdout?.off("data", handleData);
|
|
2316
3331
|
child.stderr?.off("data", handleData);
|
|
2317
|
-
|
|
3332
|
+
resolve3({ process: child, url: parsedUrl });
|
|
2318
3333
|
};
|
|
2319
3334
|
const onError = (error) => {
|
|
2320
3335
|
clearTimeout(timeout);
|
|
@@ -2333,7 +3348,7 @@ async function startCloudflaredTunnel(command, localPort) {
|
|
|
2333
3348
|
});
|
|
2334
3349
|
}
|
|
2335
3350
|
function listenWithFallback(server, startPort) {
|
|
2336
|
-
return new Promise((
|
|
3351
|
+
return new Promise((resolve3, reject) => {
|
|
2337
3352
|
const attempt = (port) => {
|
|
2338
3353
|
const onError = (error) => {
|
|
2339
3354
|
server.off("listening", onListening);
|
|
@@ -2345,7 +3360,7 @@ function listenWithFallback(server, startPort) {
|
|
|
2345
3360
|
};
|
|
2346
3361
|
const onListening = () => {
|
|
2347
3362
|
server.off("error", onError);
|
|
2348
|
-
|
|
3363
|
+
resolve3(port);
|
|
2349
3364
|
};
|
|
2350
3365
|
server.once("error", onError);
|
|
2351
3366
|
server.once("listening", onListening);
|
|
@@ -2354,8 +3369,72 @@ function listenWithFallback(server, startPort) {
|
|
|
2354
3369
|
attempt(startPort);
|
|
2355
3370
|
});
|
|
2356
3371
|
}
|
|
3372
|
+
function getCodexGlobalStatePath2() {
|
|
3373
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
|
|
3374
|
+
return join5(codexHome, ".codex-global-state.json");
|
|
3375
|
+
}
|
|
3376
|
+
function normalizeUniqueStrings(value) {
|
|
3377
|
+
if (!Array.isArray(value)) return [];
|
|
3378
|
+
const next = [];
|
|
3379
|
+
for (const item of value) {
|
|
3380
|
+
if (typeof item !== "string") continue;
|
|
3381
|
+
const trimmed = item.trim();
|
|
3382
|
+
if (!trimmed || next.includes(trimmed)) continue;
|
|
3383
|
+
next.push(trimmed);
|
|
3384
|
+
}
|
|
3385
|
+
return next;
|
|
3386
|
+
}
|
|
3387
|
+
async function persistLaunchProject(projectPath) {
|
|
3388
|
+
const trimmed = projectPath.trim();
|
|
3389
|
+
if (!trimmed) return;
|
|
3390
|
+
const normalizedPath = isAbsolute3(trimmed) ? trimmed : resolve2(trimmed);
|
|
3391
|
+
const directoryInfo = await stat5(normalizedPath);
|
|
3392
|
+
if (!directoryInfo.isDirectory()) {
|
|
3393
|
+
throw new Error(`Not a directory: ${normalizedPath}`);
|
|
3394
|
+
}
|
|
3395
|
+
const statePath = getCodexGlobalStatePath2();
|
|
3396
|
+
let payload = {};
|
|
3397
|
+
try {
|
|
3398
|
+
const raw = await readFile4(statePath, "utf8");
|
|
3399
|
+
const parsed = JSON.parse(raw);
|
|
3400
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3401
|
+
payload = parsed;
|
|
3402
|
+
}
|
|
3403
|
+
} catch {
|
|
3404
|
+
payload = {};
|
|
3405
|
+
}
|
|
3406
|
+
const roots = normalizeUniqueStrings(payload["electron-saved-workspace-roots"]);
|
|
3407
|
+
const activeRoots = normalizeUniqueStrings(payload["active-workspace-roots"]);
|
|
3408
|
+
payload["electron-saved-workspace-roots"] = [
|
|
3409
|
+
normalizedPath,
|
|
3410
|
+
...roots.filter((value) => value !== normalizedPath)
|
|
3411
|
+
];
|
|
3412
|
+
payload["active-workspace-roots"] = [
|
|
3413
|
+
normalizedPath,
|
|
3414
|
+
...activeRoots.filter((value) => value !== normalizedPath)
|
|
3415
|
+
];
|
|
3416
|
+
await writeFile4(statePath, JSON.stringify(payload), "utf8");
|
|
3417
|
+
}
|
|
3418
|
+
async function addProjectOnly(projectPath) {
|
|
3419
|
+
const trimmed = projectPath.trim();
|
|
3420
|
+
if (!trimmed) {
|
|
3421
|
+
throw new Error("Missing project path");
|
|
3422
|
+
}
|
|
3423
|
+
await persistLaunchProject(trimmed);
|
|
3424
|
+
}
|
|
2357
3425
|
async function startServer(options) {
|
|
2358
3426
|
const version = await readCliVersion();
|
|
3427
|
+
const projectPath = options.projectPath?.trim() ?? "";
|
|
3428
|
+
if (projectPath.length > 0) {
|
|
3429
|
+
try {
|
|
3430
|
+
await persistLaunchProject(projectPath);
|
|
3431
|
+
} catch (error) {
|
|
3432
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3433
|
+
console.warn(`
|
|
3434
|
+
[project] Could not open launch project: ${message}
|
|
3435
|
+
`);
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
2359
3438
|
const codexCommand = ensureCodexInstalled() ?? resolveCodexCommand();
|
|
2360
3439
|
if (!hasCodexAuth() && codexCommand) {
|
|
2361
3440
|
console.log("\nCodex is not logged in. Starting `codex login`...\n");
|
|
@@ -2416,7 +3495,7 @@ async function startServer(options) {
|
|
|
2416
3495
|
qrcode.generate(tunnelUrl, { small: true });
|
|
2417
3496
|
console.log("");
|
|
2418
3497
|
}
|
|
2419
|
-
openBrowser(`http://localhost:${String(port)}`);
|
|
3498
|
+
if (options.open) openBrowser(`http://localhost:${String(port)}`);
|
|
2420
3499
|
function shutdown() {
|
|
2421
3500
|
console.log("\nShutting down...");
|
|
2422
3501
|
if (tunnelChild && !tunnelChild.killed) {
|
|
@@ -2439,8 +3518,20 @@ async function runLogin() {
|
|
|
2439
3518
|
console.log("\nStarting `codex login`...\n");
|
|
2440
3519
|
runOrFail(codexCommand, ["login"], "Codex login");
|
|
2441
3520
|
}
|
|
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
|
-
|
|
3521
|
+
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) => {
|
|
3522
|
+
const rawArgv = process.argv.slice(2);
|
|
3523
|
+
const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
|
|
3524
|
+
let openProjectOnly = (opts.openProject ?? "").trim();
|
|
3525
|
+
if (!openProjectOnly && openProjectFlagIndex >= 0 && projectPath?.trim()) {
|
|
3526
|
+
openProjectOnly = projectPath.trim();
|
|
3527
|
+
}
|
|
3528
|
+
if (openProjectOnly.length > 0) {
|
|
3529
|
+
await addProjectOnly(openProjectOnly);
|
|
3530
|
+
console.log(`Added project: ${openProjectOnly}`);
|
|
3531
|
+
return;
|
|
3532
|
+
}
|
|
3533
|
+
const launchProject = (projectPath ?? "").trim();
|
|
3534
|
+
await startServer({ ...opts, projectPath: launchProject });
|
|
2444
3535
|
});
|
|
2445
3536
|
program.command("login").description("Install/check Codex CLI and run `codex login`").action(runLogin);
|
|
2446
3537
|
program.command("help").description("Show codexui command help").action(() => {
|