codexapp 0.1.30 → 0.1.32

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
@@ -1,33 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import "dotenv/config";
5
4
  import { createServer as createServer2 } from "http";
6
- import { existsSync as existsSync3 } from "fs";
7
- import { readFile as readFile2 } from "fs/promises";
8
- import { homedir as homedir2 } from "os";
9
- import { join as join3 } from "path";
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";
10
9
  import { spawn as spawn2, spawnSync } from "child_process";
10
+ import { createInterface } from "readline/promises";
11
11
  import { fileURLToPath as fileURLToPath2 } from "url";
12
- import { dirname as dirname2 } from "path";
12
+ import { dirname as dirname3 } from "path";
13
+ import { get as httpsGet } from "https";
13
14
  import { Command } from "commander";
14
15
  import qrcode from "qrcode-terminal";
15
16
 
16
17
  // src/server/httpServer.ts
17
18
  import { fileURLToPath } from "url";
18
- import { dirname, extname, isAbsolute as isAbsolute2, join as join2 } from "path";
19
- import { existsSync as existsSync2 } from "fs";
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";
20
22
  import express from "express";
21
23
 
22
24
  // src/server/codexAppServerBridge.ts
23
- import "dotenv/config";
24
25
  import { spawn } from "child_process";
26
+ import { randomBytes } from "crypto";
25
27
  import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
26
- import { existsSync } from "fs";
27
28
  import { request as httpsRequest } from "https";
28
29
  import { homedir } from "os";
29
30
  import { tmpdir } from "os";
30
- import { isAbsolute, join, resolve } from "path";
31
+ import { basename, isAbsolute, join, resolve } from "path";
31
32
  import { writeFile } from "fs/promises";
32
33
  function asRecord(value) {
33
34
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -51,6 +52,46 @@ function setJson(res, statusCode, payload) {
51
52
  res.setHeader("Content-Type", "application/json; charset=utf-8");
52
53
  res.end(JSON.stringify(payload));
53
54
  }
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
+ }
54
95
  function scoreFileCandidate(path, query) {
55
96
  if (!query) return 0;
56
97
  const lowerPath = path.toLowerCase();
@@ -124,8 +165,30 @@ async function runCommand(command, args, options = {}) {
124
165
  });
125
166
  });
126
167
  }
