codexui-android 0.1.97 → 0.1.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist-cli/index.js CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { createServer as createServer2 } from "http";
5
- import { chmodSync as chmodSync2, createWriteStream, existsSync as existsSync5, mkdirSync } from "fs";
5
+ import { chmodSync as chmodSync2, createWriteStream, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
6
6
  import { readFile as readFile5, stat as stat7, writeFile as writeFile6 } from "fs/promises";
7
- import { homedir as homedir6, networkInterfaces } from "os";
8
- import { isAbsolute as isAbsolute4, join as join9, resolve as resolve3 } from "path";
7
+ import { homedir as homedir7, networkInterfaces } from "os";
8
+ import { isAbsolute as isAbsolute4, join as join10, resolve as resolve3 } from "path";
9
9
  import { spawn as spawn5 } from "child_process";
10
10
  import { createInterface as createInterface2 } from "readline/promises";
11
11
  import { fileURLToPath as fileURLToPath2 } from "url";
12
- import { dirname as dirname5 } from "path";
12
+ import { dirname as dirname6 } from "path";
13
13
  import { get as httpsGet } from "https";
14
14
  import { Command } from "commander";
15
15
  import qrcode from "qrcode-terminal";
@@ -124,37 +124,6 @@ function resolveRipgrepCommand() {
124
124
  }
125
125
  return null;
126
126
  }
127
- function resolvePythonCommand() {
128
- const candidates = process.platform === "win32" ? [
129
- { command: "python", args: [] },
130
- { command: "py", args: ["-3"] },
131
- { command: "python3", args: [] }
132
- ] : [
133
- { command: "python3", args: [] },
134
- { command: "python", args: [] }
135
- ];
136
- for (const candidate of candidates) {
137
- if (isRunnableCommand(candidate.command, [...candidate.args, "--version"])) {
138
- return candidate;
139
- }
140
- }
141
- return null;
142
- }
143
- function resolveSkillInstallerScriptPath(codexHome) {
144
- const normalizedCodexHome = codexHome?.trim();
145
- const candidates = uniqueStrings([
146
- normalizedCodexHome ? join(normalizedCodexHome, "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py") : null,
147
- process.env.CODEX_HOME?.trim() ? join(process.env.CODEX_HOME.trim(), "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py") : null,
148
- join(homedir(), ".codex", "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py"),
149
- join(homedir(), ".cursor", "skills", ".system", "skill-installer", "scripts", "install-skill-from-github.py")
150
- ]);
151
- for (const candidate of candidates) {
152
- if (existsSync(candidate)) {
153
- return candidate;
154
- }
155
- }
156
- return null;
157
- }
158
127
 
159
128
  // src/server/appServerRuntimeConfig.ts
