codexui-android 0.1.108 → 0.1.110

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
@@ -2047,6 +2047,9 @@ function deriveSkillPathInfo(skillPath, knownPaths = /* @__PURE__ */ new Set())
2047
2047
  function getSkillsInstallDir() {
2048
2048
  return join4(getCodexHomeDir2(), "skills");
2049
2049
  }
2050
+ function getSharedSkillsInstallDir() {
2051
+ return join4(getSkillsInstallDir(), "shared_skills");
2052
+ }
2050
2053
  var DEFAULT_COMMAND_TIMEOUT_MS = 12e4;
2051
2054
  var SKILL_SEARCH_METADATA_LIMIT = 20;
2052
2055
  var SKILL_SEARCH_METADATA_CONCURRENCY = 4;
@@ -2421,9 +2424,8 @@ var startupSyncStatus = {
2421
2424
  lastSuccessAtIso: "",
2422
2425
  lastError: ""
2423
2426
  };
2424
- async function scanInstalledSkillsFromDisk() {
2427
+ async function scanInstalledSkillsFromDir(skillsDir) {
2425
2428
  const map = /* @__PURE__ */ new Map();
2426
- const skillsDir = getSkillsInstallDir();
2427
2429
  try {
2428
2430
  const entries = await readdir(skillsDir, { withFileTypes: true });
2429
2431
  for (const entry of entries) {
@@ -2439,6 +2441,9 @@ async function scanInstalledSkillsFromDisk() {
2439
2441
  }
2440
2442
  return map;
2441
2443
  }
2444
+ async function scanInstalledSkillsFromDisk() {
2445
+ return await scanInstalledSkillsFromDir(getSkillsInstallDir());
2446
+ }
2442
2447
  async function collectInstalledSkillsMap(appServer) {
2443
2448
  const installedMap = await scanInstalledSkillsFromDisk();
2444
2449
  try {
@@ -2700,13 +2705,14 @@ async function writeRemoteSkillsManifest(token, repoOwner, repoName, skills) {
2700
2705
  function toGitHubTokenRemote(repoOwner, repoName, token) {
2701
2706
  return `https://x-access-token:${encodeURIComponent(token)}@github.com/${repoOwner}/${repoName}.git`;
2702
2707
  }
2703
- async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
2704
- const localDir = getSkillsInstallDir();
2708
+ async function ensureSkillsWorkingTreeRepo(repoUrl, branch, options = {}) {
2709
+ const localDir = options.localDir ?? getSkillsInstallDir();
2705
2710
  await mkdir3(localDir, { recursive: true });
2706
2711
  const gitDir = join4(localDir, ".git");
2707
2712
  let hasGitDir = false;
2708
2713
  try {
2709
- hasGitDir = (await stat3(gitDir)).isDirectory();
2714
+ const gitDirStat = await lstat(gitDir);
2715
+ hasGitDir = gitDirStat.isDirectory() || gitDirStat.isFile();
2710
2716
  } catch {
2711
2717
  hasGitDir = false;
2712
2718
  }
@@ -2726,6 +2732,14 @@ async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
2726
2732
  await runCommand2("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
2727
2733
  }
2728
2734
  await runGitFetchWithRefLockRetry(localDir);
2735
+ if (options.overwriteLocalFiles) {
2736
+ await runCommand2("git", ["reset", "--hard"], { cwd: localDir });
2737
+ await runCommand2("git", ["clean", "-fd"], { cwd: localDir });
2738
+ await runCommand2("git", ["checkout", "-B", branch, `origin/${branch}`], { cwd: localDir });
2739
+ await runCommand2("git", ["reset", "--hard", `origin/${branch}`], { cwd: localDir });
2740
+ await runCommand2("git", ["clean", "-fd"], { cwd: localDir });
2741
+ return localDir;
2742
+ }
2729
2743
  try {
2730
2744
  await runCommand2("git", ["merge", "--allow-unrelated-histories", "--no-edit", `origin/${branch}`], { cwd: localDir });
2731
2745
  } catch {
@@ -2734,6 +2748,17 @@ async function ensureSkillsWorkingTreeRepo(repoUrl, branch) {
2734
2748
  }
2735
2749
  await runCommand2("git", ["remote", "set-url", "origin", repoUrl], { cwd: localDir });
2736
2750
  await runGitFetchWithRefLockRetry(localDir);
2751
+ if (options.overwriteLocalFiles) {
2752
+ try {
2753
+ await runCommand2("git", ["reset", "--hard"], { cwd: localDir });
2754
+ } catch {
2755
+ }
2756
+ await runCommand2("git", ["clean", "-fd"], { cwd: localDir });
2757
+ await runCommand2("git", ["checkout", "-B", branch, `origin/${branch}`], { cwd: localDir });
2758
+ await runCommand2("git", ["reset", "--hard", `origin/${branch}`], { cwd: localDir });
2759
+ await runCommand2("git", ["clean", "-fd"], { cwd: localDir });
2760
+ return localDir;
2761
+ }
2737
2762
  const hasLocalChangesBeforeSync = await hasLocalUncommittedChanges(localDir);
2738
2763
  const localMtimesBeforeSync = hasLocalChangesBeforeSync ? await snapshotFileMtimes(localDir) : /* @__PURE__ */ new Map();
2739
2764
  await resolveMergeConflictsByNewerCommit(localDir, branch, localMtimesBeforeSync);
@@ -2994,13 +3019,19 @@ async function syncInstalledSkillsFolderToRepo(token, repoOwner, repoName, _inst
2994
3019
  }
2995
3020
  async function pullInstalledSkillsFolderFromRepo(token, repoOwner, repoName) {
2996
3021
  const remoteUrl = toGitHubTokenRemote(repoOwner, repoName, token);
2997
- const branch = PRIVATE_SYNC_BRANCH;
2998
- await ensureSkillsWorkingTreeRepo(remoteUrl, branch);
3022
+ const isUpstream = isUpstreamSkillsRepo(repoOwner, repoName);
3023
+ const branch = isUpstream ? PUBLIC_UPSTREAM_BRANCH_ANDROID : PRIVATE_SYNC_BRANCH;
3024
+ return await ensureSkillsWorkingTreeRepo(remoteUrl, branch, {
3025
+ ...isUpstream ? { localDir: getSharedSkillsInstallDir() } : {},
3026
+ overwriteLocalFiles: isUpstream
3027
+ });
2999
3028
  }
3000
3029
  async function bootstrapSkillsFromUpstreamIntoLocal() {
3001
3030
  const repoUrl = `https://github.com/${SYNC_UPSTREAM_SKILLS_OWNER}/${SYNC_UPSTREAM_SKILLS_REPO}.git`;
3002
- const branch = getPreferredPublicUpstreamBranch();
3003
- await ensureSkillsWorkingTreeRepo(repoUrl, branch);
3031
+ return await ensureSkillsWorkingTreeRepo(repoUrl, PUBLIC_UPSTREAM_BRANCH_ANDROID, {
3032
+ localDir: getSharedSkillsInstallDir(),
3033
+ overwriteLocalFiles: true
3034
+ });
3004
3035
  }
3005
3036
  async function collectLocalSyncedSkills(appServer) {
3006
3037
  const state = await readSkillsSyncState();
@@ -3305,12 +3336,31 @@ async function handleSkillsRoutes(req, res, url, context) {
3305
3336
  try {
3306
3337
  const state = await readSkillsSyncState();
3307
3338
  if (!state.githubToken || !state.repoOwner || !state.repoName) {
3308
- await bootstrapSkillsFromUpstreamIntoLocal();
3339
+ const repoDir = await bootstrapSkillsFromUpstreamIntoLocal();
3340
+ const localSkills2 = await scanInstalledSkillsFromDir(repoDir);
3341
+ try {
3342
+ await appServer.rpc("skills/list", { forceReload: true });
3343
+ } catch {
3344
+ }
3345
+ setJson3(res, 200, { ok: true, data: { synced: localSkills2.size, source: "upstream" } });
3346
+ return true;
3347
+ }
3348
+ if (isUpstreamSkillsRepo(state.repoOwner, state.repoName)) {
3349
+ const repoDir = await pullInstalledSkillsFolderFromRepo(state.githubToken, state.repoOwner, state.repoName);
3350
+ const localSkills2 = await scanInstalledSkillsFromDir(repoDir);
3351
+ const pulledHead2 = await runCommandWithOutput("git", ["rev-parse", "HEAD"], { cwd: repoDir }).catch(() => "");
3352
+ await writeSkillsSyncState({
3353
+ ...state,
3354
+ lastPullCommitSha: pulledHead2.trim(),
3355
+ lastSyncAttemptCount: 1,
3356
+ lastSyncError: "",
3357
+ lastSyncAtIso: (/* @__PURE__ */ new Date()).toISOString()
3358
+ });
3309
3359
  try {
3310
3360
  await appServer.rpc("skills/list", { forceReload: true });
3311
3361
  } catch {
3312
3362
  }
3313
- setJson3(res, 200, { ok: true, data: { synced: 0, source: "upstream" } });
3363
+ setJson3(res, 200, { ok: true, data: { synced: localSkills2.size, source: "upstream" } });
3314
3364
  return true;
3315
3365
  }
3316
3366
  const remote = await readRemoteSkillsManifest(state.githubToken, state.repoOwner, state.repoName);
@@ -4203,13 +4253,6 @@ var CUSTOM_PROVIDER_ID = "custom-endpoint";
4203
4253
  var OPENCODE_ZEN_PROVIDER_ID = "opencode-zen";
4204
4254
  var OPENCODE_ZEN_BASE_URL = "https://opencode.ai/zen/v1";
4205
4255
  var OPENCODE_ZEN_DEFAULT_MODEL = "big-pickle";
4206
- var OPENCODE_ZEN_FALLBACK_FREE_MODELS = [
4207
- OPENCODE_ZEN_DEFAULT_MODEL,
4208
- "minimax-m2.5-free",
4209
- "hy3-preview-free",
4210
- "nemotron-3-super-free",
4211
- "trinity-large-preview-free"
4212
- ];
4213
4256
  function createDefaultOpenCodeZenFreeModeState() {
4214
4257
  return {
4215
4258
  enabled: true,
@@ -4221,13 +4264,8 @@ function createDefaultOpenCodeZenFreeModeState() {
4221
4264
  providerKeys: {}
4222
4265
  };
4223
4266
  }
4224
- function getOpenCodeZenFreeModelIds(modelIds) {
4225
- const uniqueIds = modelIds.map((id) => id.trim()).filter((id, index, ids) => id.length > 0 && ids.indexOf(id) === index);
4226
- const freeIds = uniqueIds.filter((id) => id.endsWith("-free") || id === OPENCODE_ZEN_DEFAULT_MODEL);
4227
- return freeIds.length > 0 ? freeIds : OPENCODE_ZEN_FALLBACK_FREE_MODELS;
4228
- }
4229
- function shouldCreateDefaultFreeModeStateForMissingAuth(current, hasUsableCodexAuth) {
4230
- return current == null && !hasUsableCodexAuth;
4267
+ function shouldCreateDefaultFreeModeStateForMissingAuth(current, hasUsableCodexAuth2) {
4268
+ return current == null && !hasUsableCodexAuth2;
4231
4269
  }
4232
4270
  function getFreeModeEnvVars(state) {
4233
4271
  if (!state.enabled) return {};
@@ -4358,14 +4396,6 @@ function extractTextParts(value) {
4358
4396
  if (!Array.isArray(value)) return "";
4359
4397
  return value.map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter((part) => part.length > 0).join("\n");
4360
4398
  }
4361
- function buildReasoningOutputItem(reasoningContent) {
4362
- return {
4363
- type: "reasoning",
4364
- id: `rs_${Date.now()}`,
4365
- summary: [{ type: "summary_text", text: reasoningContent }],
4366
- content: []
4367
- };
4368
- }
4369
4399
  function responsesInputToMessages(input, instructions) {
4370
4400
  const messages = [];
4371
4401
  let pendingReasoningContent = "";
@@ -4487,7 +4517,12 @@ function chatCompletionToResponsesFormat(chatResponse, model) {
4487
4517
  });
4488
4518
  }
4489
4519
  if (message.reasoning_content) {
4490
- output.push(buildReasoningOutputItem(message.reasoning_content));
4520
+ output.push({
4521
+ type: "reasoning",
4522
+ id: `rs_${Date.now()}`,
4523
+ summary: [],
4524
+ content: [{ type: "reasoning_text", text: message.reasoning_content }]
4525
+ });
4491
4526
  }
4492
4527
  }
4493
4528
  const usage = chatResponse.usage;
@@ -4553,7 +4588,12 @@ function forwardStreamingTextResponse(upstreamRes, res, model) {
4553
4588
  const messageItem = { type: "message", role: "assistant", content: [{ type: "output_text", text: fullText }], status: "completed" };
4554
4589
  const output = [messageItem];
4555
4590
  if (fullReasoningText) {
4556
- output.push(buildReasoningOutputItem(fullReasoningText));
4591
+ output.push({
4592
+ type: "reasoning",
4593
+ id: `rs_${Date.now()}`,
4594
+ summary: [],
4595
+ content: [{ type: "reasoning_text", text: fullReasoningText }]
4596
+ });
4557
4597
  }
4558
4598
  res.write(`data: {"type":"response.output_text.done","output_index":0,"content_index":0,"text":"${escapedFull}"}
4559
4599
 
@@ -5192,10 +5232,34 @@ function shellQuote(value) {
5192
5232
  return `'${value.replace(/'/g, `'\\''`)}'`;
5193
5233
  }
5194
5234
 
5235
+ // src/pathUtils.ts
5236
+ function stripWindowsDevicePathPrefix(value) {
5237
+ const trimmed = value.trim();
5238
+ if (!trimmed) return "";
5239
+ if (trimmed.startsWith("\\\\?\\UNC\\")) {
5240
+ return `\\\\${trimmed.slice("\\\\?\\UNC\\".length)}`;
5241
+ }
5242
+ if (trimmed.startsWith("\\\\?\\")) {
5243
+ return trimmed.slice("\\\\?\\".length);
5244
+ }
5245
+ return trimmed;
5246
+ }
5247
+ function normalizePathForUi(value) {
5248
+ return stripWindowsDevicePathPrefix(value);
5249
+ }
5250
+ function isWindowsLikePath(value) {
5251
+ return /^[a-z]:[\\/]/iu.test(value) || value.startsWith("\\\\");
5252
+ }
5253
+ function isAbsoluteLikePath(value) {
5254
+ const normalized = normalizePathForUi(value);
5255
+ return normalized.startsWith("/") || isWindowsLikePath(normalized);
5256
+ }
5257
+
5195
5258
  // src/server/codexAppServerBridge.ts
5196
5259
  var COMPOSIO_CONNECTORS_PAGE_LIMIT_MAX = 1e3;
5197
5260
  var PROVIDER_MODELS_FETCH_TIMEOUT_MS = 5e3;
5198
5261
  var THREAD_RESPONSE_TURN_LIMIT = 10;
5262
+ var THREAD_TURN_PAGE_READ_CACHE_TTL_MS = 3e4;
5199
5263
  var THREAD_METHODS_WITH_TURNS = /* @__PURE__ */ new Set(["thread/read", "thread/resume", "thread/fork", "thread/rollback"]);
5200
5264
  var THREAD_SEARCH_FULL_TEXT_THREAD_LIMIT = 100;
5201
5265
  var PROJECTLESS_THREAD_DIRECTORY_MAX_ATTEMPTS = 100;
@@ -5698,11 +5762,13 @@ function trimThreadTurnsInRpcResult(method, result) {
5698
5762
  const thread = asRecord5(record?.thread);
5699
5763
  const turns = Array.isArray(thread?.turns) ? thread.turns : null;
5700
5764
  if (!record || !thread || !turns || turns.length <= THREAD_RESPONSE_TURN_LIMIT) return result;
5765
+ const startTurnIndex = Math.max(0, turns.length - THREAD_RESPONSE_TURN_LIMIT);
5701
5766
  return {
5702
5767
  ...record,
5768
+ threadTurnStartIndex: startTurnIndex,
5703
5769
  thread: {
5704
5770
  ...thread,
5705
- turns: turns.slice(-THREAD_RESPONSE_TURN_LIMIT)
5771
+ turns: turns.slice(startTurnIndex)
5706
5772
  }
5707
5773
  };
5708
5774
  }
@@ -5720,6 +5786,37 @@ function getErrorMessage5(payload, fallback) {
5720
5786
  }
5721
5787
  return fallback;
5722
5788
  }
5789
+ function isUnauthenticatedRateLimitError(error) {
5790
+ const message = getErrorMessage5(error, "").toLowerCase();
5791
+ return message.includes("authentication required") && message.includes("rate limits");
5792
+ }
5793
+ var warnedCodexAuthReadFailures = /* @__PURE__ */ new Set();
5794
+ function getErrorCode(error) {
5795
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code ?? "") : null;
5796
+ }
5797
+ function getCodexAuthReadErrorMessage(error) {
5798
+ return error instanceof Error && error.message.trim().length > 0 ? error.message : String(error);
5799
+ }
5800
+ function warnCodexAuthReadFailure(authPath, error) {
5801
+ const message = getCodexAuthReadErrorMessage(error);
5802
+ const warningKey = `${authPath}:${message}`;
5803
+ if (warnedCodexAuthReadFailures.has(warningKey)) return;
5804
+ warnedCodexAuthReadFailures.add(warningKey);
5805
+ console.warn("[codex-auth] Unable to read Codex auth state", { path: authPath, error: message });
5806
+ }
5807
+ async function hasUsableCodexAuth() {
5808
+ const authPath = getCodexAuthPath();
5809
+ try {
5810
+ const raw = await readFile3(authPath, "utf8");
5811
+ const auth = JSON.parse(raw);
5812
+ return Boolean(auth.tokens?.access_token?.trim() || auth.tokens?.refresh_token?.trim());
5813
+ } catch (error) {
5814
+ if (getErrorCode(error) !== "ENOENT") {
5815
+ warnCodexAuthReadFailure(authPath, error);
5816
+ }
5817
+ return false;
5818
+ }
5819
+ }
5723
5820
  function setJson4(res, statusCode, payload) {
5724
5821
  res.statusCode = statusCode;
5725
5822
  res.setHeader("Content-Type", "application/json; charset=utf-8");
@@ -5770,6 +5867,56 @@ async function createProjectlessThreadDirectory(prompt) {
5770
5867
  }
5771
5868
  throw new Error("Unable to create a unique new chat folder");
5772
5869
  }
5870
+ function normalizeGithubCloneUrl(rawUrl) {
5871
+ const trimmedUrl = rawUrl.trim();
5872
+ if (!trimmedUrl) throw new Error("Missing GitHub repository URL");
5873
+ const sshMatch = trimmedUrl.match(/^git@github\.com:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?$/u);
5874
+ if (sshMatch) {
5875
+ const repoName2 = sshMatch[2];
5876
+ return { url: `git@github.com:${sshMatch[1]}/${repoName2}.git`, repoName: repoName2 };
5877
+ }
5878
+ let parsed;
5879
+ try {
5880
+ parsed = new URL(trimmedUrl);
5881
+ } catch {
5882
+ throw new Error("Enter a valid GitHub repository URL");
5883
+ }
5884
+ if (parsed.hostname.toLowerCase() !== "github.com") {
5885
+ throw new Error("Only github.com repository URLs are supported");
5886
+ }
5887
+ const segments = parsed.pathname.split("/").filter(Boolean);
5888
+ if (segments.length < 2) {
5889
+ throw new Error("Enter a GitHub repository URL with owner and repository name");
5890
+ }
5891
+ const owner = segments[0];
5892
+ const repoName = segments[1].replace(/\.git$/iu, "");
5893
+ if (!/^[A-Za-z0-9_.-]+$/u.test(owner) || !/^[A-Za-z0-9_.-]+$/u.test(repoName)) {
5894
+ throw new Error("GitHub repository owner or name contains unsupported characters");
5895
+ }
5896
+ return { url: `https://github.com/${owner}/${repoName}.git`, repoName };
5897
+ }
5898
+ async function cloneGithubRepositoryIntoBase(rawUrl, rawBasePath) {
5899
+ const basePath = rawBasePath.trim();
5900
+ if (!basePath) throw new Error("Missing clone destination folder");
5901
+ const normalizedBasePath = isAbsolute2(basePath) ? basePath : resolve2(basePath);
5902
+ await ensureRealDirectory(normalizedBasePath, "Clone destination folder");
5903
+ const { url, repoName } = normalizeGithubCloneUrl(rawUrl);
5904
+ const targetPath = join6(normalizedBasePath, repoName);
5905
+ try {
5906
+ await stat4(targetPath);
5907
+ throw new Error(`Destination already exists: ${targetPath}`);
5908
+ } catch (error) {
5909
+ if (error?.code !== "ENOENT") throw error;
5910
+ }
5911
+ try {
5912
+ await runCommand3("git", ["clone", url, targetPath], { cwd: normalizedBasePath, timeoutMs: 5 * 6e4 });
5913
+ } catch (error) {
5914
+ await rm4(targetPath, { recursive: true, force: true }).catch(() => void 0);
5915
+ throw error;
5916
+ }
5917
+ await persistWorkspaceRoot(targetPath, "");
5918
+ return targetPath;
5919
+ }
5773
5920
  function normalizeHeaderValue(value) {
5774
5921
  if (typeof value === "string") {
5775
5922
  const trimmed = value.trim();
@@ -7179,14 +7326,33 @@ async function runCommand3(command, args, options = {}) {
7179
7326
  });
7180
7327
  let stdout = "";
7181
7328
  let stderr = "";
7329
+ let timedOut = false;
7330
+ let closed = false;
7331
+ const timeout = typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? setTimeout(() => {
7332
+ timedOut = true;
7333
+ proc.kill("SIGTERM");
7334
+ setTimeout(() => {
7335
+ if (!closed) proc.kill("SIGKILL");
7336
+ }, 5e3).unref();
7337
+ }, options.timeoutMs) : null;
7338
+ timeout?.unref();
7182
7339
  proc.stdout.on("data", (chunk) => {
7183
7340
  stdout += chunk.toString();
7184
7341
  });
7185
7342
  proc.stderr.on("data", (chunk) => {
7186
7343
  stderr += chunk.toString();
7187
7344
  });
7188
- proc.on("error", reject);
7345
+ proc.on("error", (error) => {
7346
+ if (timeout) clearTimeout(timeout);
7347
+ reject(error);
7348
+ });
7189
7349
  proc.on("close", (code) => {
7350
+ closed = true;
7351
+ if (timeout) clearTimeout(timeout);
7352
+ if (timedOut) {
7353
+ reject(new Error(`Command timed out after ${options.timeoutMs}ms (${command} ${args.join(" ")})`));
7354
+ return;
7355
+ }
7190
7356
  if (code === 0) {
7191
7357
  resolve4();
7192
7358
  return;
@@ -7562,23 +7728,70 @@ function readTomlString(value) {
7562
7728
  function serializeTomlString(value) {
7563
7729
  return JSON.stringify(value);
7564
7730
  }
7731
+ function parseTomlStringArray(value) {
7732
+ const trimmed = value.trim();
7733
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return [];
7734
+ try {
7735
+ const parsed = JSON.parse(trimmed);
7736
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
7737
+ } catch {
7738
+ return [];
7739
+ }
7740
+ }
7741
+ function serializeTomlStringArray(values) {
7742
+ return `[${values.map((value) => serializeTomlString(value)).join(", ")}]`;
7743
+ }
7565
7744
  function parseAutomationToml(raw) {
7566
7745
  const values = {};
7746
+ const extraTomlLines = [];
7747
+ const knownKeys = /* @__PURE__ */ new Set([
7748
+ "version",
7749
+ "id",
7750
+ "kind",
7751
+ "name",
7752
+ "prompt",
7753
+ "status",
7754
+ "rrule",
7755
+ "target_thread_id",
7756
+ "cwds",
7757
+ "created_at",
7758
+ "updated_at"
7759
+ ]);
7760
+ let isInsideExtraTable = false;
7567
7761
  for (const line of raw.split(/\r?\n/u)) {
7568
7762
  const trimmed = line.trim();
7569
- if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
7763
+ if (!trimmed || trimmed.startsWith("#")) continue;
7764
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
7765
+ isInsideExtraTable = true;
7766
+ extraTomlLines.push(trimmed);
7767
+ continue;
7768
+ }
7769
+ if (isInsideExtraTable) {
7770
+ extraTomlLines.push(trimmed);
7771
+ continue;
7772
+ }
7773
+ if (!trimmed.includes("=")) {
7774
+ extraTomlLines.push(trimmed);
7775
+ continue;
7776
+ }
7570
7777
  const separatorIndex = trimmed.indexOf("=");
7571
7778
  const key = trimmed.slice(0, separatorIndex).trim();
7572
7779
  const value = trimmed.slice(separatorIndex + 1).trim();
7573
- if (key) values[key] = value;
7780
+ if (!key) continue;
7781
+ if (knownKeys.has(key)) {
7782
+ values[key] = value;
7783
+ } else {
7784
+ extraTomlLines.push(trimmed);
7785
+ }
7574
7786
  }
7575
7787
  const id = readTomlString(values.id ?? "");
7576
- const kindValue = readTomlString(values.kind ?? "heartbeat");
7788
+ const kindValue = readTomlString(values.kind ?? (values.cwds ? "cron" : "heartbeat"));
7577
7789
  const name = readTomlString(values.name ?? "");
7578
7790
  const prompt = readTomlString(values.prompt ?? "");
7579
7791
  const rrule = readTomlString(values.rrule ?? "");
7580
7792
  const statusValue = readTomlString(values.status ?? "ACTIVE");
7581
7793
  const targetThreadId = readTomlString(values.target_thread_id ?? "") || null;
7794
+ const cwds = parseTomlStringArray(values.cwds ?? "");
7582
7795
  const createdAtMs = Number.parseInt(values.created_at ?? "", 10);
7583
7796
  const updatedAtMs = Number.parseInt(values.updated_at ?? "", 10);
7584
7797
  if (!id || !name || !prompt || !rrule) return null;
@@ -7592,6 +7805,8 @@ function parseAutomationToml(raw) {
7592
7805
  rrule,
7593
7806
  status: statusValue,
7594
7807
  targetThreadId,
7808
+ cwds,
7809
+ extraTomlLines,
7595
7810
  createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : null,
7596
7811
  updatedAtMs: Number.isFinite(updatedAtMs) ? updatedAtMs : null,
7597
7812
  nextRunAtMs: null
@@ -7605,11 +7820,19 @@ function serializeAutomationToml(record) {
7605
7820
  `name = ${serializeTomlString(record.name)}`,
7606
7821
  `prompt = ${serializeTomlString(record.prompt)}`,
7607
7822
  `status = ${serializeTomlString(record.status)}`,
7608
- `rrule = ${serializeTomlString(record.rrule)}`,
7609
- `target_thread_id = ${serializeTomlString(record.targetThreadId ?? "")}`,
7823
+ `rrule = ${serializeTomlString(record.rrule)}`
7824
+ ];
7825
+ if (record.targetThreadId) {
7826
+ lines.push(`target_thread_id = ${serializeTomlString(record.targetThreadId)}`);
7827
+ }
7828
+ if (record.cwds.length > 0) {
7829
+ lines.push(`cwds = ${serializeTomlStringArray(record.cwds)}`);
7830
+ }
7831
+ lines.push(
7610
7832
  `created_at = ${String(record.createdAtMs ?? Date.now())}`,
7611
7833
  `updated_at = ${String(record.updatedAtMs ?? Date.now())}`
7612
- ];
7834
+ );
7835
+ lines.push(...record.extraTomlLines);
7613
7836
  return `${lines.join("\n")}
7614
7837
  `;
7615
7838
  }
@@ -7693,6 +7916,8 @@ async function writeThreadHeartbeatAutomation(input) {
7693
7916
  rrule,
7694
7917
  status: input.status,
7695
7918
  targetThreadId: threadId,
7919
+ cwds: [],
7920
+ extraTomlLines: existing?.extraTomlLines ?? [],
7696
7921
  createdAtMs: existing?.createdAtMs ?? now,
7697
7922
  updatedAtMs: now,
7698
7923
  nextRunAtMs: null
@@ -7721,6 +7946,114 @@ async function deleteThreadHeartbeatAutomation(threadId, automationId = "") {
7721
7946
  await Promise.all(automations.map((automation) => rm4(join6(getCodexAutomationsDir(), automation.id), { recursive: true, force: true })));
7722
7947
  return true;
7723
7948
  }
7949
+ async function listProjectCronAutomations() {
7950
+ const automationRoot = getCodexAutomationsDir();
7951
+ const next = {};
7952
+ let entries;
7953
+ try {
7954
+ entries = await readdir2(automationRoot, { withFileTypes: true });
7955
+ } catch {
7956
+ return next;
7957
+ }
7958
+ for (const entry of entries) {
7959
+ if (!entry.isDirectory()) continue;
7960
+ const automation = await readAutomationRecordFromFile(join6(automationRoot, entry.name, "automation.toml"));
7961
+ if (!automation || automation.kind !== "cron" || automation.cwds.length === 0) continue;
7962
+ for (const cwd of automation.cwds) {
7963
+ next[cwd] = [...next[cwd] ?? [], automation];
7964
+ }
7965
+ }
7966
+ for (const automations of Object.values(next)) {
7967
+ automations.sort((first, second) => {
7968
+ const firstCreatedAt = first.createdAtMs ?? 0;
7969
+ const secondCreatedAt = second.createdAtMs ?? 0;
7970
+ if (firstCreatedAt !== secondCreatedAt) return firstCreatedAt - secondCreatedAt;
7971
+ return first.id.localeCompare(second.id);
7972
+ });
7973
+ }
7974
+ return next;
7975
+ }
7976
+ async function readProjectCronAutomations(projectName) {
7977
+ const all = await listProjectCronAutomations();
7978
+ return all[projectName] ?? [];
7979
+ }
7980
+ async function readProjectCronAutomation(projectName, automationId = "") {
7981
+ const automations = await readProjectCronAutomations(projectName);
7982
+ if (automationId) return automations.find((automation) => automation.id === automationId) ?? null;
7983
+ return automations[0] ?? null;
7984
+ }
7985
+ async function writeProjectCronAutomation(input) {
7986
+ const projectName = input.projectName.trim();
7987
+ const name = input.name.trim();
7988
+ const prompt = input.prompt.trim();
7989
+ const rrule = input.rrule.trim();
7990
+ if (!projectName || !name || !prompt || !rrule) {
7991
+ throw new Error("projectName, name, prompt, and rrule are required");
7992
+ }
7993
+ if (!isAbsoluteLikePath(projectName)) {
7994
+ throw new Error("Project automation cwd must be an absolute path");
7995
+ }
7996
+ const automationRoot = getCodexAutomationsDir();
7997
+ await mkdir4(automationRoot, { recursive: true });
7998
+ const existing = input.id ? await readProjectCronAutomation(projectName, input.id.trim()) : null;
7999
+ const entries = await readdir2(automationRoot, { withFileTypes: true }).catch(() => []);
8000
+ const existingIds = new Set(entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name));
8001
+ const id = existing?.id ?? resolveUniqueAutomationId(existingIds, projectName, name);
8002
+ const automationDir = join6(automationRoot, id);
8003
+ const now = Date.now();
8004
+ const record = {
8005
+ id,
8006
+ kind: "cron",
8007
+ name,
8008
+ prompt,
8009
+ rrule,
8010
+ status: input.status,
8011
+ targetThreadId: null,
8012
+ cwds: Array.from(/* @__PURE__ */ new Set([...existing?.cwds ?? [], projectName])),
8013
+ extraTomlLines: existing?.extraTomlLines ?? [],
8014
+ createdAtMs: existing?.createdAtMs ?? now,
8015
+ updatedAtMs: now,
8016
+ nextRunAtMs: null
8017
+ };
8018
+ await mkdir4(automationDir, { recursive: true });
8019
+ await writeFile4(join6(automationDir, "automation.toml"), serializeAutomationToml(record), "utf8");
8020
+ const memoryPath = join6(automationDir, "memory.md");
8021
+ try {
8022
+ await stat4(memoryPath);
8023
+ } catch {
8024
+ await writeFile4(memoryPath, "", "utf8");
8025
+ }
8026
+ return record;
8027
+ }
8028
+ async function deleteProjectCronAutomation(projectName, automationId = "") {
8029
+ const normalizedProjectName = projectName.trim();
8030
+ const normalizedAutomationId = automationId.trim();
8031
+ if (!normalizedProjectName || !isAbsoluteLikePath(normalizedProjectName)) return false;
8032
+ if (normalizedAutomationId) {
8033
+ const automation = await readProjectCronAutomation(normalizedProjectName, normalizedAutomationId);
8034
+ if (!automation) return false;
8035
+ const remainingCwds = automation.cwds.filter((cwd) => cwd !== normalizedProjectName);
8036
+ if (remainingCwds.length > 0) {
8037
+ const record = { ...automation, cwds: remainingCwds, updatedAtMs: Date.now() };
8038
+ await writeFile4(join6(getCodexAutomationsDir(), automation.id, "automation.toml"), serializeAutomationToml(record), "utf8");
8039
+ } else {
8040
+ await rm4(join6(getCodexAutomationsDir(), automation.id), { recursive: true, force: true });
8041
+ }
8042
+ return true;
8043
+ }
8044
+ const automations = await readProjectCronAutomations(normalizedProjectName);
8045
+ if (automations.length === 0) return false;
8046
+ await Promise.all(automations.map(async (automation) => {
8047
+ const remainingCwds = automation.cwds.filter((cwd) => cwd !== normalizedProjectName);
8048
+ if (remainingCwds.length > 0) {
8049
+ const record = { ...automation, cwds: remainingCwds, updatedAtMs: Date.now() };
8050
+ await writeFile4(join6(getCodexAutomationsDir(), automation.id, "automation.toml"), serializeAutomationToml(record), "utf8");
8051
+ return;
8052
+ }
8053
+ await rm4(join6(getCodexAutomationsDir(), automation.id), { recursive: true, force: true });
8054
+ }));
8055
+ return true;
8056
+ }
7724
8057
  var MAX_THREAD_TITLES = 500;
7725
8058
  var EMPTY_THREAD_TITLE_CACHE = { titles: {}, order: [] };
7726
8059
  var PINNED_THREAD_IDS_KEY = "pinned-thread-ids";
@@ -8463,6 +8796,8 @@ var AppServerProcess = class {
8463
8796
  this.appServerArgs = buildAppServerArgs();
8464
8797
  this.streamEventsByThreadId = /* @__PURE__ */ new Map();
8465
8798
  this.lastThreadReadSnapshotByThreadId = /* @__PURE__ */ new Map();
8799
+ this.threadTurnPageReadCacheByThreadId = /* @__PURE__ */ new Map();
8800
+ this.threadTurnPageReadPromiseByThreadId = /* @__PURE__ */ new Map();
8466
8801
  this.capturedItemsByThreadId = /* @__PURE__ */ new Map();
8467
8802
  this.liveStateCache = /* @__PURE__ */ new Map();
8468
8803
  this.chatgptAuthRefreshPromise = null;
@@ -8575,7 +8910,10 @@ var AppServerProcess = class {
8575
8910
  this.recordStreamEvent(notification);
8576
8911
  this.captureItemFromNotification(notification);
8577
8912
  const nThreadId = this.extractThreadIdFromParams(notification.params);
8578
- if (nThreadId) this.invalidateLiveStateCache(nThreadId);
8913
+ if (nThreadId) {
8914
+ this.invalidateLiveStateCache(nThreadId);
8915
+ this.threadTurnPageReadCacheByThreadId.delete(nThreadId);
8916
+ }
8579
8917
  for (const listener of this.notificationListeners) {
8580
8918
  listener(notification);
8581
8919
  }
@@ -8619,10 +8957,33 @@ var AppServerProcess = class {
8619
8957
  }
8620
8958
  storeThreadReadSnapshot(threadId, snapshot) {
8621
8959
  this.lastThreadReadSnapshotByThreadId.set(threadId, snapshot);
8960
+ this.threadTurnPageReadCacheByThreadId.delete(threadId);
8622
8961
  }
8623
8962
  getLastThreadReadSnapshot(threadId) {
8624
8963
  return this.lastThreadReadSnapshotByThreadId.get(threadId) ?? null;
8625
8964
  }
8965
+ async readThreadForTurnPage(threadId) {
8966
+ const now = Date.now();
8967
+ const cached = this.threadTurnPageReadCacheByThreadId.get(threadId);
8968
+ if (cached && cached.expiresAt > now) return cached.result;
8969
+ if (cached) this.threadTurnPageReadCacheByThreadId.delete(threadId);
8970
+ const pending = this.threadTurnPageReadPromiseByThreadId.get(threadId);
8971
+ if (pending) return pending;
8972
+ const promise = this.rpc("thread/read", {
8973
+ threadId,
8974
+ includeTurns: true
8975
+ }).then((result) => {
8976
+ this.threadTurnPageReadCacheByThreadId.set(threadId, {
8977
+ result,
8978
+ expiresAt: Date.now() + THREAD_TURN_PAGE_READ_CACHE_TTL_MS
8979
+ });
8980
+ return result;
8981
+ }).finally(() => {
8982
+ this.threadTurnPageReadPromiseByThreadId.delete(threadId);
8983
+ });
8984
+ this.threadTurnPageReadPromiseByThreadId.set(threadId, promise);
8985
+ return promise;
8986
+ }
8626
8987
  cacheLiveState(threadId, data, turnCount, sessionSize) {
8627
8988
  this.liveStateCache.set(threadId, { data, turnCount, sessionSize });
8628
8989
  }
@@ -9291,11 +9652,10 @@ async function buildThreadSearchIndex(appServer) {
9291
9652
  const docsById = new Map(docs.map((doc) => [doc.id, doc]));
9292
9653
  return { docsById };
9293
9654
  }
9294
- function createCodexBridgeMiddleware(options = {}) {
9655
+ function createCodexBridgeMiddleware() {
9295
9656
  const { appServer, terminalManager, methodCatalog, telegramBridge, backendQueueProcessor } = getSharedBridgeState();
9296
9657
  let threadSearchIndex = null;
9297
9658
  let threadSearchIndexPromise = null;
9298
- let backgroundServicesStarted = false;
9299
9659
  async function getThreadSearchIndex() {
9300
9660
  if (threadSearchIndex) return threadSearchIndex;
9301
9661
  if (!threadSearchIndexPromise) {
@@ -9308,21 +9668,14 @@ function createCodexBridgeMiddleware(options = {}) {
9308
9668
  }
9309
9669
  return threadSearchIndexPromise;
9310
9670
  }
9311
- function startBackgroundServices() {
9312
- if (backgroundServicesStarted) return;
9313
- backgroundServicesStarted = true;
9314
- void initializeSkillsSyncOnStartup(appServer);
9315
- void readTelegramBridgeConfig().then((config) => {
9316
- if (!config.botToken) return;
9317
- telegramBridge.configureToken(config.botToken);
9318
- telegramBridge.configureAllowedUserIds(config.allowedUserIds);
9319
- telegramBridge.start();
9320
- }).catch(() => {
9321
- });
9322
- }
9323
- if (options.startBackgroundServices !== false) {
9324
- startBackgroundServices();
9325
- }
9671
+ void initializeSkillsSyncOnStartup(appServer);
9672
+ void readTelegramBridgeConfig().then((config) => {
9673
+ if (!config.botToken) return;
9674
+ telegramBridge.configureToken(config.botToken);
9675
+ telegramBridge.configureAllowedUserIds(config.allowedUserIds);
9676
+ telegramBridge.start();
9677
+ }).catch(() => {
9678
+ });
9326
9679
  const middleware = async (req, res, next) => {
9327
9680
  const requestStartNs = process.hrtime.bigint();
9328
9681
  const rawUrl = req.url ?? "";
@@ -9413,11 +9766,7 @@ function createCodexBridgeMiddleware(options = {}) {
9413
9766
  }
9414
9767
  if (url.pathname.startsWith("/codex-api/free-mode")) {
9415
9768
  let readFreeModeState2 = function() {
9416
- const state = ensureDefaultFreeModeStateForMissingAuthSync(statePath) ?? { enabled: false, apiKey: null, model: FREE_MODE_DEFAULT_MODEL };
9417
- if (state.provider === "opencode-zen" && !state.model?.trim()) {
9418
- return { ...state, model: OPENCODE_ZEN_DEFAULT_MODEL };
9419
- }
9420
- return state;
9769
+ return ensureDefaultFreeModeStateForMissingAuthSync(statePath) ?? { enabled: false, apiKey: null, model: FREE_MODE_DEFAULT_MODEL };
9421
9770
  };
9422
9771
  var readFreeModeState = readFreeModeState2;
9423
9772
  const statePath = join6(getCodexHomeDir3(), FREE_MODE_STATE_FILE);
@@ -9569,7 +9918,7 @@ function createCodexBridgeMiddleware(options = {}) {
9569
9918
  if (resolvedKey) {
9570
9919
  prevKeys[providerType] = resolvedKey;
9571
9920
  }
9572
- const resolvedModel = providerType === "openrouter" ? current.model || FREE_MODE_DEFAULT_MODEL : providerType === "custom" ? await fetchCustomEndpointDefaultModel(baseUrl, resolvedKey) : OPENCODE_ZEN_DEFAULT_MODEL;
9921
+ const resolvedModel = providerType === "openrouter" ? current.model || FREE_MODE_DEFAULT_MODEL : providerType === "custom" ? await fetchCustomEndpointDefaultModel(baseUrl, resolvedKey) : "";
9573
9922
  const state = {
9574
9923
  enabled: true,
9575
9924
  apiKey: resolvedKey,
@@ -9714,7 +10063,20 @@ function createCodexBridgeMiddleware(options = {}) {
9714
10063
  setJson4(res, 400, { error: "Invalid body: expected { method, params? }" });
9715
10064
  return;
9716
10065
  }
9717
- const rpcResult = await callRpcWithArchiveRecovery(appServer, body.method, body.params ?? null);
10066
+ if (body.method === "account/rateLimits/read" && !await hasUsableCodexAuth()) {
10067
+ setJson4(res, 200, { result: null });
10068
+ return;
10069
+ }
10070
+ let rpcResult;
10071
+ try {
10072
+ rpcResult = await callRpcWithArchiveRecovery(appServer, body.method, body.params ?? null);
10073
+ } catch (error) {
10074
+ if (body.method === "account/rateLimits/read" && isUnauthenticatedRateLimitError(error)) {
10075
+ setJson4(res, 200, { result: null });
10076
+ return;
10077
+ }
10078
+ throw error;
10079
+ }
9718
10080
  const trimmedResult = trimThreadTurnsInRpcResult(body.method, rpcResult);
9719
10081
  const sanitizedResult = await sanitizeThreadTurnsInlinePayloads(body.method, trimmedResult);
9720
10082
  const result = THREAD_METHODS_WITH_TURNS.has(body.method) ? await mergeSessionSkillInputsIntoThreadResult(sanitizedResult) : sanitizedResult;
@@ -9729,6 +10091,61 @@ function createCodexBridgeMiddleware(options = {}) {
9729
10091
  setJson4(res, 200, { result });
9730
10092
  return;
9731
10093
  }
10094
+ if (req.method === "GET" && url.pathname === "/codex-api/thread-turn-page") {
10095
+ try {
10096
+ const threadId = url.searchParams.get("threadId")?.trim() ?? "";
10097
+ const beforeTurnId = url.searchParams.get("beforeTurnId")?.trim() ?? "";
10098
+ const limitRaw = url.searchParams.get("limit")?.trim() ?? String(THREAD_RESPONSE_TURN_LIMIT);
10099
+ const limit = Math.max(1, Math.min(50, Number.parseInt(limitRaw, 10) || THREAD_RESPONSE_TURN_LIMIT));
10100
+ if (!threadId) {
10101
+ setJson4(res, 400, { error: "Missing threadId" });
10102
+ return;
10103
+ }
10104
+ const threadReadResult = await appServer.readThreadForTurnPage(threadId);
10105
+ const record = asRecord5(threadReadResult);
10106
+ const thread = asRecord5(record?.thread);
10107
+ if (!record || !thread) {
10108
+ setJson4(res, 502, { error: "thread/read returned an invalid thread response" });
10109
+ return;
10110
+ }
10111
+ const turns = Array.isArray(thread.turns) ? thread.turns : [];
10112
+ const beforeIndex = beforeTurnId ? turns.findIndex((turn) => asRecord5(turn)?.id === beforeTurnId) : turns.length;
10113
+ if (beforeTurnId && beforeIndex < 0) {
10114
+ setJson4(res, 200, {
10115
+ result: {
10116
+ ...record,
10117
+ thread: {
10118
+ ...thread,
10119
+ turns: []
10120
+ }
10121
+ },
10122
+ startTurnIndex: 0,
10123
+ hasMoreOlder: false
10124
+ });
10125
+ return;
10126
+ }
10127
+ const endIndex = beforeIndex;
10128
+ const startIndex = Math.max(0, endIndex - limit);
10129
+ const pageTurns = turns.slice(startIndex, endIndex);
10130
+ const pagedResult = {
10131
+ ...record,
10132
+ thread: {
10133
+ ...thread,
10134
+ turns: pageTurns
10135
+ }
10136
+ };
10137
+ const sanitized = await sanitizeThreadTurnsInlinePayloads("thread/read", pagedResult);
10138
+ const result = await mergeSessionSkillInputsIntoThreadResult(sanitized);
10139
+ setJson4(res, 200, {
10140
+ result,
10141
+ startTurnIndex: startIndex,
10142
+ hasMoreOlder: startIndex > 0
10143
+ });
10144
+ } catch (error) {
10145
+ setJson4(res, 500, { error: getErrorMessage5(error, "Failed to load earlier thread messages") });
10146
+ }
10147
+ return;
10148
+ }
9732
10149
  if (req.method === "GET" && url.pathname === "/codex-api/thread-file-change-fallback") {
9733
10150
  const threadId = url.searchParams.get("threadId")?.trim() ?? "";
9734
10151
  if (!threadId) {
@@ -10018,28 +10435,21 @@ function createCodexBridgeMiddleware(options = {}) {
10018
10435
  try {
10019
10436
  const modelsUrl = "https://opencode.ai/zen/v1/models";
10020
10437
  const headers = {};
10021
- const hasZenApiKey = Boolean(fmState.apiKey && fmState.apiKey !== "dummy");
10022
- if (hasZenApiKey) {
10438
+ if (fmState.apiKey && fmState.apiKey !== "dummy") {
10023
10439
  headers["Authorization"] = `Bearer ${fmState.apiKey}`;
10024
10440
  }
10025
10441
  const resp = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(8e3) });
10026
10442
  if (resp.ok) {
10027
10443
  const json = await resp.json();
10028
10444
  const allIds = (json.data ?? []).map((m) => m.id).filter(Boolean);
10029
- setJson4(res, 200, {
10030
- data: getOpenCodeZenFreeModelIds(allIds),
10031
- exclusive: true,
10032
- source: "opencode-zen"
10033
- });
10445
+ const freeIds = allIds.filter((id) => id.endsWith("-free") || id === "big-pickle");
10446
+ const paidIds = allIds.filter((id) => !id.endsWith("-free") && id !== "big-pickle");
10447
+ setJson4(res, 200, { data: [...freeIds, ...paidIds], exclusive: true, source: "opencode-zen" });
10034
10448
  return;
10035
10449
  }
10036
10450
  } catch {
10037
10451
  }
10038
- setJson4(res, 200, {
10039
- data: getOpenCodeZenFreeModelIds([]),
10040
- exclusive: true,
10041
- source: "opencode-zen"
10042
- });
10452
+ setJson4(res, 200, { data: ["big-pickle", "minimax-m2.5-free", "nemotron-3-super-free", "trinity-large-preview-free"], exclusive: true, source: "opencode-zen" });
10043
10453
  return;
10044
10454
  }
10045
10455
  if (fmState.provider === "custom" && fmState.customBaseUrl) {
@@ -10342,7 +10752,7 @@ function createCodexBridgeMiddleware(options = {}) {
10342
10752
  if (currentBranch && !branchActivityByName.has(currentBranch)) {
10343
10753
  branchActivityByName.set(currentBranch, { timestamp: Number.MAX_SAFE_INTEGER, isRemote: false });
10344
10754
  }
10345
- const options2 = Array.from(branchActivityByName.entries()).map(([value, metadata]) => ({
10755
+ const options = Array.from(branchActivityByName.entries()).map(([value, metadata]) => ({
10346
10756
  value,
10347
10757
  label: value,
10348
10758
  isCurrent: value === currentBranch,
@@ -10356,7 +10766,7 @@ function createCodexBridgeMiddleware(options = {}) {
10356
10766
  setJson4(res, 200, {
10357
10767
  data: {
10358
10768
  ...state,
10359
- options: options2
10769
+ options
10360
10770
  }
10361
10771
  });
10362
10772
  } catch (error) {
@@ -10602,6 +11012,18 @@ function createCodexBridgeMiddleware(options = {}) {
10602
11012
  setJson4(res, 200, { data: { path: normalizedPath } });
10603
11013
  return;
10604
11014
  }
11015
+ if (req.method === "POST" && url.pathname === "/codex-api/github-clone") {
11016
+ const payload = asRecord5(await readJsonBody2(req));
11017
+ const repoUrl = typeof payload?.url === "string" ? payload.url.trim() : "";
11018
+ const basePath = typeof payload?.basePath === "string" ? payload.basePath.trim() : "";
11019
+ try {
11020
+ const clonedPath = await cloneGithubRepositoryIntoBase(repoUrl, basePath);
11021
+ setJson4(res, 200, { data: { path: clonedPath } });
11022
+ } catch (error) {
11023
+ setJson4(res, 400, { error: error instanceof Error ? error.message : "Failed to clone GitHub repository" });
11024
+ }
11025
+ return;
11026
+ }
10605
11027
  if (req.method === "POST" && url.pathname === "/codex-api/projectless-thread-cwd") {
10606
11028
  const payload = asRecord5(await readJsonBody2(req));
10607
11029
  const prompt = typeof payload?.prompt === "string" ? payload.prompt : null;
@@ -10730,6 +11152,11 @@ function createCodexBridgeMiddleware(options = {}) {
10730
11152
  setJson4(res, 200, { data: automationsByThreadId });
10731
11153
  return;
10732
11154
  }
11155
+ if (req.method === "GET" && url.pathname === "/codex-api/project-automations") {
11156
+ const automationsByProjectName = await listProjectCronAutomations();
11157
+ setJson4(res, 200, { data: automationsByProjectName });
11158
+ return;
11159
+ }
10733
11160
  if (req.method === "GET" && url.pathname === "/codex-api/thread-automation") {
10734
11161
  const threadId = url.searchParams.get("threadId")?.trim() ?? "";
10735
11162
  const automationId = url.searchParams.get("automationId")?.trim() ?? "";
@@ -10741,6 +11168,17 @@ function createCodexBridgeMiddleware(options = {}) {
10741
11168
  setJson4(res, 200, { data: automation });
10742
11169
  return;
10743
11170
  }
11171
+ if (req.method === "GET" && url.pathname === "/codex-api/project-automation") {
11172
+ const projectName = url.searchParams.get("projectName")?.trim() ?? "";
11173
+ const automationId = url.searchParams.get("automationId")?.trim() ?? "";
11174
+ if (!projectName) {
11175
+ setJson4(res, 400, { error: "Missing projectName" });
11176
+ return;
11177
+ }
11178
+ const automation = automationId ? await readProjectCronAutomation(projectName, automationId) : await readProjectCronAutomations(projectName);
11179
+ setJson4(res, 200, { data: automation });
11180
+ return;
11181
+ }
10744
11182
  if (req.method === "POST" && url.pathname === "/codex-api/thread-search") {
10745
11183
  const payload = asRecord5(await readJsonBody2(req));
10746
11184
  const query = typeof payload?.query === "string" ? payload.query.trim() : "";
@@ -10799,6 +11237,26 @@ function createCodexBridgeMiddleware(options = {}) {
10799
11237
  setJson4(res, 200, { data: automation });
10800
11238
  return;
10801
11239
  }
11240
+ if (req.method === "PUT" && url.pathname === "/codex-api/project-automation") {
11241
+ const payload = asRecord5(await readJsonBody2(req));
11242
+ const projectName = typeof payload?.projectName === "string" ? payload.projectName.trim() : "";
11243
+ const id = typeof payload?.id === "string" ? payload.id.trim() : "";
11244
+ const name = typeof payload?.name === "string" ? payload.name.trim() : "";
11245
+ const prompt = typeof payload?.prompt === "string" ? payload.prompt.trim() : "";
11246
+ const rrule = typeof payload?.rrule === "string" ? payload.rrule.trim() : "";
11247
+ const status = payload?.status === "PAUSED" ? "PAUSED" : "ACTIVE";
11248
+ if (!projectName || !name || !prompt || !rrule) {
11249
+ setJson4(res, 400, { error: "projectName, name, prompt, and rrule are required" });
11250
+ return;
11251
+ }
11252
+ if (!isAbsoluteLikePath(projectName)) {
11253
+ setJson4(res, 400, { error: "Project automation cwd must be an absolute path" });
11254
+ return;
11255
+ }
11256
+ const automation = await writeProjectCronAutomation({ projectName, id, name, prompt, rrule, status });
11257
+ setJson4(res, 200, { data: automation });
11258
+ return;
11259
+ }
10802
11260
  if (req.method === "POST" && url.pathname === "/codex-api/thread-automation/run") {
10803
11261
  const payload = asRecord5(await readJsonBody2(req));
10804
11262
  const threadId = typeof payload?.threadId === "string" ? payload.threadId.trim() : "";
@@ -10828,6 +11286,17 @@ function createCodexBridgeMiddleware(options = {}) {
10828
11286
  setJson4(res, 200, { data: { removed } });
10829
11287
  return;
10830
11288
  }
11289
+ if (req.method === "DELETE" && url.pathname === "/codex-api/project-automation") {
11290
+ const projectName = url.searchParams.get("projectName")?.trim() ?? "";
11291
+ const automationId = url.searchParams.get("automationId")?.trim() ?? "";
11292
+ if (!projectName) {
11293
+ setJson4(res, 400, { error: "Missing projectName" });
11294
+ return;
11295
+ }
11296
+ const removed = await deleteProjectCronAutomation(projectName, automationId);
11297
+ setJson4(res, 200, { data: { removed } });
11298
+ return;
11299
+ }
10831
11300
  if (req.method === "POST" && url.pathname === "/codex-api/telegram/configure-bot") {
10832
11301
  const payload = asRecord5(await readJsonBody2(req));
10833
11302
  const botToken = typeof payload?.botToken === "string" ? payload.botToken.trim() : "";
@@ -10913,7 +11382,6 @@ data: ${JSON.stringify({ ok: true })}
10913
11382
  backendQueueProcessor.dispose();
10914
11383
  appServer.dispose();
10915
11384
  };
10916
- middleware.startBackgroundServices = startBackgroundServices;
10917
11385
  middleware.subscribeNotifications = (listener) => {
10918
11386
  const unsubscribeAppServer = appServer.onNotification((notification) => {
10919
11387
  listener({
@@ -11615,9 +12083,7 @@ function readWildcardPathParam(value) {
11615
12083
  }
11616
12084
  function createServer(options = {}) {
11617
12085
  const app = express();
11618
- const bridge = createCodexBridgeMiddleware({
11619
- startBackgroundServices: options.deferBridgeBackgroundServices !== true
11620
- });
12086
+ const bridge = createCodexBridgeMiddleware();
11621
12087
  const authSession = options.password ? createAuthSession(options.password) : null;
11622
12088
  if (authSession) {
11623
12089
  app.use(authSession.middleware);
@@ -11763,7 +12229,6 @@ function createServer(options = {}) {
11763
12229
  return {
11764
12230
  app,
11765
12231
  dispose: () => bridge.dispose(),
11766
- startBridgeBackgroundServices: () => bridge.startBackgroundServices(),
11767
12232
  attachWebSocket: (server) => {
11768
12233
  const wss = new WebSocketServer({ noServer: true });
11769
12234
  server.on("upgrade", (req, socket, head) => {
@@ -12243,15 +12708,10 @@ async function startServer(options) {
12243
12708
  const passwordResolution = resolvePassword(options.password);
12244
12709
  const password = passwordResolution.password;
12245
12710
  const generatedPasswordPath = password && passwordResolution.generated ? await persistGeneratedPassword(password) : null;
12246
- const { app, dispose, attachWebSocket, startBridgeBackgroundServices } = createServer({
12247
- password,
12248
- deferBridgeBackgroundServices: true
12249
- });
12711
+ const { app, dispose, attachWebSocket } = createServer({ password });
12250
12712
  const server = createServer2(app);
12251
12713
  attachWebSocket(server);
12252
12714
  const port = await listenWithFallback(server, requestedPort);
12253
- process.env.CODEXUI_SERVER_PORT = String(port);
12254
- startBridgeBackgroundServices();
12255
12715
  let tunnelChild = null;
12256
12716
  let tunnelUrl = null;
12257
12717
  if (options.tunnel) {