127
- async function runCommandWithOutput(command, args, options = {}) {
128
- return await new Promise((resolve2, reject) => {
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) => {
129
192
  const proc = spawn(command, args, {
130
193
  cwd: options.cwd,
131
194
  env: process.env,
@@ -142,7 +205,7 @@ async function runCommandWithOutput(command, args, options = {}) {
142
205
  proc.on("error", reject);
143
206
  proc.on("close", (code) => {
144
207
  if (code === 0) {
145
- resolve2(stdout);
208
+ resolveOutput(stdout.trim());
146
209
  return;
147
210
  }
148
211
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -208,7 +271,7 @@ async function fetchSkillsTree() {
208
271
  if (skillsTreeCache && Date.now() - skillsTreeCache.fetchedAt < TREE_CACHE_TTL_MS) {
209
272
  return skillsTreeCache.entries;
210
273
  }
211
- const resp = await ghFetch(`https://api.github.com/repos/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/git/trees/main?recursive=1`);
274
+ const resp = await ghFetch("https://api.github.com/repos/openclaw/skills/git/trees/main?recursive=1");
212
275
  if (!resp.ok) throw new Error(`GitHub tree API returned ${resp.status}`);
213
276
  const data = await resp.json();
214
277
  const metaPattern = /^skills\/([^/]+)\/([^/]+)\/_meta\.json$/;
@@ -224,7 +287,7 @@ async function fetchSkillsTree() {
224
287
  entries.push({
225
288
  name: skillName,
226
289
  owner,
227
- url: `https://github.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/tree/main/skills/${owner}/${skillName}`
290
+ url: `https://github.com/openclaw/skills/tree/main/skills/${owner}/${skillName}`
228
291
  });
229
292
  }
230
293
  skillsTreeCache = { entries, fetchedAt: Date.now() };
@@ -236,7 +299,7 @@ async function fetchMetaBatch(entries) {
236
299
  const batch = toFetch.slice(0, 50);
237
300
  const results = await Promise.allSettled(
238
301
  batch.map(async (e) => {
239
- const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${e.owner}/${e.name}/_meta.json`;
302
+ const rawUrl = `https://raw.githubusercontent.com/openclaw/skills/main/skills/${e.owner}/${e.name}/_meta.json`;
240
303
  const resp = await fetch(rawUrl);
241
304
  if (!resp.ok) return;
242
305
  const meta = await resp.json();
@@ -262,23 +325,6 @@ function buildHubEntry(e) {
262
325
  installed: false
263
326
  };
264
327
  }
265
- var GITHUB_DEVICE_CLIENT_ID = "Iv1.b507a08c87ecfe98";
266
- var DEFAULT_SKILLS_SYNC_REPO_NAME = "codexskills";
267
- var SKILLS_SYNC_MANIFEST_PATH = "installed-skills.json";
268
- var SYNC_UPSTREAM_SKILLS_OWNER = "OpenClawAndroid";
269
- var SYNC_UPSTREAM_SKILLS_REPO = "skills";
270
- var HUB_SKILLS_OWNER = "openclaw";
271
- var HUB_SKILLS_REPO = "skills";
272
- var startupSkillsSyncInitialized = false;
273
- var startupSyncStatus = {
274
- inProgress: false,
275
- mode: "idle",
276
- branch: getPreferredSyncBranch(),
277
- lastAction: "not-started",
278
- lastRunAtIso: "",
279
- lastSuccessAtIso: "",
280
- lastError: ""
281
- };
282
328
  async function scanInstalledSkillsFromDisk() {
283
329
  const map = /* @__PURE__ */ new Map();
284
330
  const skillsDir = getSkillsInstallDir();
@@ -297,448 +343,6 @@ async function scanInstalledSkillsFromDisk() {
297
343
  }
298
344
  return map;
299
345
  }
300
- function getSkillsSyncStatePath() {
301
- return join(getCodexHomeDir(), "skills-sync.json");
302
- }
303
- async function readSkillsSyncState() {
304
- try {
305
- const raw = await readFile(getSkillsSyncStatePath(), "utf8");
306
- const parsed = JSON.parse(raw);
307
- return parsed && typeof parsed === "object" ? parsed : {};
308
- } catch {
309
- return {};
310
- }
311
- }
312
- async function writeSkillsSyncState(state) {
313
- await writeFile(getSkillsSyncStatePath(), JSON.stringify(state), "utf8");
314
- }
315
- async function getGithubJson(url, token, method = "GET", body) {
316
- const resp = await fetch(url, {
317
- method,
318
- headers: {
319
- Accept: "application/vnd.github+json",
320
- "Content-Type": "application/json",
321
- Authorization: `Bearer ${token}`,
322
- "X-GitHub-Api-Version": "2022-11-28",
323
- "User-Agent": "codex-web-local"
324
- },
325
- body: body ? JSON.stringify(body) : void 0
326
- });
327
- if (!resp.ok) {
328
- const text = await resp.text();
329
- throw new Error(`GitHub API ${method} ${url} failed (${resp.status}): ${text}`);
330
- }
331
- return await resp.json();
332
- }
333
- async function startGithubDeviceLogin() {
334
- const resp = await fetch("https://github.com/login/device/code", {
335
- method: "POST",
336
- headers: {
337
- Accept: "application/json",
338
- "Content-Type": "application/x-www-form-urlencoded",
339
- "User-Agent": "codex-web-local"
340
- },
341
- body: new URLSearchParams({
342
- client_id: GITHUB_DEVICE_CLIENT_ID,
343
- scope: "repo read:user"
344
- })
345
- });
346
- if (!resp.ok) {
347
- throw new Error(`GitHub device flow init failed (${resp.status})`);
348
- }
349
- return await resp.json();
350
- }
351
- async function completeGithubDeviceLogin(deviceCode) {
352
- const resp = await fetch("https://github.com/login/oauth/access_token", {
353
- method: "POST",
354
- headers: {
355
- Accept: "application/json",
356
- "Content-Type": "application/x-www-form-urlencoded",
357
- "User-Agent": "codex-web-local"
358
- },
359
- body: new URLSearchParams({
360
- client_id: GITHUB_DEVICE_CLIENT_ID,
361
- device_code: deviceCode,
362
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
363
- })
364
- });
365
- if (!resp.ok) {
366
- throw new Error(`GitHub token exchange failed (${resp.status})`);
367
- }
368
- const payload = await resp.json();
369
- if (!payload.access_token) return { token: null, error: payload.error || "unknown_error" };
370
- return { token: payload.access_token, error: null };
371
- }
372
- function isAndroidLikeRuntime() {
373
- if (process.platform === "android") return true;
374
- if (existsSync("/data/data/com.termux")) return true;
375
- if (process.env.TERMUX_VERSION) return true;
376
- const prefix = process.env.PREFIX?.toLowerCase() ?? "";
377
- if (prefix.includes("/com.termux/")) return true;
378
- const proot = process.env.PROOT_TMP_DIR?.toLowerCase() ?? "";
379
- if (proot.length > 0) return true;
380
- return false;
381
- }
382
- function getPreferredSyncBranch() {
383
- return isAndroidLikeRuntime() ? "android" : "main";
384
- }
385
- function isUpstreamSkillsRepo(repoOwner, repoName) {
386
- return repoOwner.toLowerCase() === SYNC_UPSTREAM_SKILLS_OWNER.toLowerCase() && repoName.toLowerCase() === SYNC_UPSTREAM_SKILLS_REPO.toLowerCase();
387
- }
388
- async function resolveGithubUsername(token) {
389
- const user = await getGithubJson("https://api.github.com/user", token);
390
- return user.login;
391
- }
392
- async function ensurePrivateForkFromUpstream(token, username, repoName) {
393
- const repoUrl = `https://api.github.com/repos/${username}/${repoName}`;
394
- let created = false;
395
- const existing = await fetch(repoUrl, {
396
- headers: {
397
- Accept: "application/vnd.github+json",
398
- Authorization: `Bearer ${token}`,
399
- "X-GitHub-Api-Version": "2022-11-28",
400
- "User-Agent": "codex-web-local"
401
- }
402
- });
403
- if (existing.ok) {
404
- const details = await existing.json();
405
- if (details.private === true) return;
406
- await getGithubJson(repoUrl, token, "PATCH", { private: true });
407
- return;
408
- }
409
- if (existing.status !== 404) {
410
- throw new Error(`Failed to check personal repo existence (${existing.status})`);
411
- }
412
- await getGithubJson(
413
- "https://api.github.com/user/repos",
414
- token,
415
- "POST",
416
- { name: repoName, private: true, auto_init: false, description: "Codex skills private mirror sync" }
417
- );
418
- created = true;
419
- let ready = false;
420
- for (let i = 0; i < 20; i++) {
421
- const check = await fetch(repoUrl, {
422
- headers: {
423
- Accept: "application/vnd.github+json",
424
- Authorization: `Bearer ${token}`,
425
- "X-GitHub-Api-Version": "2022-11-28",
426
- "User-Agent": "codex-web-local"
427
- }
428
- });
429
- if (check.ok) {
430
- ready = true;
431
- break;
432
- }
433
- await new Promise((resolve2) => setTimeout(resolve2, 1e3));
434
- }
435
- if (!ready) throw new Error("Private mirror repo was created but is not available yet");
436
- if (!created) return;
437
- const tmp = await mkdtemp(join(tmpdir(), "codex-skills-seed-"));
438
- try {
439
- const upstreamUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
440
- const branch = getPreferredSyncBranch();
441
- try {
442
- await runCommand("git", ["clone", "--depth", "1", "--single-branch", "--branch", branch, upstreamUrl, tmp]);
443
- } catch {
444
- await runCommand("git", ["clone", "--depth", "1", upstreamUrl, tmp]);
445
- }
446
- const privateRemote = toGitHubTokenRemote(username, repoName, token);
447
- await runCommand("git", ["remote", "set-url", "origin", privateRemote], { cwd: tmp });
448
- try {
449
- await runCommand("git", ["checkout", "-B", branch], { cwd: tmp });
450
- } catch {
451
- }
452
- await runCommand("git", ["push", "-u", "origin", `HEAD:${branch}`], { cwd: tmp });
453
- } finally {
454
- await rm(tmp, { recursive: true, force: true });
455
- }
456
- }
457
- async function readRemoteSkillsManifest(token, repoOwner, repoName) {
458
- const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
459
- const resp = await fetch(url, {
460
- headers: {
461
- Accept: "application/vnd.github+json",
462
- Authorization: `Bearer ${token}`,
463
- "X-GitHub-Api-Version": "2022-11-28",
464
- "User-Agent": "codex-web-local"
465
- }
466
- });
467
- if (resp.status === 404) return [];
468
- if (!resp.ok) throw new Error(`Failed to read remote manifest (${resp.status})`);
469
- const payload = await resp.json();
470
- const content = payload.content ? Buffer.from(payload.content.replace(/\n/g, ""), "base64").toString("utf8") : "[]";
471
- const parsed = JSON.parse(content);
472
- if (!Array.isArray(parsed)) return [];
473
- const skills = [];
474
- for (const row of parsed) {
475
- const item = asRecord(row);
476
- const owner = typeof item?.owner === "string" ? item.owner : "";
477
- const name = typeof item?.name === "string" ? item.name : "";
478
- if (!name) continue;
479
- skills.push({ ...owner ? { owner } : {}, name, enabled: item?.enabled !== false });
480
- }
481
- return skills;
482
- }
483
- async function writeRemoteSkillsManifest(token, repoOwner, repoName, skills) {
484
- const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${SKILLS_SYNC_MANIFEST_PATH}`;
485
- let sha = "";
486
- const existing = await fetch(url, {
487
- headers: {
488
- Accept: "application/vnd.github+json",
489
- Authorization: `Bearer ${token}`,
490
- "X-GitHub-Api-Version": "2022-11-28",
491
- "User-Agent": "codex-web-local"
492
- }
493
- });
494
- if (existing.ok) {
495
- const payload = await existing.json();
496
- sha = payload.sha ?? "";
497
- }
498
- const content = Buffer.from(JSON.stringify(skills, null, 2), "utf8").toString("base64");
499
- await getGithubJson(url, token, "PUT", {
500
- message: "Update synced skills manifest",
501
- content,
502
- ...sha ? { sha } : {}
503
- });
504
- }
505
- function toGitHubTokenRemote(repoOwner, repoName, token) {
506
- return `https://x-access-token:${encodeURIComponent(token)}@github.com/${repoOwner}/${repoName}.git`;
507
- }
508
- async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
509
- const localDir = getSkillsInstallDir();
510
- await mkdir(localDir, { recursive: true });
511
- const gitDir = join(localDir, ".git");
512
- let hasGitDir = false;
513
- try {
514
- hasGitDir = (await stat(gitDir)).isDirectory();
515
- } catch {
516
- hasGitDir = false;
517
- }
518
- if (!hasGitDir) {
519
- await runCommand("git", ["init"], { cwd: localDir });
520
- await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: localDir });
521
- await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: localDir });
522
- await runCommand("git", ["add", "-A"], { cwd: localDir });
523
- try {
524
- await runCommand("git", ["commit", "-m", "Local skills snapshot before sync"], { cwd: localDir });
525
- } catch {
526
- }
527
- await runCommand("git", ["branch", "-M", branch], { cwd: localDir });
528
- try {
529
- await runCommand("git", ["remote", "add", "origin", repoUrl], { cwd: localDir });
530
- } catch {
531
- await runCommand("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
532
- }
533
- await runCommand("git", ["fetch", "origin"], { cwd: localDir });
534
- try {
535
- await runCommand("git", ["merge", "--allow-unrelated-histories", "--no-edit", `origin/${branch}`], { cwd: localDir });
536
- } catch {
537
- }
538
- return localDir;
539
- }
540
- await runCommand("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
541
- await runCommand("git", ["fetch", "origin"], { cwd: localDir });
542
- await resolveMergeConflictsByNewerCommit(localDir, branch);
543
- try {
544
- await runCommand("git", ["checkout", branch], { cwd: localDir });
545
- } catch {
546
- await resolveMergeConflictsByNewerCommit(localDir, branch);
547
- await runCommand("git", ["checkout", "-B", branch], { cwd: localDir });
548
- }
549
- await resolveMergeConflictsByNewerCommit(localDir, branch);
550
- try {
551
- await runCommand("git", ["stash", "push", "--include-untracked", "-m", "codex-skills-autostash"], { cwd: localDir });
552
- } catch {
553
- }
554
- try {
555
- await runCommand("git", ["pull", "--no-rebase", "origin", branch], { cwd: localDir });
556
- } catch {
557
- await resolveMergeConflictsByNewerCommit(localDir, branch);
558
- }
559
- try {
560
- await runCommand("git", ["stash", "pop"], { cwd: localDir });
561
- } catch {
562
- }
563
- return localDir;
564
- }
565
- async function resolveMergeConflictsByNewerCommit(repoDir, branch) {
566
- const unmerged = (await runCommandWithOutput("git", ["diff", "--name-only", "--diff-filter=U"], { cwd: repoDir })).split(/\r?\n/).map((row) => row.trim()).filter(Boolean);
567
- if (unmerged.length === 0) return;
568
- for (const path of unmerged) {
569
- const oursTime = await getCommitTime(repoDir, "HEAD", path);
570
- const theirsTime = await getCommitTime(repoDir, `origin/${branch}`, path);
571
- if (theirsTime > oursTime) {
572
- await runCommand("git", ["checkout", "--theirs", "--", path], { cwd: repoDir });
573
- } else {
574
- await runCommand("git", ["checkout", "--ours", "--", path], { cwd: repoDir });
575
- }
576
- await runCommand("git", ["add", "--", path], { cwd: repoDir });
577
- }
578
- const mergeHead = (await runCommandWithOutput("git", ["rev-parse", "-q", "--verify", "MERGE_HEAD"], { cwd: repoDir })).trim();
579
- if (mergeHead) {
580
- await runCommand("git", ["commit", "-m", "Auto-resolve skills merge by newer file"], { cwd: repoDir });
581
- }
582
- }
583
- async function getCommitTime(repoDir, ref, path) {
584
- try {
585
- const output = (await runCommandWithOutput("git", ["log", "-1", "--format=%ct", ref, "--", path], { cwd: repoDir })).trim();
586
- return output ? Number.parseInt(output, 10) : 0;
587
- } catch {
588
- return 0;
589
- }
590
- }
591
- async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _installedMap) {
592
- const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
593
- const branch = getPreferredSyncBranch();
594
- const repoDir = await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
595
- const addPaths = ["."];
596
- void _installedMap;
597
- await runCommand("git", ["config", "user.email", "skills-sync@local"], { cwd: repoDir });
598
- await runCommand("git", ["config", "user.name", "Skills Sync"], { cwd: repoDir });
599
- await runCommand("git", ["add", ...addPaths], { cwd: repoDir });
600
- const status = (await runCommandWithOutput("git", ["status", "--porcelain"], { cwd: repoDir })).trim();
601
- if (!status) return;
602
- await runCommand("git", ["commit", "-m", "Sync installed skills folder and manifest"], { cwd: repoDir });
603
- await runCommand("git", ["push", "origin", `HEAD:${branch}`], { cwd: repoDir });
604
- }
605
- async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName, _localSkillsDir) {
606
- const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
607
- const branch = getPreferredSyncBranch();
608
- await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
609
- }
610
- async function bootstrapSkillsFromUpstreamIntoLocal(_localSkillsDir) {
611
- void _localSkillsDir;
612
- const repoUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
613
- const branch = getPreferredSyncBranch();
614
- await ensureSkillsWorkingTreeRepo(repoUrl, branch);
615
- }
616
- async function collectLocalSyncedSkills(appServer) {
617
- const state = await readSkillsSyncState();
618
- const owners = { ...state.installedOwners ?? {} };
619
- const tree = await fetchSkillsTree();
620
- const uniqueOwnerByName = /* @__PURE__ */ new Map();
621
- const ambiguousNames = /* @__PURE__ */ new Set();
622
- for (const entry of tree) {
623
- if (ambiguousNames.has(entry.name)) continue;
624
- const existingOwner = uniqueOwnerByName.get(entry.name);
625
- if (!existingOwner) {
626
- uniqueOwnerByName.set(entry.name, entry.owner);
627
- continue;
628
- }
629
- if (existingOwner !== entry.owner) {
630
- uniqueOwnerByName.delete(entry.name);
631
- ambiguousNames.add(entry.name);
632
- }
633
- }
634
- const skills = await appServer.rpc("skills/list", {});
635
- const seen = /* @__PURE__ */ new Set();
636
- const synced = [];
637
- let ownersChanged = false;
638
- for (const entry of skills.data ?? []) {
639
- for (const skill of entry.skills ?? []) {
640
- const name = typeof skill.name === "string" ? skill.name : "";
641
- if (!name || seen.has(name)) continue;
642
- seen.add(name);
643
- let owner = owners[name];
644
- if (!owner) {
645
- owner = uniqueOwnerByName.get(name) ?? "";
646
- if (owner) {
647
- owners[name] = owner;
648
- ownersChanged = true;
649
- }
650
- }
651
- synced.push({ ...owner ? { owner } : {}, name, enabled: skill.enabled !== false });
652
- }
653
- }
654
- if (ownersChanged) {
655
- await writeSkillsSyncState({ ...state, installedOwners: owners });
656
- }
657
- synced.sort((a, b) => `${a.owner ?? ""}/${a.name}`.localeCompare(`${b.owner ?? ""}/${b.name}`));
658
- return synced;
659
- }
660
- async function autoPushSyncedSkills(appServer) {
661
- const state = await readSkillsSyncState();
662
- if (!state.githubToken || !state.repoOwner || !state.repoName) return;
663
- if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
664
- throw new Error("Refusing to push to upstream skills repository");
665
- }
666
- const local = await collectLocalSyncedSkills(appServer);
667
- const installedMap = await scanInstalledSkillsFromDisk();
668
- await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
669
- await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
670
- }
671
- async function initializeSkillsSyncOnStartup(appServer) {
672
- if (startupSkillsSyncInitialized) return;
673
- startupSkillsSyncInitialized = true;
674
- startupSyncStatus.inProgress = true;
675
- startupSyncStatus.lastRunAtIso = (/* @__PURE__ */ new Date()).toISOString();
676
- startupSyncStatus.lastError = "";
677
- startupSyncStatus.branch = getPreferredSyncBranch();
678
- try {
679
- const state = await readSkillsSyncState();
680
- const localSkillsDir = getSkillsInstallDir();
681
- if (!state.githubToken) {
682
- if (!isAndroidLikeRuntime()) {
683
- startupSyncStatus.mode = "idle";
684
- startupSyncStatus.lastAction = "skip-upstream-non-android";
685
- startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
686
- return;
687
- }
688
- startupSyncStatus.mode = "unauthenticated-bootstrap";
689
- startupSyncStatus.lastAction = "pull-upstream";
690
- await bootstrapSkillsFromUpstreamIntoLocal(localSkillsDir);
691
- try {
692
- await appServer.rpc("skills/list", { forceReload: true });
693
- } catch {
694
- }
695
- startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
696
- startupSyncStatus.lastAction = "pull-upstream-complete";
697
- return;
698
- }
699
- startupSyncStatus.mode = "authenticated-fork-sync";
700
- startupSyncStatus.lastAction = "ensure-private-fork";
701
- const username = state.githubUsername || await resolveGithubUsername(state.githubToken);
702
- const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
703
- await ensurePrivateForkFromUpstream(state.githubToken, username, repoName);
704
- const nextState = { ...state, githubUsername: username, repoOwner: username, repoName };
705
- await writeSkillsSyncState(nextState);
706
- startupSyncStatus.lastAction = "pull-private-fork";
707
- await pullInstalledSkillsFolderFromRepo(state.githubToken, username, repoName, localSkillsDir);
708
- try {
709
- await appServer.rpc("skills/list", { forceReload: true });
710
- } catch {
711
- }
712
- startupSyncStatus.lastAction = "push-private-fork";
713
- await autoPushSyncedSkills(appServer);
714
- startupSyncStatus.lastSuccessAtIso = (/* @__PURE__ */ new Date()).toISOString();
715
- startupSyncStatus.lastAction = "startup-sync-complete";
716
- } catch (error) {
717
- startupSyncStatus.lastError = getErrorMessage(error, "startup-sync-failed");
718
- startupSyncStatus.lastAction = "startup-sync-failed";
719
- } finally {
720
- startupSyncStatus.inProgress = false;
721
- }
722
- }
723
- async function finalizeGithubLoginAndSync(token, username, appServer) {
724
- const repoName = DEFAULT_SKILLS_SYNC_REPO_NAME;
725
- await ensurePrivateForkFromUpstream(token, username, repoName);
726
- const current = await readSkillsSyncState();
727
- await writeSkillsSyncState({
728
- ...current,
729
- githubToken: token,
730
- githubUsername: username,
731
- repoOwner: username,
732
- repoName
733
- });
734
- const localDir = getSkillsInstallDir();
735
- await pullInstalledSkillsFolderFromRepo(token, username, repoName, localDir);
736
- try {
737
- await appServer.rpc("skills/list", { forceReload: true });
738
- } catch {
739
- }
740
- await autoPushSyncedSkills(appServer);
741
- }
742
346
  async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
743
347
  const q = query.toLowerCase().trim();
744
348
  let filtered = q ? allEntries.filter((s) => {
@@ -1330,9 +934,82 @@ function getSharedBridgeState() {
1330
934
  globalScope[SHARED_BRIDGE_KEY] = created;
1331
935
  return created;
1332
936
  }
937
+ async function loadAllThreadsForSearch(appServer) {
938
+ const threads = [];
939
+ let cursor = null;
940
+ do {
941
+ const response = asRecord(await appServer.rpc("thread/list", {
942
+ archived: false,
943
+ limit: 100,
944
+ sortKey: "updated_at",
945
+ cursor
946
+ }));
947
+ const data = Array.isArray(response?.data) ? response.data : [];
948
+ for (const row of data) {
949
+ const record = asRecord(row);
950
+ const id = typeof record?.id === "string" ? record.id : "";
951
+ if (!id) continue;
952
+ const title = typeof record?.name === "string" && record.name.trim().length > 0 ? record.name.trim() : typeof record?.preview === "string" && record.preview.trim().length > 0 ? record.preview.trim() : "Untitled thread";
953
+ const preview = typeof record?.preview === "string" ? record.preview : "";
954
+ threads.push({ id, title, preview });
955
+ }
956
+ cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
957
+ } while (cursor);
958
+ const docs = [];
959
+ const concurrency = 4;
960
+ for (let offset = 0; offset < threads.length; offset += concurrency) {
961
+ const batch = threads.slice(offset, offset + concurrency);
962
+ const loaded = await Promise.all(batch.map(async (thread) => {
963
+ try {
964
+ const readResponse = await appServer.rpc("thread/read", {
965
+ threadId: thread.id,
966
+ includeTurns: true
967
+ });
968
+ const messageText = extractThreadMessageText(readResponse);
969
+ const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
970
+ return {
971
+ id: thread.id,
972
+ title: thread.title,
973
+ preview: thread.preview,
974
+ messageText,
975
+ searchableText
976
+ };
977
+ } catch {
978
+ const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
979
+ return {
980
+ id: thread.id,
981
+ title: thread.title,
982
+ preview: thread.preview,
983
+ messageText: "",
984
+ searchableText
985
+ };
986
+ }
987
+ }));
988
+ docs.push(...loaded);
989
+ }
990
+ return docs;
991
+ }
992
+ async function buildThreadSearchIndex(appServer) {
993
+ const docs = await loadAllThreadsForSearch(appServer);
994
+ const docsById = new Map(docs.map((doc) => [doc.id, doc]));
995
+ return { docsById };
996
+ }
1333
997
  function createCodexBridgeMiddleware() {
1334
998
  const { appServer, methodCatalog } = getSharedBridgeState();
1335
- void initializeSkillsSyncOnStartup(appServer);
999
+ let threadSearchIndex = null;
1000
+ let threadSearchIndexPromise = null;
1001
+ async function getThreadSearchIndex() {
1002
+ if (threadSearchIndex) return threadSearchIndex;
1003
+ if (!threadSearchIndexPromise) {
1004
+ threadSearchIndexPromise = buildThreadSearchIndex(appServer).then((index) => {
1005
+ threadSearchIndex = index;
1006
+ return index;
1007
+ }).finally(() => {
1008
+ threadSearchIndexPromise = null;
1009
+ });
1010
+ }
1011
+ return threadSearchIndexPromise;
1012
+ }
1336
1013
  const middleware = async (req, res, next) => {
1337
1014
  try {
1338
1015
  if (!req.url) {
@@ -1398,6 +1075,76 @@ function createCodexBridgeMiddleware() {
1398
1075
  setJson(res, 200, { data: { path: homedir() } });
1399
1076
  return;
1400
1077
  }
1078
+ if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
1079
+ const payload = asRecord(await readJsonBody(req));
1080
+ const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
1081
+ if (!rawSourceCwd) {
1082
+ setJson(res, 400, { error: "Missing sourceCwd" });
1083
+ return;
1084
+ }
1085
+ const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
1086
+ try {
1087
+ const sourceInfo = await stat(sourceCwd);
1088
+ if (!sourceInfo.isDirectory()) {
1089
+ setJson(res, 400, { error: "sourceCwd is not a directory" });
1090
+ return;
1091
+ }
1092
+ } catch {
1093
+ setJson(res, 404, { error: "sourceCwd does not exist" });
1094
+ return;
1095
+ }
1096
+ try {
1097
+ let gitRoot = "";
1098
+ try {
1099
+ gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
1100
+ } catch (error) {
1101
+ if (!isNotGitRepositoryError(error)) throw error;
1102
+ await runCommand("git", ["init"], { cwd: sourceCwd });
1103
+ gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
1104
+ }
1105
+ const repoName = basename(gitRoot) || "repo";
1106
+ const worktreesRoot = join(getCodexHomeDir(), "worktrees");
1107
+ await mkdir(worktreesRoot, { recursive: true });
1108
+ let worktreeId = "";
1109
+ let worktreeParent = "";
1110
+ let worktreeCwd = "";
1111
+ for (let attempt = 0; attempt < 12; attempt += 1) {
1112
+ const candidate = randomBytes(2).toString("hex");
1113
+ const parent = join(worktreesRoot, candidate);
1114
+ try {
1115
+ await stat(parent);
1116
+ continue;
1117
+ } catch {
1118
+ worktreeId = candidate;
1119
+ worktreeParent = parent;
1120
+ worktreeCwd = join(parent, repoName);
1121
+ break;
1122
+ }
1123
+ }
1124
+ if (!worktreeId || !worktreeParent || !worktreeCwd) {
1125
+ throw new Error("Failed to allocate a unique worktree id");
1126
+ }
1127
+ const branch = `codex/${worktreeId}`;
1128
+ await mkdir(worktreeParent, { recursive: true });
1129
+ try {
1130
+ await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1131
+ } catch (error) {
1132
+ if (!isMissingHeadError(error)) throw error;
1133
+ await ensureRepoHasInitialCommit(gitRoot);
1134
+ await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1135
+ }
1136
+ setJson(res, 200, {
1137
+ data: {
1138
+ cwd: worktreeCwd,
1139
+ branch,
1140
+ gitRoot
1141
+ }
1142
+ });
1143
+ } catch (error) {
1144
+ setJson(res, 500, { error: getErrorMessage(error, "Failed to create worktree") });
1145
+ }
1146
+ return;
1147
+ }
1401
1148
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
1402
1149
  const payload = await readJsonBody(req);
1403
1150
  const record = asRecord(payload);
@@ -1523,6 +1270,20 @@ function createCodexBridgeMiddleware() {
1523
1270
  setJson(res, 200, { data: cache });
1524
1271
  return;
1525
1272
  }
1273
+ if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
1274
+ const payload = asRecord(await readJsonBody(req));
1275
+ const query = typeof payload?.query === "string" ? payload.query.trim() : "";
1276
+ const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
1277
+ const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
1278
+ if (!query) {
1279
+ setJson(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
1280
+ return;
1281
+ }
1282
+ const index = await getThreadSearchIndex();
1283
+ 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 } });
1285
+ return;
1286
+ }
1526
1287
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
1527
1288
  const payload = asRecord(await readJsonBody(req));
1528
1289
  const id = typeof payload?.id === "string" ? payload.id : "";
@@ -1579,182 +1340,6 @@ function createCodexBridgeMiddleware() {
1579
1340
  }
1580
1341
  return;
1581
1342
  }
1582
- if (req.method === "GET" && url.pathname === "/codex-api/skills-sync/status") {
1583
- const state = await readSkillsSyncState();
1584
- setJson(res, 200, {
1585
- data: {
1586
- loggedIn: Boolean(state.githubToken),
1587
- githubUsername: state.githubUsername ?? "",
1588
- repoOwner: state.repoOwner ?? "",
1589
- repoName: state.repoName ?? "",
1590
- configured: Boolean(state.githubToken && state.repoOwner && state.repoName),
1591
- startup: {
1592
- inProgress: startupSyncStatus.inProgress,
1593
- mode: startupSyncStatus.mode,
1594
- branch: startupSyncStatus.branch,
1595
- lastAction: startupSyncStatus.lastAction,
1596
- lastRunAtIso: startupSyncStatus.lastRunAtIso,
1597
- lastSuccessAtIso: startupSyncStatus.lastSuccessAtIso,
1598
- lastError: startupSyncStatus.lastError
1599
- }
1600
- }
1601
- });
1602
- return;
1603
- }
1604
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/start-login") {
1605
- try {
1606
- const started = await startGithubDeviceLogin();
1607
- setJson(res, 200, { data: started });
1608
- } catch (error) {
1609
- setJson(res, 502, { error: getErrorMessage(error, "Failed to start GitHub login") });
1610
- }
1611
- return;
1612
- }
1613
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/token-login") {
1614
- try {
1615
- const payload = asRecord(await readJsonBody(req));
1616
- const token = typeof payload?.token === "string" ? payload.token.trim() : "";
1617
- if (!token) {
1618
- setJson(res, 400, { error: "Missing GitHub token" });
1619
- return;
1620
- }
1621
- const username = await resolveGithubUsername(token);
1622
- await finalizeGithubLoginAndSync(token, username, appServer);
1623
- setJson(res, 200, { ok: true, data: { githubUsername: username } });
1624
- } catch (error) {
1625
- setJson(res, 502, { error: getErrorMessage(error, "Failed to login with GitHub token") });
1626
- }
1627
- return;
1628
- }
1629
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/logout") {
1630
- try {
1631
- const state = await readSkillsSyncState();
1632
- await writeSkillsSyncState({
1633
- ...state,
1634
- githubToken: void 0,
1635
- githubUsername: void 0,
1636
- repoOwner: void 0,
1637
- repoName: void 0
1638
- });
1639
- setJson(res, 200, { ok: true });
1640
- } catch (error) {
1641
- setJson(res, 500, { error: getErrorMessage(error, "Failed to logout GitHub") });
1642
- }
1643
- return;
1644
- }
1645
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/github/complete-login") {
1646
- try {
1647
- const payload = asRecord(await readJsonBody(req));
1648
- const deviceCode = typeof payload?.deviceCode === "string" ? payload.deviceCode : "";
1649
- if (!deviceCode) {
1650
- setJson(res, 400, { error: "Missing deviceCode" });
1651
- return;
1652
- }
1653
- const result = await completeGithubDeviceLogin(deviceCode);
1654
- if (!result.token) {
1655
- setJson(res, 200, { ok: false, pending: result.error === "authorization_pending", error: result.error || "login_failed" });
1656
- return;
1657
- }
1658
- const token = result.token;
1659
- const username = await resolveGithubUsername(token);
1660
- await finalizeGithubLoginAndSync(token, username, appServer);
1661
- setJson(res, 200, { ok: true, data: { githubUsername: username } });
1662
- } catch (error) {
1663
- setJson(res, 502, { error: getErrorMessage(error, "Failed to complete GitHub login") });
1664
- }
1665
- return;
1666
- }
1667
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/push") {
1668
- try {
1669
- const state = await readSkillsSyncState();
1670
- if (!state.githubToken || !state.repoOwner || !state.repoName) {
1671
- setJson(res, 400, { error: "Skills sync is not configured yet" });
1672
- return;
1673
- }
1674
- if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
1675
- setJson(res, 400, { error: "Refusing to push to upstream repository" });
1676
- return;
1677
- }
1678
- const local = await collectLocalSyncedSkills(appServer);
1679
- const installedMap = await scanInstalledSkillsFromDisk();
1680
- await writeRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName, local);
1681
- await syncInstalledSkillsFolderToRepo(state.githubToken, state.repoOwner, state.repoName, installedMap);
1682
- setJson(res, 200, { ok: true, data: { synced: local.length } });
1683
- } catch (error) {
1684
- setJson(res, 502, { error: getErrorMessage(error, "Failed to push synced skills") });
1685
- }
1686
- return;
1687
- }
1688
- if (req.method === "POST" && url.pathname === "/codex-api/skills-sync/pull") {
1689
- try {
1690
- const state = await readSkillsSyncState();
1691
- if (!state.githubToken || !state.repoOwner || !state.repoName) {
1692
- setJson(res, 400, { error: "Skills sync is not configured yet" });
1693
- return;
1694
- }
1695
- const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
1696
- const tree = await fetchSkillsTree();
1697
- const uniqueOwnerByName = /* @__PURE__ */ new Map();
1698
- const ambiguousNames = /* @__PURE__ */ new Set();
1699
- for (const entry of tree) {
1700
- if (ambiguousNames.has(entry.name)) continue;
1701
- const existingOwner = uniqueOwnerByName.get(entry.name);
1702
- if (!existingOwner) {
1703
- uniqueOwnerByName.set(entry.name, entry.owner);
1704
- continue;
1705
- }
1706
- if (existingOwner !== entry.owner) {
1707
- uniqueOwnerByName.delete(entry.name);
1708
- ambiguousNames.add(entry.name);
1709
- }
1710
- }
1711
- const localDir = await detectUserSkillsDir(appServer);
1712
- await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName, localDir);
1713
- const installerScript = "/Users/igor/.cursor/skills/.system/skill-installer/scripts/install-skill-from-github.py";
1714
- const localSkills = await scanInstalledSkillsFromDisk();
1715
- for (const skill of remote) {
1716
- const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
1717
- if (!owner) {
1718
- continue;
1719
- }
1720
- if (!localSkills.has(skill.name)) {
1721
- await runCommand("python3", [
1722
- installerScript,
1723
- "--repo",
1724
- `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
1725
- "--path",
1726
- `skills/${owner}/${skill.name}`,
1727
- "--dest",
1728
- localDir,
1729
- "--method",
1730
- "git"
1731
- ]);
1732
- }
1733
- const skillPath = join(localDir, skill.name);
1734
- await appServer.rpc("skills/config/write", { path: skillPath, enabled: skill.enabled });
1735
- }
1736
- const remoteNames = new Set(remote.map((row) => row.name));
1737
- for (const [name, localInfo] of localSkills.entries()) {
1738
- if (!remoteNames.has(name)) {
1739
- await rm(localInfo.path.replace(/\/SKILL\.md$/, ""), { recursive: true, force: true });
1740
- }
1741
- }
1742
- const nextOwners = {};
1743
- for (const item of remote) {
1744
- const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
1745
- if (owner) nextOwners[item.name] = owner;
1746
- }
1747
- await writeSkillsSyncState({ ...state, installedOwners: nextOwners });
1748
- try {
1749
- await appServer.rpc("skills/list", { forceReload: true });
1750
- } catch {
1751
- }
1752
- setJson(res, 200, { ok: true, data: { synced: remote.length } });
1753
- } catch (error) {
1754
- setJson(res, 502, { error: getErrorMessage(error, "Failed to pull synced skills") });
1755
- }
1756
- return;
1757
- }
1758
1343
  if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
1759
1344
  try {
1760
1345
  const owner = url.searchParams.get("owner") || "";
@@ -1763,7 +1348,7 @@ function createCodexBridgeMiddleware() {
1763
1348
  setJson(res, 400, { error: "Missing owner or name" });
1764
1349
  return;
1765
1350
  }
1766
- const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
1351
+ const rawUrl = `https://raw.githubusercontent.com/openclaw/skills/main/skills/${owner}/${name}/SKILL.md`;
1767
1352
  const resp = await fetch(rawUrl);
1768
1353
  if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
1769
1354
  const content = await resp.text();
@@ -1788,7 +1373,7 @@ function createCodexBridgeMiddleware() {
1788
1373
  await runCommand("python3", [
1789
1374
  installerScript,
1790
1375
  "--repo",
1791
- `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
1376
+ "openclaw/skills",
1792
1377
  "--path",
1793
1378
  skillPathInRepo,
1794
1379
  "--dest",
@@ -1798,10 +1383,6 @@ function createCodexBridgeMiddleware() {
1798
1383
  ]);
1799
1384
  const skillDir = join(installDest, name);
1800
1385
  await ensureInstalledSkillIsValid(appServer, skillDir);
1801
- const syncState = await readSkillsSyncState();
1802
- const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
1803
- await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1804
- await autoPushSyncedSkills(appServer);
1805
1386
  setJson(res, 200, { ok: true, path: skillDir });
1806
1387
  } catch (error) {
1807
1388
  setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
@@ -1819,13 +1400,6 @@ function createCodexBridgeMiddleware() {
1819
1400
  return;
1820
1401
  }
1821
1402
  await rm(target, { recursive: true, force: true });
1822
- if (name) {
1823
- const syncState = await readSkillsSyncState();
1824
- const nextOwners = { ...syncState.installedOwners ?? {} };
1825
- delete nextOwners[name];
1826
- await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
1827
- }
1828
- await autoPushSyncedSkills(appServer);
1829
1403
  try {
1830
1404
  await appServer.rpc("skills/list", { forceReload: true });
1831
1405
  } catch {
@@ -1873,6 +1447,7 @@ data: ${JSON.stringify({ ok: true })}
1873
1447
  }
1874
1448
  };
1875
1449
  middleware.dispose = () => {
1450
+ threadSearchIndex = null;
1876
1451
  appServer.dispose();
1877
1452
  };
1878
1453
  middleware.subscribeNotifications = (listener) => {
@@ -1887,7 +1462,7 @@ data: ${JSON.stringify({ ok: true })}
1887
1462
  }
1888
1463
 
1889
1464
  // src/server/authMiddleware.ts
1890
- import { randomBytes, timingSafeEqual } from "crypto";
1465
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1891
1466
  var TOKEN_COOKIE = "codex_web_local_token";
1892
1467
  function constantTimeCompare(a, b) {
1893
1468
  const bufA = Buffer.from(a);
@@ -1985,7 +1560,7 @@ function createAuthSession(password) {
1985
1560
  res.status(401).json({ error: "Invalid password" });
1986
1561
  return;
1987
1562
  }
1988
- const token = randomBytes(32).toString("hex");
1563
+ const token = randomBytes2(32).toString("hex");
1989
1564
  validTokens.add(token);
1990
1565
  res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
1991
1566
  res.json({ ok: true });
@@ -2004,11 +1579,277 @@ function createAuthSession(password) {
2004
1579
  };
2005
1580
  }
2006
1581
 
1582
+ // 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";
1585
+ var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
1586
+ ".txt",
1587
+ ".md",
1588
+ ".json",
1589
+ ".js",
1590
+ ".ts",
1591
+ ".tsx",
1592
+ ".jsx",
1593
+ ".css",
1594
+ ".scss",
1595
+ ".html",
1596
+ ".htm",
1597
+ ".xml",
1598
+ ".yml",
1599
+ ".yaml",
1600
+ ".log",
1601
+ ".csv",
1602
+ ".env",
1603
+ ".py",
1604
+ ".sh",
1605
+ ".toml",
1606
+ ".ini",
1607
+ ".conf",
1608
+ ".sql",
1609
+ ".bat",
1610
+ ".cmd",
1611
+ ".ps1"
1612
+ ]);
1613
+ function languageForPath(pathValue) {
1614
+ const extension = extname(pathValue).toLowerCase();
1615
+ switch (extension) {
1616
+ case ".js":
1617
+ return "javascript";
1618
+ case ".ts":
1619
+ return "typescript";
1620
+ case ".jsx":
1621
+ return "javascript";
1622
+ case ".tsx":
1623
+ return "typescript";
1624
+ case ".py":
1625
+ return "python";
1626
+ case ".sh":
1627
+ return "sh";
1628
+ case ".css":
1629
+ case ".scss":
1630
+ return "css";
1631
+ case ".html":
1632
+ case ".htm":
1633
+ return "html";
1634
+ case ".json":
1635
+ return "json";
1636
+ case ".md":
1637
+ return "markdown";
1638
+ case ".yaml":
1639
+ case ".yml":
1640
+ return "yaml";
1641
+ case ".xml":
1642
+ return "xml";
1643
+ case ".sql":
1644
+ return "sql";
1645
+ case ".toml":
1646
+ return "ini";
1647
+ case ".ini":
1648
+ case ".conf":
1649
+ return "ini";
1650
+ default:
1651
+ return "plaintext";
1652
+ }
1653
+ }
1654
+ function normalizeLocalPath(rawPath) {
1655
+ const trimmed = rawPath.trim();
1656
+ if (!trimmed) return "";
1657
+ if (trimmed.startsWith("file://")) {
1658
+ try {
1659
+ return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
1660
+ } catch {
1661
+ return trimmed.replace(/^file:\/\//u, "");
1662
+ }
1663
+ }
1664
+ return trimmed;
1665
+ }
1666
+ function decodeBrowsePath(rawPath) {
1667
+ if (!rawPath) return "";
1668
+ try {
1669
+ return decodeURIComponent(rawPath);
1670
+ } catch {
1671
+ return rawPath;
1672
+ }
1673
+ }
1674
+ function isTextEditablePath(pathValue) {
1675
+ return TEXT_EDITABLE_EXTENSIONS.has(extname(pathValue).toLowerCase());
1676
+ }
1677
+ function looksLikeTextBuffer(buffer) {
1678
+ if (buffer.length === 0) return true;
1679
+ for (const byte of buffer) {
1680
+ if (byte === 0) return false;
1681
+ }
1682
+ const decoded = buffer.toString("utf8");
1683
+ const replacementCount = (decoded.match(/\uFFFD/gu) ?? []).length;
1684
+ return replacementCount / decoded.length < 0.05;
1685
+ }
1686
+ async function probeFileIsText(localPath) {
1687
+ const handle = await open(localPath, "r");
1688
+ try {
1689
+ const sample = Buffer.allocUnsafe(4096);
1690
+ const { bytesRead } = await handle.read(sample, 0, sample.length, 0);
1691
+ return looksLikeTextBuffer(sample.subarray(0, bytesRead));
1692
+ } finally {
1693
+ await handle.close();
1694
+ }
1695
+ }
1696
+ async function isTextEditableFile(localPath) {
1697
+ if (isTextEditablePath(localPath)) return true;
1698
+ try {
1699
+ const fileStat = await stat2(localPath);
1700
+ if (!fileStat.isFile()) return false;
1701
+ return await probeFileIsText(localPath);
1702
+ } catch {
1703
+ return false;
1704
+ }
1705
+ }
1706
+ function escapeHtml(value) {
1707
+ return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
1708
+ }
1709
+ function toBrowseHref(pathValue) {
1710
+ return `/codex-local-browse${encodeURI(pathValue)}`;
1711
+ }
1712
+ function toEditHref(pathValue) {
1713
+ return `/codex-local-edit${encodeURI(pathValue)}`;
1714
+ }
1715
+ function escapeForInlineScriptString(value) {
1716
+ return JSON.stringify(value).replace(/<\//gu, "<\\/").replace(/<!--/gu, "<\\!--").replace(/\u2028/gu, "\\u2028").replace(/\u2029/gu, "\\u2029");
1717
+ }
1718
+ async function getDirectoryItems(localPath) {
1719
+ const entries = await readdir2(localPath, { withFileTypes: true });
1720
+ const withMeta = await Promise.all(entries.map(async (entry) => {
1721
+ const entryPath = join2(localPath, entry.name);
1722
+ const entryStat = await stat2(entryPath);
1723
+ const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
1724
+ return {
1725
+ name: entry.name,
1726
+ path: entryPath,
1727
+ isDirectory: entry.isDirectory(),
1728
+ editable,
1729
+ mtimeMs: entryStat.mtimeMs
1730
+ };
1731
+ }));
1732
+ return withMeta.sort((a, b) => {
1733
+ if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs;
1734
+ if (a.isDirectory && !b.isDirectory) return -1;
1735
+ if (!a.isDirectory && b.isDirectory) return 1;
1736
+ return a.name.localeCompare(b.name);
1737
+ });
1738
+ }
1739
+ async function createDirectoryListingHtml(localPath) {
1740
+ const items = await getDirectoryItems(localPath);
1741
+ const parentPath = dirname(localPath);
1742
+ const rows = items.map((item) => {
1743
+ const suffix = item.isDirectory ? "/" : "";
1744
+ 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>`;
1746
+ }).join("\n");
1747
+ const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
1748
+ return `<!doctype html>
1749
+ <html lang="en">
1750
+ <head>
1751
+ <meta charset="utf-8" />
1752
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1753
+ <title>Index of ${escapeHtml(localPath)}</title>
1754
+ <style>
1755
+ body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 16px; background: #0b1020; color: #dbe6ff; }
1756
+ a { color: #8cc2ff; text-decoration: none; }
1757
+ a:hover { text-decoration: underline; }
1758
+ ul { list-style: none; padding: 0; margin: 12px 0 0; display: flex; flex-direction: column; gap: 8px; }
1759
+ .file-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 10px; }
1760
+ .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; }
1762
+ .icon-btn:hover { filter: brightness(1.08); text-decoration: none; }
1763
+ h1 { font-size: 18px; margin: 0; word-break: break-all; }
1764
+ @media (max-width: 640px) {
1765
+ body { margin: 12px; }
1766
+ .file-row { gap: 8px; }
1767
+ .file-link { font-size: 15px; padding: 12px; }
1768
+ .icon-btn { width: 44px; height: 44px; }
1769
+ }
1770
+ </style>
1771
+ </head>
1772
+ <body>
1773
+ <h1>Index of ${escapeHtml(localPath)}</h1>
1774
+ ${parentLink}
1775
+ <ul>${rows}</ul>
1776
+ </body>
1777
+ </html>`;
1778
+ }
1779
+ async function createTextEditorHtml(localPath) {
1780
+ const content = await readFile2(localPath, "utf8");
1781
+ const parentPath = dirname(localPath);
1782
+ const language = languageForPath(localPath);
1783
+ const safeContentLiteral = escapeForInlineScriptString(content);
1784
+ return `<!doctype html>
1785
+ <html lang="en">
1786
+ <head>
1787
+ <meta charset="utf-8" />
1788
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1789
+ <title>Edit ${escapeHtml(localPath)}</title>
1790
+ <style>
1791
+ html, body { width: 100%; height: 100%; margin: 0; }
1792
+ body { font-family: ui-monospace, Menlo, Monaco, monospace; background: #0b1020; color: #dbe6ff; display: flex; flex-direction: column; overflow: hidden; }
1793
+ .toolbar { position: sticky; top: 0; z-index: 10; display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; background: #0b1020; border-bottom: 1px solid #243a5a; }
1794
+ .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
1795
+ button, a { background: #1b2a4a; color: #dbe6ff; border: 1px solid #345; padding: 6px 10px; border-radius: 6px; text-decoration: none; cursor: pointer; }
1796
+ button:hover, a:hover { filter: brightness(1.08); }
1797
+ #editor { flex: 1 1 auto; min-height: 0; width: 100%; border: none; overflow: hidden; }
1798
+ #status { margin-left: 8px; color: #8cc2ff; }
1799
+ .ace_editor { background: #07101f !important; color: #dbe6ff !important; width: 100% !important; height: 100% !important; }
1800
+ .ace_gutter { background: #07101f !important; color: #6f8eb5 !important; }
1801
+ .ace_marker-layer .ace_active-line { background: #10213c !important; }
1802
+ .ace_marker-layer .ace_selection { background: rgba(140, 194, 255, 0.3) !important; }
1803
+ .meta { opacity: 0.9; font-size: 12px; overflow-wrap: anywhere; }
1804
+ </style>
1805
+ </head>
1806
+ <body>
1807
+ <div class="toolbar">
1808
+ <div class="row">
1809
+ <a href="${escapeHtml(toBrowseHref(parentPath))}">Back</a>
1810
+ <button id="saveBtn" type="button">Save</button>
1811
+ <span id="status"></span>
1812
+ </div>
1813
+ <div class="meta">${escapeHtml(localPath)} \xB7 ${escapeHtml(language)}</div>
1814
+ </div>
1815
+ <div id="editor"></div>
1816
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.2/ace.js"></script>
1817
+ <script>
1818
+ const saveBtn = document.getElementById('saveBtn');
1819
+ const status = document.getElementById('status');
1820
+ const editor = ace.edit('editor');
1821
+ editor.setTheme('ace/theme/tomorrow_night');
1822
+ editor.session.setMode('ace/mode/${escapeHtml(language)}');
1823
+ editor.setValue(${safeContentLiteral}, -1);
1824
+ editor.setOptions({
1825
+ fontSize: '13px',
1826
+ wrap: true,
1827
+ showPrintMargin: false,
1828
+ useSoftTabs: true,
1829
+ tabSize: 2,
1830
+ behavioursEnabled: true,
1831
+ });
1832
+ editor.resize();
1833
+
1834
+ saveBtn.addEventListener('click', async () => {
1835
+ status.textContent = 'Saving...';
1836
+ const response = await fetch(location.pathname, {
1837
+ method: 'PUT',
1838
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
1839
+ body: editor.getValue(),
1840
+ });
1841
+ status.textContent = response.ok ? 'Saved' : 'Save failed';
1842
+ });
1843
+ </script>
1844
+ </body>
1845
+ </html>`;
1846
+ }
1847
+
2007
1848
  // src/server/httpServer.ts
2008
1849
  import { WebSocketServer } from "ws";
2009
- var __dirname = dirname(fileURLToPath(import.meta.url));
2010
- var distDir = join2(__dirname, "..", "dist");
2011
- var spaEntryFile = join2(distDir, "index.html");
1850
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
1851
+ var distDir = join3(__dirname, "..", "dist");
1852
+ var spaEntryFile = join3(distDir, "index.html");
2012
1853
  var IMAGE_CONTENT_TYPES = {
2013
1854
  ".avif": "image/avif",
2014
1855
  ".bmp": "image/bmp",
@@ -2031,6 +1872,11 @@ function normalizeLocalImagePath(rawPath) {
2031
1872
  }
2032
1873
  return trimmed;
2033
1874
  }
1875
+ function readWildcardPathParam(value) {
1876
+ if (typeof value === "string") return value;
1877
+ if (Array.isArray(value)) return value.join("/");
1878
+ return "";
1879
+ }
2034
1880
  function createServer(options = {}) {
2035
1881
  const app = express();
2036
1882
  const bridge = createCodexBridgeMiddleware();
@@ -2046,7 +1892,7 @@ function createServer(options = {}) {
2046
1892
  res.status(400).json({ error: "Expected absolute local file path." });
2047
1893
  return;
2048
1894
  }
2049
- const contentType = IMAGE_CONTENT_TYPES[extname(localPath).toLowerCase()];
1895
+ const contentType = IMAGE_CONTENT_TYPES[extname2(localPath).toLowerCase()];
2050
1896
  if (!contentType) {
2051
1897
  res.status(415).json({ error: "Unsupported image type." });
2052
1898
  return;
@@ -2058,7 +1904,82 @@ function createServer(options = {}) {
2058
1904
  if (!res.headersSent) res.status(404).json({ error: "Image file not found." });
2059
1905
  });
2060
1906
  });
2061
- const hasFrontendAssets = existsSync2(spaEntryFile);
1907
+ app.get("/codex-local-file", (req, res) => {
1908
+ const rawPath = typeof req.query.path === "string" ? req.query.path : "";
1909
+ const localPath = normalizeLocalPath(rawPath);
1910
+ if (!localPath || !isAbsolute2(localPath)) {
1911
+ res.status(400).json({ error: "Expected absolute local file path." });
1912
+ return;
1913
+ }
1914
+ res.setHeader("Cache-Control", "private, no-store");
1915
+ res.setHeader("Content-Disposition", "inline");
1916
+ res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
1917
+ if (!error) return;
1918
+ if (!res.headersSent) res.status(404).json({ error: "File not found." });
1919
+ });
1920
+ });
1921
+ app.get("/codex-local-browse/*path", async (req, res) => {
1922
+ const rawPath = readWildcardPathParam(req.params.path);
1923
+ const localPath = decodeBrowsePath(`/${rawPath}`);
1924
+ if (!localPath || !isAbsolute2(localPath)) {
1925
+ res.status(400).json({ error: "Expected absolute local file path." });
1926
+ return;
1927
+ }
1928
+ try {
1929
+ const fileStat = await stat3(localPath);
1930
+ res.setHeader("Cache-Control", "private, no-store");
1931
+ if (fileStat.isDirectory()) {
1932
+ const html = await createDirectoryListingHtml(localPath);
1933
+ res.status(200).type("text/html; charset=utf-8").send(html);
1934
+ return;
1935
+ }
1936
+ res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
1937
+ if (!error) return;
1938
+ if (!res.headersSent) res.status(404).json({ error: "File not found." });
1939
+ });
1940
+ } catch {
1941
+ res.status(404).json({ error: "File not found." });
1942
+ }
1943
+ });
1944
+ app.get("/codex-local-edit/*path", async (req, res) => {
1945
+ const rawPath = readWildcardPathParam(req.params.path);
1946
+ const localPath = decodeBrowsePath(`/${rawPath}`);
1947
+ if (!localPath || !isAbsolute2(localPath)) {
1948
+ res.status(400).json({ error: "Expected absolute local file path." });
1949
+ return;
1950
+ }
1951
+ try {
1952
+ const fileStat = await stat3(localPath);
1953
+ if (!fileStat.isFile()) {
1954
+ res.status(400).json({ error: "Expected file path." });
1955
+ return;
1956
+ }
1957
+ const html = await createTextEditorHtml(localPath);
1958
+ res.status(200).type("text/html; charset=utf-8").send(html);
1959
+ } catch {
1960
+ res.status(404).json({ error: "File not found." });
1961
+ }
1962
+ });
1963
+ app.put("/codex-local-edit/*path", express.text({ type: "*/*", limit: "10mb" }), async (req, res) => {
1964
+ const rawPath = readWildcardPathParam(req.params.path);
1965
+ const localPath = decodeBrowsePath(`/${rawPath}`);
1966
+ if (!localPath || !isAbsolute2(localPath)) {
1967
+ res.status(400).json({ error: "Expected absolute local file path." });
1968
+ return;
1969
+ }
1970
+ if (!await isTextEditableFile(localPath)) {
1971
+ res.status(415).json({ error: "Only text-like files are editable." });
1972
+ return;
1973
+ }
1974
+ const body = typeof req.body === "string" ? req.body : "";
1975
+ try {
1976
+ await writeFile2(localPath, body, "utf8");
1977
+ res.status(200).json({ ok: true });
1978
+ } catch {
1979
+ res.status(404).json({ error: "File not found." });
1980
+ }
1981
+ });
1982
+ const hasFrontendAssets = existsSync(spaEntryFile);
2062
1983
  if (hasFrontendAssets) {
2063
1984
  app.use(express.static(distDir));
2064
1985
  }
