codexapp 0.1.45 → 0.1.47

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();
1243
+ });
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}`));
291
1253
  });
292
- }
293
- skillsTreeCache = { entries, fetchedAt: Date.now() };
294
- return entries;
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,11 +1323,11 @@ 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);
399
1332
  const token = auth.tokens?.access_token;
400
1333
  if (!token) return null;
@@ -404,13 +1337,21 @@ async function readCodexAuth() {
404
1337
  }
405
1338
  }
406
1339
  function getCodexGlobalStatePath() {
407
- return join(getCodexHomeDir(), ".codex-global-state.json");
1340
+ return join2(getCodexHomeDir2(), ".codex-global-state.json");
1341
+ }
1342
+ function getCodexSessionIndexPath() {
1343
+ return join2(getCodexHomeDir2(), "session_index.jsonl");
408
1344
  }
409
1345
  var MAX_THREAD_TITLES = 500;
1346
+ var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
1347
+ var sessionIndexThreadTitleCacheState = {
1348
+ fileSignature: null,
1349
+ cache: EMPTY_THREAD_TITLE_CACHE
1350
+ };
410
1351
  function normalizeThreadTitleCache(value) {
411
- const record = asRecord(value);
412
- if (!record) return { titles: {}, order: [] };
413
- const rawTitles = asRecord(record.titles);
1352
+ const record = asRecord2(value);
1353
+ if (!record) return EMPTY_THREAD_TITLE_CACHE;
1354
+ const rawTitles = asRecord2(record.titles);
414
1355
  const titles = {};
415
1356
  if (rawTitles) {
416
1357
  for (const [k, v] of Object.entries(rawTitles)) {
@@ -433,35 +1374,139 @@ function removeFromThreadTitleCache(cache, id) {
433
1374
  const { [id]: _, ...titles } = cache.titles;
434
1375
  return { titles, order: cache.order.filter((o) => o !== id) };
435
1376
  }
1377
+ function normalizeSessionIndexThreadTitle(value) {
1378
+ const record = asRecord2(value);
1379
+ if (!record) return null;
1380
+ const id = typeof record.id === "string" ? record.id.trim() : "";
1381
+ const title = typeof record.thread_name === "string" ? record.thread_name.trim() : "";
1382
+ const updatedAtIso = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
1383
+ const updatedAtMs = updatedAtIso ? Date.parse(updatedAtIso) : Number.NaN;
1384
+ if (!id || !title) return null;
1385
+ return {
1386
+ id,
1387
+ title,
1388
+ updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : 0
1389
+ };
1390
+ }
1391
+ function trimThreadTitleCache(cache) {
1392
+ const titles = { ...cache.titles };
1393
+ const order = cache.order.filter((id) => {
1394
+ if (!titles[id]) return false;
1395
+ return true;
1396
+ }).slice(0, MAX_THREAD_TITLES);
1397
+ for (const id of Object.keys(titles)) {
1398
+ if (!order.includes(id)) {
1399
+ delete titles[id];
1400
+ }
1401
+ }
1402
+ return { titles, order };
1403
+ }
1404
+ function mergeThreadTitleCaches(base, overlay) {
1405
+ const titles = { ...base.titles, ...overlay.titles };
1406
+ const order = [];
1407
+ for (const id of [...overlay.order, ...base.order]) {
1408
+ if (!titles[id] || order.includes(id)) continue;
1409
+ order.push(id);
1410
+ }
1411
+ for (const id of Object.keys(titles)) {
1412
+ if (!order.includes(id)) {
1413
+ order.push(id);
1414
+ }
1415
+ }
1416
+ return trimThreadTitleCache({ titles, order });
1417
+ }
436
1418
  async function readThreadTitleCache() {
437
1419
  const statePath = getCodexGlobalStatePath();
438
1420
  try {
439
- const raw = await readFile(statePath, "utf8");
440
- const payload = asRecord(JSON.parse(raw)) ?? {};
1421
+ const raw = await readFile2(statePath, "utf8");
1422
+ const payload = asRecord2(JSON.parse(raw)) ?? {};
441
1423
  return normalizeThreadTitleCache(payload["thread-titles"]);
442
1424
  } catch {
443
- return { titles: {}, order: [] };
1425
+ return EMPTY_THREAD_TITLE_CACHE;
444
1426
  }
445
1427
  }
446
1428
  async function writeThreadTitleCache(cache) {
447
1429
  const statePath = getCodexGlobalStatePath();
448
1430
  let payload = {};
449
1431
  try {
450
- const raw = await readFile(statePath, "utf8");
451
- payload = asRecord(JSON.parse(raw)) ?? {};
1432
+ const raw = await readFile2(statePath, "utf8");
1433
+ payload = asRecord2(JSON.parse(raw)) ?? {};
452
1434
  } catch {
453
1435
  payload = {};
454
1436
  }
455
1437
  payload["thread-titles"] = cache;
456
- await writeFile(statePath, JSON.stringify(payload), "utf8");
1438
+ await writeFile2(statePath, JSON.stringify(payload), "utf8");
1439
+ }
1440
+ function getSessionIndexFileSignature(stats) {
1441
+ return `${String(stats.mtimeMs)}:${String(stats.size)}`;
1442
+ }
1443
+ async function parseThreadTitlesFromSessionIndex(sessionIndexPath) {
1444
+ const latestById = /* @__PURE__ */ new Map();
1445
+ const input = createReadStream(sessionIndexPath, { encoding: "utf8" });
1446
+ const lines = createInterface({
1447
+ input,
1448
+ crlfDelay: Infinity
1449
+ });
1450
+ try {
1451
+ for await (const line of lines) {
1452
+ const trimmed = line.trim();
1453
+ if (!trimmed) continue;
1454
+ try {
1455
+ const entry = normalizeSessionIndexThreadTitle(JSON.parse(trimmed));
1456
+ if (!entry) continue;
1457
+ const previous = latestById.get(entry.id);
1458
+ if (!previous || entry.updatedAtMs >= previous.updatedAtMs) {
1459
+ latestById.set(entry.id, entry);
1460
+ }
1461
+ } catch {
1462
+ }
1463
+ }
1464
+ } finally {
1465
+ lines.close();
1466
+ input.close();
1467
+ }
1468
+ const entries = Array.from(latestById.values()).sort((first, second) => second.updatedAtMs - first.updatedAtMs);
1469
+ const titles = {};
1470
+ const order = [];
1471
+ for (const entry of entries) {
1472
+ titles[entry.id] = entry.title;
1473
+ order.push(entry.id);
1474
+ }
1475
+ return trimThreadTitleCache({ titles, order });
1476
+ }
1477
+ async function readThreadTitlesFromSessionIndex() {
1478
+ const sessionIndexPath = getCodexSessionIndexPath();
1479
+ try {
1480
+ const stats = await stat2(sessionIndexPath);
1481
+ const fileSignature = getSessionIndexFileSignature(stats);
1482
+ if (sessionIndexThreadTitleCacheState.fileSignature === fileSignature) {
1483
+ return sessionIndexThreadTitleCacheState.cache;
1484
+ }
1485
+ const cache = await parseThreadTitlesFromSessionIndex(sessionIndexPath);
1486
+ sessionIndexThreadTitleCacheState = { fileSignature, cache };
1487
+ return cache;
1488
+ } catch {
1489
+ sessionIndexThreadTitleCacheState = {
1490
+ fileSignature: "missing",
1491
+ cache: EMPTY_THREAD_TITLE_CACHE
1492
+ };
1493
+ return sessionIndexThreadTitleCacheState.cache;
1494
+ }
1495
+ }
1496
+ async function readMergedThreadTitleCache() {
1497
+ const [sessionIndexCache, persistedCache] = await Promise.all([
1498
+ readThreadTitlesFromSessionIndex(),
1499
+ readThreadTitleCache()
1500
+ ]);
1501
+ return mergeThreadTitleCaches(persistedCache, sessionIndexCache);
457
1502
  }
458
1503
  async function readWorkspaceRootsState() {
459
1504
  const statePath = getCodexGlobalStatePath();
460
1505
  let payload = {};
461
1506
  try {
462
- const raw = await readFile(statePath, "utf8");
1507
+ const raw = await readFile2(statePath, "utf8");
463
1508
  const parsed = JSON.parse(raw);
464
- payload = asRecord(parsed) ?? {};
1509
+ payload = asRecord2(parsed) ?? {};
465
1510
  } catch {
466
1511
  payload = {};
467
1512
  }
@@ -475,15 +1520,15 @@ async function writeWorkspaceRootsState(nextState) {
475
1520
  const statePath = getCodexGlobalStatePath();
476
1521
  let payload = {};
477
1522
  try {
478
- const raw = await readFile(statePath, "utf8");
479
- payload = asRecord(JSON.parse(raw)) ?? {};
1523
+ const raw = await readFile2(statePath, "utf8");
1524
+ payload = asRecord2(JSON.parse(raw)) ?? {};
480
1525
  } catch {
481
1526
  payload = {};
482
1527
  }
483
1528
  payload["electron-saved-workspace-roots"] = normalizeStringArray(nextState.order);
484
1529
  payload["electron-workspace-root-labels"] = normalizeStringRecord(nextState.labels);
485
1530
  payload["active-workspace-roots"] = normalizeStringArray(nextState.active);
486
- await writeFile(statePath, JSON.stringify(payload), "utf8");
1531
+ await writeFile2(statePath, JSON.stringify(payload), "utf8");
487
1532
  }
488
1533
  async function readJsonBody(req) {
489
1534
  const raw = await readRawBody(req);
@@ -521,7 +1566,7 @@ function handleFileUpload(req, res) {
521
1566
  const contentType = req.headers["content-type"] ?? "";
522
1567
  const boundaryMatch = contentType.match(/boundary=(.+)/i);
523
1568
  if (!boundaryMatch) {
524
- setJson(res, 400, { error: "Missing multipart boundary" });
1569
+ setJson2(res, 400, { error: "Missing multipart boundary" });
525
1570
  return;
526
1571
  }
527
1572
  const boundary = boundaryMatch[1];
@@ -551,49 +1596,96 @@ function handleFileUpload(req, res) {
551
1596
  break;
552
1597
  }
553
1598
  if (!fileData) {
554
- setJson(res, 400, { error: "No file in request" });
1599
+ setJson2(res, 400, { error: "No file in request" });
555
1600
  return;
556
1601
  }
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 });
1602
+ const uploadDir = join2(tmpdir2(), "codex-web-uploads");
1603
+ await mkdir2(uploadDir, { recursive: true });
1604
+ const destDir = await mkdtemp2(join2(uploadDir, "f-"));
1605
+ const destPath = join2(destDir, fileName);
1606
+ await writeFile2(destPath, fileData);
1607
+ setJson2(res, 200, { path: destPath });
563
1608
  } catch (err) {
564
- setJson(res, 500, { error: getErrorMessage(err, "Upload failed") });
1609
+ setJson2(res, 500, { error: getErrorMessage2(err, "Upload failed") });
565
1610
  }
566
1611
  });
567
1612
  req.on("error", (err) => {
568
- setJson(res, 500, { error: getErrorMessage(err, "Upload stream error") });
1613
+ setJson2(res, 500, { error: getErrorMessage2(err, "Upload stream error") });
1614
+ });
1615
+ }
1616
+ function httpPost(url, headers, body) {
1617
+ const doRequest = url.startsWith("http://") ? httpRequest : httpsRequest;
1618
+ return new Promise((resolve3, reject) => {
1619
+ const req = doRequest(url, { method: "POST", headers }, (res) => {
1620
+ const chunks = [];
1621
+ res.on("data", (c) => chunks.push(c));
1622
+ res.on("end", () => resolve3({ status: res.statusCode ?? 500, body: Buffer.concat(chunks).toString("utf8") }));
1623
+ res.on("error", reject);
1624
+ });
1625
+ req.on("error", reject);
1626
+ req.write(body);
1627
+ req.end();
1628
+ });
1629
+ }
1630
+ var curlImpersonateAvailable = null;
1631
+ function curlImpersonatePost(url, headers, body) {
1632
+ return new Promise((resolve3, reject) => {
1633
+ const args = ["-s", "-w", "\n%{http_code}", "-X", "POST", url];
1634
+ for (const [k, v] of Object.entries(headers)) {
1635
+ if (k.toLowerCase() === "content-length") continue;
1636
+ args.push("-H", `${k}: ${String(v)}`);
1637
+ }
1638
+ args.push("--data-binary", "@-");
1639
+ const proc = spawn2("curl-impersonate-chrome", args, {
1640
+ env: { ...process.env, CURL_IMPERSONATE: "chrome116" },
1641
+ stdio: ["pipe", "pipe", "pipe"]
1642
+ });
1643
+ const chunks = [];
1644
+ proc.stdout.on("data", (c) => chunks.push(c));
1645
+ proc.on("error", (e) => {
1646
+ curlImpersonateAvailable = false;
1647
+ reject(e);
1648
+ });
1649
+ proc.on("close", (code) => {
1650
+ const raw = Buffer.concat(chunks).toString("utf8");
1651
+ const lastNewline = raw.lastIndexOf("\n");
1652
+ const statusStr = lastNewline >= 0 ? raw.slice(lastNewline + 1).trim() : "";
1653
+ const responseBody = lastNewline >= 0 ? raw.slice(0, lastNewline) : raw;
1654
+ const status = parseInt(statusStr, 10) || (code === 0 ? 200 : 500);
1655
+ curlImpersonateAvailable = true;
1656
+ resolve3({ status, body: responseBody });
1657
+ });
1658
+ proc.stdin.write(body);
1659
+ proc.stdin.end();
569
1660
  });
570
1661
  }
571
1662
  async function proxyTranscribe(body, contentType, authToken, accountId) {
572
- const headers = {
1663
+ const chatgptHeaders = {
573
1664
  "Content-Type": contentType,
574
1665
  "Content-Length": body.length,
575
1666
  Authorization: `Bearer ${authToken}`,
576
1667
  originator: "Codex Desktop",
577
1668
  "User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`
