codexapp 0.1.39 → 0.1.40

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