@@ -2129,11 +2050,11 @@ function generatePassword() {
2129
2050
 
2130
2051
  // src/cli/index.ts
2131
2052
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
2132
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
2053
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
2133
2054
  async function readCliVersion() {
2134
2055
  try {
2135
- const packageJsonPath = join3(__dirname2, "..", "package.json");
2136
- const raw = await readFile2(packageJsonPath, "utf8");
2056
+ const packageJsonPath = join4(__dirname2, "..", "package.json");
2057
+ const raw = await readFile3(packageJsonPath, "utf8");
2137
2058
  const parsed = JSON.parse(raw);
2138
2059
  return typeof parsed.version === "string" ? parsed.version : "unknown";
2139
2060
  } catch {
@@ -2158,29 +2079,127 @@ function runWithStatus(command, args) {
2158
2079
  return result.status ?? -1;
2159
2080
  }
2160
2081
  function getUserNpmPrefix() {
2161
- return join3(homedir2(), ".npm-global");
2082
+ return join4(homedir2(), ".npm-global");
2162
2083
  }
2163
2084
  function resolveCodexCommand() {
2164
2085
  if (canRun("codex", ["--version"])) {
2165
2086
  return "codex";
2166
2087
  }
2167
- const userCandidate = join3(getUserNpmPrefix(), "bin", "codex");
2168
- if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
2088
+ const userCandidate = join4(getUserNpmPrefix(), "bin", "codex");
2089
+ if (existsSync2(userCandidate) && canRun(userCandidate, ["--version"])) {
2169
2090
  return userCandidate;
2170
2091
  }
2171
2092
  const prefix = process.env.PREFIX?.trim();
2172
2093
  if (!prefix) {
2173
2094
  return null;
2174
2095
  }
2175
- const candidate = join3(prefix, "bin", "codex");
2176
- if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
2096
+ const candidate = join4(prefix, "bin", "codex");
2097
+ if (existsSync2(candidate) && canRun(candidate, ["--version"])) {
2177
2098
  return candidate;
2178
2099
  }
2179
2100
  return null;
2180
2101
  }
2102
+ function resolveCloudflaredCommand() {
2103
+ if (canRun("cloudflared", ["--version"])) {
2104
+ return "cloudflared";
2105
+ }
2106
+ const localCandidate = join4(homedir2(), ".local", "bin", "cloudflared");
2107
+ if (existsSync2(localCandidate) && canRun(localCandidate, ["--version"])) {
2108
+ return localCandidate;
2109
+ }
2110
+ return null;
2111
+ }
2112
+ function mapCloudflaredLinuxArch(arch) {
2113
+ if (arch === "x64") {
2114
+ return "amd64";
2115
+ }
2116
+ if (arch === "arm64") {
2117
+ return "arm64";
2118
+ }
2119
+ return null;
2120
+ }
2121
+ function downloadFile(url, destination) {
2122
+ return new Promise((resolve2, reject) => {
2123
+ const request = (currentUrl) => {
2124
+ httpsGet(currentUrl, (response) => {
2125
+ const code = response.statusCode ?? 0;
2126
+ if (code >= 300 && code < 400 && response.headers.location) {
2127
+ response.resume();
2128
+ request(response.headers.location);
2129
+ return;
2130
+ }
2131
+ if (code !== 200) {
2132
+ response.resume();
2133
+ reject(new Error(`Download failed with HTTP status ${String(code)}`));
2134
+ return;
2135
+ }
2136
+ const file = createWriteStream(destination, { mode: 493 });
2137
+ response.pipe(file);
2138
+ file.on("finish", () => {
2139
+ file.close();
2140
+ resolve2();
2141
+ });
2142
+ file.on("error", reject);
2143
+ }).on("error", reject);
2144
+ };
2145
+ request(url);
2146
+ });
2147
+ }
2148
+ async function ensureCloudflaredInstalledLinux() {
2149
+ const current = resolveCloudflaredCommand();
2150
+ if (current) {
2151
+ return current;
2152
+ }
2153
+ if (process.platform !== "linux") {
2154
+ return null;
2155
+ }
2156
+ const mappedArch = mapCloudflaredLinuxArch(process.arch);
2157
+ if (!mappedArch) {
2158
+ throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
2159
+ }
2160
+ const userBinDir = join4(homedir2(), ".local", "bin");
2161
+ mkdirSync(userBinDir, { recursive: true });
2162
+ const destination = join4(userBinDir, "cloudflared");
2163
+ const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
2164
+ console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
2165
+ await downloadFile(downloadUrl, destination);
2166
+ chmodSync(destination, 493);
2167
+ process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
2168
+ const installed = resolveCloudflaredCommand();
2169
+ if (!installed) {
2170
+ throw new Error("cloudflared download completed but executable is still not available");
2171
+ }
2172
+ console.log("\ncloudflared installed.\n");
2173
+ return installed;
2174
+ }
2175
+ async function shouldInstallCloudflaredInteractively() {
2176
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
2177
+ console.warn("\n[cloudflared] cloudflared is missing and terminal is non-interactive, skipping install.");
2178
+ return false;
2179
+ }
2180
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
2181
+ try {
2182
+ const answer = await prompt.question("cloudflared is not installed. Install it now to ~/.local/bin? [y/N] ");
2183
+ const normalized = answer.trim().toLowerCase();
2184
+ return normalized === "y" || normalized === "yes";
2185
+ } finally {
2186
+ prompt.close();
2187
+ }
2188
+ }
2189
+ async function resolveCloudflaredForTunnel() {
2190
+ const current = resolveCloudflaredCommand();
2191
+ if (current) {
2192
+ return current;
2193
+ }
2194
+ const installApproved = await shouldInstallCloudflaredInteractively();
2195
+ if (!installApproved) {
2196
+ return null;
2197
+ }
2198
+ return ensureCloudflaredInstalledLinux();
2199
+ }
2181
2200
  function hasCodexAuth() {
2182
- const codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex");
2183
- return existsSync3(join3(codexHome, "auth.json"));
2201
+ const codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex");
2202
+ return existsSync2(join4(codexHome, "auth.json"));
2184
2203
  }
2185
2204
  function ensureCodexInstalled() {
2186
2205
  let codexCommand = resolveCodexCommand();
@@ -2198,7 +2217,7 @@ function ensureCodexInstalled() {
2198
2217
  Global npm install requires elevated permissions. Retrying with --prefix ${userPrefix}...
2199
2218
  `);
2200
2219
  runOrFail("npm", ["install", "-g", "--prefix", userPrefix, pkg], `${label} (user prefix)`);
2201
- process.env.PATH = `${join3(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
2220
+ process.env.PATH = `${join4(userPrefix, "bin")}:${process.env.PATH ?? ""}`;
2202
2221
  };
2203
2222
  if (isTermuxRuntime()) {
2204
2223
  console.log("\nCodex CLI not found. Installing Termux-compatible Codex CLI from npm...\n");
@@ -2259,9 +2278,27 @@ function parseCloudflaredUrl(chunk) {
2259
2278
  }
2260
2279
  return urlMatch[urlMatch.length - 1] ?? null;
2261
2280
  }
2262
- async function startCloudflaredTunnel(localPort) {
2281
+ function getAccessibleUrls(port) {
2282
+ 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) {
2290
+ continue;
2291
+ }
2292
+ if (entry.family === "IPv4") {
2293
+ urls.add(`http://${entry.address}:${String(port)}`);
2294
+ }
2295
+ }
2296
+ }
2297
+ return Array.from(urls);
2298
+ }
2299
+ async function startCloudflaredTunnel(command, localPort) {
2263
2300
  return new Promise((resolve2, reject) => {
2264
- const child = spawn2("cloudflared", ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
2301
+ const child = spawn2(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
2265
2302
  stdio: ["ignore", "pipe", "pipe"]
2266
2303
  });
2267
2304
  const timeout = setTimeout(() => {
@@ -2312,7 +2349,7 @@ function listenWithFallback(server, startPort) {
2312
2349
  };
2313
2350
  server.once("error", onError);
2314
2351
  server.once("listening", onListening);
2315
- server.listen(port);
2352
+ server.listen(port, "0.0.0.0");
2316
2353
  };
2317
2354
  attempt(startPort);
2318
2355
  });
@@ -2334,7 +2371,11 @@ async function startServer(options) {
2334
2371
  let tunnelUrl = null;
2335
2372
  if (options.tunnel) {
2336
2373
  try {
2337
- const tunnel = await startCloudflaredTunnel(port);
2374
+ const cloudflaredCommand = await resolveCloudflaredForTunnel();
2375
+ if (!cloudflaredCommand) {
2376
+ throw new Error("cloudflared is not installed");
2377
+ }
2378
+ const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
2338
2379
  tunnelChild = tunnel.process;
2339
2380
  tunnelUrl = tunnel.url;
2340
2381
  } catch (error) {
@@ -2349,8 +2390,15 @@ async function startServer(options) {
2349
2390
  ` Version: ${version}`,
2350
2391
  " GitHub: https://github.com/friuns2/codexui",
2351
2392
  "",
2352
- ` Local: http://localhost:${String(port)}`
2393
+ ` Bind: http://0.0.0.0:${String(port)}`
2353
2394
  ];
2395
+ const accessUrls = getAccessibleUrls(port);
2396
+ if (accessUrls.length > 0) {
2397
+ lines.push(` Local: ${accessUrls[0]}`);
2398
+ for (const accessUrl of accessUrls.slice(1)) {
2399
+ lines.push(` Network: ${accessUrl}`);
2400
+ }
2401
+ }
2354
2402
  if (port !== requestedPort) {
2355
2403
  lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
2356
2404
  }
@@ -2359,9 +2407,7 @@ async function startServer(options) {
2359
2407
  }
2360
2408
  if (tunnelUrl) {
2361
2409
  lines.push(` Tunnel: ${tunnelUrl}`);
2362
- lines.push("");
2363
- lines.push(" Tunnel QR code:");
2364
- lines.push(` URL: ${tunnelUrl}`);
2410
+ lines.push(" Tunnel QR code below");
2365
2411
  }
2366
2412
  printTermuxKeepAlive(lines);
2367
2413
  lines.push("");