codexapp 0.1.45 → 0.1.46

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,12 +2,12 @@
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";
10
- import { createInterface } from "readline/promises";
5
+ import { chmodSync, createWriteStream, existsSync as existsSync4, mkdirSync } from "fs";
6
+ import { readFile as readFile4, stat as stat5, writeFile as writeFile4 } from "fs/promises";
7
+ import { homedir as homedir3, networkInterfaces } from "os";
8
+ import { isAbsolute as isAbsolute3, join as join5, resolve as resolve2 } from "path";
9
+ import { spawn as spawn3, spawnSync } from "child_process";
10
+ import { createInterface as createInterface2 } from "readline/promises";
11
11
  import { fileURLToPath as fileURLToPath2 } from "url";
12
12
  import { dirname as dirname3 } from "path";
13
13
  import { get as httpsGet } from "https";
@@ -16,19 +16,30 @@ 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 existsSync3 } 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";
25
+ import { spawn as spawn2 } from "child_process";
26
26
  import { randomBytes } from "crypto";
27
- import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
27
+ import { mkdtemp as mkdtemp2, readFile as readFile2, mkdir as mkdir2, stat as stat2 } from "fs/promises";
28
+ import { createReadStream } from "fs";
29
+ import { request as httpRequest } from "http";
28
30
  import { request as httpsRequest } from "https";
29
- import { homedir } from "os";
30
- import { tmpdir } from "os";
31
- import { basename, isAbsolute, join, resolve } from "path";
31
+ import { homedir as homedir2 } from "os";
32
+ import { tmpdir as tmpdir2 } from "os";
33
+ import { basename, isAbsolute, join as join2, resolve } from "path";
34
+ import { createInterface } from "readline";
35
+ import { writeFile as writeFile2 } from "fs/promises";
36
+
37
+ // src/server/skillsRoutes.ts
38
+ import { spawn } from "child_process";
39
+ import { mkdtemp, readFile, readdir, rm, mkdir, stat, lstat, readlink, symlink } from "fs/promises";
40
+ import { existsSync } from "fs";
41
+ import { homedir, tmpdir } from "os";
42
+ import { join } from "path";
32
43
  import { writeFile } from "fs/promises";