160
129
  var SANDBOX_MODES = /* @__PURE__ */ new Set([
@@ -216,16 +185,16 @@ function parseApprovalPolicy(value) {
216
185
 
217
186
  // src/server/httpServer.ts
218
187
  import { fileURLToPath } from "url";
219
- import { dirname as dirname4, extname as extname3, isAbsolute as isAbsolute3, join as join8 } from "path";
220
- import { existsSync as existsSync4 } from "fs";
188
+ import { dirname as dirname5, extname as extname3, isAbsolute as isAbsolute3, join as join9 } from "path";
189
+ import { existsSync as existsSync6 } from "fs";
221
190
  import { writeFile as writeFile5, stat as stat6 } from "fs/promises";
222
191
  import express from "express";
223
192
 
224
193
  // src/server/codexAppServerBridge.ts
225
- import { spawn as spawn4 } from "child_process";
194
+ import { spawn as spawn4, spawnSync as spawnSync4 } from "child_process";
226
195
  import { createHash as createHash2, randomBytes } from "crypto";
227
- import { mkdtemp as mkdtemp3, readFile as readFile3, readdir as readdir2, rm as rm4, mkdir as mkdir4, stat as stat4 } from "fs/promises";
228
- import { createReadStream, readFileSync as readFileSync2 } from "fs";
196
+ import { mkdtemp as mkdtemp3, readFile as readFile3, readdir as readdir2, rm as rm4, mkdir as mkdir4, stat as stat4, lstat as lstat2 } from "fs/promises";
197
+ import { createReadStream, existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
229
198
  import { request as httpRequest2 } from "http";
230
199
  import { request as httpsRequest2 } from "https";
231
200
  import { homedir as homedir5 } from "os";
@@ -1778,6 +1747,50 @@ function getCodexHomeDir2() {
1778
1747
  const codexHome = process.env.CODEX_HOME?.trim();
1779
1748
  return codexHome && codexHome.length > 0 ? codexHome : join4(homedir3(), ".codex");
1780
1749
  }
1750
+ function splitAbsolutePath(pathValue) {
1751
+ return pathValue.split("/").filter(Boolean);
1752
+ }
1753
+ function buildAbsolutePath(parts) {
1754
+ return `/${parts.join("/")}`;
1755
+ }
1756
+ function normalizeSkillMarkdownPath(skillPath) {
1757
+ if (!skillPath) return "";
1758
+ return skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md`;
1759
+ }
1760
+ function deriveSkillPathInfo(skillPath, knownPaths = /* @__PURE__ */ new Set()) {
1761
+ const normalizedPath = normalizeSkillMarkdownPath(skillPath);
1762
+ const parts = splitAbsolutePath(normalizedPath);
1763
+ if (parts.length < 2) return null;
1764
+ const pluginSkillsIndex = parts.lastIndexOf("skills");
1765
+ if (pluginSkillsIndex >= 2) {
1766
+ const pluginName = parts[pluginSkillsIndex - 2] ?? "";
1767
+ if (pluginName) {
1768
+ const rootSkillPath = buildAbsolutePath([...parts.slice(0, pluginSkillsIndex + 1), pluginName, "SKILL.md"]);
1769
+ if (knownPaths.has(rootSkillPath)) {
1770
+ return {
1771
+ normalizedPath,
1772
+ rootSkillPath,
1773
+ rootSkillName: pluginName,
1774
+ installDir: buildAbsolutePath(parts.slice(0, pluginSkillsIndex + 1)),
1775
+ isNestedSkill: normalizedPath !== rootSkillPath
1776
+ };
1777
+ }
1778
+ }
1779
+ }
1780
+ const firstSkillsIndex = parts.indexOf("skills");
1781
+ if (firstSkillsIndex < 0 || firstSkillsIndex + 1 >= parts.length - 1) return null;
1782
+ const rootSkillName = parts[firstSkillsIndex + 1] ?? "";
1783
+ if (!rootSkillName) return null;
1784
+ const rootParts = parts.slice(0, firstSkillsIndex + 2);
1785
+ const installDirParts = parts.slice(0, firstSkillsIndex + 1);
1786
+ return {
1787
+ normalizedPath,
1788
+ rootSkillPath: buildAbsolutePath([...rootParts, "SKILL.md"]),
1789
+ rootSkillName,
1790
+ installDir: buildAbsolutePath(installDirParts),
1791
+ isNestedSkill: normalizedPath !== buildAbsolutePath([...rootParts, "SKILL.md"])
1792
+ };
1793
+ }
1781
1794
  function getSkillsInstallDir() {
1782
1795
  return join4(getCodexHomeDir2(), "skills");
1783
1796
  }
@@ -1889,109 +1902,94 @@ async function detectUserSkillsDir(appServer) {
1889
1902
  for (const entry of result.data ?? []) {
1890
1903
  for (const skill of entry.skills ?? []) {
1891
1904
  if (skill.scope !== "user" || !skill.path) continue;
1892
- const parts = skill.path.split("/").filter(Boolean);
1893
- if (parts.length < 2) continue;
1894
- return `/${parts.slice(0, -2).join("/")}`;
1905
+ const skillInfo = deriveSkillPathInfo(skill.path);
1906
+ if (!skillInfo) continue;
1907
+ return skillInfo.installDir;
1895
1908
  }
1896
1909
  }
1897
1910
  } catch {
1898
1911
  }
1899
1912
  return getSkillsInstallDir();
1900
1913
  }
1901
- async function ensureInstalledSkillIsValid(appServer, skillPath) {
1902
- const result = await appServer.rpc("skills/list", { forceReload: true });
1903
- const normalized = skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md`;
1904
- for (const entry of result.data ?? []) {
1905
- for (const error of entry.errors ?? []) {
1906
- if (error.path === normalized) {
1907
- throw new Error(error.message || "Installed skill is invalid");
1908
- }
1909
- }
1910
- }
1911
- }
1912
- var TREE_CACHE_TTL_MS = 5 * 60 * 1e3;
1913
- var skillsTreeCache = null;
1914
- var metaCache = /* @__PURE__ */ new Map();
1915
- async function getGhToken() {
1914
+ async function runGitFetchWithRefLockRetry(repoDir, args = ["fetch", "origin"]) {
1916
1915
  try {
1917
- const proc = spawn3("gh", ["auth", "token"], { stdio: ["ignore", "pipe", "ignore"] });
1918
- let out = "";
1919
- proc.stdout.on("data", (d) => {
1920
- out += d.toString();
1921
- });
1922
- return new Promise((resolve4) => {
1923
- proc.on("close", (code) => resolve4(code === 0 ? out.trim() : null));
1924
- proc.on("error", () => resolve4(null));
1925
- });
1926
- } catch {
1927
- return null;
1916
+ await runCommand2("git", args, { cwd: repoDir });
1917
+ } catch (error) {
1918
+ const message = getErrorMessage3(error, "");
1919
+ if (!message.includes("cannot lock ref 'refs/remotes/origin/")) throw error;
1920
+ const branchMatch = message.match(/refs\/remotes\/origin\/([^\s':]+)/);
1921
+ if (!branchMatch?.[1]) throw error;
1922
+ const refPath = join4(repoDir, ".git", "refs", "remotes", "origin", branchMatch[1]);
1923
+ try {
1924
+ await rm3(refPath, { force: true });
1925
+ } catch {
1926
+ }
1927
+ await runCommand2("git", args, { cwd: repoDir });
1928
1928
  }
1929
1929
  }
1930
- async function ghFetch(url) {
1931
- const token = await getGhToken();
1932
- const headers = {
1933
- Accept: "application/vnd.github+json",
1934
- "User-Agent": "codex-web-local"
1930
+ function buildLocalHubEntry(info) {
1931
+ return {
1932
+ name: info.name,
1933
+ owner: "local",
1934
+ description: "",
1935
+ displayName: "",
1936
+ publishedAt: 0,
1937
+ avatarUrl: "",
1938
+ url: "",
1939
+ installed: true,
1940
+ path: info.path,
1941
+ enabled: info.enabled
1935
1942
  };
1936
- if (token) headers.Authorization = `Bearer ${token}`;
1937
- return fetch(url, { headers });
1938
1943
  }
1939
- async function fetchSkillsTree() {
1940
- if (skillsTreeCache && Date.now() - skillsTreeCache.fetchedAt < TREE_CACHE_TTL_MS) {
1941
- return skillsTreeCache.entries;
1942
- }
1943
- const resp = await ghFetch(`https://api.github.com/repos/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/git/trees/main?recursive=1`);
1944
- if (!resp.ok) throw new Error(`GitHub tree API returned ${resp.status}`);
1945
- const data = await resp.json();
1946
- const metaPattern = /^skills\/([^/]+)\/([^/]+)\/_meta\.json$/;
1947
- const seen = /* @__PURE__ */ new Set();
1948
- const entries = [];
1949
- for (const node of data.tree ?? []) {
1950
- const match = metaPattern.exec(node.path);
1951
- if (!match) continue;
1952
- const [, owner, skillName] = match;
1953
- const key = `${owner}/${skillName}`;
1954
- if (seen.has(key)) continue;
1955
- seen.add(key);
1956
- entries.push({
1957
- name: skillName,
1958
- owner,
1959
- url: `https://github.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/tree/main/skills/${owner}/${skillName}`
1960
- });
1961
- }
1962
- skillsTreeCache = { entries, fetchedAt: Date.now() };
1963
- return entries;
1964
- }
1965
- async function fetchMetaBatch(entries) {
1966
- const toFetch = entries.filter((e) => !metaCache.has(`${e.owner}/${e.name}`));
1967
- if (toFetch.length === 0) return;
1968
- const batch = toFetch.slice(0, 50);
1969
- await Promise.allSettled(
1970
- batch.map(async (e) => {
1971
- const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${e.owner}/${e.name}/_meta.json`;
1972
- const resp = await fetch(rawUrl);
1973
- if (!resp.ok) return;
1974
- const meta = await resp.json();
1975
- metaCache.set(`${e.owner}/${e.name}`, {
1976
- displayName: typeof meta.displayName === "string" ? meta.displayName : "",
1977
- description: typeof meta.displayName === "string" ? meta.displayName : "",
1978
- publishedAt: meta.latest?.publishedAt ?? 0
1979
- });
1980
- })
1944
+ function groupRpcSkillRecords(skills) {
1945
+ const normalizedPathSet = new Set(
1946
+ skills.map((skill) => normalizeSkillMarkdownPath(typeof skill.path === "string" ? skill.path : "")).filter(Boolean)
1981
1947
  );
1982
- }
1983
- function buildHubEntry(e) {
1984
- const cached = metaCache.get(`${e.owner}/${e.name}`);
1985
- return {
1986
- name: e.name,
1987
- owner: e.owner,
1988
- description: cached?.description ?? "",
1989
- displayName: cached?.displayName ?? "",
1990
- publishedAt: cached?.publishedAt ?? 0,
1991
- avatarUrl: `https://github.com/${e.owner}.png?size=40`,
1992
- url: e.url,
1993
- installed: false
1994
- };
1948
+ const grouped = /* @__PURE__ */ new Map();
1949
+ for (const skill of skills) {
1950
+ const rawPath = typeof skill.path === "string" ? skill.path : "";
1951
+ const pathInfo = rawPath ? deriveSkillPathInfo(rawPath, normalizedPathSet) : null;
1952
+ const groupingKey = pathInfo && pathInfo.isNestedSkill && normalizedPathSet.has(pathInfo.rootSkillPath) ? pathInfo.rootSkillPath : pathInfo?.normalizedPath || rawPath || `${skill.scope ?? ""}:${skill.name ?? ""}`;
1953
+ const existing = grouped.get(groupingKey);
1954
+ const isRootEntry = pathInfo?.normalizedPath === groupingKey;
1955
+ const groupedName = pathInfo && groupingKey === pathInfo.rootSkillPath ? pathInfo.rootSkillName : skill.name;
1956
+ if (!existing) {
1957
+ grouped.set(groupingKey, {
1958
+ preferred: isRootEntry ? {
1959
+ ...skill,
1960
+ name: groupedName,
1961
+ path: groupingKey
1962
+ } : {
1963
+ ...skill,
1964
+ name: groupedName,
1965
+ path: groupingKey
1966
+ },
1967
+ hasRoot: isRootEntry,
1968
+ anyEnabled: skill.enabled !== false
1969
+ });
1970
+ continue;
1971
+ }
1972
+ existing.anyEnabled = existing.anyEnabled || skill.enabled !== false;
1973
+ if (!existing.hasRoot && isRootEntry) {
1974
+ existing.preferred = {
1975
+ ...skill,
1976
+ name: groupedName,
1977
+ path: groupingKey
1978
+ };
1979
+ existing.hasRoot = true;
1980
+ continue;
1981
+ }
1982
+ if (!existing.preferred.description && skill.description) {
1983
+ existing.preferred = { ...existing.preferred, description: skill.description };
1984
+ }
1985
+ if (!existing.preferred.shortDescription && skill.shortDescription) {
1986
+ existing.preferred = { ...existing.preferred, shortDescription: skill.shortDescription };
1987
+ }
1988
+ }
1989
+ return Array.from(grouped.values()).map(({ preferred, anyEnabled }) => ({
1990
+ ...preferred,
1991
+ enabled: preferred.enabled ?? anyEnabled
1992
+ }));
1995
1993
  }
1996
1994
  var GITHUB_DEVICE_CLIENT_ID = "Iv1.b507a08c87ecfe98";
1997
1995
  var DEFAULT_SKILLS_SYNC_REPO_NAME = "codexskills";
@@ -2001,8 +1999,6 @@ var SYNC_UPSTREAM_SKILLS_REPO = "skills";
2001
1999
  var PRIVATE_SYNC_BRANCH = "main";
2002
2000
  var PUBLIC_UPSTREAM_BRANCH_ANDROID = "android";
2003
2001
  var PUBLIC_UPSTREAM_BRANCH_DEFAULT = "main";
2004
- var HUB_SKILLS_OWNER = "openclaw";
2005
- var HUB_SKILLS_REPO = "skills";
2006
2002
  var startupSkillsSyncInitialized = false;
2007
2003
  var startupSyncStatus = {
2008
2004
  inProgress: false,
@@ -2280,7 +2276,7 @@ async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
2280
2276
  } catch {
2281
2277
  await runCommand2("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
2282
2278
  }
2283
- await runCommand2("git", ["fetch", "origin"], { cwd: localDir });
2279
+ await runGitFetchWithRefLockRetry(localDir);
2284
2280
  try {
2285
2281
  await runCommand2("git", ["merge", "--allow-unrelated-histories", "--no-edit", `origin/${branch}`], { cwd: localDir });
2286
2282
  } catch {
@@ -2288,7 +2284,7 @@ async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
2288
2284
  return localDir;
2289
2285
  }
2290
2286
  await runCommand2("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
2291
- await runCommand2("git", ["fetch", "origin"], { cwd: localDir });
2287
+ await runGitFetchWithRefLockRetry(localDir);
2292
2288
  const hasLocalChangesBeforeSync = await hasLocalUncommittedChanges(localDir);
2293
2289
  const localMtimesBeforeSync = hasLocalChangesBeforeSync ? await snapshotFileMtimes(localDir) : /* @__PURE__ */ new Map();
2294
2290
  await resolveMergeConflictsByNewerCommit(localDir, branch, localMtimesBeforeSync);
@@ -2308,7 +2304,7 @@ async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
2308
2304
  } catch {
2309
2305
  }
2310
2306
  let pulledMtimes = /* @__PURE__ */ new Map();
2311
- await runCommand2("git", ["fetch", "origin", branch], { cwd: localDir });
2307
+ await runGitFetchWithRefLockRetry(localDir, ["fetch", "origin", branch]);
2312
2308
  await runCommand2("git", ["reset", "--hard", `origin/${branch}`], { cwd: localDir });
2313
2309
  pulledMtimes = await snapshotFileMtimes(localDir);
2314
2310
  if (createdAutostash) {
@@ -2464,6 +2460,11 @@ async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _inst
2464
2460
  }
2465
2461
  await runCommand2("git", ["checkout", `origin/${branch2}`, "--", filePath], { cwd: repoDir2 });
2466
2462
  }
2463
+ try {
2464
+ await runCommand2("git", ["cat-file", "-e", `origin/${branch2}:shared_skills`], { cwd: repoDir2 });
2465
+ await runCommand2("git", ["checkout", `origin/${branch2}`, "--", "shared_skills"], { cwd: repoDir2 });
2466
+ } catch {
2467
+ }
2467
2468
  }
2468
2469
  function isNonFastForwardPushError(error) {
2469
2470
  const text = getErrorMessage3(error, "").toLowerCase();
@@ -2474,7 +2475,7 @@ async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _inst
2474
2475
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2475
2476
  const hasLocalChangesBeforeReconcile = await hasLocalUncommittedChanges(repoDir2);
2476
2477
  const localMtimesBeforeReconcile = hasLocalChangesBeforeReconcile ? await snapshotFileMtimes(repoDir2) : /* @__PURE__ */ new Map();
2477
- await runCommand2("git", ["fetch", "origin"], { cwd: repoDir2 });
2478
+ await runGitFetchWithRefLockRetry(repoDir2);
2478
2479
  try {
2479
2480
  await runCommand2("git", ["rebase", `origin/${branch2}`], { cwd: repoDir2 });
2480
2481
  } catch {
@@ -2490,7 +2491,7 @@ async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _inst
2490
2491
  }
2491
2492
  }
2492
2493
  try {
2493
- await runCommand2("git", ["push", "origin", `HEAD:${branch2}`], { cwd: repoDir2 });
2494
+ await runCommand2("git", ["push", "--no-recurse-submodules", "origin", `HEAD:${branch2}`], { cwd: repoDir2 });
2494
2495
  const state = await readSkillsSyncState();
2495
2496
  const pushedHead = await runCommandWithOutput("git", ["rev-parse", "HEAD"], { cwd: repoDir2 });
2496
2497
  await writeSkillsSyncState({
@@ -2542,38 +2543,16 @@ async function bootstrapSkillsFromUpstreamIntoLocal() {
2542
2543
  async function collectLocalSyncedSkills(appServer) {
2543
2544
  const state = await readSkillsSyncState();
2544
2545
  const owners = { ...state.installedOwners ?? {} };
2545
- const tree = await fetchSkillsTree();
2546
- const uniqueOwnerByName = /* @__PURE__ */ new Map();
2547
- const ambiguousNames = /* @__PURE__ */ new Set();
2548
- for (const entry of tree) {
2549
- if (ambiguousNames.has(entry.name)) continue;
2550
- const existingOwner = uniqueOwnerByName.get(entry.name);
2551
- if (!existingOwner) {
2552
- uniqueOwnerByName.set(entry.name, entry.owner);
2553
- continue;
2554
- }
2555
- if (existingOwner !== entry.owner) {
2556
- uniqueOwnerByName.delete(entry.name);
2557
- ambiguousNames.add(entry.name);
2558
- }
2559
- }
2560
2546
  const skills = await appServer.rpc("skills/list", {});
2561
2547
  const seen = /* @__PURE__ */ new Set();
2562
2548
  const synced = [];
2563
2549
  let ownersChanged = false;
2564
2550
  for (const entry of skills.data ?? []) {
2565
- for (const skill of entry.skills ?? []) {
2551
+ for (const skill of groupRpcSkillRecords(entry.skills ?? [])) {
2566
2552
  const name = typeof skill.name === "string" ? skill.name : "";
2567
- if (!name || seen.has(name)) continue;
2553
+ if (!name || skill.scope !== "user" || seen.has(name)) continue;
2568
2554
  seen.add(name);
2569
- let owner = owners[name];
2570
- if (!owner) {
2571
- owner = uniqueOwnerByName.get(name) ?? "";
2572
- if (owner) {
2573
- owners[name] = owner;
2574
- ownersChanged = true;
2575
- }
2576
- }
2555
+ const owner = owners[name] ?? "";
2577
2556
  synced.push({ ...owner ? { owner } : {}, name, enabled: skill.enabled !== false });
2578
2557
  }
2579
2558
  }
@@ -2709,44 +2688,15 @@ async function finalizeGithubLoginAndSync(token, username, appServer) {
2709
2688
  }
2710
2689
  await autoPushSyncedSkills(appServer);
2711
2690
  }
2712
- async function searchSkillsHub(allEntries, query, limit, sort, installedMap) {
2713
- const q = query.toLowerCase().trim();
2714
- const filtered = q ? allEntries.filter((s) => {
2715
- if (s.name.toLowerCase().includes(q) || s.owner.toLowerCase().includes(q)) return true;
2716
- const cached = metaCache.get(`${s.owner}/${s.name}`);
2717
- return Boolean(cached?.displayName?.toLowerCase().includes(q));
2718
- }) : allEntries;
2719
- const page = filtered.slice(0, Math.min(limit * 2, 200));
2720
- await fetchMetaBatch(page);
2721
- let results = page.map(buildHubEntry);
2722
- if (sort === "date") {
2723
- results.sort((a, b) => b.publishedAt - a.publishedAt);
2724
- } else if (q) {
2725
- results.sort((a, b) => {
2726
- const aExact = a.name.toLowerCase() === q ? 1 : 0;
2727
- const bExact = b.name.toLowerCase() === q ? 1 : 0;
2728
- if (aExact !== bExact) return bExact - aExact;
2729
- return b.publishedAt - a.publishedAt;
2730
- });
2731
- }
2732
- return results.slice(0, limit).map((s) => {
2733
- const local = installedMap.get(s.name);
2734
- return local ? { ...s, installed: true, path: local.path, enabled: local.enabled } : s;
2735
- });
2736
- }
2737
2691
  async function handleSkillsRoutes(req, res, url, context) {
2738
2692
  const { appServer, readJsonBody: readJsonBody2 } = context;
2739
2693
  if (req.method === "GET" && url.pathname === "/codex-api/skills-hub") {
2740
2694
  try {
2741
- const q = url.searchParams.get("q") || "";
2742
- const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") || "50", 10) || 50, 1), 200);
2743
- const sort = url.searchParams.get("sort") || "date";
2744
- const allEntries = await fetchSkillsTree();
2745
2695
  const installedMap = await scanInstalledSkillsFromDisk();
2746
2696
  try {
2747
2697
  const result = await appServer.rpc("skills/list", {});
2748
2698
  for (const entry of result.data ?? []) {
2749
- for (const skill of entry.skills ?? []) {
2699
+ for (const skill of groupRpcSkillRecords(entry.skills ?? [])) {
2750
2700
  if (skill.name) {
2751
2701
  installedMap.set(skill.name, { name: skill.name, path: skill.path ?? "", enabled: skill.enabled !== false });
2752
2702
  }
@@ -2754,25 +2704,12 @@ async function handleSkillsRoutes(req, res, url, context) {
2754
2704
  }
2755
2705
  } catch {
2756
2706
  }
2757
- const installedHubEntries = allEntries.filter((e) => installedMap.has(e.name));
2758
- await fetchMetaBatch(installedHubEntries);
2759
2707
  const installed = [];
2760
2708
  for (const [, info] of installedMap) {
2761
- const hubEntry = allEntries.find((e) => e.name === info.name);
2762
- const base = hubEntry ? buildHubEntry(hubEntry) : {
2763
- name: info.name,
2764
- owner: "local",
2765
- description: "",
2766
- displayName: "",
2767
- publishedAt: 0,
2768
- avatarUrl: "",
2769
- url: "",
2770
- installed: false
2771
- };
2772
- installed.push({ ...base, installed: true, path: info.path, enabled: info.enabled });
2709
+ installed.push(buildLocalHubEntry(info));
2773
2710
  }
2774
- const results = await searchSkillsHub(allEntries, q, limit, sort, installedMap);
2775
- setJson3(res, 200, { data: results, installed, total: allEntries.length });
2711
+ installed.sort((a, b) => a.name.localeCompare(b.name));
2712
+ setJson3(res, 200, { installed });
2776
2713
  } catch (error) {
2777
2714
  setJson3(res, 502, { error: getErrorMessage3(error, "Failed to fetch skills hub") });
2778
2715
  }
@@ -2913,27 +2850,12 @@ async function handleSkillsRoutes(req, res, url, context) {
2913
2850
  return true;
2914
2851
  }
2915
2852
  const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
2916
- const tree = await fetchSkillsTree();
2917
- const uniqueOwnerByName = /* @__PURE__ */ new Map();
2918
- const ambiguousNames = /* @__PURE__ */ new Set();
2919
- for (const entry of tree) {
2920
- if (ambiguousNames.has(entry.name)) continue;
2921
- const existingOwner = uniqueOwnerByName.get(entry.name);
2922
- if (!existingOwner) {
2923
- uniqueOwnerByName.set(entry.name, entry.owner);
2924
- continue;
2925
- }
2926
- if (existingOwner !== entry.owner) {
2927
- uniqueOwnerByName.delete(entry.name);
2928
- ambiguousNames.add(entry.name);
2929
- }
2930
- }
2931
2853
  const localDir = await detectUserSkillsDir(appServer);
2932
2854
  await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
2933
2855
  const localSkills = await scanInstalledSkillsFromDisk();
2934
2856
  const missingAfterPull = [];
2935
2857
  for (const skill of remote) {
2936
- const owner = skill.owner || uniqueOwnerByName.get(skill.name) || "";
2858
+ const owner = skill.owner || "";
2937
2859
  if (!owner) continue;
2938
2860
  if (!localSkills.has(skill.name)) {
2939
2861
  missingAfterPull.push(`${owner}/${skill.name}`);
@@ -2953,7 +2875,7 @@ async function handleSkillsRoutes(req, res, url, context) {
2953
2875
  }
2954
2876
  const nextOwners = {};
2955
2877
  for (const item of remote) {
2956
- const owner = item.owner || uniqueOwnerByName.get(item.name) || "";
2878
+ const owner = item.owner || "";
2957
2879
  if (owner) nextOwners[item.name] = owner;
2958
2880
  }
2959
2881
  const pulledHead = await runCommandWithOutput("git", ["rev-parse", "HEAD"], { cwd: getSkillsInstallDir() }).catch(() => "");
@@ -2990,74 +2912,20 @@ async function handleSkillsRoutes(req, res, url, context) {
2990
2912
  const installedInfo = installedMap.get(name);
2991
2913
  const localSkillPath = installedInfo?.path || (skillPath ? skillPath.endsWith("/SKILL.md") ? skillPath : `${skillPath}/SKILL.md` : "");
2992
2914
  if (localSkillPath) {
2993
- const content2 = await readFile2(localSkillPath, "utf8");
2994
- const description2 = extractSkillDescriptionFromMarkdown(content2);
2995
- setJson3(res, 200, { content: content2, description: description2, source: "local" });
2915
+ const content = await readFile2(localSkillPath, "utf8");
2916
+ const description = extractSkillDescriptionFromMarkdown(content);
2917
+ setJson3(res, 200, { content, description, source: "local" });
2996
2918
  return true;
2997
2919
  }
2998
2920
  }
2999
- const rawUrl = `https://raw.githubusercontent.com/${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}/main/skills/${owner}/${name}/SKILL.md`;
3000
- const resp = await fetch(rawUrl);
3001
- if (!resp.ok) throw new Error(`Failed to fetch SKILL.md: ${resp.status}`);
3002
- const content = await resp.text();
3003
- const description = extractSkillDescriptionFromMarkdown(content);
3004
- setJson3(res, 200, { content, description, source: "remote" });
2921
+ setJson3(res, 404, { error: "Only installed local skills are available in Skills Hub." });
3005
2922
  } catch (error) {
3006
2923
  setJson3(res, 502, { error: getErrorMessage3(error, "Failed to fetch SKILL.md") });
3007
2924
  }
3008
2925
  return true;
3009
2926
  }
3010
2927
  if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/install") {
3011
- try {
3012
- const payload = asRecord3(await readJsonBody2(req));
3013
- const owner = typeof payload?.owner === "string" ? payload.owner : "";
3014
- const name = typeof payload?.name === "string" ? payload.name : "";
3015
- if (!owner || !name) {
3016
- setJson3(res, 400, { error: "Missing owner or name" });
3017
- return true;
3018
- }
3019
- const installerScript = resolveSkillInstallerScriptPath(getCodexHomeDir2());
3020
- if (!installerScript) {
3021
- throw new Error("Skill installer script not found");
3022
- }
3023
- const pythonCommand = resolvePythonCommand();
3024
- if (!pythonCommand) {
3025
- throw new Error("Python 3 is required to install skills");
3026
- }
3027
- const installDest = await withTimeout(
3028
- detectUserSkillsDir(appServer),
3029
- 1e4,
3030
- "detectUserSkillsDir"
3031
- ).catch(() => getSkillsInstallDir());
3032
- const skillDir = join4(installDest, name);
3033
- if (existsSync2(skillDir)) {
3034
- await rm3(skillDir, { recursive: true, force: true });
3035
- }
3036
- await runCommand2(pythonCommand.command, [
3037
- ...pythonCommand.args,
3038
- installerScript,
3039
- "--repo",
3040
- `${HUB_SKILLS_OWNER}/${HUB_SKILLS_REPO}`,
3041
- "--path",
3042
- `skills/${owner}/${name}`,
3043
- "--dest",
3044
- installDest,
3045
- "--method",
3046
- "git"
3047
- ], { timeoutMs: 9e4 });
3048
- try {
3049
- await withTimeout(ensureInstalledSkillIsValid(appServer, skillDir), 1e4, "ensureInstalledSkillIsValid");
3050
- } catch {
3051
- }
3052
- const syncState = await readSkillsSyncState();
3053
- const nextOwners = { ...syncState.installedOwners ?? {}, [name]: owner };
3054
- await writeSkillsSyncState({ ...syncState, installedOwners: nextOwners });
3055
- autoPushSyncedSkills(appServer).catch(() => {
3056
- });
3057
- setJson3(res, 200, { ok: true, path: skillDir });
3058
- } catch (error) {
3059
- setJson3(res, 502, { error: getErrorMessage3(error, "Failed to install skill") });
3060
- }
2928
+ setJson3(res, 410, { error: "Remote Skills Hub installation is disabled." });
3061
2929
  return true;
3062
2930
  }
3063
2931
  if (req.method === "POST" && url.pathname === "/codex-api/skills-hub/uninstall") {
@@ -4749,16 +4617,20 @@ function spawnSyncCommand(command, args = [], options = {}) {
4749
4617
  }
4750
4618
 
4751
4619
  // src/server/codexAppServerBridge.ts
4620
+ var COMPOSIO_CONNECTORS_PAGE_LIMIT_MAX = 1e3;
4752
4621
  var PROVIDER_MODELS_FETCH_TIMEOUT_MS = 5e3;
4753
4622
  var THREAD_RESPONSE_TURN_LIMIT = 10;
4754
4623
  var THREAD_METHODS_WITH_TURNS = /* @__PURE__ */ new Set(["thread/read", "thread/resume", "thread/fork", "thread/rollback"]);
4755
4624
  var THREAD_SEARCH_FULL_TEXT_THREAD_LIMIT = 100;
4625
+ var PROJECTLESS_THREAD_DIRECTORY_MAX_ATTEMPTS = 100;
4626
+ var PROJECTLESS_THREAD_SLUG_MAX_LENGTH = 80;
4756
4627
  var API_PERF_LOGGING_ENV_KEY = "CODEXUI_API_PERF_LOGGING";
4757
4628
  var API_PERF_MS_THRESHOLD_ENV_KEY = "CODEXUI_API_PERF_MS_THRESHOLD";
4758
4629
  var API_PERF_BODY_MB_THRESHOLD_ENV_KEY = "CODEXUI_API_PERF_BODY_MB_THRESHOLD";
4759
4630
  var DEFAULT_API_PERF_MS_THRESHOLD = 300;
4760
4631
  var DEFAULT_API_PERF_BODY_MB_THRESHOLD = 1;
4761
4632
  var MB_DIVISOR = 1024 * 1024;
4633
+ var COMPOSIO_USER_DATA_PATH = join6(homedir5(), ".composio", "user_data.json");
4762
4634
  function readEnvValueFromFile(filePath, key) {
4763
4635
  try {
4764
4636
  const content = readFileSync2(filePath, "utf8");
@@ -4830,13 +4702,42 @@ function asRecord5(value) {
4830
4702
  function isInlineDataUrl(value) {
4831
4703
  return /^data:/iu.test(value.trim());
4832
4704
  }
4705
+ function inferImageMimeTypeFromBytes(bytes) {
4706
+ if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) {
4707
+ return "image/png";
4708
+ }
4709
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
4710
+ return "image/jpeg";
4711
+ }
4712
+ if (bytes.length >= 12 && bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
4713
+ return "image/webp";
4714
+ }
4715
+ if (bytes.length >= 6 && bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56 && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97) {
4716
+ return "image/gif";
4717
+ }
4718
+ return null;
4719
+ }
4720
+ function inferImageMimeTypeFromBase64(value) {
4721
+ const compact = value.trim().replace(/\s+/gu, "");
4722
+ if (compact.length < 32 || !/^[A-Za-z0-9+/]+={0,2}$/u.test(compact)) return null;
4723
+ try {
4724
+ return inferImageMimeTypeFromBytes(Buffer.from(compact.slice(0, 64), "base64"));
4725
+ } catch {
4726
+ return null;
4727
+ }
4728
+ }
4833
4729
  function normalizeBase64ImageDataUrl(value, mimeType) {
4834
4730
  const trimmed = value.trim();
4835
4731
  if (!trimmed) return null;
4836
- if (isInlineDataUrl(trimmed)) return trimmed;
4732
+ if (isInlineDataUrl(trimmed)) {
4733
+ return /^data:image\//iu.test(trimmed) ? trimmed : null;
4734
+ }
4837
4735
  const compact = trimmed.replace(/\s+/gu, "");
4838
- if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(compact)) return null;
4839
- return `data:${mimeType};base64,${compact}`;
4736
+ const inferredMimeType = inferImageMimeTypeFromBase64(compact);
4737
+ if (!inferredMimeType) return null;
4738
+ const normalizedMimeType = mimeType.trim().toLowerCase();
4739
+ const finalMimeType = normalizedMimeType.startsWith("image/") && normalizedMimeType !== "image/*" ? normalizedMimeType : inferredMimeType;
4740
+ return `data:${finalMimeType};base64,${compact}`;
4840
4741
  }
4841
4742
  function extensionFromMimeType(mimeType) {
4842
4743
  const normalized = mimeType.trim().toLowerCase();
@@ -4888,6 +4789,30 @@ async function persistInlineDataUrlToLocalFile(dataUrl, baseName) {
4888
4789
  function toLocalImageProxyUrl(path) {
4889
4790
  return `/codex-local-image?path=${encodeURIComponent(path)}`;
4890
4791
  }
4792
+ var INLINE_IMAGE_FIELD_NAMES = /* @__PURE__ */ new Set([
4793
+ "b64_json",
4794
+ "image",
4795
+ "image_url",
4796
+ "images",
4797
+ "result",
4798
+ "url"
4799
+ ]);
4800
+ function isPotentialInlineImageField(fieldName) {
4801
+ return typeof fieldName === "string" && INLINE_IMAGE_FIELD_NAMES.has(fieldName);
4802
+ }
4803
+ async function sanitizeInlineImageString(value, context) {
4804
+ if (!isPotentialInlineImageField(context.fieldName)) {
4805
+ return { value, changed: false };
4806
+ }
4807
+ const dataUrl = normalizeBase64ImageDataUrl(value, "image/*");
4808
+ if (!dataUrl) return { value, changed: false };
4809
+ const localUrl = await persistInlineDataUrlToLocalFile(
4810
+ dataUrl,
4811
+ `inline-image-${context.turnId}-${context.itemId}-${context.fieldName}-${String(context.blockIndex)}`
4812
+ );
4813
+ if (!localUrl) return { value, changed: false };
4814
+ return { value: toLocalImageProxyUrl(localUrl), changed: true };
4815
+ }
4891
4816
  async function sanitizeInlineUserContentBlock(block, context) {
4892
4817
  const record = asRecord5(block);
4893
4818
  if (!record) return block;
@@ -4896,10 +4821,16 @@ async function sanitizeInlineUserContentBlock(block, context) {
4896
4821
  if (imageUrl && isInlineDataUrl(imageUrl)) {
4897
4822
  const localUrl = await persistInlineDataUrlToLocalFile(imageUrl, `inline-image-${context.turnId}-${context.itemId}-${String(context.blockIndex)}`);
4898
4823
  if (localUrl) {
4824
+ const nextRecord = { ...record };
4825
+ if (typeof record.url === "string") {
4826
+ nextRecord.url = toLocalImageProxyUrl(localUrl);
4827
+ }
4828
+ if (typeof record.image_url === "string") {
4829
+ nextRecord.image_url = toLocalImageProxyUrl(localUrl);
4830
+ }
4899
4831
  return {
4900
- ...record,
4901
- type: "image",
4902
- url: toLocalImageProxyUrl(localUrl)
4832
+ ...nextRecord,
4833
+ type: "image"
4903
4834
  };
4904
4835
  }
4905
4836
  const target = toAttachmentLinkTarget(record, `inline-image/${context.turnId}/${context.itemId}/${String(context.blockIndex)}`);
@@ -4947,6 +4878,9 @@ async function sanitizeInlinePayloadDeep(value, context) {
4947
4878
  if (maybeBlock !== value) {
4948
4879
  return { value: maybeBlock, changed: true };
4949
4880
  }
4881
+ if (typeof value === "string") {
4882
+ return sanitizeInlineImageString(value, context);
4883
+ }
4950
4884
  if (Array.isArray(value)) {
4951
4885
  let changed2 = false;
4952
4886
  const nextArray = [];
@@ -4954,7 +4888,8 @@ async function sanitizeInlinePayloadDeep(value, context) {
4954
4888
  const nested = await sanitizeInlinePayloadDeep(value[index], {
4955
4889
  turnId: context.turnId,
4956
4890
  itemId: context.itemId,
4957
- blockIndex: index
4891
+ blockIndex: index,
4892
+ fieldName: context.fieldName
4958
4893
  });
4959
4894
  if (nested.changed) changed2 = true;
4960
4895
  nextArray.push(nested.value);
@@ -4969,7 +4904,8 @@ async function sanitizeInlinePayloadDeep(value, context) {
4969
4904
  const nested = await sanitizeInlinePayloadDeep(nestedValue, {
4970
4905
  turnId: context.turnId,
4971
4906
  itemId: context.itemId,
4972
- blockIndex: context.blockIndex
4907
+ blockIndex: context.blockIndex,
4908
+ fieldName: key
4973
4909
  });
4974
4910
  if (nested.changed) changed = true;
4975
4911
  nextRecord[key] = nested.value;
@@ -5073,6 +5009,45 @@ function logProviderModelDiscoveryWarning(message, details) {
5073
5009
  function isTimeoutError(payload) {
5074
5010
  return payload instanceof Error && (payload.name === "AbortError" || payload.name === "TimeoutError");
5075
5011
  }
5012
+ function formatProjectlessDateSegment(date = /* @__PURE__ */ new Date()) {
5013
+ const month = String(date.getMonth() + 1).padStart(2, "0");
5014
+ const day = String(date.getDate()).padStart(2, "0");
5015
+ return `${date.getFullYear()}-${month}-${day}`;
5016
+ }
5017
+ function buildProjectlessPromptSlug(prompt) {
5018
+ const slug = prompt?.toLowerCase().match(/[a-z0-9]+/g)?.slice(0, 6).join("-").slice(0, PROJECTLESS_THREAD_SLUG_MAX_LENGTH);
5019
+ return slug && slug.length > 0 ? slug : "new-chat";
5020
+ }
5021
+ async function ensureRealDirectory(path, label) {
5022
+ const info = await lstat2(path);
5023
+ if (info.isSymbolicLink() || !info.isDirectory()) {
5024
+ throw new Error(`${label} must be a real directory`);
5025
+ }
5026
+ }
5027
+ async function createProjectlessThreadDirectory(prompt) {
5028
+ const workspaceRoot = join6(homedir5(), "Documents", "Codex");
5029
+ await mkdir4(workspaceRoot, { recursive: true });
5030
+ await ensureRealDirectory(workspaceRoot, "Projectless workspace root");
5031
+ const dateDir = join6(workspaceRoot, formatProjectlessDateSegment());
5032
+ await mkdir4(dateDir, { recursive: true });
5033
+ await ensureRealDirectory(dateDir, "Projectless thread date directory");
5034
+ const slug = buildProjectlessPromptSlug(prompt);
5035
+ for (let index = 0; index < PROJECTLESS_THREAD_DIRECTORY_MAX_ATTEMPTS; index += 1) {
5036
+ const folderName = index === 0 ? slug : `${slug}-${index + 1}`;
5037
+ const cwd = join6(dateDir, folderName);
5038
+ try {
5039
+ await mkdir4(cwd, { recursive: false });
5040
+ return { cwd, outputDirectory: cwd, workspaceRoot };
5041
+ } catch {
5042
+ try {
5043
+ await stat4(cwd);
5044
+ } catch {
5045
+ throw new Error("Failed to create new chat folder");
5046
+ }
5047
+ }
5048
+ }
5049
+ throw new Error("Unable to create a unique new chat folder");
5050
+ }
5076
5051
  function normalizeHeaderValue(value) {
5077
5052
  if (typeof value === "string") {
5078
5053
  const trimmed = value.trim();
@@ -5272,6 +5247,414 @@ function extractThreadMessageText(threadReadPayload) {
5272
5247
  function readNonEmptyString(value) {
5273
5248
  return typeof value === "string" && value.trim().length > 0 ? value : "";
5274
5249
  }
5250
+ async function listTerminalQuickCommands(cwd) {
5251
+ const normalizedCwd = isAbsolute2(cwd) ? cwd : resolve2(cwd);
5252
+ const info = await stat4(normalizedCwd);
5253
+ if (!info.isDirectory()) {
5254
+ throw new Error("Terminal cwd is not a directory");
5255
+ }
5256
+ const commands = [];
5257
+ const seen = /* @__PURE__ */ new Set();
5258
+ const addCommand = (command) => {
5259
+ if (!command.value || seen.has(command.value)) return;
5260
+ seen.add(command.value);
5261
+ commands.push(command);
5262
+ };
5263
+ await addPackageJsonCommands(normalizedCwd, addCommand);
5264
+ await addMakefileCommands(normalizedCwd, addCommand);
5265
+ await addRootScriptCommands(normalizedCwd, addCommand);
5266
+ await addScriptsDirectoryCommands(normalizedCwd, addCommand);
5267
+ return commands;
5268
+ }
5269
+ async function addPackageJsonCommands(cwd, addCommand) {
5270
+ try {
5271
+ const raw = await readFile3(join6(cwd, "package.json"), "utf8");
5272
+ const parsed = JSON.parse(raw);
5273
+ const record = asRecord5(parsed);
5274
+ const scripts = asRecord5(record?.scripts);
5275
+ if (!scripts) return;
5276
+ const packageManager = resolvePackageManager(cwd);
5277
+ for (const scriptName of Object.keys(scripts)) {
5278
+ if (typeof scripts[scriptName] !== "string") continue;
5279
+ const value = formatPackageScriptCommand(packageManager, scriptName);
5280
+ addCommand({
5281
+ label: value,
5282
+ value,
5283
+ source: "package"
5284
+ });
5285
+ }
5286
+ } catch {
5287
+ }
5288
+ }
5289
+ async function addMakefileCommands(cwd, addCommand) {
5290
+ const makefilePath = existsSync4(join6(cwd, "Makefile")) ? join6(cwd, "Makefile") : existsSync4(join6(cwd, "makefile")) ? join6(cwd, "makefile") : "";
5291
+ if (!makefilePath) return;
5292
+ try {
5293
+ const raw = await readFile3(makefilePath, "utf8");
5294
+ for (const line of raw.split(/\r?\n/)) {
5295
+ const match = /^([A-Za-z0-9_.@%/+~-][A-Za-z0-9_.@%/+~-]*)\s*:(?![=])/.exec(line);
5296
+ if (!match) continue;
5297
+ const target = match[1];
5298
+ if (!target || target.startsWith(".")) continue;
5299
+ const value = `make ${quoteShellTokenIfNeeded(target)}`;
5300
+ addCommand({
5301
+ label: value,
5302
+ value,
5303
+ source: "make"
5304
+ });
5305
+ }
5306
+ } catch {
5307
+ }
5308
+ }
5309
+ async function addRootScriptCommands(cwd, addCommand) {
5310
+ await addScriptFileCommands(cwd, ".", addCommand);
5311
+ }
5312
+ async function addScriptsDirectoryCommands(cwd, addCommand) {
5313
+ await addScriptFileCommands(join6(cwd, "scripts"), "./scripts", addCommand);
5314
+ }
5315
+ async function addScriptFileCommands(directory, commandPrefix, addCommand) {
5316
+ try {
5317
+ const entries = await readdir2(directory, { withFileTypes: true });
5318
+ for (const entry of entries) {
5319
+ if (!entry.isFile()) continue;
5320
+ if (!entry.name.endsWith(".sh") && !entry.name.endsWith(".cmd")) continue;
5321
+ const value = `${commandPrefix}/${quoteShellTokenIfNeeded(entry.name)}`;
5322
+ addCommand({
5323
+ label: value,
5324
+ value,
5325
+ source: "script"
5326
+ });
5327
+ }
5328
+ } catch {
5329
+ }
5330
+ }
5331
+ function resolvePackageManager(cwd) {
5332
+ if (existsSync4(join6(cwd, "pnpm-lock.yaml"))) return "pnpm";
5333
+ if (existsSync4(join6(cwd, "yarn.lock"))) return "yarn";
5334
+ if (existsSync4(join6(cwd, "bun.lock")) || existsSync4(join6(cwd, "bun.lockb"))) return "bun";
5335
+ return "npm";
5336
+ }
5337
+ function formatPackageScriptCommand(packageManager, scriptName) {
5338
+ const quoted = quoteShellTokenIfNeeded(scriptName);
5339
+ if (packageManager === "npm") return `npm run ${quoted}`;
5340
+ if (packageManager === "pnpm") return `pnpm run ${quoted}`;
5341
+ if (packageManager === "bun") return `bun run ${quoted}`;
5342
+ return `yarn ${quoted}`;
5343
+ }
5344
+ function quoteShellTokenIfNeeded(value) {
5345
+ return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
5346
+ }
5347
+ function readBoolean2(value) {
5348
+ return value === true;
5349
+ }
5350
+ function readNumber2(value) {
5351
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
5352
+ }
5353
+ function resolveComposioCommand() {
5354
+ const candidates = [
5355
+ process.env.CODEXUI_COMPOSIO_COMMAND?.trim() ?? "",
5356
+ join6(homedir5(), ".composio", "composio"),
5357
+ "composio"
5358
+ ];
5359
+ for (const candidate of candidates) {
5360
+ if (!candidate) continue;
5361
+ if ((candidate.includes("/") || candidate.includes("\\")) && !existsSync4(candidate)) continue;
5362
+ const invocation = getSpawnInvocation(candidate, ["--version"]);
5363
+ const probe = spawnSync4(invocation.command, invocation.args, {
5364
+ stdio: "ignore",
5365
+ windowsHide: true
5366
+ });
5367
+ if (!probe.error && probe.status === 0) {
5368
+ return candidate;
5369
+ }
5370
+ }
5371
+ return null;
5372
+ }
5373
+ function parseComposioJson(stdout, fallback) {
5374
+ const trimmed = stdout.trim();
5375
+ if (!trimmed) {
5376
+ throw new Error(fallback);
5377
+ }
5378
+ return JSON.parse(trimmed);
5379
+ }
5380
+ async function runComposioJson(args, fallback) {
5381
+ const command = resolveComposioCommand();
5382
+ if (!command) {
5383
+ throw new Error("Composio CLI is not installed");
5384
+ }
5385
+ const invocation = getSpawnInvocation(command, args);
5386
+ const child = spawn4(invocation.command, invocation.args, {
5387
+ env: process.env,
5388
+ stdio: ["ignore", "pipe", "pipe"],
5389
+ windowsHide: true
5390
+ });
5391
+ let stdout = "";
5392
+ let stderr = "";
5393
+ child.stdout.setEncoding("utf8");
5394
+ child.stderr.setEncoding("utf8");
5395
+ child.stdout.on("data", (chunk) => {
5396
+ stdout += chunk;
5397
+ });
5398
+ child.stderr.on("data", (chunk) => {
5399
+ stderr += chunk;
5400
+ });
5401
+ const exitCode = await new Promise((resolveExit, reject) => {
5402
+ child.once("error", reject);
5403
+ child.once("close", (code) => resolveExit(code ?? 0));
5404
+ });
5405
+ if (exitCode !== 0) {
5406
+ throw new Error(stderr.trim() || stdout.trim() || fallback);
5407
+ }
5408
+ try {
5409
+ return parseComposioJson(stdout, fallback);
5410
+ } catch (error) {
5411
+ const details = stderr.trim() || stdout.trim();
5412
+ throw new Error(details || getErrorMessage5(error, fallback));
5413
+ }
5414
+ }
5415
+ async function readComposioUserData() {
5416
+ try {
5417
+ const raw = await readFile3(COMPOSIO_USER_DATA_PATH, "utf8");
5418
+ const payload = asRecord5(JSON.parse(raw));
5419
+ if (!payload) return null;
5420
+ return {
5421
+ apiKey: readNonEmptyString(payload.api_key),
5422
+ baseUrl: readNonEmptyString(payload.base_url),
5423
+ webUrl: readNonEmptyString(payload.web_url),
5424
+ orgId: readNonEmptyString(payload.org_id),
5425
+ testUserId: readNonEmptyString(payload.test_user_id)
5426
+ };
5427
+ } catch {
5428
+ return null;
5429
+ }
5430
+ }
5431
+ function normalizeComposioConnection(value) {
5432
+ const record = asRecord5(value);
5433
+ if (!record) return null;
5434
+ const authConfig = asRecord5(record.auth_config);
5435
+ return {
5436
+ id: readNonEmptyString(record.id),
5437
+ wordId: readNonEmptyString(record.word_id),
5438
+ alias: readNonEmptyString(record.alias),
5439
+ status: readNonEmptyString(record.status),
5440
+ authScheme: readNonEmptyString(record.authScheme || authConfig?.auth_scheme),
5441
+ createdAt: readNonEmptyString(record.created_at),
5442
+ updatedAt: readNonEmptyString(record.updated_at),
5443
+ isComposioManaged: readBoolean2(authConfig?.is_composio_managed),
5444
+ isDisabled: readBoolean2(record.is_disabled)
5445
+ };
5446
+ }
5447
+ function normalizeComposioToolkit(value, connectionsBySlug) {
5448
+ const record = asRecord5(value);
5449
+ if (!record) return null;
5450
+ const slug = readNonEmptyString(record.slug);
5451
+ if (!slug) return null;
5452
+ const connectionRows = connectionsBySlug.get(slug) ?? [];
5453
+ return {
5454
+ slug,
5455
+ name: readNonEmptyString(record.name),
5456
+ description: readNonEmptyString(record.description),
5457
+ logoUrl: readNonEmptyString(record.logo || record.meta && asRecord5(record.meta)?.logo),
5458
+ latestVersion: readNonEmptyString(record.latest_version || record.latestVersion),
5459
+ toolsCount: readNumber2(record.tools_count),
5460
+ triggersCount: readNumber2(record.triggers_count),
5461
+ isNoAuth: readBoolean2(record.is_no_auth),
5462
+ enabled: record.enabled !== false,
5463
+ authModes: Array.isArray(record.auth_modes) ? record.auth_modes.map(readNonEmptyString).filter(Boolean) : [],
5464
+ activeCount: connectionRows.filter((row) => row.status === "ACTIVE" && !row.isDisabled).length,
5465
+ totalConnections: connectionRows.length,
5466
+ connectionStatuses: [...new Set(connectionRows.map((row) => row.status).filter(Boolean))]
5467
+ };
5468
+ }
5469
+ function normalizeComposioTool(value) {
5470
+ const record = asRecord5(value);
5471
+ if (!record) return null;
5472
+ const slug = readNonEmptyString(record.slug);
5473
+ if (!slug) return null;
5474
+ return {
5475
+ slug,
5476
+ name: readNonEmptyString(record.name),
5477
+ description: readNonEmptyString(record.description)
5478
+ };
5479
+ }
5480
+ async function readComposioConnectionsBySlug() {
5481
+ const payload = asRecord5(await runComposioJson(["connections", "list"], "Failed to list Composio connections"));
5482
+ const bySlug = /* @__PURE__ */ new Map();
5483
+ for (const [slug, rawRows] of Object.entries(payload ?? {})) {
5484
+ if (!Array.isArray(rawRows)) continue;
5485
+ const rows = rawRows.map(normalizeComposioConnection).filter((row) => row !== null);
5486
+ bySlug.set(slug, rows);
5487
+ }
5488
+ return bySlug;
5489
+ }
5490
+ async function readComposioStatus() {
5491
+ const cliVersion = (() => {
5492
+ const command = resolveComposioCommand();
5493
+ if (!command) return "";
5494
+ const invocation = getSpawnInvocation(command, ["--version"]);
5495
+ const probe = spawnSync4(invocation.command, invocation.args, {
5496
+ encoding: "utf8",
5497
+ windowsHide: true
5498
+ });
5499
+ return probe.status === 0 ? probe.stdout.trim() : "";
5500
+ })();
5501
+ const userData = await readComposioUserData();
5502
+ if (!resolveComposioCommand()) {
5503
+ return {
5504
+ available: false,
5505
+ authenticated: false,
5506
+ cliVersion,
5507
+ email: "",
5508
+ defaultOrgName: "",
5509
+ defaultOrgId: userData?.orgId ?? "",
5510
+ webUrl: userData?.webUrl ?? "",
5511
+ baseUrl: userData?.baseUrl ?? "",
5512
+ testUserId: userData?.testUserId ?? ""
5513
+ };
5514
+ }
5515
+ try {
5516
+ const payload = asRecord5(await runComposioJson(["whoami"], "Failed to read Composio account status"));
5517
+ return {
5518
+ available: true,
5519
+ authenticated: true,
5520
+ cliVersion,
5521
+ email: readNonEmptyString(payload?.email),
5522
+ defaultOrgName: readNonEmptyString(payload?.default_org_name),
5523
+ defaultOrgId: readNonEmptyString(payload?.default_org_id) || userData?.orgId || "",
5524
+ webUrl: userData?.webUrl || "https://dashboard.composio.dev/",
5525
+ baseUrl: userData?.baseUrl || "https://backend.composio.dev",
5526
+ testUserId: readNonEmptyString(payload?.test_user_id) || userData?.testUserId || ""
5527
+ };
5528
+ } catch {
5529
+ return {
5530
+ available: true,
5531
+ authenticated: false,
5532
+ cliVersion,
5533
+ email: "",
5534
+ defaultOrgName: "",
5535
+ defaultOrgId: userData?.orgId ?? "",
5536
+ webUrl: userData?.webUrl || "https://dashboard.composio.dev/",
5537
+ baseUrl: userData?.baseUrl || "https://backend.composio.dev",
5538
+ testUserId: userData?.testUserId ?? ""
5539
+ };
5540
+ }
5541
+ }
5542
+ async function listComposioConnectors(query, cursor = null, limit = 50) {
5543
+ const args = ["dev", "toolkits", "list", "--limit", String(COMPOSIO_CONNECTORS_PAGE_LIMIT_MAX)];
5544
+ const trimmedQuery = query.trim();
5545
+ if (trimmedQuery) {
5546
+ args.push("--query", trimmedQuery);
5547
+ }
5548
+ const [payload, connectionsBySlug] = await Promise.all([
5549
+ runComposioJson(args, "Failed to list Composio toolkits"),
5550
+ readComposioConnectionsBySlug()
5551
+ ]);
5552
+ const allRows = payload.map((item) => normalizeComposioToolkit(item, connectionsBySlug)).filter((row) => row !== null);
5553
+ const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.min(COMPOSIO_CONNECTORS_PAGE_LIMIT_MAX, Math.floor(limit))) : 50;
5554
+ const safeCursor = parseComposioCursor(cursor, allRows.length);
5555
+ return {
5556
+ data: allRows.slice(safeCursor, safeCursor + safeLimit),
5557
+ nextCursor: safeCursor + safeLimit < allRows.length ? String(safeCursor + safeLimit) : null,
5558
+ total: allRows.length
5559
+ };
5560
+ }
5561
+ function parseComposioCursor(cursor, maxLength) {
5562
+ const trimmed = cursor?.trim() ?? "";
5563
+ const parsed = Number.parseInt(trimmed, 10);
5564
+ if (!Number.isFinite(parsed) || Number.isNaN(parsed) || parsed <= 0) return 0;
5565
+ if (parsed >= maxLength) return maxLength;
5566
+ return parsed;
5567
+ }
5568
+ function parseComposioLimit(rawLimit) {
5569
+ const parsed = Number.parseInt((rawLimit ?? "").trim(), 10);
5570
+ if (!Number.isFinite(parsed) || Number.isNaN(parsed) || parsed <= 0) return 50;
5571
+ return Math.max(1, Math.min(COMPOSIO_CONNECTORS_PAGE_LIMIT_MAX, parsed));
5572
+ }
5573
+ async function readComposioConnectorDetail(slug) {
5574
+ const normalizedSlug = slug.trim();
5575
+ if (!normalizedSlug) {
5576
+ throw new Error("Missing Composio connector slug");
5577
+ }
5578
+ const [infoPayload, toolsPayload, connectionsPayload, userData] = await Promise.all([
5579
+ runComposioJson(["dev", "toolkits", "info", normalizedSlug], `Failed to load Composio toolkit ${normalizedSlug}`),
5580
+ runComposioJson(["tools", "list", normalizedSlug, "--limit", "10"], `Failed to list tools for ${normalizedSlug}`),
5581
+ runComposioJson(["link", normalizedSlug, "--list"], `Failed to list connections for ${normalizedSlug}`),
5582
+ readComposioUserData()
5583
+ ]);
5584
+ const connections = Array.isArray(connectionsPayload.items) ? connectionsPayload.items.map(normalizeComposioConnection).filter((row) => row !== null) : [];
5585
+ const connector = normalizeComposioToolkit(infoPayload, /* @__PURE__ */ new Map([[normalizedSlug, connections]]));
5586
+ if (!connector) {
5587
+ throw new Error(`Unknown Composio connector: ${normalizedSlug}`);
5588
+ }
5589
+ return {
5590
+ connector,
5591
+ connections,
5592
+ tools: Array.isArray(toolsPayload) ? toolsPayload.map(normalizeComposioTool).filter((row) => row !== null) : [],
5593
+ dashboardUrl: userData?.webUrl || "https://dashboard.composio.dev/"
5594
+ };
5595
+ }
5596
+ async function startComposioLink(slug) {
5597
+ const normalizedSlug = slug.trim();
5598
+ if (!normalizedSlug) {
5599
+ throw new Error("Missing Composio connector slug");
5600
+ }
5601
+ const payload = asRecord5(await runComposioJson(["link", normalizedSlug, "--no-wait"], `Failed to start Composio link for ${normalizedSlug}`));
5602
+ return {
5603
+ status: readNonEmptyString(payload?.status),
5604
+ message: readNonEmptyString(payload?.message),
5605
+ connectedAccountId: readNonEmptyString(payload?.connected_account_id),
5606
+ redirectUrl: readNonEmptyString(payload?.redirect_url),
5607
+ toolkit: readNonEmptyString(payload?.toolkit),
5608
+ projectType: readNonEmptyString(payload?.project_type)
5609
+ };
5610
+ }
5611
+ async function startComposioLogin() {
5612
+ const command = resolveComposioCommand();
5613
+ if (!command) {
5614
+ throw new Error("Composio CLI is not installed");
5615
+ }
5616
+ const invocation = getSpawnInvocation(command, ["login", "-y"]);
5617
+ const proc = spawn4(invocation.command, invocation.args, {
5618
+ cwd: process.cwd(),
5619
+ env: process.env,
5620
+ detached: true,
5621
+ stdio: "ignore",
5622
+ windowsHide: true
5623
+ });
5624
+ proc.unref();
5625
+ return {
5626
+ status: "started",
5627
+ message: "Composio CLI login started",
5628
+ loginUrl: "",
5629
+ cliKey: "",
5630
+ expiresAt: ""
5631
+ };
5632
+ }
5633
+ async function installComposioCli() {
5634
+ const homeBin = join6(homedir5(), ".npm-global", "bin");
5635
+ const command = "npm";
5636
+ const args = ["install", "-g", "@composio/cli@latest"];
5637
+ const invocation = getSpawnInvocation(command, args);
5638
+ const env = {
5639
+ ...process.env,
5640
+ npm_config_prefix: process.env.npm_config_prefix?.trim() || join6(homedir5(), ".npm-global"),
5641
+ PATH: process.env.PATH ? `${homeBin}:${process.env.PATH}` : homeBin
5642
+ };
5643
+ const result = spawnSync4(invocation.command, invocation.args, {
5644
+ encoding: "utf8",
5645
+ env,
5646
+ windowsHide: true
5647
+ });
5648
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
5649
+ if (result.error || result.status !== 0) {
5650
+ throw new Error(output || result.error?.message || "Failed to install Composio CLI");
5651
+ }
5652
+ return {
5653
+ ok: true,
5654
+ command: `${command} ${args.join(" ")}`,
5655
+ output
5656
+ };
5657
+ }
5275
5658
  function countRecoveredContentLines(value) {
5276
5659
  if (!value) return 0;
5277
5660
  const normalized = value.replace(/\r\n/g, "\n");
@@ -5863,54 +6246,6 @@ function scoreFileCandidate(path, query) {
5863
6246
  if (lowerPath.includes(lowerQuery)) return 4;
5864
6247
  return 10;
5865
6248
  }
5866
- function decodeHtmlEntities(value) {
5867
- return value.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x2F;/gi, "/");
5868
- }
5869
- function stripHtml(value) {
5870
- return decodeHtmlEntities(value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim());
5871
- }
5872
- function parseGithubTrendingHtml(html, limit) {
5873
- const rows = html.match(/<article[\s\S]*?<\/article>/g) ?? [];
5874
- const items = [];
5875
- let seq = Date.now();
5876
- for (const row of rows) {
5877
- const repoBlockMatch = row.match(/<h2[\s\S]*?<\/h2>/);
5878
- const hrefMatch = repoBlockMatch?.[0]?.match(/href="\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)"/);
5879
- if (!hrefMatch) continue;
5880
- const fullName = hrefMatch[1] ?? "";
5881
- if (!fullName || items.some((item) => item.fullName === fullName)) continue;
5882
- const descriptionMatch = row.match(/<p[^>]*class="[^"]*col-9[^"]*"[^>]*>([\s\S]*?)<\/p>/) ?? row.match(/<p[^>]*class="[^"]*color-fg-muted[^"]*"[^>]*>([\s\S]*?)<\/p>/) ?? row.match(/<p[^>]*>([\s\S]*?)<\/p>/);
5883
- const languageMatch = row.match(/programmingLanguage[^>]*>\s*([\s\S]*?)\s*<\/span>/);
5884
- const starsMatch = row.match(/href="\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/stargazers"[\s\S]*?>([\s\S]*?)<\/a>/);
5885
- const starsText = stripHtml(starsMatch?.[1] ?? "").replace(/,/g, "");
5886
- const stars = Number.parseInt(starsText, 10);
5887
- items.push({
5888
- id: seq,
5889
- fullName,
5890
- url: `https://github.com/${fullName}`,
5891
- description: stripHtml(descriptionMatch?.[1] ?? ""),
5892
- language: stripHtml(languageMatch?.[1] ?? ""),
5893
- stars: Number.isFinite(stars) ? stars : 0
5894
- });
5895
- seq += 1;
5896
- if (items.length >= limit) break;
5897
- }
5898
- return items;
5899
- }
5900
- async function fetchGithubTrending(since, limit) {
5901
- const endpoint = `https://github.com/trending?since=${since}`;
5902
- const response = await fetch(endpoint, {
5903
- headers: {
5904
- "User-Agent": "codex-web-local",
5905
- Accept: "text/html"
5906
- }
5907
- });
5908
- if (!response.ok) {
5909
- throw new Error(`GitHub trending fetch failed (${response.status})`);
5910
- }
5911
- const html = await response.text();
5912
- return parseGithubTrendingHtml(html, limit);
5913
- }
5914
6249
  async function listFilesWithRipgrep(cwd) {
5915
6250
  return await new Promise((resolve4, reject) => {
5916
6251
  const ripgrepCommand = resolveRipgrepCommand();
@@ -5947,6 +6282,79 @@ function getCodexHomeDir3() {
5947
6282
  const codexHome = process.env.CODEX_HOME?.trim();
5948
6283
  return codexHome && codexHome.length > 0 ? codexHome : join6(homedir5(), ".codex");
5949
6284
  }
6285
+ function getPromptsDir() {
6286
+ return join6(getCodexHomeDir3(), "prompts");
6287
+ }
6288
+ function promptNameToFileName(name) {
6289
+ const trimmed = name.trim();
6290
+ const withoutExtension = trimmed.replace(/\.md$/i, "");
6291
+ const sanitized = withoutExtension.replace(/[\/\\:*?"<>|]/g, " ").replace(/\s+/g, " ").trim();
6292
+ return `${sanitized || "prompt"}.md`;
6293
+ }
6294
+ function buildPromptDescription(content) {
6295
+ const firstNonEmptyLine = content.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
6296
+ return firstNonEmptyLine.slice(0, 120);
6297
+ }
6298
+ async function listComposerPrompts() {
6299
+ const promptsDir = getPromptsDir();
6300
+ try {
6301
+ const entries = await readdir2(promptsDir, { withFileTypes: true });
6302
+ const prompts = await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md")).map(async (entry) => {
6303
+ const promptPath = join6(promptsDir, entry.name);
6304
+ const content = await readFile3(promptPath, "utf8");
6305
+ return {
6306
+ name: entry.name.replace(/\.md$/i, ""),
6307
+ path: promptPath,
6308
+ content,
6309
+ description: buildPromptDescription(content)
6310
+ };
6311
+ }));
6312
+ return prompts.sort((a, b) => a.name.localeCompare(b.name));
6313
+ } catch (error) {
6314
+ if (error?.code === "ENOENT") return [];
6315
+ throw error;
6316
+ }
6317
+ }
6318
+ async function createComposerPromptFile(name, content) {
6319
+ const trimmedName = name.trim();
6320
+ if (!trimmedName) throw new Error("Prompt name is required");
6321
+ const trimmedContent = content.trim();
6322
+ if (!trimmedContent) throw new Error("Prompt content is required");
6323
+ const promptsDir = getPromptsDir();
6324
+ await mkdir4(promptsDir, { recursive: true });
6325
+ const baseFileName = promptNameToFileName(trimmedName);
6326
+ let targetPath = join6(promptsDir, baseFileName);
6327
+ let suffix = 2;
6328
+ while (existsSync4(targetPath)) {
6329
+ const nextFileName = `${baseFileName.replace(/\.md$/i, "")}-${suffix}.md`;
6330
+ targetPath = join6(promptsDir, nextFileName);
6331
+ suffix += 1;
6332
+ }
6333
+ await writeFile4(targetPath, `${trimmedContent}
6334
+ `, "utf8");
6335
+ return {
6336
+ name: basename4(targetPath).replace(/\.md$/i, ""),
6337
+ path: targetPath,
6338
+ content: `${trimmedContent}
6339
+ `,
6340
+ description: buildPromptDescription(trimmedContent)
6341
+ };
6342
+ }
6343
+ async function removeComposerPromptFile(promptPath) {
6344
+ const resolvedPath = resolve2(promptPath);
6345
+ const promptsDir = resolve2(getPromptsDir());
6346
+ const relative = resolvedPath.startsWith(`${promptsDir}/`) ? resolvedPath.slice(promptsDir.length + 1) : "";
6347
+ if (!relative || relative.includes("..") || !resolvedPath.toLowerCase().endsWith(".md")) {
6348
+ throw new Error("Invalid prompt path");
6349
+ }
6350
+ try {
6351
+ await rm4(resolvedPath, { force: false });
6352
+ return true;
6353
+ } catch (error) {
6354
+ if (error?.code === "ENOENT") return false;
6355
+ throw error;
6356
+ }
6357
+ }
5950
6358
  async function runCommand3(command, args, options = {}) {
5951
6359
  await new Promise((resolve4, reject) => {
5952
6360
  const proc = spawn4(command, args, {
@@ -6346,6 +6754,109 @@ async function writePinnedThreadIds(threadIds) {
6346
6754
  payload[PINNED_THREAD_IDS_KEY] = normalizePinnedThreadIds(threadIds);
6347
6755
  await writeFile4(statePath, JSON.stringify(payload), "utf8");
6348
6756
  }
6757
+ var FIRST_LAUNCH_PLUGINS_CARD_DISMISSED_KEY = "first-launch-plugins-card-dismissed";
6758
+ var THREAD_QUEUE_STATE_KEY = "thread-queue-state";
6759
+ function normalizeStoredQueuedMessage(value) {
6760
+ const record = asRecord5(value);
6761
+ if (!record) return null;
6762
+ const id = typeof record.id === "string" ? record.id.trim() : "";
6763
+ if (!id) return null;
6764
+ const normalizeNamedPathItems = (items) => {
6765
+ if (!Array.isArray(items)) return [];
6766
+ return items.flatMap((item) => {
6767
+ const itemRecord = asRecord5(item);
6768
+ if (!itemRecord) return [];
6769
+ const name = typeof itemRecord.name === "string" ? itemRecord.name.trim() : "";
6770
+ const path = typeof itemRecord.path === "string" ? itemRecord.path.trim() : "";
6771
+ return name && path ? [{ name, path }] : [];
6772
+ });
6773
+ };
6774
+ const normalizeFileAttachments = (items) => {
6775
+ if (!Array.isArray(items)) return [];
6776
+ return items.flatMap((item) => {
6777
+ const itemRecord = asRecord5(item);
6778
+ if (!itemRecord) return [];
6779
+ const label = typeof itemRecord.label === "string" ? itemRecord.label.trim() : "";
6780
+ const path = typeof itemRecord.path === "string" ? itemRecord.path.trim() : "";
6781
+ const fsPath = typeof itemRecord.fsPath === "string" ? itemRecord.fsPath.trim() : "";
6782
+ return label && path && fsPath ? [{ label, path, fsPath }] : [];
6783
+ });
6784
+ };
6785
+ return {
6786
+ id,
6787
+ text: typeof record.text === "string" ? record.text : "",
6788
+ imageUrls: normalizeStringArray(record.imageUrls),
6789
+ skills: normalizeNamedPathItems(record.skills),
6790
+ fileAttachments: normalizeFileAttachments(record.fileAttachments),
6791
+ collaborationMode: record.collaborationMode === "plan" ? "plan" : "default"
6792
+ };
6793
+ }
6794
+ function normalizeThreadQueueState(value) {
6795
+ const record = asRecord5(value);
6796
+ if (!record) return {};
6797
+ const state = {};
6798
+ for (const [threadId, rawMessages] of Object.entries(record)) {
6799
+ const normalizedThreadId = threadId.trim();
6800
+ if (!normalizedThreadId || !Array.isArray(rawMessages)) continue;
6801
+ const messages = rawMessages.flatMap((item) => {
6802
+ const message = normalizeStoredQueuedMessage(item);
6803
+ return message ? [message] : [];
6804
+ });
6805
+ if (messages.length > 0) {
6806
+ state[normalizedThreadId] = messages;
6807
+ }
6808
+ }
6809
+ return state;
6810
+ }
6811
+ async function readThreadQueueState() {
6812
+ const statePath = getCodexGlobalStatePath();
6813
+ try {
6814
+ const raw = await readFile3(statePath, "utf8");
6815
+ const payload = asRecord5(JSON.parse(raw)) ?? {};
6816
+ return normalizeThreadQueueState(payload[THREAD_QUEUE_STATE_KEY]);
6817
+ } catch {
6818
+ return {};
6819
+ }
6820
+ }
6821
+ async function writeThreadQueueState(nextState) {
6822
+ const statePath = getCodexGlobalStatePath();
6823
+ let payload = {};
6824
+ try {
6825
+ const raw = await readFile3(statePath, "utf8");
6826
+ payload = asRecord5(JSON.parse(raw)) ?? {};
6827
+ } catch {
6828
+ payload = {};
6829
+ }
6830
+ const normalized = normalizeThreadQueueState(nextState);
6831
+ if (Object.keys(normalized).length > 0) {
6832
+ payload[THREAD_QUEUE_STATE_KEY] = normalized;
6833
+ } else {
6834
+ delete payload[THREAD_QUEUE_STATE_KEY];
6835
+ }
6836
+ await writeFile4(statePath, JSON.stringify(payload), "utf8");
6837
+ }
6838
+ async function readFirstLaunchPluginsCardDismissed() {
6839
+ const statePath = getCodexGlobalStatePath();
6840
+ try {
6841
+ const raw = await readFile3(statePath, "utf8");
6842
+ const payload = asRecord5(JSON.parse(raw)) ?? {};
6843
+ return payload[FIRST_LAUNCH_PLUGINS_CARD_DISMISSED_KEY] === true;
6844
+ } catch {
6845
+ return false;
6846
+ }
6847
+ }
6848
+ async function writeFirstLaunchPluginsCardDismissed(dismissed) {
6849
+ const statePath = getCodexGlobalStatePath();
6850
+ let payload = {};
6851
+ try {
6852
+ const raw = await readFile3(statePath, "utf8");
6853
+ payload = asRecord5(JSON.parse(raw)) ?? {};
6854
+ } catch {
6855
+ payload = {};
6856
+ }
6857
+ payload[FIRST_LAUNCH_PLUGINS_CARD_DISMISSED_KEY] = dismissed === true;
6858
+ await writeFile4(statePath, JSON.stringify(payload), "utf8");
6859
+ }
6349
6860
  function getSessionIndexFileSignature(stats) {
6350
6861
  return `${String(stats.mtimeMs)}:${String(stats.size)}`;
6351
6862
  }
@@ -6651,6 +7162,46 @@ async function proxyTranscribe(body, contentType, authToken, accountId) {
6651
7162
  }
6652
7163
  return result;
6653
7164
  }
7165
+ function parseConnectorLogoUrl(rawUrl) {
7166
+ const trimmed = rawUrl.trim();
7167
+ if (!trimmed.startsWith("connectors://")) return null;
7168
+ const rest = trimmed.slice("connectors://".length);
7169
+ const connectorId = (rest.split(/[/?#]/u)[0] ?? "").trim();
7170
+ if (!connectorId) return null;
7171
+ const query = rest.includes("?") ? rest.slice(rest.indexOf("?") + 1).split("#")[0] ?? "" : "";
7172
+ const theme = new URLSearchParams(query).get("theme")?.toLowerCase() === "dark" ? "dark" : "light";
7173
+ return { connectorId, theme };
7174
+ }
7175
+ async function fetchConnectorLogo(rawUrl) {
7176
+ const parsed = parseConnectorLogoUrl(rawUrl);
7177
+ if (!parsed) throw new Error("Unsupported connector logo URL");
7178
+ const auth = await readCodexAuth();
7179
+ if (!auth) throw new Error("No auth token available for connector logo");
7180
+ const endpoint = `https://chatgpt.com/backend-api/aip/connectors/${encodeURIComponent(parsed.connectorId)}/logo?theme=${parsed.theme}`;
7181
+ const response = await fetch(endpoint, {
7182
+ headers: {
7183
+ Authorization: `Bearer ${auth.accessToken}`,
7184
+ originator: "Codex Desktop",
7185
+ "User-Agent": `Codex Desktop/0.1.0 (${process.platform}; ${process.arch})`,
7186
+ ...auth.accountId ? { "ChatGPT-Account-Id": auth.accountId } : {}
7187
+ },
7188
+ signal: AbortSignal.timeout(1e4)
7189
+ });
7190
+ if (!response.ok) throw new Error(`Connector logo fetch failed (${response.status})`);
7191
+ const contentType = response.headers.get("content-type") ?? "";
7192
+ if (contentType.includes("application/json")) {
7193
+ const payload = asRecord5(await response.json());
7194
+ const body = asRecord5(payload?.body);
7195
+ const base64 = readNonEmptyString(body?.base64);
7196
+ const nestedContentType = readNonEmptyString(body?.contentType) ?? readNonEmptyString(body?.content_type);
7197
+ if (!base64 || !nestedContentType) throw new Error("Connector logo response was missing image data");
7198
+ return { contentType: nestedContentType, body: Buffer.from(base64, "base64") };
7199
+ }
7200
+ return {
7201
+ contentType: contentType || "image/png",
7202
+ body: Buffer.from(await response.arrayBuffer())
7203
+ };
7204
+ }
6654
7205
  var STREAM_EVENT_BUFFER_LIMIT = 400;
6655
7206
  var MERGEABLE_ITEM_TYPES = /* @__PURE__ */ new Set([
6656
7207
  "commandExecution",
@@ -7543,6 +8094,19 @@ function createCodexBridgeMiddleware() {
7543
8094
  setJson4(res, 200, terminalManager.getAvailability());
7544
8095
  return;
7545
8096
  }
8097
+ if (req.method === "GET" && url.pathname === "/codex-api/thread-terminal/quick-commands") {
8098
+ const cwd = url.searchParams.get("cwd")?.trim() ?? "";
8099
+ if (!cwd) {
8100
+ setJson4(res, 400, { error: "Missing cwd" });
8101
+ return;
8102
+ }
8103
+ try {
8104
+ setJson4(res, 200, { commands: await listTerminalQuickCommands(cwd) });
8105
+ } catch (error) {
8106
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to load terminal quick commands") });
8107
+ }
8108
+ return;
8109
+ }
7546
8110
  if (req.method === "POST" && url.pathname === "/codex-api/thread-terminal/attach") {
7547
8111
  const availability = terminalManager.getAvailability();
7548
8112
  if (!availability.available) {
@@ -7844,6 +8408,77 @@ function createCodexBridgeMiddleware() {
7844
8408
  res.end(upstream.body);
7845
8409
  return;
7846
8410
  }
8411
+ if (req.method === "GET" && url.pathname === "/codex-api/composio/status") {
8412
+ try {
8413
+ setJson4(res, 200, await readComposioStatus());
8414
+ } catch (error) {
8415
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to read Composio status") });
8416
+ }
8417
+ return;
8418
+ }
8419
+ if (req.method === "GET" && url.pathname === "/codex-api/composio/connectors") {
8420
+ try {
8421
+ const query = url.searchParams.get("query") ?? "";
8422
+ const cursor = url.searchParams.get("cursor")?.trim() ?? null;
8423
+ const limit = parseComposioLimit(url.searchParams.get("limit"));
8424
+ setJson4(res, 200, await listComposioConnectors(query, cursor, limit));
8425
+ } catch (error) {
8426
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to list Composio connectors") });
8427
+ }
8428
+ return;
8429
+ }
8430
+ if (req.method === "GET" && url.pathname === "/codex-api/composio/connector") {
8431
+ try {
8432
+ const slug = url.searchParams.get("slug") ?? "";
8433
+ setJson4(res, 200, await readComposioConnectorDetail(slug));
8434
+ } catch (error) {
8435
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to load Composio connector") });
8436
+ }
8437
+ return;
8438
+ }
8439
+ if (req.method === "POST" && url.pathname === "/codex-api/composio/link") {
8440
+ try {
8441
+ const payload = asRecord5(await readJsonBody(req));
8442
+ const slug = readNonEmptyString(payload?.slug);
8443
+ setJson4(res, 200, await startComposioLink(slug));
8444
+ } catch (error) {
8445
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to start Composio login") });
8446
+ }
8447
+ return;
8448
+ }
8449
+ if (req.method === "POST" && url.pathname === "/codex-api/composio/login") {
8450
+ try {
8451
+ setJson4(res, 200, await startComposioLogin());
8452
+ } catch (error) {
8453
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to start Composio CLI login") });
8454
+ }
8455
+ return;
8456
+ }
8457
+ if (req.method === "POST" && url.pathname === "/codex-api/composio/install") {
8458
+ try {
8459
+ setJson4(res, 200, await installComposioCli());
8460
+ } catch (error) {
8461
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to install Composio CLI") });
8462
+ }
8463
+ return;
8464
+ }
8465
+ if (req.method === "GET" && url.pathname === "/codex-api/connector-logo") {
8466
+ const src = url.searchParams.get("src")?.trim() ?? "";
8467
+ if (!src) {
8468
+ setJson4(res, 400, { error: "Missing src" });
8469
+ return;
8470
+ }
8471
+ try {
8472
+ const logo = await fetchConnectorLogo(src);
8473
+ res.statusCode = 200;
8474
+ res.setHeader("Content-Type", logo.contentType);
8475
+ res.setHeader("Cache-Control", "private, max-age=3600");
8476
+ res.end(logo.body);
8477
+ } catch (error) {
8478
+ setJson4(res, 502, { error: getErrorMessage5(error, "Failed to fetch connector logo") });
8479
+ }
8480
+ return;
8481
+ }
7847
8482
  if (req.method === "POST" && url.pathname === "/codex-api/server-requests/respond") {
7848
8483
  const payload = await readJsonBody(req);
7849
8484
  await appServer.respondToServerRequest(payload);
@@ -7925,21 +8560,13 @@ function createCodexBridgeMiddleware() {
7925
8560
  setJson4(res, 200, { data: state });
7926
8561
  return;
7927
8562
  }
7928
- if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
7929
- setJson4(res, 200, { data: { path: homedir5() } });
8563
+ if (req.method === "GET" && url.pathname === "/codex-api/thread-queue-state") {
8564
+ const state = await readThreadQueueState();
8565
+ setJson4(res, 200, { data: state });
7930
8566
  return;
7931
8567
  }
7932
- if (req.method === "GET" && url.pathname === "/codex-api/github-trending") {
7933
- const sinceRaw = (url.searchParams.get("since") ?? "").trim().toLowerCase();
7934
- const since = sinceRaw === "weekly" ? "weekly" : sinceRaw === "monthly" ? "monthly" : "daily";
7935
- const limitRaw = Number.parseInt((url.searchParams.get("limit") ?? "6").trim(), 10);
7936
- const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(10, limitRaw)) : 6;
7937
- try {
7938
- const data = await fetchGithubTrending(since, limit);
7939
- setJson4(res, 200, { data });
7940
- } catch (error) {
7941
- setJson4(res, 502, { error: getErrorMessage5(error, "Failed to fetch GitHub trending") });
7942
- }
8568
+ if (req.method === "GET" && url.pathname === "/codex-api/home-directory") {
8569
+ setJson4(res, 200, { data: { path: homedir5() } });
7943
8570
  return;
7944
8571
  }
7945
8572
  if (req.method === "POST" && url.pathname === "/codex-api/worktree/create") {
@@ -8201,6 +8828,17 @@ function createCodexBridgeMiddleware() {
8201
8828
  setJson4(res, 200, { ok: true });
8202
8829
  return;
8203
8830
  }
8831
+ if (req.method === "PUT" && url.pathname === "/codex-api/thread-queue-state") {
8832
+ const payload = await readJsonBody(req);
8833
+ const record = asRecord5(payload);
8834
+ if (!record) {
8835
+ setJson4(res, 400, { error: "Invalid body: expected object" });
8836
+ return;
8837
+ }
8838
+ await writeThreadQueueState(normalizeThreadQueueState(record));
8839
+ setJson4(res, 200, { ok: true });
8840
+ return;
8841
+ }
8204
8842
  if (req.method === "POST" && url.pathname === "/codex-api/project-root") {
8205
8843
  const payload = asRecord5(await readJsonBody(req));
8206
8844
  const rawPath = typeof payload?.path === "string" ? payload.path.trim() : "";
@@ -8262,6 +8900,17 @@ function createCodexBridgeMiddleware() {
8262
8900
  setJson4(res, 200, { data: { path: normalizedPath } });
8263
8901
  return;
8264
8902
  }
8903
+ if (req.method === "POST" && url.pathname === "/codex-api/projectless-thread-cwd") {
8904
+ const payload = asRecord5(await readJsonBody(req));
8905
+ const prompt = typeof payload?.prompt === "string" ? payload.prompt : null;
8906
+ try {
8907
+ const directory = await createProjectlessThreadDirectory(prompt);
8908
+ setJson4(res, 200, { data: directory });
8909
+ } catch (error) {
8910
+ setJson4(res, 500, { error: error instanceof Error ? error.message : "Failed to create new chat folder" });
8911
+ }
8912
+ return;
8913
+ }
8265
8914
  if (req.method === "GET" && url.pathname === "/codex-api/project-root-suggestion") {
8266
8915
  const basePath = url.searchParams.get("basePath")?.trim() ?? "";
8267
8916
  if (!basePath) {
@@ -8325,6 +8974,40 @@ function createCodexBridgeMiddleware() {
8325
8974
  }
8326
8975
  return;
8327
8976
  }
8977
+ if (req.method === "GET" && url.pathname === "/codex-api/prompts") {
8978
+ setJson4(res, 200, { data: await listComposerPrompts() });
8979
+ return;
8980
+ }
8981
+ if (req.method === "POST" && url.pathname === "/codex-api/prompts") {
8982
+ const payload = asRecord5(await readJsonBody(req));
8983
+ const name = typeof payload?.name === "string" ? payload.name.trim() : "";
8984
+ const content = typeof payload?.content === "string" ? payload.content : "";
8985
+ if (!name || !content.trim()) {
8986
+ setJson4(res, 400, { error: "Prompt name and content are required" });
8987
+ return;
8988
+ }
8989
+ try {
8990
+ const prompt = await createComposerPromptFile(name, content);
8991
+ setJson4(res, 200, { data: prompt });
8992
+ } catch (error) {
8993
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to create prompt") });
8994
+ }
8995
+ return;
8996
+ }
8997
+ if (req.method === "DELETE" && url.pathname === "/codex-api/prompts") {
8998
+ const promptPath = url.searchParams.get("path")?.trim() ?? "";
8999
+ if (!promptPath) {
9000
+ setJson4(res, 400, { error: "Missing path" });
9001
+ return;
9002
+ }
9003
+ try {
9004
+ const removed = await removeComposerPromptFile(promptPath);
9005
+ setJson4(res, 200, { data: { removed } });
9006
+ } catch (error) {
9007
+ setJson4(res, 400, { error: getErrorMessage5(error, "Failed to remove prompt") });
9008
+ }
9009
+ return;
9010
+ }
8328
9011
  if (req.method === "GET" && url.pathname === "/codex-api/thread-titles") {
8329
9012
  const cache = await readMergedThreadTitleCache();
8330
9013
  setJson4(res, 200, { data: cache });
@@ -8335,6 +9018,11 @@ function createCodexBridgeMiddleware() {
8335
9018
  setJson4(res, 200, { data: { threadIds } });
8336
9019
  return;
8337
9020
  }
9021
+ if (req.method === "GET" && url.pathname === "/codex-api/preferences/first-launch-plugins-card") {
9022
+ const dismissed = await readFirstLaunchPluginsCardDismissed();
9023
+ setJson4(res, 200, { data: { dismissed } });
9024
+ return;
9025
+ }
8338
9026
  if (req.method === "GET" && url.pathname === "/codex-api/thread-automations") {
8339
9027
  const automationsByThreadId = await listThreadHeartbeatAutomations();
8340
9028
  setJson4(res, 200, { data: automationsByThreadId });
@@ -8385,6 +9073,13 @@ function createCodexBridgeMiddleware() {
8385
9073
  setJson4(res, 200, { ok: true });
8386
9074
  return;
8387
9075
  }
9076
+ if (req.method === "PUT" && url.pathname === "/codex-api/preferences/first-launch-plugins-card") {
9077
+ const payload = asRecord5(await readJsonBody(req));
9078
+ const dismissed = payload?.dismissed === true;
9079
+ await writeFirstLaunchPluginsCardDismissed(dismissed);
9080
+ setJson4(res, 200, { ok: true });
9081
+ return;
9082
+ }
8388
9083
  if (req.method === "PUT" && url.pathname === "/codex-api/thread-automation") {
8389
9084
  const payload = asRecord5(await readJsonBody(req));
8390
9085
  const threadId = typeof payload?.threadId === "string" ? payload.threadId.trim() : "";
@@ -8517,7 +9212,13 @@ data: ${JSON.stringify({ ok: true })}
8517
9212
 
8518
9213
  // src/server/authMiddleware.ts
8519
9214
  import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
9215
+ import { existsSync as existsSync5, mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
9216
+ import { homedir as homedir6 } from "os";
9217
+ import { dirname as dirname3, join as join7 } from "path";
8520
9218
  var TOKEN_COOKIE = "portal_session";
9219
+ var SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
9220
+ var SESSION_STORE_FILE = "webui-auth-sessions.json";
9221
+ var MAX_PERSISTED_TOKENS = 128;
8521
9222
  function constantTimeCompare(a, b) {
8522
9223
  const bufA = Buffer.from(a);
8523
9224
  const bufB = Buffer.from(b);
@@ -8565,6 +9266,69 @@ function isTrustedTailscaleIPv6(remote) {
8565
9266
  function isTrustedTailscaleRemote(remote) {
8566
9267
  return isTrustedTailscaleIPv4(remote) || isTrustedTailscaleIPv6(remote);
8567
9268
  }
9269
+ function getCodexHomeDir4() {
9270
+ const codexHome = process.env.CODEX_HOME?.trim();
9271
+ return codexHome && codexHome.length > 0 ? codexHome : join7(homedir6(), ".codex");
9272
+ }
9273
+ function getSessionStorePath() {
9274
+ return join7(getCodexHomeDir4(), SESSION_STORE_FILE);
9275
+ }
9276
+ function readPersistedSessions() {
9277
+ const sessionStorePath = getSessionStorePath();
9278
+ if (!existsSync5(sessionStorePath)) return /* @__PURE__ */ new Map();
9279
+ try {
9280
+ const raw = readFileSync3(sessionStorePath, "utf8");
9281
+ const parsed = JSON.parse(raw);
9282
+ const now = Date.now();
9283
+ const sessions = /* @__PURE__ */ new Map();
9284
+ for (const entry of parsed.tokens ?? []) {
9285
+ const token = typeof entry?.value === "string" ? entry.value : "";
9286
+ const expiresAt = typeof entry?.expiresAt === "number" ? entry.expiresAt : 0;
9287
+ if (!token || !Number.isFinite(expiresAt) || expiresAt <= now) continue;
9288
+ sessions.set(token, expiresAt);
9289
+ }
9290
+ return sessions;
9291
+ } catch {
9292
+ return /* @__PURE__ */ new Map();
9293
+ }
9294
+ }
9295
+ function persistSessions(validTokens) {
9296
+ const sessionStorePath = getSessionStorePath();
9297
+ mkdirSync(dirname3(sessionStorePath), { recursive: true });
9298
+ const tokens = Array.from(validTokens.entries()).sort((left, right) => right[1] - left[1]).slice(0, MAX_PERSISTED_TOKENS).map(([value, expiresAt]) => ({ value, expiresAt }));
9299
+ const tmpPath = `${sessionStorePath}.tmp`;
9300
+ writeFileSync2(tmpPath, `${JSON.stringify({ tokens }, null, 2)}
9301
+ `, { encoding: "utf8", mode: 384 });
9302
+ renameSync(tmpPath, sessionStorePath);
9303
+ }
9304
+ function tryPersistSessions(validTokens) {
9305
+ try {
9306
+ persistSessions(validTokens);
9307
+ } catch (error) {
9308
+ console.warn("[auth] failed to persist login sessions:", error);
9309
+ }
9310
+ }
9311
+ function pruneExpiredSessions(validTokens) {
9312
+ const now = Date.now();
9313
+ let changed = false;
9314
+ for (const [token, expiresAt] of validTokens.entries()) {
9315
+ if (expiresAt > now) continue;
9316
+ validTokens.delete(token);
9317
+ changed = true;
9318
+ }
9319
+ return changed;
9320
+ }
9321
+ function buildSessionCookie(token, expiresAt) {
9322
+ const maxAgeSeconds = Math.max(0, Math.floor((expiresAt - Date.now()) / 1e3));
9323
+ return [
9324
+ `${TOKEN_COOKIE}=${token}`,
9325
+ "Path=/",
9326
+ "HttpOnly",
9327
+ "SameSite=Lax",
9328
+ `Max-Age=${String(maxAgeSeconds)}`,
9329
+ `Expires=${new Date(expiresAt).toUTCString()}`
9330
+ ].join("; ");
9331
+ }
8568
9332
  function isAuthorizedByRequestLike(remoteAddress, hostHeader, cookieHeader, validTokens) {
8569
9333
  const remote = remoteAddress ?? "";
8570
9334
  if (isLocalhostRemote(remote) && isLocalhostHost(hostHeader ?? "")) {
@@ -8575,7 +9339,9 @@ function isAuthorizedByRequestLike(remoteAddress, hostHeader, cookieHeader, vali
8575
9339
  }
8576
9340
  const cookies = parseCookies(cookieHeader);
8577
9341
  const token = cookies[TOKEN_COOKIE];
8578
- return Boolean(token && validTokens.has(token));
9342
+ if (!token) return false;
9343
+ const expiresAt = validTokens.get(token);
9344
+ return typeof expiresAt === "number" && expiresAt > Date.now();
8579
9345
  }
8580
9346
  var LOGIN_PAGE_HTML = `<!DOCTYPE html>
8581
9347
  <html lang="en">
@@ -8619,8 +9385,14 @@ form.addEventListener('submit',async e=>{
8619
9385
  </body>
8620
9386
  </html>`;
8621
9387
  function createAuthSession(password) {
8622
- const validTokens = /* @__PURE__ */ new Set();
9388
+ const validTokens = readPersistedSessions();
9389
+ if (pruneExpiredSessions(validTokens)) {
9390
+ tryPersistSessions(validTokens);
9391
+ }
8623
9392
  const middleware = (req, res, next) => {
9393
+ if (pruneExpiredSessions(validTokens)) {
9394
+ tryPersistSessions(validTokens);
9395
+ }
8624
9396
  if (isAuthorizedByRequestLike(req.socket.remoteAddress, req.headers.host, req.headers.cookie, validTokens)) {
8625
9397
  next();
8626
9398
  return;
@@ -8632,19 +9404,27 @@ function createAuthSession(password) {
8632
9404
  body += chunk;
8633
9405
  });
8634
9406
  req.on("end", () => {
9407
+ let parsed;
9408
+ try {
9409
+ parsed = JSON.parse(body);
9410
+ } catch {
9411
+ res.status(400).json({ error: "Invalid request body" });
9412
+ return;
9413
+ }
9414
+ const provided = typeof parsed.password === "string" ? parsed.password : "";
9415
+ if (!constantTimeCompare(provided, password)) {
9416
+ res.status(401).json({ error: "Invalid password" });
9417
+ return;
9418
+ }
8635
9419
  try {
8636
- const parsed = JSON.parse(body);
8637
- const provided = typeof parsed.password === "string" ? parsed.password : "";
8638
- if (!constantTimeCompare(provided, password)) {
8639
- res.status(401).json({ error: "Invalid password" });
8640
- return;
8641
- }
8642
9420
  const token = randomBytes2(32).toString("hex");
8643
- validTokens.add(token);
8644
- res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
9421
+ const expiresAt = Date.now() + SESSION_TTL_MS;
9422
+ validTokens.set(token, expiresAt);
9423
+ tryPersistSessions(validTokens);
9424
+ res.setHeader("Set-Cookie", buildSessionCookie(token, expiresAt));
8645
9425
  res.json({ ok: true });
8646
9426
  } catch {
8647
- res.status(400).json({ error: "Invalid request body" });
9427
+ res.status(500).json({ error: "Failed to create login session" });
8648
9428
  }
8649
9429
  });
8650
9430
  return;
@@ -8653,8 +9433,10 @@ function createAuthSession(password) {
8653
9433
  const provided = req.path.slice("/password=".length);
8654
9434
  if (constantTimeCompare(provided, password)) {
8655
9435
  const token = randomBytes2(32).toString("hex");
8656
- validTokens.add(token);
8657
- res.setHeader("Set-Cookie", `${TOKEN_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict`);
9436
+ const expiresAt = Date.now() + SESSION_TTL_MS;
9437
+ validTokens.set(token, expiresAt);
9438
+ tryPersistSessions(validTokens);
9439
+ res.setHeader("Set-Cookie", buildSessionCookie(token, expiresAt));
8658
9440
  res.redirect(302, "/");
8659
9441
  return;
8660
9442
  }
@@ -8669,7 +9451,7 @@ function createAuthSession(password) {
8669
9451
  }
8670
9452
 
8671
9453
  // src/server/localBrowseUi.ts
8672
- import { dirname as dirname3, extname as extname2, join as join7 } from "path";
9454
+ import { dirname as dirname4, extname as extname2, join as join8 } from "path";
8673
9455
  import { open, readFile as readFile4, readdir as readdir3, stat as stat5 } from "fs/promises";
8674
9456
  var TEXT_EDITABLE_EXTENSIONS = /* @__PURE__ */ new Set([
8675
9457
  ".txt",
@@ -8817,7 +9599,7 @@ function escapeForInlineScriptString(value) {
8817
9599
  async function getDirectoryItems(localPath) {
8818
9600
  const entries = await readdir3(localPath, { withFileTypes: true });
8819
9601
  const withMeta = await Promise.all(entries.map(async (entry) => {
8820
- const entryPath = join7(localPath, entry.name);
9602
+ const entryPath = join8(localPath, entry.name);
8821
9603
  const entryStat = await stat5(entryPath);
8822
9604
  const editable = !entry.isDirectory() && await isTextEditableFile(entryPath);
8823
9605
  return {
@@ -8838,7 +9620,7 @@ async function getDirectoryItems(localPath) {
8838
9620
  function projectCreationTargetPath(parentPath, newProjectName) {
8839
9621
  const normalizedName = normalizeNewProjectName(newProjectName);
8840
9622
  if (!normalizedName) return "";
8841
- return join7(parentPath, normalizedName);
9623
+ return join8(parentPath, normalizedName);
8842
9624
  }
8843
9625
  function projectCreationButtonLabel(newProjectName) {
8844
9626
  const normalizedName = normalizeNewProjectName(newProjectName);
@@ -8867,7 +9649,7 @@ async function getLocalDirectoryListing(localPath, options = {}) {
8867
9649
  const entries = await readdir3(localPath, { withFileTypes: true });
8868
9650
  const directories = entries.filter((entry) => entry.isDirectory()).map((entry) => ({
8869
9651
  name: entry.name,
8870
- path: join7(localPath, entry.name)
9652
+ path: join8(localPath, entry.name)
8871
9653
  })).filter((entry) => options.showHidden === true || !isHiddenName(entry.name)).sort((a, b) => {
8872
9654
  const aHidden = isHiddenName(a.name);
8873
9655
  const bHidden = isHiddenName(b.name);
@@ -8876,14 +9658,14 @@ async function getLocalDirectoryListing(localPath, options = {}) {
8876
9658
  });
8877
9659
  return {
8878
9660
  path: localPath,
8879
- parentPath: dirname3(localPath),
9661
+ parentPath: dirname4(localPath),
8880
9662
  entries: directories
8881
9663
  };
8882
9664
  }
8883
9665
  async function createDirectoryListingHtml(localPath, options) {
8884
9666
  const newProjectName = normalizeNewProjectName(options?.newProjectName ?? "");
8885
9667
  const items = await getDirectoryItems(localPath);
8886
- const parentPath = dirname3(localPath);
9668
+ const parentPath = dirname4(localPath);
8887
9669
  const rows = items.map((item) => {
8888
9670
  const suffix = item.isDirectory ? "/" : "";
8889
9671
  const editAction = item.editable ? ` <a class="icon-btn" aria-label="Edit ${escapeHtml2(item.name)}" href="${escapeHtml2(toEditHref(item.path, newProjectName))}" title="Edit">\u270F\uFE0F</a>` : "";
@@ -8989,7 +9771,7 @@ async function createDirectoryListingHtml(localPath, options) {
8989
9771
  }
8990
9772
  async function createTextEditorHtml(localPath) {
8991
9773
  const content = await readFile4(localPath, "utf8");
8992
- const parentPath = dirname3(localPath);
9774
+ const parentPath = dirname4(localPath);
8993
9775
  const language = languageForPath(localPath);
8994
9776
  const safeContentLiteral = escapeForInlineScriptString(content);
8995
9777
  return `<!doctype html>
@@ -9058,9 +9840,9 @@ async function createTextEditorHtml(localPath) {
9058
9840
 
9059
9841
  // src/server/httpServer.ts
9060
9842
  import { WebSocketServer } from "ws";
9061
- var __dirname = dirname4(fileURLToPath(import.meta.url));
9062
- var distDir = join8(__dirname, "..", "dist");
9063
- var spaEntryFile = join8(distDir, "index.html");
9843
+ var __dirname = dirname5(fileURLToPath(import.meta.url));
9844
+ var distDir = join9(__dirname, "..", "dist");
9845
+ var spaEntryFile = join9(distDir, "index.html");
9064
9846
  var IMAGE_CONTENT_TYPES = {
9065
9847
  ".avif": "image/avif",
9066
9848
  ".bmp": "image/bmp",
@@ -9229,7 +10011,7 @@ function createServer(options = {}) {
9229
10011
  res.status(404).json({ error: "File not found." });
9230
10012
  }
9231
10013
  });
9232
- const hasFrontendAssets = existsSync4(spaEntryFile);
10014
+ const hasFrontendAssets = existsSync6(spaEntryFile);
9233
10015
  if (hasFrontendAssets) {
9234
10016
  app.use(express.static(distDir));
9235
10017
  }
@@ -9299,26 +10081,26 @@ function generatePassword() {
9299
10081
 
9300
10082
  // src/cli/index.ts
9301
10083
  var program = new Command().name("codexui").description("Web interface for Codex app-server");
9302
- var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
10084
+ var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
9303
10085
  var hasPromptedCloudflaredInstall = false;
9304
10086
  function getCodexHomePath() {
9305
- return process.env.CODEX_HOME?.trim() || join9(homedir6(), ".codex");
10087
+ return process.env.CODEX_HOME?.trim() || join10(homedir7(), ".codex");
9306
10088
  }
9307
10089
  function getCloudflaredPromptMarkerPath() {
9308
- return join9(getCodexHomePath(), ".cloudflared-install-prompted");
10090
+ return join10(getCodexHomePath(), ".cloudflared-install-prompted");
9309
10091
  }
9310
10092
  function hasPromptedCloudflaredInstallPersisted() {
9311
- return existsSync5(getCloudflaredPromptMarkerPath());
10093
+ return existsSync7(getCloudflaredPromptMarkerPath());
9312
10094
  }
9313
10095
  async function persistCloudflaredInstallPrompted() {
9314
10096
  const codexHome = getCodexHomePath();
9315
- mkdirSync(codexHome, { recursive: true });
10097
+ mkdirSync2(codexHome, { recursive: true });
9316
10098
  await writeFile6(getCloudflaredPromptMarkerPath(), `${Date.now()}
9317
10099
  `, "utf8");
9318
10100
  }
9319
10101
  async function readCliVersion() {
9320
10102
  try {
9321
- const packageJsonPath = join9(__dirname2, "..", "package.json");
10103
+ const packageJsonPath = join10(__dirname2, "..", "package.json");
9322
10104
  const raw = await readFile5(packageJsonPath, "utf8");
9323
10105
  const parsed = JSON.parse(raw);
9324
10106
  return typeof parsed.version === "string" ? parsed.version : "unknown";
@@ -9343,8 +10125,8 @@ function resolveCloudflaredCommand() {
9343
10125
  if (canRunCommand("cloudflared", ["--version"])) {
9344
10126
  return "cloudflared";
9345
10127
  }
9346
- const localCandidate = join9(homedir6(), ".local", "bin", "cloudflared");
9347
- if (existsSync5(localCandidate) && canRunCommand(localCandidate, ["--version"])) {
10128
+ const localCandidate = join10(homedir7(), ".local", "bin", "cloudflared");
10129
+ if (existsSync7(localCandidate) && canRunCommand(localCandidate, ["--version"])) {
9348
10130
  return localCandidate;
9349
10131
  }
9350
10132
  return null;
@@ -9397,9 +10179,9 @@ async function ensureCloudflaredInstalledLinux() {
9397
10179
  if (!mappedArch) {
9398
10180
  throw new Error(`cloudflared auto-install is not supported for Linux architecture: ${process.arch}`);
9399
10181
  }
9400
- const userBinDir = join9(homedir6(), ".local", "bin");
9401
- mkdirSync(userBinDir, { recursive: true });
9402
- const destination = join9(userBinDir, "cloudflared");
10182
+ const userBinDir = join10(homedir7(), ".local", "bin");
10183
+ mkdirSync2(userBinDir, { recursive: true });
10184
+ const destination = join10(userBinDir, "cloudflared");
9403
10185
  const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${mappedArch}`;
9404
10186
  console.log("\ncloudflared not found. Installing to ~/.local/bin...\n");
9405
10187
  await downloadFile(downloadUrl, destination);
@@ -9450,7 +10232,7 @@ async function resolveCloudflaredForTunnel() {
9450
10232
  }
9451
10233
  function hasCodexAuth() {
9452
10234
  const codexHome = getCodexHomePath();
9453
- return existsSync5(join9(codexHome, "auth.json"));
10235
+ return existsSync7(join10(codexHome, "auth.json"));
9454
10236
  }
9455
10237
  function ensureCodexInstalled() {
9456
10238
  let codexCommand = resolveCodexCommand();
@@ -9640,7 +10422,7 @@ function listenWithFallback(server, startPort) {
9640
10422
  }
9641
10423
  function getCodexGlobalStatePath2() {
9642
10424
  const codexHome = getCodexHomePath();
9643
- return join9(codexHome, ".codex-global-state.json");
10425
+ return join10(codexHome, ".codex-global-state.json");
9644
10426
  }
9645
10427
  function normalizeUniqueStrings(value) {
9646
10428
  if (!Array.isArray(value)) return [];