codexapp 0.1.28 → 0.1.30

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,33 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
+ import "dotenv/config";
4
5
  import { createServer as createServer2 } from "http";
5
- import { chmodSync, createWriteStream, existsSync as existsSync2, mkdirSync } from "fs";
6
+ import { existsSync as existsSync3 } from "fs";
6
7
  import { readFile as readFile2 } from "fs/promises";
7
- import { homedir as homedir2, networkInterfaces } from "os";
8
+ import { homedir as homedir2 } from "os";
8
9
  import { join as join3 } from "path";
9
10
  import { spawn as spawn2, spawnSync } from "child_process";
10
11
  import { fileURLToPath as fileURLToPath2 } from "url";
11
12
  import { dirname as dirname2 } from "path";
12
- import { get as httpsGet } from "https";
13
13
  import { Command } from "commander";
14
14
  import qrcode from "qrcode-terminal";
15
15
 
16
16
  // src/server/httpServer.ts
17
17
  import { fileURLToPath } from "url";
18
18
  import { dirname, extname, isAbsolute as isAbsolute2, join as join2 } from "path";
19
- import { existsSync } from "fs";
20
- import { readdir as readdir2, stat as stat2 } from "fs/promises";
19
+ import { existsSync as existsSync2 } from "fs";
21
20
  import express from "express";
22
21
 
23
22
  // src/server/codexAppServerBridge.ts
23
+ import "dotenv/config";
24
24
  import { spawn } from "child_process";
25
- import { randomBytes } from "crypto";
26
25
  import { mkdtemp, readFile, readdir, rm, mkdir, stat } from "fs/promises";
26
+ import { existsSync } from "fs";
27
27
  import { request as httpsRequest } from "https";
28
28
  import { homedir } from "os";
29
29
  import { tmpdir } from "os";
30
- import { basename, isAbsolute, join, resolve } from "path";
30
+ import { isAbsolute, join, resolve } from "path";
31
31
  import { writeFile } from "fs/promises";
32
32
  function asRecord(value) {
33
33
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -51,46 +51,6 @@ function setJson(res, statusCode, payload) {
51
51
  res.setHeader("Content-Type", "application/json; charset=utf-8");
52
52
  res.end(JSON.stringify(payload));
53
53
  }
54
- function extractThreadMessageText(threadReadPayload) {
55
- const payload = asRecord(threadReadPayload);
56
- const thread = asRecord(payload?.thread);
57
- const turns = Array.isArray(thread?.turns) ? thread.turns : [];
58
- const parts = [];
59
- for (const turn of turns) {
60
- const turnRecord = asRecord(turn);
61
- const items = Array.isArray(turnRecord?.items) ? turnRecord.items : [];
62
- for (const item of items) {
63
- const itemRecord = asRecord(item);
64
- const type = typeof itemRecord?.type === "string" ? itemRecord.type : "";
65
- if (type === "agentMessage" && typeof itemRecord?.text === "string" && itemRecord.text.trim().length > 0) {
66
- parts.push(itemRecord.text.trim());
67
- continue;
68
- }
69
- if (type === "userMessage") {
70
- const content = Array.isArray(itemRecord?.content) ? itemRecord.content : [];
71
- for (const block of content) {
72
- const blockRecord = asRecord(block);
73
- if (blockRecord?.type === "text" && typeof blockRecord.text === "string" && blockRecord.text.trim().length > 0) {
74
- parts.push(blockRecord.text.trim());
75
- }
76
- }
77
- continue;
78
- }
79
- if (type === "commandExecution") {
80
- const command = typeof itemRecord?.command === "string" ? itemRecord.command.trim() : "";
81
- const output = typeof itemRecord?.aggregatedOutput === "string" ? itemRecord.aggregatedOutput.trim() : "";
82
- if (command) parts.push(command);
83
- if (output) parts.push(output);
84
- }
85
- }
86
- }
87
- return parts.join("\n").trim();
88
- }
89
- function isExactPhraseMatch(query, doc) {
90
- const q = query.trim().toLowerCase();
91
- if (!q) return false;
92
- return doc.title.toLowerCase().includes(q) || doc.preview.toLowerCase().includes(q) || doc.messageText.toLowerCase().includes(q);
93
- }
94
54
  function scoreFileCandidate(path, query) {
95
55
  if (!query) return 0;
96
56
  const lowerPath = path.toLowerCase();
@@ -164,30 +124,8 @@ async function runCommand(command, args, options = {}) {
164
124
  });
165
125
  });
