codexapp 0.1.37 → 0.1.39

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