codex-to-im 1.0.59 → 1.0.60

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.
@@ -4011,7 +4011,7 @@ var require_utils2 = __commonJS({
4011
4011
  // node_modules/qrcode/lib/renderer/png.js
4012
4012
  var require_png2 = __commonJS({
4013
4013
  "node_modules/qrcode/lib/renderer/png.js"(exports) {
4014
- var fs9 = __require("fs");
4014
+ var fs10 = __require("fs");
4015
4015
  var PNG = require_png().PNG;
4016
4016
  var Utils = require_utils2();
4017
4017
  exports.render = function render(qrData, options) {
@@ -4063,7 +4063,7 @@ var require_png2 = __commonJS({
4063
4063
  called = true;
4064
4064
  cb.apply(null, args);
4065
4065
  };
4066
- const stream = fs9.createWriteStream(path10);
4066
+ const stream = fs10.createWriteStream(path10);
4067
4067
  stream.on("error", done);
4068
4068
  stream.on("close", done);
4069
4069
  exports.renderToFileStream(stream, qrData, options);
@@ -4130,9 +4130,9 @@ var require_utf8 = __commonJS({
4130
4130
  cb = options;
4131
4131
  options = void 0;
4132
4132
  }
4133
- const fs9 = __require("fs");
4133
+ const fs10 = __require("fs");
4134
4134
  const utf8 = exports.render(qrData, options);
4135
- fs9.writeFile(path10, utf8, cb);
4135
+ fs10.writeFile(path10, utf8, cb);
4136
4136
  };
4137
4137
  }
4138
4138
  });
@@ -4306,10 +4306,10 @@ var require_svg = __commonJS({
4306
4306
  cb = options;
4307
4307
  options = void 0;
4308
4308
  }
4309
- const fs9 = __require("fs");
4309
+ const fs10 = __require("fs");
4310
4310
  const svgTag = exports.render(qrData, options);
4311
4311
  const xmlStr = '<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' + svgTag;
4312
- fs9.writeFile(path10, xmlStr, cb);
4312
+ fs10.writeFile(path10, xmlStr, cb);
4313
4313
  };
4314
4314
  }
4315
4315
  });
@@ -4566,12 +4566,13 @@ var require_lib = __commonJS({
4566
4566
 
4567
4567
  // src/ui-server.ts
4568
4568
  import http from "node:http";
4569
- import crypto5 from "node:crypto";
4569
+ import crypto8 from "node:crypto";
4570
4570
  import net from "node:net";
4571
4571
  import os5 from "node:os";
4572
4572
 
4573
4573
  // src/config.ts
4574
- import fs from "node:fs";
4574
+ import fs2 from "node:fs";
4575
+ import crypto2 from "node:crypto";
4575
4576
  import os from "node:os";
4576
4577
  import path from "node:path";
4577
4578
 
@@ -4598,10 +4599,109 @@ function normalizeChannelId(value) {
4598
4599
  return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "channel";
4599
4600
  }
4600
4601
 
4602
+ // src/file-lock.ts
4603
+ import crypto from "node:crypto";
4604
+ import fs from "node:fs";
4605
+ var DEFAULT_LOCK_TIMEOUT_MS = 5e3;
4606
+ var DEFAULT_STALE_LOCK_MS = 3e4;
4607
+ var LOCK_RETRY_INTERVAL_MS = 25;
4608
+ var WAIT_ARRAY = new Int32Array(new SharedArrayBuffer(4));
4609
+ var ACTIVE_LOCKS = /* @__PURE__ */ new Map();
4610
+ function sleepSync(ms) {
4611
+ Atomics.wait(WAIT_ARRAY, 0, 0, Math.max(1, ms));
4612
+ }
4613
+ function isProcessAlive(pid) {
4614
+ if (!Number.isInteger(pid) || pid <= 0) return false;
4615
+ try {
4616
+ process.kill(pid, 0);
4617
+ return true;
4618
+ } catch (error) {
4619
+ return error.code === "EPERM";
4620
+ }
4621
+ }
4622
+ function removeStaleLock(lockPath, staleAfterMs) {
4623
+ try {
4624
+ const stat = fs.statSync(lockPath);
4625
+ if (Date.now() - stat.mtimeMs < staleAfterMs) return false;
4626
+ let ownerPid = 0;
4627
+ try {
4628
+ const parsed = JSON.parse(fs.readFileSync(lockPath, "utf8"));
4629
+ ownerPid = typeof parsed.pid === "number" ? parsed.pid : 0;
4630
+ } catch {
4631
+ }
4632
+ if (ownerPid && isProcessAlive(ownerPid)) return false;
4633
+ fs.rmSync(lockPath, { force: true });
4634
+ return true;
4635
+ } catch (error) {
4636
+ return error.code === "ENOENT";
4637
+ }
4638
+ }
4639
+ function withFileLock(targetPath, operation, options) {
4640
+ const lockPath = `${targetPath}.lock`;
4641
+ const activeDepth = ACTIVE_LOCKS.get(lockPath) || 0;
4642
+ if (activeDepth > 0) {
4643
+ ACTIVE_LOCKS.set(lockPath, activeDepth + 1);
4644
+ try {
4645
+ return operation();
4646
+ } finally {
4647
+ const nextDepth = (ACTIVE_LOCKS.get(lockPath) || 1) - 1;
4648
+ if (nextDepth > 0) ACTIVE_LOCKS.set(lockPath, nextDepth);
4649
+ else ACTIVE_LOCKS.delete(lockPath);
4650
+ }
4651
+ }
4652
+ const timeoutMs = Math.max(0, options?.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS);
4653
+ const staleAfterMs = Math.max(timeoutMs, options?.staleAfterMs ?? DEFAULT_STALE_LOCK_MS);
4654
+ const deadline = Date.now() + timeoutMs;
4655
+ const record = {
4656
+ token: crypto.randomUUID(),
4657
+ pid: process.pid,
4658
+ createdAt: Date.now()
4659
+ };
4660
+ let handle = null;
4661
+ while (handle === null) {
4662
+ try {
4663
+ handle = fs.openSync(lockPath, "wx", 384);
4664
+ fs.writeFileSync(handle, JSON.stringify(record), "utf8");
4665
+ } catch (error) {
4666
+ const code = error.code;
4667
+ if (code !== "EEXIST") throw error;
4668
+ if (removeStaleLock(lockPath, staleAfterMs)) continue;
4669
+ if (Date.now() >= deadline) {
4670
+ throw new Error(`Timed out waiting for file lock: ${lockPath}`);
4671
+ }
4672
+ sleepSync(Math.min(LOCK_RETRY_INTERVAL_MS, Math.max(1, deadline - Date.now())));
4673
+ }
4674
+ }
4675
+ try {
4676
+ ACTIVE_LOCKS.set(lockPath, 1);
4677
+ return operation();
4678
+ } finally {
4679
+ ACTIVE_LOCKS.delete(lockPath);
4680
+ try {
4681
+ fs.closeSync(handle);
4682
+ } catch {
4683
+ }
4684
+ try {
4685
+ const current = JSON.parse(fs.readFileSync(lockPath, "utf8"));
4686
+ if (current.token === record.token) fs.rmSync(lockPath, { force: true });
4687
+ } catch {
4688
+ }
4689
+ }
4690
+ }
4691
+ function withFileLocks(targetPaths, operation) {
4692
+ const paths = Array.from(new Set(targetPaths)).sort();
4693
+ const acquire = (index) => {
4694
+ if (index >= paths.length) return operation();
4695
+ return withFileLock(paths[index], () => acquire(index + 1));
4696
+ };
4697
+ return acquire(0);
4698
+ }
4699
+
4601
4700
  // src/config.ts
4602
4701
  function isSupportedChannelProvider(value) {
4603
4702
  return value === "feishu" || value === "weixin";
4604
4703
  }
4704
+ var RAW_CHANNELS = Symbol("rawConfigV2Channels");
4605
4705
  function toFeishuConfig(channel) {
4606
4706
  return channel?.provider === "feishu" ? channel.config : void 0;
4607
4707
  }
@@ -4641,7 +4741,7 @@ function parseEnvFile(content) {
4641
4741
  }
4642
4742
  function loadRawConfigEnv() {
4643
4743
  try {
4644
- return parseEnvFile(fs.readFileSync(CONFIG_PATH, "utf-8"));
4744
+ return parseEnvFile(fs2.readFileSync(CONFIG_PATH, "utf-8"));
4645
4745
  } catch {
4646
4746
  return /* @__PURE__ */ new Map();
4647
4747
  }
@@ -4671,26 +4771,66 @@ function nowIso() {
4671
4771
  return (/* @__PURE__ */ new Date()).toISOString();
4672
4772
  }
4673
4773
  function ensureConfigDir() {
4674
- fs.mkdirSync(CTI_HOME, { recursive: true });
4774
+ fs2.mkdirSync(CTI_HOME, { recursive: true });
4675
4775
  }
4676
4776
  function readConfigV2File() {
4777
+ if (!fs2.existsSync(CONFIG_V2_PATH)) return null;
4778
+ let content;
4677
4779
  try {
4678
- const parsed = JSON.parse(fs.readFileSync(CONFIG_V2_PATH, "utf-8"));
4679
- if (parsed && parsed.schemaVersion === 2 && parsed.runtime && Array.isArray(parsed.channels)) {
4680
- parsed.runtime.provider = normalizeRuntimeProvider(parsed.runtime.provider);
4681
- parsed.channels = normalizeChannelInstances(parsed.channels);
4682
- return parsed;
4780
+ content = fs2.readFileSync(CONFIG_V2_PATH, "utf-8");
4781
+ } catch (error) {
4782
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH}: ${error instanceof Error ? error.message : String(error)}`);
4783
+ }
4784
+ let raw;
4785
+ try {
4786
+ raw = JSON.parse(content);
4787
+ } catch (error) {
4788
+ throw new Error(`\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH} \u4E0D\u662F\u6709\u6548 JSON\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6: ${error instanceof Error ? error.message : String(error)}`);
4789
+ }
4790
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
4791
+ throw new Error(`\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH} \u7684\u6839\u8282\u70B9\u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6\u3002`);
4792
+ }
4793
+ if (raw.schemaVersion !== 2) {
4794
+ throw new Error(`\u4E0D\u652F\u6301\u914D\u7F6E schemaVersion=${String(raw.schemaVersion)}\uFF0C\u5DF2\u62D2\u7EDD\u7528\u5F53\u524D\u7248\u672C\u8986\u76D6\u3002`);
4795
+ }
4796
+ if (!raw.runtime || typeof raw.runtime !== "object" || Array.isArray(raw.runtime)) {
4797
+ throw new Error(`\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH} \u7F3A\u5C11\u6709\u6548\u7684 runtime \u5BF9\u8C61\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6\u3002`);
4798
+ }
4799
+ if (!Array.isArray(raw.channels)) {
4800
+ throw new Error(`\u914D\u7F6E\u6587\u4EF6 ${CONFIG_V2_PATH} \u7F3A\u5C11 channels \u6570\u7EC4\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6\u3002`);
4801
+ }
4802
+ const rawChannels = raw.channels;
4803
+ const parsed = {
4804
+ ...raw,
4805
+ schemaVersion: 2,
4806
+ runtime: {
4807
+ ...raw.runtime,
4808
+ provider: normalizeRuntimeProvider(raw.runtime.provider)
4809
+ },
4810
+ channels: normalizeChannelInstances(rawChannels)
4811
+ };
4812
+ Object.defineProperty(parsed, RAW_CHANNELS, { value: rawChannels, enumerable: false });
4813
+ return parsed;
4814
+ }
4815
+ function atomicWriteFile(filePath, content) {
4816
+ ensureConfigDir();
4817
+ const tmpPath = `${filePath}.${process.pid}.${crypto2.randomUUID()}.tmp`;
4818
+ try {
4819
+ fs2.writeFileSync(tmpPath, content, { mode: 384 });
4820
+ fs2.renameSync(tmpPath, filePath);
4821
+ } finally {
4822
+ try {
4823
+ fs2.rmSync(tmpPath, { force: true });
4824
+ } catch {
4683
4825
  }
4684
- return null;
4685
- } catch {
4686
- return null;
4687
4826
  }
4688
4827
  }
4689
4828
  function writeConfigV2File(config) {
4690
- ensureConfigDir();
4691
- const tmpPath = CONFIG_V2_PATH + ".tmp";
4692
- fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), { mode: 384 });
4693
- fs.renameSync(tmpPath, CONFIG_V2_PATH);
4829
+ const serialized = {
4830
+ ...config,
4831
+ channels: config[RAW_CHANNELS] || config.channels
4832
+ };
4833
+ atomicWriteFile(CONFIG_V2_PATH, JSON.stringify(serialized, null, 2));
4694
4834
  }
4695
4835
  function defaultAliasForProvider(provider) {
4696
4836
  return provider === "feishu" ? "\u98DE\u4E66" : "\u5FAE\u4FE1";
@@ -4711,6 +4851,7 @@ function normalizeChannelInstances(value) {
4711
4851
  const config = record.config && typeof record.config === "object" ? record.config : {};
4712
4852
  const timestamp = nowIso();
4713
4853
  return [{
4854
+ ...record,
4714
4855
  id: normalizeChannelId(
4715
4856
  typeof record.id === "string" && record.id.trim() ? record.id : buildDefaultChannelId(provider)
4716
4857
  ),
@@ -4811,13 +4952,45 @@ function expandConfig(v2) {
4811
4952
  uiAccessToken: v2.runtime.uiAccessToken || void 0
4812
4953
  };
4813
4954
  }
4955
+ function mergeRawChannelRecords(current, channels) {
4956
+ const remaining = new Map(channels.map((channel) => [channel.id, channel]));
4957
+ const rawChannels = current?.[RAW_CHANNELS] || current?.channels || [];
4958
+ const merged = [];
4959
+ for (const rawEntry of rawChannels) {
4960
+ if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
4961
+ const rawRecord = rawEntry;
4962
+ if (!isSupportedChannelProvider(rawRecord.provider)) {
4963
+ merged.push(rawEntry);
4964
+ continue;
4965
+ }
4966
+ const rawId = normalizeChannelId(
4967
+ typeof rawRecord.id === "string" && rawRecord.id.trim() ? rawRecord.id : buildDefaultChannelId(rawRecord.provider)
4968
+ );
4969
+ const replacement = remaining.get(rawId);
4970
+ if (!replacement) continue;
4971
+ remaining.delete(rawId);
4972
+ const rawConfig = rawRecord.config && typeof rawRecord.config === "object" && !Array.isArray(rawRecord.config) ? rawRecord.config : {};
4973
+ merged.push({
4974
+ ...rawRecord,
4975
+ ...replacement,
4976
+ config: {
4977
+ ...rawConfig,
4978
+ ...replacement.config
4979
+ }
4980
+ });
4981
+ }
4982
+ merged.push(...remaining.values());
4983
+ return merged;
4984
+ }
4814
4985
  function buildV2FileFromExpandedConfig(config, current) {
4815
4986
  const hasExplicitChannels = Array.isArray(config.channels);
4816
4987
  let channels = hasExplicitChannels ? [...config.channels || []] : [...current?.channels || []];
4817
4988
  channels = normalizeChannelInstances(channels);
4818
- return {
4989
+ const next = {
4990
+ ...current || {},
4819
4991
  schemaVersion: 2,
4820
4992
  runtime: {
4993
+ ...current?.runtime || {},
4821
4994
  provider: config.runtime,
4822
4995
  defaultWorkspaceRoot: config.defaultWorkspaceRoot,
4823
4996
  defaultModel: config.defaultModel,
@@ -4837,33 +5010,42 @@ function buildV2FileFromExpandedConfig(config, current) {
4837
5010
  alias: channel.alias?.trim() || defaultAliasForProvider(channel.provider)
4838
5011
  }))
4839
5012
  };
5013
+ Object.defineProperty(next, RAW_CHANNELS, {
5014
+ value: mergeRawChannelRecords(current, next.channels),
5015
+ enumerable: false
5016
+ });
5017
+ return next;
4840
5018
  }
4841
5019
  function loadConfig() {
4842
5020
  const current = readConfigV2File();
4843
5021
  if (current) return expandConfig(current);
4844
- const legacyEnv = loadRawConfigEnv();
4845
- if (legacyEnv.size > 0) {
4846
- const migrated = migrateLegacyEnvToV2(legacyEnv);
4847
- writeConfigV2File(migrated);
4848
- return expandConfig(migrated);
4849
- }
4850
- const empty = {
4851
- schemaVersion: 2,
4852
- runtime: {
4853
- provider: "codex",
4854
- defaultWorkspaceRoot: DEFAULT_WORKSPACE_ROOT,
4855
- defaultMode: "code",
4856
- historyMessageLimit: 8,
4857
- streamStatusIdleStartSeconds: DEFAULT_STREAM_STATUS_IDLE_START_SECONDS,
4858
- streamStatusCheckIntervalSeconds: DEFAULT_STREAM_STATUS_CHECK_INTERVAL_SECONDS,
4859
- codexSkipGitRepoCheck: true,
4860
- codexSandboxMode: "workspace-write",
4861
- codexReasoningEffort: "medium",
4862
- uiAllowLan: false
4863
- },
4864
- channels: []
4865
- };
4866
- return expandConfig(empty);
5022
+ return withFileLock(CONFIG_V2_PATH, () => {
5023
+ const concurrentlyCreated = readConfigV2File();
5024
+ if (concurrentlyCreated) return expandConfig(concurrentlyCreated);
5025
+ const legacyEnv = loadRawConfigEnv();
5026
+ if (legacyEnv.size > 0) {
5027
+ const migrated = migrateLegacyEnvToV2(legacyEnv);
5028
+ writeConfigV2File(migrated);
5029
+ return expandConfig(migrated);
5030
+ }
5031
+ const empty = {
5032
+ schemaVersion: 2,
5033
+ runtime: {
5034
+ provider: "codex",
5035
+ defaultWorkspaceRoot: DEFAULT_WORKSPACE_ROOT,
5036
+ defaultMode: "code",
5037
+ historyMessageLimit: 8,
5038
+ streamStatusIdleStartSeconds: DEFAULT_STREAM_STATUS_IDLE_START_SECONDS,
5039
+ streamStatusCheckIntervalSeconds: DEFAULT_STREAM_STATUS_CHECK_INTERVAL_SECONDS,
5040
+ codexSkipGitRepoCheck: true,
5041
+ codexSandboxMode: "workspace-write",
5042
+ codexReasoningEffort: "medium",
5043
+ uiAllowLan: false
5044
+ },
5045
+ channels: []
5046
+ };
5047
+ return expandConfig(empty);
5048
+ });
4867
5049
  }
4868
5050
  function formatEnvLine(key, value) {
4869
5051
  if (value === void 0 || value === "") return "";
@@ -4871,64 +5053,63 @@ function formatEnvLine(key, value) {
4871
5053
  `;
4872
5054
  }
4873
5055
  function saveConfig(config) {
4874
- const current = readConfigV2File();
4875
- const next = buildV2FileFromExpandedConfig(config, current);
4876
- writeConfigV2File(next);
4877
- let out = "";
4878
- out += formatEnvLine("CTI_RUNTIME", next.runtime.provider);
4879
- out += formatEnvLine(
4880
- "CTI_ENABLED_CHANNELS",
4881
- Array.from(new Set(next.channels.filter((channel) => channel.enabled).map((channel) => channel.provider))).join(",")
4882
- );
4883
- out += formatEnvLine("CTI_DEFAULT_WORKSPACE_ROOT", next.runtime.defaultWorkspaceRoot);
4884
- out += formatEnvLine("CTI_DEFAULT_MODEL", next.runtime.defaultModel);
4885
- out += formatEnvLine("CTI_DEFAULT_MODE", next.runtime.defaultMode);
4886
- if (next.runtime.historyMessageLimit !== void 0) {
4887
- out += formatEnvLine("CTI_HISTORY_MESSAGE_LIMIT", String(next.runtime.historyMessageLimit));
4888
- }
4889
- if (next.runtime.streamStatusIdleStartSeconds !== void 0) {
4890
- out += formatEnvLine("CTI_STREAM_STATUS_IDLE_START_SECONDS", String(next.runtime.streamStatusIdleStartSeconds));
4891
- }
4892
- if (next.runtime.streamStatusCheckIntervalSeconds !== void 0) {
4893
- out += formatEnvLine("CTI_STREAM_STATUS_CHECK_INTERVAL_SECONDS", String(next.runtime.streamStatusCheckIntervalSeconds));
4894
- }
4895
- if (next.runtime.codexSkipGitRepoCheck !== void 0) {
4896
- out += formatEnvLine("CTI_CODEX_SKIP_GIT_REPO_CHECK", String(next.runtime.codexSkipGitRepoCheck));
4897
- }
4898
- out += formatEnvLine("CTI_CODEX_SANDBOX_MODE", next.runtime.codexSandboxMode);
4899
- out += formatEnvLine("CTI_CODEX_REASONING_EFFORT", next.runtime.codexReasoningEffort);
4900
- out += formatEnvLine("CTI_UI_ALLOW_LAN", String(next.runtime.uiAllowLan === true));
4901
- out += formatEnvLine("CTI_UI_ACCESS_TOKEN", next.runtime.uiAccessToken);
4902
- const feishu = getChannelByProvider(next, "feishu");
4903
- const feishuConfig = toFeishuConfig(feishu);
4904
- if (feishuConfig) {
4905
- out += formatEnvLine("CTI_FEISHU_APP_ID", feishuConfig.appId);
4906
- out += formatEnvLine("CTI_FEISHU_APP_SECRET", feishuConfig.appSecret);
4907
- out += formatEnvLine("CTI_FEISHU_SITE", feishuConfig.site);
4908
- out += formatEnvLine("CTI_FEISHU_ALLOWED_USERS", feishuConfig.allowedUsers?.join(","));
4909
- if (feishuConfig.streamingEnabled !== void 0) {
4910
- out += formatEnvLine("CTI_FEISHU_STREAMING_ENABLED", String(feishuConfig.streamingEnabled));
4911
- }
4912
- if (feishuConfig.feedbackMarkdownEnabled !== void 0) {
4913
- out += formatEnvLine("CTI_FEISHU_COMMAND_MARKDOWN_ENABLED", String(feishuConfig.feedbackMarkdownEnabled));
4914
- }
4915
- }
4916
- const weixin = getChannelByProvider(next, "weixin");
4917
- const weixinConfig = toWeixinConfig(weixin);
4918
- if (weixinConfig) {
4919
- out += formatEnvLine("CTI_WEIXIN_BASE_URL", weixinConfig.baseUrl);
4920
- out += formatEnvLine("CTI_WEIXIN_CDN_BASE_URL", weixinConfig.cdnBaseUrl);
4921
- if (weixinConfig.mediaEnabled !== void 0) {
4922
- out += formatEnvLine("CTI_WEIXIN_MEDIA_ENABLED", String(weixinConfig.mediaEnabled));
4923
- }
4924
- if (weixinConfig.feedbackMarkdownEnabled !== void 0) {
4925
- out += formatEnvLine("CTI_WEIXIN_COMMAND_MARKDOWN_ENABLED", String(weixinConfig.feedbackMarkdownEnabled));
4926
- }
4927
- }
4928
- ensureConfigDir();
4929
- const tmpPath = CONFIG_PATH + ".tmp";
4930
- fs.writeFileSync(tmpPath, out, { mode: 384 });
4931
- fs.renameSync(tmpPath, CONFIG_PATH);
5056
+ withFileLock(CONFIG_V2_PATH, () => {
5057
+ const current = readConfigV2File();
5058
+ const next = buildV2FileFromExpandedConfig(config, current);
5059
+ writeConfigV2File(next);
5060
+ let out = "";
5061
+ out += formatEnvLine("CTI_RUNTIME", next.runtime.provider);
5062
+ out += formatEnvLine(
5063
+ "CTI_ENABLED_CHANNELS",
5064
+ Array.from(new Set(next.channels.filter((channel) => channel.enabled).map((channel) => channel.provider))).join(",")
5065
+ );
5066
+ out += formatEnvLine("CTI_DEFAULT_WORKSPACE_ROOT", next.runtime.defaultWorkspaceRoot);
5067
+ out += formatEnvLine("CTI_DEFAULT_MODEL", next.runtime.defaultModel);
5068
+ out += formatEnvLine("CTI_DEFAULT_MODE", next.runtime.defaultMode);
5069
+ if (next.runtime.historyMessageLimit !== void 0) {
5070
+ out += formatEnvLine("CTI_HISTORY_MESSAGE_LIMIT", String(next.runtime.historyMessageLimit));
5071
+ }
5072
+ if (next.runtime.streamStatusIdleStartSeconds !== void 0) {
5073
+ out += formatEnvLine("CTI_STREAM_STATUS_IDLE_START_SECONDS", String(next.runtime.streamStatusIdleStartSeconds));
5074
+ }
5075
+ if (next.runtime.streamStatusCheckIntervalSeconds !== void 0) {
5076
+ out += formatEnvLine("CTI_STREAM_STATUS_CHECK_INTERVAL_SECONDS", String(next.runtime.streamStatusCheckIntervalSeconds));
5077
+ }
5078
+ if (next.runtime.codexSkipGitRepoCheck !== void 0) {
5079
+ out += formatEnvLine("CTI_CODEX_SKIP_GIT_REPO_CHECK", String(next.runtime.codexSkipGitRepoCheck));
5080
+ }
5081
+ out += formatEnvLine("CTI_CODEX_SANDBOX_MODE", next.runtime.codexSandboxMode);
5082
+ out += formatEnvLine("CTI_CODEX_REASONING_EFFORT", next.runtime.codexReasoningEffort);
5083
+ out += formatEnvLine("CTI_UI_ALLOW_LAN", String(next.runtime.uiAllowLan === true));
5084
+ out += formatEnvLine("CTI_UI_ACCESS_TOKEN", next.runtime.uiAccessToken);
5085
+ const feishu = getChannelByProvider(next, "feishu");
5086
+ const feishuConfig = toFeishuConfig(feishu);
5087
+ if (feishuConfig) {
5088
+ out += formatEnvLine("CTI_FEISHU_APP_ID", feishuConfig.appId);
5089
+ out += formatEnvLine("CTI_FEISHU_APP_SECRET", feishuConfig.appSecret);
5090
+ out += formatEnvLine("CTI_FEISHU_SITE", feishuConfig.site);
5091
+ out += formatEnvLine("CTI_FEISHU_ALLOWED_USERS", feishuConfig.allowedUsers?.join(","));
5092
+ if (feishuConfig.streamingEnabled !== void 0) {
5093
+ out += formatEnvLine("CTI_FEISHU_STREAMING_ENABLED", String(feishuConfig.streamingEnabled));
5094
+ }
5095
+ if (feishuConfig.feedbackMarkdownEnabled !== void 0) {
5096
+ out += formatEnvLine("CTI_FEISHU_COMMAND_MARKDOWN_ENABLED", String(feishuConfig.feedbackMarkdownEnabled));
5097
+ }
5098
+ }
5099
+ const weixin = getChannelByProvider(next, "weixin");
5100
+ const weixinConfig = toWeixinConfig(weixin);
5101
+ if (weixinConfig) {
5102
+ out += formatEnvLine("CTI_WEIXIN_BASE_URL", weixinConfig.baseUrl);
5103
+ out += formatEnvLine("CTI_WEIXIN_CDN_BASE_URL", weixinConfig.cdnBaseUrl);
5104
+ if (weixinConfig.mediaEnabled !== void 0) {
5105
+ out += formatEnvLine("CTI_WEIXIN_MEDIA_ENABLED", String(weixinConfig.mediaEnabled));
5106
+ }
5107
+ if (weixinConfig.feedbackMarkdownEnabled !== void 0) {
5108
+ out += formatEnvLine("CTI_WEIXIN_COMMAND_MARKDOWN_ENABLED", String(weixinConfig.feedbackMarkdownEnabled));
5109
+ }
5110
+ }
5111
+ atomicWriteFile(CONFIG_PATH, out);
5112
+ });
4932
5113
  }
4933
5114
  function listChannelInstances(config) {
4934
5115
  return [...config?.channels || loadConfig().channels || []];
@@ -5025,10 +5206,10 @@ function configToSettings(config) {
5025
5206
  }
5026
5207
 
5027
5208
  // src/desktop-sessions.ts
5028
- import fs2 from "node:fs";
5209
+ import fs3 from "node:fs";
5029
5210
  import os2 from "node:os";
5030
5211
  import path2 from "node:path";
5031
- import crypto from "node:crypto";
5212
+ import crypto3 from "node:crypto";
5032
5213
  import { DatabaseSync } from "node:sqlite";
5033
5214
  var ACTIVE_WINDOW_MS = 15 * 60 * 1e3;
5034
5215
  var MAX_SESSION_META_BYTES = 4 * 1024 * 1024;
@@ -5053,13 +5234,13 @@ function getDesktopStateDbPath() {
5053
5234
  const codexHome = getCodexHome();
5054
5235
  let entries;
5055
5236
  try {
5056
- entries = fs2.readdirSync(codexHome, { withFileTypes: true });
5237
+ entries = fs3.readdirSync(codexHome, { withFileTypes: true });
5057
5238
  } catch {
5058
5239
  return null;
5059
5240
  }
5060
5241
  const candidates = entries.filter((entry) => entry.isFile() && /^state_\d+\.sqlite$/i.test(entry.name)).map((entry) => path2.join(codexHome, entry.name)).sort((left, right) => {
5061
5242
  try {
5062
- return fs2.statSync(right).mtimeMs - fs2.statSync(left).mtimeMs;
5243
+ return fs3.statSync(right).mtimeMs - fs3.statSync(left).mtimeMs;
5063
5244
  } catch {
5064
5245
  return 0;
5065
5246
  }
@@ -5084,10 +5265,10 @@ function isInternalSkillWorkspace(cwd) {
5084
5265
  }
5085
5266
  function loadSavedWorkspaceRoots() {
5086
5267
  const statePath = getCodexGlobalStatePath();
5087
- if (!fs2.existsSync(statePath)) return null;
5268
+ if (!fs3.existsSync(statePath)) return null;
5088
5269
  let parsed;
5089
5270
  try {
5090
- parsed = JSON.parse(fs2.readFileSync(statePath, "utf-8"));
5271
+ parsed = JSON.parse(fs3.readFileSync(statePath, "utf-8"));
5091
5272
  } catch {
5092
5273
  return null;
5093
5274
  }
@@ -5102,10 +5283,10 @@ function isWithinSavedWorkspaceRoots(cwd, roots) {
5102
5283
  }
5103
5284
  function loadArchivedThreadIds() {
5104
5285
  const archivedRoot = getArchivedSessionsRoot();
5105
- if (!fs2.existsSync(archivedRoot)) return /* @__PURE__ */ new Set();
5286
+ if (!fs3.existsSync(archivedRoot)) return /* @__PURE__ */ new Set();
5106
5287
  let entries;
5107
5288
  try {
5108
- entries = fs2.readdirSync(archivedRoot, { withFileTypes: true });
5289
+ entries = fs3.readdirSync(archivedRoot, { withFileTypes: true });
5109
5290
  } catch {
5110
5291
  return /* @__PURE__ */ new Set();
5111
5292
  }
@@ -5118,13 +5299,13 @@ function loadArchivedThreadIds() {
5118
5299
  return ids;
5119
5300
  }
5120
5301
  function readFirstLine(filePath, maxBytes = MAX_SESSION_META_BYTES) {
5121
- const fd = fs2.openSync(filePath, "r");
5302
+ const fd = fs3.openSync(filePath, "r");
5122
5303
  try {
5123
5304
  const chunks = [];
5124
5305
  let bytesReadTotal = 0;
5125
5306
  const buffer = Buffer.alloc(4096);
5126
5307
  while (bytesReadTotal < maxBytes) {
5127
- const bytesRead = fs2.readSync(fd, buffer, 0, buffer.length, bytesReadTotal);
5308
+ const bytesRead = fs3.readSync(fd, buffer, 0, buffer.length, bytesReadTotal);
5128
5309
  if (bytesRead <= 0) break;
5129
5310
  const slice = Buffer.from(buffer.subarray(0, bytesRead));
5130
5311
  chunks.push(slice);
@@ -5137,31 +5318,31 @@ function readFirstLine(filePath, maxBytes = MAX_SESSION_META_BYTES) {
5137
5318
  }
5138
5319
  return Buffer.concat(chunks).toString("utf-8").split(/\r?\n/, 1)[0] || "";
5139
5320
  } finally {
5140
- fs2.closeSync(fd);
5321
+ fs3.closeSync(fd);
5141
5322
  }
5142
5323
  }
5143
5324
  function readFilePrefix(filePath, maxBytes = MAX_SESSION_TITLE_SCAN_BYTES) {
5144
- const fd = fs2.openSync(filePath, "r");
5325
+ const fd = fs3.openSync(filePath, "r");
5145
5326
  try {
5146
5327
  const buffer = Buffer.alloc(Math.min(maxBytes, 64 * 1024));
5147
5328
  const chunks = [];
5148
5329
  let offset = 0;
5149
5330
  while (offset < maxBytes) {
5150
5331
  const bytesToRead = Math.min(buffer.length, maxBytes - offset);
5151
- const bytesRead = fs2.readSync(fd, buffer, 0, bytesToRead, offset);
5332
+ const bytesRead = fs3.readSync(fd, buffer, 0, bytesToRead, offset);
5152
5333
  if (bytesRead <= 0) break;
5153
5334
  chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
5154
5335
  offset += bytesRead;
5155
5336
  }
5156
5337
  return Buffer.concat(chunks).toString("utf-8");
5157
5338
  } finally {
5158
- fs2.closeSync(fd);
5339
+ fs3.closeSync(fd);
5159
5340
  }
5160
5341
  }
5161
5342
  function walkSessionFiles(dirPath, target) {
5162
5343
  let entries;
5163
5344
  try {
5164
- entries = fs2.readdirSync(dirPath, { withFileTypes: true });
5345
+ entries = fs3.readdirSync(dirPath, { withFileTypes: true });
5165
5346
  } catch {
5166
5347
  return;
5167
5348
  }
@@ -5184,10 +5365,10 @@ function isDesktopLike(meta) {
5184
5365
  }
5185
5366
  function loadThreadIndexEntries(archivedThreadIds) {
5186
5367
  const indexPath = getSessionIndexPath();
5187
- if (!fs2.existsSync(indexPath)) return /* @__PURE__ */ new Map();
5368
+ if (!fs3.existsSync(indexPath)) return /* @__PURE__ */ new Map();
5188
5369
  let content = "";
5189
5370
  try {
5190
- content = fs2.readFileSync(indexPath, "utf-8");
5371
+ content = fs3.readFileSync(indexPath, "utf-8");
5191
5372
  } catch {
5192
5373
  return /* @__PURE__ */ new Map();
5193
5374
  }
@@ -5227,7 +5408,7 @@ function parseUpdatedAtValue(value) {
5227
5408
  }
5228
5409
  function loadVisibleDesktopThreads(limit) {
5229
5410
  const dbPath = getDesktopStateDbPath();
5230
- if (!dbPath || !fs2.existsSync(dbPath)) return null;
5411
+ if (!dbPath || !fs3.existsSync(dbPath)) return null;
5231
5412
  let db = null;
5232
5413
  try {
5233
5414
  db = new DatabaseSync(dbPath, { readOnly: true });
@@ -5294,7 +5475,7 @@ function parseDesktopSession(filePath, threadIndexEntries, archivedThreadIds) {
5294
5475
  }
5295
5476
  let stat;
5296
5477
  try {
5297
- stat = fs2.statSync(filePath);
5478
+ stat = fs3.statSync(filePath);
5298
5479
  } catch {
5299
5480
  return null;
5300
5481
  }
@@ -5369,7 +5550,7 @@ function isSessionEventLine(line) {
5369
5550
  }
5370
5551
  function listDesktopSessions(limit) {
5371
5552
  const root = getCodexSessionsRoot();
5372
- if (!fs2.existsSync(root)) return [];
5553
+ if (!fs3.existsSync(root)) return [];
5373
5554
  const archivedThreadIds = loadArchivedThreadIds();
5374
5555
  const threadIndexEntries = loadThreadIndexEntries(archivedThreadIds);
5375
5556
  const savedWorkspaceRoots = loadSavedWorkspaceRoots();
@@ -5407,9 +5588,17 @@ function listDesktopSessions(limit) {
5407
5588
  }
5408
5589
  return sessions.sort((a, b) => b.lastEventAt.localeCompare(a.lastEventAt)).slice(0, typeof limit === "number" && Number.isFinite(limit) && limit > 0 ? Math.max(1, Math.floor(limit)) : void 0);
5409
5590
  }
5591
+ var DESKTOP_SESSION_LOOKUP_CACHE_TTL_MS = 1e3;
5592
+ var desktopSessionLookupCache = null;
5410
5593
  function getDesktopSessionByThreadId(threadId) {
5411
- const sessions = listDesktopSessions(200);
5412
- return sessions.find((session) => session.threadId === threadId) || null;
5594
+ const timestamp = Date.now();
5595
+ if (!desktopSessionLookupCache || desktopSessionLookupCache.expiresAt <= timestamp) {
5596
+ desktopSessionLookupCache = {
5597
+ expiresAt: timestamp + DESKTOP_SESSION_LOOKUP_CACHE_TTL_MS,
5598
+ sessions: new Map(listDesktopSessions().map((session) => [session.threadId, session]))
5599
+ };
5600
+ }
5601
+ return desktopSessionLookupCache.sessions.get(threadId) || null;
5413
5602
  }
5414
5603
  function isArchivedDesktopThread(threadId) {
5415
5604
  return loadArchivedThreadIds().has(threadId);
@@ -5727,25 +5916,25 @@ function removeBinding(store, bindingId) {
5727
5916
  }
5728
5917
 
5729
5918
  // src/service-manager.ts
5730
- import fs4 from "node:fs";
5919
+ import fs5 from "node:fs";
5731
5920
  import os3 from "node:os";
5732
5921
  import path5 from "node:path";
5733
5922
  import { spawn } from "node:child_process";
5734
5923
  import { fileURLToPath } from "node:url";
5735
5924
 
5736
5925
  // src/bridge-instance-lock.ts
5737
- import fs3 from "node:fs";
5926
+ import fs4 from "node:fs";
5738
5927
  import path4 from "node:path";
5739
5928
  var runtimeDir = path4.join(CTI_HOME, "runtime");
5740
5929
  var bridgeInstanceLockFile = path4.join(runtimeDir, "bridge.instance.lock");
5741
5930
  function readJsonFile(filePath, fallback) {
5742
5931
  try {
5743
- return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
5932
+ return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
5744
5933
  } catch {
5745
5934
  return fallback;
5746
5935
  }
5747
5936
  }
5748
- function isProcessAlive(pid) {
5937
+ function isProcessAlive2(pid) {
5749
5938
  if (!pid) return false;
5750
5939
  try {
5751
5940
  process.kill(pid, 0);
@@ -5761,11 +5950,11 @@ function readBridgeInstanceLock(filePath = bridgeInstanceLockFile) {
5761
5950
  if (!Number.isFinite(pid) || pid <= 0 || !createdAt) return null;
5762
5951
  return { pid, createdAt };
5763
5952
  }
5764
- function clearStaleBridgeInstanceLock(filePath = bridgeInstanceLockFile, isAlive = isProcessAlive) {
5953
+ function clearStaleBridgeInstanceLock(filePath = bridgeInstanceLockFile, isAlive = isProcessAlive2) {
5765
5954
  const existing = readBridgeInstanceLock(filePath);
5766
5955
  if (existing && isAlive(existing.pid)) return;
5767
5956
  try {
5768
- fs3.unlinkSync(filePath);
5957
+ fs4.unlinkSync(filePath);
5769
5958
  } catch {
5770
5959
  }
5771
5960
  }
@@ -5786,26 +5975,26 @@ var npmUninstallLogFile = path5.join(runtimeDir2, "npm-uninstall.log");
5786
5975
  var WINDOWS_HIDE = process.platform === "win32" ? { windowsHide: true } : {};
5787
5976
  var BRIDGE_START_LOCK_STALE_MS = 3e4;
5788
5977
  function ensureDirs() {
5789
- fs4.mkdirSync(runtimeDir2, { recursive: true });
5790
- fs4.mkdirSync(logsDir, { recursive: true });
5978
+ fs5.mkdirSync(runtimeDir2, { recursive: true });
5979
+ fs5.mkdirSync(logsDir, { recursive: true });
5791
5980
  }
5792
5981
  function readJsonFile2(filePath, fallback) {
5793
5982
  try {
5794
- return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
5983
+ return JSON.parse(fs5.readFileSync(filePath, "utf-8"));
5795
5984
  } catch {
5796
5985
  return fallback;
5797
5986
  }
5798
5987
  }
5799
5988
  function readPid(filePath) {
5800
5989
  try {
5801
- const raw = fs4.readFileSync(filePath, "utf-8").trim();
5990
+ const raw = fs5.readFileSync(filePath, "utf-8").trim();
5802
5991
  const pid = Number(raw);
5803
5992
  return Number.isFinite(pid) ? pid : void 0;
5804
5993
  } catch {
5805
5994
  return void 0;
5806
5995
  }
5807
5996
  }
5808
- function isProcessAlive2(pid) {
5997
+ function isProcessAlive3(pid) {
5809
5998
  if (!pid) return false;
5810
5999
  try {
5811
6000
  process.kill(pid, 0);
@@ -5823,7 +6012,7 @@ function collectTrackedBridgePids(bridgePid, statusPid, instanceLockPid) {
5823
6012
  }
5824
6013
  return [...unique];
5825
6014
  }
5826
- function resolveTrackedBridgePid(bridgePid, statusPid, instanceLockPid, isAlive = isProcessAlive2) {
6015
+ function resolveTrackedBridgePid(bridgePid, statusPid, instanceLockPid, isAlive = isProcessAlive3) {
5827
6016
  if (isAlive(bridgePid)) return bridgePid;
5828
6017
  if (isAlive(statusPid)) return statusPid;
5829
6018
  if (isAlive(instanceLockPid)) return instanceLockPid;
@@ -5839,7 +6028,7 @@ function getTrackedBridgePids(status) {
5839
6028
  }
5840
6029
  function clearBridgePidFile() {
5841
6030
  try {
5842
- fs4.unlinkSync(bridgePidFile);
6031
+ fs5.unlinkSync(bridgePidFile);
5843
6032
  } catch {
5844
6033
  }
5845
6034
  }
@@ -5854,7 +6043,7 @@ function isBridgeStartLockStale(lock, options = {}) {
5854
6043
  if (!lock) return true;
5855
6044
  const nowMs = options.nowMs ?? Date.now();
5856
6045
  const staleMs = options.staleMs ?? BRIDGE_START_LOCK_STALE_MS;
5857
- const isAlive = options.isAlive ?? isProcessAlive2;
6046
+ const isAlive = options.isAlive ?? isProcessAlive3;
5858
6047
  const createdAtMs = Date.parse(lock.createdAt);
5859
6048
  if (!Number.isFinite(createdAtMs)) return true;
5860
6049
  if (!isAlive(lock.pid)) return true;
@@ -5870,7 +6059,7 @@ function tryAcquireBridgeStartLock(options = {}) {
5870
6059
  };
5871
6060
  for (let attempt = 0; attempt < 2; attempt += 1) {
5872
6061
  try {
5873
- fs4.writeFileSync(filePath, JSON.stringify(payload, null, 2), { encoding: "utf-8", flag: "wx" });
6062
+ fs5.writeFileSync(filePath, JSON.stringify(payload, null, 2), { encoding: "utf-8", flag: "wx" });
5874
6063
  return { acquired: true };
5875
6064
  } catch (error) {
5876
6065
  const code = error.code;
@@ -5884,7 +6073,7 @@ function tryAcquireBridgeStartLock(options = {}) {
5884
6073
  return { acquired: false, holderPid: existing2?.pid };
5885
6074
  }
5886
6075
  try {
5887
- fs4.unlinkSync(filePath);
6076
+ fs5.unlinkSync(filePath);
5888
6077
  } catch {
5889
6078
  }
5890
6079
  }
@@ -5896,14 +6085,14 @@ function releaseBridgeStartLock(filePath = bridgeStartLockFile, ownerPid = proce
5896
6085
  const existing = readBridgeStartLock(filePath);
5897
6086
  if (!existing) {
5898
6087
  try {
5899
- fs4.unlinkSync(filePath);
6088
+ fs5.unlinkSync(filePath);
5900
6089
  } catch {
5901
6090
  }
5902
6091
  return;
5903
6092
  }
5904
6093
  if (existing.pid !== ownerPid) return;
5905
6094
  try {
5906
- fs4.unlinkSync(filePath);
6095
+ fs5.unlinkSync(filePath);
5907
6096
  } catch {
5908
6097
  }
5909
6098
  }
@@ -5966,7 +6155,7 @@ function getBridgeStatus() {
5966
6155
  status.pid,
5967
6156
  readBridgeInstanceLock()?.pid
5968
6157
  );
5969
- if (!isProcessAlive2(pid)) {
6158
+ if (!isProcessAlive3(pid)) {
5970
6159
  return {
5971
6160
  ...status,
5972
6161
  pid,
@@ -5981,7 +6170,7 @@ function getBridgeStatus() {
5981
6170
  }
5982
6171
  function getUiServerStatus() {
5983
6172
  const status = readJsonFile2(uiStatusFile, { running: false, port: uiPort });
5984
- if (!isProcessAlive2(status.pid)) {
6173
+ if (!isProcessAlive3(status.pid)) {
5985
6174
  return {
5986
6175
  ...status,
5987
6176
  running: false,
@@ -6051,7 +6240,7 @@ async function waitForBridgeStartupTurn(timeoutMs = 2e4) {
6051
6240
  async function startBridge() {
6052
6241
  ensureDirs();
6053
6242
  const current = getBridgeStatus();
6054
- const extraAlivePids = getTrackedBridgePids(current).filter((pid) => pid !== current.pid && isProcessAlive2(pid));
6243
+ const extraAlivePids = getTrackedBridgePids(current).filter((pid) => pid !== current.pid && isProcessAlive3(pid));
6055
6244
  if (current.running && extraAlivePids.length === 0) return current;
6056
6245
  if (current.running && extraAlivePids.length > 0) {
6057
6246
  await stopBridge();
@@ -6076,17 +6265,17 @@ async function startBridge() {
6076
6265
  startLockHeld = true;
6077
6266
  try {
6078
6267
  const currentAfterLock = getBridgeStatus();
6079
- const extraAlivePidsAfterLock = getTrackedBridgePids(currentAfterLock).filter((pid) => pid !== currentAfterLock.pid && isProcessAlive2(pid));
6268
+ const extraAlivePidsAfterLock = getTrackedBridgePids(currentAfterLock).filter((pid) => pid !== currentAfterLock.pid && isProcessAlive3(pid));
6080
6269
  if (currentAfterLock.running && extraAlivePidsAfterLock.length === 0) return currentAfterLock;
6081
6270
  if (currentAfterLock.running && extraAlivePidsAfterLock.length > 0) {
6082
6271
  await stopBridge();
6083
6272
  }
6084
6273
  const daemonEntry = path5.join(packageRoot, "dist", "daemon.mjs");
6085
- if (!fs4.existsSync(daemonEntry)) {
6274
+ if (!fs5.existsSync(daemonEntry)) {
6086
6275
  throw new Error(`Daemon bundle not found at ${daemonEntry}. Run npm run build first.`);
6087
6276
  }
6088
- const stdoutFd = fs4.openSync(path5.join(logsDir, "bridge-launcher.out.log"), "a");
6089
- const stderrFd = fs4.openSync(path5.join(logsDir, "bridge-launcher.err.log"), "a");
6277
+ const stdoutFd = fs5.openSync(path5.join(logsDir, "bridge-launcher.out.log"), "a");
6278
+ const stderrFd = fs5.openSync(path5.join(logsDir, "bridge-launcher.err.log"), "a");
6090
6279
  const child = spawn(process.execPath, [daemonEntry], {
6091
6280
  cwd: packageRoot,
6092
6281
  detached: true,
@@ -6110,7 +6299,7 @@ async function startBridge() {
6110
6299
  }
6111
6300
  async function stopBridge() {
6112
6301
  const status = readJsonFile2(bridgeStatusFile, { running: false });
6113
- const pids = getTrackedBridgePids(status).filter((pid) => isProcessAlive2(pid));
6302
+ const pids = getTrackedBridgePids(status).filter((pid) => isProcessAlive3(pid));
6114
6303
  if (pids.length === 0) {
6115
6304
  clearBridgePidFile();
6116
6305
  clearStaleBridgeInstanceLock();
@@ -6135,7 +6324,7 @@ async function stopBridge() {
6135
6324
  }
6136
6325
  const startedAt = Date.now();
6137
6326
  while (Date.now() - startedAt < 1e4) {
6138
- if (pids.every((pid) => !isProcessAlive2(pid))) {
6327
+ if (pids.every((pid) => !isProcessAlive3(pid))) {
6139
6328
  clearBridgePidFile();
6140
6329
  clearStaleBridgeInstanceLock();
6141
6330
  return getBridgeStatus();
@@ -6207,30 +6396,30 @@ async function getBridgeAutostartStatus() {
6207
6396
  function getBridgeLogs(lines = 200) {
6208
6397
  ensureDirs();
6209
6398
  const filePath = path5.join(logsDir, "bridge.log");
6210
- if (!fs4.existsSync(filePath)) return "";
6211
- const all = fs4.readFileSync(filePath, "utf-8").split(/\r?\n/);
6399
+ if (!fs5.existsSync(filePath)) return "";
6400
+ const all = fs5.readFileSync(filePath, "utf-8").split(/\r?\n/);
6212
6401
  return all.slice(Math.max(0, all.length - lines)).join("\n");
6213
6402
  }
6214
6403
  function writeUiServerStatus(status) {
6215
6404
  ensureDirs();
6216
- fs4.writeFileSync(uiStatusFile, JSON.stringify(status, null, 2), "utf-8");
6405
+ fs5.writeFileSync(uiStatusFile, JSON.stringify(status, null, 2), "utf-8");
6217
6406
  }
6218
6407
  async function installCodexIntegration() {
6219
6408
  const sourceSkill = path5.join(packageRoot, "SKILL.md");
6220
- if (!fs4.existsSync(sourceSkill)) {
6409
+ if (!fs5.existsSync(sourceSkill)) {
6221
6410
  throw new Error(`SKILL.md not found at ${sourceSkill}`);
6222
6411
  }
6223
6412
  const skillsDir = path5.join(os3.homedir(), ".codex", "skills");
6224
6413
  const targetDir = path5.join(skillsDir, "codex-to-im");
6225
- fs4.mkdirSync(skillsDir, { recursive: true });
6226
- if (fs4.existsSync(targetDir)) {
6414
+ fs5.mkdirSync(skillsDir, { recursive: true });
6415
+ if (fs5.existsSync(targetDir)) {
6227
6416
  return { targetDir, method: "existing" };
6228
6417
  }
6229
6418
  try {
6230
- fs4.symlinkSync(packageRoot, targetDir, process.platform === "win32" ? "junction" : "dir");
6419
+ fs5.symlinkSync(packageRoot, targetDir, process.platform === "win32" ? "junction" : "dir");
6231
6420
  return { targetDir, method: "junction" };
6232
6421
  } catch {
6233
- fs4.cpSync(packageRoot, targetDir, {
6422
+ fs5.cpSync(packageRoot, targetDir, {
6234
6423
  recursive: true,
6235
6424
  filter: (source) => {
6236
6425
  const relative = path5.relative(packageRoot, source);
@@ -6245,36 +6434,55 @@ async function installCodexIntegration() {
6245
6434
  }
6246
6435
  function isCodexIntegrationInstalled() {
6247
6436
  const targetDir = path5.join(os3.homedir(), ".codex", "skills", "codex-to-im");
6248
- return fs4.existsSync(path5.join(targetDir, "SKILL.md"));
6437
+ return fs5.existsSync(path5.join(targetDir, "SKILL.md"));
6249
6438
  }
6250
6439
 
6251
6440
  // src/store.ts
6252
- import fs5 from "node:fs";
6441
+ import fs6 from "node:fs";
6253
6442
  import path6 from "node:path";
6254
- import crypto2 from "node:crypto";
6443
+ import crypto4 from "node:crypto";
6255
6444
  var DATA_DIR = path6.join(CTI_HOME, "data");
6256
6445
  var MESSAGES_DIR = path6.join(DATA_DIR, "messages");
6446
+ var SESSIONS_PATH = path6.join(DATA_DIR, "sessions.json");
6447
+ var BINDINGS_PATH = path6.join(DATA_DIR, "bindings.json");
6448
+ var PERMISSIONS_PATH = path6.join(DATA_DIR, "permissions.json");
6449
+ var OFFSETS_PATH = path6.join(DATA_DIR, "offsets.json");
6450
+ var DEDUP_PATH = path6.join(DATA_DIR, "dedup.json");
6451
+ var AUDIT_PATH = path6.join(DATA_DIR, "audit.json");
6452
+ var DEFAULT_DEDUP_TTL_MS = 5 * 60 * 1e3;
6453
+ var INBOUND_DEDUP_TTL_MS = 24 * 60 * 60 * 1e3;
6257
6454
  function ensureDir(dir) {
6258
- fs5.mkdirSync(dir, { recursive: true });
6455
+ fs6.mkdirSync(dir, { recursive: true });
6259
6456
  }
6260
6457
  function atomicWrite(filePath, data) {
6261
- const tmp = filePath + ".tmp";
6262
- fs5.writeFileSync(tmp, data, "utf-8");
6263
- fs5.renameSync(tmp, filePath);
6458
+ const tmp = `${filePath}.${process.pid}.${crypto4.randomUUID()}.tmp`;
6459
+ try {
6460
+ fs6.writeFileSync(tmp, data, "utf-8");
6461
+ fs6.renameSync(tmp, filePath);
6462
+ } finally {
6463
+ try {
6464
+ fs6.rmSync(tmp, { force: true });
6465
+ } catch {
6466
+ }
6467
+ }
6468
+ }
6469
+ function dedupTtlMs(key) {
6470
+ return key.startsWith("inbound:") ? INBOUND_DEDUP_TTL_MS : DEFAULT_DEDUP_TTL_MS;
6264
6471
  }
6265
6472
  function readJson(filePath, fallback) {
6266
6473
  try {
6267
- const raw = fs5.readFileSync(filePath, "utf-8");
6474
+ const raw = fs6.readFileSync(filePath, "utf-8");
6268
6475
  return JSON.parse(raw);
6269
- } catch {
6270
- return fallback;
6476
+ } catch (error) {
6477
+ if (error.code === "ENOENT") return fallback;
6478
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6 JSON \u6570\u636E\u6587\u4EF6 ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
6271
6479
  }
6272
6480
  }
6273
6481
  function writeJson(filePath, data) {
6274
6482
  atomicWrite(filePath, JSON.stringify(data, null, 2));
6275
6483
  }
6276
6484
  function uuid() {
6277
- return crypto2.randomUUID();
6485
+ return crypto4.randomUUID();
6278
6486
  }
6279
6487
  function now() {
6280
6488
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -6333,38 +6541,38 @@ var JsonFileStore = class {
6333
6541
  this.reloadSessions();
6334
6542
  this.reloadBindings();
6335
6543
  const perms = readJson(
6336
- path6.join(DATA_DIR, "permissions.json"),
6544
+ PERMISSIONS_PATH,
6337
6545
  {}
6338
6546
  );
6339
6547
  for (const [id, p] of Object.entries(perms)) {
6340
6548
  this.permissionLinks.set(id, p);
6341
6549
  }
6342
6550
  const offsets = readJson(
6343
- path6.join(DATA_DIR, "offsets.json"),
6551
+ OFFSETS_PATH,
6344
6552
  {}
6345
6553
  );
6346
6554
  for (const [k, v] of Object.entries(offsets)) {
6347
6555
  this.offsets.set(k, v);
6348
6556
  }
6349
6557
  const dedup = readJson(
6350
- path6.join(DATA_DIR, "dedup.json"),
6558
+ DEDUP_PATH,
6351
6559
  {}
6352
6560
  );
6353
6561
  for (const [k, v] of Object.entries(dedup)) {
6354
6562
  this.dedupKeys.set(k, v);
6355
6563
  }
6356
- this.auditLog = readJson(path6.join(DATA_DIR, "audit.json"), []);
6564
+ this.auditLog = readJson(AUDIT_PATH, []);
6357
6565
  }
6358
6566
  reloadSessions() {
6359
6567
  const sessions = readJson(
6360
- path6.join(DATA_DIR, "sessions.json"),
6568
+ SESSIONS_PATH,
6361
6569
  {}
6362
6570
  );
6363
6571
  this.sessions = new Map(Object.entries(sessions));
6364
6572
  }
6365
6573
  reloadBindings() {
6366
6574
  const bindings = readJson(
6367
- path6.join(DATA_DIR, "bindings.json"),
6575
+ BINDINGS_PATH,
6368
6576
  {}
6369
6577
  );
6370
6578
  const normalized = /* @__PURE__ */ new Map();
@@ -6378,41 +6586,62 @@ var JsonFileStore = class {
6378
6586
  }
6379
6587
  this.bindings = normalized;
6380
6588
  if (changed) {
6381
- this.persistBindings();
6589
+ withFileLock(BINDINGS_PATH, () => {
6590
+ const latest = readJson(BINDINGS_PATH, {});
6591
+ this.bindings = new Map(Object.values(latest).map((binding) => {
6592
+ const upgraded = upgradeLegacyBinding(binding);
6593
+ return [`${upgraded.channelType}:${upgraded.chatId}`, upgraded];
6594
+ }));
6595
+ this.persistBindings();
6596
+ });
6382
6597
  }
6383
6598
  }
6599
+ reloadPermissions() {
6600
+ this.permissionLinks = new Map(Object.entries(
6601
+ readJson(PERMISSIONS_PATH, {})
6602
+ ));
6603
+ }
6604
+ reloadOffsets() {
6605
+ this.offsets = new Map(Object.entries(readJson(OFFSETS_PATH, {})));
6606
+ }
6607
+ reloadDedup() {
6608
+ this.dedupKeys = new Map(Object.entries(readJson(DEDUP_PATH, {})));
6609
+ }
6610
+ reloadAudit() {
6611
+ this.auditLog = readJson(AUDIT_PATH, []);
6612
+ }
6384
6613
  persistSessions() {
6385
6614
  writeJson(
6386
- path6.join(DATA_DIR, "sessions.json"),
6615
+ SESSIONS_PATH,
6387
6616
  Object.fromEntries(this.sessions)
6388
6617
  );
6389
6618
  }
6390
6619
  persistBindings() {
6391
6620
  writeJson(
6392
- path6.join(DATA_DIR, "bindings.json"),
6621
+ BINDINGS_PATH,
6393
6622
  Object.fromEntries(this.bindings)
6394
6623
  );
6395
6624
  }
6396
6625
  persistPermissions() {
6397
6626
  writeJson(
6398
- path6.join(DATA_DIR, "permissions.json"),
6627
+ PERMISSIONS_PATH,
6399
6628
  Object.fromEntries(this.permissionLinks)
6400
6629
  );
6401
6630
  }
6402
6631
  persistOffsets() {
6403
6632
  writeJson(
6404
- path6.join(DATA_DIR, "offsets.json"),
6633
+ OFFSETS_PATH,
6405
6634
  Object.fromEntries(this.offsets)
6406
6635
  );
6407
6636
  }
6408
6637
  persistDedup() {
6409
6638
  writeJson(
6410
- path6.join(DATA_DIR, "dedup.json"),
6639
+ DEDUP_PATH,
6411
6640
  Object.fromEntries(this.dedupKeys)
6412
6641
  );
6413
6642
  }
6414
6643
  persistAudit() {
6415
- writeJson(path6.join(DATA_DIR, "audit.json"), this.auditLog);
6644
+ writeJson(AUDIT_PATH, this.auditLog);
6416
6645
  }
6417
6646
  persistMessages(sessionId) {
6418
6647
  const msgs = this.messages.get(sessionId) || [];
@@ -6429,6 +6658,32 @@ var JsonFileStore = class {
6429
6658
  this.messages.set(sessionId, msgs);
6430
6659
  return msgs;
6431
6660
  }
6661
+ mutateBindings(mutation) {
6662
+ return withFileLock(BINDINGS_PATH, () => {
6663
+ this.reloadBindings();
6664
+ const result = mutation();
6665
+ this.persistBindings();
6666
+ return result;
6667
+ });
6668
+ }
6669
+ mutateSessions(mutation) {
6670
+ return withFileLock(SESSIONS_PATH, () => {
6671
+ this.reloadSessions();
6672
+ const result = mutation();
6673
+ this.persistSessions();
6674
+ return result;
6675
+ });
6676
+ }
6677
+ mutateMessages(sessionId, mutation) {
6678
+ const messagePath = path6.join(MESSAGES_DIR, `${sessionId}.json`);
6679
+ return withFileLock(messagePath, () => {
6680
+ const messages = readJson(messagePath, []);
6681
+ this.messages.set(sessionId, messages);
6682
+ const result = mutation(messages);
6683
+ this.persistMessages(sessionId);
6684
+ return result;
6685
+ });
6686
+ }
6432
6687
  // ── Settings ──
6433
6688
  refreshSettings() {
6434
6689
  if (!this.dynamicSettings) return;
@@ -6451,66 +6706,65 @@ var JsonFileStore = class {
6451
6706
  return this.bindings.get(`${channelType}:${chatId}`) ?? null;
6452
6707
  }
6453
6708
  upsertChannelBinding(data) {
6454
- this.reloadBindings();
6455
- const key = `${data.channelType}:${data.chatId}`;
6456
- const existing = this.bindings.get(key);
6457
- if (existing) {
6458
- const updated = {
6459
- ...existing,
6709
+ return this.mutateBindings(() => {
6710
+ const key = `${data.channelType}:${data.chatId}`;
6711
+ const existing = this.bindings.get(key);
6712
+ if (existing) {
6713
+ const updated = {
6714
+ ...existing,
6715
+ codepilotSessionId: data.codepilotSessionId,
6716
+ sdkSessionId: data.sdkSessionId ?? existing.sdkSessionId,
6717
+ channelProvider: data.channelProvider ?? existing.channelProvider,
6718
+ channelAlias: data.channelAlias ?? existing.channelAlias,
6719
+ chatUserId: data.chatUserId ?? existing.chatUserId,
6720
+ chatDisplayName: data.chatDisplayName ?? existing.chatDisplayName,
6721
+ workingDirectory: data.workingDirectory,
6722
+ model: data.model,
6723
+ mode: data.mode ?? existing.mode,
6724
+ updatedAt: now()
6725
+ };
6726
+ this.bindings.set(key, updated);
6727
+ return updated;
6728
+ }
6729
+ const binding = {
6730
+ id: uuid(),
6731
+ channelType: data.channelType,
6732
+ channelProvider: data.channelProvider,
6733
+ channelAlias: data.channelAlias,
6734
+ chatId: data.chatId,
6735
+ chatUserId: data.chatUserId,
6736
+ chatDisplayName: data.chatDisplayName,
6460
6737
  codepilotSessionId: data.codepilotSessionId,
6461
- sdkSessionId: data.sdkSessionId ?? existing.sdkSessionId,
6462
- channelProvider: data.channelProvider ?? existing.channelProvider,
6463
- channelAlias: data.channelAlias ?? existing.channelAlias,
6464
- chatUserId: data.chatUserId ?? existing.chatUserId,
6465
- chatDisplayName: data.chatDisplayName ?? existing.chatDisplayName,
6738
+ sdkSessionId: data.sdkSessionId ?? "",
6466
6739
  workingDirectory: data.workingDirectory,
6467
6740
  model: data.model,
6468
- mode: data.mode ?? existing.mode,
6741
+ mode: data.mode || this.getSetting("bridge_default_mode") || "code",
6742
+ active: true,
6743
+ createdAt: now(),
6469
6744
  updatedAt: now()
6470
6745
  };
6471
- this.bindings.set(key, updated);
6472
- this.persistBindings();
6473
- return updated;
6474
- }
6475
- const binding = {
6476
- id: uuid(),
6477
- channelType: data.channelType,
6478
- channelProvider: data.channelProvider,
6479
- channelAlias: data.channelAlias,
6480
- chatId: data.chatId,
6481
- chatUserId: data.chatUserId,
6482
- chatDisplayName: data.chatDisplayName,
6483
- codepilotSessionId: data.codepilotSessionId,
6484
- sdkSessionId: data.sdkSessionId ?? "",
6485
- workingDirectory: data.workingDirectory,
6486
- model: data.model,
6487
- mode: data.mode || this.getSetting("bridge_default_mode") || "code",
6488
- active: true,
6489
- createdAt: now(),
6490
- updatedAt: now()
6491
- };
6492
- this.bindings.set(key, binding);
6493
- this.persistBindings();
6494
- return binding;
6746
+ this.bindings.set(key, binding);
6747
+ return binding;
6748
+ });
6495
6749
  }
6496
6750
  deleteChannelBinding(id) {
6497
- this.reloadBindings();
6498
- for (const [key, binding] of this.bindings) {
6499
- if (binding.id !== id) continue;
6500
- this.bindings.delete(key);
6501
- this.persistBindings();
6502
- return;
6503
- }
6751
+ this.mutateBindings(() => {
6752
+ for (const [key, binding] of this.bindings) {
6753
+ if (binding.id !== id) continue;
6754
+ this.bindings.delete(key);
6755
+ return;
6756
+ }
6757
+ });
6504
6758
  }
6505
6759
  updateChannelBinding(id, updates) {
6506
- this.reloadBindings();
6507
- for (const [key, b] of this.bindings) {
6508
- if (b.id === id) {
6509
- this.bindings.set(key, { ...b, ...updates, updatedAt: now() });
6510
- this.persistBindings();
6511
- break;
6760
+ this.mutateBindings(() => {
6761
+ for (const [key, b] of this.bindings) {
6762
+ if (b.id === id) {
6763
+ this.bindings.set(key, { ...b, ...updates, updatedAt: now() });
6764
+ break;
6765
+ }
6512
6766
  }
6513
- }
6767
+ });
6514
6768
  }
6515
6769
  listChannelBindings(channelType) {
6516
6770
  this.reloadBindings();
@@ -6537,74 +6791,78 @@ var JsonFileStore = class {
6537
6791
  return null;
6538
6792
  }
6539
6793
  createSession(name, model, systemPrompt, cwd, mode, options) {
6540
- this.reloadSessions();
6541
- const timestamp = now();
6542
- const session = {
6543
- id: uuid(),
6544
- name,
6545
- working_directory: cwd || process.cwd(),
6546
- model,
6547
- preferred_mode: mode,
6548
- system_prompt: systemPrompt,
6549
- reasoning_effort: options?.reasoningEffort,
6550
- session_type: options?.sessionType || "normal",
6551
- hidden: options?.hidden === true,
6552
- parent_session_id: options?.parentSessionId,
6553
- expires_at: options?.expiresAt,
6554
- created_at: timestamp,
6555
- updated_at: timestamp
6556
- };
6557
- this.sessions.set(session.id, session);
6558
- this.persistSessions();
6559
- return session;
6794
+ return this.mutateSessions(() => {
6795
+ const timestamp = now();
6796
+ const session = {
6797
+ id: uuid(),
6798
+ name,
6799
+ working_directory: cwd || process.cwd(),
6800
+ model,
6801
+ preferred_mode: mode,
6802
+ system_prompt: systemPrompt,
6803
+ reasoning_effort: options?.reasoningEffort,
6804
+ session_type: options?.sessionType || "normal",
6805
+ hidden: options?.hidden === true,
6806
+ parent_session_id: options?.parentSessionId,
6807
+ expires_at: options?.expiresAt,
6808
+ created_at: timestamp,
6809
+ updated_at: timestamp
6810
+ };
6811
+ this.sessions.set(session.id, session);
6812
+ return session;
6813
+ });
6560
6814
  }
6561
6815
  updateSessionProviderId(sessionId, providerId) {
6562
- this.reloadSessions();
6563
- const s = this.sessions.get(sessionId);
6564
- if (s) {
6816
+ this.mutateSessions(() => {
6817
+ const s = this.sessions.get(sessionId);
6818
+ if (!s) return;
6565
6819
  s.provider_id = providerId;
6566
6820
  s.updated_at = now();
6567
- this.persistSessions();
6568
- }
6821
+ });
6569
6822
  }
6570
6823
  updateSession(sessionId, updates, options) {
6571
- this.reloadSessions();
6572
- const session = this.sessions.get(sessionId);
6573
- if (!session) return;
6574
- const next = {
6575
- ...session,
6576
- ...updates,
6577
- id: session.id,
6578
- updated_at: options?.touch === false ? session.updated_at : now()
6579
- };
6580
- this.sessions.set(sessionId, next);
6581
- this.persistSessions();
6824
+ this.mutateSessions(() => {
6825
+ const session = this.sessions.get(sessionId);
6826
+ if (!session) return;
6827
+ const next = {
6828
+ ...session,
6829
+ ...updates,
6830
+ id: session.id,
6831
+ updated_at: options?.touch === false ? session.updated_at : now()
6832
+ };
6833
+ this.sessions.set(sessionId, next);
6834
+ });
6582
6835
  }
6583
6836
  deleteSession(sessionId) {
6584
- this.reloadSessions();
6585
- this.reloadBindings();
6586
- this.sessions.delete(sessionId);
6587
- for (const [key, binding] of this.bindings) {
6588
- if (binding.codepilotSessionId === sessionId) {
6589
- this.bindings.delete(key);
6837
+ const messagePath = path6.join(MESSAGES_DIR, `${sessionId}.json`);
6838
+ withFileLocks([SESSIONS_PATH, BINDINGS_PATH, messagePath], () => {
6839
+ this.reloadSessions();
6840
+ this.reloadBindings();
6841
+ this.sessions.delete(sessionId);
6842
+ for (const [key, binding] of this.bindings) {
6843
+ if (binding.codepilotSessionId === sessionId) {
6844
+ this.bindings.delete(key);
6845
+ }
6590
6846
  }
6591
- }
6592
- this.messages.delete(sessionId);
6593
- try {
6594
- fs5.rmSync(path6.join(MESSAGES_DIR, `${sessionId}.json`), { force: true });
6595
- } catch {
6596
- }
6597
- this.persistSessions();
6598
- this.persistBindings();
6847
+ this.messages.delete(sessionId);
6848
+ try {
6849
+ fs6.rmSync(messagePath, { force: true });
6850
+ } catch {
6851
+ }
6852
+ this.persistSessions();
6853
+ this.persistBindings();
6854
+ });
6599
6855
  }
6600
6856
  // ── Messages ──
6601
6857
  addMessage(sessionId, role, content, _usage) {
6602
- const msgs = this.loadMessages(sessionId);
6603
- msgs.push({ role, content });
6604
- this.persistMessages(sessionId);
6858
+ this.mutateMessages(sessionId, (messages) => {
6859
+ messages.push({ role, content });
6860
+ });
6605
6861
  }
6606
6862
  getMessages(sessionId, opts) {
6607
- const msgs = this.loadMessages(sessionId);
6863
+ const messagePath = path6.join(MESSAGES_DIR, `${sessionId}.json`);
6864
+ const msgs = readJson(messagePath, []);
6865
+ this.messages.set(sessionId, msgs);
6608
6866
  if (opts?.limit && opts.limit > 0) {
6609
6867
  return { messages: msgs.slice(-opts.limit) };
6610
6868
  }
@@ -6636,61 +6894,62 @@ var JsonFileStore = class {
6636
6894
  }
6637
6895
  }
6638
6896
  setSessionRuntimeStatus(_sessionId, _status) {
6639
- this.reloadSessions();
6640
- const session = this.sessions.get(_sessionId);
6641
- if (!session) return;
6642
- const queuedCount = session.queued_count && session.queued_count > 0 ? session.queued_count : 0;
6643
- let runtimeStatus;
6644
- if (_status === "running") {
6645
- runtimeStatus = queuedCount > 0 ? "queued" : "running";
6646
- } else if (_status === "idle") {
6647
- runtimeStatus = queuedCount > 0 ? "queued" : "idle";
6648
- } else {
6649
- runtimeStatus = session.runtime_status;
6650
- }
6651
- const next = {
6652
- ...session,
6653
- runtime_status: runtimeStatus,
6654
- last_runtime_update_at: now(),
6655
- updated_at: now()
6656
- };
6657
- this.sessions.set(_sessionId, next);
6658
- this.persistSessions();
6897
+ this.mutateSessions(() => {
6898
+ const session = this.sessions.get(_sessionId);
6899
+ if (!session) return;
6900
+ const queuedCount = session.queued_count && session.queued_count > 0 ? session.queued_count : 0;
6901
+ let runtimeStatus;
6902
+ if (_status === "running") {
6903
+ runtimeStatus = queuedCount > 0 ? "queued" : "running";
6904
+ } else if (_status === "idle") {
6905
+ runtimeStatus = queuedCount > 0 ? "queued" : "idle";
6906
+ } else {
6907
+ runtimeStatus = session.runtime_status;
6908
+ }
6909
+ const next = {
6910
+ ...session,
6911
+ runtime_status: runtimeStatus,
6912
+ last_runtime_update_at: now(),
6913
+ updated_at: now()
6914
+ };
6915
+ this.sessions.set(_sessionId, next);
6916
+ });
6659
6917
  }
6660
6918
  // ── SDK Session ──
6661
6919
  updateSdkSessionId(sessionId, sdkSessionId) {
6662
- this.reloadSessions();
6663
- this.reloadBindings();
6664
- const s = this.sessions.get(sessionId);
6665
- if (s) {
6666
- s.sdk_session_id = sdkSessionId;
6667
- if (sdkSessionId) {
6668
- s.codex_thread_id = sdkSessionId;
6669
- s.thread_origin = s.thread_origin || "bridge";
6670
- } else {
6671
- delete s.codex_thread_id;
6672
- if (s.thread_origin !== "desktop") {
6673
- delete s.thread_origin;
6920
+ withFileLocks([SESSIONS_PATH, BINDINGS_PATH], () => {
6921
+ this.reloadSessions();
6922
+ this.reloadBindings();
6923
+ const s = this.sessions.get(sessionId);
6924
+ if (s) {
6925
+ s.sdk_session_id = sdkSessionId;
6926
+ if (sdkSessionId) {
6927
+ s.codex_thread_id = sdkSessionId;
6928
+ s.thread_origin = s.thread_origin || "bridge";
6929
+ } else {
6930
+ delete s.codex_thread_id;
6931
+ if (s.thread_origin !== "desktop") {
6932
+ delete s.thread_origin;
6933
+ }
6674
6934
  }
6935
+ s.updated_at = now();
6675
6936
  }
6676
- s.updated_at = now();
6677
- this.persistSessions();
6678
- }
6679
- for (const [key, b] of this.bindings) {
6680
- if (b.codepilotSessionId === sessionId) {
6681
- this.bindings.set(key, { ...b, sdkSessionId, updatedAt: now() });
6937
+ for (const [key, b] of this.bindings) {
6938
+ if (b.codepilotSessionId === sessionId) {
6939
+ this.bindings.set(key, { ...b, sdkSessionId, updatedAt: now() });
6940
+ }
6682
6941
  }
6683
- }
6684
- this.persistBindings();
6942
+ this.persistSessions();
6943
+ this.persistBindings();
6944
+ });
6685
6945
  }
6686
6946
  updateSessionModel(sessionId, model) {
6687
- this.reloadSessions();
6688
- const s = this.sessions.get(sessionId);
6689
- if (s) {
6947
+ this.mutateSessions(() => {
6948
+ const s = this.sessions.get(sessionId);
6949
+ if (!s) return;
6690
6950
  s.model = model;
6691
6951
  s.updated_at = now();
6692
- this.persistSessions();
6693
- }
6952
+ });
6694
6953
  }
6695
6954
  syncSdkTasks(_sessionId, _todos) {
6696
6955
  }
@@ -6703,66 +6962,84 @@ var JsonFileStore = class {
6703
6962
  }
6704
6963
  // ── Audit & Dedup ──
6705
6964
  insertAuditLog(entry) {
6706
- this.auditLog.push({
6707
- ...entry,
6708
- id: uuid(),
6709
- createdAt: now()
6965
+ withFileLock(AUDIT_PATH, () => {
6966
+ this.reloadAudit();
6967
+ this.auditLog.push({
6968
+ ...entry,
6969
+ id: uuid(),
6970
+ createdAt: now()
6971
+ });
6972
+ if (this.auditLog.length > 1e3) {
6973
+ this.auditLog = this.auditLog.slice(-1e3);
6974
+ }
6975
+ this.persistAudit();
6710
6976
  });
6711
- if (this.auditLog.length > 1e3) {
6712
- this.auditLog = this.auditLog.slice(-1e3);
6713
- }
6714
- this.persistAudit();
6715
6977
  }
6716
6978
  checkDedup(key) {
6979
+ this.reloadDedup();
6717
6980
  const ts = this.dedupKeys.get(key);
6718
6981
  if (ts === void 0) return false;
6719
- if (Date.now() - ts > 5 * 60 * 1e3) {
6982
+ if (Date.now() - ts > dedupTtlMs(key)) {
6720
6983
  this.dedupKeys.delete(key);
6721
6984
  return false;
6722
6985
  }
6723
6986
  return true;
6724
6987
  }
6725
6988
  insertDedup(key) {
6726
- this.dedupKeys.set(key, Date.now());
6727
- this.persistDedup();
6989
+ withFileLock(DEDUP_PATH, () => {
6990
+ this.reloadDedup();
6991
+ this.dedupKeys.set(key, Date.now());
6992
+ this.persistDedup();
6993
+ });
6728
6994
  }
6729
6995
  cleanupExpiredDedup() {
6730
- const cutoff = Date.now() - 5 * 60 * 1e3;
6731
- let changed = false;
6732
- for (const [key, ts] of this.dedupKeys) {
6733
- if (ts < cutoff) {
6734
- this.dedupKeys.delete(key);
6735
- changed = true;
6996
+ withFileLock(DEDUP_PATH, () => {
6997
+ this.reloadDedup();
6998
+ const timestamp = Date.now();
6999
+ let changed = false;
7000
+ for (const [key, ts] of this.dedupKeys) {
7001
+ if (timestamp - ts > dedupTtlMs(key)) {
7002
+ this.dedupKeys.delete(key);
7003
+ changed = true;
7004
+ }
6736
7005
  }
6737
- }
6738
- if (changed) this.persistDedup();
7006
+ if (changed) this.persistDedup();
7007
+ });
6739
7008
  }
6740
7009
  insertOutboundRef(_ref) {
6741
7010
  }
6742
7011
  // ── Permission Links ──
6743
7012
  insertPermissionLink(link) {
6744
- const record = {
6745
- permissionRequestId: link.permissionRequestId,
6746
- chatId: link.chatId,
6747
- messageId: link.messageId,
6748
- sessionId: link.sessionId,
6749
- resolved: false,
6750
- suggestions: link.suggestions
6751
- };
6752
- this.permissionLinks.set(link.permissionRequestId, record);
6753
- this.persistPermissions();
7013
+ withFileLock(PERMISSIONS_PATH, () => {
7014
+ this.reloadPermissions();
7015
+ const record = {
7016
+ permissionRequestId: link.permissionRequestId,
7017
+ chatId: link.chatId,
7018
+ messageId: link.messageId,
7019
+ sessionId: link.sessionId,
7020
+ resolved: false,
7021
+ suggestions: link.suggestions
7022
+ };
7023
+ this.permissionLinks.set(link.permissionRequestId, record);
7024
+ this.persistPermissions();
7025
+ });
6754
7026
  }
6755
7027
  getPermissionLink(permissionRequestId) {
7028
+ this.reloadPermissions();
6756
7029
  return this.permissionLinks.get(permissionRequestId) ?? null;
6757
7030
  }
6758
7031
  markPermissionLinkResolved(permissionRequestId) {
6759
- const link = this.permissionLinks.get(permissionRequestId);
6760
- if (!link || link.resolved) return false;
6761
- link.resolved = true;
6762
- this.persistPermissions();
6763
- return true;
7032
+ return withFileLock(PERMISSIONS_PATH, () => {
7033
+ this.reloadPermissions();
7034
+ const link = this.permissionLinks.get(permissionRequestId);
7035
+ if (!link || link.resolved) return false;
7036
+ link.resolved = true;
7037
+ this.persistPermissions();
7038
+ return true;
7039
+ });
6764
7040
  }
6765
7041
  listPendingPermissionLinksByChat(chatId) {
7042
+ this.reloadPermissions();
6766
7043
  const result = [];
6767
7044
  for (const link of this.permissionLinks.values()) {
6768
7045
  if (link.chatId === chatId && !link.resolved) {
@@ -6773,23 +7050,27 @@ var JsonFileStore = class {
6773
7050
  }
6774
7051
  // ── Channel Offsets ──
6775
7052
  getChannelOffset(key) {
7053
+ this.reloadOffsets();
6776
7054
  return this.offsets.get(key) ?? "0";
6777
7055
  }
6778
7056
  setChannelOffset(key, offset) {
6779
- this.offsets.set(key, offset);
6780
- this.persistOffsets();
7057
+ withFileLock(OFFSETS_PATH, () => {
7058
+ this.reloadOffsets();
7059
+ this.offsets.set(key, offset);
7060
+ this.persistOffsets();
7061
+ });
6781
7062
  }
6782
7063
  };
6783
7064
 
6784
7065
  // src/weixin-login.ts
6785
7066
  var import_qrcode = __toESM(require_lib(), 1);
6786
- import fs7 from "node:fs";
7067
+ import fs8 from "node:fs";
6787
7068
  import path8 from "node:path";
6788
- import crypto4 from "node:crypto";
7069
+ import crypto7 from "node:crypto";
6789
7070
  import { spawn as spawn2 } from "node:child_process";
6790
7071
 
6791
7072
  // src/adapters/weixin/weixin-api.ts
6792
- import crypto3 from "node:crypto";
7073
+ import crypto5 from "node:crypto";
6793
7074
 
6794
7075
  // src/adapters/weixin/weixin-types.ts
6795
7076
  var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
@@ -6838,7 +7119,8 @@ async function pollLoginQrStatus(qrcode, baseUrl) {
6838
7119
  }
6839
7120
 
6840
7121
  // src/weixin-store.ts
6841
- import fs6 from "node:fs";
7122
+ import fs7 from "node:fs";
7123
+ import crypto6 from "node:crypto";
6842
7124
  import path7 from "node:path";
6843
7125
  var DATA_DIR2 = path7.join(CTI_HOME, "data");
6844
7126
  var ACCOUNTS_PATH = path7.join(DATA_DIR2, "weixin-accounts.json");
@@ -6846,19 +7128,27 @@ var CONTEXT_TOKENS_PATH = path7.join(DATA_DIR2, "weixin-context-tokens.json");
6846
7128
  var DEFAULT_BASE_URL2 = "https://ilinkai.weixin.qq.com";
6847
7129
  var DEFAULT_CDN_BASE_URL2 = "https://novac2c.cdn.weixin.qq.com/c2c";
6848
7130
  function ensureDir2(dir) {
6849
- fs6.mkdirSync(dir, { recursive: true });
7131
+ fs7.mkdirSync(dir, { recursive: true });
6850
7132
  }
6851
7133
  function atomicWrite2(filePath, data) {
6852
- const tmpPath = `${filePath}.tmp`;
6853
- fs6.writeFileSync(tmpPath, data, "utf-8");
6854
- fs6.renameSync(tmpPath, filePath);
7134
+ const tmpPath = `${filePath}.${process.pid}.${crypto6.randomUUID()}.tmp`;
7135
+ try {
7136
+ fs7.writeFileSync(tmpPath, data, "utf-8");
7137
+ fs7.renameSync(tmpPath, filePath);
7138
+ } finally {
7139
+ try {
7140
+ fs7.rmSync(tmpPath, { force: true });
7141
+ } catch {
7142
+ }
7143
+ }
6855
7144
  }
6856
7145
  function readJson2(filePath, fallback) {
6857
7146
  try {
6858
- const raw = fs6.readFileSync(filePath, "utf-8");
7147
+ const raw = fs7.readFileSync(filePath, "utf-8");
6859
7148
  return JSON.parse(raw);
6860
- } catch {
6861
- return fallback;
7149
+ } catch (error) {
7150
+ if (error.code === "ENOENT") return fallback;
7151
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u5FAE\u4FE1\u6570\u636E\u6587\u4EF6 ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
6862
7152
  }
6863
7153
  }
6864
7154
  function now2() {
@@ -6907,37 +7197,38 @@ function listWeixinAccounts() {
6907
7197
  return sortAccountsByRecency(readAccounts());
6908
7198
  }
6909
7199
  function upsertWeixinAccount(params) {
6910
- const accounts = readAccounts();
6911
- const existing = accounts.find((account2) => account2.accountId === params.accountId);
6912
- const timestamp = now2();
6913
- const account = existing ? {
6914
- ...existing,
6915
- userId: params.userId ?? existing.userId,
6916
- baseUrl: params.baseUrl ?? existing.baseUrl,
6917
- cdnBaseUrl: params.cdnBaseUrl ?? existing.cdnBaseUrl,
6918
- token: params.token ?? existing.token,
6919
- name: params.name ?? existing.name,
6920
- enabled: params.enabled ?? existing.enabled,
6921
- lastLoginAt: timestamp,
6922
- updatedAt: timestamp
6923
- } : {
6924
- accountId: params.accountId,
6925
- userId: params.userId ?? "",
6926
- baseUrl: params.baseUrl ?? DEFAULT_BASE_URL2,
6927
- cdnBaseUrl: params.cdnBaseUrl ?? DEFAULT_CDN_BASE_URL2,
6928
- token: params.token ?? "",
6929
- name: params.name ?? params.accountId,
6930
- enabled: params.enabled ?? true,
6931
- lastLoginAt: timestamp,
6932
- createdAt: timestamp,
6933
- updatedAt: timestamp
6934
- };
6935
- const nextAccounts = [
6936
- account,
6937
- ...accounts.filter((item) => item.accountId !== account.accountId)
6938
- ];
6939
- writeAccounts(nextAccounts);
6940
- return account;
7200
+ return withFileLock(ACCOUNTS_PATH, () => {
7201
+ const accounts = readAccounts();
7202
+ const existing = accounts.find((account2) => account2.accountId === params.accountId);
7203
+ const timestamp = now2();
7204
+ const account = existing ? {
7205
+ ...existing,
7206
+ userId: params.userId ?? existing.userId,
7207
+ baseUrl: params.baseUrl ?? existing.baseUrl,
7208
+ cdnBaseUrl: params.cdnBaseUrl ?? existing.cdnBaseUrl,
7209
+ token: params.token ?? existing.token,
7210
+ name: params.name ?? existing.name,
7211
+ enabled: params.enabled ?? existing.enabled,
7212
+ lastLoginAt: timestamp,
7213
+ updatedAt: timestamp
7214
+ } : {
7215
+ accountId: params.accountId,
7216
+ userId: params.userId ?? "",
7217
+ baseUrl: params.baseUrl ?? DEFAULT_BASE_URL2,
7218
+ cdnBaseUrl: params.cdnBaseUrl ?? DEFAULT_CDN_BASE_URL2,
7219
+ token: params.token ?? "",
7220
+ name: params.name ?? params.accountId,
7221
+ enabled: params.enabled ?? true,
7222
+ lastLoginAt: timestamp,
7223
+ createdAt: timestamp,
7224
+ updatedAt: timestamp
7225
+ };
7226
+ writeAccounts([
7227
+ account,
7228
+ ...accounts.filter((item) => item.accountId !== account.accountId)
7229
+ ]);
7230
+ return account;
7231
+ });
6941
7232
  }
6942
7233
 
6943
7234
  // src/weixin-login.ts
@@ -6949,7 +7240,7 @@ var RUNTIME_DIR = path8.join(CTI_HOME, "runtime");
6949
7240
  var HTML_PATH = path8.join(RUNTIME_DIR, "weixin-login.html");
6950
7241
  var webLoginSessions = /* @__PURE__ */ new Map();
6951
7242
  function ensureRuntimeDir() {
6952
- fs7.mkdirSync(RUNTIME_DIR, { recursive: true });
7243
+ fs8.mkdirSync(RUNTIME_DIR, { recursive: true });
6953
7244
  }
6954
7245
  function escapeHtml(text2) {
6955
7246
  return text2.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
@@ -7262,7 +7553,7 @@ function buildWeixinLoginPopupHtml(sessionId) {
7262
7553
  async function writeQrHtml(session) {
7263
7554
  ensureRuntimeDir();
7264
7555
  const qrSvg = await buildQrSvg(session.qrImageUrl);
7265
- fs7.writeFileSync(HTML_PATH, buildQrHtml(qrSvg), "utf-8");
7556
+ fs8.writeFileSync(HTML_PATH, buildQrHtml(qrSvg), "utf-8");
7266
7557
  }
7267
7558
  function openQrHtml() {
7268
7559
  try {
@@ -7451,7 +7742,7 @@ async function startWeixinLoginWebSession(options = {}) {
7451
7742
  ensureRuntimeDir();
7452
7743
  const config = options.config || {};
7453
7744
  const seed = await createSession(0, config.baseUrl);
7454
- const sessionId = crypto4.randomUUID();
7745
+ const sessionId = crypto7.randomUUID();
7455
7746
  const qrSvg = await buildQrSvg(seed.qrImageUrl);
7456
7747
  const session = {
7457
7748
  id: sessionId,
@@ -7533,7 +7824,7 @@ if (isMainModule) {
7533
7824
  }
7534
7825
 
7535
7826
  // src/codex-models.ts
7536
- import fs8 from "node:fs";
7827
+ import fs9 from "node:fs";
7537
7828
  import os4 from "node:os";
7538
7829
  import path9 from "node:path";
7539
7830
  var DEFAULT_CODEX_CONFIG_PATH = path9.join(os4.homedir(), ".codex", "config.toml");
@@ -7586,7 +7877,7 @@ var KNOWN_CODEX_MODEL_FALLBACKS = [
7586
7877
  ];
7587
7878
  function readConfiguredCodexModel(configPath = DEFAULT_CODEX_CONFIG_PATH) {
7588
7879
  try {
7589
- const raw = fs8.readFileSync(configPath, "utf-8");
7880
+ const raw = fs9.readFileSync(configPath, "utf-8");
7590
7881
  let inSection = false;
7591
7882
  for (const line of raw.split(/\r?\n/)) {
7592
7883
  const trimmed = line.trim();
@@ -7630,7 +7921,7 @@ function parseSupportedReasoningLevels(value) {
7630
7921
  }
7631
7922
  function listCachedCodexModels(cachePath = DEFAULT_CODEX_MODELS_CACHE_PATH) {
7632
7923
  try {
7633
- const raw = fs8.readFileSync(cachePath, "utf-8");
7924
+ const raw = fs9.readFileSync(cachePath, "utf-8");
7634
7925
  const parsed = JSON.parse(raw);
7635
7926
  if (!Array.isArray(parsed.models)) return [];
7636
7927
  const seen = /* @__PURE__ */ new Set();
@@ -7696,6 +7987,74 @@ function listAvailableCodexModels(cachePath = DEFAULT_CODEX_MODELS_CACHE_PATH, a
7696
7987
  return merged;
7697
7988
  }
7698
7989
 
7990
+ // src/ui-http-security.ts
7991
+ var DEFAULT_JSON_BODY_LIMIT_BYTES = 1024 * 1024;
7992
+ var HttpRequestError = class extends Error {
7993
+ constructor(statusCode, message) {
7994
+ super(message);
7995
+ this.statusCode = statusCode;
7996
+ this.name = "HttpRequestError";
7997
+ }
7998
+ };
7999
+ function isMutationMethod(method) {
8000
+ return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE";
8001
+ }
8002
+ function getExpectedOrigin(headers) {
8003
+ const host = headers.host?.trim();
8004
+ if (!host) throw new HttpRequestError(400, "\u8BF7\u6C42\u7F3A\u5C11 Host header\u3002");
8005
+ return `http://${host}`;
8006
+ }
8007
+ function assertTrustedMutationRequest(request) {
8008
+ if (!isMutationMethod(request.method)) return;
8009
+ const fetchSite = request.headers["sec-fetch-site"];
8010
+ if (fetchSite === "cross-site") {
8011
+ throw new HttpRequestError(403, "\u5DF2\u62D2\u7EDD\u8DE8\u7AD9\u7BA1\u7406\u8BF7\u6C42\u3002");
8012
+ }
8013
+ const origin = request.headers.origin;
8014
+ if (!origin) return;
8015
+ if (origin === "null") {
8016
+ throw new HttpRequestError(403, "\u5DF2\u62D2\u7EDD\u6765\u6E90\u4E0D\u660E\u7684\u7BA1\u7406\u8BF7\u6C42\u3002");
8017
+ }
8018
+ let normalizedOrigin;
8019
+ try {
8020
+ normalizedOrigin = new URL(origin).origin;
8021
+ } catch {
8022
+ throw new HttpRequestError(403, "\u8BF7\u6C42 Origin \u65E0\u6548\u3002");
8023
+ }
8024
+ if (normalizedOrigin !== getExpectedOrigin(request.headers)) {
8025
+ throw new HttpRequestError(403, "\u5DF2\u62D2\u7EDD\u975E\u540C\u6E90\u7BA1\u7406\u8BF7\u6C42\u3002");
8026
+ }
8027
+ }
8028
+ async function readJsonBody(request, maxBytes = DEFAULT_JSON_BODY_LIMIT_BYTES) {
8029
+ const boundedMaxBytes = Math.max(1, maxBytes);
8030
+ const declaredLength = Number(request.headers["content-length"]);
8031
+ if (Number.isFinite(declaredLength) && declaredLength > boundedMaxBytes) {
8032
+ throw new HttpRequestError(413, `\u8BF7\u6C42\u4F53\u8D85\u8FC7 ${boundedMaxBytes} \u5B57\u8282\u9650\u5236\u3002`);
8033
+ }
8034
+ const chunks = [];
8035
+ let receivedBytes = 0;
8036
+ for await (const chunk of request) {
8037
+ const buffer = Buffer.from(chunk);
8038
+ receivedBytes += buffer.length;
8039
+ if (receivedBytes > boundedMaxBytes) {
8040
+ request.resume();
8041
+ throw new HttpRequestError(413, `\u8BF7\u6C42\u4F53\u8D85\u8FC7 ${boundedMaxBytes} \u5B57\u8282\u9650\u5236\u3002`);
8042
+ }
8043
+ chunks.push(buffer);
8044
+ }
8045
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
8046
+ if (!raw) return {};
8047
+ const contentType = request.headers["content-type"] || "";
8048
+ if (!/^application\/json(?:\s*;|$)/i.test(contentType)) {
8049
+ throw new HttpRequestError(415, "\u8BF7\u6C42\u4F53\u5FC5\u987B\u4F7F\u7528 application/json\u3002");
8050
+ }
8051
+ try {
8052
+ return JSON.parse(raw);
8053
+ } catch {
8054
+ throw new HttpRequestError(400, "\u8BF7\u6C42\u4F53\u4E0D\u662F\u6709\u6548 JSON\u3002");
8055
+ }
8056
+ }
8057
+
7699
8058
  // src/ui-server.ts
7700
8059
  var port = 4781;
7701
8060
  var serverStartTime = (/* @__PURE__ */ new Date()).toISOString();
@@ -7781,14 +8140,6 @@ function text(response, statusCode, body) {
7781
8140
  response.writeHead(statusCode, { "Content-Type": "text/plain; charset=utf-8" });
7782
8141
  response.end(body);
7783
8142
  }
7784
- async function readJsonBody(request) {
7785
- const chunks = [];
7786
- for await (const chunk of request) {
7787
- chunks.push(Buffer.from(chunk));
7788
- }
7789
- const raw = Buffer.concat(chunks).toString("utf-8").trim();
7790
- return raw ? JSON.parse(raw) : {};
7791
- }
7792
8143
  function asString(value) {
7793
8144
  if (typeof value !== "string") return void 0;
7794
8145
  const trimmed = value.trim();
@@ -7869,14 +8220,14 @@ function channelToPayload(channel) {
7869
8220
  };
7870
8221
  }
7871
8222
  function generateAccessToken() {
7872
- return crypto5.randomBytes(18).toString("base64url");
8223
+ return crypto8.randomBytes(18).toString("base64url");
7873
8224
  }
7874
8225
  function timingSafeMatch(left, right) {
7875
8226
  if (!left || !right) return false;
7876
8227
  const leftBuffer = Buffer.from(left);
7877
8228
  const rightBuffer = Buffer.from(right);
7878
8229
  if (leftBuffer.length !== rightBuffer.length) return false;
7879
- return crypto5.timingSafeEqual(leftBuffer, rightBuffer);
8230
+ return crypto8.timingSafeEqual(leftBuffer, rightBuffer);
7880
8231
  }
7881
8232
  function parseCookies(request) {
7882
8233
  const header = request.headers.cookie;
@@ -11627,6 +11978,11 @@ function renderHtml() {
11627
11978
  }
11628
11979
  var server = http.createServer(async (request, response) => {
11629
11980
  try {
11981
+ response.setHeader("X-Content-Type-Options", "nosniff");
11982
+ response.setHeader("X-Frame-Options", "DENY");
11983
+ response.setHeader("Referrer-Policy", "no-referrer");
11984
+ response.setHeader("Cache-Control", "no-store");
11985
+ assertTrustedMutationRequest(request);
11630
11986
  const url = new URL(request.url || "/", getUiServerUrl(port));
11631
11987
  const config = loadConfig();
11632
11988
  const localRequest = isLocalRequest(request);
@@ -11964,16 +12320,18 @@ var server = http.createServer(async (request, response) => {
11964
12320
  }
11965
12321
  text(response, 404, "Not found");
11966
12322
  } catch (error) {
11967
- json(response, 500, {
12323
+ const statusCode = error instanceof HttpRequestError ? error.statusCode : 500;
12324
+ json(response, statusCode, {
11968
12325
  error: error instanceof Error ? error.message : String(error)
11969
12326
  });
11970
12327
  }
11971
12328
  });
11972
12329
  async function startServer() {
11973
12330
  port = await resolveUiPort(parsePreferredPort());
12331
+ const listenHost = loadConfig().uiAllowLan === true ? "0.0.0.0" : "127.0.0.1";
11974
12332
  await new Promise((resolve, reject) => {
11975
12333
  server.once("error", reject);
11976
- server.listen(port, "0.0.0.0", () => {
12334
+ server.listen(port, listenHost, () => {
11977
12335
  server.removeListener("error", reject);
11978
12336
  resolve();
11979
12337
  });