codexapp 0.1.39 → 0.1.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-Dwgxc732.js +1424 -0
- package/dist/assets/index-eKA4YUIc.css +1 -0
- package/dist/index.html +2 -2
- package/dist-cli/index.js +1268 -446
- package/dist-cli/index.js.map +1 -1
- package/package.json +2 -1
- package/dist/assets/index-BPh5uQ3U.js +0 -42
- package/dist/assets/index-D7HKxnG0.css +0 -1
package/dist-cli/index.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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
|
|
5
|
+
import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } from "fs";
|
|
6
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
7
|
+
import { homedir as homedir3, networkInterfaces } from "os";
|
|
8
|
+
import { join as join5 } from "path";
|
|
9
|
+
import { spawn as spawn3, spawnSync } from "child_process";
|
|
10
10
|
import { createInterface } from "readline/promises";
|
|
11
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12
12
|
import { dirname as dirname3 } from "path";
|
|
@@ -16,52 +16,1140 @@ 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 existsSync2 } 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";
|
|
26
|
-
import { randomBytes } from "crypto";
|
|
27
|
-
import { mkdtemp, readFile
|
|
28
|
-
import { request as httpsRequest } from "https";
|
|
29
|
-
import { homedir } from "os";
|
|
30
|
-
import { tmpdir } from "os";
|
|
31
|
-
import { basename, isAbsolute, join, resolve } from "path";
|
|
32
|
-
import { writeFile } from "fs/promises";
|
|
33
|
-
|
|
25
|
+
import { spawn as spawn2 } from "child_process";
|
|
26
|
+
import { randomBytes } from "crypto";
|
|
27
|
+
import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as stat2 } from "fs/promises";
|
|
28
|
+
import { request as httpsRequest } from "https";
|
|
29
|
+
import { homedir as homedir2 } from "os";
|
|
30
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
31
|
+
import { basename, isAbsolute, join as join2, resolve } from "path";
|
|
32
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
33
|
+
|
|
34
|
+
// src/server/skillsRoutes.ts
|
|
35
|
+
import { spawn } from "child_process";
|
|
36
|
+
import { mkdtemp, readFile, readdir, rm, mkdir, stat, lstat, readlink, symlink } from "fs/promises";
|
|
37
|
+
import { existsSync } from "fs";
|
|
38
|
+
import { homedir, tmpdir } from "os";
|
|
39
|
+
import { join } from "path";
|
|
40
|
+
import { writeFile } from "fs/promises";
|
|
41
|
+
function asRecord(value) {
|
|
42
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
43
|
+
}
|
|
44
|
+
function getErrorMessage(payload, fallback) {
|
|
45
|
+
if (payload instanceof Error && payload.message.trim().length > 0) {
|
|
46
|
+
return payload.message;
|
|
47
|
+
}
|
|
48
|
+
const record = asRecord(payload);
|
|
49
|
+
if (!record) return fallback;
|
|
50
|
+
const error = record.error;
|
|
51
|
+
if (typeof error === "string" && error.length > 0) return error;
|
|
52
|
+
const nestedError = asRecord(error);
|
|
53
|
+
if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
|
|
54
|
+
return nestedError.message;
|
|
55
|
+
}
|
|
56
|
+
return fallback;
|
|
57
|
+
}
|
|
58
|
+
function setJson(res, statusCode, payload) {
|
|
59
|
+
res.statusCode = statusCode;
|
|
60
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
61
|
+
res.end(JSON.stringify(payload));
|
|
62
|
+
}
|
|
63
|
+
function getCodexHomeDir() {
|
|
64
|
+
const codexHome = process.env.CODEX_HOME?.trim();
|
|
65
|
+
return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
|
|
66
|
+
}
|
|
67
|
+
function getSkillsInstallDir() {
|
|
68
|
+
return join(getCodexHomeDir(), "skills");
|
|
69
|
+
}
|
|
70
|
+
async function runCommand(command, args, options = {}) {
|
|
71
|
+
await new Promise((resolve2, reject) => {
|
|
72
|
+
const proc = spawn(command, args, {
|
|
73
|
+
cwd: options.cwd,
|
|
74
|
+
env: process.env,
|
|
75
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
76
|
+
});
|
|
77
|
+
let stdout = "";
|
|
78
|
+
let stderr = "";
|
|
79
|
+
proc.stdout.on("data", (chunk) => {
|
|
80
|
+
stdout += chunk.toString();
|
|
81
|
+
});
|
|
82
|
+
proc.stderr.on("data", (chunk) => {
|
|
83
|
+
stderr += chunk.toString();
|
|
84
|
+
});
|
|
85
|
+
proc.on("error", reject);
|
|
86
|
+
proc.on("close", (code) => {
|
|
87
|
+
if (code === 0) {
|
|
88
|
+
resolve2();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
92
|
+
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
93
|
+
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async function runCommandWithOutput(command, args, options = {}) {
|
|
98
|
+
return await new Promise((resolve2, reject) => {
|
|
99
|
+
const proc = spawn(command, args, {
|
|
100
|
+
cwd: options.cwd,
|
|
101
|
+
env: process.env,
|
|
102
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
103
|
+
});
|
|
104
|
+
let stdout = "";
|
|
105
|
+
let stderr = "";
|
|
106
|
+
proc.stdout.on("data", (chunk) => {
|
|
107
|
+
stdout += chunk.toString();
|
|
108
|
+
});
|
|
109
|
+
proc.stderr.on("data", (chunk) => {
|
|
110
|
+
stderr += chunk.toString();
|
|
111
|
+
});
|
|
112
|
+
proc.on("error", reject);
|
|
113
|
+
proc.on("close", (code) => {
|
|
114
|
+
if (code === 0) {
|
|
115
|
+
resolve2(stdout.trim());
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
119
|
+
const suffix = details.length > 0 ? `: ${details}` : "";
|
|
120
|
+
reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async function detectUserSkillsDir(appServer) {
|
|
125
|
+
try {
|
|
126
|
+
const result = await appServer.rpc("skills/list", {});
|
|
127
|
+
for (const entry of result.data ?? []) {
|
|
128
|
+
for (const skill of entry.skills ?? []) {
|
|
129
|
+
if (skill.scope !== "user" || !skill.path) continue;
|
|
130
|
+
const parts = skill.path.split("/").filter(Boolean);
|
|
131
|
+
if (parts.length < 2) continue;
|
|
132
|
+
return `/${parts.slice(0, -2).join("/")}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
return getSkillsInstallDir();
|
|
138
|
+
}
|
|
139
|
+
async function ensureInstalledSkillIsValid(appServer, skillPath) {
|
|
140
|
+
const result = await appServer.rpc("skills/list", { forceReload: true });
|
|
141
|
+
const normalized = skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md`;
|
|
142
|
+
for (const entry of result.data ?? []) {
|
|
143
|
+
for (const error of entry.errors ?? []) {
|
|
144
|
+
if (error.path === normalized) {
|
|
145
|
+
throw new Error(error.message || "Installed skill is invalid");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
var TREE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
151
|
+
var skillsTreeCache = null;
|
|
152
|
+
var metaCache = /* @__PURE__ */ new Map();
|
|
153
|
+
async function getGhToken() {
|
|
154
|
+
try {
|
|
155
|
+
const proc = spawn("gh", ["auth", "token"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
156
|
+
let out = "";
|
|
157
|
+
proc.stdout.on("data", (d) => {
|
|
158
|
+
out += d.toString();
|
|
159
|
+
});
|
|
160
|
+
return new Promise((resolve2) => {
|
|
161
|
+
proc.on("close", (code) => resolve2(code === 0 ? out.trim() : null));
|
|
162
|
+
proc.on("error", () => resolve2(null));
|
|
163
|
+
});
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function ghFetch(url) {
|
|
169
|
+
const token = await getGhToken();
|
|
170
|
+
const headers = {
|
|
171
|
+
Accept: "application/vnd.github+json",
|
|
172
|
+
"User-Agent": "codex-web-local"
|
|
173
|
+
};
|
|
174
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
175
|
+
return fetch(url, { headers });
|
|
176
|
+
}
|
|
177
|
+
async function fetchSkillsTree() {
|
|
178
|
+
if (skillsTreeCache && Date.now() - skillsTreeCache.fetchedAt < TREE_CACHE_TTL_MS) {
|
|
179
|
+
return skillsTreeCache.entries;
|
|
180
|
+
}
|
|
181
|
+
const resp = await ghFetch(`https://api.github.com/repos/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/git/trees/main?recursive=1`);
|
|
182
|
+
if (!resp.ok) throw new Error(`GitHub tree API returned ${resp.status}`);
|
|
183
|
+
const data = await resp.json();
|
|
184
|
+
const metaPattern = /^skills\/([^/]+)\/([^/]+)\/_meta\.json$/;
|
|
185
|
+
const seen = /* @__PURE__ */ new Set();
|
|
186
|
+
const entries = [];
|
|
187
|
+
for (const node of data.tree ?? []) {
|
|
188
|
+
const match = metaPattern.exec(node.path);
|
|
189
|
+
if (!match) continue;
|
|
190
|
+
const [, owner, skillName] = match;
|
|
191
|
+
const key = `${owner}/${skillName}`;
|
|
192
|
+
if (seen.has(key)) continue;
|
|
193
|
+
seen.add(key);
|
|
194
|
+
entries.push({
|
|
195
|
+
name: skillName,
|
|
196
|
+
owner,
|
|
197
|
+
url: `https://github.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/tree/main/skills/${owner}/${skillName}`
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
skillsTreeCache = { entries, fetchedAt: Date.now() };
|
|
201
|
+
return entries;
|
|
202
|
+
}
|
|
203
|
+
async function fetchMetaBatch(entries) {
|
|
204
|
+
const toFetch = entries.filter((e) => !metaCache.has(`${e.owner}/${e.name}`));
|
|
205
|
+
if (toFetch.length === 0) return;
|
|
206
|
+
const batch = toFetch.slice(0, 50);
|
|
207
|
+
await Promise.allSettled(
|
|
208
|
+
batch.map(async (e) => {
|
|
209
|
+
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${e.owner}/${e.name}/_meta.json`;
|
|
210
|
+
const resp = await fetch(rawUrl);
|
|
211
|
+
if (!resp.ok) return;
|
|
212
|
+
const meta = await resp.json();
|
|
213
|
+
metaCache.set(`${e.owner}/${e.name}`, {
|
|
214
|
+
displayName: typeof meta.displayName === "string" ? meta.displayName : "",
|
|
215
|
+
description: typeof meta.displayName === "string" ? meta.displayName : "",
|
|
216
|
+
publishedAt: meta.latest?.publishedAt ?? 0
|
|
217
|
+
});
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
function buildHubEntry(e) {
|
|
222
|
+
const cached = metaCache.get(`${e.owner}/${e.name}`);
|
|
223
|
+
return {
|
|
224
|
+
name: e.name,
|
|
225
|
+
owner: e.owner,
|
|
226
|
+
description: cached?.description ?? "",
|
|
227
|
+
displayName: cached?.displayName ?? "",
|
|
228
|
+
publishedAt: cached?.publishedAt ?? 0,
|
|
229
|
+
avatarUrl: `https://github.com/${e.owner}.png?size=40`,
|
|
230
|
+
url: e.url,
|
|
231
|
+
installed: false
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
var GITHUB_DEVICE_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
235
|
+
var DEFAULT_SKILLS_SYNC_REPO_NAME = "codexskills";
|
|
236
|
+
var SKILLS_SYNC_MANIFEST_PATH = "installed-skills.json";
|
|
237
|
+
var SYNC_UPSTREAM_SKILLS_OWNER = "OpenClawAndroid";
|
|
238
|
+
var SYNC_UPSTREAM_SKILLS_REPO = "skills";
|
|
239
|
+
var HUB_SKILLS_OWNER = "openclaw";
|
|
240
|
+
var HUB_SKILLS_REPO = "skills";
|
|
241
|
+
var startupSkillsSyncInitialized = false;
|
|
242
|
+
var startupSyncStatus = {
|
|
243
|
+
inProgress: false,
|
|
244
|
+
mode: "idle",
|
|
245
|
+
branch: getPreferredSyncBranch(),
|
|
246
|
+
lastAction: "not-started",
|
|
247
|
+
lastRunAtIso: "",
|
|
248
|
+
lastSuccessAtIso: "",
|
|
249
|
+
lastError: ""
|
|
250
|
+
};
|
|
251
|
+
async function scanInstalledSkillsFromDisk() {
|
|
252
|
+
const map = /* @__PURE__ */ new Map();
|
|
253
|
+
const skillsDir = getSkillsInstallDir();
|
|
254
|
+
try {
|
|
255
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
258
|
+
const skillMd = join(skillsDir, entry.name, "SKILL.md");
|
|
259
|
+
try {
|
|
260
|
+
await stat(skillMd);
|
|
261
|
+
map.set(entry.name, { name: entry.name, path: skillMd, enabled: true });
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
return map;
|
|
268
|
+
}
|
|
269
|
+
function getSkillsSyncStatePath() {
|
|
270
|
+
return join(getCodexHomeDir(), "skills-sync.json");
|
|
271
|
+
}
|
|
272
|
+
async function readSkillsSyncState() {
|
|
273
|
+
try {
|
|
274
|
+
const raw = await readFile(getSkillsSyncStatePath(), "utf8");
|
|
275
|
+
const parsed = JSON.parse(raw);
|
|
276
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
277
|
+
} catch {
|
|
278
|
+
return {};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function writeSkillsSyncState(state) {
|
|
282
|
+
await writeFile(getSkillsSyncStatePath(), JSON.stringify(state), "utf8");
|
|
283
|
+
}
|
|
284
|
+
async function getGithubJson(url, token, method = "GET", body) {
|
|
285
|
+
const resp = await fetch(url, {
|
|
286
|
+
method,
|
|
287
|
+
headers: {
|
|
288
|
+
Accept: "application/vnd.github+json",
|
|
289
|
+
"Content-Type": "application/json",
|
|
290
|
+
Authorization: `Bearer ${token}`,
|
|
291
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
292
|
+
"User-Agent": "codex-web-local"
|
|
293
|
+
},
|
|
294
|
+
body: body ? JSON.stringify(body) : void 0
|
|
295
|
+
});
|
|
296
|
+
if (!resp.ok) {
|
|
297
|
+
const text = await resp.text();
|
|
298
|
+
throw new Error(`GitHub API ${method} ${url} failed (${resp.status}): ${text}`);
|
|
299
|
+
}
|
|
300
|
+
return await resp.json();
|
|
301
|
+
}
|
|
302
|
+
async function startGithubDeviceLogin() {
|
|
303
|
+
const resp = await fetch("https://github.com/login/device/code", {
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers: {
|
|
306
|
+
Accept: "application/json",
|
|
307
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
308
|
+
"User-Agent": "codex-web-local"
|
|
309
|
+
},
|
|
310
|
+
body: new URLSearchParams({
|
|
311
|
+
client_id: GITHUB_DEVICE_CLIENT_ID,
|
|
312
|
+
scope: "repo read:user"
|
|
313
|
+
})
|
|
314
|
+
});
|
|
315
|
+
if (!resp.ok) {
|
|
316
|
+
throw new Error(`GitHub device flow init failed (${resp.status})`);
|
|
317
|
+
}
|
|
318
|
+
return await resp.json();
|
|
319
|
+
}
|
|
320
|
+
async function completeGithubDeviceLogin(deviceCode) {
|
|
321
|
+
const resp = await fetch("https://github.com/login/oauth/access_token", {
|
|
322
|
+
method: "POST",
|
|
323
|
+
headers: {
|
|
324
|
+
Accept: "application/json",
|
|
325
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
326
|
+
"User-Agent": "codex-web-local"
|
|
327
|
+
},
|
|
328
|
+
body: new URLSearchParams({
|
|
329
|
+
client_id: GITHUB_DEVICE_CLIENT_ID,
|
|
330
|
+
device_code: deviceCode,
|
|
331
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
332
|
+
})
|
|
333
|
+
});
|
|
334
|
+
if (!resp.ok) {
|
|
335
|
+
throw new Error(`GitHub token exchange failed (${resp.status})`);
|
|
336
|
+
}
|
|
337
|
+
const payload = await resp.json();
|
|
338
|
+
if (!payload.access_token) return { token: null, error: payload.error || "unknown_error" };
|
|
339
|
+
return { token: payload.access_token, error: null };
|
|
340
|
+
}
|
|
341
|
+
function isAndroidLikeRuntime() {
|
|
342
|
+
if (process.platform === "android") return true;
|
|
343
|
+
if (existsSync("/data/data/com.termux")) return true;
|
|
344
|
+
if (process.env.TERMUX_VERSION) return true;
|
|
345
|
+
const prefix = process.env.PREFIX?.toLowerCase() ?? "";
|
|
346
|
+
if (prefix.includes("/com.termux/")) return true;
|
|
347
|
+
const proot = process.env.PROOT_TMP_DIR?.toLowerCase() ?? "";
|
|
348
|
+
return proot.length > 0;
|
|
349
|
+
}
|
|
350
|
+
function getPreferredSyncBranch() {
|
|
351
|
+
return isAndroidLikeRuntime() ? "android" : "main";
|
|
352
|
+
}
|
|
353
|
+
function isUpstreamSkillsRepo(repoOwner, repoName) {
|
|
354
|
+
return repoOwner.toLowerCase() === SYNC_UPSTREAM_SKILLS_OWNER.toLowerCase() && repoName.toLowerCase() === SYNC_UPSTREAM_SKILLS_REPO.toLowerCase();
|
|
355
|
+
}
|
|
356
|
+
async function resolveGithubUsername(token) {
|
|
357
|
+
const user = await getGithubJson("https://api.github.com/user", token);
|
|
358
|
+
return user.login;
|
|
359
|
+
}
|
|
360
|
+
async function ensurePrivateForkFromUpstream(token, username, repoName) {
|
|
361
|
+
const repoUrl = `https://api.github.com/repos/${username}/${repoName}`;
|
|
362
|
+
let created = false;
|
|
363
|
+
const existing = await fetch(repoUrl, {
|
|
364
|
+
headers: {
|
|
365
|
+
Accept: "application/vnd.github+json",
|
|
366
|
+
Authorization: `Bearer ${token}`,
|
|
367
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
368
|
+
"User-Agent": "codex-web-local"
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
if (existing.ok) {
|
|
372
|
+
const details = await existing.json();
|
|
373
|
+
if (details.private === true) return;
|
|
374
|
+
await getGithubJson(repoUrl, token, "PATCH", { private: true });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (existing.status !== 404) {
|
|
378
|
+
throw new Error(`Failed to check personal repo existence (${existing.status})`);
|
|
379
|
+
}
|
|
380
|
+
await getGithubJson(
|
|
381
|
+
"https://api.github.com/user/repos",
|
|
382
|
+
token,
|
|
383
|
+
"POST",
|
|
384
|
+
{ name: repoName, private: true, auto_init: false, description: "Codex skills private mirror sync" }
|
|
385
|
+
);
|
|
386
|
+
created = true;
|
|
387
|
+
let ready = false;
|
|
388
|
+
for (let i = 0; i < 20; i++) {
|
|
389
|
+
const check = await fetch(repoUrl, {
|
|
390
|
+
headers: {
|
|
391
|
+
Accept: "application/vnd.github+json",
|
|
392
|
+
Authorization: `Bearer ${token}`,
|
|
393
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
394
|
+
"User-Agent": "codex-web-local"
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
if (check.ok) {
|
|
398
|
+
ready = true;
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
402
|
+
}
|
|
403
|
+
if (!ready) throw new Error("Private mirror repo was created but is not available yet");
|
|
404
|
+
if (!created) return;
|
|
405
|
+
const tmp = await mkdtemp(join(tmpdir(), "codex-skills-seed-"));
|
|
406
|
+
try {
|
|
407
|
+
const upstreamUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
|
|
408
|
+
const branch = getPreferredSyncBranch();
|
|
409
|
+
try {
|
|
410
|
+
await runCommand("git", ["clone", "--depth", "1", "--single-branch", "--branch", branch, upstreamUrl, tmp]);
|
|
411
|
+
} catch {
|
|
412
|
+
await runCommand("git", ["clone", "--depth", "1", upstreamUrl, tmp]);
|
|
413
|
+
}
|
|
414
|
+
const privateRemote = toGitHubTokenRemote(username, repoName, token);
|
|
415
|
+
await runCommand("git", ["remote", "set-url", "origin", privateRemote], { cwd: tmp });
|
|
416
|
+
try {
|
|
417
|
+
await runCommand("git", ["checkout", "-B", branch], { cwd: tmp });
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
await runCommand("git", ["push", "-u", "origin", `HEAD:${branch}`], { cwd: tmp });
|
|
421
|
+
} finally {
|
|
422
|
+
await rm(tmp, { recursive: true, force: true });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async function readRemoteSkillsManifest(token, repoOwner, repoName) {
|
|
426
|
+
const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
|
|
427
|
+
const resp = await fetch(url, {
|
|
428
|
+
headers: {
|
|
429
|
+
Accept: "application/vnd.github+json",
|
|
430
|
+
Authorization: `Bearer ${token}`,
|
|
431
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
432
|
+
"User-Agent": "codex-web-local"
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
if (resp.status === 404) return [];
|
|
436
|
+
if (!resp.ok) throw new Error(`Failed to read remote manifest (${resp.status})`);
|
|
437
|
+
const payload = await resp.json();
|
|
438
|
+
const content = payload.content ? Buffer.from(payload.content.replace(/\n/g, ""), "base64").toString("utf8") : "[]";
|
|
439
|
+
const parsed = JSON.parse(content);
|
|
440
|
+
if (!Array.isArray(parsed)) return [];
|
|
441
|
+
const skills = [];
|
|
442
|
+
for (const row of parsed) {
|
|
443
|
+
const item = asRecord(row);
|
|
444
|
+
const owner = typeof item?.owner === "string" ? item.owner : "";
|
|
445
|
+
const name = typeof item?.name === "string" ? item.name : "";
|
|
446
|
+
if (!name) continue;
|
|
447
|
+
skills.push({ ...owner ? { owner } : {}, name, enabled: item?.enabled !== false });
|
|
448
|
+
}
|
|
449
|
+
return skills;
|
|
450
|
+
}
|
|
451
|
+
async function writeRemoteSkillsManifest(token, repoOwner, repoName, skills) {
|
|
452
|
+
const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
|
|
453
|
+
let sha = "";
|
|
454
|
+
const existing = await fetch(url, {
|
|
455
|
+
headers: {
|
|
456
|
+
Accept: "application/vnd.github+json",
|
|
457
|
+
Authorization: `Bearer ${token}`,
|
|
458
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
459
|
+
"User-Agent": "codex-web-local"
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
if (existing.ok) {
|
|
463
|
+
const payload = await existing.json();
|
|
464
|
+
sha = payload.sha ?? "";
|
|
465
|
+
}
|
|
466
|
+
const content = Buffer.from(JSON.stringify(skills, null, 2), "utf8").toString("base64");
|
|
467
|
+
await getGithubJson(url, token, "PUT", {
|
|
468
|
+
message: "Update synced skills manifest",
|
|
469
|
+
content,
|
|
470
|
+
...sha ? { sha } : {}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function toGitHubTokenRemote(repoOwner, repoName, token) {
|
|
474
|
+
return `https://x-access-token:${encodeURIComponent(token)}@github.com/${repoOwner}/${repoName}.git`;
|
|
475
|
+
}
|
|
476
|
+
async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
|
|
477
|
+
const localDir = getSkillsInstallDir();
|
|
478
|
+
await mkdir(localDir, { recursive: true });
|
|
479
|
+
const gitDir = join(localDir, ".git");
|
|
480
|
+
let hasGitDir = false;
|
|
481
|
+
try {
|
|
482
|
+
hasGitDir = (await stat(gitDir)).isDirectory();
|
|
483
|
+
} catch {
|
|
484
|
+
hasGitDir = false;
|
|
485
|
+
}
|
|
486
|
+
if (!hasGitDir) {
|
|
487
|
+
await runCommand("git", ["init"], { cwd: localDir });
|
|
488
|
+
await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: localDir });
|
|
489
|
+
await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: localDir });
|
|
490
|
+
await runCommand("git", ["add", "-A"], { cwd: localDir });
|
|
491
|
+
try {
|
|
492
|
+
await runCommand("git", ["commit", "-m", "Local skills snapshot before sync"], { cwd: localDir });
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
await runCommand("git", ["branch", "-M", branch], { cwd: localDir });
|
|
496
|
+
try {
|
|
497
|
+
await runCommand("git", ["remote", "add", "origin", repoUrl], { cwd: localDir });
|
|
498
|
+
} catch {
|
|
499
|
+
await runCommand("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
|
|
500
|
+
}
|
|
501
|
+
await runCommand("git", ["fetch", "origin"], { cwd: localDir });
|
|
502
|
+
try {
|
|
503
|
+
await runCommand("git", ["merge", "--allow-unrelated-histories", "--no-edit", `origin/${branch}`], { cwd: localDir });
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
return localDir;
|
|
507
|
+
}
|
|
508
|
+
await runCommand("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
|
|
509
|
+
await runCommand("git", ["fetch", "origin"], { cwd: localDir });
|
|
510
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
511
|
+
try {
|
|
512
|
+
await runCommand("git", ["checkout", branch], { cwd: localDir });
|
|
513
|
+
} catch {
|
|
514
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
515
|
+
await runCommand("git", ["checkout", "-B", branch], { cwd: localDir });
|
|
516
|
+
}
|
|
517
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
518
|
+
const localMtimesBeforePull = await snapshotFileMtimes(localDir);
|
|
519
|
+
try {
|
|
520
|
+
await runCommand("git", ["stash", "push", "--include-untracked", "-m", "codex-skills-autostash"], { cwd: localDir });
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
523
|
+
let pulledMtimes = /* @__PURE__ */ new Map();
|
|
524
|
+
try {
|
|
525
|
+
await runCommand("git", ["pull", "--no-rebase", "origin", branch], { cwd: localDir });
|
|
526
|
+
pulledMtimes = await snapshotFileMtimes(localDir);
|
|
527
|
+
} catch {
|
|
528
|
+
await resolveMergeConflictsByNewerCommit(localDir, branch);
|
|
529
|
+
pulledMtimes = await snapshotFileMtimes(localDir);
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
await runCommand("git", ["stash", "pop"], { cwd: localDir });
|
|
533
|
+
} catch {
|
|
534
|
+
await resolveStashPopConflictsByFileTime(localDir, localMtimesBeforePull, pulledMtimes);
|
|
535
|
+
}
|
|
536
|
+
return localDir;
|
|
537
|
+
}
|
|
538
|
+
async function resolveMergeConflictsByNewerCommit(repoDir, branch) {
|
|
539
|
+
const unmerged = (await runCommandWithOutput("git", ["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir })).split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
|
|
540
|
+
if (unmerged.length === 0) return;
|
|
541
|
+
for (const path of unmerged) {
|
|
542
|
+
const oursTime = await getCommitTime(repoDir, "HEAD", path);
|
|
543
|
+
const theirsTime = await getCommitTime(repoDir, `origin/${branch}`, path);
|
|
544
|
+
if (theirsTime > oursTime) {
|
|
545
|
+
await runCommand("git", ["checkout", "--theirs", "--", path], { cwd: repoDir });
|
|
546
|
+
} else {
|
|
547
|
+
await runCommand("git", ["checkout", "--ours", "--", path], { cwd: repoDir });
|
|
548
|
+
}
|
|
549
|
+
await runCommand("git", ["add", "--", path], { cwd: repoDir });
|
|
550
|
+
}
|
|
551
|
+
const mergeHead = (await runCommandWithOutput("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], { cwd: repoDir })).trim();
|
|
552
|
+
if (mergeHead) {
|
|
553
|
+
await runCommand("git", ["commit", "-m", "Auto-resolve skills merge by newer file"], { cwd: repoDir });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async function getCommitTime(repoDir, ref, path) {
|
|
557
|
+
try {
|
|
558
|
+
const output = (await runCommandWithOutput("git", ["log", "-1", "--format=%ct", ref, "--", path], { cwd: repoDir })).trim();
|
|
559
|
+
return output ? Number.parseInt(output, 10) : 0;
|
|
560
|
+
} catch {
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async function resolveStashPopConflictsByFileTime(repoDir, localMtimesBeforePull, pulledMtimes) {
|
|
565
|
+
const unmerged = (await runCommandWithOutput("git", ["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir })).split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
|
|
566
|
+
if (unmerged.length === 0) return;
|
|
567
|
+
for (const path of unmerged) {
|
|
568
|
+
const localMtime = localMtimesBeforePull.get(path) ?? 0;
|
|
569
|
+
const pulledMtime = pulledMtimes.get(path) ?? 0;
|
|
570
|
+
const side = localMtime >= pulledMtime ? "--theirs" : "--ours";
|
|
571
|
+
await runCommand("git", ["checkout", side, "--", path], { cwd: repoDir });
|
|
572
|
+
await runCommand("git", ["add", "--", path], { cwd: repoDir });
|
|
573
|
+
}
|
|
574
|
+
const mergeHead = (await runCommandWithOutput("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], { cwd: repoDir })).trim();
|
|
575
|
+
if (mergeHead) {
|
|
576
|
+
await runCommand("git", ["commit", "-m", "Auto-resolve stash-pop conflicts by file time"], { cwd: repoDir });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async function snapshotFileMtimes(dir) {
|
|
580
|
+
const mtimes = /* @__PURE__ */ new Map();
|
|
581
|
+
await walkFileMtimes(dir, dir, mtimes);
|
|
582
|
+
return mtimes;
|
|
583
|
+
}
|
|
584
|
+
async function walkFileMtimes(rootDir, currentDir, out) {
|
|
585
|
+
let entries;
|
|
586
|
+
try {
|
|
587
|
+
entries = await readdir(currentDir, { withFileTypes: true });
|
|
588
|
+
} catch {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
for (const entry of entries) {
|
|
592
|
+
const entryName = String(entry.name);
|
|
593
|
+
if (entryName === ".git") continue;
|
|
594
|
+
const absolutePath = join(currentDir, entryName);
|
|
595
|
+
const relativePath = absolutePath.slice(rootDir.length + 1);
|
|
596
|
+
if (entry.isDirectory()) {
|
|
597
|
+
await walkFileMtimes(rootDir, absolutePath, out);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (!entry.isFile()) continue;
|
|
601
|
+
try {
|
|
602
|
+
const info = await stat(absolutePath);
|
|
603
|
+
out.set(relativePath, info.mtimeMs);
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _installedMap) {
|
|
609
|
+
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
610
|
+
const branch = getPreferredSyncBranch();
|
|
611
|
+
const repoDir = await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
612
|
+
void _installedMap;
|
|
613
|
+
await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: repoDir });
|
|
614
|
+
await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: repoDir });
|
|
615
|
+
await runCommand("git", ["add", "."], { cwd: repoDir });
|
|
616
|
+
const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
|
|
617
|
+
if (!status) return;
|
|
618
|
+
await runCommand("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
|
|
619
|
+
await runCommand("git", ["push", "origin", `HEAD:${branch}`], { cwd: repoDir });
|
|
620
|
+
}
|
|
621
|
+
async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName) {
|
|
622
|
+
const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
|
|
623
|
+
const branch = getPreferredSyncBranch();
|
|
624
|
+
await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
|
|
625
|
+
}
|
|
626
|
+
async function bootstrapSkillsFromUpstreamIntoLocal() {
|
|
627
|
+
const repoUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
|
|
628
|
+
const branch = getPreferredSyncBranch();
|
|
629
|
+
await ensureSkillsWorkingTreeRepo(repoUrl, branch);
|
|
630
|
+
}
|
|
631
|
+
async function collectLocalSyncedSkills(appServer) {
|
|
632
|
+
const state = await readSkillsSyncState();
|
|
633
|
+
const owners = { ...state.installedOwners ?? {} };
|
|
634
|
+
const tree = await fetchSkillsTree();
|
|
635
|
+
const uniqueOwnerByName = /* @__PURE__ */ new Map();
|
|
636
|
+
const ambiguousNames = /* @__PURE__ */ new Set();
|
|
637
|
+
for (const entry of tree) {
|
|
638
|
+
if (ambiguousNames.has(entry.name)) continue;
|
|
639
|
+
const existingOwner = uniqueOwnerByName.get(entry.name);
|
|
640
|
+
if (!existingOwner) {
|
|
641
|
+
uniqueOwnerByName.set(entry.name, entry.owner);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (existingOwner !== entry.owner) {
|
|
645
|
+
uniqueOwnerByName.delete(entry.name);
|
|
646
|
+
ambiguousNames.add(entry.name);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const skills = await appServer.rpc("skills/list", {});
|
|
650
|
+
const seen = /* @__PURE__ */ new Set();
|
|
651
|
+
const synced = [];
|
|
652
|
+
let ownersChanged = false;
|
|
653
|
+
for (const entry of skills.data ?? []) {
|
|
654
|
+
for (const skill of entry.skills ?? []) {
|
|
655
|
+
const name = typeof skill.name === "string" ? skill.name : "";
|
|
656
|
+
if (!name || seen.has(name)) continue;
|
|
657
|
+
seen.add(name);
|
|
658
|
+
let owner = owners[name];
|
|
659
|
+
if (!owner) {
|
|
660
|
+
owner = uniqueOwnerByName.get(name) ?? "";
|
|
661
|
+
if (owner) {
|
|
662
|
+
owners[name] = owner;
|
|
663
|
+
ownersChanged = true;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
synced.push({ ...owner ? { owner } : {}, name, enabled: skill.enabled !== false });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (ownersChanged) {
|
|
670
|
+
await writeSkillsSyncState({ ...state, installedOwners: owners });
|
|
671
|
+
}
|
|
672
|
+
synced.sort((a, b) => `${a.owner ?? ""}/${a.name}`.localeCompare(`${b.owner ?? ""}/${b.name}`));
|
|
673
|
+
return synced;
|
|
674
|
+
}
|
|
675
|
+
async function autoPushSyncedSkills(appServer) {
|
|
676
|
+
const state = await readSkillsSyncState();
|
|
677
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) return;
|
|
678
|
+
if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
|
|
679
|
+
throw new Error("Refusing to push to upstream skills repository");
|
|
680
|
+
}
|
|
681
|
+
const local = await collectLocalSyncedSkills(appServer);
|
|
682
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
683
|
+
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
684
|
+
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
685
|
+
}
|
|
686
|
+
async function ensureCodexAgentsSymlinkToSkillsAgents() {
|
|
687
|
+
const codexHomeDir = getCodexHomeDir();
|
|
688
|
+
const skillsAgentsPath = join(codexHomeDir, "skills", "AGENTS.md");
|
|
689
|
+
const codexAgentsPath = join(codexHomeDir, "AGENTS.md");
|
|
690
|
+
await mkdir(join(codexHomeDir, "skills"), { recursive: true });
|
|
691
|
+
let copiedFromCodex = false;
|
|
692
|
+
try {
|
|
693
|
+
const codexAgentsStat = await lstat(codexAgentsPath);
|
|
694
|
+
if (codexAgentsStat.isFile() || codexAgentsStat.isSymbolicLink()) {
|
|
695
|
+
const content = await readFile(codexAgentsPath, "utf8");
|
|
696
|
+
await writeFile(skillsAgentsPath, content, "utf8");
|
|
697
|
+
copiedFromCodex = true;
|
|
698
|
+
} else {
|
|
699
|
+
await rm(codexAgentsPath, { force: true, recursive: true });
|
|
700
|
+
}
|
|
701
|
+
} catch {
|
|
702
|
+
}
|
|
703
|
+
if (!copiedFromCodex) {
|
|
704
|
+
try {
|
|
705
|
+
const skillsAgentsStat = await stat(skillsAgentsPath);
|
|
706
|
+
if (!skillsAgentsStat.isFile()) {
|
|
707
|
+
await rm(skillsAgentsPath, { force: true, recursive: true });
|
|
708
|
+
await writeFile(skillsAgentsPath, "", "utf8");
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
await writeFile(skillsAgentsPath, "", "utf8");
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const relativeTarget = join("skills", "AGENTS.md");
|
|
715
|
+
try {
|
|
716
|
+
const current = await lstat(codexAgentsPath);
|
|
717
|
+
if (current.isSymbolicLink()) {
|
|
718
|
+
const existingTarget = await readlink(codexAgentsPath);
|
|
719
|
+
if (existingTarget === relativeTarget) return;
|
|
720
|
+
}
|
|
721
|
+
await rm(codexAgentsPath, { force: true, recursive: true });
|
|
722
|
+
} catch {
|
|
723
|
+
}
|
|
724
|
+
await symlink(relativeTarget, codexAgentsPath);
|
|
725
|
+
}
|
|
726
|
+
async function initializeSkillsSyncOnStartup(appServer) {
|
|
727
|
+
if (startupSkillsSyncInitialized) return;
|
|
728
|
+
startupSkillsSyncInitialized = true;
|
|
729
|
+
startupSyncStatus.inProgress = true;
|
|
730
|
+
startupSyncStatus.lastRunAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
731
|
+
startupSyncStatus.lastError = "";
|
|
732
|
+
startupSyncStatus.branch = getPreferredSyncBranch();
|
|
733
|
+
try {
|
|
734
|
+
const state = await readSkillsSyncState();
|
|
735
|
+
if (!state.githubToken) {
|
|
736
|
+
await ensureCodexAgentsSymlinkToSkillsAgents();
|
|
737
|
+
if (!isAndroidLikeRuntime()) {
|
|
738
|
+
startupSyncStatus.mode = "idle";
|
|
739
|
+
startupSyncStatus.lastAction = "skip-upstream-non-android";
|
|
740
|
+
startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
startupSyncStatus.mode = "unauthenticated-bootstrap";
|
|
744
|
+
startupSyncStatus.lastAction = "pull-upstream";
|
|
745
|
+
await bootstrapSkillsFromUpstreamIntoLocal();
|
|
746
|
+
try {
|
|
747
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
748
|
+
} catch {
|
|
749
|
+
}
|
|
750
|
+
startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
751
|
+
startupSyncStatus.lastAction = "pull-upstream-complete";
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
startupSyncStatus.mode = "authenticated-fork-sync";
|
|
755
|
+
startupSyncStatus.lastAction = "ensure-private-fork";
|
|
756
|
+
const username = state.githubUsername || await resolveGithubUsername(state.githubToken);
|
|
757
|
+
const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
|
|
758
|
+
await ensurePrivateForkFromUpstream(state.githubToken, username, repoName);
|
|
759
|
+
await writeSkillsSyncState({ ...state, githubUsername: username, repoOwner: username, repoName });
|
|
760
|
+
startupSyncStatus.lastAction = "pull-private-fork";
|
|
761
|
+
await pullInstalledSkillsFolderFromRepo(state.githubToken, username, repoName);
|
|
762
|
+
try {
|
|
763
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
764
|
+
} catch {
|
|
765
|
+
}
|
|
766
|
+
startupSyncStatus.lastAction = "push-private-fork";
|
|
767
|
+
await autoPushSyncedSkills(appServer);
|
|
768
|
+
startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
769
|
+
startupSyncStatus.lastAction = "startup-sync-complete";
|
|
770
|
+
} catch (error) {
|
|
771
|
+
startupSyncStatus.lastError = getErrorMessage(error, "startup-sync-failed");
|
|
772
|
+
startupSyncStatus.lastAction = "startup-sync-failed";
|
|
773
|
+
} finally {
|
|
774
|
+
startupSyncStatus.inProgress = false;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async function finalizeGithubLoginAndSync(token, username, appServer) {
|
|
778
|
+
const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
|
|
779
|
+
await ensurePrivateForkFromUpstream(token, username, repoName);
|
|
780
|
+
const current = await readSkillsSyncState();
|
|
781
|
+
await writeSkillsSyncState({ ...current, githubToken: token, githubUsername: username, repoOwner: username, repoName });
|
|
782
|
+
await pullInstalledSkillsFolderFromRepo(token, username, repoName);
|
|
783
|
+
try {
|
|
784
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
785
|
+
} catch {
|
|
786
|
+
}
|
|
787
|
+
await autoPushSyncedSkills(appServer);
|
|
788
|
+
}
|
|
789
|
+
async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
|
|
790
|
+
const q = query.toLowerCase().trim();
|
|
791
|
+
const filtered = q ? allEntries.filter((s) => {
|
|
792
|
+
if (s.name.toLowerCase().includes(q) || s.owner.toLowerCase().includes(q)) return true;
|
|
793
|
+
const cached = metaCache.get(`${s.owner}/${s.name}`);
|
|
794
|
+
return Boolean(cached?.displayName?.toLowerCase().includes(q));
|
|
795
|
+
}) : allEntries;
|
|
796
|
+
const page = filtered.slice(0, Math.min(limit * 2, 200));
|
|
797
|
+
await fetchMetaBatch(page);
|
|
798
|
+
let results = page.map(buildHubEntry);
|
|
799
|
+
if (sort === "date") {
|
|
800
|
+
results.sort((a, b) => b.publishedAt - a.publishedAt);
|
|
801
|
+
} else if (q) {
|
|
802
|
+
results.sort((a, b) => {
|
|
803
|
+
const aExact = a.name.toLowerCase() === q ? 1 : 0;
|
|
804
|
+
const bExact = b.name.toLowerCase() === q ? 1 : 0;
|
|
805
|
+
if (aExact !== bExact) return bExact - aExact;
|
|
806
|
+
return b.publishedAt - a.publishedAt;
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
return results.slice(0, limit).map((s) => {
|
|
810
|
+
const local = installedMap.get(s.name);
|
|
811
|
+
return local ? { ...s, installed: true, path: local.path, enabled: local.enabled } : s;
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
async function handleSkillsRoutes(req, res, url, context) {
|
|
815
|
+
const { appServer, readJsonBody: readJsonBody2 } = context;
|
|
816
|
+
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
|
|
817
|
+
try {
|
|
818
|
+
const q = url.searchParams.get("q") || "";
|
|
819
|
+
const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
|
|
820
|
+
const sort = url.searchParams.get("sort") || "date";
|
|
821
|
+
const allEntries = await fetchSkillsTree();
|
|
822
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
823
|
+
try {
|
|
824
|
+
const result = await appServer.rpc("skills/list", {});
|
|
825
|
+
for (const entry of result.data ?? []) {
|
|
826
|
+
for (const skill of entry.skills ?? []) {
|
|
827
|
+
if (skill.name) {
|
|
828
|
+
installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
} catch {
|
|
833
|
+
}
|
|
834
|
+
const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
|
|
835
|
+
await fetchMetaBatch(installedHubEntries);
|
|
836
|
+
const installed = [];
|
|
837
|
+
for (const [, info] of installedMap) {
|
|
838
|
+
const hubEntry = allEntries.find((e) => e.name === info.name);
|
|
839
|
+
const base = hubEntry ? buildHubEntry(hubEntry) : {
|
|
840
|
+
name: info.name,
|
|
841
|
+
owner: "local",
|
|
842
|
+
description: "",
|
|
843
|
+
displayName: "",
|
|
844
|
+
publishedAt: 0,
|
|
845
|
+
avatarUrl: "",
|
|
846
|
+
url: "",
|
|
847
|
+
installed: false
|
|
848
|
+
};
|
|
849
|
+
installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
|
|
850
|
+
}
|
|
851
|
+
const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
|
|
852
|
+
setJson(res, 200, { data: results, installed, total: allEntries.length });
|
|
853
|
+
} catch (error) {
|
|
854
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch skills hub") });
|
|
855
|
+
}
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
|
|
859
|
+
const state = await readSkillsSyncState();
|
|
860
|
+
setJson(res, 200, {
|
|
861
|
+
data: {
|
|
862
|
+
loggedIn: Boolean(state.githubToken),
|
|
863
|
+
githubUsername: state.githubUsername ?? "",
|
|
864
|
+
repoOwner: state.repoOwner ?? "",
|
|
865
|
+
repoName: state.repoName ?? "",
|
|
866
|
+
configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
|
|
867
|
+
startup: {
|
|
868
|
+
inProgress: startupSyncStatus.inProgress,
|
|
869
|
+
mode: startupSyncStatus.mode,
|
|
870
|
+
branch: startupSyncStatus.branch,
|
|
871
|
+
lastAction: startupSyncStatus.lastAction,
|
|
872
|
+
lastRunAtIso: startupSyncStatus.lastRunAtIso,
|
|
873
|
+
lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
|
|
874
|
+
lastError: startupSyncStatus.lastError
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
880
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
|
|
881
|
+
try {
|
|
882
|
+
const started = await startGithubDeviceLogin();
|
|
883
|
+
setJson(res, 200, { data: started });
|
|
884
|
+
} catch (error) {
|
|
885
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to start GitHub login") });
|
|
886
|
+
}
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
889
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
|
|
890
|
+
try {
|
|
891
|
+
const payload = asRecord(await readJsonBody2(req));
|
|
892
|
+
const token = typeof payload?.token === "string" ? payload.token.trim() : "";
|
|
893
|
+
if (!token) {
|
|
894
|
+
setJson(res, 400, { error: "Missing GitHub token" });
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
const username = await resolveGithubUsername(token);
|
|
898
|
+
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
899
|
+
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
900
|
+
} catch (error) {
|
|
901
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to login with GitHub token") });
|
|
902
|
+
}
|
|
903
|
+
return true;
|
|
904
|
+
}
|
|
905
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
|
|
906
|
+
try {
|
|
907
|
+
const state = await readSkillsSyncState();
|
|
908
|
+
await writeSkillsSyncState({
|
|
909
|
+
...state,
|
|
910
|
+
githubToken: void 0,
|
|
911
|
+
githubUsername: void 0,
|
|
912
|
+
repoOwner: void 0,
|
|
913
|
+
repoName: void 0
|
|
914
|
+
});
|
|
915
|
+
setJson(res, 200, { ok: true });
|
|
916
|
+
} catch (error) {
|
|
917
|
+
setJson(res, 500, { error: getErrorMessage(error, "Failed to logout GitHub") });
|
|
918
|
+
}
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
|
|
922
|
+
try {
|
|
923
|
+
const payload = asRecord(await readJsonBody2(req));
|
|
924
|
+
const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
|
|
925
|
+
if (!deviceCode) {
|
|
926
|
+
setJson(res, 400, { error: "Missing deviceCode" });
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
const result = await completeGithubDeviceLogin(deviceCode);
|
|
930
|
+
if (!result.token) {
|
|
931
|
+
setJson(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
|
|
932
|
+
return true;
|
|
933
|
+
}
|
|
934
|
+
const token = result.token;
|
|
935
|
+
const username = await resolveGithubUsername(token);
|
|
936
|
+
await finalizeGithubLoginAndSync(token, username, appServer);
|
|
937
|
+
setJson(res, 200, { ok: true, data: { githubUsername: username } });
|
|
938
|
+
} catch (error) {
|
|
939
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to complete GitHub login") });
|
|
940
|
+
}
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
|
|
944
|
+
try {
|
|
945
|
+
const state = await readSkillsSyncState();
|
|
946
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
947
|
+
setJson(res, 400, { error: "Skills sync is not configured yet" });
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
|
|
951
|
+
setJson(res, 400, { error: "Refusing to push to upstream repository" });
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
const local = await collectLocalSyncedSkills(appServer);
|
|
955
|
+
const installedMap = await scanInstalledSkillsFromDisk();
|
|
956
|
+
await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
|
|
957
|
+
await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
|
|
958
|
+
setJson(res, 200, { ok: true, data: { synced: local.length } });
|
|
959
|
+
} catch (error) {
|
|
960
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to push synced skills") });
|
|
961
|
+
}
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
|
|
965
|
+
try {
|
|
966
|
+
const state = await readSkillsSyncState();
|
|
967
|
+
if (!state.githubToken || !state.repoOwner || !state.repoName) {
|
|
968
|
+
await bootstrapSkillsFromUpstreamIntoLocal();
|
|
969
|
+
try {
|
|
970
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
971
|
+
} catch {
|
|
972
|
+
}
|
|
973
|
+
setJson(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
|
|
974
|
+
return true;
|
|
975
|
+
}
|
|
976
|
+
const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
|
|
977
|
+
const tree = await fetchSkillsTree();
|
|
978
|
+
const uniqueOwnerByName = /* @__PURE__ */ new Map();
|
|
979
|
+
const ambiguousNames = /* @__PURE__ */ new Set();
|
|
980
|
+
for (const entry of tree) {
|
|
981
|
+
if (ambiguousNames.has(entry.name)) continue;
|
|
982
|
+
const existingOwner = uniqueOwnerByName.get(entry.name);
|
|
983
|
+
if (!existingOwner) {
|
|
984
|
+
uniqueOwnerByName.set(entry.name, entry.owner);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (existingOwner !== entry.owner) {
|
|
988
|
+
uniqueOwnerByName.delete(entry.name);
|
|
989
|
+
ambiguousNames.add(entry.name);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
const localDir = await detectUserSkillsDir(appServer);
|
|
993
|
+
await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
|
|
994
|
+
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
995
|
+
const localSkills = await scanInstalledSkillsFromDisk();
|
|
996
|
+
for (const skill of remote) {
|
|
997
|
+
const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
|
|
998
|
+
if (!owner) continue;
|
|
999
|
+
if (!localSkills.has(skill.name)) {
|
|
1000
|
+
await runCommand("python3", [
|
|
1001
|
+
installerScript,
|
|
1002
|
+
"--repo",
|
|
1003
|
+
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
1004
|
+
"--path",
|
|
1005
|
+
`skills/${owner}/${skill.name}`,
|
|
1006
|
+
"--dest",
|
|
1007
|
+
localDir,
|
|
1008
|
+
"--method",
|
|
1009
|
+
"git"
|
|
1010
|
+
]);
|
|
1011
|
+
}
|
|
1012
|
+
const skillPath = join(localDir, skill.name);
|
|
1013
|
+
await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
|
|
1014
|
+
}
|
|
1015
|
+
const remoteNames = new Set(remote.map((row) => row.name));
|
|
1016
|
+
for (const [name, localInfo] of localSkills.entries()) {
|
|
1017
|
+
if (!remoteNames.has(name)) {
|
|
1018
|
+
await rm(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
const nextOwners = {};
|
|
1022
|
+
for (const item of remote) {
|
|
1023
|
+
const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
|
|
1024
|
+
if (owner) nextOwners[item.name] = owner;
|
|
1025
|
+
}
|
|
1026
|
+
await writeSkillsSyncState({ ...state, installedOwners: nextOwners });
|
|
1027
|
+
try {
|
|
1028
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
1029
|
+
} catch {
|
|
1030
|
+
}
|
|
1031
|
+
setJson(res, 200, { ok: true, data: { synced: remote.length } });
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to pull synced skills") });
|
|
1034
|
+
}
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
|
|
1038
|
+
try {
|
|
1039
|
+
const owner = url.searchParams.get("owner") || "";
|
|
1040
|
+
const name = url.searchParams.get("name") || "";
|
|
1041
|
+
if (!owner || !name) {
|
|
1042
|
+
setJson(res, 400, { error: "Missing owner or name" });
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
|
|
1046
|
+
const resp = await fetch(rawUrl);
|
|
1047
|
+
if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
|
|
1048
|
+
const content = await resp.text();
|
|
1049
|
+
setJson(res, 200, { content });
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
|
|
1052
|
+
}
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
|
|
1056
|
+
try {
|
|
1057
|
+
const payload = asRecord(await readJsonBody2(req));
|
|
1058
|
+
const owner = typeof payload?.owner === "string" ? payload.owner : "";
|
|
1059
|
+
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1060
|
+
if (!owner || !name) {
|
|
1061
|
+
setJson(res, 400, { error: "Missing owner or name" });
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
|
|
1065
|
+
const installDest = await detectUserSkillsDir(appServer);
|
|
1066
|
+
await runCommand("python3", [
|
|
1067
|
+
installerScript,
|
|
1068
|
+
"--repo",
|
|
1069
|
+
`${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
|
|
1070
|
+
"--path",
|
|
1071
|
+
`skills/${owner}/${name}`,
|
|
1072
|
+
"--dest",
|
|
1073
|
+
installDest,
|
|
1074
|
+
"--method",
|
|
1075
|
+
"git"
|
|
1076
|
+
]);
|
|
1077
|
+
const skillDir = join(installDest, name);
|
|
1078
|
+
await ensureInstalledSkillIsValid(appServer, skillDir);
|
|
1079
|
+
const syncState = await readSkillsSyncState();
|
|
1080
|
+
const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
|
|
1081
|
+
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1082
|
+
await autoPushSyncedSkills(appServer);
|
|
1083
|
+
setJson(res, 200, { ok: true, path: skillDir });
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
|
|
1086
|
+
}
|
|
1087
|
+
return true;
|
|
1088
|
+
}
|
|
1089
|
+
if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
|
|
1090
|
+
try {
|
|
1091
|
+
const payload = asRecord(await readJsonBody2(req));
|
|
1092
|
+
const name = typeof payload?.name === "string" ? payload.name : "";
|
|
1093
|
+
const path = typeof payload?.path === "string" ? payload.path : "";
|
|
1094
|
+
const target = path || (name ? join(getSkillsInstallDir(), name) : "");
|
|
1095
|
+
if (!target) {
|
|
1096
|
+
setJson(res, 400, { error: "Missing name or path" });
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
await rm(target, { recursive: true, force: true });
|
|
1100
|
+
if (name) {
|
|
1101
|
+
const syncState = await readSkillsSyncState();
|
|
1102
|
+
const nextOwners = { ...syncState.installedOwners ?? {} };
|
|
1103
|
+
delete nextOwners[name];
|
|
1104
|
+
await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
|
|
1105
|
+
}
|
|
1106
|
+
await autoPushSyncedSkills(appServer);
|
|
1107
|
+
try {
|
|
1108
|
+
await appServer.rpc("skills/list", { forceReload: true });
|
|
1109
|
+
} catch {
|
|
1110
|
+
}
|
|
1111
|
+
setJson(res, 200, { ok: true, deletedPath: target });
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
setJson(res, 502, { error: getErrorMessage(error, "Failed to uninstall skill") });
|
|
1114
|
+
}
|
|
1115
|
+
return true;
|
|
1116
|
+
}
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/server/codexAppServerBridge.ts
|
|
1121
|
+
function asRecord2(value) {
|
|
34
1122
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
35
1123
|
}
|
|
36
|
-
function
|
|
1124
|
+
function getErrorMessage2(payload, fallback) {
|
|
37
1125
|
if (payload instanceof Error && payload.message.trim().length > 0) {
|
|
38
1126
|
return payload.message;
|
|
39
1127
|
}
|
|
40
|
-
const record =
|
|
1128
|
+
const record = asRecord2(payload);
|
|
41
1129
|
if (!record) return fallback;
|
|
42
1130
|
const error = record.error;
|
|
43
1131
|
if (typeof error === "string" && error.length > 0) return error;
|
|
44
|
-
const nestedError =
|
|
1132
|
+
const nestedError = asRecord2(error);
|
|
45
1133
|
if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
|
|
46
1134
|
return nestedError.message;
|
|
47
1135
|
}
|
|
48
1136
|
return fallback;
|
|
49
1137
|
}
|
|
50
|
-
function
|
|
1138
|
+
function setJson2(res, statusCode, payload) {
|
|
51
1139
|
res.statusCode = statusCode;
|
|
52
1140
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
53
1141
|
res.end(JSON.stringify(payload));
|
|
54
1142
|
}
|
|
55
1143
|
function extractThreadMessageText(threadReadPayload) {
|
|
56
|
-
const payload =
|
|
57
|
-
const thread =
|
|
1144
|
+
const payload = asRecord2(threadReadPayload);
|
|
1145
|
+
const thread = asRecord2(payload?.thread);
|
|
58
1146
|
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
59
1147
|
const parts = [];
|
|
60
1148
|
for (const turn of turns) {
|
|
61
|
-
const turnRecord =
|
|
1149
|
+
const turnRecord = asRecord2(turn);
|
|
62
1150
|
const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
|
|
63
1151
|
for (const item of items) {
|
|
64
|
-
const itemRecord =
|
|
1152
|
+
const itemRecord = asRecord2(item);
|
|
65
1153
|
const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
|
|
66
1154
|
if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
|
|
67
1155
|
parts.push(itemRecord.text.trim());
|
|
@@ -70,7 +1158,7 @@ function extractThreadMessageText(threadReadPayload) {
|
|
|
70
1158
|
if (type === "userMessage") {
|
|
71
1159
|
const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
|
|
72
1160
|
for (const block of content) {
|
|
73
|
-
const blockRecord =
|
|
1161
|
+
const blockRecord = asRecord2(block);
|
|
74
1162
|
if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
|
|
75
1163
|
parts.push(blockRecord.text.trim());
|
|
76
1164
|
}
|
|
@@ -106,7 +1194,7 @@ function scoreFileCandidate(path, query) {
|
|
|
106
1194
|
}
|
|
107
1195
|
async function listFilesWithRipgrep(cwd) {
|
|
108
1196
|
return await new Promise((resolve2, reject) => {
|
|
109
|
-
const proc =
|
|
1197
|
+
const proc = spawn2("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
|
|
110
1198
|
cwd,
|
|
111
1199
|
env: process.env,
|
|
112
1200
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -131,16 +1219,13 @@ async function listFilesWithRipgrep(cwd) {
|
|
|
131
1219
|
});
|
|
132
1220
|
});
|
|
133
1221
|
}
|
|
134
|
-
function
|
|
1222
|
+
function getCodexHomeDir2() {
|
|
135
1223
|
const codexHome = process.env.CODEX_HOME?.trim();
|
|
136
|
-
return codexHome && codexHome.length > 0 ? codexHome :
|
|
137
|
-
}
|
|
138
|
-
function getSkillsInstallDir() {
|
|
139
|
-
return join(getCodexHomeDir(), "skills");
|
|
1224
|
+
return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
|
|
140
1225
|
}
|
|
141
|
-
async function
|
|
1226
|
+
async function runCommand2(command, args, options = {}) {
|
|
142
1227
|
await new Promise((resolve2, reject) => {
|
|
143
|
-
const proc =
|
|
1228
|
+
const proc = spawn2(command, args, {
|
|
144
1229
|
cwd: options.cwd,
|
|
145
1230
|
env: process.env,
|
|
146
1231
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -166,30 +1251,30 @@ async function runCommand(command, args, options = {}) {
|
|
|
166
1251
|
});
|
|
167
1252
|
}
|
|
168
1253
|
function isMissingHeadError(error) {
|
|
169
|
-
const message =
|
|
1254
|
+
const message = getErrorMessage2(error, "").toLowerCase();
|
|
170
1255
|
return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
|
|
171
1256
|
}
|
|
172
1257
|
function isNotGitRepositoryError(error) {
|
|
173
|
-
const message =
|
|
1258
|
+
const message = getErrorMessage2(error, "").toLowerCase();
|
|
174
1259
|
return message.includes("not a git repository") || message.includes("fatal: not a git repository");
|
|
175
1260
|
}
|
|
176
1261
|
async function ensureRepoHasInitialCommit(repoRoot) {
|
|
177
|
-
const agentsPath =
|
|
1262
|
+
const agentsPath = join2(repoRoot, "AGENTS.md");
|
|
178
1263
|
try {
|
|
179
|
-
await
|
|
1264
|
+
await stat2(agentsPath);
|
|
180
1265
|
} catch {
|
|
181
|
-
await
|
|
1266
|
+
await writeFile2(agentsPath, "", "utf8");
|
|
182
1267
|
}
|
|
183
|
-
await
|
|
184
|
-
await
|
|
1268
|
+
await runCommand2("git", ["add", "AGENTS.md"], { cwd: repoRoot });
|
|
1269
|
+
await runCommand2(
|
|
185
1270
|
"git",
|
|
186
1271
|
["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
|
|
187
1272
|
{ cwd: repoRoot }
|
|
188
1273
|
);
|
|
189
1274
|
}
|
|
190
1275
|
async function runCommandCapture(command, args, options = {}) {
|
|
191
|
-
return await new Promise((
|
|
192
|
-
const proc =
|
|
1276
|
+
return await new Promise((resolve2, reject) => {
|
|
1277
|
+
const proc = spawn2(command, args, {
|
|
193
1278
|
cwd: options.cwd,
|
|
194
1279
|
env: process.env,
|
|
195
1280
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -205,7 +1290,7 @@ async function runCommandCapture(command, args, options = {}) {
|
|
|
205
1290
|
proc.on("error", reject);
|
|
206
1291
|
proc.on("close", (code) => {
|
|
207
1292
|
if (code === 0) {
|
|
208
|
-
|
|
1293
|
+
resolve2(stdout.trim());
|
|
209
1294
|
return;
|
|
210
1295
|
}
|
|
211
1296
|
const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
@@ -214,161 +1299,6 @@ async function runCommandCapture(command, args, options = {}) {
|
|
|
214
1299
|
});
|
|
215
1300
|
});
|
|
216
1301
|
}
|
|
217
|
-
async function detectUserSkillsDir(appServer) {
|
|
218
|
-
try {
|
|
219
|
-
const result = await appServer.rpc("skills/list", {});
|
|
220
|
-
for (const entry of result.data ?? []) {
|
|
221
|
-
for (const skill of entry.skills ?? []) {
|
|
222
|
-
if (skill.scope !== "user" || !skill.path) continue;
|
|
223
|
-
const parts = skill.path.split("/").filter(Boolean);
|
|
224
|
-
if (parts.length < 2) continue;
|
|
225
|
-
return `/${parts.slice(0, -2).join("/")}`;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
} catch {
|
|
229
|
-
}
|
|
230
|
-
return getSkillsInstallDir();
|
|
231
|
-
}
|
|
232
|
-
async function ensureInstalledSkillIsValid(appServer, skillPath) {
|
|
233
|
-
const result = await appServer.rpc("skills/list", { forceReload: true });
|
|
234
|
-
const normalized = skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md`;
|
|
235
|
-
for (const entry of result.data ?? []) {
|
|
236
|
-
for (const error of entry.errors ?? []) {
|
|
237
|
-
if (error.path === normalized) {
|
|
238
|
-
throw new Error(error.message || "Installed skill is invalid");
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
var TREE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
244
|
-
var skillsTreeCache = null;
|
|
245
|
-
var metaCache = /* @__PURE__ */ new Map();
|
|
246
|
-
async function getGhToken() {
|
|
247
|
-
try {
|
|
248
|
-
const proc = spawn("gh", ["auth", "token"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
249
|
-
let out = "";
|
|
250
|
-
proc.stdout.on("data", (d) => {
|
|
251
|
-
out += d.toString();
|
|
252
|
-
});
|
|
253
|
-
return new Promise((resolve2) => {
|
|
254
|
-
proc.on("close", (code) => resolve2(code === 0 ? out.trim() : null));
|
|
255
|
-
proc.on("error", () => resolve2(null));
|
|
256
|
-
});
|
|
257
|
-
} catch {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
async function ghFetch(url) {
|
|
262
|
-
const token = await getGhToken();
|
|
263
|
-
const headers = {
|
|
264
|
-
Accept: "application/vnd.github+json",
|
|
265
|
-
"User-Agent": "codex-web-local"
|
|
266
|
-
};
|
|
267
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
268
|
-
return fetch(url, { headers });
|
|
269
|
-
}
|
|
270
|
-
async function fetchSkillsTree() {
|
|
271
|
-
if (skillsTreeCache && Date.now() - skillsTreeCache.fetchedAt < TREE_CACHE_TTL_MS) {
|
|
272
|
-
return skillsTreeCache.entries;
|
|
273
|
-
}
|
|
274
|
-
const resp = await ghFetch("https://api.github.com/repos/openclaw/skills/git/trees/main?recursive=1");
|
|
275
|
-
if (!resp.ok) throw new Error(`GitHub tree API returned ${resp.status}`);
|
|
276
|
-
const data = await resp.json();
|
|
277
|
-
const metaPattern = /^skills\/([^/]+)\/([^/]+)\/_meta\.json$/;
|
|
278
|
-
const seen = /* @__PURE__ */ new Set();
|
|
279
|
-
const entries = [];
|
|
280
|
-
for (const node of data.tree ?? []) {
|
|
281
|
-
const match = metaPattern.exec(node.path);
|
|
282
|
-
if (!match) continue;
|
|
283
|
-
const [, owner, skillName] = match;
|
|
284
|
-
const key = `${owner}/${skillName}`;
|
|
285
|
-
if (seen.has(key)) continue;
|
|
286
|
-
seen.add(key);
|
|
287
|
-
entries.push({
|
|
288
|
-
name: skillName,
|
|
289
|
-
owner,
|
|
290
|
-
url: `https://github.com/openclaw/skills/tree/main/skills/${owner}/${skillName}`
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
skillsTreeCache = { entries, fetchedAt: Date.now() };
|
|
294
|
-
return entries;
|
|
295
|
-
}
|
|
296
|
-
async function fetchMetaBatch(entries) {
|
|
297
|
-
const toFetch = entries.filter((e) => !metaCache.has(`${e.owner}/${e.name}`));
|
|
298
|
-
if (toFetch.length === 0) return;
|
|
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;
|
|
314
|
-
}
|
|
315
|
-
function buildHubEntry(e) {
|
|
316
|
-
const cached = metaCache.get(`${e.owner}/${e.name}`);
|
|
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
|
-
};
|
|
327
|
-
}
|
|
328
|
-
async function scanInstalledSkillsFromDisk() {
|
|
329
|
-
const map = /* @__PURE__ */ new Map();
|
|
330
|
-
const skillsDir = getSkillsInstallDir();
|
|
331
|
-
try {
|
|
332
|
-
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
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
|
-
}
|
|
342
|
-
} catch {
|
|
343
|
-
}
|
|
344
|
-
return map;
|
|
345
|
-
}
|
|
346
|
-
async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
|
|
347
|
-
const q = query.toLowerCase().trim();
|
|
348
|
-
let filtered = q ? allEntries.filter((s) => {
|
|
349
|
-
if (s.name.toLowerCase().includes(q) || s.owner.toLowerCase().includes(q)) return true;
|
|
350
|
-
const cached = metaCache.get(`${s.owner}/${s.name}`);
|
|
351
|
-
if (cached?.displayName?.toLowerCase().includes(q)) return true;
|
|
352
|
-
return false;
|
|
353
|
-
}) : allEntries;
|
|
354
|
-
const page = filtered.slice(0, Math.min(limit * 2, 200));
|
|
355
|
-
await fetchMetaBatch(page);
|
|
356
|
-
let results = page.map(buildHubEntry);
|
|
357
|
-
if (sort === "date") {
|
|
358
|
-
results.sort((a, b) => b.publishedAt - a.publishedAt);
|
|
359
|
-
} else if (q) {
|
|
360
|
-
results.sort((a, b) => {
|
|
361
|
-
const aExact = a.name.toLowerCase() === q ? 1 : 0;
|
|
362
|
-
const bExact = b.name.toLowerCase() === q ? 1 : 0;
|
|
363
|
-
if (aExact !== bExact) return bExact - aExact;
|
|
364
|
-
return b.publishedAt - a.publishedAt;
|
|
365
|
-
});
|
|
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
|
-
});
|
|
371
|
-
}
|
|
372
1302
|
function normalizeStringArray(value) {
|
|
373
1303
|
if (!Array.isArray(value)) return [];
|
|
374
1304
|
const normalized = [];
|
|
@@ -390,11 +1320,11 @@ function normalizeStringRecord(value) {
|
|
|
390
1320
|
return next;
|
|
391
1321
|
}
|
|
392
1322
|
function getCodexAuthPath() {
|
|
393
|
-
return
|
|
1323
|
+
return join2(getCodexHomeDir2(), "auth.json");
|
|
394
1324
|
}
|
|
395
1325
|
async function readCodexAuth() {
|
|
396
1326
|
try {
|
|
397
|
-
const raw = await
|
|
1327
|
+
const raw = await readFile2(getCodexAuthPath(), "utf8");
|
|
398
1328
|
const auth = JSON.parse(raw);
|
|
399
1329
|
const token = auth.tokens?.access_token;
|
|
400
1330
|
if (!token) return null;
|
|
@@ -404,13 +1334,13 @@ async function readCodexAuth() {
|
|
|
404
1334
|
}
|
|
405
1335
|
}
|
|
406
1336
|
function getCodexGlobalStatePath() {
|
|
407
|
-
return
|
|
1337
|
+
return join2(getCodexHomeDir2(), ".codex-global-state.json");
|
|
408
1338
|
}
|
|
409
1339
|
var MAX_THREAD_TITLES = 500;
|
|
410
1340
|
function normalizeThreadTitleCache(value) {
|
|
411
|
-
const record =
|
|
1341
|
+
const record = asRecord2(value);
|
|
412
1342
|
if (!record) return { titles: {}, order: [] };
|
|
413
|
-
const rawTitles =
|
|
1343
|
+
const rawTitles = asRecord2(record.titles);
|
|
414
1344
|
const titles = {};
|
|
415
1345
|
if (rawTitles) {
|
|
416
1346
|
for (const [k, v] of Object.entries(rawTitles)) {
|
|
@@ -436,8 +1366,8 @@ function removeFromThreadTitleCache(cache, id) {
|
|
|
436
1366
|
async function readThreadTitleCache() {
|
|
437
1367
|
const statePath = getCodexGlobalStatePath();
|
|
438
1368
|
try {
|
|
439
|
-
const raw = await
|
|
440
|
-
const payload =
|
|
1369
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1370
|
+
const payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
441
1371
|
return normalizeThreadTitleCache(payload["thread-titles"]);
|
|
442
1372
|
} catch {
|
|
443
1373
|
return { titles: {}, order: [] };
|
|
@@ -447,21 +1377,21 @@ async function writeThreadTitleCache(cache) {
|
|
|
447
1377
|
const statePath = getCodexGlobalStatePath();
|
|
448
1378
|
let payload = {};
|
|
449
1379
|
try {
|
|
450
|
-
const raw = await
|
|
451
|
-
payload =
|
|
1380
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1381
|
+
payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
452
1382
|
} catch {
|
|
453
1383
|
payload = {};
|
|
454
1384
|
}
|
|
455
1385
|
payload["thread-titles"] = cache;
|
|
456
|
-
await
|
|
1386
|
+
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
457
1387
|
}
|
|
458
1388
|
async function readWorkspaceRootsState() {
|
|
459
1389
|
const statePath = getCodexGlobalStatePath();
|
|
460
1390
|
let payload = {};
|
|
461
1391
|
try {
|
|
462
|
-
const raw = await
|
|
1392
|
+
const raw = await readFile2(statePath, "utf8");
|
|
463
1393
|
const parsed = JSON.parse(raw);
|
|
464
|
-
payload =
|
|
1394
|
+
payload = asRecord2(parsed) ?? {};
|
|
465
1395
|
} catch {
|
|
466
1396
|
payload = {};
|
|
467
1397
|
}
|
|
@@ -475,15 +1405,15 @@ async function writeWorkspaceRootsState(nextState) {
|
|
|
475
1405
|
const statePath = getCodexGlobalStatePath();
|
|
476
1406
|
let payload = {};
|
|
477
1407
|
try {
|
|
478
|
-
const raw = await
|
|
479
|
-
payload =
|
|
1408
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1409
|
+
payload = asRecord2(JSON.parse(raw)) ?? {};
|
|
480
1410
|
} catch {
|
|
481
1411
|
payload = {};
|
|
482
1412
|
}
|
|
483
1413
|
payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
|
|
484
1414
|
payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
|
|
485
1415
|
payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
|
|
486
|
-
await
|
|
1416
|
+
await writeFile2(statePath, JSON.stringify(payload), "utf8");
|
|
487
1417
|
}
|
|
488
1418
|
async function readJsonBody(req) {
|
|
489
1419
|
const raw = await readRawBody(req);
|
|
@@ -521,7 +1451,7 @@ function handleFileUpload(req, res) {
|
|
|
521
1451
|
const contentType = req.headers["content-type"] ?? "";
|
|
522
1452
|
const boundaryMatch = contentType.match(/boundary=(.+)/i);
|
|
523
1453
|
if (!boundaryMatch) {
|
|
524
|
-
|
|
1454
|
+
setJson2(res, 400, { error: "Missing multipart boundary" });
|
|
525
1455
|
return;
|
|
526
1456
|
}
|
|
527
1457
|
const boundary = boundaryMatch[1];
|
|
@@ -551,21 +1481,21 @@ function handleFileUpload(req, res) {
|
|
|
551
1481
|
break;
|
|
552
1482
|
}
|
|
553
1483
|
if (!fileData) {
|
|
554
|
-
|
|
1484
|
+
setJson2(res, 400, { error: "No file in request" });
|
|
555
1485
|
return;
|
|
556
1486
|
}
|
|
557
|
-
const uploadDir =
|
|
558
|
-
await
|
|
559
|
-
const destDir = await
|
|
560
|
-
const destPath =
|
|
561
|
-
await
|
|
562
|
-
|
|
1487
|
+
const uploadDir = join2(tmpdir2(), "codex-web-uploads");
|
|
1488
|
+
await mkdir2(uploadDir, { recursive: true });
|
|
1489
|
+
const destDir = await mkdtemp2(join2(uploadDir, "f-"));
|
|
1490
|
+
const destPath = join2(destDir, fileName);
|
|
1491
|
+
await writeFile2(destPath, fileData);
|
|
1492
|
+
setJson2(res, 200, { path: destPath });
|
|
563
1493
|
} catch (err) {
|
|
564
|
-
|
|
1494
|
+
setJson2(res, 500, { error: getErrorMessage2(err, "Upload failed") });
|
|
565
1495
|
}
|
|
566
1496
|
});
|
|
567
1497
|
req.on("error", (err) => {
|
|
568
|
-
|
|
1498
|
+
setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
|
|
569
1499
|
});
|
|
570
1500
|
}
|
|
571
1501
|
async function proxyTranscribe(body, contentType, authToken, accountId) {
|
|
@@ -617,7 +1547,7 @@ var AppServerProcess = class {
|
|
|
617
1547
|
start() {
|
|
618
1548
|
if (this.process) return;
|
|
619
1549
|
this.stopping = false;
|
|
620
|
-
const proc =
|
|
1550
|
+
const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
621
1551
|
this.process = proc;
|
|
622
1552
|
proc.stdout.setEncoding("utf8");
|
|
623
1553
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -711,7 +1641,7 @@ var AppServerProcess = class {
|
|
|
711
1641
|
}
|
|
712
1642
|
this.pendingServerRequests.delete(requestId);
|
|
713
1643
|
this.sendServerRequestReply(requestId, reply);
|
|
714
|
-
const requestParams =
|
|
1644
|
+
const requestParams = asRecord2(pendingRequest.params);
|
|
715
1645
|
const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
|
|
716
1646
|
this.emitNotification({
|
|
717
1647
|
method: "server/request/resolved",
|
|
@@ -780,7 +1710,7 @@ var AppServerProcess = class {
|
|
|
780
1710
|
}
|
|
781
1711
|
async respondToServerRequest(payload) {
|
|
782
1712
|
await this.ensureInitialized();
|
|
783
|
-
const body =
|
|
1713
|
+
const body = asRecord2(payload);
|
|
784
1714
|
if (!body) {
|
|
785
1715
|
throw new Error("Invalid response payload: expected object");
|
|
786
1716
|
}
|
|
@@ -788,7 +1718,7 @@ var AppServerProcess = class {
|
|
|
788
1718
|
if (typeof id !== "number" || !Number.isInteger(id)) {
|
|
789
1719
|
throw new Error('Invalid response payload: "id" must be an integer');
|
|
790
1720
|
}
|
|
791
|
-
const rawError =
|
|
1721
|
+
const rawError = asRecord2(body.error);
|
|
792
1722
|
if (rawError) {
|
|
793
1723
|
const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
|
|
794
1724
|
const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
|
|
@@ -843,7 +1773,7 @@ var MethodCatalog = class {
|
|
|
843
1773
|
}
|
|
844
1774
|
async runGenerateSchemaCommand(outDir) {
|
|
845
1775
|
await new Promise((resolve2, reject) => {
|
|
846
|
-
const process2 =
|
|
1776
|
+
const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
|
|
847
1777
|
stdio: ["ignore", "ignore", "pipe"]
|
|
848
1778
|
});
|
|
849
1779
|
let stderr = "";
|
|
@@ -862,13 +1792,13 @@ var MethodCatalog = class {
|
|
|
862
1792
|
});
|
|
863
1793
|
}
|
|
864
1794
|
extractMethodsFromClientRequest(payload) {
|
|
865
|
-
const root =
|
|
1795
|
+
const root = asRecord2(payload);
|
|
866
1796
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
867
1797
|
const methods = /* @__PURE__ */ new Set();
|
|
868
1798
|
for (const entry of oneOf) {
|
|
869
|
-
const row =
|
|
870
|
-
const properties =
|
|
871
|
-
const methodDef =
|
|
1799
|
+
const row = asRecord2(entry);
|
|
1800
|
+
const properties = asRecord2(row?.properties);
|
|
1801
|
+
const methodDef = asRecord2(properties?.method);
|
|
872
1802
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
873
1803
|
for (const item of methodEnum) {
|
|
874
1804
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -879,13 +1809,13 @@ var MethodCatalog = class {
|
|
|
879
1809
|
return Array.from(methods).sort((a, b) => a.localeCompare(b));
|
|
880
1810
|
}
|
|
881
1811
|
extractMethodsFromServerNotification(payload) {
|
|
882
|
-
const root =
|
|
1812
|
+
const root = asRecord2(payload);
|
|
883
1813
|
const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
|
|
884
1814
|
const methods = /* @__PURE__ */ new Set();
|
|
885
1815
|
for (const entry of oneOf) {
|
|
886
|
-
const row =
|
|
887
|
-
const properties =
|
|
888
|
-
const methodDef =
|
|
1816
|
+
const row = asRecord2(entry);
|
|
1817
|
+
const properties = asRecord2(row?.properties);
|
|
1818
|
+
const methodDef = asRecord2(properties?.method);
|
|
889
1819
|
const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
|
|
890
1820
|
for (const item of methodEnum) {
|
|
891
1821
|
if (typeof item === "string" && item.length > 0) {
|
|
@@ -899,10 +1829,10 @@ var MethodCatalog = class {
|
|
|
899
1829
|
if (this.methodCache) {
|
|
900
1830
|
return this.methodCache;
|
|
901
1831
|
}
|
|
902
|
-
const outDir = await
|
|
1832
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
903
1833
|
await this.runGenerateSchemaCommand(outDir);
|
|
904
|
-
const clientRequestPath =
|
|
905
|
-
const raw = await
|
|
1834
|
+
const clientRequestPath = join2(outDir, "ClientRequest.json");
|
|
1835
|
+
const raw = await readFile2(clientRequestPath, "utf8");
|
|
906
1836
|
const parsed = JSON.parse(raw);
|
|
907
1837
|
const methods = this.extractMethodsFromClientRequest(parsed);
|
|
908
1838
|
this.methodCache = methods;
|
|
@@ -912,10 +1842,10 @@ var MethodCatalog = class {
|
|
|
912
1842
|
if (this.notificationCache) {
|
|
913
1843
|
return this.notificationCache;
|
|
914
1844
|
}
|
|
915
|
-
const outDir = await
|
|
1845
|
+
const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
|
|
916
1846
|
await this.runGenerateSchemaCommand(outDir);
|
|
917
|
-
const serverNotificationPath =
|
|
918
|
-
const raw = await
|
|
1847
|
+
const serverNotificationPath = join2(outDir, "ServerNotification.json");
|
|
1848
|
+
const raw = await readFile2(serverNotificationPath, "utf8");
|
|
919
1849
|
const parsed = JSON.parse(raw);
|
|
920
1850
|
const methods = this.extractMethodsFromServerNotification(parsed);
|
|
921
1851
|
this.notificationCache = methods;
|
|
@@ -938,7 +1868,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
938
1868
|
const threads = [];
|
|
939
1869
|
let cursor = null;
|
|
940
1870
|
do {
|
|
941
|
-
const response =
|
|
1871
|
+
const response = asRecord2(await appServer.rpc("thread/list", {
|
|
942
1872
|
archived: false,
|
|
943
1873
|
limit: 100,
|
|
944
1874
|
sortKey: "updated_at",
|
|
@@ -946,7 +1876,7 @@ async function loadAllThreadsForSearch(appServer) {
|
|
|
946
1876
|
}));
|
|
947
1877
|
const data = Array.isArray(response?.data) ? response.data : [];
|
|
948
1878
|
for (const row of data) {
|
|
949
|
-
const record =
|
|
1879
|
+
const record = asRecord2(row);
|
|
950
1880
|
const id = typeof record?.id === "string" ? record.id : "";
|
|
951
1881
|
if (!id) continue;
|
|
952
1882
|
const title = typeof record?.name === "string" && record.name.trim().length > 0 ? record.name.trim() : typeof record?.preview === "string" && record.preview.trim().length > 0 ? record.preview.trim() : "Untitled thread";
|
|
@@ -1010,6 +1940,7 @@ function createCodexBridgeMiddleware() {
|
|
|
1010
1940
|
}
|
|
1011
1941
|
return threadSearchIndexPromise;
|
|
1012
1942
|
}
|
|
1943
|
+
void initializeSkillsSyncOnStartup(appServer);
|
|
1013
1944
|
const middleware = async (req, res, next) => {
|
|
1014
1945
|
try {
|
|
1015
1946
|
if (!req.url) {
|
|
@@ -1017,25 +1948,28 @@ function createCodexBridgeMiddleware() {
|
|
|
1017
1948
|
return;
|
|
1018
1949
|
}
|
|
1019
1950
|
const url = new URL(req.url, "http://localhost");
|
|
1951
|
+
if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1020
1954
|
if (req.method === "POST" && url.pathname === "/codex-api/upload-file") {
|
|
1021
1955
|
handleFileUpload(req, res);
|
|
1022
1956
|
return;
|
|
1023
1957
|
}
|
|
1024
1958
|
if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
|
|
1025
1959
|
const payload = await readJsonBody(req);
|
|
1026
|
-
const body =
|
|
1960
|
+
const body = asRecord2(payload);
|
|
1027
1961
|
if (!body || typeof body.method !== "string" || body.method.length === 0) {
|
|
1028
|
-
|
|
1962
|
+
setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
|
|
1029
1963
|
return;
|
|
1030
1964
|
}
|
|
1031
1965
|
const result = await appServer.rpc(body.method, body.params ?? null);
|
|
1032
|
-
|
|
1966
|
+
setJson2(res, 200, { result });
|
|
1033
1967
|
return;
|
|
1034
1968
|
}
|
|
1035
1969
|
if (req.method === "POST" && url.pathname === "/codex-api/transcribe") {
|
|
1036
1970
|
const auth = await readCodexAuth();
|
|
1037
1971
|
if (!auth) {
|
|
1038
|
-
|
|
1972
|
+
setJson2(res, 401, { error: "No auth token available for transcription" });
|
|
1039
1973
|
return;
|
|
1040
1974
|
}
|
|
1041
1975
|
const rawBody = await readRawBody(req);
|
|
@@ -1049,48 +1983,48 @@ function createCodexBridgeMiddleware() {
|
|
|
1049
1983
|
if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
|
|
1050
1984
|
const payload = await readJsonBody(req);
|
|
1051
1985
|
await appServer.respondToServerRequest(payload);
|
|
1052
|
-
|
|
1986
|
+
setJson2(res, 200, { ok: true });
|
|
1053
1987
|
return;
|
|
1054
1988
|
}
|
|
1055
1989
|
if (req.method === "GET" && url.pathname === "/codex-api/server-requests/pending") {
|
|
1056
|
-
|
|
1990
|
+
setJson2(res, 200, { data: appServer.listPendingServerRequests() });
|
|
1057
1991
|
return;
|
|
1058
1992
|
}
|
|
1059
1993
|
if (req.method === "GET" && url.pathname === "/codex-api/meta/methods") {
|
|
1060
1994
|
const methods = await methodCatalog.listMethods();
|
|
1061
|
-
|
|
1995
|
+
setJson2(res, 200, { data: methods });
|
|
1062
1996
|
return;
|
|
1063
1997
|
}
|
|
1064
1998
|
if (req.method === "GET" && url.pathname === "/codex-api/meta/notifications") {
|
|
1065
1999
|
const methods = await methodCatalog.listNotificationMethods();
|
|
1066
|
-
|
|
2000
|
+
setJson2(res, 200, { data: methods });
|
|
1067
2001
|
return;
|
|
1068
2002
|
}
|
|
1069
2003
|
if (req.method === "GET" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1070
2004
|
const state = await readWorkspaceRootsState();
|
|
1071
|
-
|
|
2005
|
+
setJson2(res, 200, { data: state });
|
|
1072
2006
|
return;
|
|
1073
2007
|
}
|
|
1074
2008
|
if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
|
|
1075
|
-
|
|
2009
|
+
setJson2(res, 200, { data: { path: homedir2() } });
|
|
1076
2010
|
return;
|
|
1077
2011
|
}
|
|
1078
2012
|
if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
|
|
1079
|
-
const payload =
|
|
2013
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1080
2014
|
const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
|
|
1081
2015
|
if (!rawSourceCwd) {
|
|
1082
|
-
|
|
2016
|
+
setJson2(res, 400, { error: "Missing sourceCwd" });
|
|
1083
2017
|
return;
|
|
1084
2018
|
}
|
|
1085
2019
|
const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
|
|
1086
2020
|
try {
|
|
1087
|
-
const sourceInfo = await
|
|
2021
|
+
const sourceInfo = await stat2(sourceCwd);
|
|
1088
2022
|
if (!sourceInfo.isDirectory()) {
|
|
1089
|
-
|
|
2023
|
+
setJson2(res, 400, { error: "sourceCwd is not a directory" });
|
|
1090
2024
|
return;
|
|
1091
2025
|
}
|
|
1092
2026
|
} catch {
|
|
1093
|
-
|
|
2027
|
+
setJson2(res, 404, { error: "sourceCwd does not exist" });
|
|
1094
2028
|
return;
|
|
1095
2029
|
}
|
|
1096
2030
|
try {
|
|
@@ -1099,25 +2033,25 @@ function createCodexBridgeMiddleware() {
|
|
|
1099
2033
|
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1100
2034
|
} catch (error) {
|
|
1101
2035
|
if (!isNotGitRepositoryError(error)) throw error;
|
|
1102
|
-
await
|
|
2036
|
+
await runCommand2("git", ["init"], { cwd: sourceCwd });
|
|
1103
2037
|
gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
|
|
1104
2038
|
}
|
|
1105
2039
|
const repoName = basename(gitRoot) || "repo";
|
|
1106
|
-
const worktreesRoot =
|
|
1107
|
-
await
|
|
2040
|
+
const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
|
|
2041
|
+
await mkdir2(worktreesRoot, { recursive: true });
|
|
1108
2042
|
let worktreeId = "";
|
|
1109
2043
|
let worktreeParent = "";
|
|
1110
2044
|
let worktreeCwd = "";
|
|
1111
2045
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
1112
2046
|
const candidate = randomBytes(2).toString("hex");
|
|
1113
|
-
const parent =
|
|
2047
|
+
const parent = join2(worktreesRoot, candidate);
|
|
1114
2048
|
try {
|
|
1115
|
-
await
|
|
2049
|
+
await stat2(parent);
|
|
1116
2050
|
continue;
|
|
1117
2051
|
} catch {
|
|
1118
2052
|
worktreeId = candidate;
|
|
1119
2053
|
worktreeParent = parent;
|
|
1120
|
-
worktreeCwd =
|
|
2054
|
+
worktreeCwd = join2(parent, repoName);
|
|
1121
2055
|
break;
|
|
1122
2056
|
}
|
|
1123
2057
|
}
|
|
@@ -1125,15 +2059,15 @@ function createCodexBridgeMiddleware() {
|
|
|
1125
2059
|
throw new Error("Failed to allocate a unique worktree id");
|
|
1126
2060
|
}
|
|
1127
2061
|
const branch = `codex/${worktreeId}`;
|
|
1128
|
-
await
|
|
2062
|
+
await mkdir2(worktreeParent, { recursive: true });
|
|
1129
2063
|
try {
|
|
1130
|
-
await
|
|
2064
|
+
await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1131
2065
|
} catch (error) {
|
|
1132
2066
|
if (!isMissingHeadError(error)) throw error;
|
|
1133
2067
|
await ensureRepoHasInitialCommit(gitRoot);
|
|
1134
|
-
await
|
|
2068
|
+
await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
|
|
1135
2069
|
}
|
|
1136
|
-
|
|
2070
|
+
setJson2(res, 200, {
|
|
1137
2071
|
data: {
|
|
1138
2072
|
cwd: worktreeCwd,
|
|
1139
2073
|
branch,
|
|
@@ -1141,15 +2075,15 @@ function createCodexBridgeMiddleware() {
|
|
|
1141
2075
|
}
|
|
1142
2076
|
});
|
|
1143
2077
|
} catch (error) {
|
|
1144
|
-
|
|
2078
|
+
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to create worktree") });
|
|
1145
2079
|
}
|
|
1146
2080
|
return;
|
|
1147
2081
|
}
|
|
1148
2082
|
if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
|
|
1149
2083
|
const payload = await readJsonBody(req);
|
|
1150
|
-
const record =
|
|
2084
|
+
const record = asRecord2(payload);
|
|
1151
2085
|
if (!record) {
|
|
1152
|
-
|
|
2086
|
+
setJson2(res, 400, { error: "Invalid body: expected object" });
|
|
1153
2087
|
return;
|
|
1154
2088
|
}
|
|
1155
2089
|
const nextState = {
|
|
@@ -1158,33 +2092,33 @@ function createCodexBridgeMiddleware() {
|
|
|
1158
2092
|
active: normalizeStringArray(record.active)
|
|
1159
2093
|
};
|
|
1160
2094
|
await writeWorkspaceRootsState(nextState);
|
|
1161
|
-
|
|
2095
|
+
setJson2(res, 200, { ok: true });
|
|
1162
2096
|
return;
|
|
1163
2097
|
}
|
|
1164
2098
|
if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
|
|
1165
|
-
const payload =
|
|
2099
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1166
2100
|
const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
|
|
1167
2101
|
const createIfMissing = payload?.createIfMissing === true;
|
|
1168
2102
|
const label = typeof payload?.label === "string" ? payload.label : "";
|
|
1169
2103
|
if (!rawPath) {
|
|
1170
|
-
|
|
2104
|
+
setJson2(res, 400, { error: "Missing path" });
|
|
1171
2105
|
return;
|
|
1172
2106
|
}
|
|
1173
2107
|
const normalizedPath = isAbsolute(rawPath) ? rawPath : resolve(rawPath);
|
|
1174
2108
|
let pathExists = true;
|
|
1175
2109
|
try {
|
|
1176
|
-
const info = await
|
|
2110
|
+
const info = await stat2(normalizedPath);
|
|
1177
2111
|
if (!info.isDirectory()) {
|
|
1178
|
-
|
|
2112
|
+
setJson2(res, 400, { error: "Path exists but is not a directory" });
|
|
1179
2113
|
return;
|
|
1180
2114
|
}
|
|
1181
2115
|
} catch {
|
|
1182
2116
|
pathExists = false;
|
|
1183
2117
|
}
|
|
1184
2118
|
if (!pathExists && createIfMissing) {
|
|
1185
|
-
await
|
|
2119
|
+
await mkdir2(normalizedPath, { recursive: true });
|
|
1186
2120
|
} else if (!pathExists) {
|
|
1187
|
-
|
|
2121
|
+
setJson2(res, 404, { error: "Directory does not exist" });
|
|
1188
2122
|
return;
|
|
1189
2123
|
}
|
|
1190
2124
|
const existingState = await readWorkspaceRootsState();
|
|
@@ -1199,215 +2133,103 @@ function createCodexBridgeMiddleware() {
|
|
|
1199
2133
|
labels: nextLabels,
|
|
1200
2134
|
active: nextActive
|
|
1201
2135
|
});
|
|
1202
|
-
|
|
2136
|
+
setJson2(res, 200, { data: { path: normalizedPath } });
|
|
1203
2137
|
return;
|
|
1204
2138
|
}
|
|
1205
2139
|
if (req.method === "GET" && url.pathname === "/codex-api/project-root-suggestion") {
|
|
1206
2140
|
const basePath = url.searchParams.get("basePath")?.trim() ?? "";
|
|
1207
2141
|
if (!basePath) {
|
|
1208
|
-
|
|
2142
|
+
setJson2(res, 400, { error: "Missing basePath" });
|
|
1209
2143
|
return;
|
|
1210
2144
|
}
|
|
1211
2145
|
const normalizedBasePath = isAbsolute(basePath) ? basePath : resolve(basePath);
|
|
1212
2146
|
try {
|
|
1213
|
-
const baseInfo = await
|
|
2147
|
+
const baseInfo = await stat2(normalizedBasePath);
|
|
1214
2148
|
if (!baseInfo.isDirectory()) {
|
|
1215
|
-
|
|
2149
|
+
setJson2(res, 400, { error: "basePath is not a directory" });
|
|
1216
2150
|
return;
|
|
1217
2151
|
}
|
|
1218
2152
|
} catch {
|
|
1219
|
-
|
|
2153
|
+
setJson2(res, 404, { error: "basePath does not exist" });
|
|
1220
2154
|
return;
|
|
1221
2155
|
}
|
|
1222
2156
|
let index = 1;
|
|
1223
2157
|
while (index < 1e5) {
|
|
1224
2158
|
const candidateName = `New Project (${String(index)})`;
|
|
1225
|
-
const candidatePath =
|
|
2159
|
+
const candidatePath = join2(normalizedBasePath, candidateName);
|
|
1226
2160
|
try {
|
|
1227
|
-
await
|
|
2161
|
+
await stat2(candidatePath);
|
|
1228
2162
|
index += 1;
|
|
1229
2163
|
continue;
|
|
1230
2164
|
} catch {
|
|
1231
|
-
|
|
2165
|
+
setJson2(res, 200, { data: { name: candidateName, path: candidatePath } });
|
|
1232
2166
|
return;
|
|
1233
2167
|
}
|
|
1234
2168
|
}
|
|
1235
|
-
|
|
2169
|
+
setJson2(res, 500, { error: "Failed to compute project name suggestion" });
|
|
1236
2170
|
return;
|
|
1237
2171
|
}
|
|
1238
2172
|
if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
|
|
1239
|
-
const payload =
|
|
2173
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1240
2174
|
const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
|
|
1241
2175
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1242
2176
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
|
|
1243
2177
|
const limit = Math.max(1, Math.min(100, Math.floor(limitRaw)));
|
|
1244
2178
|
if (!rawCwd) {
|
|
1245
|
-
|
|
2179
|
+
setJson2(res, 400, { error: "Missing cwd" });
|
|
1246
2180
|
return;
|
|
1247
2181
|
}
|
|
1248
2182
|
const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
|
|
1249
2183
|
try {
|
|
1250
|
-
const info = await
|
|
2184
|
+
const info = await stat2(cwd);
|
|
1251
2185
|
if (!info.isDirectory()) {
|
|
1252
|
-
|
|
2186
|
+
setJson2(res, 400, { error: "cwd is not a directory" });
|
|
1253
2187
|
return;
|
|
1254
2188
|
}
|
|
1255
2189
|
} catch {
|
|
1256
|
-
|
|
2190
|
+
setJson2(res, 404, { error: "cwd does not exist" });
|
|
1257
2191
|
return;
|
|
1258
2192
|
}
|
|
1259
2193
|
try {
|
|
1260
2194
|
const files = await listFilesWithRipgrep(cwd);
|
|
1261
2195
|
const scored = files.map((path) => ({ path, score: scoreFileCandidate(path, query) })).filter((row) => query.length === 0 || row.score < 10).sort((a, b) => a.score - b.score || a.path.localeCompare(b.path)).slice(0, limit).map((row) => ({ path: row.path }));
|
|
1262
|
-
|
|
2196
|
+
setJson2(res, 200, { data: scored });
|
|
1263
2197
|
} catch (error) {
|
|
1264
|
-
|
|
2198
|
+
setJson2(res, 500, { error: getErrorMessage2(error, "Failed to search files") });
|
|
1265
2199
|
}
|
|
1266
2200
|
return;
|
|
1267
2201
|
}
|
|
1268
2202
|
if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
|
|
1269
2203
|
const cache = await readThreadTitleCache();
|
|
1270
|
-
|
|
2204
|
+
setJson2(res, 200, { data: cache });
|
|
1271
2205
|
return;
|
|
1272
2206
|
}
|
|
1273
2207
|
if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
|
|
1274
|
-
const payload =
|
|
2208
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1275
2209
|
const query = typeof payload?.query === "string" ? payload.query.trim() : "";
|
|
1276
2210
|
const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
|
|
1277
2211
|
const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
|
|
1278
2212
|
if (!query) {
|
|
1279
|
-
|
|
2213
|
+
setJson2(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
|
|
1280
2214
|
return;
|
|
1281
2215
|
}
|
|
1282
2216
|
const index = await getThreadSearchIndex();
|
|
1283
2217
|
const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
|
|
1284
|
-
|
|
2218
|
+
setJson2(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
|
|
1285
2219
|
return;
|
|
1286
2220
|
}
|
|
1287
2221
|
if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
|
|
1288
|
-
const payload =
|
|
2222
|
+
const payload = asRecord2(await readJsonBody(req));
|
|
1289
2223
|
const id = typeof payload?.id === "string" ? payload.id : "";
|
|
1290
2224
|
const title = typeof payload?.title === "string" ? payload.title : "";
|
|
1291
2225
|
if (!id) {
|
|
1292
|
-
|
|
2226
|
+
setJson2(res, 400, { error: "Missing id" });
|
|
1293
2227
|
return;
|
|
1294
2228
|
}
|
|
1295
2229
|
const cache = await readThreadTitleCache();
|
|
1296
2230
|
const next2 = title ? updateThreadTitleCache(cache, id, title) : removeFromThreadTitleCache(cache, id);
|
|
1297
2231
|
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
|
-
}
|
|
2232
|
+
setJson2(res, 200, { ok: true });
|
|
1411
2233
|
return;
|
|
1412
2234
|
}
|
|
1413
2235
|
if (req.method === "GET" && url.pathname === "/codex-api/events") {
|
|
@@ -1442,8 +2264,8 @@ data: ${JSON.stringify({ ok: true })}
|
|
|
1442
2264
|
}
|
|
1443
2265
|
next();
|
|
1444
2266
|
} catch (error) {
|
|
1445
|
-
const message =
|
|
1446
|
-
|
|
2267
|
+
const message = getErrorMessage2(error, "Unknown bridge error");
|
|
2268
|
+
setJson2(res, 502, { error: message });
|
|
1447
2269
|
}
|
|
1448
2270
|
};
|
|
1449
2271
|
middleware.dispose = () => {
|
|
@@ -1580,8 +2402,8 @@ function createAuthSession(password) {
|
|
|
1580
2402
|
}
|
|
1581
2403
|
|
|
1582
2404
|
// src/server/localBrowseUi.ts
|
|
1583
|
-
import { dirname, extname, join as
|
|
1584
|
-
import { open, readFile as
|
|
2405
|
+
import { dirname, extname, join as join3 } from "path";
|
|
2406
|
+
import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
1585
2407
|
var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1586
2408
|
".txt",
|
|
1587
2409
|
".md",
|
|
@@ -1696,7 +2518,7 @@ async function probeFileIsText(localPath) {
|
|
|
1696
2518
|
async function isTextEditableFile(localPath) {
|
|
1697
2519
|
if (isTextEditablePath(localPath)) return true;
|
|
1698
2520
|
try {
|
|
1699
|
-
const fileStat = await
|
|
2521
|
+
const fileStat = await stat3(localPath);
|
|
1700
2522
|
if (!fileStat.isFile()) return false;
|
|
1701
2523
|
return await probeFileIsText(localPath);
|
|
1702
2524
|
} catch {
|
|
@@ -1716,10 +2538,10 @@ function escapeForInlineScriptString(value) {
|
|
|
1716
2538
|
return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
|
|
1717
2539
|
}
|
|
1718
2540
|
async function getDirectoryItems(localPath) {
|
|
1719
|
-
const entries = await
|
|
2541
|
+
const entries = await readdir3(localPath, { withFileTypes: true });
|
|
1720
2542
|
const withMeta = await Promise.all(entries.map(async (entry) => {
|
|
1721
|
-
const entryPath =
|
|
1722
|
-
const entryStat = await
|
|
2543
|
+
const entryPath = join3(localPath, entry.name);
|
|
2544
|
+
const entryStat = await stat3(entryPath);
|
|
1723
2545
|
const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
|
|
1724
2546
|
return {
|
|
1725
2547
|
name: entry.name,
|
|
@@ -1777,7 +2599,7 @@ async function createDirectoryListingHtml(localPath) {
|
|
|
1777
2599
|
</html>`;
|
|
1778
2600
|
}
|
|
1779
2601
|
async function createTextEditorHtml(localPath) {
|
|
1780
|
-
const content = await
|
|
2602
|
+
const content = await readFile3(localPath, "utf8");
|
|
1781
2603
|
const parentPath = dirname(localPath);
|
|
1782
2604
|
const language = languageForPath(localPath);
|
|
1783
2605
|
const safeContentLiteral = escapeForInlineScriptString(content);
|
|
@@ -1848,8 +2670,8 @@ async function createTextEditorHtml(localPath) {
|
|
|
1848
2670
|
// src/server/httpServer.ts
|
|
1849
2671
|
import { WebSocketServer } from "ws";
|
|
1850
2672
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
1851
|
-
var distDir =
|
|
1852
|
-
var spaEntryFile =
|
|
2673
|
+
var distDir = join4(__dirname, "..", "dist");
|
|
2674
|
+
var spaEntryFile = join4(distDir, "index.html");
|
|
1853
2675
|
var IMAGE_CONTENT_TYPES = {
|
|
1854
2676
|
".avif": "image/avif",
|
|
1855
2677
|
".bmp": "image/bmp",
|
|
@@ -1926,7 +2748,7 @@ function createServer(options = {}) {
|
|
|
1926
2748
|
return;
|
|
1927
2749
|
}
|
|
1928
2750
|
try {
|
|
1929
|
-
const fileStat = await
|
|
2751
|
+
const fileStat = await stat4(localPath);
|
|
1930
2752
|
res.setHeader("Cache-Control", "private, no-store");
|
|
1931
2753
|
if (fileStat.isDirectory()) {
|
|
1932
2754
|
const html = await createDirectoryListingHtml(localPath);
|
|
@@ -1949,7 +2771,7 @@ function createServer(options = {}) {
|
|
|
1949
2771
|
return;
|
|
1950
2772
|
}
|
|
1951
2773
|
try {
|
|
1952
|
-
const fileStat = await
|
|
2774
|
+
const fileStat = await stat4(localPath);
|
|
1953
2775
|
if (!fileStat.isFile()) {
|
|
1954
2776
|
res.status(400).json({ error: "Expected file path." });
|
|
1955
2777
|
return;
|
|
@@ -1973,13 +2795,13 @@ function createServer(options = {}) {
|
|
|
1973
2795
|
}
|
|
1974
2796
|
const body = typeof req.body === "string" ? req.body : "";
|
|
1975
2797
|
try {
|
|
1976
|
-
await
|
|
2798
|
+
await writeFile3(localPath, body, "utf8");
|
|
1977
2799
|
res.status(200).json({ ok: true });
|
|
1978
2800
|
} catch {
|
|
1979
2801
|
res.status(404).json({ error: "File not found." });
|
|
1980
2802
|
}
|
|
1981
2803
|
});
|
|
1982
|
-
const hasFrontendAssets =
|
|
2804
|
+
const hasFrontendAssets = existsSync2(spaEntryFile);
|
|
1983
2805
|
if (hasFrontendAssets) {
|
|
1984
2806
|
app.use(express.static(distDir));
|
|
1985
2807
|
}
|
|
@@ -2053,8 +2875,8 @@ var program = new Command().name("codexui").description("Web interface for Codex
|
|
|
2053
2875
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
2054
2876
|
async function readCliVersion() {
|
|
2055
2877
|
try {
|
|
2056
|
-
const packageJsonPath =
|
|
2057
|
-
const raw = await
|
|
2878
|
+
const packageJsonPath = join5(__dirname2, "..", "package.json");
|
|
2879
|
+
const raw = await readFile4(packageJsonPath, "utf8");
|
|
2058
2880
|
const parsed = JSON.parse(raw);
|
|
2059
2881
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
2060
2882
|
} catch {
|
|
@@ -2079,22 +2901,22 @@ function runWithStatus(command, args) {
|
|
|
2079
2901
|
return result.status ?? -1;
|
|
2080
2902
|
}
|
|
2081
2903
|
function getUserNpmPrefix() {
|
|
2082
|
-
return
|
|
2904
|
+
return join5(homedir3(), ".npm-global");
|
|
2083
2905
|
}
|
|
2084
2906
|
function resolveCodexCommand() {
|
|
2085
2907
|
if (canRun("codex", ["--version"])) {
|
|
2086
2908
|
return "codex";
|
|
2087
2909
|
}
|
|
2088
|
-
const userCandidate =
|
|
2089
|
-
if (
|
|
2910
|
+
const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
|
|
2911
|
+
if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
|
|
2090
2912
|
return userCandidate;
|
|
2091
2913
|
}
|
|
2092
2914
|
const prefix = process.env.PREFIX?.trim();
|
|
2093
2915
|
if (!prefix) {
|
|
2094
2916
|
return null;
|
|
2095
2917
|
}
|
|
2096
|
-
const candidate =
|
|
2097
|
-
if (
|
|
2918
|
+
const candidate = join5(prefix, "bin", "codex");
|
|
2919
|
+
if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
|
|
2098
2920
|
return candidate;
|
|
2099
2921
|
}
|
|
2100
2922
|
return null;
|
|
@@ -2103,8 +2925,8 @@ function resolveCloudflaredCommand() {
|
|
|
2103
2925
|
if (canRun("cloudflared", ["--version"])) {
|
|
2104
2926
|
return "cloudflared";
|
|
2105
2927
|
}
|
|
2106
|
-
const localCandidate =
|
|
2107
|
-
if (
|
|
2928
|
+
const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
|
|
2929
|
+
if (existsSync3(localCandidate) && canRun(localCandidate, ["--version"])) {
|
|
2108
2930
|
return localCandidate;
|
|
2109
2931
|
}
|
|
2110
2932
|
return null;
|
|
@@ -2157,9 +2979,9 @@ async function ensureCloudflaredInstalledLinux() {
|
|
|
2157
2979
|
if (!mappedArch) {
|
|
2158
2980
|
throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
|
|
2159
2981
|
}
|
|
2160
|
-
const userBinDir =
|
|
2982
|
+
const userBinDir = join5(homedir3(), ".local", "bin");
|
|
2161
2983
|
mkdirSync(userBinDir, { recursive: true });
|
|
2162
|
-
const destination =
|
|
2984
|
+
const destination = join5(userBinDir, "cloudflared");
|
|
2163
2985
|
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
|
|
2164
2986
|
console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
|
|
2165
2987
|
await downloadFile(downloadUrl, destination);
|
|
@@ -2198,8 +3020,8 @@ async function resolveCloudflaredForTunnel() {
|
|
|
2198
3020
|
return ensureCloudflaredInstalledLinux();
|
|
2199
3021
|
}
|
|
2200
3022
|
function hasCodexAuth() {
|
|
2201
|
-
const codexHome = process.env.CODEX_HOME?.trim() ||
|
|
2202
|
-
return
|
|
3023
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
|
|
3024
|
+
return existsSync3(join5(codexHome, "auth.json"));
|
|
2203
3025
|
}
|
|
2204
3026
|
function ensureCodexInstalled() {
|
|
2205
3027
|
let codexCommand = resolveCodexCommand();
|
|
@@ -2217,7 +3039,7 @@ function ensureCodexInstalled() {
|
|
|
2217
3039
|
Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
|
|
2218
3040
|
`);
|
|
2219
3041
|
runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
|
|
2220
|
-
process.env.PATH = `${
|
|
3042
|
+
process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
|
|
2221
3043
|
};
|
|
2222
3044
|
if (isTermuxRuntime()) {
|
|
2223
3045
|
console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
|
|
@@ -2266,7 +3088,7 @@ function printTermuxKeepAlive(lines) {
|
|
|
2266
3088
|
}
|
|
2267
3089
|
function openBrowser(url) {
|
|
2268
3090
|
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 =
|
|
3091
|
+
const child = spawn3(command.cmd, command.args, { detached: true, stdio: "ignore" });
|
|
2270
3092
|
child.on("error", () => {
|
|
2271
3093
|
});
|
|
2272
3094
|
child.unref();
|
|
@@ -2298,7 +3120,7 @@ function getAccessibleUrls(port) {
|
|
|
2298
3120
|
}
|
|
2299
3121
|
async function startCloudflaredTunnel(command, localPort) {
|
|
2300
3122
|
return new Promise((resolve2, reject) => {
|
|
2301
|
-
const child =
|
|
3123
|
+
const child = spawn3(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
|
|
2302
3124
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2303
3125
|
});
|
|
2304
3126
|
const timeout = setTimeout(() => {
|