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