578
1669
  };
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
- });
1670
+ if (accountId) chatgptHeaders["ChatGPT-Account-Id"] = accountId;
1671
+ const postFn = curlImpersonateAvailable !== false ? curlImpersonatePost : httpPost;
1672
+ let result;
1673
+ try {
1674
+ result = await postFn("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
1675
+ } catch {
1676
+ result = await httpPost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
1677
+ }
1678
+ if (result.status === 403 && result.body.includes("cf_chl")) {
1679
+ if (curlImpersonateAvailable !== false && postFn !== curlImpersonatePost) {
1680
+ try {
1681
+ const ciResult = await curlImpersonatePost("https://chatgpt.com/backend-api/transcribe", chatgptHeaders, body);
1682
+ if (ciResult.status !== 403) return ciResult;
1683
+ } catch {
1684
+ }
1685
+ }
1686
+ return { status: 503, body: JSON.stringify({ error: "Transcription blocked by Cloudflare. Install curl-impersonate-chrome." }) };
1687
+ }
1688
+ return result;
597
1689
  }
598
1690
  var AppServerProcess = class {
599
1691
  constructor() {
@@ -617,7 +1709,7 @@ var AppServerProcess = class {
617
1709
  start() {
618
1710
  if (this.process) return;
619
1711
  this.stopping = false;
620
- const proc = spawn("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
1712
+ const proc = spawn2("codex", this.appServerArgs, { stdio: ["pipe", "pipe", "pipe"] });
621
1713
  this.process = proc;
622
1714
  proc.stdout.setEncoding("utf8");
623
1715
  proc.stdout.on("data", (chunk) => {
@@ -711,7 +1803,7 @@ var AppServerProcess = class {
711
1803
  }
712
1804
  this.pendingServerRequests.delete(requestId);
713
1805
  this.sendServerRequestReply(requestId, reply);
714
- const requestParams = asRecord(pendingRequest.params);
1806
+ const requestParams = asRecord2(pendingRequest.params);
715
1807
  const threadId = typeof requestParams?.threadId === "string" && requestParams.threadId.length > 0 ? requestParams.threadId : "";
716
1808
  this.emitNotification({
717
1809
  method: "server/request/resolved",
@@ -740,8 +1832,8 @@ var AppServerProcess = class {
740
1832
  async call(method, params) {
741
1833
  this.start();
742
1834
  const id = this.nextId++;
743
- return new Promise((resolve2, reject) => {
744
- this.pending.set(id, { resolve: resolve2, reject });
1835
+ return new Promise((resolve3, reject) => {
1836
+ this.pending.set(id, { resolve: resolve3, reject });
745
1837
  this.sendLine({
746
1838
  jsonrpc: "2.0",
747
1839
  id,
@@ -780,7 +1872,7 @@ var AppServerProcess = class {
780
1872
  }
781
1873
  async respondToServerRequest(payload) {
782
1874
  await this.ensureInitialized();
783
- const body = asRecord(payload);
1875
+ const body = asRecord2(payload);
784
1876
  if (!body) {
785
1877
  throw new Error("Invalid response payload: expected object");
786
1878
  }
@@ -788,7 +1880,7 @@ var AppServerProcess = class {
788
1880
  if (typeof id !== "number" || !Number.isInteger(id)) {
789
1881
  throw new Error('Invalid response payload: "id" must be an integer');
790
1882
  }
791
- const rawError = asRecord(body.error);
1883
+ const rawError = asRecord2(body.error);
792
1884
  if (rawError) {
793
1885
  const message = typeof rawError.message === "string" && rawError.message.trim().length > 0 ? rawError.message.trim() : "Server request rejected by client";
794
1886
  const code = typeof rawError.code === "number" && Number.isFinite(rawError.code) ? Math.trunc(rawError.code) : -32e3;
@@ -842,8 +1934,8 @@ var MethodCatalog = class {
842
1934
  this.notificationCache = null;
843
1935
  }
844
1936
  async runGenerateSchemaCommand(outDir) {
845
- await new Promise((resolve2, reject) => {
846
- const process2 = spawn("codex", ["app-server", "generate-json-schema", "--out", outDir], {
1937
+ await new Promise((resolve3, reject) => {
1938
+ const process2 = spawn2("codex", ["app-server", "generate-json-schema", "--out", outDir], {
847
1939
  stdio: ["ignore", "ignore", "pipe"]
848
1940
  });
849
1941
  let stderr = "";
@@ -854,7 +1946,7 @@ var MethodCatalog = class {
854
1946
  process2.on("error", reject);
855
1947
  process2.on("exit", (code) => {
856
1948
  if (code === 0) {
857
- resolve2();
1949
+ resolve3();
858
1950
  return;
859
1951
  }
860
1952
  reject(new Error(stderr.trim() || `generate-json-schema exited with code ${String(code)}`));
@@ -862,13 +1954,13 @@ var MethodCatalog = class {
862
1954
  });
863
1955
  }
864
1956
  extractMethodsFromClientRequest(payload) {
865
- const root = asRecord(payload);
1957
+ const root = asRecord2(payload);
866
1958
  const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
867
1959
  const methods = /* @__PURE__ */ new Set();
868
1960
  for (const entry of oneOf) {
869
- const row = asRecord(entry);
870
- const properties = asRecord(row?.properties);
871
- const methodDef = asRecord(properties?.method);
1961
+ const row = asRecord2(entry);
1962
+ const properties = asRecord2(row?.properties);
1963
+ const methodDef = asRecord2(properties?.method);
872
1964
  const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
873
1965
  for (const item of methodEnum) {
874
1966
  if (typeof item === "string" && item.length > 0) {
@@ -879,13 +1971,13 @@ var MethodCatalog = class {
879
1971
  return Array.from(methods).sort((a, b) => a.localeCompare(b));
880
1972
  }
881
1973
  extractMethodsFromServerNotification(payload) {
882
- const root = asRecord(payload);
1974
+ const root = asRecord2(payload);
883
1975
  const oneOf = Array.isArray(root?.oneOf) ? root.oneOf : [];
884
1976
  const methods = /* @__PURE__ */ new Set();
885
1977
  for (const entry of oneOf) {
886
- const row = asRecord(entry);
887
- const properties = asRecord(row?.properties);
888
- const methodDef = asRecord(properties?.method);
1978
+ const row = asRecord2(entry);
1979
+ const properties = asRecord2(row?.properties);
1980
+ const methodDef = asRecord2(properties?.method);
889
1981
  const methodEnum = Array.isArray(methodDef?.enum) ? methodDef.enum : [];
890
1982
  for (const item of methodEnum) {
891
1983
  if (typeof item === "string" && item.length > 0) {
@@ -899,10 +1991,10 @@ var MethodCatalog = class {
899
1991
  if (this.methodCache) {
900
1992
  return this.methodCache;
901
1993
  }
902
- const outDir = await mkdtemp(join(tmpdir(), "codex-web-local-schema-"));
1994
+ const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
903
1995
  await this.runGenerateSchemaCommand(outDir);
904
- const clientRequestPath = join(outDir, "ClientRequest.json");
905
- const raw = await readFile(clientRequestPath, "utf8");
1996
+ const clientRequestPath = join2(outDir, "ClientRequest.json");
1997
+ const raw = await readFile2(clientRequestPath, "utf8");
906
1998
  const parsed = JSON.parse(raw);
907
1999
  const methods = this.extractMethodsFromClientRequest(parsed);
908
2000
  this.methodCache = methods;
@@ -912,10 +2004,10 @@ var MethodCatalog = class {
912
2004
  if (this.notificationCache) {
913
2005
  return this.notificationCache;
914
2006
  }
915
- const outDir = await mkdtemp(join(tmpdir(), "codex-web-local-schema-"));
2007
+ const outDir = await mkdtemp2(join2(tmpdir2(), "codex-web-local-schema-"));
916
2008
  await this.runGenerateSchemaCommand(outDir);
917
- const serverNotificationPath = join(outDir, "ServerNotification.json");
918
- const raw = await readFile(serverNotificationPath, "utf8");
2009
+ const serverNotificationPath = join2(outDir, "ServerNotification.json");
2010
+ const raw = await readFile2(serverNotificationPath, "utf8");
919
2011
  const parsed = JSON.parse(raw);
920
2012
  const methods = this.extractMethodsFromServerNotification(parsed);
921
2013
  this.notificationCache = methods;
@@ -938,7 +2030,7 @@ async function loadAllThreadsForSearch(appServer) {
938
2030
  const threads = [];
939
2031
  let cursor = null;
940
2032
  do {
941
- const response = asRecord(await appServer.rpc("thread/list", {
2033
+ const response = asRecord2(await appServer.rpc("thread/list", {
942
2034
  archived: false,
943
2035
  limit: 100,
944
2036
  sortKey: "updated_at",
@@ -946,7 +2038,7 @@ async function loadAllThreadsForSearch(appServer) {
946
2038
  }));
947
2039
  const data = Array.isArray(response?.data) ? response.data : [];
948
2040
  for (const row of data) {
949
- const record = asRecord(row);
2041
+ const record = asRecord2(row);
950
2042
  const id = typeof record?.id === "string" ? record.id : "";
951
2043
  if (!id) continue;
952
2044
  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 +2102,7 @@ function createCodexBridgeMiddleware() {
1010
2102
  }
1011
2103
  return threadSearchIndexPromise;
1012
2104
  }
2105
+ void initializeSkillsSyncOnStartup(appServer);
1013
2106
  const middleware = async (req, res, next) => {
1014
2107
  try {
1015
2108
  if (!req.url) {
@@ -1017,25 +2110,28 @@ function createCodexBridgeMiddleware() {
1017
2110
  return;
1018
2111
  }
1019
2112
  const url = new URL(req.url, "http://localhost");
2113
+ if (await handleSkillsRoutes(req, res, url, { appServer, readJsonBody })) {
2114
+ return;
2115
+ }
1020
2116
  if (req.method === "POST" && url.pathname === "/codex-api/upload-file") {
1021
2117
  handleFileUpload(req, res);
1022
2118
  return;
1023
2119
  }
1024
2120
  if (req.method === "POST" && url.pathname === "/codex-api/rpc") {
1025
2121
  const payload = await readJsonBody(req);
1026
- const body = asRecord(payload);
2122
+ const body = asRecord2(payload);
1027
2123
  if (!body || typeof body.method !== "string" || body.method.length === 0) {
1028
- setJson(res, 400, { error: "Invalid body: expected { method, params? }" });
2124
+ setJson2(res, 400, { error: "Invalid body: expected { method, params? }" });
1029
2125
  return;
1030
2126
  }
1031
2127
  const result = await appServer.rpc(body.method, body.params ?? null);
1032
- setJson(res, 200, { result });
2128
+ setJson2(res, 200, { result });
1033
2129
  return;
1034
2130
  }
1035
2131
  if (req.method === "POST" && url.pathname === "/codex-api/transcribe") {
1036
2132
  const auth = await readCodexAuth();
1037
2133
  if (!auth) {
1038
- setJson(res, 401, { error: "No auth token available for transcription" });
2134
+ setJson2(res, 401, { error: "No auth token available for transcription" });
1039
2135
  return;
1040
2136
  }
1041
2137
  const rawBody = await readRawBody(req);
@@ -1049,48 +2145,48 @@ function createCodexBridgeMiddleware() {
1049
2145
  if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
1050
2146
  const payload = await readJsonBody(req);
1051
2147
  await appServer.respondToServerRequest(payload);
1052
- setJson(res, 200, { ok: true });
2148
+ setJson2(res, 200, { ok: true });
1053
2149
  return;
1054
2150
  }
1055
2151
  if (req.method === "GET" && url.pathname === "/codex-api/server-requests/pending") {
1056
- setJson(res, 200, { data: appServer.listPendingServerRequests() });
2152
+ setJson2(res, 200, { data: appServer.listPendingServerRequests() });
1057
2153
  return;
1058
2154
  }
1059
2155
  if (req.method === "GET" && url.pathname === "/codex-api/meta/methods") {
1060
2156
  const methods = await methodCatalog.listMethods();
1061
- setJson(res, 200, { data: methods });
2157
+ setJson2(res, 200, { data: methods });
1062
2158
  return;
1063
2159
  }
1064
2160
  if (req.method === "GET" && url.pathname === "/codex-api/meta/notifications") {
1065
2161
  const methods = await methodCatalog.listNotificationMethods();
1066
- setJson(res, 200, { data: methods });
2162
+ setJson2(res, 200, { data: methods });
1067
2163
  return;
1068
2164
  }
1069
2165
  if (req.method === "GET" && url.pathname === "/codex-api/workspace-roots-state") {
1070
2166
  const state = await readWorkspaceRootsState();
1071
- setJson(res, 200, { data: state });
2167
+ setJson2(res, 200, { data: state });
1072
2168
  return;
1073
2169
  }
1074
2170
  if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
1075
- setJson(res, 200, { data: { path: homedir() } });
2171
+ setJson2(res, 200, { data: { path: homedir2() } });
1076
2172
  return;
1077
2173
  }
1078
2174
  if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
1079
- const payload = asRecord(await readJsonBody(req));
2175
+ const payload = asRecord2(await readJsonBody(req));
1080
2176
  const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
1081
2177
  if (!rawSourceCwd) {
1082
- setJson(res, 400, { error: "Missing sourceCwd" });
2178
+ setJson2(res, 400, { error: "Missing sourceCwd" });
1083
2179
  return;
1084
2180
  }
1085
2181
  const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
1086
2182
  try {
1087
- const sourceInfo = await stat(sourceCwd);
2183
+ const sourceInfo = await stat2(sourceCwd);
1088
2184
  if (!sourceInfo.isDirectory()) {
1089
- setJson(res, 400, { error: "sourceCwd is not a directory" });
2185
+ setJson2(res, 400, { error: "sourceCwd is not a directory" });
1090
2186
  return;
1091
2187
  }
1092
2188
  } catch {
1093
- setJson(res, 404, { error: "sourceCwd does not exist" });
2189
+ setJson2(res, 404, { error: "sourceCwd does not exist" });
1094
2190
  return;
1095
2191
  }
1096
2192
  try {
@@ -1099,25 +2195,25 @@ function createCodexBridgeMiddleware() {
1099
2195
  gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
1100
2196
  } catch (error) {
1101
2197
  if (!isNotGitRepositoryError(error)) throw error;
1102
- await runCommand("git", ["init"], { cwd: sourceCwd });
2198
+ await runCommand2("git", ["init"], { cwd: sourceCwd });
1103
2199
  gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
1104
2200
  }
1105
2201
  const repoName = basename(gitRoot) || "repo";
1106
- const worktreesRoot = join(getCodexHomeDir(), "worktrees");
1107
- await mkdir(worktreesRoot, { recursive: true });
2202
+ const worktreesRoot = join2(getCodexHomeDir2(), "worktrees");
2203
+ await mkdir2(worktreesRoot, { recursive: true });
1108
2204
  let worktreeId = "";
1109
2205
  let worktreeParent = "";
1110
2206
  let worktreeCwd = "";
1111
2207
  for (let attempt = 0; attempt < 12; attempt += 1) {
1112
2208
  const candidate = randomBytes(2).toString("hex");
1113
- const parent = join(worktreesRoot, candidate);
2209
+ const parent = join2(worktreesRoot, candidate);
1114
2210
  try {
1115
- await stat(parent);
2211
+ await stat2(parent);
1116
2212
  continue;
1117
2213
  } catch {
1118
2214
  worktreeId = candidate;
1119
2215
  worktreeParent = parent;
1120
- worktreeCwd = join(parent, repoName);
2216
+ worktreeCwd = join2(parent, repoName);
1121
2217
  break;
1122
2218
  }
1123
2219
  }
@@ -1125,15 +2221,15 @@ function createCodexBridgeMiddleware() {
1125
2221
  throw new Error("Failed to allocate a unique worktree id");
1126
2222
  }
1127
2223
  const branch = `codex/${worktreeId}`;
1128
- await mkdir(worktreeParent, { recursive: true });
2224
+ await mkdir2(worktreeParent, { recursive: true });
1129
2225
  try {
1130
- await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
2226
+ await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1131
2227
  } catch (error) {
1132
2228
  if (!isMissingHeadError(error)) throw error;
1133
2229
  await ensureRepoHasInitialCommit(gitRoot);
1134
- await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
2230
+ await runCommand2("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1135
2231
  }
1136
- setJson(res, 200, {
2232
+ setJson2(res, 200, {
1137
2233
  data: {
1138
2234
  cwd: worktreeCwd,
1139
2235
  branch,
@@ -1141,15 +2237,15 @@ function createCodexBridgeMiddleware() {
1141
2237
  }
1142
2238
  });
1143
2239
  } catch (error) {
1144
- setJson(res, 500, { error: getErrorMessage(error, "Failed to create worktree") });
2240
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to create worktree") });
1145
2241
  }
1146
2242
  return;
1147
2243
  }
1148
2244
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
1149
2245
  const payload = await readJsonBody(req);
1150
- const record = asRecord(payload);
2246
+ const record = asRecord2(payload);
1151
2247
  if (!record) {
1152
- setJson(res, 400, { error: "Invalid body: expected object" });
2248
+ setJson2(res, 400, { error: "Invalid body: expected object" });
1153
2249
  return;
1154
2250
  }
1155
2251
  const nextState = {
@@ -1158,33 +2254,33 @@ function createCodexBridgeMiddleware() {
1158
2254
  active: normalizeStringArray(record.active)
1159
2255
  };
1160
2256
  await writeWorkspaceRootsState(nextState);
1161
- setJson(res, 200, { ok: true });
2257
+ setJson2(res, 200, { ok: true });
1162
2258
  return;
1163
2259
  }
1164
2260
  if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
1165
- const payload = asRecord(await readJsonBody(req));
2261
+ const payload = asRecord2(await readJsonBody(req));
1166
2262
  const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
1167
2263
  const createIfMissing = payload?.createIfMissing === true;
1168
2264
  const label = typeof payload?.label === "string" ? payload.label : "";
1169
2265
  if (!rawPath) {
1170
- setJson(res, 400, { error: "Missing path" });
2266
+ setJson2(res, 400, { error: "Missing path" });
1171
2267
  return;
1172
2268
  }
1173
2269
  const normalizedPath = isAbsolute(rawPath) ? rawPath : resolve(rawPath);
1174
2270
  let pathExists = true;
1175
2271
  try {
1176
- const info = await stat(normalizedPath);
2272
+ const info = await stat2(normalizedPath);
1177
2273
  if (!info.isDirectory()) {
1178
- setJson(res, 400, { error: "Path exists but is not a directory" });
2274
+ setJson2(res, 400, { error: "Path exists but is not a directory" });
1179
2275
  return;
1180
2276
  }
1181
2277
  } catch {
1182
2278
  pathExists = false;
1183
2279
  }
1184
2280
  if (!pathExists && createIfMissing) {
1185
- await mkdir(normalizedPath, { recursive: true });
2281
+ await mkdir2(normalizedPath, { recursive: true });
1186
2282
  } else if (!pathExists) {
1187
- setJson(res, 404, { error: "Directory does not exist" });
2283
+ setJson2(res, 404, { error: "Directory does not exist" });
1188
2284
  return;
1189
2285
  }
1190
2286
  const existingState = await readWorkspaceRootsState();
@@ -1199,215 +2295,103 @@ function createCodexBridgeMiddleware() {
1199
2295
  labels: nextLabels,
1200
2296
  active: nextActive
1201
2297
  });
1202
- setJson(res, 200, { data: { path: normalizedPath } });
2298
+ setJson2(res, 200, { data: { path: normalizedPath } });
1203
2299
  return;
1204
2300
  }
1205
2301
  if (req.method === "GET" && url.pathname === "/codex-api/project-root-suggestion") {
1206
2302
  const basePath = url.searchParams.get("basePath")?.trim() ?? "";
1207
2303
  if (!basePath) {
1208
- setJson(res, 400, { error: "Missing basePath" });
2304
+ setJson2(res, 400, { error: "Missing basePath" });
1209
2305
  return;
1210
2306
  }
1211
2307
  const normalizedBasePath = isAbsolute(basePath) ? basePath : resolve(basePath);
1212
2308
  try {
1213
- const baseInfo = await stat(normalizedBasePath);
2309
+ const baseInfo = await stat2(normalizedBasePath);
1214
2310
  if (!baseInfo.isDirectory()) {
1215
- setJson(res, 400, { error: "basePath is not a directory" });
2311
+ setJson2(res, 400, { error: "basePath is not a directory" });
1216
2312
  return;
1217
2313
  }
1218
2314
  } catch {
1219
- setJson(res, 404, { error: "basePath does not exist" });
2315
+ setJson2(res, 404, { error: "basePath does not exist" });
1220
2316
  return;
1221
2317
  }
1222
2318
  let index = 1;
1223
2319
  while (index < 1e5) {
1224
2320
  const candidateName = `New Project (${String(index)})`;
1225
- const candidatePath = join(normalizedBasePath, candidateName);
2321
+ const candidatePath = join2(normalizedBasePath, candidateName);
1226
2322
  try {
1227
- await stat(candidatePath);
2323
+ await stat2(candidatePath);
1228
2324
  index += 1;
1229
2325
  continue;
1230
2326
  } catch {
1231
- setJson(res, 200, { data: { name: candidateName, path: candidatePath } });
2327
+ setJson2(res, 200, { data: { name: candidateName, path: candidatePath } });
1232
2328
  return;
1233
2329
  }
1234
2330
  }
1235
- setJson(res, 500, { error: "Failed to compute project name suggestion" });
2331
+ setJson2(res, 500, { error: "Failed to compute project name suggestion" });
1236
2332
  return;
1237
2333
  }
1238
2334
  if (req.method === "POST" && url.pathname === "/codex-api/composer-file-search") {
1239
- const payload = asRecord(await readJsonBody(req));
2335
+ const payload = asRecord2(await readJsonBody(req));
1240
2336
  const rawCwd = typeof payload?.cwd === "string" ? payload.cwd.trim() : "";
1241
2337
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
1242
2338
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 20;
1243
2339
  const limit = Math.max(1, Math.min(100, Math.floor(limitRaw)));
1244
2340
  if (!rawCwd) {
1245
- setJson(res, 400, { error: "Missing cwd" });
2341
+ setJson2(res, 400, { error: "Missing cwd" });
1246
2342
  return;
1247
2343
  }
1248
2344
  const cwd = isAbsolute(rawCwd) ? rawCwd : resolve(rawCwd);
1249
2345
  try {
1250
- const info = await stat(cwd);
2346
+ const info = await stat2(cwd);
1251
2347
  if (!info.isDirectory()) {
1252
- setJson(res, 400, { error: "cwd is not a directory" });
2348
+ setJson2(res, 400, { error: "cwd is not a directory" });
1253
2349
  return;
1254
2350
  }
1255
2351
  } catch {
1256
- setJson(res, 404, { error: "cwd does not exist" });
2352
+ setJson2(res, 404, { error: "cwd does not exist" });
1257
2353
  return;
1258
2354
  }
1259
2355
  try {
1260
2356
  const files = await listFilesWithRipgrep(cwd);
1261
2357
  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 });
2358
+ setJson2(res, 200, { data: scored });
1263
2359
  } catch (error) {
1264
- setJson(res, 500, { error: getErrorMessage(error, "Failed to search files") });
2360
+ setJson2(res, 500, { error: getErrorMessage2(error, "Failed to search files") });
1265
2361
  }
1266
2362
  return;
1267
2363
  }
1268
2364
  if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
1269
- const cache = await readThreadTitleCache();
1270
- setJson(res, 200, { data: cache });
2365
+ const cache = await readMergedThreadTitleCache();
2366
+ setJson2(res, 200, { data: cache });
1271
2367
  return;
1272
2368
  }
1273
2369
  if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
1274
- const payload = asRecord(await readJsonBody(req));
2370
+ const payload = asRecord2(await readJsonBody(req));
1275
2371
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
1276
2372
  const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
1277
2373
  const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
1278
2374
  if (!query) {
1279
- setJson(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
2375
+ setJson2(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
1280
2376
  return;
1281
2377
  }
1282
2378
  const index = await getThreadSearchIndex();
1283
2379
  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 } });
2380
+ setJson2(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
1285
2381
  return;
1286
2382
  }
1287
2383
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
1288
- const payload = asRecord(await readJsonBody(req));
2384
+ const payload = asRecord2(await readJsonBody(req));
1289
2385
  const id = typeof payload?.id === "string" ? payload.id : "";
1290
2386
  const title = typeof payload?.title === "string" ? payload.title : "";
1291
2387
  if (!id) {
1292
- setJson(res, 400, { error: "Missing id" });
2388
+ setJson2(res, 400, { error: "Missing id" });
1293
2389
  return;
1294
2390
  }
1295
2391
  const cache = await readThreadTitleCache();
1296
2392
  const next2 = title ? updateThreadTitleCache(cache, id, title) : removeFromThreadTitleCache(cache, id);
1297
2393
  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
- }
2394
+ setJson2(res, 200, { ok: true });
1411
2395
  return;
1412
2396
  }
1413
2397
  if (req.method === "GET" && url.pathname === "/codex-api/events") {
@@ -1442,8 +2426,8 @@ data: ${JSON.stringify({ ok: true })}
1442
2426
  }
1443
2427
  next();
1444
2428
  } catch (error) {
1445
- const message = getErrorMessage(error, "Unknown bridge error");
1446
- setJson(res, 502, { error: message });
2429
+ const message = getErrorMessage2(error, "Unknown bridge error");
2430
+ setJson2(res, 502, { error: message });
1447
2431
  }
1448
2432
  };
1449
2433
  middleware.dispose = () => {
@@ -1580,8 +2564,8 @@ function createAuthSession(password) {
1580
2564
  }
1581
2565
 
1582
2566
  // 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";
2567
+ import { dirname, extname, join as join3 } from "path";
2568
+ import { open, readFile as readFile3, readdir as readdir3, stat as stat3 } from "fs/promises";
1585
2569
  var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
1586
2570
  ".txt",
1587
2571
  ".md",
@@ -1696,7 +2680,7 @@ async function probeFileIsText(localPath) {
1696
2680
  async function isTextEditableFile(localPath) {
1697
2681
  if (isTextEditablePath(localPath)) return true;
1698
2682
  try {
1699
- const fileStat = await stat2(localPath);
2683
+ const fileStat = await stat3(localPath);
1700
2684
  if (!fileStat.isFile()) return false;
1701
2685
  return await probeFileIsText(localPath);
1702
2686
  } catch {
@@ -1716,10 +2700,10 @@ function escapeForInlineScriptString(value) {
1716
2700
  return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
1717
2701
  }
1718
2702
  async function getDirectoryItems(localPath) {
1719
- const entries = await readdir2(localPath, { withFileTypes: true });
2703
+ const entries = await readdir3(localPath, { withFileTypes: true });
1720
2704
  const withMeta = await Promise.all(entries.map(async (entry) => {
1721
- const entryPath = join2(localPath, entry.name);
1722
- const entryStat = await stat2(entryPath);
2705
+ const entryPath = join3(localPath, entry.name);
2706
+ const entryStat = await stat3(entryPath);
1723
2707
  const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
1724
2708
  return {
1725
2709
  name: entry.name,
@@ -1742,9 +2726,9 @@ async function createDirectoryListingHtml(localPath) {
1742
2726
  const rows = items.map((item) => {
1743
2727
  const suffix = item.isDirectory ? "/" : "";
1744
2728
  const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml(item.name)}" href="${escapeHtml(toEditHref(item.path))}" title="Edit">\u270F\uFE0F</a>` : "";
1745
- return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a>${editAction}</li>`;
2729
+ return `<li class="file-row"><a class="file-link" href="${escapeHtml(toBrowseHref(item.path))}">${escapeHtml(item.name)}${suffix}</a><span class="row-actions">${editAction}</span></li>`;
1746
2730
  }).join("\n");
1747
- const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
2731
+ const parentLink = localPath !== parentPath ? `<a href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : "";
1748
2732
  return `<!doctype html>
1749
2733
  <html lang="en">
1750
2734
  <head>
@@ -1758,8 +2742,27 @@ async function createDirectoryListingHtml(localPath) {
1758
2742
  ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
1759
2743
  .file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
1760
2744
  .file-link { display: block; padding: 10px 12px; border: 1px solid #28405f; border-radius: 10px; background: #0f1b33; overflow-wrap: anywhere; }
1761
- .icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; text-decoration: none; }
2745
+ .header-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
2746
+ .header-parent-link { color: #9ec8ff; font-size: 14px; padding: 8px 10px; border: 1px solid #2a4569; border-radius: 10px; background: #101f3a; }
2747
+ .header-parent-link:hover { text-decoration: none; filter: brightness(1.08); }
2748
+ .header-open-btn {
2749
+ height: 42px;
2750
+ padding: 0 14px;
2751
+ border: 1px solid #4f8de0;
2752
+ border-radius: 10px;
2753
+ background: linear-gradient(135deg, #2e6ee6 0%, #3d8cff 100%);
2754
+ color: #eef6ff;
2755
+ font-weight: 700;
2756
+ letter-spacing: 0.01em;
2757
+ cursor: pointer;
2758
+ box-shadow: 0 6px 18px rgba(33, 90, 199, 0.35);
2759
+ }
2760
+ .header-open-btn:hover { filter: brightness(1.08); }
2761
+ .header-open-btn:disabled { opacity: 0.6; cursor: default; }
2762
+ .row-actions { display: inline-flex; align-items: center; gap: 8px; min-width: 42px; justify-content: flex-end; }
2763
+ .icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border: 1px solid #36557a; border-radius: 10px; background: #162643; color: #dbe6ff; text-decoration: none; cursor: pointer; }
1762
2764
  .icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
2765
+ .status { margin: 10px 0 0; color: #8cc2ff; min-height: 1.25em; }
1763
2766
  h1 { font-size: 18px; margin: 0; word-break: break-all; }
1764
2767
  @media (max-width: 640px) {
1765
2768
  body { margin: 12px; }
@@ -1771,13 +2774,51 @@ async function createDirectoryListingHtml(localPath) {
1771
2774
  </head>
1772
2775
  <body>
1773
2776
  <h1>Index of ${escapeHtml(localPath)}</h1>
1774
- ${parentLink}
2777
+ <div class="header-actions">
2778
+ ${parentLink ? `<a class="header-parent-link" href="${escapeHtml(toBrowseHref(parentPath))}">..</a>` : ""}
2779
+ <button class="header-open-btn open-folder-btn" type="button" aria-label="Open current folder in Codex" title="Open folder in Codex" data-path="${escapeHtml(localPath)}">Open folder in Codex</button>
2780
+ </div>
2781
+ <p id="status" class="status"></p>
1775
2782
  <ul>${rows}</ul>
2783
+ <script>
2784
+ const status = document.getElementById('status');
2785
+ document.addEventListener('click', async (event) => {
2786
+ const target = event.target;
2787
+ if (!(target instanceof Element)) return;
2788
+ const button = target.closest('.open-folder-btn');
2789
+ if (!(button instanceof HTMLButtonElement)) return;
2790
+
2791
+ const path = button.getAttribute('data-path') || '';
2792
+ if (!path) return;
2793
+ button.disabled = true;
2794
+ status.textContent = 'Opening folder in Codex...';
2795
+ try {
2796
+ const response = await fetch('/codex-api/project-root', {
2797
+ method: 'POST',
2798
+ headers: { 'Content-Type': 'application/json' },
2799
+ body: JSON.stringify({
2800
+ path,
2801
+ createIfMissing: false,
2802
+ label: '',
2803
+ }),
2804
+ });
2805
+ if (!response.ok) {
2806
+ status.textContent = 'Failed to open folder.';
2807
+ button.disabled = false;
2808
+ return;
2809
+ }
2810
+ window.location.assign('/#/');
2811
+ } catch {
2812
+ status.textContent = 'Failed to open folder.';
2813
+ button.disabled = false;
2814
+ }
2815
+ });
2816
+ </script>
1776
2817
  </body>
1777
2818
  </html>`;
1778
2819
  }
1779
2820
  async function createTextEditorHtml(localPath) {
1780
- const content = await readFile2(localPath, "utf8");
2821
+ const content = await readFile3(localPath, "utf8");
1781
2822
  const parentPath = dirname(localPath);
1782
2823
  const language = languageForPath(localPath);
1783
2824
  const safeContentLiteral = escapeForInlineScriptString(content);
@@ -1848,8 +2889,8 @@ async function createTextEditorHtml(localPath) {
1848
2889
  // src/server/httpServer.ts
1849
2890
  import { WebSocketServer } from "ws";
1850
2891
  var __dirname = dirname2(fileURLToPath(import.meta.url));
1851
- var distDir = join3(__dirname, "..", "dist");
1852
- var spaEntryFile = join3(distDir, "index.html");
2892
+ var distDir = join4(__dirname, "..", "dist");
2893
+ var spaEntryFile = join4(distDir, "index.html");
1853
2894
  var IMAGE_CONTENT_TYPES = {
1854
2895
  ".avif": "image/avif",
1855
2896
  ".bmp": "image/bmp",
@@ -1860,6 +2901,20 @@ var IMAGE_CONTENT_TYPES = {
1860
2901
  ".svg": "image/svg+xml",
1861
2902
  ".webp": "image/webp"
1862
2903
  };
2904
+ function renderFrontendMissingHtml(message, details) {
2905
+ const lines = details && details.length > 0 ? `<pre>${details.join("\n")}</pre>` : "";
2906
+ return [
2907
+ "<!doctype html>",
2908
+ '<html lang="en">',
2909
+ '<head><meta charset="utf-8"><title>Codex Web UI Error</title></head>',
2910
+ "<body>",
2911
+ `<h1>${message}</h1>`,
2912
+ lines,
2913
+ '<p><a href="/">Back to chat</a></p>',
2914
+ "</body>",
2915
+ "</html>"
2916
+ ].join("");
2917
+ }
1863
2918
  function normalizeLocalImagePath(rawPath) {
1864
2919
  const trimmed = rawPath.trim();
1865
2920
  if (!trimmed) return "";
@@ -1926,7 +2981,7 @@ function createServer(options = {}) {
1926
2981
  return;
1927
2982
  }
1928
2983
  try {
1929
- const fileStat = await stat3(localPath);
2984
+ const fileStat = await stat4(localPath);
1930
2985
  res.setHeader("Cache-Control", "private, no-store");
1931
2986
  if (fileStat.isDirectory()) {
1932
2987
  const html = await createDirectoryListingHtml(localPath);
@@ -1949,7 +3004,7 @@ function createServer(options = {}) {
1949
3004
  return;
1950
3005
  }
1951
3006
  try {
1952
- const fileStat = await stat3(localPath);
3007
+ const fileStat = await stat4(localPath);
1953
3008
  if (!fileStat.isFile()) {
1954
3009
  res.status(400).json({ error: "Expected file path." });
1955
3010
  return;
@@ -1973,32 +3028,31 @@ function createServer(options = {}) {
1973
3028
  }
1974
3029
  const body = typeof req.body === "string" ? req.body : "";
1975
3030
  try {
1976
- await writeFile2(localPath, body, "utf8");
3031
+ await writeFile3(localPath, body, "utf8");
1977
3032
  res.status(200).json({ ok: true });
1978
3033
  } catch {
1979
3034
  res.status(404).json({ error: "File not found." });
1980
3035
  }
1981
3036
  });
1982
- const hasFrontendAssets = existsSync(spaEntryFile);
3037
+ const hasFrontendAssets = existsSync3(spaEntryFile);
1983
3038
  if (hasFrontendAssets) {
1984
3039
  app.use(express.static(distDir));
1985
3040
  }
1986
3041
  app.use((_req, res) => {
1987
3042
  if (!hasFrontendAssets) {
1988
- res.status(503).type("text/plain").send(
1989
- [
1990
- "Codex web UI assets are missing.",
3043
+ res.status(503).type("text/html; charset=utf-8").send(
3044
+ renderFrontendMissingHtml("Codex web UI assets are missing.", [
1991
3045
  `Expected: ${spaEntryFile}`,
1992
3046
  "If running from source, build frontend assets with: npm run build:frontend",
1993
3047
  "If running with npx, clear the npx cache and reinstall codexapp."
1994
- ].join("\n")
3048
+ ])
1995
3049
  );
1996
3050
  return;
1997
3051
  }
1998
3052
  res.sendFile(spaEntryFile, (error) => {
1999
3053
  if (!error) return;
2000
3054
  if (!res.headersSent) {
2001
- res.status(404).type("text/plain").send("Frontend entry file not found.");
3055
+ res.status(404).type("text/html; charset=utf-8").send(renderFrontendMissingHtml("Frontend entry file not found."));
2002
3056
  }
2003
3057
  });
2004
3058
  });
@@ -2053,8 +3107,8 @@ var program = new Command().name("codexui").description("Web interface for Codex
2053
3107
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
2054
3108
  async function readCliVersion() {
2055
3109
  try {
2056
- const packageJsonPath = join4(__dirname2, "..", "package.json");
2057
- const raw = await readFile3(packageJsonPath, "utf8");
3110
+ const packageJsonPath = join5(__dirname2, "..", "package.json");
3111
+ const raw = await readFile4(packageJsonPath, "utf8");
2058
3112
  const parsed = JSON.parse(raw);
2059
3113
  return typeof parsed.version === "string" ? parsed.version : "unknown";
2060
3114
  } catch {
@@ -2079,22 +3133,22 @@ function runWithStatus(command, args) {
2079
3133
  return result.status ?? -1;
2080
3134
  }
2081
3135
  function getUserNpmPrefix() {
2082
- return join4(homedir2(), ".npm-global");
3136
+ return join5(homedir3(), ".npm-global");
2083
3137
  }
2084
3138
  function resolveCodexCommand() {
2085
3139
  if (canRun("codex", ["--version"])) {
2086
3140
  return "codex";
2087
3141
  }
2088
- const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
2089
- if (existsSync2(userCandidate) && canRun(userCandidate, ["--version"])) {
3142
+ const userCandidate = join5(getUserNpmPrefix(), "bin", "codex");
3143
+ if (existsSync4(userCandidate) && canRun(userCandidate, ["--version"])) {
2090
3144
  return userCandidate;
2091
3145
  }
2092
3146
  const prefix = process.env.PREFIX?.trim();
2093
3147
  if (!prefix) {
2094
3148
  return null;
2095
3149
  }
2096
- const candidate = join4(prefix, "bin", "codex");
2097
- if (existsSync2(candidate) && canRun(candidate, ["--version"])) {
3150
+ const candidate = join5(prefix, "bin", "codex");
3151
+ if (existsSync4(candidate) && canRun(candidate, ["--version"])) {
2098
3152
  return candidate;
2099
3153
  }
2100
3154
  return null;
@@ -2103,8 +3157,8 @@ function resolveCloudflaredCommand() {
2103
3157
  if (canRun("cloudflared", ["--version"])) {
2104
3158
  return "cloudflared";
2105
3159
  }
2106
- const localCandidate = join4(homedir2(), ".local", "bin", "cloudflared");
2107
- if (existsSync2(localCandidate) && canRun(localCandidate, ["--version"])) {
3160
+ const localCandidate = join5(homedir3(), ".local", "bin", "cloudflared");
3161
+ if (existsSync4(localCandidate) && canRun(localCandidate, ["--version"])) {
2108
3162
  return localCandidate;
2109
3163
  }
2110
3164
  return null;
@@ -2119,7 +3173,7 @@ function mapCloudflaredLinuxArch(arch) {
2119
3173
  return null;
2120
3174
  }
2121
3175
  function downloadFile(url, destination) {
2122
- return new Promise((resolve2, reject) => {
3176
+ return new Promise((resolve3, reject) => {
2123
3177
  const request = (currentUrl) => {
2124
3178
  httpsGet(currentUrl, (response) => {
2125
3179
  const code = response.statusCode ?? 0;
@@ -2137,7 +3191,7 @@ function downloadFile(url, destination) {
2137
3191
  response.pipe(file);
2138
3192
  file.on("finish", () => {
2139
3193
  file.close();
2140
- resolve2();
3194
+ resolve3();
2141
3195
  });
2142
3196
  file.on("error", reject);
2143
3197
  }).on("error", reject);
@@ -2157,9 +3211,9 @@ async function ensureCloudflaredInstalledLinux() {
2157
3211
  if (!mappedArch) {
2158
3212
  throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
2159
3213
  }
2160
- const userBinDir = join4(homedir2(), ".local", "bin");
3214
+ const userBinDir = join5(homedir3(), ".local", "bin");
2161
3215
  mkdirSync(userBinDir, { recursive: true });
2162
- const destination = join4(userBinDir, "cloudflared");
3216
+ const destination = join5(userBinDir, "cloudflared");
2163
3217
  const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
2164
3218
  console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
2165
3219
  await downloadFile(downloadUrl, destination);
@@ -2177,7 +3231,7 @@ async function shouldInstallCloudflaredInteractively() {
2177
3231
  console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
2178
3232
  return false;
2179
3233
  }
2180
- const prompt = createInterface({ input: process.stdin, output: process.stdout });
3234
+ const prompt = createInterface2({ input: process.stdin, output: process.stdout });
2181
3235
  try {
2182
3236
  const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
2183
3237
  const normalized = answer.trim().toLowerCase();
@@ -2198,8 +3252,8 @@ async function resolveCloudflaredForTunnel() {
2198
3252
  return ensureCloudflaredInstalledLinux();
2199
3253
  }
2200
3254
  function hasCodexAuth() {
2201
- const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
2202
- return existsSync2(join4(codexHome, "auth.json"));
3255
+ const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3256
+ return existsSync4(join5(codexHome, "auth.json"));
2203
3257
  }
2204
3258
  function ensureCodexInstalled() {
2205
3259
  let codexCommand = resolveCodexCommand();
@@ -2217,7 +3271,7 @@ function ensureCodexInstalled() {
2217
3271
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
2218
3272
  `);
2219
3273
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
2220
- process.env.PATH = `${join4(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
3274
+ process.env.PATH = `${join5(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
2221
3275
  };
2222
3276
  if (isTermuxRuntime()) {
2223
3277
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -2266,7 +3320,7 @@ function printTermuxKeepAlive(lines) {
2266
3320
  }
2267
3321
  function openBrowser(url) {
2268
3322
  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" });
3323
+ const child = spawn3(command.cmd, command.args, { detached: true, stdio: "ignore" });
2270
3324
  child.on("error", () => {
2271
3325
  });
2272
3326
  child.unref();
@@ -2280,25 +3334,28 @@ function parseCloudflaredUrl(chunk) {
2280
3334
  }
2281
3335
  function getAccessibleUrls(port) {
2282
3336
  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) {
3337
+ try {
3338
+ const interfaces = networkInterfaces();
3339
+ for (const entries of Object.values(interfaces)) {
3340
+ if (!entries) {
2290
3341
  continue;
2291
3342
  }
2292
- if (entry.family === "IPv4") {
2293
- urls.add(`http://${entry.address}:${String(port)}`);
3343
+ for (const entry of entries) {
3344
+ if (entry.internal) {
3345
+ continue;
3346
+ }
3347
+ if (entry.family === "IPv4") {
3348
+ urls.add(`http://${entry.address}:${String(port)}`);
3349
+ }
2294
3350
  }
2295
3351
  }
3352
+ } catch {
2296
3353
  }
2297
3354
  return Array.from(urls);
2298
3355
  }
2299
3356
  async function startCloudflaredTunnel(command, localPort) {
2300
- return new Promise((resolve2, reject) => {
2301
- const child = spawn2(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
3357
+ return new Promise((resolve3, reject) => {
3358
+ const child = spawn3(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
2302
3359
  stdio: ["ignore", "pipe", "pipe"]
2303
3360
  });
2304
3361
  const timeout = setTimeout(() => {
@@ -2314,7 +3371,7 @@ async function startCloudflaredTunnel(command, localPort) {
2314
3371
  clearTimeout(timeout);
2315
3372
  child.stdout?.off("data", handleData);
2316
3373
  child.stderr?.off("data", handleData);
2317
- resolve2({ process: child, url: parsedUrl });
3374
+ resolve3({ process: child, url: parsedUrl });
2318
3375
  };
2319
3376
  const onError = (error) => {
2320
3377
  clearTimeout(timeout);
@@ -2333,7 +3390,7 @@ async function startCloudflaredTunnel(command, localPort) {
2333
3390
  });
2334
3391
  }
2335
3392
  function listenWithFallback(server, startPort) {
2336
- return new Promise((resolve2, reject) => {
3393
+ return new Promise((resolve3, reject) => {
2337
3394
  const attempt = (port) => {
2338
3395
  const onError = (error) => {
2339
3396
  server.off("listening", onListening);
@@ -2345,7 +3402,7 @@ function listenWithFallback(server, startPort) {
2345
3402
  };
2346
3403
  const onListening = () => {
2347
3404
  server.off("error", onError);
2348
- resolve2(port);
3405
+ resolve3(port);
2349
3406
  };
2350
3407
  server.once("error", onError);
2351
3408
  server.once("listening", onListening);
@@ -2354,8 +3411,72 @@ function listenWithFallback(server, startPort) {
2354
3411
  attempt(startPort);
2355
3412
  });
2356
3413
  }
3414
+ function getCodexGlobalStatePath2() {
3415
+ const codexHome = process.env.CODEX_HOME?.trim() || join5(homedir3(), ".codex");
3416
+ return join5(codexHome, ".codex-global-state.json");
3417
+ }
3418
+ function normalizeUniqueStrings(value) {
3419
+ if (!Array.isArray(value)) return [];
3420
+ const next = [];
3421
+ for (const item of value) {
3422
+ if (typeof item !== "string") continue;
3423
+ const trimmed = item.trim();
3424
+ if (!trimmed || next.includes(trimmed)) continue;
3425
+ next.push(trimmed);
3426
+ }
3427
+ return next;
3428
+ }
3429
+ async function persistLaunchProject(projectPath) {
3430
+ const trimmed = projectPath.trim();
3431
+ if (!trimmed) return;
3432
+ const normalizedPath = isAbsolute3(trimmed) ? trimmed : resolve2(trimmed);
3433
+ const directoryInfo = await stat5(normalizedPath);
3434
+ if (!directoryInfo.isDirectory()) {
3435
+ throw new Error(`Not a directory: ${normalizedPath}`);
3436
+ }
3437
+ const statePath = getCodexGlobalStatePath2();
3438
+ let payload = {};
3439
+ try {
3440
+ const raw = await readFile4(statePath, "utf8");
3441
+ const parsed = JSON.parse(raw);
3442
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3443
+ payload = parsed;
3444
+ }
3445
+ } catch {
3446
+ payload = {};
3447
+ }
3448
+ const roots = normalizeUniqueStrings(payload["electron-saved-workspace-roots"]);
3449
+ const activeRoots = normalizeUniqueStrings(payload["active-workspace-roots"]);
3450
+ payload["electron-saved-workspace-roots"] = [
3451
+ normalizedPath,
3452
+ ...roots.filter((value) => value !== normalizedPath)
3453
+ ];
3454
+ payload["active-workspace-roots"] = [
3455
+ normalizedPath,
3456
+ ...activeRoots.filter((value) => value !== normalizedPath)
3457
+ ];
3458
+ await writeFile4(statePath, JSON.stringify(payload), "utf8");
3459
+ }
3460
+ async function addProjectOnly(projectPath) {
3461
+ const trimmed = projectPath.trim();
3462
+ if (!trimmed) {
3463
+ throw new Error("Missing project path");
3464
+ }
3465
+ await persistLaunchProject(trimmed);
3466
+ }
2357
3467
  async function startServer(options) {
2358
3468
  const version = await readCliVersion();
3469
+ const projectPath = options.projectPath?.trim() ?? "";
3470
+ if (projectPath.length > 0) {
3471
+ try {
3472
+ await persistLaunchProject(projectPath);
3473
+ } catch (error) {
3474
+ const message = error instanceof Error ? error.message : String(error);
3475
+ console.warn(`
3476
+ [project] Could not open launch project: ${message}
3477
+ `);
3478
+ }
3479
+ }
2359
3480
  const codexCommand = ensureCodexInstalled() ?? resolveCodexCommand();
2360
3481
  if (!hasCodexAuth() && codexCommand) {
2361
3482
  console.log("\nCodex is not logged in. Starting `codex login`...\n");
@@ -2416,7 +3537,7 @@ async function startServer(options) {
2416
3537
  qrcode.generate(tunnelUrl, { small: true });
2417
3538
  console.log("");
2418
3539
  }
2419
- openBrowser(`http://localhost:${String(port)}`);
3540
+ if (options.open) openBrowser(`http://localhost:${String(port)}`);
2420
3541
  function shutdown() {
2421
3542
  console.log("\nShutting down...");
2422
3543
  if (tunnelChild && !tunnelChild.killed) {
@@ -2439,8 +3560,20 @@ async function runLogin() {
2439
3560
  console.log("\nStarting `codex login`...\n");
2440
3561
  runOrFail(codexCommand, ["login"], "Codex login");
2441
3562
  }
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);
3563
+ 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) => {
3564
+ const rawArgv = process.argv.slice(2);
3565
+ const openProjectFlagIndex = rawArgv.findIndex((arg) => arg === "--open-project" || arg.startsWith("--open-project="));
3566
+ let openProjectOnly = (opts.openProject ?? "").trim();
3567
+ if (!openProjectOnly && openProjectFlagIndex >= 0 && projectPath?.trim()) {
3568
+ openProjectOnly = projectPath.trim();
3569
+ }
3570
+ if (openProjectOnly.length > 0) {
3571
+ await addProjectOnly(openProjectOnly);
3572
+ console.log(`Added project: ${openProjectOnly}`);
3573
+ return;
3574
+ }
3575
+ const launchProject = (projectPath ?? "").trim();
3576
+ await startServer({ ...opts, projectPath: launchProject });
2444
3577
  });
2445
3578
  program.command("login").description("Install/check Codex CLI and run `codex login`").action(runLogin);
2446
3579
  program.command("help").description("Show codexui command help").action(() => {