166
126
  }
167
- function isMissingHeadError(error) {
168
- const message = getErrorMessage(error, "").toLowerCase();
169
- return message.includes("not a valid object name: 'head'") || message.includes("not a valid object name: head");
170
- }
171
- function isNotGitRepositoryError(error) {
172
- const message = getErrorMessage(error, "").toLowerCase();
173
- return message.includes("not a git repository") || message.includes("fatal: not a git repository");
174
- }
175
- async function ensureRepoHasInitialCommit(repoRoot) {
176
- const agentsPath = join(repoRoot, "AGENTS.md");
177
- try {
178
- await stat(agentsPath);
179
- } catch {
180
- await writeFile(agentsPath, "", "utf8");
181
- }
182
- await runCommand("git", ["add", "AGENTS.md"], { cwd: repoRoot });
183
- await runCommand(
184
- "git",
185
- ["-c", "user.name=Codex", "-c", "user.email=codex@local", "commit", "-m", "Initialize repository for worktree support"],
186
- { cwd: repoRoot }
187
- );
188
- }
189
- async function runCommandCapture(command, args, options = {}) {
190
- return await new Promise((resolveOutput, reject) => {
127
+ async function runCommandWithOutput(command, args, options = {}) {
128
+ return await new Promise((resolve2, reject) => {
191
129
  const proc = spawn(command, args, {
192
130
  cwd: options.cwd,
193
131
  env: process.env,
@@ -204,7 +142,7 @@ async function runCommandCapture(command, args, options = {}) {
204
142
  proc.on("error", reject);
205
143
  proc.on("close", (code) => {
206
144
  if (code === 0) {
207
- resolveOutput(stdout.trim());
145
+ resolve2(stdout);
208
146
  return;
209
147
  }
210
148
  const details = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
@@ -270,7 +208,7 @@ async function fetchSkillsTree() {
270
208
  if (skillsTreeCache && Date.now() - skillsTreeCache.fetchedAt < TREE_CACHE_TTL_MS) {
271
209
  return skillsTreeCache.entries;
272
210
  }
273
- const resp = await ghFetch("https://api.github.com/repos/openclaw/skills/git/trees/main?recursive=1");
211
+ const resp = await ghFetch(`https://api.github.com/repos/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/git/trees/main?recursive=1`);
274
212
  if (!resp.ok) throw new Error(`GitHub tree API returned ${resp.status}`);
275
213
  const data = await resp.json();
276
214
  const metaPattern = /^skills\/([^/]+)\/([^/]+)\/_meta\.json$/;
@@ -286,7 +224,7 @@ async function fetchSkillsTree() {
286
224
  entries.push({
287
225
  name: skillName,
288
226
  owner,
289
- url: `https://github.com/openclaw/skills/tree/main/skills/${owner}/${skillName}`
227
+ url: `https://github.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/tree/main/skills/${owner}/${skillName}`
290
228
  });
291
229
  }
292
230
  skillsTreeCache = { entries, fetchedAt: Date.now() };
@@ -298,7 +236,7 @@ async function fetchMetaBatch(entries) {
298
236
  const batch = toFetch.slice(0, 50);
299
237
  const results = await Promise.allSettled(
300
238
  batch.map(async (e) => {
301
- const rawUrl = `https://raw.githubusercontent.com/openclaw/skills/main/skills/${e.owner}/${e.name}/_meta.json`;
239
+ const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${e.owner}/${e.name}/_meta.json`;
302
240
  const resp = await fetch(rawUrl);
303
241
  if (!resp.ok) return;
304
242
  const meta = await resp.json();
@@ -324,6 +262,23 @@ function buildHubEntry(e) {
324
262
  installed: false
325
263
  };
326
264
  }
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
+ };
327
282
  async function scanInstalledSkillsFromDisk() {
328
283
  const map = /* @__PURE__ */ new Map();
329
284
  const skillsDir = getSkillsInstallDir();
@@ -342,6 +297,448 @@ async function scanInstalledSkillsFromDisk() {
342
297
  }
343
298
  return map;
344
299
  }
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
+ }
345
742
  async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
346
743
  const q = query.toLowerCase().trim();
347
744
  let filtered = q ? allEntries.filter((s) => {
@@ -933,82 +1330,9 @@ function getSharedBridgeState() {
933
1330
  globalScope[SHARED_BRIDGE_KEY] = created;
934
1331
  return created;
935
1332
  }
936
- async function loadAllThreadsForSearch(appServer) {
937
- const threads = [];
938
- let cursor = null;
939
- do {
940
- const response = asRecord(await appServer.rpc("thread/list", {
941
- archived: false,
942
- limit: 100,
943
- sortKey: "updated_at",
944
- cursor
945
- }));
946
- const data = Array.isArray(response?.data) ? response.data : [];
947
- for (const row of data) {
948
- const record = asRecord(row);
949
- const id = typeof record?.id === "string" ? record.id : "";
950
- if (!id) continue;
951
- 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";
952
- const preview = typeof record?.preview === "string" ? record.preview : "";
953
- threads.push({ id, title, preview });
954
- }
955
- cursor = typeof response?.nextCursor === "string" && response.nextCursor.length > 0 ? response.nextCursor : null;
956
- } while (cursor);
957
- const docs = [];
958
- const concurrency = 4;
959
- for (let offset = 0; offset < threads.length; offset += concurrency) {
960
- const batch = threads.slice(offset, offset + concurrency);
961
- const loaded = await Promise.all(batch.map(async (thread) => {
962
- try {
963
- const readResponse = await appServer.rpc("thread/read", {
964
- threadId: thread.id,
965
- includeTurns: true
966
- });
967
- const messageText = extractThreadMessageText(readResponse);
968
- const searchableText = [thread.title, thread.preview, messageText].filter(Boolean).join("\n");
969
- return {
970
- id: thread.id,
971
- title: thread.title,
972
- preview: thread.preview,
973
- messageText,
974
- searchableText
975
- };
976
- } catch {
977
- const searchableText = [thread.title, thread.preview].filter(Boolean).join("\n");
978
- return {
979
- id: thread.id,
980
- title: thread.title,
981
- preview: thread.preview,
982
- messageText: "",
983
- searchableText
984
- };
985
- }
986
- }));
987
- docs.push(...loaded);
988
- }
989
- return docs;
990
- }
991
- async function buildThreadSearchIndex(appServer) {
992
- const docs = await loadAllThreadsForSearch(appServer);
993
- const docsById = new Map(docs.map((doc) => [doc.id, doc]));
994
- return { docsById };
995
- }
996
1333
  function createCodexBridgeMiddleware() {
997
1334
  const { appServer, methodCatalog } = getSharedBridgeState();
998
- let threadSearchIndex = null;
999
- let threadSearchIndexPromise = null;
1000
- async function getThreadSearchIndex() {
1001
- if (threadSearchIndex) return threadSearchIndex;
1002
- if (!threadSearchIndexPromise) {
1003
- threadSearchIndexPromise = buildThreadSearchIndex(appServer).then((index) => {
1004
- threadSearchIndex = index;
1005
- return index;
1006
- }).finally(() => {
1007
- threadSearchIndexPromise = null;
1008
- });
1009
- }
1010
- return threadSearchIndexPromise;
1011
- }
1335
+ void initializeSkillsSyncOnStartup(appServer);
1012
1336
  const middleware = async (req, res, next) => {
1013
1337
  try {
1014
1338
  if (!req.url) {
@@ -1074,76 +1398,6 @@ function createCodexBridgeMiddleware() {
1074
1398
  setJson(res, 200, { data: { path: homedir() } });
1075
1399
  return;
1076
1400
  }
1077
- if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
1078
- const payload = asRecord(await readJsonBody(req));
1079
- const rawSourceCwd = typeof payload?.sourceCwd === "string" ? payload.sourceCwd.trim() : "";
1080
- if (!rawSourceCwd) {
1081
- setJson(res, 400, { error: "Missing sourceCwd" });
1082
- return;
1083
- }
1084
- const sourceCwd = isAbsolute(rawSourceCwd) ? rawSourceCwd : resolve(rawSourceCwd);
1085
- try {
1086
- const sourceInfo = await stat(sourceCwd);
1087
- if (!sourceInfo.isDirectory()) {
1088
- setJson(res, 400, { error: "sourceCwd is not a directory" });
1089
- return;
1090
- }
1091
- } catch {
1092
- setJson(res, 404, { error: "sourceCwd does not exist" });
1093
- return;
1094
- }
1095
- try {
1096
- let gitRoot = "";
1097
- try {
1098
- gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
1099
- } catch (error) {
1100
- if (!isNotGitRepositoryError(error)) throw error;
1101
- await runCommand("git", ["init"], { cwd: sourceCwd });
1102
- gitRoot = await runCommandCapture("git", ["rev-parse", "--show-toplevel"], { cwd: sourceCwd });
1103
- }
1104
- const repoName = basename(gitRoot) || "repo";
1105
- const worktreesRoot = join(getCodexHomeDir(), "worktrees");
1106
- await mkdir(worktreesRoot, { recursive: true });
1107
- let worktreeId = "";
1108
- let worktreeParent = "";
1109
- let worktreeCwd = "";
1110
- for (let attempt = 0; attempt < 12; attempt += 1) {
1111
- const candidate = randomBytes(2).toString("hex");
1112
- const parent = join(worktreesRoot, candidate);
1113
- try {
1114
- await stat(parent);
1115
- continue;
1116
- } catch {
1117
- worktreeId = candidate;
1118
- worktreeParent = parent;
1119
- worktreeCwd = join(parent, repoName);
1120
- break;
1121
- }
1122
- }
1123
- if (!worktreeId || !worktreeParent || !worktreeCwd) {
1124
- throw new Error("Failed to allocate a unique worktree id");
1125
- }
1126
- const branch = `codex/${worktreeId}`;
1127
- await mkdir(worktreeParent, { recursive: true });
1128
- try {
1129
- await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1130
- } catch (error) {
1131
- if (!isMissingHeadError(error)) throw error;
1132
- await ensureRepoHasInitialCommit(gitRoot);
1133
- await runCommand("git", ["worktree", "add", "-b", branch, worktreeCwd, "HEAD"], { cwd: gitRoot });
1134
- }
1135
- setJson(res, 200, {
1136
- data: {
1137
- cwd: worktreeCwd,
1138
- branch,
1139
- gitRoot
1140
- }
1141
- });
1142
- } catch (error) {
1143
- setJson(res, 500, { error: getErrorMessage(error, "Failed to create worktree") });
1144
- }
1145
- return;
1146
- }
1147
1401
  if (req.method === "PUT" && url.pathname === "/codex-api/workspace-roots-state") {
1148
1402
  const payload = await readJsonBody(req);
1149
1403
  const record = asRecord(payload);
@@ -1269,20 +1523,6 @@ function createCodexBridgeMiddleware() {
1269
1523
  setJson(res, 200, { data: cache });
1270
1524
  return;
1271
1525
  }
1272
- if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
1273
- const payload = asRecord(await readJsonBody(req));
1274
- const query = typeof payload?.query === "string" ? payload.query.trim() : "";
1275
- const limitRaw = typeof payload?.limit === "number" ? payload.limit : 200;
1276
- const limit = Math.max(1, Math.min(1e3, Math.floor(limitRaw)));
1277
- if (!query) {
1278
- setJson(res, 200, { data: { threadIds: [], indexedThreadCount: 0 } });
1279
- return;
1280
- }
1281
- const index = await getThreadSearchIndex();
1282
- const matchedIds = Array.from(index.docsById.entries()).filter(([, doc]) => isExactPhraseMatch(query, doc)).slice(0, limit).map(([id]) => id);
1283
- setJson(res, 200, { data: { threadIds: matchedIds, indexedThreadCount: index.docsById.size } });
1284
- return;
1285
- }
1286
1526
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-titles") {
1287
1527
  const payload = asRecord(await readJsonBody(req));
1288
1528
  const id = typeof payload?.id === "string" ? payload.id : "";
@@ -1339,6 +1579,182 @@ function createCodexBridgeMiddleware() {
1339
1579
  }
1340
1580
  return;
1341
1581
  }
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
+ }
1342
1758
  if (req.method === "GET" && url.pathname === "/codex-api/skills-hub/readme") {
1343
1759
  try {
1344
1760
  const owner = url.searchParams.get("owner") || "";
@@ -1347,7 +1763,7 @@ function createCodexBridgeMiddleware() {
1347
1763
  setJson(res, 400, { error: "Missing owner or name" });
1348
1764
  return;
1349
1765
  }
1350
- const rawUrl = `https://raw.githubusercontent.com/openclaw/skills/main/skills/${owner}/${name}/SKILL.md`;
1766
+ const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
1351
1767
  const resp = await fetch(rawUrl);
1352
1768
  if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
1353
1769
  const content = await resp.text();
@@ -1372,7 +1788,7 @@ function createCodexBridgeMiddleware() {
1372
1788
  await runCommand("python3", [
1373
1789
  installerScript,
1374
1790
  "--repo",
1375
- "openclaw/skills",
1791
+ `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
1376
1792
  "--path",
1377
1793
  skillPathInRepo,
1378
1794
  "--dest",
@@ -1382,6 +1798,10 @@ function createCodexBridgeMiddleware() {
1382
1798
  ]);
1383
1799
  const skillDir = join(installDest, name);
1384
1800
  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);
1385
1805
  setJson(res, 200, { ok: true, path: skillDir });
1386
1806
  } catch (error) {
1387
1807
  setJson(res, 502, { error: getErrorMessage(error, "Failed to install skill") });
@@ -1399,6 +1819,13 @@ function createCodexBridgeMiddleware() {
1399
1819
  return;
1400
1820
  }
1401
1821
  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);
1402
1829
  try {
1403
1830
  await appServer.rpc("skills/list", { forceReload: true });
1404
1831
  } catch {
@@ -1446,7 +1873,6 @@ data: ${JSON.stringify({ ok: true })}
1446
1873
  }
1447
1874
  };
1448
1875
  middleware.dispose = () => {
1449
- threadSearchIndex = null;
1450
1876
  appServer.dispose();
1451
1877
  };
1452
1878
  middleware.subscribeNotifications = (listener) => {
@@ -1461,7 +1887,7 @@ data: ${JSON.stringify({ ok: true })}
1461
1887
  }
1462
1888
 
1463
1889
  // src/server/authMiddleware.ts
1464
- import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1890
+ import { randomBytes, timingSafeEqual } from "crypto";
1465
1891
  var TOKEN_COOKIE = "codex_web_local_token";
1466
1892
  function constantTimeCompare(a, b) {
1467
1893
  const bufA = Buffer.from(a);
@@ -1559,7 +1985,7 @@ function createAuthSession(password) {
1559
1985
  res.status(401).json({ error: "Invalid password" });
1560
1986
  return;
1561
1987
  }
1562
- const token = randomBytes2(32).toString("hex");
1988
+ const token = randomBytes(32).toString("hex");
1563
1989
  validTokens.add(token);
1564
1990
  res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
1565
1991
  res.json({ ok: true });
@@ -1605,69 +2031,6 @@ function normalizeLocalImagePath(rawPath) {
1605
2031
  }
1606
2032
  return trimmed;
1607
2033
  }
1608
- function normalizeLocalPath(rawPath) {
1609
- const trimmed = rawPath.trim();
1610
- if (!trimmed) return "";
1611
- if (trimmed.startsWith("file://")) {
1612
- try {
1613
- return decodeURIComponent(trimmed.replace(/^file:\/\//u, ""));
1614
- } catch {
1615
- return trimmed.replace(/^file:\/\//u, "");
1616
- }
1617
- }
1618
- return trimmed;
1619
- }
1620
- function decodeBrowsePath(rawPath) {
1621
- if (!rawPath) return "";
1622
- try {
1623
- return decodeURIComponent(rawPath);
1624
- } catch {
1625
- return rawPath;
1626
- }
1627
- }
1628
- function escapeHtml(value) {
1629
- return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
1630
- }
1631
- function toBrowseHref(pathValue) {
1632
- return `/codex-local-browse${encodeURI(pathValue)}`;
1633
- }
1634
- async function renderDirectoryListing(res, localPath) {
1635
- const entries = await readdir2(localPath, { withFileTypes: true });
1636
- const sorted = entries.slice().sort((a, b) => {
1637
- if (a.isDirectory() && !b.isDirectory()) return -1;
1638
- if (!a.isDirectory() && b.isDirectory()) return 1;
1639
- return a.name.localeCompare(b.name);
1640
- });
1641
- const parentPath = dirname(localPath);
1642
- const rows = sorted.map((entry) => {
1643
- const entryPath = join2(localPath, entry.name);
1644
- const suffix = entry.isDirectory() ? "/" : "";
1645
- return `<li><a href="${escapeHtml(toBrowseHref(entryPath))}">${escapeHtml(entry.name)}${suffix}</a></li>`;
1646
- }).join("\n");
1647
- const parentLink = localPath !== parentPath ? `<p><a href="${escapeHtml(toBrowseHref(parentPath))}">..</a></p>` : "";
1648
- const html = `<!doctype html>
1649
- <html lang="en">
1650
- <head>
1651
- <meta charset="utf-8" />
1652
- <meta name="viewport" content="width=device-width, initial-scale=1" />
1653
- <title>Index of ${escapeHtml(localPath)}</title>
1654
- <style>
1655
- body { font-family: ui-monospace, Menlo, Monaco, monospace; margin: 24px; background: #0b1020; color: #dbe6ff; }
1656
- a { color: #8cc2ff; text-decoration: none; }
1657
- a:hover { text-decoration: underline; }
1658
- ul { list-style: none; padding: 0; margin: 12px 0 0; }
1659
- li { padding: 3px 0; }
1660
- h1 { font-size: 18px; margin: 0; word-break: break-all; }
1661
- </style>
1662
- </head>
1663
- <body>
1664
- <h1>Index of ${escapeHtml(localPath)}</h1>
1665
- ${parentLink}
1666
- <ul>${rows}</ul>
1667
- </body>
1668
- </html>`;
1669
- res.status(200).type("text/html; charset=utf-8").send(html);
1670
- }
1671
2034
  function createServer(options = {}) {
1672
2035
  const app = express();
1673
2036
  const bridge = createCodexBridgeMiddleware();
@@ -1695,43 +2058,7 @@ function createServer(options = {}) {
1695
2058
  if (!res.headersSent) res.status(404).json({ error: "Image file not found." });
1696
2059
  });
1697
2060
  });
1698
- app.get("/codex-local-file", (req, res) => {
1699
- const rawPath = typeof req.query.path === "string" ? req.query.path : "";
1700
- const localPath = normalizeLocalPath(rawPath);
1701
- if (!localPath || !isAbsolute2(localPath)) {
1702
- res.status(400).json({ error: "Expected absolute local file path." });
1703
- return;
1704
- }
1705
- res.setHeader("Cache-Control", "private, no-store");
1706
- res.setHeader("Content-Disposition", "inline");
1707
- res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
1708
- if (!error) return;
1709
- if (!res.headersSent) res.status(404).json({ error: "File not found." });
1710
- });
1711
- });
1712
- app.get("/codex-local-browse/*path", async (req, res) => {
1713
- const rawPath = typeof req.params.path === "string" ? req.params.path : "";
1714
- const localPath = decodeBrowsePath(`/${rawPath}`);
1715
- if (!localPath || !isAbsolute2(localPath)) {
1716
- res.status(400).json({ error: "Expected absolute local file path." });
1717
- return;
1718
- }
1719
- try {
1720
- const fileStat = await stat2(localPath);
1721
- res.setHeader("Cache-Control", "private, no-store");
1722
- if (fileStat.isDirectory()) {
1723
- await renderDirectoryListing(res, localPath);
1724
- return;
1725
- }
1726
- res.sendFile(localPath, { dotfiles: "allow" }, (error) => {
1727
- if (!error) return;
1728
- if (!res.headersSent) res.status(404).json({ error: "File not found." });
1729
- });
1730
- } catch {
1731
- res.status(404).json({ error: "File not found." });
1732
- }
1733
- });
1734
- const hasFrontendAssets = existsSync(spaEntryFile);
2061
+ const hasFrontendAssets = existsSync2(spaEntryFile);
1735
2062
  if (hasFrontendAssets) {
1736
2063
  app.use(express.static(distDir));
1737
2064
  }
@@ -1838,7 +2165,7 @@ function resolveCodexCommand() {
1838
2165
  return "codex";
1839
2166
  }
1840
2167
  const userCandidate = join3(getUserNpmPrefix(), "bin", "codex");
1841
- if (existsSync2(userCandidate) && canRun(userCandidate, ["--version"])) {
2168
+ if (existsSync3(userCandidate) && canRun(userCandidate, ["--version"])) {
1842
2169
  return userCandidate;
1843
2170
  }
1844
2171
  const prefix = process.env.PREFIX?.trim();
@@ -1846,87 +2173,14 @@ function resolveCodexCommand() {
1846
2173
  return null;
1847
2174
  }
1848
2175
  const candidate = join3(prefix, "bin", "codex");
1849
- if (existsSync2(candidate) && canRun(candidate, ["--version"])) {
2176
+ if (existsSync3(candidate) && canRun(candidate, ["--version"])) {
1850
2177
  return candidate;
1851
2178
  }
1852
2179
  return null;
1853
2180
  }
1854
- function resolveCloudflaredCommand() {
1855
- if (canRun("cloudflared", ["--version"])) {
1856
- return "cloudflared";
1857
- }
1858
- const localCandidate = join3(homedir2(), ".local", "bin", "cloudflared");
1859
- if (existsSync2(localCandidate) && canRun(localCandidate, ["--version"])) {
1860
- return localCandidate;
1861
- }
1862
- return null;
1863
- }
1864
- function mapCloudflaredLinuxArch(arch) {
1865
- if (arch === "x64") {
1866
- return "amd64";
1867
- }
1868
- if (arch === "arm64") {
1869
- return "arm64";
1870
- }
1871
- return null;
1872
- }
1873
- function downloadFile(url, destination) {
1874
- return new Promise((resolve2, reject) => {
1875
- const request = (currentUrl) => {
1876
- httpsGet(currentUrl, (response) => {
1877
- const code = response.statusCode ?? 0;
1878
- if (code >= 300 && code < 400 && response.headers.location) {
1879
- response.resume();
1880
- request(response.headers.location);
1881
- return;
1882
- }
1883
- if (code !== 200) {
1884
- response.resume();
1885
- reject(new Error(`Download failed with HTTP status ${String(code)}`));
1886
- return;
1887
- }
1888
- const file = createWriteStream(destination, { mode: 493 });
1889
- response.pipe(file);
1890
- file.on("finish", () => {
1891
- file.close();
1892
- resolve2();
1893
- });
1894
- file.on("error", reject);
1895
- }).on("error", reject);
1896
- };
1897
- request(url);
1898
- });
1899
- }
1900
- async function ensureCloudflaredInstalledLinux() {
1901
- const current = resolveCloudflaredCommand();
1902
- if (current) {
1903
- return current;
1904
- }
1905
- if (process.platform !== "linux") {
1906
- return null;
1907
- }
1908
- const mappedArch = mapCloudflaredLinuxArch(process.arch);
1909
- if (!mappedArch) {
1910
- throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
1911
- }
1912
- const userBinDir = join3(homedir2(), ".local", "bin");
1913
- mkdirSync(userBinDir, { recursive: true });
1914
- const destination = join3(userBinDir, "cloudflared");
1915
- const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
1916
- console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
1917
- await downloadFile(downloadUrl, destination);
1918
- chmodSync(destination, 493);
1919
- process.env.PATH = `${userBinDir}:${process.env.PATH ?? ""}`;
1920
- const installed = resolveCloudflaredCommand();
1921
- if (!installed) {
1922
- throw new Error("cloudflared download completed but executable is still not available");
1923
- }
1924
- console.log("\ncloudflared installed.\n");
1925
- return installed;
1926
- }
1927
2181
  function hasCodexAuth() {
1928
2182
  const codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex");
1929
- return existsSync2(join3(codexHome, "auth.json"));
2183
+ return existsSync3(join3(codexHome, "auth.json"));
1930
2184
  }
1931
2185
  function ensureCodexInstalled() {
1932
2186
  let codexCommand = resolveCodexCommand();
@@ -2005,27 +2259,9 @@ function parseCloudflaredUrl(chunk) {
2005
2259
  }
2006
2260
  return urlMatch[urlMatch.length - 1] ?? null;
2007
2261
  }
2008
- function getAccessibleUrls(port) {
2009
- const urls = /* @__PURE__ */ new Set([`http://localhost:${String(port)}`]);
2010
- const interfaces = networkInterfaces();
2011
- for (const entries of Object.values(interfaces)) {
2012
- if (!entries) {
2013
- continue;
2014
- }
2015
- for (const entry of entries) {
2016
- if (entry.internal) {
2017
- continue;
2018
- }
2019
- if (entry.family === "IPv4") {
2020
- urls.add(`http://${entry.address}:${String(port)}`);
2021
- }
2022
- }
2023
- }
2024
- return Array.from(urls);
2025
- }
2026
- async function startCloudflaredTunnel(command, localPort) {
2262
+ async function startCloudflaredTunnel(localPort) {
2027
2263
  return new Promise((resolve2, reject) => {
2028
- const child = spawn2(command, ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
2264
+ const child = spawn2("cloudflared", ["tunnel", "--url", `http://localhost:${String(localPort)}`], {
2029
2265
  stdio: ["ignore", "pipe", "pipe"]
2030
2266
  });
2031
2267
  const timeout = setTimeout(() => {
@@ -2076,7 +2312,7 @@ function listenWithFallback(server, startPort) {
2076
2312
  };
2077
2313
  server.once("error", onError);
2078
2314
  server.once("listening", onListening);
2079
- server.listen(port, "0.0.0.0");
2315
+ server.listen(port);
2080
2316
  };
2081
2317
  attempt(startPort);
2082
2318
  });
@@ -2098,8 +2334,7 @@ async function startServer(options) {
2098
2334
  let tunnelUrl = null;
2099
2335
  if (options.tunnel) {
2100
2336
  try {
2101
- const cloudflaredCommand = await ensureCloudflaredInstalledLinux() ?? "cloudflared";
2102
- const tunnel = await startCloudflaredTunnel(cloudflaredCommand, port);
2337
+ const tunnel = await startCloudflaredTunnel(port);
2103
2338
  tunnelChild = tunnel.process;
2104
2339
  tunnelUrl = tunnel.url;
2105
2340
  } catch (error) {
@@ -2114,15 +2349,8 @@ async function startServer(options) {
2114
2349
  ` Version: ${version}`,
2115
2350
  " GitHub: https://github.com/friuns2/codexui",
2116
2351
  "",
2117
- ` Bind: http://0.0.0.0:${String(port)}`
2352
+ ` Local: http://localhost:${String(port)}`
2118
2353
  ];
2119
- const accessUrls = getAccessibleUrls(port);
2120
- if (accessUrls.length > 0) {
2121
- lines.push(` Local: ${accessUrls[0]}`);
2122
- for (const accessUrl of accessUrls.slice(1)) {
2123
- lines.push(` Network: ${accessUrl}`);
2124
- }
2125
- }
2126
2354
  if (port !== requestedPort) {
2127
2355
  lines.push(` Requested port ${String(requestedPort)} was unavailable; using ${String(port)}.`);
2128
2356
  }
@@ -2131,7 +2359,9 @@ async function startServer(options) {
2131
2359
  }
2132
2360
  if (tunnelUrl) {
2133
2361
  lines.push(` Tunnel: ${tunnelUrl}`);
2134
- lines.push(" Tunnel QR code below");
2362
+ lines.push("");
2363
+ lines.push(" Tunnel QR code:");
2364
+ lines.push(` URL: ${tunnelUrl}`);
2135
2365
  }
2136
2366
  printTermuxKeepAlive(lines);
2137
2367
  lines.push("");