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