33
44
  function asRecord(value) {
34
45
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -52,85 +63,6 @@ function setJson(res, statusCode, payload) {
52
63
  res.setHeader("Content-Type", "application/json; charset=utf-8");
53
64
  res.end(JSON.stringify(payload));
54
65
  }
55
- function extractThreadMessageText(threadReadPayload) {
56
- const payload = asRecord(threadReadPayload);
57
- const thread = asRecord(payload?.thread);
58
- const turns = Array.isArray(thread?.turns) ? thread.turns : [];
59
- const parts = [];
60
- for (const turn of turns) {
61
- const turnRecord = asRecord(turn);
62
- const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
63
- for (const item of items) {
64
- const itemRecord = asRecord(item);
65
- const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
66
- if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
67
- parts.push(itemRecord.text.trim());
68
- continue;
69
- }
70
- if (type === "userMessage") {
71
- const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
72
- for (const block of content) {
73
- const blockRecord = asRecord(block);
74
- if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
75
- parts.push(blockRecord.text.trim());
76
- }
77
- }
78
- continue;
79
- }
80
- if (type === "commandExecution") {
81
- const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
82
- const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
83
- if (command) parts.push(command);
84
- if (output) parts.push(output);
85
- }
86
- }
87
- }
88
- return parts.join("\n").trim();
89
- }
90
- function isExactPhraseMatch(query, doc) {
91
- const q = query.trim().toLowerCase();
92
- if (!q) return false;
93
- return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
94
- }
95
- function scoreFileCandidate(path, query) {
96
- if (!query) return 0;
97
- const lowerPath = path.toLowerCase();
98
- const lowerQuery = query.toLowerCase();
99
- const baseName = lowerPath.slice(lowerPath.lastIndexOf("/") + 1);
100
- if (baseName === lowerQuery) return 0;
101
- if (baseName.startsWith(lowerQuery)) return 1;
102
- if (baseName.includes(lowerQuery)) return 2;
103
- if (lowerPath.includes(`/${lowerQuery}`)) return 3;
104
- if (lowerPath.includes(lowerQuery)) return 4;
105
- return 10;
106
- }
107
- async function listFilesWithRipgrep(cwd) {
108
- return await new Promise((resolve2, reject) => {
109
- const proc = spawn("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
110
- cwd,
111
- env: process.env,
112
- stdio: ["ignore", "pipe", "pipe"]
113
- });
114
- let stdout = "";
115
- let stderr = "";
116
- proc.stdout.on("data", (chunk) => {
117
- stdout += chunk.toString();
118
- });
119
- proc.stderr.on("data", (chunk) => {
120
- stderr += chunk.toString();
121
- });
122
- proc.on("error", reject);
123
- proc.on("close", (code) => {
124
- if (code === 0) {
125
- const rows = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
126
- resolve2(rows);
127
- return;
128
- }
129
- const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
130
- reject(new Error(details || "rg --files failed"));
131
- });
132
- });
133
- }
134
66
  function getCodexHomeDir() {
135
67
  const codexHome = process.env.CODEX_HOME?.trim();
136
68
  return codexHome && codexHome.length > 0 ? codexHome : join(homedir(), ".codex");
@@ -139,7 +71,7 @@ function getSkillsInstallDir() {
139
71
  return join(getCodexHomeDir(), "skills");
140
72
  }
141
73
  async function runCommand(command, args, options = {}) {
142
- await new Promise((resolve2, reject) => {
74
+ await new Promise((resolve3, reject) => {
143
75
  const proc = spawn(command, args, {
144
76
  cwd: options.cwd,
145
77
  env: process.env,
@@ -156,7 +88,7 @@ async function runCommand(command, args, options = {}) {
156
88
  proc.on("error", reject);
157
89
  proc.on("close", (code) => {
158
90
  if (code === 0) {
159
- resolve2();
91
+ resolve3();
160
92
  return;
161
93
  }
162
94
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -165,30 +97,8 @@ async function runCommand(command, args, options = {}) {
165
97
  });
166
98
  });
167
99
  }
168
- function isMissingHeadError(error) {
169
- const message = getErrorMessage(error, "").toLowerCase();
170
- return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
171
- }
172
- function isNotGitRepositoryError(error) {
173
- const message = getErrorMessage(error, "").toLowerCase();
174
- return message.includes("not a git repository") || message.includes("fatal: not a git repository");
175
- }
176
- async function ensureRepoHasInitialCommit(repoRoot) {
177
- const agentsPath = join(repoRoot, "AGENTS.md");
178
- try {
179
- await stat(agentsPath);
180
- } catch {
181
- await writeFile(agentsPath, "", "utf8");
182
- }
183
- await runCommand("git", ["add", "AGENTS.md"], { cwd: repoRoot });
184
- await runCommand(
185
- "git",
186
- ["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
187
- { cwd: repoRoot }
188
- );
189
- }
190
- async function runCommandCapture(command, args, options = {}) {
191
- return await new Promise((resolveOutput, reject) => {
100
+ async function runCommandWithOutput(command, args, options = {}) {
101
+ return await new Promise((resolve3, reject) => {
192
102
  const proc = spawn(command, args, {
193
103
  cwd: options.cwd,
194
104
  env: process.env,
@@ -205,7 +115,7 @@ async function runCommandCapture(command, args, options = {}) {
205
115
  proc.on("error", reject);
206
116
  proc.on("close", (code) => {
207
117
  if (code === 0) {
208
- resolveOutput(stdout.trim());
118
+ resolve3(stdout.trim());
209
119
  return;
210
120
  }
211
121
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -250,9 +160,9 @@ async function getGhToken() {
250
160
  proc.stdout.on("data", (d) => {
251
161
  out += d.toString();
252
162
  });
253
- return new Promise((resolve2) => {
254
- proc.on("close", (code) => resolve2(code === 0 ? out.trim() : null));
255
- proc.on("error", () => resolve2(null));
163
+ return new Promise((resolve3) => {
164
+ proc.on("close", (code) => resolve3(code === 0 ? out.trim() : null));
165
+ proc.on("error", () => resolve3(null));
256
166
  });
257
167
  } catch {
258
168
  return null;
@@ -271,7 +181,7 @@ async function fetchSkillsTree() {
271
181
  if (skillsTreeCache && Date.now() - skillsTreeCache.fetchedAt < TREE_CACHE_TTL_MS) {
272
182
  return skillsTreeCache.entries;
273
183
  }
274
- const resp = await ghFetch("https://api.github.com/repos/openclaw/skills/git/trees/main?recursive=1");
184
+ const resp = await ghFetch(`https://api.github.com/repos/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/git/trees/main?recursive=1`);
275
185
  if (!resp.ok) throw new Error(`GitHub tree API returned ${resp.status}`);
276
186
  const data = await resp.json();
277
187
  const metaPattern = /^skills\/([^/]+)\/([^/]+)\/_meta\.json$/;
@@ -287,86 +197,1109 @@ async function fetchSkillsTree() {
287
197
  entries.push({
288
198
  name: skillName,
289
199
  owner,
290
- url: `https://github.com/openclaw/skills/tree/main/skills/${owner}/${skillName}`
200
+ url: `https://github.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/tree/main/skills/${owner}/${skillName}`
201
+ });
202
+ }
203
+ skillsTreeCache = { entries, fetchedAt: Date.now() };
204
+ return entries;
205
+ }
206
+ async function fetchMetaBatch(entries) {
207
+ const toFetch = entries.filter((e) => !metaCache.has(`${e.owner}/${e.name}`));
208
+ if (toFetch.length === 0) return;
209
+ const batch = toFetch.slice(0, 50);
210
+ await Promise.allSettled(
211
+ batch.map(async (e) => {
212
+ const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${e.owner}/${e.name}/_meta.json`;
213
+ const resp = await fetch(rawUrl);
214
+ if (!resp.ok) return;
215
+ const meta = await resp.json();
216
+ metaCache.set(`${e.owner}/${e.name}`, {
217
+ displayName: typeof meta.displayName === "string" ? meta.displayName : "",
218
+ description: typeof meta.displayName === "string" ? meta.displayName : "",
219
+ publishedAt: meta.latest?.publishedAt ?? 0
220
+ });
221
+ })
222
+ );
223
+ }
224
+ function buildHubEntry(e) {
225
+ const cached = metaCache.get(`${e.owner}/${e.name}`);
226
+ return {
227
+ name: e.name,
228
+ owner: e.owner,
229
+ description: cached?.description ?? "",
230
+ displayName: cached?.displayName ?? "",
231
+ publishedAt: cached?.publishedAt ?? 0,
232
+ avatarUrl: `https://github.com/${e.owner}.png?size=40`,
233
+ url: e.url,
234
+ installed: false
235
+ };
236
+ }
237
+ var GITHUB_DEVICE_CLIENT_ID = "Iv1.b507a08c87ecfe98";
238
+ var DEFAULT_SKILLS_SYNC_REPO_NAME = "codexskills";
239
+ var SKILLS_SYNC_MANIFEST_PATH = "installed-skills.json";
240
+ var SYNC_UPSTREAM_SKILLS_OWNER = "OpenClawAndroid";
241
+ var SYNC_UPSTREAM_SKILLS_REPO = "skills";
242
+ var HUB_SKILLS_OWNER = "openclaw";
243
+ var HUB_SKILLS_REPO = "skills";
244
+ var startupSkillsSyncInitialized = false;
245
+ var startupSyncStatus = {
246
+ inProgress: false,
247
+ mode: "idle",
248
+ branch: getPreferredSyncBranch(),
249
+ lastAction: "not-started",
250
+ lastRunAtIso: "",
251
+ lastSuccessAtIso: "",
252
+ lastError: ""
253
+ };
254
+ async function scanInstalledSkillsFromDisk() {
255
+ const map = /* @__PURE__ */ new Map();
256
+ const skillsDir = getSkillsInstallDir();
257
+ try {
258
+ const entries = await readdir(skillsDir, { withFileTypes: true });
259
+ for (const entry of entries) {
260
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
261
+ const skillMd = join(skillsDir, entry.name, "SKILL.md");
262
+ try {
263
+ await stat(skillMd);
264
+ map.set(entry.name, { name: entry.name, path: skillMd, enabled: true });
265
+ } catch {
266
+ }
267
+ }
268
+ } catch {
269
+ }
270
+ return map;
271
+ }
272
+ function getSkillsSyncStatePath() {
273
+ return join(getCodexHomeDir(), "skills-sync.json");
274
+ }
275
+ async function readSkillsSyncState() {
276
+ try {
277
+ const raw = await readFile(getSkillsSyncStatePath(), "utf8");
278
+ const parsed = JSON.parse(raw);
279
+ return parsed && typeof parsed === "object" ? parsed : {};
280
+ } catch {
281
+ return {};
282
+ }
283
+ }
284
+ async function writeSkillsSyncState(state) {
285
+ await writeFile(getSkillsSyncStatePath(), JSON.stringify(state), "utf8");
286
+ }
287
+ async function getGithubJson(url, token, method = "GET", body) {
288
+ const resp = await fetch(url, {
289
+ method,
290
+ headers: {
291
+ Accept: "application/vnd.github+json",
292
+ "Content-Type": "application/json",
293
+ Authorization: `Bearer ${token}`,
294
+ "X-GitHub-Api-Version": "2022-11-28",
295
+ "User-Agent": "codex-web-local"
296
+ },
297
+ body: body ? JSON.stringify(body) : void 0
298
+ });
299
+ if (!resp.ok) {
300
+ const text = await resp.text();
301
+ throw new Error(`GitHub API ${method} ${url} failed (${resp.status}): ${text}`);
302
+ }
303
+ return await resp.json();
304
+ }
305
+ async function startGithubDeviceLogin() {
306
+ const resp = await fetch("https://github.com/login/device/code", {
307
+ method: "POST",
308
+ headers: {
309
+ Accept: "application/json",
310
+ "Content-Type": "application/x-www-form-urlencoded",
311
+ "User-Agent": "codex-web-local"
312
+ },
313
+ body: new URLSearchParams({
314
+ client_id: GITHUB_DEVICE_CLIENT_ID,
315
+ scope: "repo read:user"
316
+ })
317
+ });
318
+ if (!resp.ok) {
319
+ throw new Error(`GitHub device flow init failed (${resp.status})`);
320
+ }
321
+ return await resp.json();
322
+ }
323
+ async function completeGithubDeviceLogin(deviceCode) {
324
+ const resp = await fetch("https://github.com/login/oauth/access_token", {
325
+ method: "POST",
326
+ headers: {
327
+ Accept: "application/json",
328
+ "Content-Type": "application/x-www-form-urlencoded",
329
+ "User-Agent": "codex-web-local"
330
+ },
331
+ body: new URLSearchParams({
332
+ client_id: GITHUB_DEVICE_CLIENT_ID,
333
+ device_code: deviceCode,
334
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
335
+ })
336
+ });
337
+ if (!resp.ok) {
338
+ throw new Error(`GitHub token exchange failed (${resp.status})`);
339
+ }
340
+ const payload = await resp.json();
341
+ if (!payload.access_token) return { token: null, error: payload.error || "unknown_error" };
342
+ return { token: payload.access_token, error: null };
343
+ }
344
+ function isAndroidLikeRuntime() {
345
+ if (process.platform === "android") return true;
346
+ if (existsSync("/data/data/com.termux")) return true;
347
+ if (process.env.TERMUX_VERSION) return true;
348
+ const prefix = process.env.PREFIX?.toLowerCase() ?? "";
349
+ if (prefix.includes("/com.termux/")) return true;
350
+ const proot = process.env.PROOT_TMP_DIR?.toLowerCase() ?? "";
351
+ return proot.length > 0;
352
+ }
353
+ function getPreferredSyncBranch() {
354
+ return isAndroidLikeRuntime() ? "android" : "main";
355
+ }
356
+ function isUpstreamSkillsRepo(repoOwner, repoName) {
357
+ return repoOwner.toLowerCase() === SYNC_UPSTREAM_SKILLS_OWNER.toLowerCase() && repoName.toLowerCase() === SYNC_UPSTREAM_SKILLS_REPO.toLowerCase();
358
+ }
359
+ async function resolveGithubUsername(token) {
360
+ const user = await getGithubJson("https://api.github.com/user", token);
361
+ return user.login;
362
+ }
363
+ async function ensurePrivateForkFromUpstream(token, username, repoName) {
364
+ const repoUrl = `https://api.github.com/repos/${username}/${repoName}`;
365
+ let created = false;
366
+ const existing = await fetch(repoUrl, {
367
+ headers: {
368
+ Accept: "application/vnd.github+json",
369
+ Authorization: `Bearer ${token}`,
370
+ "X-GitHub-Api-Version": "2022-11-28",
371
+ "User-Agent": "codex-web-local"
372
+ }
373
+ });
374
+ if (existing.ok) {
375
+ const details = await existing.json();
376
+ if (details.private === true) return;
377
+ await getGithubJson(repoUrl, token, "PATCH", { private: true });
378
+ return;
379
+ }
380
+ if (existing.status !== 404) {
381
+ throw new Error(`Failed to check personal repo existence (${existing.status})`);
382
+ }
383
+ await getGithubJson(
384
+ "https://api.github.com/user/repos",
385
+ token,
386
+ "POST",
387
+ { name: repoName, private: true, auto_init: false, description: "Codex skills private mirror sync" }
388
+ );
389
+ created = true;
390
+ let ready = false;
391
+ for (let i = 0; i < 20; i++) {
392
+ const check = await fetch(repoUrl, {
393
+ headers: {
394
+ Accept: "application/vnd.github+json",
395
+ Authorization: `Bearer ${token}`,
396
+ "X-GitHub-Api-Version": "2022-11-28",
397
+ "User-Agent": "codex-web-local"
398
+ }
399
+ });
400
+ if (check.ok) {
401
+ ready = true;
402
+ break;
403
+ }
404
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
405
+ }
406
+ if (!ready) throw new Error("Private mirror repo was created but is not available yet");
407
+ if (!created) return;
408
+ const tmp = await mkdtemp(join(tmpdir(), "codex-skills-seed-"));
409
+ try {
410
+ const upstreamUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
411
+ const branch = getPreferredSyncBranch();
412
+ try {
413
+ await runCommand("git", ["clone", "--depth", "1", "--single-branch", "--branch", branch, upstreamUrl, tmp]);
414
+ } catch {
415
+ await runCommand("git", ["clone", "--depth", "1", upstreamUrl, tmp]);
416
+ }
417
+ const privateRemote = toGitHubTokenRemote(username, repoName, token);
418
+ await runCommand("git", ["remote", "set-url", "origin", privateRemote], { cwd: tmp });
419
+ try {
420
+ await runCommand("git", ["checkout", "-B", branch], { cwd: tmp });
421
+ } catch {
422
+ }
423
+ await runCommand("git", ["push", "-u", "origin", `HEAD:${branch}`], { cwd: tmp });
424
+ } finally {
425
+ await rm(tmp, { recursive: true, force: true });
426
+ }
427
+ }
428
+ async function readRemoteSkillsManifest(token, repoOwner, repoName) {
429
+ const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
430
+ const resp = await fetch(url, {
431
+ headers: {
432
+ Accept: "application/vnd.github+json",
433
+ Authorization: `Bearer ${token}`,
434
+ "X-GitHub-Api-Version": "2022-11-28",
435
+ "User-Agent": "codex-web-local"
436
+ }
437
+ });
438
+ if (resp.status === 404) return [];
439
+ if (!resp.ok) throw new Error(`Failed to read remote manifest (${resp.status})`);
440
+ const payload = await resp.json();
441
+ const content = payload.content ? Buffer.from(payload.content.replace(/\n/g, ""), "base64").toString("utf8") : "[]";
442
+ const parsed = JSON.parse(content);
443
+ if (!Array.isArray(parsed)) return [];
444
+ const skills = [];
445
+ for (const row of parsed) {
446
+ const item = asRecord(row);
447
+ const owner = typeof item?.owner === "string" ? item.owner : "";
448
+ const name = typeof item?.name === "string" ? item.name : "";
449
+ if (!name) continue;
450
+ skills.push({ ...owner ? { owner } : {}, name, enabled: item?.enabled !== false });
451
+ }
452
+ return skills;
453
+ }
454
+ async function writeRemoteSkillsManifest(token, repoOwner, repoName, skills) {
455
+ const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
456
+ let sha = "";
457
+ const existing = await fetch(url, {
458
+ headers: {
459
+ Accept: "application/vnd.github+json",
460
+ Authorization: `Bearer ${token}`,
461
+ "X-GitHub-Api-Version": "2022-11-28",
462
+ "User-Agent": "codex-web-local"
463
+ }
464
+ });
465
+ if (existing.ok) {
466
+ const payload = await existing.json();
467
+ sha = payload.sha ?? "";
468
+ }
469
+ const content = Buffer.from(JSON.stringify(skills, null, 2), "utf8").toString("base64");
470
+ await getGithubJson(url, token, "PUT", {
471
+ message: "Update synced skills manifest",
472
+ content,
473
+ ...sha ? { sha } : {}
474
+ });
475
+ }
476
+ function toGitHubTokenRemote(repoOwner, repoName, token) {
477
+ return `https://x-access-token:${encodeURIComponent(token)}@github.com/${repoOwner}/${repoName}.git`;
478
+ }
479
+ async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
480
+ const localDir = getSkillsInstallDir();
481
+ await mkdir(localDir, { recursive: true });
482
+ const gitDir = join(localDir, ".git");
483
+ let hasGitDir = false;
484
+ try {
485
+ hasGitDir = (await stat(gitDir)).isDirectory();
486
+ } catch {
487
+ hasGitDir = false;
488
+ }
489
+ if (!hasGitDir) {
490
+ await runCommand("git", ["init"], { cwd: localDir });
491
+ await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: localDir });
492
+ await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: localDir });
493
+ await runCommand("git", ["add", "-A"], { cwd: localDir });
494
+ try {
495
+ await runCommand("git", ["commit", "-m", "Local skills snapshot before sync"], { cwd: localDir });
496
+ } catch {
497
+ }
498
+ await runCommand("git", ["branch", "-M", branch], { cwd: localDir });
499
+ try {
500
+ await runCommand("git", ["remote", "add", "origin", repoUrl], { cwd: localDir });
501
+ } catch {
502
+ await runCommand("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
503
+ }
504
+ await runCommand("git", ["fetch", "origin"], { cwd: localDir });
505
+ try {
506
+ await runCommand("git", ["merge", "--allow-unrelated-histories", "--no-edit", `origin/${branch}`], { cwd: localDir });
507
+ } catch {
508
+ }
509
+ return localDir;
510
+ }
511
+ await runCommand("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
512
+ await runCommand("git", ["fetch", "origin"], { cwd: localDir });
513
+ await resolveMergeConflictsByNewerCommit(localDir, branch);
514
+ try {
515
+ await runCommand("git", ["checkout", branch], { cwd: localDir });
516
+ } catch {
517
+ await resolveMergeConflictsByNewerCommit(localDir, branch);
518
+ await runCommand("git", ["checkout", "-B", branch], { cwd: localDir });
519
+ }
520
+ await resolveMergeConflictsByNewerCommit(localDir, branch);
521
+ const localMtimesBeforePull = await snapshotFileMtimes(localDir);
522
+ try {
523
+ await runCommand("git", ["stash", "push", "--include-untracked", "-m", "codex-skills-autostash"], { cwd: localDir });
524
+ } catch {
525
+ }
526
+ let pulledMtimes = /* @__PURE__ */ new Map();
527
+ try {
528
+ await runCommand("git", ["pull", "--no-rebase", "origin", branch], { cwd: localDir });
529
+ pulledMtimes = await snapshotFileMtimes(localDir);
530
+ } catch {
531
+ await resolveMergeConflictsByNewerCommit(localDir, branch);
532
+ pulledMtimes = await snapshotFileMtimes(localDir);
533
+ }
534
+ try {
535
+ await runCommand("git", ["stash", "pop"], { cwd: localDir });
536
+ } catch {
537
+ await resolveStashPopConflictsByFileTime(localDir, localMtimesBeforePull, pulledMtimes);
538
+ }
539
+ return localDir;
540
+ }
541
+ async function resolveMergeConflictsByNewerCommit(repoDir, branch) {
542
+ const unmerged = (await runCommandWithOutput("git", ["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir })).split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
543
+ if (unmerged.length === 0) return;
544
+ for (const path of unmerged) {
545
+ const oursTime = await getCommitTime(repoDir, "HEAD", path);
546
+ const theirsTime = await getCommitTime(repoDir, `origin/${branch}`, path);
547
+ if (theirsTime > oursTime) {
548
+ await runCommand("git", ["checkout", "--theirs", "--", path], { cwd: repoDir });
549
+ } else {
550
+ await runCommand("git", ["checkout", "--ours", "--", path], { cwd: repoDir });
551
+ }
552
+ await runCommand("git", ["add", "--", path], { cwd: repoDir });
553
+ }
554
+ const mergeHead = (await runCommandWithOutput("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], { cwd: repoDir })).trim();
555
+ if (mergeHead) {
556
+ await runCommand("git", ["commit", "-m", "Auto-resolve skills merge by newer file"], { cwd: repoDir });
557
+ }
558
+ }
559
+ async function getCommitTime(repoDir, ref, path) {
560
+ try {
561
+ const output = (await runCommandWithOutput("git", ["log", "-1", "--format=%ct", ref, "--", path], { cwd: repoDir })).trim();
562
+ return output ? Number.parseInt(output, 10) : 0;
563
+ } catch {
564
+ return 0;
565
+ }
566
+ }
567
+ async function resolveStashPopConflictsByFileTime(repoDir, localMtimesBeforePull, pulledMtimes) {
568
+ const unmerged = (await runCommandWithOutput("git", ["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir })).split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
569
+ if (unmerged.length === 0) return;
570
+ for (const path of unmerged) {
571
+ const localMtime = localMtimesBeforePull.get(path) ?? 0;
572
+ const pulledMtime = pulledMtimes.get(path) ?? 0;
573
+ const side = localMtime >= pulledMtime ? "--theirs" : "--ours";
574
+ await runCommand("git", ["checkout", side, "--", path], { cwd: repoDir });
575
+ await runCommand("git", ["add", "--", path], { cwd: repoDir });
576
+ }
577
+ const mergeHead = (await runCommandWithOutput("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], { cwd: repoDir })).trim();
578
+ if (mergeHead) {
579
+ await runCommand("git", ["commit", "-m", "Auto-resolve stash-pop conflicts by file time"], { cwd: repoDir });
580
+ }
581
+ }
582
+ async function snapshotFileMtimes(dir) {
583
+ const mtimes = /* @__PURE__ */ new Map();
584
+ await walkFileMtimes(dir, dir, mtimes);
585
+ return mtimes;
586
+ }
587
+ async function walkFileMtimes(rootDir, currentDir, out) {
588
+ let entries;
589
+ try {
590
+ entries = await readdir(currentDir, { withFileTypes: true });
591
+ } catch {
592
+ return;
593
+ }
594
+ for (const entry of entries) {
595
+ const entryName = String(entry.name);
596
+ if (entryName === ".git") continue;
597
+ const absolutePath = join(currentDir, entryName);
598
+ const relativePath = absolutePath.slice(rootDir.length + 1);
599
+ if (entry.isDirectory()) {
600
+ await walkFileMtimes(rootDir, absolutePath, out);
601
+ continue;
602
+ }
603
+ if (!entry.isFile()) continue;
604
+ try {
605
+ const info = await stat(absolutePath);
606
+ out.set(relativePath, info.mtimeMs);
607
+ } catch {
608
+ }
609
+ }
610
+ }
611
+ async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _installedMap) {
612
+ const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
613
+ const branch = getPreferredSyncBranch();
614
+ const repoDir = await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
615
+ void _installedMap;
616
+ await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: repoDir });
617
+ await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: repoDir });
618
+ await runCommand("git", ["add", "."], { cwd: repoDir });
619
+ const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
620
+ if (!status) return;
621
+ await runCommand("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
622
+ await runCommand("git", ["push", "origin", `HEAD:${branch}`], { cwd: repoDir });
623
+ }
624
+ async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName) {
625
+ const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
626
+ const branch = getPreferredSyncBranch();
627
+ await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
628
+ }
629
+ async function bootstrapSkillsFromUpstreamIntoLocal() {
630
+ const repoUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
631
+ const branch = getPreferredSyncBranch();
632
+ await ensureSkillsWorkingTreeRepo(repoUrl, branch);
633
+ }
634
+ async function collectLocalSyncedSkills(appServer) {
635
+ const state = await readSkillsSyncState();
636
+ const owners = { ...state.installedOwners ?? {} };
637
+ const tree = await fetchSkillsTree();
638
+ const uniqueOwnerByName = /* @__PURE__ */ new Map();
639
+ const ambiguousNames = /* @__PURE__ */ new Set();
640
+ for (const entry of tree) {
641
+ if (ambiguousNames.has(entry.name)) continue;
642
+ const existingOwner = uniqueOwnerByName.get(entry.name);
643
+ if (!existingOwner) {
644
+ uniqueOwnerByName.set(entry.name, entry.owner);
645
+ continue;
646
+ }
647
+ if (existingOwner !== entry.owner) {
648
+ uniqueOwnerByName.delete(entry.name);
649
+ ambiguousNames.add(entry.name);
650
+ }
651
+ }
652
+ const skills = await appServer.rpc("skills/list", {});
653
+ const seen = /* @__PURE__ */ new Set();
654
+ const synced = [];
655
+ let ownersChanged = false;
656
+ for (const entry of skills.data ?? []) {
657
+ for (const skill of entry.skills ?? []) {
658
+ const name = typeof skill.name === "string" ? skill.name : "";
659
+ if (!name || seen.has(name)) continue;
660
+ seen.add(name);
661
+ let owner = owners[name];
662
+ if (!owner) {
663
+ owner = uniqueOwnerByName.get(name) ?? "";
664
+ if (owner) {
665
+ owners[name] = owner;
666
+ ownersChanged = true;
667
+ }
668
+ }
669
+ synced.push({ ...owner ? { owner } : {}, name, enabled: skill.enabled !== false });
670
+ }
671
+ }
672
+ if (ownersChanged) {
673
+ await writeSkillsSyncState({ ...state, installedOwners: owners });
674
+ }
675
+ synced.sort((a, b) => `${a.owner ?? ""}/${a.name}`.localeCompare(`${b.owner ?? ""}/${b.name}`));
676
+ return synced;
677
+ }
678
+ async function autoPushSyncedSkills(appServer) {
679
+ const state = await readSkillsSyncState();
680
+ if (!state.githubToken || !state.repoOwner || !state.repoName) return;
681
+ if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
682
+ throw new Error("Refusing to push to upstream skills repository");
683
+ }
684
+ const local = await collectLocalSyncedSkills(appServer);
685
+ const installedMap = await scanInstalledSkillsFromDisk();
686
+ await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
687
+ await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
688
+ }
689
+ async function ensureCodexAgentsSymlinkToSkillsAgents() {
690
+ const codexHomeDir = getCodexHomeDir();
691
+ const skillsAgentsPath = join(codexHomeDir, "skills", "AGENTS.md");
692
+ const codexAgentsPath = join(codexHomeDir, "AGENTS.md");
693
+ await mkdir(join(codexHomeDir, "skills"), { recursive: true });
694
+ let copiedFromCodex = false;
695
+ try {
696
+ const codexAgentsStat = await lstat(codexAgentsPath);
697
+ if (codexAgentsStat.isFile() || codexAgentsStat.isSymbolicLink()) {
698
+ const content = await readFile(codexAgentsPath, "utf8");
699
+ await writeFile(skillsAgentsPath, content, "utf8");
700
+ copiedFromCodex = true;
701
+ } else {
702
+ await rm(codexAgentsPath, { force: true, recursive: true });
703
+ }
704
+ } catch {
705
+ }
706
+ if (!copiedFromCodex) {
707
+ try {
708
+ const skillsAgentsStat = await stat(skillsAgentsPath);
709
+ if (!skillsAgentsStat.isFile()) {
710
+ await rm(skillsAgentsPath, { force: true, recursive: true });
711
+ await writeFile(skillsAgentsPath, "", "utf8");
712
+ }
713
+ } catch {
714
+ await writeFile(skillsAgentsPath, "", "utf8");
715
+ }
716
+ }
717
+ const relativeTarget = join("skills", "AGENTS.md");
718
+ try {
719
+ const current = await lstat(codexAgentsPath);
720
+ if (current.isSymbolicLink()) {
721
+ const existingTarget = await readlink(codexAgentsPath);
722
+ if (existingTarget === relativeTarget) return;
723
+ }
724
+ await rm(codexAgentsPath, { force: true, recursive: true });
725
+ } catch {
726
+ }
727
+ await symlink(relativeTarget, codexAgentsPath);
728
+ }
729
+ async function initializeSkillsSyncOnStartup(appServer) {
730
+ if (startupSkillsSyncInitialized) return;
731
+ startupSkillsSyncInitialized = true;
732
+ startupSyncStatus.inProgress = true;
733
+ startupSyncStatus.lastRunAtIso = (/* @__PURE__ */ new Date()).toISOString();
734
+ startupSyncStatus.lastError = "";
735
+ startupSyncStatus.branch = getPreferredSyncBranch();
736
+ try {
737
+ const state = await readSkillsSyncState();
738
+ if (!state.githubToken) {
739
+ await ensureCodexAgentsSymlinkToSkillsAgents();
740
+ if (!isAndroidLikeRuntime()) {
741
+ startupSyncStatus.mode = "idle";
742
+ startupSyncStatus.lastAction = "skip-upstream-non-android";
743
+ startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
744
+ return;
745
+ }
746
+ startupSyncStatus.mode = "unauthenticated-bootstrap";
747
+ startupSyncStatus.lastAction = "pull-upstream";
748
+ await bootstrapSkillsFromUpstreamIntoLocal();
749
+ try {
750
+ await appServer.rpc("skills/list", { forceReload: true });
751
+ } catch {
752
+ }
753
+ startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
754
+ startupSyncStatus.lastAction = "pull-upstream-complete";
755
+ return;
756
+ }
757
+ startupSyncStatus.mode = "authenticated-fork-sync";
758
+ startupSyncStatus.lastAction = "ensure-private-fork";
759
+ const username = state.githubUsername || await resolveGithubUsername(state.githubToken);
760
+ const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
761
+ await ensurePrivateForkFromUpstream(state.githubToken, username, repoName);
762
+ await writeSkillsSyncState({ ...state, githubUsername: username, repoOwner: username, repoName });
763
+ startupSyncStatus.lastAction = "pull-private-fork";
764
+ await pullInstalledSkillsFolderFromRepo(state.githubToken, username, repoName);
765
+ try {
766
+ await appServer.rpc("skills/list", { forceReload: true });
767
+ } catch {
768
+ }
769
+ startupSyncStatus.lastAction = "push-private-fork";
770
+ await autoPushSyncedSkills(appServer);
771
+ startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
772
+ startupSyncStatus.lastAction = "startup-sync-complete";
773
+ } catch (error) {
774
+ startupSyncStatus.lastError = getErrorMessage(error, "startup-sync-failed");
775
+ startupSyncStatus.lastAction = "startup-sync-failed";
776
+ } finally {
777
+ startupSyncStatus.inProgress = false;
778
+ }
779
+ }
780
+ async function finalizeGithubLoginAndSync(token, username, appServer) {
781
+ const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
782
+ await ensurePrivateForkFromUpstream(token, username, repoName);
783
+ const current = await readSkillsSyncState();
784
+ await writeSkillsSyncState({ ...current, githubToken: token, githubUsername: username, repoOwner: username, repoName });
785
+ await pullInstalledSkillsFolderFromRepo(token, username, repoName);
786
+ try {
787
+ await appServer.rpc("skills/list", { forceReload: true });
788
+ } catch {
789
+ }
790
+ await autoPushSyncedSkills(appServer);
791
+ }
792
+ async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
793
+ const q = query.toLowerCase().trim();
794
+ const filtered = q ? allEntries.filter((s) => {
795
+ if (s.name.toLowerCase().includes(q) || s.owner.toLowerCase().includes(q)) return true;
796
+ const cached = metaCache.get(`${s.owner}/${s.name}`);
797
+ return Boolean(cached?.displayName?.toLowerCase().includes(q));
798
+ }) : allEntries;
799
+ const page = filtered.slice(0, Math.min(limit * 2, 200));
800
+ await fetchMetaBatch(page);
801
+ let results = page.map(buildHubEntry);
802
+ if (sort === "date") {
803
+ results.sort((a, b) => b.publishedAt - a.publishedAt);
804
+ } else if (q) {
805
+ results.sort((a, b) => {
806
+ const aExact = a.name.toLowerCase() === q ? 1 : 0;
807
+ const bExact = b.name.toLowerCase() === q ? 1 : 0;
808
+ if (aExact !== bExact) return bExact - aExact;
809
+ return b.publishedAt - a.publishedAt;
810
+ });
811
+ }
812
+ return results.slice(0, limit).map((s) => {
813
+ const local = installedMap.get(s.name);
814
+ return local ? { ...s, installed: true, path: local.path, enabled: local.enabled } : s;
815
+ });
816
+ }
817
+ async function handleSkillsRoutes(req, res, url, context) {
818
+ const { appServer, readJsonBody: readJsonBody2 } = context;
819
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
820
+ try {
821
+ const q = url.searchParams.get("q") || "";
822
+ const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
823
+ const sort = url.searchParams.get("sort") || "date";
824
+ const allEntries = await fetchSkillsTree();
825
+ const installedMap = await scanInstalledSkillsFromDisk();
826
+ try {
827
+ const result = await appServer.rpc("skills/list", {});
828
+ for (const entry of result.data ?? []) {
829
+ for (const skill of entry.skills ?? []) {
830
+ if (skill.name) {
831
+ installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
832
+ }
833
+ }
834
+ }
835
+ } catch {
836
+ }
837
+ const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
838
+ await fetchMetaBatch(installedHubEntries);
839
+ const installed = [];
840
+ for (const [, info] of installedMap) {
841
+ const hubEntry = allEntries.find((e) => e.name === info.name);
842
+ const base = hubEntry ? buildHubEntry(hubEntry) : {
843
+ name: info.name,
844
+ owner: "local",
845
+ description: "",
846
+ displayName: "",
847
+ publishedAt: 0,
848
+ avatarUrl: "",
849
+ url: "",
850
+ installed: false
851
+ };
852
+ installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
853
+ }
854
+ const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
855
+ setJson(res, 200, { data: results, installed, total: allEntries.length });
856
+ } catch (error) {
857
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch skills hub") });
858
+ }
859
+ return true;
860
+ }
861
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
862
+ const state = await readSkillsSyncState();
863
+ setJson(res, 200, {
864
+ data: {
865
+ loggedIn: Boolean(state.githubToken),
866
+ githubUsername: state.githubUsername ?? "",
867
+ repoOwner: state.repoOwner ?? "",
868
+ repoName: state.repoName ?? "",
869
+ configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
870
+ startup: {
871
+ inProgress: startupSyncStatus.inProgress,
872
+ mode: startupSyncStatus.mode,
873
+ branch: startupSyncStatus.branch,
874
+ lastAction: startupSyncStatus.lastAction,
875
+ lastRunAtIso: startupSyncStatus.lastRunAtIso,
876
+ lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
877
+ lastError: startupSyncStatus.lastError
878
+ }
879
+ }
880
+ });
881
+ return true;
882
+ }
883
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
884
+ try {
885
+ const started = await startGithubDeviceLogin();
886
+ setJson(res, 200, { data: started });
887
+ } catch (error) {
888
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to start GitHub login") });
889
+ }
890
+ return true;
891
+ }
892
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
893
+ try {
894
+ const payload = asRecord(await readJsonBody2(req));
895
+ const token = typeof payload?.token === "string" ? payload.token.trim() : "";
896
+ if (!token) {
897
+ setJson(res, 400, { error: "Missing GitHub token" });
898
+ return true;
899
+ }
900
+ const username = await resolveGithubUsername(token);
901
+ await finalizeGithubLoginAndSync(token, username, appServer);
902
+ setJson(res, 200, { ok: true, data: { githubUsername: username } });
903
+ } catch (error) {
904
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to login with GitHub token") });
905
+ }
906
+ return true;
907
+ }
908
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
909
+ try {
910
+ const state = await readSkillsSyncState();
911
+ await writeSkillsSyncState({
912
+ ...state,
913
+ githubToken: void 0,
914
+ githubUsername: void 0,
915
+ repoOwner: void 0,
916
+ repoName: void 0
917
+ });
918
+ setJson(res, 200, { ok: true });
919
+ } catch (error) {
920
+ setJson(res, 500, { error: getErrorMessage(error, "Failed to logout GitHub") });
921
+ }
922
+ return true;
923
+ }
924
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
925
+ try {
926
+ const payload = asRecord(await readJsonBody2(req));
927
+ const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
928
+ if (!deviceCode) {
929
+ setJson(res, 400, { error: "Missing deviceCode" });
930
+ return true;
931
+ }
932
+ const result = await completeGithubDeviceLogin(deviceCode);
933
+ if (!result.token) {
934
+ setJson(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
935
+ return true;
936
+ }
937
+ const token = result.token;
938
+ const username = await resolveGithubUsername(token);
939
+ await finalizeGithubLoginAndSync(token, username, appServer);
940
+ setJson(res, 200, { ok: true, data: { githubUsername: username } });
941
+ } catch (error) {
942
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to complete GitHub login") });
943
+ }
944
+ return true;
945
+ }
946
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
947
+ try {
948
+ const state = await readSkillsSyncState();
949
+ if (!state.githubToken || !state.repoOwner || !state.repoName) {
950
+ setJson(res, 400, { error: "Skills sync is not configured yet" });
951
+ return true;
952
+ }
953
+ if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
954
+ setJson(res, 400, { error: "Refusing to push to upstream repository" });
955
+ return true;
956
+ }
957
+ const local = await collectLocalSyncedSkills(appServer);
958
+ const installedMap = await scanInstalledSkillsFromDisk();
959
+ await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
960
+ await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
961
+ setJson(res, 200, { ok: true, data: { synced: local.length } });
962
+ } catch (error) {
963
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to push synced skills") });
964
+ }
965
+ return true;
966
+ }
967
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
968
+ try {
969
+ const state = await readSkillsSyncState();
970
+ if (!state.githubToken || !state.repoOwner || !state.repoName) {
971
+ await bootstrapSkillsFromUpstreamIntoLocal();
972
+ try {
973
+ await appServer.rpc("skills/list", { forceReload: true });
974
+ } catch {
975
+ }
976
+ setJson(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
977
+ return true;
978
+ }
979
+ const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
980
+ const tree = await fetchSkillsTree();
981
+ const uniqueOwnerByName = /* @__PURE__ */ new Map();
982
+ const ambiguousNames = /* @__PURE__ */ new Set();
983
+ for (const entry of tree) {
984
+ if (ambiguousNames.has(entry.name)) continue;
985
+ const existingOwner = uniqueOwnerByName.get(entry.name);
986
+ if (!existingOwner) {
987
+ uniqueOwnerByName.set(entry.name, entry.owner);
988
+ continue;
989
+ }
990
+ if (existingOwner !== entry.owner) {
991
+ uniqueOwnerByName.delete(entry.name);
992
+ ambiguousNames.add(entry.name);
993
+ }
994
+ }
995
+ const localDir = await detectUserSkillsDir(appServer);
996
+ await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
997
+ const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
998
+ const localSkills = await scanInstalledSkillsFromDisk();
999
+ for (const skill of remote) {
1000
+ const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
1001
+ if (!owner) continue;
1002
+ if (!localSkills.has(skill.name)) {
1003
+ await runCommand("python3", [
1004
+ installerScript,
1005
+ "--repo",
1006
+ `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
1007
+ "--path",
1008
+ `skills/${owner}/${skill.name}`,
1009
+ "--dest",
1010
+ localDir,
1011
+ "--method",
1012
+ "git"
1013
+ ]);
1014
+ }
1015
+ const skillPath = join(localDir, skill.name);
1016
+ await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
1017
+ }
1018
+ const remoteNames = new Set(remote.map((row) => row.name));
1019
+ for (const [name, localInfo] of localSkills.entries()) {
1020
+ if (!remoteNames.has(name)) {
1021
+ await rm(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
1022
+ }
1023
+ }
1024
+ const nextOwners = {};
1025
+ for (const item of remote) {
1026
+ const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
1027
+ if (owner) nextOwners[item.name] = owner;
1028
+ }
1029
+ await writeSkillsSyncState({ ...state, installedOwners: nextOwners });
1030
+ try {
1031
+ await appServer.rpc("skills/list", { forceReload: true });
1032
+ } catch {
1033
+ }
1034
+ setJson(res, 200, { ok: true, data: { synced: remote.length } });
1035
+ } catch (error) {
1036
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to pull synced skills") });
1037
+ }
1038
+ return true;
1039
+ }
1040
+ if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
1041
+ try {
1042
+ const owner = url.searchParams.get("owner") || "";
1043
+ const name = url.searchParams.get("name") || "";
1044
+ if (!owner || !name) {
1045
+ setJson(res, 400, { error: "Missing owner or name" });
1046
+ return true;
1047
+ }
1048
+ const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
1049
+ const resp = await fetch(rawUrl);
1050
+ if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
1051
+ const content = await resp.text();
1052
+ setJson(res, 200, { content });
1053
+ } catch (error) {
1054
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to fetch SKILL.md") });
1055
+ }
1056
+ return true;
1057
+ }
1058
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
1059
+ try {
1060
+ const payload = asRecord(await readJsonBody2(req));
1061
+ const owner = typeof payload?.owner === "string" ? payload.owner : "";
1062
+ const name = typeof payload?.name === "string" ? payload.name : "";
1063
+ if (!owner || !name) {
1064
+ setJson(res, 400, { error: "Missing owner or name" });
1065
+ return true;
1066
+ }
1067
+ const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
1068
+ const installDest = await detectUserSkillsDir(appServer);
1069
+ await runCommand("python3", [
1070
+ installerScript,
1071
+ "--repo",
1072
+ `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
1073
+ "--path",
1074
+ `skills/${owner}/${name}`,
1075
+ "--dest",
1076
+ installDest,
1077
+ "--method",
1078
+ "git"
1079
+ ]);
1080
+ const skillDir = join(installDest, name);
1081
+ await ensureInstalledSkillIsValid(appServer, skillDir);
1082
+ const syncState = await readSkillsSyncState();
1083
+ const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
1084
+ await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1085
+ await autoPushSyncedSkills(appServer);
1086
+ setJson(res, 200, { ok: true, path: skillDir });
1087
+ } catch (error) {
1088
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
1089
+ }
1090
+ return true;
1091
+ }
1092
+ if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
1093
+ try {
1094
+ const payload = asRecord(await readJsonBody2(req));
1095
+ const name = typeof payload?.name === "string" ? payload.name : "";
1096
+ const path = typeof payload?.path === "string" ? payload.path : "";
1097
+ const target = path || (name ? join(getSkillsInstallDir(), name) : "");
1098
+ if (!target) {
1099
+ setJson(res, 400, { error: "Missing name or path" });
1100
+ return true;
1101
+ }
1102
+ await rm(target, { recursive: true, force: true });
1103
+ if (name) {
1104
+ const syncState = await readSkillsSyncState();
1105
+ const nextOwners = { ...syncState.installedOwners ?? {} };
1106
+ delete nextOwners[name];
1107
+ await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1108
+ }
1109
+ await autoPushSyncedSkills(appServer);
1110
+ try {
1111
+ await appServer.rpc("skills/list", { forceReload: true });
1112
+ } catch {
1113
+ }
1114
+ setJson(res, 200, { ok: true, deletedPath: target });
1115
+ } catch (error) {
1116
+ setJson(res, 502, { error: getErrorMessage(error, "Failed to uninstall skill") });
1117
+ }
1118
+ return true;
1119
+ }
1120
+ return false;
1121
+ }
1122
+
1123
+ // src/server/codexAppServerBridge.ts
1124
+ function asRecord2(value) {
1125
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
1126
+ }
1127
+ function getErrorMessage2(payload, fallback) {
1128
+ if (payload instanceof Error && payload.message.trim().length > 0) {
1129
+ return payload.message;
1130
+ }
1131
+ const record = asRecord2(payload);
1132
+ if (!record) return fallback;
1133
+ const error = record.error;
1134
+ if (typeof error === "string" && error.length > 0) return error;
1135
+ const nestedError = asRecord2(error);
1136
+ if (nestedError && typeof nestedError.message === "string" && nestedError.message.length > 0) {
1137
+ return nestedError.message;
1138
+ }
1139
+ return fallback;
1140
+ }
1141
+ function setJson2(res, statusCode, payload) {
1142
+ res.statusCode = statusCode;
1143
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
1144
+ res.end(JSON.stringify(payload));
1145
+ }
1146
+ function extractThreadMessageText(threadReadPayload) {
1147
+ const payload = asRecord2(threadReadPayload);
1148
+ const thread = asRecord2(payload?.thread);
1149
+ const turns = Array.isArray(thread?.turns) ? thread.turns : [];
1150
+ const parts = [];
1151
+ for (const turn of turns) {
1152
+ const turnRecord = asRecord2(turn);
1153
+ const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
1154
+ for (const item of items) {
1155
+ const itemRecord = asRecord2(item);
1156
+ const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
1157
+ if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
1158
+ parts.push(itemRecord.text.trim());
1159
+ continue;
1160
+ }
1161
+ if (type === "userMessage") {
1162
+ const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
1163
+ for (const block of content) {
1164
+ const blockRecord = asRecord2(block);
1165
+ if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
1166
+ parts.push(blockRecord.text.trim());
1167
+ }
1168
+ }
1169
+ continue;
1170
+ }
1171
+ if (type === "commandExecution") {
1172
+ const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
1173
+ const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
1174
+ if (command) parts.push(command);
1175
+ if (output) parts.push(output);
1176
+ }
1177
+ }
1178
+ }
1179
+ return parts.join("\n").trim();
1180
+ }
1181
+ function isExactPhraseMatch(query, doc) {
1182
+ const q = query.trim().toLowerCase();
1183
+ if (!q) return false;
1184
+ return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
1185
+ }
1186
+ function scoreFileCandidate(path, query) {
1187
+ if (!query) return 0;
1188
+ const lowerPath = path.toLowerCase();
1189
+ const lowerQuery = query.toLowerCase();
1190
+ const baseName = lowerPath.slice(lowerPath.lastIndexOf("/") + 1);
1191
+ if (baseName === lowerQuery) return 0;
1192
+ if (baseName.startsWith(lowerQuery)) return 1;
1193
+ if (baseName.includes(lowerQuery)) return 2;
1194
+ if (lowerPath.includes(`/${lowerQuery}`)) return 3;
1195
+ if (lowerPath.includes(lowerQuery)) return 4;
1196
+ return 10;
1197
+ }
1198
+ async function listFilesWithRipgrep(cwd) {
1199
+ return await new Promise((resolve3, reject) => {
1200
+ const proc = spawn2("rg", ["--files", "--hidden", "-g", "!.git", "-g", "!node_modules"], {
1201
+ cwd,
1202
+ env: process.env,
1203
+ stdio: ["ignore", "pipe", "pipe"]
1204
+ });
1205
+ let stdout = "";
1206
+ let stderr = "";
1207
+ proc.stdout.on("data", (chunk) => {
1208
+ stdout += chunk.toString();
1209
+ });
1210
+ proc.stderr.on("data", (chunk) => {
1211
+ stderr += chunk.toString();
1212
+ });
1213
+ proc.on("error", reject);
1214
+ proc.on("close", (code) => {
1215
+ if (code === 0) {
1216
+ const rows = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1217
+ resolve3(rows);
1218
+ return;
1219
+ }
1220
+ const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
1221
+ reject(new Error(details || "rg --files failed"));
1222
+ });
1223
+ });
1224
+ }
1225
+ function getCodexHomeDir2() {
1226
+ const codexHome = process.env.CODEX_HOME?.trim();
1227
+ return codexHome && codexHome.length > 0 ? codexHome : join2(homedir2(), ".codex");
1228
+ }
1229
+ async function runCommand2(command, args, options = {}) {
1230
+ await new Promise((resolve3, reject) => {
1231
+ const proc = spawn2(command, args, {
1232
+ cwd: options.cwd,
1233
+ env: process.env,
1234
+ stdio: ["ignore", "pipe", "pipe"]
1235
+ });
1236
+ let stdout = "";
1237
+ let stderr = "";
1238
+ proc.stdout.on("data", (chunk) => {
1239
+ stdout += chunk.toString();
1240
+ });
1241
+ proc.stderr.on("data", (chunk) => {
1242
+ stderr += chunk.toString();
291
1243
  });
292
- }
293
- skillsTreeCache = { entries, fetchedAt: Date.now() };
294
- return entries;
1244
+ proc.on("error", reject);
1245
+ proc.on("close", (code) => {
1246
+ if (code === 0) {
1247
+ resolve3();
1248
+ return;
1249
+ }
1250
+ const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
1251
+ const suffix = details.length > 0 ? `: ${details}` : "";
1252
+ reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
1253
+ });
1254
+ });
295
1255
  }
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;
1256
+ function isMissingHeadError(error) {
1257
+ const message = getErrorMessage2(error, "").toLowerCase();
1258
+ return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head") || message.includes("invalid reference: head");
314
1259
  }
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
- };
1260
+ function isNotGitRepositoryError(error) {
1261
+ const message = getErrorMessage2(error, "").toLowerCase();
1262
+ return message.includes("not a git repository") || message.includes("fatal: not a git repository");
327
1263
  }
328
- async function scanInstalledSkillsFromDisk() {
329
- const map = /* @__PURE__ */ new Map();
330
- const skillsDir = getSkillsInstallDir();
1264
+ async function ensureRepoHasInitialCommit(repoRoot) {
1265
+ const agentsPath = join2(repoRoot, "AGENTS.md");
331
1266
  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
- }
1267
+ await stat2(agentsPath);
342
1268
  } catch {
1269
+ await writeFile2(agentsPath, "", "utf8");
343
1270
  }
344
- return map;
1271
+ await runCommand2("git", ["add", "AGENTS.md"], { cwd: repoRoot });
1272
+ await runCommand2(
1273
+ "git",
1274
+ ["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
1275
+ { cwd: repoRoot }
1276
+ );
345
1277
  }
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;
1278
+ async function runCommandCapture(command, args, options = {}) {
1279
+ return await new Promise((resolve3, reject) => {
1280
+ const proc = spawn2(command, args, {
1281
+ cwd: options.cwd,
1282
+ env: process.env,
1283
+ stdio: ["ignore", "pipe", "pipe"]
1284
+ });
1285
+ let stdout = "";
1286
+ let stderr = "";
1287
+ proc.stdout.on("data", (chunk) => {
1288
+ stdout += chunk.toString();
1289
+ });
1290
+ proc.stderr.on("data", (chunk) => {
1291
+ stderr += chunk.toString();
1292
+ });
1293
+ proc.on("error", reject);
1294
+ proc.on("close", (code) => {
1295
+ if (code === 0) {
1296
+ resolve3(stdout.trim());
1297
+ return;
1298
+ }
1299
+ const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
1300
+ const suffix = details.length > 0 ? `: ${details}` : "";
1301
+ reject(new Error(`Command failed (${command} ${args.join(" ")})${suffix}`));
365
1302
  });
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
1303
  });
371
1304
  }
372
1305
  function normalizeStringArray(value) {
@@ -390,27 +1323,36 @@ function normalizeStringRecord(value) {
390
1323
  return next;
391
1324
  }
392
1325
  function getCodexAuthPath() {
393
- return join(getCodexHomeDir(), "auth.json");
1326
+ return join2(getCodexHomeDir2(), "auth.json");
394
1327
  }
395
1328
  async function readCodexAuth() {
396
1329
  try {
397
- const raw = await readFile(getCodexAuthPath(), "utf8");
1330
+ const raw = await readFile2(getCodexAuthPath(), "utf8");
398
1331
  const auth = JSON.parse(raw);
1332
+ const apiKey = auth.OPENAI_API_KEY || process.env.OPENAI_API_KEY || void 0;
399
1333
  const token = auth.tokens?.access_token;
400
- if (!token) return null;
401
- return { accessToken: token, accountId: auth.tokens?.account_id ?? void 0 };
1334
+ if (!token && !apiKey) return null;
1335
+ return { accessToken: token ?? "", accountId: auth.tokens?.account_id ?? void 0, apiKey };
402
1336
  } catch {
403
1337
  return null;
404
1338
  }
405
1339
  }
406
1340
  function getCodexGlobalStatePath() {
407
- return join(getCodexHomeDir(), ".codex-global-state.json");
1341
+ return join2(getCodexHomeDir2(), ".codex-global-state.json");
1342
+ }
1343
+ function getCodexSessionIndexPath() {
1344
+ return join2(getCodexHomeDir2(), "session_index.jsonl");
408
1345
  }
409
1346
  var MAX_THREAD_TITLES = 500;
1347
+ var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
1348
+ var sessionIndexThreadTitleCacheState = {
1349
+ fileSignature: null,
1350
+ cache: EMPTY_THREAD_TITLE_CACHE
1351
+ };
410
1352
  function normalizeThreadTitleCache(value) {
411
- const record = asRecord(value);
412
- if (!record) return { titles: {}, order: [] };
413
- const rawTitles = asRecord(record.titles);
1353
+ const record = asRecord2(value);
1354
+ if (!record) return EMPTY_THREAD_TITLE_CACHE;
1355
+ const rawTitles = asRecord2(record.titles);
414
1356
  const titles = {};
415
1357
  if (rawTitles) {
416
1358
  for (const [k, v] of Object.entries(rawTitles)) {
@@ -433,35 +1375,139 @@ function removeFromThreadTitleCache(cache, id) {
433
1375
  const { [id]: _, ...titles } = cache.titles;
434
1376
  return { titles, order: cache.order.filter((o) => o !== id) };
435
1377
  }
1378
+ function normalizeSessionIndexThreadTitle(value) {
1379
+ const record = asRecord2(value);
1380
+ if (!record) return null;
1381
+ const id = typeof record.id === "string" ? record.id.trim() : "";
1382
+ const title = typeof record.thread_name === "string" ? record.thread_name.trim() : "";
1383
+ const updatedAtIso = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
1384
+ const updatedAtMs = updatedAtIso ? Date.parse(updatedAtIso) : Number.NaN;
1385
+ if (!id || !title) return null;
1386
+ return {
1387
+ id,
1388
+ title,
1389
+ updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0
1390
+ };
1391
+ }
1392
+ function trimThreadTitleCache(cache) {
1393
+ const titles = { ...cache.titles };
1394
+ const order = cache.order.filter((id) => {
1395
+ if (!titles[id]) return false;
1396
+ return true;
1397
+ }).slice(0, MAX_THREAD_TITLES);
1398
+ for (const id of Object.keys(titles)) {
1399
+ if (!order.includes(id)) {
1400
+ delete titles[id];
1401
+ }
1402
+ }
1403
+ return { titles, order };
1404
+ }
1405
+ function mergeThreadTitleCaches(base, overlay) {
1406
+ const titles = { ...base.titles, ...overlay.titles };
1407
+ const order = [];
1408
+ for (const id of [...overlay.order, ...base.order]) {
1409
+ if (!titles[id] || order.includes(id)) continue;
1410
+ order.push(id);
1411
+ }
1412
+ for (const id of Object.keys(titles)) {
1413
+ if (!order.includes(id)) {
1414
+ order.push(id);
1415
+ }
1416
+ }
1417
+ return trimThreadTitleCache({ titles, order });
1418
+ }
436
1419
  async function readThreadTitleCache() {
437
1420
  const statePath = getCodexGlobalStatePath();
438
1421
  try {
439
- const raw = await readFile(statePath, "utf8");
440
- const payload = asRecord(JSON.parse(raw)) ?? {};
1422
+ const raw = await readFile2(statePath, "utf8");
1423
+ const payload = asRecord2(JSON.parse(raw)) ?? {};
441
1424
  return normalizeThreadTitleCache(payload["thread-titles"]);
442
1425
  } catch {
443
- return { titles: {}, order: [] };
1426
+ return EMPTY_THREAD_TITLE_CACHE;
444
1427
  }
445
1428
  }
446
1429
  async function writeThreadTitleCache(cache) {
447
1430
  const statePath = getCodexGlobalStatePath();
448
1431
  let payload = {};
449
1432
  try {
450
- const raw = await readFile(statePath, "utf8");
451
- payload = asRecord(JSON.parse(raw)) ?? {};
1433
+ const raw = await readFile2(statePath, "utf8");
1434
+ payload = asRecord2(JSON.parse(raw)) ?? {};
452
1435
  } catch {
453
1436
  payload = {};
454
1437
  }
455
1438
  payload["thread-titles"] = cache;
456
- await writeFile(statePath, JSON.stringify(payload), "utf8");
1439
+ await writeFile2(statePath, JSON.stringify(payload), "utf8");
1440
+ }
1441
+ function getSessionIndexFileSignature(stats) {
1442
+ return `${String(stats.mtimeMs)}:${String(stats.size)}`;
1443
+ }
1444
+ async function parseThreadTitlesFromSessionIndex(sessionIndexPath) {
1445
+ const latestById = /* @__PURE__ */ new Map();
1446
+ const input = createReadStream(sessionIndexPath, { encoding: "utf8" });
1447
+ const lines = createInterface({
1448
+ input,
1449
+ crlfDelay: Infinity
1450
+ });
1451
+ try {
1452
+ for await (const line of lines) {
1453
+ const trimmed = line.trim();
1454
+ if (!trimmed) continue;
1455
+ try {
1456
+ const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed));
1457
+ if (!entry) continue;
1458
+ const previous = latestById.get(entry.id);
1459
+ if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
1460
+ latestById.set(entry.id, entry);
1461
+ }
1462
+ } catch {
1463
+ }
1464
+ }
1465
+ } finally {
1466
+ lines.close();
1467
+ input.close();
1468
+ }
1469
+ const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs);
1470
+ const titles = {};
1471
+ const order = [];
1472
+ for (const entry of entries) {
1473
+ titles[entry.id] = entry.title;
1474
+ order.push(entry.id);
1475
+ }
1476
+ return trimThreadTitleCache({ titles, order });
1477
+ }
1478
+ async function readThreadTitlesFromSessionIndex() {
1479
+ const sessionIndexPath = getCodexSessionIndexPath();
1480
+ try {
1481
+ const stats = await stat2(sessionIndexPath);
1482
+ const fileSignature = getSessionIndexFileSignature(stats);
1483
+ if (sessionIndexThreadTitleCacheState.fileSignature === fileSignature) {
1484
+ return sessionIndexThreadTitleCacheState.cache;
1485
+ }
1486
+ const cache = await parseThreadTitlesFromSessionIndex(sessionIndexPath);
1487
+ sessionIndexThreadTitleCacheState = { fileSignature, cache };
1488
+ return cache;
1489
+ } catch {
1490
+ sessionIndexThreadTitleCacheState = {
1491
+ fileSignature: "missing",
1492
+ cache: EMPTY_THREAD_TITLE_CACHE
1493
+ };
1494
+ return sessionIndexThreadTitleCacheState.cache;
1495
+ }
1496
+ }
1497
+ async function readMergedThreadTitleCache() {
1498
+ const [sessionIndexCache, persistedCache] = await Promise.all([
1499
+ readThreadTitlesFromSessionIndex(),
1500
+ readThreadTitleCache()
1501
+ ]);
1502
+ return mergeThreadTitleCaches(persistedCache, sessionIndexCache);
457
1503
  }
458
1504
  async function readWorkspaceRootsState() {
459
1505
  const statePath = getCodexGlobalStatePath();
460
1506
  let payload = {};
461
1507
  try {
462
- const raw = await readFile(statePath, "utf8");
1508
+ const raw = await readFile2(statePath, "utf8");
463
1509
  const parsed = JSON.parse(raw);
464
- payload = asRecord(parsed) ?? {};
1510
+ payload = asRecord2(parsed) ?? {};
465
1511
  } catch {
466
1512
  payload = {};
467
1513
  }
@@ -475,15 +1521,15 @@ async function writeWorkspaceRootsState(nextState) {
475
1521
  const statePath = getCodexGlobalStatePath();
476
1522
  let payload = {};
477
1523
  try {
478
- const raw = await readFile(statePath, "utf8");
479
- payload = asRecord(JSON.parse(raw)) ?? {};
1524
+ const raw = await readFile2(statePath, "utf8");
1525
+ payload = asRecord2(JSON.parse(raw)) ?? {};
480
1526
  } catch {
481
1527
  payload = {};
482
1528
  }
483
1529
  payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
484
1530
  payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
485
1531
  payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
486
- await writeFile(statePath, JSON.stringify(payload), "utf8");
1532
+ await writeFile2(statePath, JSON.stringify(payload), "utf8");
487
1533
  }
488
1534
  async function readJsonBody(req) {
489
1535
  const raw = await readRawBody(req);
@@ -521,7 +1567,7 @@ function handleFileUpload(req, res) {
521
1567
  const contentType = req.headers["content-type"] ?? "";
522
1568
  const boundaryMatch = contentType.match(/boundary=(.+)/i);
523
1569
  if (!boundaryMatch) {
524
- setJson(res, 400, { error: "Missing multipart boundary" });
1570
+ setJson2(res, 400, { error: "Missing multipart boundary" });
525
1571
  return;
526
1572
  }
527
1573
  const boundary = boundaryMatch[1];
@@ -551,49 +1597,110 @@ function handleFileUpload(req, res) {
551
1597
  break;
552
1598
  }
553
1599
  if (!fileData) {
554
- setJson(res, 400, { error: "No file in request" });
1600
+ setJson2(res, 400, { error: "No file in request" });
555
1601
  return;
556
1602
  }
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 });
1603
+ const uploadDir = join2(tmpdir2(), "codex-web-uploads");
1604
+ await mkdir2(uploadDir, { recursive: true });
1605
+ const destDir = await mkdtemp2(join2(uploadDir, "f-"));
1606
+ const destPath = join2(destDir, fileName);
1607
+ await writeFile2(destPath, fileData);
1608
+ setJson2(res, 200, { path: destPath });
563
1609
  } catch (err) {
564
- setJson(res, 500, { error: getErrorMessage(err, "Upload failed") });
1610
+ setJson2(res, 500, { error: getErrorMessage2(err, "Upload failed") });
565
1611
  }
566
1612
  });
567
1613
  req.on("error", (err) => {
568
- setJson(res, 500, { error: getErrorMessage(err, "Upload stream error") });
1614
+ setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
569
1615
  });
570
1616
  }
571
- async function proxyTranscribe(body, contentType, authToken, accountId) {
572
- const headers = {
1617
+ function httpPost(url, headers, body) {
1618
+ const doRequest = url.startsWith("http://") ? httpRequest : httpsRequest;
1619
+ return new Promise((resolve3, reject) => {
1620
+ const req = doRequest(url, { method: "POST", headers }, (res) => {
1621
+ const chunks = [];
1622
+ res.on("data", (c) => chunks.push(c));
1623
+ res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
1624
+ res.on("error", reject);
1625
+ });
1626
+ req.on("error", reject);
1627
+ req.write(body);
1628
+ req.end();
1629
+ });
1630
+ }
1631
+ var curlImpersonateAvailable = null;
1632
+ function curlImpersonatePost(url, headers, body) {
1633
+ return new Promise((resolve3, reject) => {
1634
+ const args = ["-s", "-w", "\n%{http_code}", "-X", "POST", url];
1635
+ for (const [k, v] of Object.entries(headers)) {
1636
+ if (k.toLowerCase() === "content-length") continue;
1637
+ args.push("-H", `${k}: ${String(v)}`);
1638
+ }
1639
+ args.push("--data-binary", "@-");
1640
+ const proc = spawn2("curl-impersonate-chrome", args, {
1641
+ env: { ...process.env, CURL_IMPERSONATE: "chrome116" },
1642
+ stdio: ["pipe", "pipe", "pipe"]
1643
+ });
1644
+ const chunks = [];
1645
+ proc.stdout.on("data", (c) => chunks.push(c));
1646
+ proc.on("error", (e) => {
1647
+ curlImpersonateAvailable = false;
1648
+ reject(e);
1649
+ });
1650
+ proc.on("close", (code) => {
1651
+ const raw = Buffer.concat(chunks).toString("utf8");
1652
+ const lastNewline = raw.lastIndexOf("\n");
1653
+ const statusStr = lastNewline >= 0 ? raw.slice(lastNewline + 1).trim() : "";
1654
+ const responseBody = lastNewline >= 0 ? raw.slice(0, lastNewline) : raw;
1655
+ const status = parseInt(statusStr, 10) || (code === 0 ? 200 : 500);
1656
+ curlImpersonateAvailable = true;
1657
+ resolve3({ status, body: responseBody });
1658
+ });
1659
+ proc.stdin.write(body);
1660
+ proc.stdin.end();
1661
+ });
1662
+ }
1663
+ var TRANSCRIBE_RELAY_URL = process.env.TRANSCRIBE_RELAY_URL || "http://127.0.0.1:1090/relay-transcribe";
1664
+ async function tryRelay(headers, body) {
1665
+ try {
1666
+ const resp = await httpPost(TRANSCRIBE_RELAY_URL, headers, body);
1667
+ if (resp.status !== 0) return resp;
1668
+ } catch {
1669
+ }
1670
+ return null;
1671
+ }
1672
+ async function proxyTranscribe(body, contentType, authToken, accountId, apiKey) {
1673
+ const chatgptHeaders = {
573
1674
  "Content-Type": contentType,
574
1675
  "Content-Length": body.length,
575
- Authorization: `Bearer ${authToken}`,
1676
+ Authorization: `Bearer ${authToken || apiKey || ""}`,
576
1677
  originator: "Codex Desktop",
577
1678
  "User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
578
1679
  };
579
- if (accountId) {
580
- headers["ChatGPT-Account-Id"] = accountId;
581
- }
582
- return new Promise((resolve2, reject) => {
583
- const req = httpsRequest(
584
- "https://chatgpt.com/backend-api/transcribe",
585
- { method: "POST", headers },
586
- (res) => {
587
- const chunks = [];
588
- res.on("data", (c) => chunks.push(c));
589
- res.on("end", () => resolve2({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
590
- res.on("error", reject);
591
- }
592
- );
593
- req.on("error", reject);
594
- req.write(body);
595
- req.end();
596
- });
1680
+ if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
1681
+ const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
1682
+ let result;
1683
+ try {
1684
+ result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
1685
+ } catch {
1686
+ result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
1687
+ }
1688
+ if (result.status === 403 && result.body.includes("cf_chl")) {
1689
+ if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
1690
+ try {
1691
+ const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
1692
+ if (ciResult.status !== 403) return ciResult;
1693
+ } catch {
1694
+ }
1695
+ }
1696
+ const relayed = await tryRelay(chatgptHeaders, body);
1697
+ if (relayed && relayed.status !== 403) return relayed;
1698
+ if (apiKey) {
1699
+ return httpPost("https://api.openai.com/v1/audio/transcriptions", { ...chatgptHeaders, Authorization: `Bearer ${apiKey}` }, body);
1700
+ }
1701
+ return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome, start relay, or set OPENAI_API_KEY." }) };
1702
+ }
1703
+ return result;
597
1704
  }
598
1705
  var AppServerProcess = class {
599
1706
  constructor() {
@@ -617,7 +1724,7 @@ var AppServerProcess = class {
617
1724
  start() {
618
1725
  if (this.process) return;
619
1726
  this.stopping = false;
620
- const proc = spawn("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
1727
+ const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
621
1728
  this.process = proc;
622
1729
  proc.stdout.setEncoding("utf8");
623
1730
  proc.stdout.on("data", (chunk) => {
@@ -711,7 +1818,7 @@ var AppServerProcess = class {
711
1818
  }
712
1819
  this.pendingServerRequests.delete(requestId);
713
1820
  this.sendServerRequestReply(requestId, reply);
714
- const requestParams = asRecord(pendingRequest.params);
1821
+ const requestParams = asRecord2(pendingRequest.params);
715
1822
  const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
716
1823
  this.emitNotification({
717
1824
  method: "server/request/resolved",
@@ -740,8 +1847,8 @@ var AppServerProcess = class {
740
1847
  async call(method, params) {
741
1848
  this.start();
742
1849
  const id = this.nextId++;
743
- return new Promise((resolve2, reject) => {
744
- this.pending.set(id, { resolve: resolve2, reject });
1850
+ return new Promise((resolve3, reject) => {
1851
+ this.pending.set(id, { resolve: resolve3, reject });
745
1852
  this.sendLine({
746
1853
  jsonrpc: "2.0",
747
1854
  id,
@@ -780,7 +1887,7 @@ var AppServerProcess = class {
780
1887
  }
781
1888
  async respondToServerRequest(payload) {
782
1889
  await this.ensureInitialized();
783
- const body = asRecord(payload);
1890
+ const body = asRecord2(payload);
784
1891
  if (!body) {
785
1892
  throw new Error("Invalid response payload: expected object");
786
1893
  }
@@ -788,7 +1895,7 @@ var AppServerProcess = class {
788
1895
  if (typeof id !== "number" || !Number.isInteger(id)) {
789
1896
  throw new Error('Invalid response payload: "id" must be an integer');
790
1897
  }
791
- const rawError = asRecord(body.error);
1898
+ const rawError = asRecord2(body.error);
792
1899
  if (rawError) {
793
1900
  const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
794
1901
  const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
@@ -842,8 +1949,8 @@ var MethodCatalog = class {
842
1949
  this.notificationCache = null;
843
1950
  }
844
1951
  async runGenerateSchemaCommand(outDir) {
845
- await new Promise((resolve2, reject) => {
846
- const process2 = spawn("codex", ["app-server", "generate-json-schema", "--out", outDir], {
1952
+ await new Promise((resolve3, reject) => {
1953
+ const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
847
1954
  stdio: ["ignore", "ignore", "pipe"]
848
1955
  });
849
1956
  let stderr = "";
@@ -854,7 +1961,7 @@ var MethodCatalog = class {
854
1961
  process2.on("error", reject);
855
1962
  process2.on("exit", (code) => {
856
1963
  if (code === 0) {
857
- resolve2();
1964
+ resolve3();
858
1965
  return;
859
1966
  }
860
1967
  reject(new Error(stderr.trim() || `generate-json-schema exited with code ${String(code)}`));
@@ -862,13 +1969,13 @@ var MethodCatalog = class {
862
1969
  });
863
1970
  }
864
1971
  extractMethodsFromClientRequest(payload) {
865
- const root = asRecord(payload);
1972
+ const root = asRecord2(payload);
866
1973
  const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
867
1974
  const methods = /* @__PURE__ */ new Set();
868
1975
  for (const entry of oneOf) {
869
- const row = asRecord(entry);
870
- const properties = asRecord(row?.properties);
871
- const methodDef = asRecord(properties?.method);
1976
+ const row = asRecord2(entry);
1977
+ const properties = asRecord2(row?.properties);
1978
+ const methodDef = asRecord2(properties?.method);
872
1979
  const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
873
1980
  for (const item of methodEnum) {
874
1981
  if (typeof item === "string" && item.length > 0) {
@@ -879,13 +1986,13 @@ var MethodCatalog = class {
879
1986
  return Array.from(methods).sort((a, b) => a.localeCompare(b));
880
1987
  }
881
1988
  extractMethodsFromServerNotification(payload) {
882
- const root = asRecord(payload);
1989
+ const root = asRecord2(payload);
883
1990
  const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
884
1991
  const methods = /* @__PURE__ */ new Set();
885
1992
  for (const entry of oneOf) {
886
- const row = asRecord(entry);
887
- const properties = asRecord(row?.properties);
888
- const methodDef = asRecord(properties?.method);
1993
+ const row = asRecord2(entry);
1994
+ const properties = asRecord2(row?.properties);
1995
+ const methodDef = asRecord2(properties?.method);
889
1996
  const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
890
1997
  for (const item of methodEnum) {
891
1998
  if (typeof item === "string" && item.length > 0) {
@@ -899,10 +2006,10 @@ var MethodCatalog = class {
899
2006
  if (this.methodCache) {
900
2007
  return this.methodCache;
901
2008
  }
902
- const outDir = await mkdtemp(join(tmpdir(), "codex-web-local-schema-"));
2009
+ const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
903
2010
  await this.runGenerateSchemaCommand(outDir);
904
- const clientRequestPath = join(outDir, "ClientRequest.json");
905
- const raw = await readFile(clientRequestPath, "utf8");
2011
+ const clientRequestPath = join2(outDir, "ClientRequest.json");
2012
+ const raw = await readFile2(clientRequestPath, "utf8");
906
2013
  const parsed = JSON.parse(raw);
907
2014
  const methods = this.extractMethodsFromClientRequest(parsed);
908
2015
  this.methodCache = methods;
@@ -912,10 +2019,10 @@ var MethodCatalog = class {
912
2019
  if (this.notificationCache) {
913
2020
  return this.notificationCache;
914
2021
  }
915
- const outDir = await mkdtemp(join(tmpdir(), "codex-web-local-schema-"));
2022
+ const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
916
2023
  await this.runGenerateSchemaCommand(outDir);
917
- const serverNotificationPath = join(outDir, "ServerNotification.json");
918
- const raw = await readFile(serverNotificationPath, "utf8");
2024
+ const serverNotificationPath = join2(outDir, "ServerNotification.json");
2025
+ const raw = await readFile2(serverNotificationPath, "utf8");
919
2026
  const parsed = JSON.parse(raw);
920
2027
  const methods = this.extractMethodsFromServerNotification(parsed);
921
2028
  this.notificationCache = methods;
@@ -938,7 +2045,7 @@ async function loadAllThreadsForSearch(appServer) {
938
2045
  const threads = [];
939
2046
  let cursor = null;
940
2047
  do {
941
- const response = asRecord(await appServer.rpc("thread/list", {
2048
+ const response = asRecord2(await appServer.rpc("thread/list", {
942
2049
  archived: false,
943
2050
  limit: 100,
944
2051
  sortKey: "updated_at",
@@ -946,7 +2053,7 @@ async function loadAllThreadsForSearch(appServer) {
946
2053
  }));
947
2054
  const data = Array.isArray(response?.data) ? response.data : [];
948
2055
  for (const row of data) {
949
- const record = asRecord(row);
2056
+ const record = asRecord2(row);
950
2057
  const id = typeof record?.id === "string" ? record.id : "";
951
2058
  if (!id) continue;
952
2059
  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 +2117,7 @@ function createCodexBridgeMiddleware() {
1010
2117
  }
1011
2118
  return threadSearchIndexPromise;
1012
2119
  }
2120
+ void initializeSkillsSyncOnStartup(appServer);
1013
2121
  const middleware = async (req, res, next) => {
1014
2122
  try {
1015
2123
  if (!req.url) {
@@ -1017,30 +2125,33 @@ function createCodexBridgeMiddleware() {
1017
2125
  return;
1018
2126
  }
1019
2127
  const url = new URL(req.url, "http://localhost");
2128
+ if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
2129
+ return;
2130
+ }
1020
2131
  if (req.method === "POST" && url.pathname === "/codex-api/upload-file") {
1021
2132
  handleFileUpload(req, res);
1022
2133
  return;
1023
2134
  }
1024
2135
  if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
1025
2136
  const payload = await readJsonBody(req);
1026
- const body = asRecord(payload);
2137
+ const body = asRecord2(payload);
1027
2138
  if (!body || typeof body.method !== "string" || body.method.length === 0) {
1028
- setJson(res, 400, { error: "Invalid body: expected { method, params? }" });
2139
+ setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
1029
2140
  return;
1030
2141
  }
1031
2142
  const result = await appServer.rpc(body.method, body.params ?? null);
1032
- setJson(res, 200, { result });
2143
+ setJson2(res, 200, { result });
1033
2144
  return;
1034
2145
  }
1035
2146
  if (req.method === "POST" && url.pathname === "/codex-api/transcribe") {
1036
2147
  const auth = await readCodexAuth();
1037
2148
  if (!auth) {
1038
- setJson(res, 401, { error: "No auth token available for transcription" });
2149
+ setJson2(res, 401, { error: "No auth token available for transcription" });
1039
2150
  return;
1040
2151
  }
1041
2152
  const rawBody = await readRawBody(req);
1042
2153
  const incomingCt = req.headers["content-type"] ?? "application/octet-stream";
1043
- const upstream = await proxyTranscribe(rawBody, incomingCt, auth.accessToken, auth.accountId);
2154
+ const upstream = await proxyTranscribe(rawBody, incomingCt, auth.accessToken, auth.accountId, auth.apiKey);
1044
2155
  res.statusCode = upstream.status;
1045
2156
  res.setHeader("Content-Type", "application/json; charset=utf-8");
1046
2157
  res.end(upstream.body);
@@ -1049,48 +2160,48 @@ function createCodexBridgeMiddleware() {
1049
2160
  if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
1050
2161
  const payload = await readJsonBody(req);
1051
2162
  await appServer.respondToServerRequest(payload);
1052
- setJson(res, 200, { ok: true });
2163
+ setJson2(res, 200, { ok: true });
1053
2164
  return;
1054
2165
  }
1055
2166
  if (req.method === "GET" && url.pathname === "/codex-api/server-requests/pending") {
1056
- setJson(res, 200, { data: appServer.listPendingServerRequests() });
2167
+ setJson2(res, 200, { data: appServer.listPendingServerRequests() });
1057
2168
  return;
1058
2169
  }
1059
2170
  if (req.method === "GET" && url.pathname === "/codex-api/meta/methods") {
1060
2171
  const methods = await methodCatalog.listMethods();
1061
- setJson(res, 200, { data: methods });
2172
+ setJson2(res, 200, { data: methods });
1062
2173
  return;
1063
2174
  }
1064
2175
  if (req.method === "GET" && url.pathname === "/codex-api/meta/notifications") {
1065
2176
  const methods = await methodCatalog.listNotificationMethods();
1066
- setJson(res, 200, { data: methods });
2177
+ setJson2(res, 200, { data: methods });
1067
2178
  return;
1068
2179
  }
1069
2180
  if (req.method === "GET" && url.pathname === "/codex-api/workspace-roots-state") {
1070
2181
  const state = await readWorkspaceRootsState();
1071
- setJson(res, 200, { data: state });
2182
+ setJson2(res, 200, { data: state });
1072
2183
  return;
1073
2184
  }
1074
2185
  if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
1075
- setJson(res, 200, { data: { path: homedir() } });
2186
+ setJson2(res, 200, { data: { path: homedir2() } });
1076
2187
  return;
1077
2188
  }
1078
2189
  if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
1079
- const payload = asRecord(await readJsonBody(req));
2190
+ const payload = asRecord2(await readJsonBody(req));
1080
2191
  const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
1081
2192
  if (!rawSourceCwd) {
1082
- setJson(res, 400, { error: "Missing sourceCwd" });
2193
+ setJson2(res, 400, { error: "Missing sourceCwd" });
1083
2194
  return;
1084
2195
  }
1085
2196
  const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
1086
2197
  try {
1087
- const sourceInfo = await stat(sourceCwd);
2198
+ const sourceInfo = await stat2(sourceCwd);
1088
2199
  if (!sourceInfo.isDirectory()) {
1089
- setJson(res, 400, { error: "sourceCwd is not a directory" });
2200
+ setJson2(res, 400, { error: "sourceCwd is not a directory" });
1090
2201
  return;
1091
2202
  }
1092
2203
  } catch {
1093
- setJson(res, 404, { error: "sourceCwd does not exist" });
2204
+ setJson2(res, 404, { error: "sourceCwd does not exist" });
1094
2205
  return;
1095
2206
  }
1096
2207
  try {
@@ -1099,25 +2210,25 @@ function createCodexBridgeMiddleware() {
1099
2210
  gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
1100
2211
  } catch (error) {
1101
2212
  if (!isNotGitRepositoryError(error)) throw error;
1102
- await runCommand("git", ["init"], { cwd: sourceCwd });
2213
+ await runCommand2("git", ["init"], { cwd: sourceCwd });
1103
2214
  gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
1104
2215
  }
1105
2216
  const repoName = basename(gitRoot) || "repo";
1106
- const worktreesRoot = join(getCodexHomeDir(), "worktrees");
1107
- await mkdir(worktreesRoot, { recursive: true });
2217
+ const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
2218
+ await mkdir2(worktreesRoot, { recursive: true });
1108
2219
  let worktreeId = "";
1109
2220
  let worktreeParent = "";
1110
2221
  let worktreeCwd = "";
1111
2222
  for (let attempt = 0; attempt < 12; attempt += 1) {
1112
2223
  const candidate = randomBytes(2).toString("hex");
1113
- const parent = join(worktreesRoot, candidate);
2224
+ const parent = join2(worktreesRoot, candidate);
1114
2225
  try {
1115
- await stat(parent);
2226
+ await stat2(parent);
1116
2227
  continue;
1117
2228
  } catch {
1118
2229
  worktreeId = candidate;
1119
2230
  worktreeParent = parent;
1120
- worktreeCwd = join(parent, repoName);
2231
+ worktreeCwd = join2(parent, repoName);
1121
2232
  break;
1122
2233
  }
1123
2234
  }
@@ -1125,15 +2236,15 @@ function createCodexBridgeMiddleware() {
1125
2236
  throw new Error("Failed to allocate a unique worktree id");
1126
2237
  }
1127
2238
  const branch = `codex/${worktreeId}`;
1128
- await mkdir(worktreeParent, { recursive: true });
2239
+ await mkdir2(worktreeParent, { recursive: true });
1129
2240
  try {
1130
- await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
2241
+ await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1131
2242
  } catch (error) {
1132
2243
  if (!isMissingHeadError(error)) throw error;
1133
2244
  await ensureRepoHasInitialCommit(gitRoot);
1134
- await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
2245
+ await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1135
2246
  }
1136
- setJson(res, 200, {
2247
+ setJson2(res, 200, {
1137
2248
  data: {
1138
2249
  cwd: worktreeCwd,
1139
2250
  branch,
@@ -1141,15 +2252,15 @@ function createCodexBridgeMiddleware() {
1141
2252
  }
1142
2253
  });
1143
2254
  } catch (error) {
1144
- setJson(res, 500, { error: getErrorMessage(error, "Failed to create worktree") });
2255
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to create worktree") });
1145
2256
  }
1146
2257
  return;
1147
2258
  }
1148
2259
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
1149
2260
  const payload = await readJsonBody(req);
1150
- const record = asRecord(payload);
2261
+ const record = asRecord2(payload);
1151
2262
  if (!record) {
1152
- setJson(res, 400, { error: "Invalid body: expected object" });
2263
+ setJson2(res, 400, { error: "Invalid body: expected object" });
1153
2264
  return;
1154
2265
  }
1155
2266
  const nextState = {
@@ -1158,33 +2269,33 @@ function createCodexBridgeMiddleware() {
1158
2269
  active: normalizeStringArray(record.active)
1159
2270
  };
1160
2271
  await writeWorkspaceRootsState(nextState);
1161
- setJson(res, 200, { ok: true });
2272
+ setJson2(res, 200, { ok: true });
1162
2273
  return;
1163
2274
  }
1164
2275
  if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
1165
- const payload = asRecord(await readJsonBody(req));
2276
+ const payload = asRecord2(await readJsonBody(req));
1166
2277
  const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
1167
2278
  const createIfMissing = payload?.createIfMissing === true;
1168
2279
  const label = typeof payload?.label === "string" ? payload.label : "";
1169
2280
  if (!rawPath) {
1170
- setJson(res, 400, { error: "Missing path" });
2281
+ setJson2(res, 400, { error: "Missing path" });
1171
2282
  return;
1172
2283
  }
1173
2284
  const normalizedPath = isAbsolute(rawPath) ? rawPath : resolve(rawPath);
1174
2285
  let pathExists = true;
1175
2286
  try {
1176
- const info = await stat(normalizedPath);
2287
+ const info = await stat2(normalizedPath);
1177
2288
  if (!info.isDirectory()) {
1178
- setJson(res, 400, { error: "Path exists but is not a directory" });
2289
+ setJson2(res, 400, { error: "Path exists but is not a directory" });
1179
2290
  return;
1180
2291
  }
1181
2292
  } catch {
1182
2293
  pathExists = false;
1183
2294
  }
1184
2295
  if (!pathExists && createIfMissing) {
1185
- await mkdir(normalizedPath, { recursive: true });
2296
+ await mkdir2(normalizedPath, { recursive: true });
1186
2297
  } else if (!pathExists) {
1187
- setJson(res, 404, { error: "Directory does not exist" });
2298
+ setJson2(res, 404, { error: "Directory does not exist" });
1188
2299
  return;
1189
2300
  }
1190
2301
  const existingState = await readWorkspaceRootsState();
@@ -1199,215 +2310,103 @@ function createCodexBridgeMiddleware() {
1199
2310
  labels: nextLabels,
1200
2311
  active: nextActive
1201
2312
  });
1202
- setJson(res, 200, { data: { path: normalizedPath } });
2313
+ setJson2(res, 200, { data: { path: normalizedPath } });
1203
2314
  return;
1204
2315
  }
1205
2316
  if (req.method === "GET" && url.pathname === "/codex-api/project-root-suggestion") {
1206
2317
  const basePath = url.searchParams.get("basePath")?.trim() ?? "";
1207
2318
  if (!basePath) {
1208
- setJson(res, 400, { error: "Missing basePath" });
2319
+ setJson2(res, 400, { error: "Missing basePath" });
1209
2320
  return;
1210
2321
  }
1211
2322
  const normalizedBasePath = isAbsolute(basePath) ? basePath : resolve(basePath);
1212
2323
  try {
1213
- const baseInfo = await stat(normalizedBasePath);
2324
+ const baseInfo = await stat2(normalizedBasePath);
1214
2325
  if (!baseInfo.isDirectory()) {
1215
- setJson(res, 400, { error: "basePath is not a directory" });
2326
+ setJson2(res, 400, { error: "basePath is not a directory" });
1216
2327
  return;
1217
2328
  }
1218
2329
  } catch {
1219
- setJson(res, 404, { error: "basePath does not exist" });
2330
+ setJson2(res, 404, { error: "basePath does not exist" });
1220
2331
  return;
1221
2332
  }
1222
2333
  let index = 1;
1223
2334
  while (index < 1e5) {
1224
2335
  const candidateName = `New Project (${String(index)})`;
1225
- const candidatePath = join(normalizedBasePath, candidateName);
2336
+ const candidatePath = join2(normalizedBasePath, candidateName);
1226
2337
  try {
1227
- await stat(candidatePath);
2338
+ await stat2(candidatePath);
1228
2339
  index += 1;
1229
2340
  continue;
1230
2341
  } catch {
1231
- setJson(res, 200, { data: { name: candidateName, path: candidatePath } });
2342
+ setJson2(res, 200, { data: { name: candidateName, path: candidatePath } });
1232
2343
  return;
1233
2344
  }
1234
2345
  }
1235
- setJson(res, 500, { error: "Failed to compute project name suggestion" });
2346
+ setJson2(res, 500, { error: "Failed to compute project name suggestion" });
1236
2347
  return;
1237
2348
  }
1238
2349
  if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
1239
- const payload = asRecord(await readJsonBody(req));
2350
+ const payload = asRecord2(await readJsonBody(req));
1240
2351
  const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
1241
2352
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
1242
2353
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
1243
2354
  const limit = Math.max(1, Math.min(100, Math.floor(limitRaw)));
1244
2355
  if (!rawCwd) {
1245
- setJson(res, 400, { error: "Missing cwd" });
2356
+ setJson2(res, 400, { error: "Missing cwd" });
1246
2357
  return;
1247
2358
  }
1248
2359
  const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
1249
2360
  try {
1250
- const info = await stat(cwd);
2361
+ const info = await stat2(cwd);
1251
2362
  if (!info.isDirectory()) {
1252
- setJson(res, 400, { error: "cwd is not a directory" });
2363
+ setJson2(res, 400, { error: "cwd is not a directory" });
1253
2364
  return;
1254
2365
  }
1255
2366
  } catch {
1256
- setJson(res, 404, { error: "cwd does not exist" });
2367
+ setJson2(res, 404, { error: "cwd does not exist" });
1257
2368
  return;
1258
2369
  }
1259
2370
  try {
1260
2371
  const files = await listFilesWithRipgrep(cwd);
1261
2372
  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 });
2373
+ setJson2(res, 200, { data: scored });
1263
2374
  } catch (error) {
1264
- setJson(res, 500, { error: getErrorMessage(error, "Failed to search files") });
2375
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to search files") });
1265
2376
  }
1266
2377
  return;
1267
2378
  }
1268
2379
  if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
1269
- const cache = await readThreadTitleCache();
1270
- setJson(res, 200, { data: cache });
2380
+ const cache = await readMergedThreadTitleCache();
2381
+ setJson2(res, 200, { data: cache });
1271
2382
  return;
1272
2383
  }
1273
2384
  if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
1274
- const payload = asRecord(await readJsonBody(req));
2385
+ const payload = asRecord2(await readJsonBody(req));
1275
2386
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
1276
2387
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
1277
2388
  const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
1278
2389
  if (!query) {
1279
- setJson(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
2390
+ setJson2(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
1280
2391
  return;
1281
2392
  }
1282
2393
  const index = await getThreadSearchIndex();
1283
2394
  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 } });
2395
+ setJson2(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
1285
2396
  return;
1286
2397
  }
1287
2398
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
1288
- const payload = asRecord(await readJsonBody(req));
2399
+ const payload = asRecord2(await readJsonBody(req));
1289
2400
  const id = typeof payload?.id === "string" ? payload.id : "";
1290
2401
  const title = typeof payload?.title === "string" ? payload.title : "";
1291
2402
  if (!id) {
1292
- setJson(res, 400, { error: "Missing id" });
2403
+ setJson2(res, 400, { error: "Missing id" });
1293
2404
  return;
1294
2405
  }
1295
2406
  const cache = await readThreadTitleCache();
1296
2407
  const next2 = title ? updateThreadTitleCache(cache, id, title) : removeFromThreadTitleCache(cache, id);
1297
2408
  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
- }
2409
+ setJson2(res, 200, { ok: true });
1411
2410
  return;
1412
2411
  }
1413
2412
  if (req.method === "GET" && url.pathname === "/codex-api/events") {
@@ -1442,8 +2441,8 @@ data: ${JSON.stringify({ ok: true })}
1442
2441
  }
1443
2442
  next();
1444
2443
  } catch (error) {
1445
- const message = getErrorMessage(error, "Unknown bridge error");
1446
- setJson(res, 502, { error: message });
2444
+ const message = getErrorMessage2(error, "Unknown bridge error");
2445
+ setJson2(res, 502, { error: message });
1447
2446
  }
1448
2447
  };
1449
2448
  middleware.dispose = () => {
@@ -1580,8 +2579,8 @@ function createAuthSession(password) {
1580
2579
  }
1581
2580
 
1582
2581
  // 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";
2582
+ import { dirname, extname, join as join3 } from "path";
2583
+ import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
1585
2584
  var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
1586
2585
  ".txt",
1587
2586
  ".md",
@@ -1696,7 +2695,7 @@ async function probeFileIsText(localPath) {
1696
2695
  async function isTextEditableFile(localPath) {
1697
2696
  if (isTextEditablePath(localPath)) return true;
1698
2697
  try {
1699
- const fileStat = await stat2(localPath);
2698
+ const fileStat = await stat3(localPath);
1700
2699
  if (!fileStat.isFile()) return false;
1701
2700
  return await probeFileIsText(localPath);
1702
2701
  } catch {
@@ -1716,10 +2715,10 @@ function escapeForInlineScriptString(value) {
1716
2715
  return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
1717
2716
  }
1718
2717
  async function getDirectoryItems(localPath) {
1719
- const entries = await readdir2(localPath, { withFileTypes: true });
2718
+ const entries = await readdir3(localPath, { withFileTypes: true });
1720
2719
  const withMeta = await Promise.all(entries.map(async (entry) => {
1721
- const entryPath = join2(localPath, entry.name);
1722
- const entryStat = await stat2(entryPath);
2720
+ const entryPath = join3(localPath, entry.name);
2721
+ const entryStat = await stat3(entryPath);
1723
2722
  const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
1724
2723
  return {
1725
2724
  name: entry.name,
@@ -1777,7 +2776,7 @@ async function createDirectoryListingHtml(localPath) {
1777
2776
  </html>`;
1778
2777
  }
1779
2778
  async function createTextEditorHtml(localPath) {
1780
- const content = await readFile2(localPath, "utf8");
2779
+ const content = await readFile3(localPath, "utf8");
1781
2780
  const parentPath = dirname(localPath);
1782
2781
  const language = languageForPath(localPath);
1783
2782
  const safeContentLiteral = escapeForInlineScriptString(content);
@@ -1848,8 +2847,8 @@ async function createTextEditorHtml(localPath) {
1848
2847
  // src/server/httpServer.ts
1849
2848
  import { WebSocketServer } from "ws";
1850
2849
  var __dirname = dirname2(fileURLToPath(import.meta.url));
1851
- var distDir = join3(__dirname, "..", "dist");
1852
- var spaEntryFile = join3(distDir, "index.html");
2850
+ var distDir = join4(__dirname, "..", "dist");
2851
+ var spaEntryFile = join4(distDir, "index.html");
1853
2852
  var IMAGE_CONTENT_TYPES = {
1854
2853
  ".avif": "image/avif",
1855
2854
  ".bmp": "image/bmp",
@@ -1860,6 +2859,20 @@ var IMAGE_CONTENT_TYPES = {
1860
2859
  ".svg": "image/svg+xml",
1861
2860
  ".webp": "image/webp"
1862
2861
  };
2862
+ function renderFrontendMissingHtml(message, details) {
2863
+ const lines = details && details.length > 0 ? `<pre>${details.join("\n")}</pre>` : "";
2864
+ return [
2865
+ "<!doctype html>",
2866
+ '<html lang="en">',
2867
+ '<head><meta charset="utf-8"><title>Codex Web UI Error</title></head>',
2868
+ "<body>",
2869
+ `<h1>${message}</h1>`,
2870
+ lines,
2871
+ '<p><a href="/">Back to chat</a></p>',
2872
+ "</body>",
2873
+ "</html>"
2874
+ ].join("");
2875
+ }
1863
2876
  function normalizeLocalImagePath(rawPath) {
1864
2877
  const trimmed = rawPath.trim();
1865
2878
  if (!trimmed) return "";
@@ -1926,7 +2939,7 @@ function createServer(options = {}) {
1926
2939
  return;
1927
2940
  }
1928
2941
  try {
1929
- const fileStat = await stat3(localPath);
2942
+ const fileStat = await stat4(localPath);
1930
2943
  res.setHeader("Cache-Control", "private, no-store");
1931
2944
  if (fileStat.isDirectory()) {
1932
2945
  const html = await createDirectoryListingHtml(localPath);
@@ -1949,7 +2962,7 @@ function createServer(options = {}) {
1949
2962
  return;
1950
2963
  }
1951
2964
  try {
1952
- const fileStat = await stat3(localPath);
2965
+ const fileStat = await stat4(localPath);
1953
2966
  if (!fileStat.isFile()) {
1954
2967
  res.status(400).json({ error: "Expected file path." });
1955
2968
  return;
@@ -1973,32 +2986,31 @@ function createServer(options = {}) {
1973
2986
  }
1974
2987
  const body = typeof req.body === "string" ? req.body : "";
1975
2988
  try {
1976
- await writeFile2(localPath, body, "utf8");
2989
+ await writeFile3(localPath, body, "utf8");
1977
2990
  res.status(200).json({ ok: true });
1978
2991
  } catch {
1979
2992
  res.status(404).json({ error: "File not found." });
1980
2993
  }
1981
2994
  });
1982
- const hasFrontendAssets = existsSync(spaEntryFile);
2995
+ const hasFrontendAssets = existsSync3(spaEntryFile);
1983
2996
  if (hasFrontendAssets) {
1984
2997
  app.use(express.static(distDir));
1985
2998
  }
1986
2999
  app.use((_req, res) => {
1987
3000
  if (!hasFrontendAssets) {
1988
- res.status(503).type("text/plain").send(
1989
- [
1990
- "Codex web UI assets are missing.",
3001
+ res.status(503).type("text/html; charset=utf-8").send(
3002
+ renderFrontendMissingHtml("Codex web UI assets are missing.", [
1991
3003
  `Expected: ${spaEntryFile}`,
1992
3004
  "If running from source, build frontend assets with: npm run build:frontend",
1993
3005
  "If running with npx, clear the npx cache and reinstall codexapp."
1994
- ].join("\n")
3006
+ ])
1995
3007
  );
1996
3008
  return;
1997
3009
  }
1998
3010
  res.sendFile(spaEntryFile, (error) => {
1999
3011
  if (!error) return;
2000
3012
  if (!res.headersSent) {
2001
- res.status(404).type("text/plain").send("Frontend entry file not found.");
3013
+ res.status(404).type("text/html; charset=utf-8").send(renderFrontendMissingHtml("Frontend entry file not found."));
2002
3014
  }
2003
3015
  });
2004
3016
  });
@@ -2053,8 +3065,8 @@ var program = new Command().name("codexui").description("Web interface for Codex
2053
3065
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
2054
3066
  async function readCliVersion() {
2055
3067
  try {
2056
- const packageJsonPath = join4(__dirname2, "..", "package.json");
2057
- const raw = await readFile3(packageJsonPath, "utf8");
3068
+ const packageJsonPath = join5(__dirname2, "..", "package.json");
3069
+ const raw = await readFile4(packageJsonPath, "utf8");
2058
3070
  const parsed = JSON.parse(raw);
2059
3071
  return typeof parsed.version === "string" ? parsed.version : "unknown";
2060
3072
  } catch {
@@ -2079,22 +3091,22 @@ function runWithStatus(command, args) {
2079
3091
  return result.status ?? -1;
2080
3092
  }
2081
3093
  function getUserNpmPrefix() {
2082
- return join4(homedir2(), ".npm-global");
3094
+ return join5(homedir3(), ".npm-global");
2083
3095
  }
2084
3096
  function resolveCodexCommand() {
2085
3097
  if (canRun("codex", ["--version"])) {
2086
3098
  return "codex";
2087
3099
  }
2088
- const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
2089
- if (existsSync2(userCandidate) && canRun(userCandidate, ["--version"])) {
3100
+ const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
3101
+ if (existsSync4(userCandidate) && canRun(userCandidate, ["--version"])) {
2090
3102
  return userCandidate;
2091
3103
  }
2092
3104
  const prefix = process.env.PREFIX?.trim();
2093
3105
  if (!prefix) {
2094
3106
  return null;
2095
3107
  }
2096
- const candidate = join4(prefix, "bin", "codex");
2097
- if (existsSync2(candidate) && canRun(candidate, ["--version"])) {
3108
+ const candidate = join5(prefix, "bin", "codex");
3109
+ if (existsSync4(candidate) && canRun(candidate, ["--version"])) {
2098
3110
  return candidate;
2099
3111
  }
2100
3112
  return null;
@@ -2103,8 +3115,8 @@ function resolveCloudflaredCommand() {
2103
3115
  if (canRun("cloudflared", ["--version"])) {
2104
3116
  return "cloudflared";
2105
3117
  }
2106
- const localCandidate = join4(homedir2(), ".local", "bin", "cloudflared");
2107
- if (existsSync2(localCandidate) && canRun(localCandidate, ["--version"])) {
3118
+ const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
3119
+ if (existsSync4(localCandidate) && canRun(localCandidate, ["--version"])) {
2108
3120
  return localCandidate;
2109
3121
  }
2110
3122
  return null;
@@ -2119,7 +3131,7 @@ function mapCloudflaredLinuxArch(arch) {
2119
3131
  return null;
2120
3132
  }
2121
3133
  function downloadFile(url, destination) {
2122
- return new Promise((resolve2, reject) => {
3134
+ return new Promise((resolve3, reject) => {
2123
3135
  const request = (currentUrl) => {
2124
3136
  httpsGet(currentUrl, (response) => {
2125
3137
  const code = response.statusCode ?? 0;
@@ -2137,7 +3149,7 @@ function downloadFile(url, destination) {
2137
3149
  response.pipe(file);
2138
3150
  file.on("finish", () => {
2139
3151
  file.close();
2140
- resolve2();
3152
+ resolve3();
2141
3153
  });
2142
3154
  file.on("error", reject);
2143
3155
  }).on("error", reject);
@@ -2157,9 +3169,9 @@ async function ensureCloudflaredInstalledLinux() {
2157
3169
  if (!mappedArch) {
2158
3170
  throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
2159
3171
  }
2160
- const userBinDir = join4(homedir2(), ".local", "bin");
3172
+ const userBinDir = join5(homedir3(), ".local", "bin");
2161
3173
  mkdirSync(userBinDir, { recursive: true });
2162
- const destination = join4(userBinDir, "cloudflared");
3174
+ const destination = join5(userBinDir, "cloudflared");
2163
3175
  const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
2164
3176
  console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
2165
3177
  await downloadFile(downloadUrl, destination);
@@ -2177,7 +3189,7 @@ async function shouldInstallCloudflaredInteractively() {
2177
3189
  console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
2178
3190
  return false;
2179
3191
  }
2180
- const prompt = createInterface({ input: process.stdin, output: process.stdout });
3192
+ const prompt = createInterface2({ input: process.stdin, output: process.stdout });
2181
3193
  try {
2182
3194
  const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
2183
3195
  const normalized = answer.trim().toLowerCase();
@@ -2198,8 +3210,8 @@ async function resolveCloudflaredForTunnel() {
2198
3210
  return ensureCloudflaredInstalledLinux();
2199
3211
  }
2200
3212
  function hasCodexAuth() {
2201
- const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
2202
- return existsSync2(join4(codexHome, "auth.json"));
3213
+ const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3214
+ return existsSync4(join5(codexHome, "auth.json"));
2203
3215
  }
2204
3216
  function ensureCodexInstalled() {
2205
3217
  let codexCommand = resolveCodexCommand();
@@ -2217,7 +3229,7 @@ function ensureCodexInstalled() {
2217
3229
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
2218
3230
  `);
2219
3231
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
2220
- process.env.PATH = `${join4(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
3232
+ process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
2221
3233
  };
2222
3234
  if (isTermuxRuntime()) {
2223
3235
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -2266,7 +3278,7 @@ function printTermuxKeepAlive(lines) {
2266
3278
  }
2267
3279
  function openBrowser(url) {
2268
3280
  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" });
3281
+ const child = spawn3(command.cmd, command.args, { detached: true, stdio: "ignore" });
2270
3282
  child.on("error", () => {
2271
3283
  });
2272
3284
  child.unref();
@@ -2280,25 +3292,28 @@ function parseCloudflaredUrl(chunk) {
2280
3292
  }
2281
3293
  function getAccessibleUrls(port) {
2282
3294
  const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
2283
- const interfaces = networkInterfaces();
2284
- for (const entries of Object.values(interfaces)) {
2285
- if (!entries) {
2286
- continue;
2287
- }
2288
- for (const entry of entries) {
2289
- if (entry.internal) {
3295
+ try {
3296
+ const interfaces = networkInterfaces();
3297
+ for (const entries of Object.values(interfaces)) {
3298
+ if (!entries) {
2290
3299
  continue;
2291
3300
  }
2292
- if (entry.family === "IPv4") {
2293
- urls.add(`http://${entry.address}:${String(port)}`);
3301
+ for (const entry of entries) {
3302
+ if (entry.internal) {
3303
+ continue;
3304
+ }
3305
+ if (entry.family === "IPv4") {
3306
+ urls.add(`http://${entry.address}:${String(port)}`);
3307
+ }
2294
3308
  }
2295
3309
  }
3310
+ } catch {
2296
3311
  }
2297
3312
  return Array.from(urls);
2298
3313
  }
2299
3314
  async function startCloudflaredTunnel(command, localPort) {
2300
- return new Promise((resolve2, reject) => {
2301
- const child = spawn2(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
3315
+ return new Promise((resolve3, reject) => {
3316
+ const child = spawn3(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
2302
3317
  stdio: ["ignore", "pipe", "pipe"]
2303
3318
  });
2304
3319
  const timeout = setTimeout(() => {
@@ -2314,7 +3329,7 @@ async function startCloudflaredTunnel(command, localPort) {
2314
3329
  clearTimeout(timeout);
2315
3330
  child.stdout?.off("data", handleData);
2316
3331
  child.stderr?.off("data", handleData);
2317
- resolve2({ process: child, url: parsedUrl });
3332
+ resolve3({ process: child, url: parsedUrl });
2318
3333
  };
2319
3334
  const onError = (error) => {
2320
3335
  clearTimeout(timeout);
@@ -2333,7 +3348,7 @@ async function startCloudflaredTunnel(command, localPort) {
2333
3348
  });
2334
3349
  }
2335
3350
  function listenWithFallback(server, startPort) {
2336
- return new Promise((resolve2, reject) => {
3351
+ return new Promise((resolve3, reject) => {
2337
3352
  const attempt = (port) => {
2338
3353
  const onError = (error) => {
2339
3354
  server.off("listening", onListening);
@@ -2345,7 +3360,7 @@ function listenWithFallback(server, startPort) {
2345
3360
  };
2346
3361
  const onListening = () => {
2347
3362
  server.off("error", onError);
2348
- resolve2(port);
3363
+ resolve3(port);
2349
3364
  };
2350
3365
  server.once("error", onError);
2351
3366
  server.once("listening", onListening);
@@ -2354,8 +3369,72 @@ function listenWithFallback(server, startPort) {
2354
3369
  attempt(startPort);
2355
3370
  });
2356
3371
  }
3372
+ function getCodexGlobalStatePath2() {
3373
+ const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3374
+ return join5(codexHome, ".codex-global-state.json");
3375
+ }
3376
+ function normalizeUniqueStrings(value) {
3377
+ if (!Array.isArray(value)) return [];
3378
+ const next = [];
3379
+ for (const item of value) {
3380
+ if (typeof item !== "string") continue;
3381
+ const trimmed = item.trim();
3382
+ if (!trimmed || next.includes(trimmed)) continue;
3383
+ next.push(trimmed);
3384
+ }
3385
+ return next;
3386
+ }
3387
+ async function persistLaunchProject(projectPath) {
3388
+ const trimmed = projectPath.trim();
3389
+ if (!trimmed) return;
3390
+ const normalizedPath = isAbsolute3(trimmed) ? trimmed : resolve2(trimmed);
3391
+ const directoryInfo = await stat5(normalizedPath);
3392
+ if (!directoryInfo.isDirectory()) {
3393
+ throw new Error(`Not a directory: ${normalizedPath}`);
3394
+ }
3395
+ const statePath = getCodexGlobalStatePath2();
3396
+ let payload = {};
3397
+ try {
3398
+ const raw = await readFile4(statePath, "utf8");
3399
+ const parsed = JSON.parse(raw);
3400
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3401
+ payload = parsed;
3402
+ }
3403
+ } catch {
3404
+ payload = {};
3405
+ }
3406
+ const roots = normalizeUniqueStrings(payload["electron-saved-workspace-roots"]);
3407
+ const activeRoots = normalizeUniqueStrings(payload["active-workspace-roots"]);
3408
+ payload["electron-saved-workspace-roots"] = [
3409
+ normalizedPath,
3410
+ ...roots.filter((value) => value !== normalizedPath)
3411
+ ];
3412
+ payload["active-workspace-roots"] = [
3413
+ normalizedPath,
3414
+ ...activeRoots.filter((value) => value !== normalizedPath)
3415
+ ];
3416
+ await writeFile4(statePath, JSON.stringify(payload), "utf8");
3417
+ }
3418
+ async function addProjectOnly(projectPath) {
3419
+ const trimmed = projectPath.trim();
3420
+ if (!trimmed) {
3421
+ throw new Error("Missing project path");
3422
+ }
3423
+ await persistLaunchProject(trimmed);
3424
+ }
2357
3425
  async function startServer(options) {
2358
3426
  const version = await readCliVersion();
3427
+ const projectPath = options.projectPath?.trim() ?? "";
3428
+ if (projectPath.length > 0) {
3429
+ try {
3430
+ await persistLaunchProject(projectPath);
3431
+ } catch (error) {
3432
+ const message = error instanceof Error ? error.message : String(error);
3433
+ console.warn(`
3434
+ [project] Could not open launch project: ${message}
3435
+ `);
3436
+ }
3437
+ }
2359
3438
  const codexCommand = ensureCodexInstalled() ?? resolveCodexCommand();
2360
3439
  if (!hasCodexAuth() && codexCommand) {
2361
3440
  console.log("\nCodex is not logged in. Starting `codex login`...\n");
@@ -2416,7 +3495,7 @@ async function startServer(options) {
2416
3495
  qrcode.generate(tunnelUrl, { small: true });
2417
3496
  console.log("");
2418
3497
  }
2419
- openBrowser(`http://localhost:${String(port)}`);
3498
+ if (options.open) openBrowser(`http://localhost:${String(port)}`);
2420
3499
  function shutdown() {
2421
3500
  console.log("\nShutting down...");
2422
3501
  if (tunnelChild && !tunnelChild.killed) {
@@ -2439,8 +3518,20 @@ async function runLogin() {
2439
3518
  console.log("\nStarting `codex login`...\n");
2440
3519
  runOrFail(codexCommand, ["login"], "Codex login");
2441
3520
  }
2442
- program.option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").action(async (opts) => {
2443
- await startServer(opts);
3521
+ program.argument("[projectPath]", "project directory to open on launch").option("--open-project <path>", "open project directory on launch (Codex desktop parity)").option("-p, --port <port>", "port to listen on", "5999").option("--password <pass>", "set a specific password").option("--no-password", "disable password protection").option("--tunnel", "start cloudflared tunnel", true).option("--no-tunnel", "disable cloudflared tunnel startup").option("--open", "open browser on startup", true).option("--no-open", "do not open browser on startup").action(async (projectPath, opts) => {
3522
+ const rawArgv = process.argv.slice(2);
3523
+ const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
3524
+ let openProjectOnly = (opts.openProject ?? "").trim();
3525
+ if (!openProjectOnly && openProjectFlagIndex >= 0 && projectPath?.trim()) {
3526
+ openProjectOnly = projectPath.trim();
3527
+ }
3528
+ if (openProjectOnly.length > 0) {
3529
+ await addProjectOnly(openProjectOnly);
3530
+ console.log(`Added project: ${openProjectOnly}`);
3531
+ return;
3532
+ }
3533
+ const launchProject = (projectPath ?? "").trim();
3534
+ await startServer({ ...opts, projectPath: launchProject });
2444
3535
  });
2445
3536
  program.command("login").description("Install/check Codex CLI and run `codex login`").action(runLogin);
2446
3537
  program.command("help").description("Show codexui command help").action(() => {