codexapp 0.1.44 → 0.1.45

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