codexmate 0.0.17 → 0.0.19

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/cli.js CHANGED
@@ -38,6 +38,7 @@ const {
38
38
  writeJsonAtomic,
39
39
  formatTimestampForFileName
40
40
  } = require('./lib/cli-file-utils');
41
+ const { buildLineDiff } = require('./lib/text-diff');
41
42
  const {
42
43
  extractModelNames,
43
44
  hasModelsListPayload,
@@ -63,7 +64,8 @@ const {
63
64
  } = require('./lib/workflow-engine');
64
65
 
65
66
  const DEFAULT_WEB_PORT = 3737;
66
- const DEFAULT_WEB_HOST = '127.0.0.1';
67
+ const DEFAULT_WEB_HOST = '0.0.0.0';
68
+ const DEFAULT_WEB_OPEN_HOST = '127.0.0.1';
67
69
 
68
70
  // ============================================================================
69
71
  // 配置
@@ -78,6 +80,9 @@ const CURRENT_MODELS_FILE = path.join(CONFIG_DIR, 'provider-current-models.json'
78
80
  const INIT_MARK_FILE = path.join(CONFIG_DIR, 'codexmate-init.json');
79
81
  const BUILTIN_PROXY_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-proxy.json');
80
82
  const CODEX_SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
83
+ const SESSION_TRASH_DIR = path.join(CONFIG_DIR, 'codexmate-session-trash');
84
+ const SESSION_TRASH_FILES_DIR = path.join(SESSION_TRASH_DIR, 'files');
85
+ const SESSION_TRASH_INDEX_FILE = path.join(SESSION_TRASH_DIR, 'index.json');
81
86
  const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
82
87
  const OPENCLAW_CONFIG_FILE = path.join(OPENCLAW_DIR, 'openclaw.json');
83
88
  const OPENCLAW_WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
@@ -94,6 +99,7 @@ const DEFAULT_MODELS = ['gpt-5.3-codex', 'gpt-5.1-codex-max', 'gpt-4-turbo', 'gp
94
99
  const SPEED_TEST_TIMEOUT_MS = 8000;
95
100
  const HEALTH_CHECK_TIMEOUT_MS = 6000;
96
101
  const MAX_SESSION_LIST_SIZE = 300;
102
+ const MAX_SESSION_TRASH_LIST_SIZE = 500;
97
103
  const MAX_EXPORT_MESSAGES = 1000;
98
104
  const DEFAULT_SESSION_DETAIL_MESSAGES = 300;
99
105
  const MAX_SESSION_DETAIL_MESSAGES = 1000;
@@ -102,6 +108,7 @@ const CODEXMATE_MANAGED_MARKER = '# codexmate-managed: true';
102
108
  const SESSION_LIST_CACHE_TTL_MS = 4000;
103
109
  const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
104
110
  const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
111
+ const EXACT_MESSAGE_COUNT_CACHE_MAX_ENTRIES = 800;
105
112
  const DEFAULT_CONTENT_SCAN_LIMIT = 50;
106
113
  const SESSION_SCAN_FACTOR = 4;
107
114
  const SESSION_SCAN_MIN_FILES = 800;
@@ -110,9 +117,13 @@ const AGENTS_FILE_NAME = 'AGENTS.md';
110
117
  const CODEX_SKILLS_DIR = path.join(CONFIG_DIR, 'skills');
111
118
  const CLAUDE_SKILLS_DIR = path.join(CLAUDE_DIR, 'skills');
112
119
  const AGENTS_SKILLS_DIR = path.join(os.homedir(), '.agents', 'skills');
120
+ const SKILL_TARGETS = Object.freeze([
121
+ Object.freeze({ app: 'codex', label: 'Codex', dir: getCodexSkillsDir() }),
122
+ Object.freeze({ app: 'claude', label: 'Claude Code', dir: getClaudeSkillsDir() })
123
+ ]);
113
124
  const SKILL_IMPORT_SOURCES = Object.freeze([
114
- { app: 'claude', label: 'Claude Code', dir: CLAUDE_SKILLS_DIR },
115
- { app: 'agents', label: 'Agents', dir: AGENTS_SKILLS_DIR }
125
+ ...SKILL_TARGETS,
126
+ Object.freeze({ app: 'agents', label: 'Agents', dir: AGENTS_SKILLS_DIR })
116
127
  ]);
117
128
  const MODELS_CACHE_TTL_MS = 60 * 1000;
118
129
  const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
@@ -121,6 +132,7 @@ const MODELS_RESPONSE_MAX_BYTES = 1024 * 1024;
121
132
  const MAX_RECENT_CONFIGS = 3;
122
133
  const MAX_UPLOAD_SIZE = 200 * 1024 * 1024;
123
134
  const MAX_SKILLS_ZIP_UPLOAD_SIZE = 20 * 1024 * 1024;
135
+ const MAX_API_BODY_SIZE = 4 * 1024 * 1024;
124
136
  const MAX_SKILLS_ZIP_ENTRY_COUNT = 2000;
125
137
  const MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
126
138
  const DEFAULT_EXTRACT_SUFFIXES = Object.freeze(['.json']);
@@ -160,6 +172,38 @@ const CLI_INSTALL_TARGETS = Object.freeze([
160
172
  const HTTP_KEEP_ALIVE_AGENT = new http.Agent({ keepAlive: true });
161
173
  const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
162
174
 
175
+ function getCodexSkillsDir() {
176
+ const envCodexHome = typeof process.env.CODEX_HOME === 'string' ? process.env.CODEX_HOME.trim() : '';
177
+ if (envCodexHome) {
178
+ const target = path.join(envCodexHome, 'skills');
179
+ return resolveExistingDir([target], target);
180
+ }
181
+ const xdgConfig = typeof process.env.XDG_CONFIG_HOME === 'string' ? process.env.XDG_CONFIG_HOME.trim() : '';
182
+ if (xdgConfig) {
183
+ const target = path.join(xdgConfig, 'codex', 'skills');
184
+ return resolveExistingDir([target], target);
185
+ }
186
+ const homeConfigDir = path.join(os.homedir(), '.config', 'codex', 'skills');
187
+ return resolveExistingDir([homeConfigDir], CODEX_SKILLS_DIR);
188
+ }
189
+
190
+ function getClaudeSkillsDir() {
191
+ const envClaudeHome = typeof process.env.CLAUDE_HOME === 'string' && process.env.CLAUDE_HOME.trim()
192
+ ? process.env.CLAUDE_HOME.trim()
193
+ : (typeof process.env.CLAUDE_CONFIG_DIR === 'string' ? process.env.CLAUDE_CONFIG_DIR.trim() : '');
194
+ if (envClaudeHome) {
195
+ const target = path.join(envClaudeHome, 'skills');
196
+ return resolveExistingDir([target], target);
197
+ }
198
+ const xdgConfig = typeof process.env.XDG_CONFIG_HOME === 'string' ? process.env.XDG_CONFIG_HOME.trim() : '';
199
+ if (xdgConfig) {
200
+ const target = path.join(xdgConfig, 'claude', 'skills');
201
+ return resolveExistingDir([target], target);
202
+ }
203
+ const homeConfigDir = path.join(os.homedir(), '.config', 'claude', 'skills');
204
+ return resolveExistingDir([homeConfigDir], CLAUDE_SKILLS_DIR);
205
+ }
206
+
163
207
  function resolveWebPort() {
164
208
  const raw = process.env.CODEXMATE_PORT;
165
209
  if (!raw) return DEFAULT_WEB_PORT;
@@ -202,13 +246,14 @@ stream_idle_timeout_ms = 300000
202
246
 
203
247
  let g_initNotice = '';
204
248
  let g_sessionListCache = new Map();
249
+ let g_exactMessageCountCache = new Map();
205
250
  let g_modelsCache = new Map();
206
251
  let g_modelsInFlight = new Map();
207
252
  let g_builtinProxyRuntime = null;
208
253
  const DEFAULT_LOCAL_PROVIDER_NAME = 'local';
209
254
 
210
255
  function isBuiltinProxyProvider(providerName) {
211
- return typeof providerName === 'string' && providerName.trim() === BUILTIN_PROXY_PROVIDER_NAME;
256
+ return typeof providerName === 'string' && providerName.trim().toLowerCase() === BUILTIN_PROXY_PROVIDER_NAME.toLowerCase();
212
257
  }
213
258
 
214
259
  function isReservedProviderNameForCreation(providerName) {
@@ -881,12 +926,18 @@ function normalizeAuthRegistry(raw) {
881
926
  };
882
927
  }
883
928
 
929
+ function ensureAuthProfileStoragePrepared() {
930
+ ensureDir(AUTH_PROFILES_DIR);
931
+ }
932
+
884
933
  function readAuthRegistry() {
934
+ ensureAuthProfileStoragePrepared();
885
935
  const parsed = readJsonFile(AUTH_REGISTRY_FILE, null);
886
936
  return normalizeAuthRegistry(parsed);
887
937
  }
888
938
 
889
939
  function writeAuthRegistry(registry) {
940
+ ensureAuthProfileStoragePrepared();
890
941
  writeJsonAtomic(AUTH_REGISTRY_FILE, normalizeAuthRegistry(registry));
891
942
  }
892
943
 
@@ -945,6 +996,7 @@ function listAuthProfilesInfo() {
945
996
  }
946
997
 
947
998
  function upsertAuthProfile(payload, options = {}) {
999
+ ensureAuthProfileStoragePrepared();
948
1000
  const safePayload = parseAuthProfileJson(JSON.stringify(payload || {}));
949
1001
  const sourceFile = typeof options.sourceFile === 'string' ? options.sourceFile : '';
950
1002
  const preferredName = normalizeAuthProfileName(options.name || '');
@@ -1034,6 +1086,7 @@ function importAuthProfileFromUpload(payload = {}) {
1034
1086
  }
1035
1087
 
1036
1088
  function switchAuthProfile(name, options = {}) {
1089
+ ensureAuthProfileStoragePrepared();
1037
1090
  const profileName = normalizeAuthProfileName(name);
1038
1091
  if (!profileName) {
1039
1092
  throw new Error('认证名称不能为空');
@@ -1079,6 +1132,7 @@ function switchAuthProfile(name, options = {}) {
1079
1132
  }
1080
1133
 
1081
1134
  function deleteAuthProfile(name) {
1135
+ ensureAuthProfileStoragePrepared();
1082
1136
  const profileName = normalizeAuthProfileName(name);
1083
1137
  if (!profileName) {
1084
1138
  return { error: '认证名称不能为空' };
@@ -1127,6 +1181,7 @@ function deleteAuthProfile(name) {
1127
1181
  }
1128
1182
 
1129
1183
  function resolveAuthTokenFromCurrentProfile() {
1184
+ ensureAuthProfileStoragePrepared();
1130
1185
  const registry = readAuthRegistry();
1131
1186
  if (!registry.current) return '';
1132
1187
  const profile = registry.items.find((item) => item.name === registry.current);
@@ -1382,8 +1437,40 @@ function normalizeCodexSkillName(name) {
1382
1437
  return { name: value };
1383
1438
  }
1384
1439
 
1385
- function isSkillDirectoryEntry(entryName) {
1386
- const targetPath = path.join(CODEX_SKILLS_DIR, entryName);
1440
+ function normalizeSkillTargetApp(app) {
1441
+ const value = typeof app === 'string' ? app.trim().toLowerCase() : '';
1442
+ return SKILL_TARGETS.some((item) => item.app === value) ? value : '';
1443
+ }
1444
+
1445
+ function getSkillTargetByApp(app) {
1446
+ const normalizedApp = normalizeSkillTargetApp(app);
1447
+ if (!normalizedApp) return null;
1448
+ return SKILL_TARGETS.find((item) => item.app === normalizedApp) || null;
1449
+ }
1450
+
1451
+ function resolveSkillTarget(params = {}, defaultApp = 'codex') {
1452
+ const hasExplicitTargetApp = !!(params && typeof params === 'object'
1453
+ && Object.prototype.hasOwnProperty.call(params, 'targetApp'));
1454
+ const hasExplicitTarget = !!(params && typeof params === 'object'
1455
+ && Object.prototype.hasOwnProperty.call(params, 'target'));
1456
+ const hasAnyExplicitTarget = hasExplicitTargetApp || hasExplicitTarget;
1457
+ const rawTargetApp = hasExplicitTargetApp ? params.targetApp : '';
1458
+ const rawTarget = hasExplicitTarget ? params.target : '';
1459
+ const raw = rawTargetApp || rawTarget || '';
1460
+ if (hasAnyExplicitTarget && raw === '') {
1461
+ return null;
1462
+ }
1463
+ if (hasAnyExplicitTarget && !getSkillTargetByApp(raw)) {
1464
+ return null;
1465
+ }
1466
+ return getSkillTargetByApp(raw)
1467
+ || getSkillTargetByApp(defaultApp)
1468
+ || SKILL_TARGETS[0]
1469
+ || null;
1470
+ }
1471
+
1472
+ function isSkillDirectoryEntryAtRoot(rootDir, entryName) {
1473
+ const targetPath = path.join(rootDir, entryName);
1387
1474
  try {
1388
1475
  const stat = fs.statSync(targetPath);
1389
1476
  return stat.isDirectory();
@@ -1552,13 +1639,13 @@ function readCodexSkillMetadata(skillPath) {
1552
1639
  }
1553
1640
  }
1554
1641
 
1555
- function getCodexSkillEntryInfoByName(entryName) {
1556
- const targetPath = path.join(CODEX_SKILLS_DIR, entryName);
1642
+ function getSkillEntryInfoByName(rootDir, entryName) {
1643
+ const targetPath = path.join(rootDir, entryName);
1557
1644
  const normalized = normalizeCodexSkillName(entryName);
1558
1645
  if (normalized.error) {
1559
1646
  return null;
1560
1647
  }
1561
- const relativePath = path.relative(CODEX_SKILLS_DIR, targetPath);
1648
+ const relativePath = path.relative(rootDir, targetPath);
1562
1649
  if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
1563
1650
  return null;
1564
1651
  }
@@ -1569,7 +1656,7 @@ function getCodexSkillEntryInfoByName(entryName) {
1569
1656
  if (!lstat.isDirectory() && !isSymbolicLink) {
1570
1657
  return null;
1571
1658
  }
1572
- if (isSymbolicLink && !isSkillDirectoryEntry(entryName)) {
1659
+ if (isSymbolicLink && !isSkillDirectoryEntryAtRoot(rootDir, entryName)) {
1573
1660
  return null;
1574
1661
  }
1575
1662
  const metadata = readCodexSkillMetadata(targetPath);
@@ -1587,26 +1674,34 @@ function getCodexSkillEntryInfoByName(entryName) {
1587
1674
  }
1588
1675
  }
1589
1676
 
1590
- function listCodexSkills() {
1591
- if (!fs.existsSync(CODEX_SKILLS_DIR)) {
1677
+ function listSkills(params = {}) {
1678
+ const target = resolveSkillTarget(params);
1679
+ if (!target) {
1680
+ return { error: '目标宿主不支持' };
1681
+ }
1682
+ if (!fs.existsSync(target.dir)) {
1592
1683
  return {
1593
- root: CODEX_SKILLS_DIR,
1684
+ targetApp: target.app,
1685
+ targetLabel: target.label,
1686
+ root: target.dir,
1594
1687
  exists: false,
1595
1688
  items: []
1596
1689
  };
1597
1690
  }
1598
1691
  try {
1599
- const entries = fs.readdirSync(CODEX_SKILLS_DIR, { withFileTypes: true });
1692
+ const entries = fs.readdirSync(target.dir, { withFileTypes: true });
1600
1693
  const items = entries
1601
1694
  .map((entry) => {
1602
1695
  const name = entry && entry.name ? entry.name : '';
1603
1696
  if (!name || name.startsWith('.')) return null;
1604
- return getCodexSkillEntryInfoByName(name);
1697
+ return getSkillEntryInfoByName(target.dir, name);
1605
1698
  })
1606
1699
  .filter(Boolean)
1607
1700
  .sort((a, b) => a.displayName.localeCompare(b.displayName, 'zh-Hans-CN'));
1608
1701
  return {
1609
- root: CODEX_SKILLS_DIR,
1702
+ targetApp: target.app,
1703
+ targetLabel: target.label,
1704
+ root: target.dir,
1610
1705
  exists: true,
1611
1706
  items
1612
1707
  };
@@ -1615,6 +1710,10 @@ function listCodexSkills() {
1615
1710
  }
1616
1711
  }
1617
1712
 
1713
+ function listCodexSkills() {
1714
+ return listSkills({ targetApp: 'codex' });
1715
+ }
1716
+
1618
1717
  function listSkillEntriesByRoot(rootDir) {
1619
1718
  if (!rootDir || !fs.existsSync(rootDir)) {
1620
1719
  return [];
@@ -1660,8 +1759,13 @@ function listSkillEntriesByRoot(rootDir) {
1660
1759
  }
1661
1760
  }
1662
1761
 
1663
- function scanUnmanagedCodexSkills() {
1664
- const existing = listCodexSkills();
1762
+ function scanUnmanagedSkills(params = {}) {
1763
+ const target = resolveSkillTarget(params);
1764
+ if (!target) {
1765
+ return { error: '目标宿主不支持' };
1766
+ }
1767
+ const targetRoot = resolveCopyTargetRoot(target.dir);
1768
+ const existing = listSkills({ targetApp: target.app });
1665
1769
  if (existing.error) {
1666
1770
  return { error: existing.error };
1667
1771
  }
@@ -1670,9 +1774,14 @@ function scanUnmanagedCodexSkills() {
1670
1774
  .filter(Boolean));
1671
1775
 
1672
1776
  const items = [];
1673
- for (const source of SKILL_IMPORT_SOURCES) {
1777
+ const sources = SKILL_IMPORT_SOURCES.filter((source) => source.app !== target.app);
1778
+ for (const source of sources) {
1674
1779
  const sourceEntries = listSkillEntriesByRoot(source.dir);
1675
1780
  for (const entry of sourceEntries) {
1781
+ const targetCandidate = path.join(targetRoot, entry.name);
1782
+ if (fs.existsSync(targetCandidate)) {
1783
+ continue;
1784
+ }
1676
1785
  if (existingNames.has(entry.name)) {
1677
1786
  continue;
1678
1787
  }
@@ -1698,9 +1807,11 @@ function scanUnmanagedCodexSkills() {
1698
1807
  });
1699
1808
 
1700
1809
  return {
1701
- root: CODEX_SKILLS_DIR,
1810
+ targetApp: target.app,
1811
+ targetLabel: target.label,
1812
+ root: target.dir,
1702
1813
  items,
1703
- sources: SKILL_IMPORT_SOURCES.map((source) => ({
1814
+ sources: sources.map((source) => ({
1704
1815
  app: source.app,
1705
1816
  label: source.label,
1706
1817
  path: source.dir,
@@ -1709,14 +1820,21 @@ function scanUnmanagedCodexSkills() {
1709
1820
  };
1710
1821
  }
1711
1822
 
1712
- function importCodexSkills(params = {}) {
1823
+ function scanUnmanagedCodexSkills() {
1824
+ return scanUnmanagedSkills({ targetApp: 'codex' });
1825
+ }
1826
+
1827
+ function importSkills(params = {}) {
1828
+ const target = resolveSkillTarget(params);
1829
+ if (!target) {
1830
+ return { error: '目标宿主不支持' };
1831
+ }
1832
+ const targetRoot = resolveCopyTargetRoot(target.dir);
1713
1833
  const rawItems = Array.isArray(params.items) ? params.items : [];
1714
1834
  if (!rawItems.length) {
1715
1835
  return { error: '请先选择要导入的 skill' };
1716
1836
  }
1717
1837
 
1718
- ensureDir(CODEX_SKILLS_DIR);
1719
-
1720
1838
  const imported = [];
1721
1839
  const failed = [];
1722
1840
  const dedup = new Set();
@@ -1741,6 +1859,14 @@ function importCodexSkills(params = {}) {
1741
1859
  });
1742
1860
  continue;
1743
1861
  }
1862
+ if (source.app === target.app) {
1863
+ failed.push({
1864
+ name: normalizedName.name,
1865
+ sourceApp: source.app,
1866
+ error: '来源与目标相同,无需导入'
1867
+ });
1868
+ continue;
1869
+ }
1744
1870
  const dedupKey = `${source.app}:${normalizedName.name}`;
1745
1871
  if (dedup.has(dedupKey)) {
1746
1872
  continue;
@@ -1766,8 +1892,8 @@ function importCodexSkills(params = {}) {
1766
1892
  continue;
1767
1893
  }
1768
1894
 
1769
- const targetPath = path.join(CODEX_SKILLS_DIR, normalizedName.name);
1770
- const targetRelative = path.relative(CODEX_SKILLS_DIR, targetPath);
1895
+ const targetPath = path.join(targetRoot, normalizedName.name);
1896
+ const targetRelative = path.relative(targetRoot, targetPath);
1771
1897
  if (targetRelative.startsWith('..') || path.isAbsolute(targetRelative)) {
1772
1898
  failed.push({
1773
1899
  name: normalizedName.name,
@@ -1780,7 +1906,7 @@ function importCodexSkills(params = {}) {
1780
1906
  failed.push({
1781
1907
  name: normalizedName.name,
1782
1908
  sourceApp: source.app,
1783
- error: 'Codex 中已存在同名 skill'
1909
+ error: `${target.label} 中已存在同名 skill`
1784
1910
  });
1785
1911
  continue;
1786
1912
  }
@@ -1806,6 +1932,15 @@ function importCodexSkills(params = {}) {
1806
1932
  });
1807
1933
  continue;
1808
1934
  }
1935
+ if (isPathInside(targetRoot, sourceDirForCopy)) {
1936
+ failed.push({
1937
+ name: normalizedName.name,
1938
+ sourceApp: source.app,
1939
+ error: '目标路径不能位于来源 skill 目录内'
1940
+ });
1941
+ continue;
1942
+ }
1943
+ ensureDir(targetRoot);
1809
1944
  const visitedRealPaths = new Set([sourceDirForCopy]);
1810
1945
  copyDirRecursive(sourceDirForCopy, targetPath, {
1811
1946
  dereferenceSymlinks: true,
@@ -1817,6 +1952,8 @@ function importCodexSkills(params = {}) {
1817
1952
  name: normalizedName.name,
1818
1953
  sourceApp: source.app,
1819
1954
  sourceLabel: source.label,
1955
+ targetApp: target.app,
1956
+ targetLabel: target.label,
1820
1957
  path: targetPath
1821
1958
  });
1822
1959
  } catch (e) {
@@ -1837,10 +1974,16 @@ function importCodexSkills(params = {}) {
1837
1974
  success: failed.length === 0,
1838
1975
  imported,
1839
1976
  failed,
1840
- root: CODEX_SKILLS_DIR
1977
+ targetApp: target.app,
1978
+ targetLabel: target.label,
1979
+ root: targetRoot
1841
1980
  };
1842
1981
  }
1843
1982
 
1983
+ function importCodexSkills(params = {}) {
1984
+ return importSkills({ ...(params || {}), targetApp: 'codex' });
1985
+ }
1986
+
1844
1987
  function collectSkillDirectoriesFromRoot(rootDir, limit = MAX_SKILLS_ZIP_ENTRY_COUNT) {
1845
1988
  const results = [];
1846
1989
  let truncated = false;
@@ -1894,15 +2037,22 @@ function resolveSkillNameFromImportedDirectory(skillDir, extractionRoot, fallbac
1894
2037
  return normalizeCodexSkillName(candidate);
1895
2038
  }
1896
2039
 
1897
- async function importCodexSkillsFromZipFile(zipPath, options = {}) {
2040
+ async function importSkillsFromZipFile(zipPath, options = {}) {
1898
2041
  const fallbackName = typeof options.fallbackName === 'string' ? options.fallbackName : '';
1899
2042
  const tempDir = typeof options.tempDir === 'string' ? options.tempDir : '';
1900
2043
  const imported = [];
1901
2044
  const failed = [];
1902
2045
  const dedupNames = new Set();
1903
2046
  const extractionRoot = path.join(tempDir || path.dirname(zipPath), 'extract');
2047
+ let target = null;
2048
+ let targetRoot = '';
1904
2049
 
1905
2050
  try {
2051
+ target = resolveSkillTarget(options, 'codex');
2052
+ if (!target) {
2053
+ return { error: '目标宿主不支持' };
2054
+ }
2055
+ targetRoot = resolveCopyTargetRoot(target.dir);
1906
2056
  await inspectZipArchiveLimits(zipPath, {
1907
2057
  maxEntryCount: MAX_SKILLS_ZIP_ENTRY_COUNT,
1908
2058
  maxUncompressedBytes: MAX_SKILLS_ZIP_UNCOMPRESSED_BYTES
@@ -1918,7 +2068,6 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
1918
2068
  return { error: '压缩包中的技能目录数量超出导入上限' };
1919
2069
  }
1920
2070
 
1921
- ensureDir(CODEX_SKILLS_DIR);
1922
2071
  for (const skillDir of discoveredDirs) {
1923
2072
  const normalizedName = resolveSkillNameFromImportedDirectory(skillDir, extractionRoot, fallbackName);
1924
2073
  if (normalizedName.error) {
@@ -1934,8 +2083,8 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
1934
2083
  }
1935
2084
  dedupNames.add(dedupKey);
1936
2085
 
1937
- const targetPath = path.join(CODEX_SKILLS_DIR, normalizedName.name);
1938
- const targetRelative = path.relative(CODEX_SKILLS_DIR, targetPath);
2086
+ const targetPath = path.join(targetRoot, normalizedName.name);
2087
+ const targetRelative = path.relative(targetRoot, targetPath);
1939
2088
  if (targetRelative.startsWith('..') || path.isAbsolute(targetRelative)) {
1940
2089
  failed.push({
1941
2090
  name: normalizedName.name,
@@ -1946,7 +2095,7 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
1946
2095
  if (fs.existsSync(targetPath)) {
1947
2096
  failed.push({
1948
2097
  name: normalizedName.name,
1949
- error: 'Codex 中已存在同名 skill'
2098
+ error: `${target.label} 中已存在同名 skill`
1950
2099
  });
1951
2100
  continue;
1952
2101
  }
@@ -1962,6 +2111,14 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
1962
2111
  });
1963
2112
  continue;
1964
2113
  }
2114
+ if (isPathInside(targetRoot, sourceRealPath)) {
2115
+ failed.push({
2116
+ name: normalizedName.name,
2117
+ error: '目标路径不能位于来源 skill 目录内'
2118
+ });
2119
+ continue;
2120
+ }
2121
+ ensureDir(targetRoot);
1965
2122
  const visitedRealPaths = new Set([sourceRealPath]);
1966
2123
  copyDirRecursive(sourceRealPath, targetPath, {
1967
2124
  dereferenceSymlinks: true,
@@ -1971,6 +2128,8 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
1971
2128
  copiedToTarget = true;
1972
2129
  imported.push({
1973
2130
  name: normalizedName.name,
2131
+ targetApp: target.app,
2132
+ targetLabel: target.label,
1974
2133
  path: targetPath
1975
2134
  });
1976
2135
  } catch (e) {
@@ -1991,7 +2150,9 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
1991
2150
  error: failed[0].error || '导入失败',
1992
2151
  imported,
1993
2152
  failed,
1994
- root: CODEX_SKILLS_DIR
2153
+ targetApp: target.app,
2154
+ targetLabel: target.label,
2155
+ root: targetRoot
1995
2156
  };
1996
2157
  }
1997
2158
 
@@ -1999,7 +2160,9 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
1999
2160
  success: failed.length === 0,
2000
2161
  imported,
2001
2162
  failed,
2002
- root: CODEX_SKILLS_DIR
2163
+ targetApp: target.app,
2164
+ targetLabel: target.label,
2165
+ root: targetRoot
2003
2166
  };
2004
2167
  } catch (e) {
2005
2168
  return {
@@ -2018,21 +2181,40 @@ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
2018
2181
  }
2019
2182
  }
2020
2183
 
2021
- async function importCodexSkillsFromZip(payload = {}) {
2184
+ async function importCodexSkillsFromZipFile(zipPath, options = {}) {
2185
+ return importSkillsFromZipFile(zipPath, { ...(options || {}), targetApp: 'codex' });
2186
+ }
2187
+
2188
+ async function importSkillsFromZip(payload = {}) {
2022
2189
  if (!payload || typeof payload.fileBase64 !== 'string' || !payload.fileBase64.trim()) {
2023
2190
  return { error: '缺少技能压缩包内容' };
2024
2191
  }
2025
- const upload = writeUploadZip(payload.fileBase64, 'codex-skills-import', payload.fileName || 'codex-skills.zip');
2192
+ const fallbackTarget = resolveSkillTarget(payload, 'codex');
2193
+ const fallbackTargetApp = fallbackTarget ? fallbackTarget.app : 'codex';
2194
+ const fallbackName = payload.fileName || `${fallbackTargetApp}-skills.zip`;
2195
+ const upload = writeUploadZip(payload.fileBase64, 'codex-skills-import', fallbackName);
2026
2196
  if (upload.error) {
2027
2197
  return { error: upload.error };
2028
2198
  }
2029
- return importCodexSkillsFromZipFile(upload.zipPath, {
2030
- tempDir: upload.tempDir,
2031
- fallbackName: payload.fileName || ''
2032
- });
2199
+ const importOptions = { tempDir: upload.tempDir, fallbackName };
2200
+ if (Object.prototype.hasOwnProperty.call(payload, 'targetApp')) {
2201
+ importOptions.targetApp = payload.targetApp;
2202
+ }
2203
+ if (Object.prototype.hasOwnProperty.call(payload, 'target')) {
2204
+ importOptions.target = payload.target;
2205
+ }
2206
+ return importSkillsFromZipFile(upload.zipPath, importOptions);
2033
2207
  }
2034
2208
 
2035
- async function exportCodexSkills(params = {}) {
2209
+ async function importCodexSkillsFromZip(payload = {}) {
2210
+ return importSkillsFromZip({ ...(payload || {}), targetApp: 'codex' });
2211
+ }
2212
+
2213
+ async function exportSkills(params = {}) {
2214
+ const target = resolveSkillTarget(params);
2215
+ if (!target) {
2216
+ return { error: '目标宿主不支持' };
2217
+ }
2036
2218
  const rawNames = Array.isArray(params.names) ? params.names : [];
2037
2219
  const uniqueNames = Array.from(new Set(rawNames
2038
2220
  .map((item) => (typeof item === 'string' ? item.trim() : ''))
@@ -2054,8 +2236,8 @@ async function exportCodexSkills(params = {}) {
2054
2236
  failed.push({ name: rawName, error: normalizedName.error });
2055
2237
  continue;
2056
2238
  }
2057
- const sourcePath = path.join(CODEX_SKILLS_DIR, normalizedName.name);
2058
- const sourceRelative = path.relative(CODEX_SKILLS_DIR, sourcePath);
2239
+ const sourcePath = path.join(target.dir, normalizedName.name);
2240
+ const sourceRelative = path.relative(target.dir, sourcePath);
2059
2241
  if (sourceRelative.startsWith('..') || path.isAbsolute(sourceRelative)) {
2060
2242
  failed.push({ name: normalizedName.name, error: '来源路径非法' });
2061
2243
  continue;
@@ -2101,12 +2283,14 @@ async function exportCodexSkills(params = {}) {
2101
2283
  error: failed[0] && failed[0].error ? failed[0].error : '无可导出的 skill',
2102
2284
  exported,
2103
2285
  failed,
2104
- root: CODEX_SKILLS_DIR
2286
+ targetApp: target.app,
2287
+ targetLabel: target.label,
2288
+ root: target.dir
2105
2289
  };
2106
2290
  }
2107
2291
 
2108
2292
  const randomToken = crypto.randomBytes(12).toString('hex');
2109
- const zipFileName = `codex-skills-${randomToken}.zip`;
2293
+ const zipFileName = `${target.app}-skills-${randomToken}.zip`;
2110
2294
  const zipFilePath = path.join(os.tmpdir(), zipFileName);
2111
2295
  if (fs.existsSync(zipFilePath)) {
2112
2296
  try {
@@ -2125,14 +2309,18 @@ async function exportCodexSkills(params = {}) {
2125
2309
  downloadPath: artifact.downloadPath,
2126
2310
  exported,
2127
2311
  failed,
2128
- root: CODEX_SKILLS_DIR
2312
+ targetApp: target.app,
2313
+ targetLabel: target.label,
2314
+ root: target.dir
2129
2315
  };
2130
2316
  } catch (e) {
2131
2317
  return {
2132
2318
  error: `导出失败:${e && e.message ? e.message : '未知错误'}`,
2133
2319
  exported,
2134
2320
  failed,
2135
- root: CODEX_SKILLS_DIR
2321
+ targetApp: target.app,
2322
+ targetLabel: target.label,
2323
+ root: target.dir
2136
2324
  };
2137
2325
  } finally {
2138
2326
  try {
@@ -2141,6 +2329,10 @@ async function exportCodexSkills(params = {}) {
2141
2329
  }
2142
2330
  }
2143
2331
 
2332
+ async function exportCodexSkills(params = {}) {
2333
+ return exportSkills({ ...(params || {}), targetApp: 'codex' });
2334
+ }
2335
+
2144
2336
  function removeDirectoryRecursive(targetPath) {
2145
2337
  if (typeof fs.rmSync === 'function') {
2146
2338
  fs.rmSync(targetPath, { recursive: true, force: false });
@@ -2149,7 +2341,11 @@ function removeDirectoryRecursive(targetPath) {
2149
2341
  fs.rmdirSync(targetPath, { recursive: true });
2150
2342
  }
2151
2343
 
2152
- function deleteCodexSkills(params = {}) {
2344
+ function deleteSkills(params = {}) {
2345
+ const target = resolveSkillTarget(params);
2346
+ if (!target) {
2347
+ return { error: '目标宿主不支持' };
2348
+ }
2153
2349
  const rawList = Array.isArray(params.names) ? params.names : [];
2154
2350
  const uniqueNames = Array.from(new Set(rawList
2155
2351
  .map((item) => (typeof item === 'string' ? item.trim() : ''))
@@ -2167,8 +2363,8 @@ function deleteCodexSkills(params = {}) {
2167
2363
  continue;
2168
2364
  }
2169
2365
 
2170
- const skillPath = path.join(CODEX_SKILLS_DIR, normalized.name);
2171
- const relativePath = path.relative(CODEX_SKILLS_DIR, skillPath);
2366
+ const skillPath = path.join(target.dir, normalized.name);
2367
+ const relativePath = path.relative(target.dir, skillPath);
2172
2368
  if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
2173
2369
  failed.push({ name: normalized.name, error: '技能路径非法' });
2174
2370
  continue;
@@ -2198,10 +2394,16 @@ function deleteCodexSkills(params = {}) {
2198
2394
  success: failed.length === 0,
2199
2395
  deleted,
2200
2396
  failed,
2201
- root: CODEX_SKILLS_DIR
2397
+ targetApp: target.app,
2398
+ targetLabel: target.label,
2399
+ root: target.dir
2202
2400
  };
2203
2401
  }
2204
2402
 
2403
+ function deleteCodexSkills(params = {}) {
2404
+ return deleteSkills({ ...(params || {}), targetApp: 'codex' });
2405
+ }
2406
+
2205
2407
  function readAgentsFile(params = {}) {
2206
2408
  const filePath = resolveAgentsFilePath(params);
2207
2409
  const dirCheck = validateAgentsBaseDir(filePath);
@@ -2218,6 +2420,15 @@ function readAgentsFile(params = {}) {
2218
2420
  };
2219
2421
  }
2220
2422
 
2423
+ if (params.metaOnly) {
2424
+ return {
2425
+ exists: true,
2426
+ path: filePath,
2427
+ content: '',
2428
+ lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n'
2429
+ };
2430
+ }
2431
+
2221
2432
  try {
2222
2433
  const raw = fs.readFileSync(filePath, 'utf-8');
2223
2434
  return {
@@ -2251,6 +2462,48 @@ function applyAgentsFile(params = {}) {
2251
2462
  }
2252
2463
  }
2253
2464
 
2465
+ function normalizeDiffText(input) {
2466
+ const safe = typeof input === 'string' ? input : '';
2467
+ return normalizeLineEnding(stripUtf8Bom(safe), '\n');
2468
+ }
2469
+
2470
+ function buildAgentsDiff(params = {}) {
2471
+ const hasBaseContent = typeof params.baseContent === 'string';
2472
+ const contextRaw = typeof params.context === 'string' ? params.context.trim() : '';
2473
+ const context = contextRaw || 'codex';
2474
+ const metaOnly = hasBaseContent;
2475
+ let readResult;
2476
+ if (context === 'openclaw') {
2477
+ readResult = readOpenclawAgentsFile({ metaOnly });
2478
+ } else if (context === 'openclaw-workspace') {
2479
+ readResult = readOpenclawWorkspaceFile({ ...params, metaOnly });
2480
+ } else if (context === 'codex') {
2481
+ readResult = readAgentsFile({ ...params, metaOnly });
2482
+ } else {
2483
+ return { error: `Unsupported agents diff context: ${context}` };
2484
+ }
2485
+ if (readResult && readResult.error) {
2486
+ return { error: readResult.error };
2487
+ }
2488
+
2489
+ const beforeText = normalizeDiffText(
2490
+ hasBaseContent ? params.baseContent : (readResult && readResult.content ? readResult.content : '')
2491
+ );
2492
+ const afterText = normalizeDiffText(params.content);
2493
+ const diff = buildLineDiff(beforeText, afterText);
2494
+ const hasChanges = diff.truncated ? beforeText !== afterText : (diff.stats.added > 0 || diff.stats.removed > 0);
2495
+ return {
2496
+ diff: {
2497
+ ...diff,
2498
+ hasChanges
2499
+ },
2500
+ path: readResult && readResult.path ? readResult.path : '',
2501
+ exists: !!(readResult && readResult.exists),
2502
+ context,
2503
+ configError: readResult && readResult.configError ? readResult.configError : ''
2504
+ };
2505
+ }
2506
+
2254
2507
  function resolveOpenclawWorkspaceDir(config) {
2255
2508
  const workspace = config
2256
2509
  && config.agents
@@ -2348,7 +2601,7 @@ function getOpenclawWorkspaceInfo() {
2348
2601
  };
2349
2602
  }
2350
2603
 
2351
- function readOpenclawAgentsFile() {
2604
+ function readOpenclawAgentsFile(params = {}) {
2352
2605
  const workspaceInfo = getOpenclawWorkspaceInfo();
2353
2606
  const baseDir = workspaceInfo.workspaceDir;
2354
2607
  const filePath = path.join(baseDir, AGENTS_FILE_NAME);
@@ -2365,7 +2618,7 @@ function readOpenclawAgentsFile() {
2365
2618
  };
2366
2619
  }
2367
2620
 
2368
- const readResult = readAgentsFile({ baseDir });
2621
+ const readResult = readAgentsFile({ baseDir, metaOnly: !!params.metaOnly });
2369
2622
  return {
2370
2623
  ...readResult,
2371
2624
  workspaceDir: baseDir,
@@ -2420,6 +2673,17 @@ function readOpenclawWorkspaceFile(params = {}) {
2420
2673
  };
2421
2674
  }
2422
2675
 
2676
+ if (params.metaOnly) {
2677
+ return {
2678
+ exists: true,
2679
+ path: filePath,
2680
+ content: '',
2681
+ lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n',
2682
+ workspaceDir: baseDir,
2683
+ configError: workspaceInfo.configError
2684
+ };
2685
+ }
2686
+
2423
2687
  try {
2424
2688
  const raw = fs.readFileSync(filePath, 'utf-8');
2425
2689
  return {
@@ -2921,11 +3185,40 @@ function buildVirtualDefaultConfig() {
2921
3185
  return toml.parse(EMPTY_CONFIG_FALLBACK_TEMPLATE);
2922
3186
  }
2923
3187
 
3188
+ function sanitizeRemovedBuiltinProxyProvider(config) {
3189
+ const safeConfig = isPlainObject(config) ? config : {};
3190
+ const providers = isPlainObject(safeConfig.model_providers) ? safeConfig.model_providers : null;
3191
+ const currentProvider = typeof safeConfig.model_provider === 'string' ? safeConfig.model_provider.trim() : '';
3192
+ const hasRemovedBuiltin = !!(providers && providers[BUILTIN_PROXY_PROVIDER_NAME]);
3193
+ const currentIsRemovedBuiltin = currentProvider === BUILTIN_PROXY_PROVIDER_NAME;
3194
+
3195
+ if (!hasRemovedBuiltin && !currentIsRemovedBuiltin) {
3196
+ return safeConfig;
3197
+ }
3198
+
3199
+ const nextProviders = providers ? { ...providers } : {};
3200
+ delete nextProviders[BUILTIN_PROXY_PROVIDER_NAME];
3201
+ const providerNames = Object.keys(nextProviders);
3202
+ const fallbackProvider = providerNames[0] || '';
3203
+ const currentModels = readCurrentModels();
3204
+ const fallbackModel = fallbackProvider
3205
+ ? (currentModels[fallbackProvider] || (typeof safeConfig.model === 'string' ? safeConfig.model : ''))
3206
+ : '';
3207
+
3208
+ return {
3209
+ ...safeConfig,
3210
+ model_providers: nextProviders,
3211
+ model_provider: currentIsRemovedBuiltin ? fallbackProvider : safeConfig.model_provider,
3212
+ model: currentIsRemovedBuiltin ? fallbackModel : safeConfig.model
3213
+ };
3214
+ }
3215
+
2924
3216
  function readConfigOrVirtualDefault() {
2925
3217
  if (fs.existsSync(CONFIG_FILE)) {
2926
3218
  try {
3219
+ removePersistedBuiltinProxyProviderFromConfig();
2927
3220
  return {
2928
- config: readConfig(),
3221
+ config: sanitizeRemovedBuiltinProxyProvider(readConfig()),
2929
3222
  isVirtual: false,
2930
3223
  reason: '',
2931
3224
  detail: '',
@@ -2942,7 +3235,9 @@ function readConfigOrVirtualDefault() {
2942
3235
  ? e.configDetail.trim()
2943
3236
  : (e && e.message ? e.message : publicReason);
2944
3237
  return {
2945
- config: errorType === 'missing' ? buildVirtualDefaultConfig() : {},
3238
+ config: errorType === 'missing'
3239
+ ? sanitizeRemovedBuiltinProxyProvider(buildVirtualDefaultConfig())
3240
+ : {},
2946
3241
  isVirtual: true,
2947
3242
  reason: publicReason,
2948
3243
  detail,
@@ -2952,7 +3247,7 @@ function readConfigOrVirtualDefault() {
2952
3247
  }
2953
3248
 
2954
3249
  return {
2955
- config: buildVirtualDefaultConfig(),
3250
+ config: sanitizeRemovedBuiltinProxyProvider(buildVirtualDefaultConfig()),
2956
3251
  isVirtual: true,
2957
3252
  reason: '未检测到 config.toml',
2958
3253
  detail: `配置文件不存在: ${CONFIG_FILE}`,
@@ -3048,8 +3343,8 @@ function getConfigTemplate(params = {}) {
3048
3343
  }
3049
3344
  } catch (e) {}
3050
3345
  }
3051
- const selectedProvider = params.provider || '';
3052
- const selectedModel = params.model || '';
3346
+ const selectedProvider = typeof params.provider === 'string' ? params.provider.trim() : '';
3347
+ const selectedModel = typeof params.model === 'string' ? params.model.trim() : '';
3053
3348
  let template = normalizeTopLevelConfigWithTemplate(content, selectedProvider, selectedModel);
3054
3349
  if (typeof params.serviceTier === 'string') {
3055
3350
  template = applyServiceTierToTemplate(template, params.serviceTier);
@@ -3126,7 +3421,7 @@ function addProviderToConfig(params = {}) {
3126
3421
  return { error: 'local provider 为系统保留名称,不可新增' };
3127
3422
  }
3128
3423
  if (isBuiltinProxyProvider(name) && !allowManaged) {
3129
- return { error: '本地代理配置为系统内建项,不可手动添加' };
3424
+ return { error: 'codexmate-proxy 为保留名称,不可手动添加' };
3130
3425
  }
3131
3426
 
3132
3427
  ensureConfigDir();
@@ -3204,7 +3499,7 @@ function updateProviderInConfig(params = {}) {
3204
3499
  if (isDefaultLocalProvider(name)) {
3205
3500
  return { error: 'local provider 为系统保留项,不可编辑' };
3206
3501
  }
3207
- return { error: '本地代理配置为系统内建项,不可编辑' };
3502
+ return { error: 'codexmate-proxy 为保留名称,不可编辑' };
3208
3503
  }
3209
3504
 
3210
3505
  try {
@@ -3222,7 +3517,7 @@ function deleteProviderFromConfig(params = {}) {
3222
3517
  if (isDefaultLocalProvider(name)) {
3223
3518
  return { error: 'local provider 为系统保留项,不可删除' };
3224
3519
  }
3225
- return { error: '本地代理配置为系统内建项,不可删除' };
3520
+ return { error: 'codexmate-proxy 为保留名称,不可删除' };
3226
3521
  }
3227
3522
  if (!fs.existsSync(CONFIG_FILE)) {
3228
3523
  return { error: 'config.toml 不存在' };
@@ -3252,7 +3547,7 @@ function performProviderDeletion(name, options = {}) {
3252
3547
  if (isNonDeletableProvider(name)) {
3253
3548
  const msg = isDefaultLocalProvider(name)
3254
3549
  ? 'local provider 为系统保留项,不可删除'
3255
- : '本地代理配置为系统内建项,不可删除';
3550
+ : 'codexmate-proxy 为保留名称,不可删除';
3256
3551
  if (!silent) console.error('错误:', msg);
3257
3552
  return { error: msg };
3258
3553
  }
@@ -3555,6 +3850,27 @@ function isPathInside(targetPath, rootPath) {
3555
3850
  return resolvedTarget.startsWith(rootWithSlash);
3556
3851
  }
3557
3852
 
3853
+ function resolveCopyTargetRoot(targetDir) {
3854
+ const suffixSegments = [];
3855
+ let current = path.resolve(targetDir || '');
3856
+ while (current && !fs.existsSync(current)) {
3857
+ const parent = path.dirname(current);
3858
+ if (!parent || parent === current) {
3859
+ break;
3860
+ }
3861
+ suffixSegments.unshift(path.basename(current));
3862
+ current = parent;
3863
+ }
3864
+ let resolvedRoot = normalizePathForCompare(current || targetDir);
3865
+ if (!resolvedRoot) {
3866
+ resolvedRoot = path.resolve(targetDir || '');
3867
+ }
3868
+ for (const segment of suffixSegments) {
3869
+ resolvedRoot = path.join(resolvedRoot, segment);
3870
+ }
3871
+ return resolvedRoot;
3872
+ }
3873
+
3558
3874
  function collectJsonlFiles(rootDir, maxFiles = 5000) {
3559
3875
  if (!fs.existsSync(rootDir)) {
3560
3876
  return [];
@@ -3656,6 +3972,102 @@ function parseJsonlHeadRecords(filePath, maxBytes = SESSION_SUMMARY_READ_BYTES)
3656
3972
  return parseJsonlContent(headText);
3657
3973
  }
3658
3974
 
3975
+ function buildClaudeStoredIndexMessageCount(messageCount) {
3976
+ const safeCount = Number.isFinite(Number(messageCount))
3977
+ ? Math.max(0, Math.floor(Number(messageCount)))
3978
+ : 0;
3979
+ return safeCount + 1;
3980
+ }
3981
+
3982
+ function getFileStatSafe(filePath) {
3983
+ try {
3984
+ return fs.statSync(filePath);
3985
+ } catch (e) {
3986
+ return null;
3987
+ }
3988
+ }
3989
+
3990
+ function getFileMtimeMs(filePath, stat = null) {
3991
+ const fileStat = stat || getFileStatSafe(filePath);
3992
+ if (!fileStat || !Number.isFinite(Number(fileStat.mtimeMs))) {
3993
+ return 0;
3994
+ }
3995
+ return Math.max(0, Math.floor(Number(fileStat.mtimeMs)));
3996
+ }
3997
+
3998
+ function isSessionSummaryMessageCountExact(stat, maxBytes = SESSION_SUMMARY_READ_BYTES) {
3999
+ if (!stat || !Number.isFinite(Number(stat.size))) {
4000
+ return false;
4001
+ }
4002
+ return Number(stat.size) <= maxBytes;
4003
+ }
4004
+
4005
+ function buildExactMessageCountCacheKey(filePath, source, stat = null) {
4006
+ const validSource = source === 'claude' ? 'claude' : (source === 'codex' ? 'codex' : '');
4007
+ if (!validSource || !filePath) {
4008
+ return '';
4009
+ }
4010
+ const mtimeMs = getFileMtimeMs(filePath, stat);
4011
+ if (!mtimeMs) {
4012
+ return '';
4013
+ }
4014
+ return `${validSource}:${path.resolve(filePath)}:${mtimeMs}`;
4015
+ }
4016
+
4017
+ function readExactMessageCountCache(filePath, source, stat = null) {
4018
+ const cacheKey = buildExactMessageCountCacheKey(filePath, source, stat);
4019
+ if (!cacheKey) {
4020
+ return null;
4021
+ }
4022
+ if (!g_exactMessageCountCache.has(cacheKey)) {
4023
+ return null;
4024
+ }
4025
+ const cached = g_exactMessageCountCache.get(cacheKey);
4026
+ g_exactMessageCountCache.delete(cacheKey);
4027
+ g_exactMessageCountCache.set(cacheKey, cached);
4028
+ return Number.isFinite(Number(cached)) ? Math.max(0, Math.floor(Number(cached))) : null;
4029
+ }
4030
+
4031
+ function writeExactMessageCountCache(filePath, source, messageCount, stat = null) {
4032
+ const cacheKey = buildExactMessageCountCacheKey(filePath, source, stat);
4033
+ const safeCount = Number.isFinite(Number(messageCount))
4034
+ ? Math.max(0, Math.floor(Number(messageCount)))
4035
+ : null;
4036
+ if (!cacheKey || safeCount === null) {
4037
+ return;
4038
+ }
4039
+ if (g_exactMessageCountCache.has(cacheKey)) {
4040
+ g_exactMessageCountCache.delete(cacheKey);
4041
+ }
4042
+ g_exactMessageCountCache.set(cacheKey, safeCount);
4043
+ if (g_exactMessageCountCache.size <= EXACT_MESSAGE_COUNT_CACHE_MAX_ENTRIES) {
4044
+ return;
4045
+ }
4046
+ const firstKey = g_exactMessageCountCache.keys().next().value;
4047
+ if (firstKey) {
4048
+ g_exactMessageCountCache.delete(firstKey);
4049
+ }
4050
+ }
4051
+
4052
+ async function mapWithConcurrency(items, concurrency, mapper) {
4053
+ const list = Array.isArray(items) ? items : [];
4054
+ if (list.length === 0) {
4055
+ return [];
4056
+ }
4057
+ const safeConcurrency = Math.max(1, Math.min(Math.floor(Number(concurrency)) || 1, list.length));
4058
+ const results = new Array(list.length);
4059
+ let nextIndex = 0;
4060
+ const workers = Array.from({ length: safeConcurrency }, async () => {
4061
+ while (nextIndex < list.length) {
4062
+ const currentIndex = nextIndex;
4063
+ nextIndex += 1;
4064
+ results[currentIndex] = await mapper(list[currentIndex], currentIndex);
4065
+ }
4066
+ });
4067
+ await Promise.all(workers);
4068
+ return results.filter((item) => item !== undefined);
4069
+ }
4070
+
3659
4071
  function isBootstrapLikeText(text) {
3660
4072
  if (!text || typeof text !== 'string') {
3661
4073
  return false;
@@ -3723,48 +4135,349 @@ function countConversationMessagesInRecords(records, source) {
3723
4135
  return removeLeadingSystemMessage(messages).length;
3724
4136
  }
3725
4137
 
3726
- function sortSessionsByUpdatedAt(items) {
3727
- items.sort((a, b) => {
3728
- const aTime = Date.parse(a.updatedAt || '') || 0;
3729
- const bTime = Date.parse(b.updatedAt || '') || 0;
3730
- return bTime - aTime;
3731
- });
3732
- return items;
3733
- }
3734
-
3735
- function mergeAndLimitSessions(items, limit) {
3736
- const deduped = [];
3737
- const seen = new Set();
3738
- for (const item of items) {
3739
- if (!item || !item.filePath) continue;
3740
- const key = `${item.source}:${item.filePath}`;
3741
- if (seen.has(key)) continue;
3742
- seen.add(key);
3743
- deduped.push(item);
4138
+ async function countConversationMessagesInFile(filePath, source) {
4139
+ const fileStat = getFileStatSafe(filePath);
4140
+ const cached = readExactMessageCountCache(filePath, source, fileStat);
4141
+ if (cached !== null) {
4142
+ return cached;
3744
4143
  }
3745
4144
 
3746
- return sortSessionsByUpdatedAt(deduped).slice(0, limit);
3747
- }
4145
+ let stream;
4146
+ let rl;
4147
+ let messageCount = 0;
4148
+ let leadingSystem = true;
3748
4149
 
3749
- function normalizeSessionPathFilter(pathFilter) {
3750
- if (typeof pathFilter !== 'string') {
3751
- return '';
4150
+ try {
4151
+ stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
4152
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
4153
+
4154
+ for await (const line of rl) {
4155
+ const trimmed = line.trim();
4156
+ if (!trimmed) continue;
4157
+
4158
+ let record;
4159
+ try {
4160
+ record = JSON.parse(trimmed);
4161
+ } catch (e) {
4162
+ continue;
4163
+ }
4164
+
4165
+ let role = '';
4166
+ let text = '';
4167
+ if (source === 'codex') {
4168
+ if (record.type === 'response_item' && record.payload && record.payload.type === 'message') {
4169
+ role = normalizeRole(record.payload.role);
4170
+ text = extractMessageText(record.payload.content);
4171
+ }
4172
+ } else {
4173
+ role = normalizeRole(record.type);
4174
+ if (role === 'assistant' || role === 'user' || role === 'system') {
4175
+ const content = record.message ? record.message.content : '';
4176
+ text = extractMessageText(content);
4177
+ } else {
4178
+ role = '';
4179
+ }
4180
+ }
4181
+ if (!role) {
4182
+ continue;
4183
+ }
4184
+
4185
+ const hasText = text.length > 0;
4186
+ if (leadingSystem && (role === 'system' || (hasText && isBootstrapLikeText(text)))) {
4187
+ continue;
4188
+ }
4189
+
4190
+ leadingSystem = false;
4191
+ messageCount += 1;
4192
+ }
4193
+ const safeCount = Math.max(0, messageCount);
4194
+ writeExactMessageCountCache(filePath, source, safeCount, fileStat);
4195
+ return safeCount;
4196
+ } catch (e) {
4197
+ const safeCount = countConversationMessagesInRecords(readJsonlRecords(filePath), source);
4198
+ writeExactMessageCountCache(filePath, source, safeCount, fileStat);
4199
+ return safeCount;
4200
+ } finally {
4201
+ if (rl) {
4202
+ try { rl.close(); } catch (e) {}
4203
+ }
4204
+ if (stream && !stream.destroyed && stream.destroy) {
4205
+ try { stream.destroy(); } catch (e) {}
4206
+ }
3752
4207
  }
3753
- const trimmed = pathFilter.trim();
3754
- return trimmed ? trimmed.toLowerCase() : '';
3755
4208
  }
3756
4209
 
3757
- function matchesSessionPathFilter(session, normalizedFilter) {
3758
- if (!normalizedFilter) {
3759
- return true;
4210
+ function appendSessionDetailTailMessage(state, record, source, lineIndex = -1) {
4211
+ if (!state || typeof state !== 'object') {
4212
+ return;
3760
4213
  }
3761
- if (!session || typeof session !== 'object') {
3762
- return false;
4214
+
4215
+ const message = extractMessageFromRecord(record, source);
4216
+ if (!message) {
4217
+ return;
3763
4218
  }
3764
4219
 
3765
- const cwd = typeof session.cwd === 'string' ? session.cwd.toLowerCase() : '';
3766
- return cwd.includes(normalizedFilter);
3767
- }
4220
+ const role = normalizeRole(message.role);
4221
+ const text = typeof message.text === 'string' ? message.text : '';
4222
+ if (!role || !text) {
4223
+ return;
4224
+ }
4225
+
4226
+ if (state.leadingSystem && (role === 'system' || isBootstrapLikeText(text))) {
4227
+ return;
4228
+ }
4229
+
4230
+ state.leadingSystem = false;
4231
+ state.totalMessages += 1;
4232
+ if (!Number.isFinite(state.tailLimit) || state.tailLimit <= 0) {
4233
+ return;
4234
+ }
4235
+
4236
+ if (state.messages.length >= state.tailLimit) {
4237
+ state.messages.shift();
4238
+ }
4239
+ state.messages.push({
4240
+ role,
4241
+ text,
4242
+ timestamp: toIsoTime(record && record.timestamp, ''),
4243
+ recordLineIndex: Number.isInteger(lineIndex) ? lineIndex : -1
4244
+ });
4245
+ }
4246
+
4247
+ function applySessionDetailRecordMetadata(record, source, state) {
4248
+ if (!state || typeof state !== 'object' || !record) {
4249
+ return;
4250
+ }
4251
+
4252
+ if (record.timestamp) {
4253
+ state.updatedAt = toIsoTime(record.timestamp, state.updatedAt);
4254
+ }
4255
+
4256
+ if (source === 'codex') {
4257
+ if (record.type === 'session_meta' && record.payload) {
4258
+ state.sessionId = record.payload.id || state.sessionId;
4259
+ state.cwd = record.payload.cwd || state.cwd;
4260
+ }
4261
+ return;
4262
+ }
4263
+
4264
+ if (!state.sessionId && record.sessionId) {
4265
+ state.sessionId = record.sessionId;
4266
+ }
4267
+ if (!state.cwd && record.cwd) {
4268
+ state.cwd = record.cwd;
4269
+ }
4270
+ }
4271
+
4272
+ function extractSessionDetailPreviewFromRecords(records, source, messageLimit) {
4273
+ const safeMessageLimit = Number.isFinite(Number(messageLimit))
4274
+ ? Math.max(1, Math.floor(Number(messageLimit)))
4275
+ : DEFAULT_SESSION_DETAIL_MESSAGES;
4276
+ const state = {
4277
+ sessionId: '',
4278
+ cwd: '',
4279
+ updatedAt: '',
4280
+ messages: [],
4281
+ tailLimit: safeMessageLimit,
4282
+ totalMessages: 0,
4283
+ leadingSystem: true
4284
+ };
4285
+
4286
+ for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
4287
+ const record = records[lineIndex];
4288
+ applySessionDetailRecordMetadata(record, source, state);
4289
+ appendSessionDetailTailMessage(state, record, source, lineIndex);
4290
+ }
4291
+
4292
+ return state;
4293
+ }
4294
+
4295
+ async function extractSessionDetailPreviewFromFile(filePath, source, messageLimit) {
4296
+ const safeMessageLimit = Number.isFinite(Number(messageLimit))
4297
+ ? Math.max(1, Math.floor(Number(messageLimit)))
4298
+ : DEFAULT_SESSION_DETAIL_MESSAGES;
4299
+ const state = {
4300
+ sessionId: '',
4301
+ cwd: '',
4302
+ updatedAt: '',
4303
+ messages: [],
4304
+ tailLimit: safeMessageLimit,
4305
+ totalMessages: 0,
4306
+ leadingSystem: true
4307
+ };
4308
+
4309
+ let stream;
4310
+ let rl;
4311
+ try {
4312
+ stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
4313
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
4314
+
4315
+ let lineIndex = 0;
4316
+ for await (const line of rl) {
4317
+ const currentLineIndex = lineIndex;
4318
+ lineIndex += 1;
4319
+
4320
+ const trimmed = line.trim();
4321
+ if (!trimmed) {
4322
+ continue;
4323
+ }
4324
+
4325
+ let record;
4326
+ try {
4327
+ record = JSON.parse(trimmed);
4328
+ } catch (e) {
4329
+ continue;
4330
+ }
4331
+
4332
+ applySessionDetailRecordMetadata(record, source, state);
4333
+ appendSessionDetailTailMessage(state, record, source, currentLineIndex);
4334
+ }
4335
+ return state;
4336
+ } catch (e) {
4337
+ return extractSessionDetailPreviewFromRecords(readJsonlRecords(filePath), source, safeMessageLimit);
4338
+ } finally {
4339
+ if (rl) {
4340
+ try { rl.close(); } catch (e) {}
4341
+ }
4342
+ if (stream && !stream.destroyed && stream.destroy) {
4343
+ try { stream.destroy(); } catch (e) {}
4344
+ }
4345
+ }
4346
+ }
4347
+
4348
+ async function resolveSessionTrashEntryExactMessageCount(entry) {
4349
+ const normalizedEntry = normalizeSessionTrashEntry(entry);
4350
+ if (!normalizedEntry) {
4351
+ return null;
4352
+ }
4353
+ const trashFilePath = resolveSessionTrashFilePath(normalizedEntry);
4354
+ if (!trashFilePath || !fs.existsSync(trashFilePath)) {
4355
+ return normalizedEntry;
4356
+ }
4357
+ const trashFileStat = getFileStatSafe(trashFilePath);
4358
+ const trashFileMtimeMs = getFileMtimeMs(trashFilePath, trashFileStat);
4359
+ if (
4360
+ Number.isFinite(Number(normalizedEntry.messageCount))
4361
+ && normalizedEntry.messageCount >= 0
4362
+ && trashFileMtimeMs > 0
4363
+ && normalizedEntry.messageCountMtimeMs === trashFileMtimeMs
4364
+ ) {
4365
+ return normalizedEntry;
4366
+ }
4367
+
4368
+ const exactMessageCount = await countConversationMessagesInFile(trashFilePath, normalizedEntry.source);
4369
+ if (!Number.isFinite(Number(exactMessageCount))) {
4370
+ return normalizedEntry;
4371
+ }
4372
+
4373
+ const safeMessageCount = Math.max(0, Math.floor(Number(exactMessageCount)));
4374
+ if (
4375
+ normalizedEntry.messageCount === safeMessageCount
4376
+ && normalizedEntry.messageCountMtimeMs === trashFileMtimeMs
4377
+ ) {
4378
+ return normalizedEntry;
4379
+ }
4380
+
4381
+ return {
4382
+ ...normalizedEntry,
4383
+ messageCount: safeMessageCount,
4384
+ messageCountMtimeMs: trashFileMtimeMs
4385
+ };
4386
+ }
4387
+
4388
+ async function hydrateSessionTrashEntries(entries, options = {}) {
4389
+ const source = options.source === 'claude' ? 'claude' : (options.source === 'codex' ? 'codex' : 'all');
4390
+ const hydratedEntries = await mapWithConcurrency(Array.isArray(entries) ? entries : [], 8, async (entry) => {
4391
+ const normalizedEntry = normalizeSessionTrashEntry(entry);
4392
+ if (!normalizedEntry) {
4393
+ return undefined;
4394
+ }
4395
+ return await resolveSessionTrashEntryExactMessageCount(normalizedEntry);
4396
+ });
4397
+
4398
+ if (source === 'codex' || source === 'claude') {
4399
+ return hydratedEntries.filter((entry) => entry.source === source);
4400
+ }
4401
+ return hydratedEntries;
4402
+ }
4403
+
4404
+ async function hydrateSessionItemsExactMessageCount(items) {
4405
+ return await mapWithConcurrency(Array.isArray(items) ? items : [], 8, async (item) => {
4406
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
4407
+ return undefined;
4408
+ }
4409
+ if (item.__messageCountExact === true) {
4410
+ return item;
4411
+ }
4412
+ const source = item.source === 'claude' ? 'claude' : (item.source === 'codex' ? 'codex' : '');
4413
+ const filePath = typeof item.filePath === 'string' ? item.filePath : '';
4414
+ if (!source || !filePath || !fs.existsSync(filePath)) {
4415
+ return item;
4416
+ }
4417
+
4418
+ const exactMessageCount = await countConversationMessagesInFile(filePath, source);
4419
+ if (!Number.isFinite(Number(exactMessageCount))) {
4420
+ return item;
4421
+ }
4422
+
4423
+ const safeMessageCount = Math.max(0, Math.floor(Number(exactMessageCount)));
4424
+ if (Number(item.messageCount) === safeMessageCount) {
4425
+ return {
4426
+ ...item,
4427
+ __messageCountExact: true
4428
+ };
4429
+ }
4430
+
4431
+ return {
4432
+ ...item,
4433
+ messageCount: safeMessageCount,
4434
+ __messageCountExact: true
4435
+ };
4436
+ });
4437
+ }
4438
+
4439
+ function sortSessionsByUpdatedAt(items) {
4440
+ items.sort((a, b) => {
4441
+ const aTime = Date.parse(a.updatedAt || '') || 0;
4442
+ const bTime = Date.parse(b.updatedAt || '') || 0;
4443
+ return bTime - aTime;
4444
+ });
4445
+ return items;
4446
+ }
4447
+
4448
+ function mergeAndLimitSessions(items, limit) {
4449
+ const deduped = [];
4450
+ const seen = new Set();
4451
+ for (const item of items) {
4452
+ if (!item || !item.filePath) continue;
4453
+ const key = `${item.source}:${item.filePath}`;
4454
+ if (seen.has(key)) continue;
4455
+ seen.add(key);
4456
+ deduped.push(item);
4457
+ }
4458
+
4459
+ return sortSessionsByUpdatedAt(deduped).slice(0, limit);
4460
+ }
4461
+
4462
+ function normalizeSessionPathFilter(pathFilter) {
4463
+ if (typeof pathFilter !== 'string') {
4464
+ return '';
4465
+ }
4466
+ const trimmed = pathFilter.trim();
4467
+ return trimmed ? trimmed.toLowerCase() : '';
4468
+ }
4469
+
4470
+ function matchesSessionPathFilter(session, normalizedFilter) {
4471
+ if (!normalizedFilter) {
4472
+ return true;
4473
+ }
4474
+ if (!session || typeof session !== 'object') {
4475
+ return false;
4476
+ }
4477
+
4478
+ const cwd = typeof session.cwd === 'string' ? session.cwd.toLowerCase() : '';
4479
+ return cwd.includes(normalizedFilter);
4480
+ }
3768
4481
 
3769
4482
  function normalizeQueryTokens(query) {
3770
4483
  if (typeof query !== 'string') {
@@ -4240,6 +4953,7 @@ function parseCodexSessionSummary(filePath) {
4240
4953
  createdAt,
4241
4954
  updatedAt,
4242
4955
  messageCount,
4956
+ __messageCountExact: isSessionSummaryMessageCountExact(stat),
4243
4957
  filePath,
4244
4958
  keywords: [],
4245
4959
  capabilities: {}
@@ -4329,6 +5043,7 @@ function parseClaudeSessionSummary(filePath) {
4329
5043
  createdAt,
4330
5044
  updatedAt,
4331
5045
  messageCount,
5046
+ __messageCountExact: isSessionSummaryMessageCountExact(stat),
4332
5047
  filePath,
4333
5048
  keywords: [],
4334
5049
  capabilities: { code: true }
@@ -4425,7 +5140,8 @@ function listClaudeSessions(limit, options = {}) {
4425
5140
  }
4426
5141
  filePath = filePath ? path.resolve(filePath) : '';
4427
5142
 
4428
- if (!fs.existsSync(filePath)) {
5143
+ const fileStat = getFileStatSafe(filePath);
5144
+ if (!fileStat) {
4429
5145
  continue;
4430
5146
  }
4431
5147
 
@@ -4434,7 +5150,7 @@ function listClaudeSessions(limit, options = {}) {
4434
5150
  let title = truncateText(entry.summary || entry.firstPrompt || sessionId, 120);
4435
5151
  let messageCount = Number.isFinite(entry.messageCount) ? Math.max(0, entry.messageCount - 1) : 0;
4436
5152
 
4437
- const quickRecords = parseJsonlHeadRecords(filePath, SESSION_TITLE_READ_BYTES);
5153
+ const quickRecords = parseJsonlHeadRecords(filePath, SESSION_SUMMARY_READ_BYTES);
4438
5154
  if (quickRecords.length > 0) {
4439
5155
  const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
4440
5156
  if (filteredCount > 0 || messageCount === 0) {
@@ -4472,6 +5188,7 @@ function listClaudeSessions(limit, options = {}) {
4472
5188
  createdAt,
4473
5189
  updatedAt,
4474
5190
  messageCount,
5191
+ __messageCountExact: quickRecords.length > 0 && isSessionSummaryMessageCountExact(fileStat),
4475
5192
  filePath,
4476
5193
  keywords,
4477
5194
  capabilities
@@ -4566,6 +5283,42 @@ function listAllSessions(params = {}) {
4566
5283
  return result;
4567
5284
  }
4568
5285
 
5286
+ async function listAllSessionsData(params = {}) {
5287
+ const source = params.source === 'codex' || params.source === 'claude'
5288
+ ? params.source
5289
+ : 'all';
5290
+ const rawLimit = Number(params.limit);
5291
+ const limit = Number.isFinite(rawLimit)
5292
+ ? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
5293
+ : 120;
5294
+ const forceRefresh = !!params.forceRefresh;
5295
+ const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
5296
+ const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
5297
+ const hasQuery = queryTokens.length > 0;
5298
+ const cacheKey = hasQuery ? '' : `exact:${source}:${limit}:${normalizedPathFilter}`;
5299
+ if (!hasQuery) {
5300
+ const cached = getSessionListCache(cacheKey, forceRefresh);
5301
+ if (cached) {
5302
+ return cached;
5303
+ }
5304
+ }
5305
+
5306
+ const sessions = listAllSessions(params);
5307
+ const hydratedSessions = await hydrateSessionItemsExactMessageCount(sessions);
5308
+ const result = hydratedSessions.map((item) => {
5309
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
5310
+ return item;
5311
+ }
5312
+ const normalized = { ...item };
5313
+ delete normalized.__messageCountExact;
5314
+ return normalized;
5315
+ });
5316
+ if (!hasQuery) {
5317
+ setSessionListCache(cacheKey, result);
5318
+ }
5319
+ return result;
5320
+ }
5321
+
4569
5322
  function listSessionPaths(params = {}) {
4570
5323
  const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
4571
5324
  if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
@@ -4647,6 +5400,19 @@ function resolveSessionFilePath(source, filePath, sessionId) {
4647
5400
  return '';
4648
5401
  }
4649
5402
 
5403
+ function getSessionFileArg(params = {}) {
5404
+ if (!params || typeof params !== 'object') {
5405
+ return '';
5406
+ }
5407
+ if (typeof params.filePath === 'string' && params.filePath.trim()) {
5408
+ return params.filePath.trim();
5409
+ }
5410
+ if (typeof params.file === 'string' && params.file.trim()) {
5411
+ return params.file.trim();
5412
+ }
5413
+ return '';
5414
+ }
5415
+
4650
5416
  function findClaudeSessionIndexPath(sessionFilePath) {
4651
5417
  const root = getClaudeProjectsDir();
4652
5418
  if (!root || !sessionFilePath) {
@@ -4786,6 +5552,124 @@ function buildProxyListenUrl(settings) {
4786
5552
  return `http://${host}:${settings.port}`;
4787
5553
  }
4788
5554
 
5555
+ function buildBuiltinProxyProviderBaseUrl(settings) {
5556
+ return `${buildProxyListenUrl(settings).replace(/\/+$/, '')}/v1`;
5557
+ }
5558
+
5559
+ function buildBuiltinProxyProviderConfig(settings) {
5560
+ return {
5561
+ name: BUILTIN_PROXY_PROVIDER_NAME,
5562
+ base_url: buildBuiltinProxyProviderBaseUrl(settings),
5563
+ wire_api: 'responses',
5564
+ requires_openai_auth: false,
5565
+ preferred_auth_method: '',
5566
+ request_max_retries: 4,
5567
+ stream_max_retries: 10,
5568
+ stream_idle_timeout_ms: 300000
5569
+ };
5570
+ }
5571
+
5572
+ function injectBuiltinProxyProvider(config) {
5573
+ return isPlainObject(config) ? config : {};
5574
+ }
5575
+
5576
+ function removePersistedBuiltinProxyProviderFromConfig() {
5577
+ if (!fs.existsSync(CONFIG_FILE)) {
5578
+ return { success: true, removed: false };
5579
+ }
5580
+
5581
+ let config;
5582
+ try {
5583
+ config = readConfig();
5584
+ } catch (e) {
5585
+ return { error: e.message || '读取 config.toml 失败' };
5586
+ }
5587
+
5588
+ if (!config.model_providers || !config.model_providers[BUILTIN_PROXY_PROVIDER_NAME]) {
5589
+ return { success: true, removed: false };
5590
+ }
5591
+
5592
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
5593
+ const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
5594
+ const hasBom = content.charCodeAt(0) === 0xFEFF;
5595
+ const providerConfig = config.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
5596
+ const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
5597
+ ? providerConfig.__codexmate_legacy_segments
5598
+ : null;
5599
+ const providerSegmentVariants = (() => {
5600
+ const variants = [];
5601
+ const seen = new Set();
5602
+ const pushVariant = (segments) => {
5603
+ const normalized = normalizeLegacySegments(segments);
5604
+ const key = buildLegacySegmentsKey(normalized);
5605
+ if (!key || seen.has(key)) return;
5606
+ seen.add(key);
5607
+ variants.push(normalized);
5608
+ };
5609
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) {
5610
+ pushVariant(providerConfig.__codexmate_legacy_segments);
5611
+ }
5612
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) {
5613
+ for (const segments of providerConfig.__codexmate_legacy_segment_variants) {
5614
+ pushVariant(segments);
5615
+ }
5616
+ }
5617
+ if (providerSegments) {
5618
+ pushVariant(providerSegments);
5619
+ }
5620
+ if (variants.length === 0) {
5621
+ pushVariant(String(BUILTIN_PROXY_PROVIDER_NAME || '').split('.').filter((item) => item));
5622
+ }
5623
+ return variants;
5624
+ })();
5625
+
5626
+ let updatedContent = null;
5627
+ const combinedRanges = [];
5628
+ for (const segments of providerSegmentVariants) {
5629
+ combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, segments));
5630
+ combinedRanges.push(...findProviderDescendantSectionRanges(content, segments));
5631
+ }
5632
+ if (combinedRanges.length === 0) {
5633
+ combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, providerSegments));
5634
+ }
5635
+
5636
+ if (combinedRanges.length > 0) {
5637
+ const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end);
5638
+ const seen = new Set();
5639
+ let removedContent = content;
5640
+ for (const range of sorted) {
5641
+ const rangeKey = `${range.start}:${range.end}`;
5642
+ if (seen.has(rangeKey)) continue;
5643
+ seen.add(rangeKey);
5644
+ removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end);
5645
+ }
5646
+ updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding);
5647
+ }
5648
+
5649
+ if (!updatedContent) {
5650
+ const rebuilt = JSON.parse(JSON.stringify(config));
5651
+ delete rebuilt.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
5652
+ const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER);
5653
+ let rebuiltToml = toml.stringify(rebuilt).trimEnd();
5654
+ rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding);
5655
+ if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) {
5656
+ rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`;
5657
+ }
5658
+ updatedContent = rebuiltToml + lineEnding;
5659
+ if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) {
5660
+ updatedContent = '\uFEFF' + updatedContent;
5661
+ }
5662
+ }
5663
+
5664
+ try {
5665
+ writeConfig(updatedContent.trimEnd() + lineEnding);
5666
+ } catch (e) {
5667
+ return { error: e.message || '写入 config.toml 失败' };
5668
+ }
5669
+
5670
+ return { success: true, removed: true };
5671
+ }
5672
+
4789
5673
  function hasCodexConfigReadyForProxy() {
4790
5674
  const result = readConfigOrVirtualDefault();
4791
5675
  if (!result || result.isVirtual) {
@@ -5061,168 +5945,703 @@ function getBuiltinProxyStatus() {
5061
5945
  };
5062
5946
  }
5063
5947
 
5064
- function applyBuiltinProxyProvider(params = {}) {
5065
- const settings = readBuiltinProxySettings();
5066
- const hostForUrl = formatHostForUrl(settings.host);
5067
- const baseUrl = `http://${hostForUrl}:${settings.port}`;
5068
-
5069
- const { config } = readConfigOrVirtualDefault();
5070
- const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
5071
- const exists = !!providers[BUILTIN_PROXY_PROVIDER_NAME];
5072
- const saveResult = exists
5073
- ? updateProviderInConfig({
5074
- name: BUILTIN_PROXY_PROVIDER_NAME,
5075
- url: baseUrl,
5076
- key: '',
5077
- allowManaged: true
5078
- })
5079
- : addProviderToConfig({
5080
- name: BUILTIN_PROXY_PROVIDER_NAME,
5081
- url: baseUrl,
5082
- key: '',
5083
- allowManaged: true
5084
- });
5085
-
5086
- if (saveResult && saveResult.error) {
5087
- return saveResult;
5948
+ function applyBuiltinProxyProvider(params = {}) {
5949
+ return { error: '该功能已移除' };
5950
+ }
5951
+
5952
+ async function ensureBuiltinProxyForCodexDefault(params = {}) {
5953
+ return { error: '该功能已移除' };
5954
+ }
5955
+
5956
+ function removeClaudeSessionIndexEntry(indexPath, sessionFilePath, sessionId) {
5957
+ if (!indexPath || !fs.existsSync(indexPath)) {
5958
+ return { removed: false, entry: null };
5959
+ }
5960
+ const index = readJsonFile(indexPath, null);
5961
+ if (!index || !Array.isArray(index.entries)) {
5962
+ return { removed: false, entry: null };
5963
+ }
5964
+ const ignoreCase = process.platform === 'win32';
5965
+ const resolvedFile = sessionFilePath
5966
+ ? normalizePathForCompare(sessionFilePath, { ignoreCase })
5967
+ : '';
5968
+ let removedEntry = null;
5969
+ const filtered = index.entries.filter((entry) => {
5970
+ if (!entry || typeof entry !== 'object') {
5971
+ return false;
5972
+ }
5973
+ if (entry.fullPath) {
5974
+ const expanded = expandHomePath(entry.fullPath);
5975
+ const entryPath = expanded
5976
+ ? normalizePathForCompare(expanded, { ignoreCase })
5977
+ : '';
5978
+ if (entryPath && resolvedFile && entryPath === resolvedFile) {
5979
+ if (!removedEntry) {
5980
+ removedEntry = entry;
5981
+ }
5982
+ return false;
5983
+ }
5984
+ }
5985
+ const entrySessionId = typeof entry.sessionId === 'string' ? entry.sessionId : '';
5986
+ if (!resolvedFile && sessionId && entrySessionId === sessionId) {
5987
+ if (!removedEntry) {
5988
+ removedEntry = entry;
5989
+ }
5990
+ return false;
5991
+ }
5992
+ return true;
5993
+ });
5994
+ if (filtered.length === index.entries.length) {
5995
+ return { removed: false, entry: null };
5996
+ }
5997
+ index.entries = filtered;
5998
+ writeJsonAtomic(indexPath, index);
5999
+ return {
6000
+ removed: true,
6001
+ entry: removedEntry && typeof removedEntry === 'object'
6002
+ ? JSON.parse(JSON.stringify(removedEntry))
6003
+ : null
6004
+ };
6005
+ }
6006
+
6007
+ function moveFileSync(sourcePath, targetPath) {
6008
+ ensureDir(path.dirname(targetPath));
6009
+ try {
6010
+ fs.renameSync(sourcePath, targetPath);
6011
+ return;
6012
+ } catch (error) {
6013
+ if (!error || error.code !== 'EXDEV') {
6014
+ throw error;
6015
+ }
6016
+ }
6017
+
6018
+ fs.copyFileSync(sourcePath, targetPath);
6019
+ try {
6020
+ fs.unlinkSync(sourcePath);
6021
+ } catch (error) {
6022
+ try {
6023
+ fs.unlinkSync(targetPath);
6024
+ } catch (_) {}
6025
+ throw error;
6026
+ }
6027
+ }
6028
+
6029
+ function buildSessionSummaryFallback(source, filePath, sessionId = '') {
6030
+ const resolvedSessionId = sessionId || path.basename(filePath, '.jsonl');
6031
+ const sourceLabel = source === 'claude' ? 'Claude Code' : 'Codex';
6032
+ return {
6033
+ source,
6034
+ sourceLabel,
6035
+ provider: source === 'claude' ? 'claude' : 'codex',
6036
+ sessionId: resolvedSessionId,
6037
+ title: resolvedSessionId,
6038
+ cwd: '',
6039
+ createdAt: '',
6040
+ updatedAt: '',
6041
+ messageCount: 0,
6042
+ filePath,
6043
+ keywords: [],
6044
+ capabilities: source === 'claude' ? { code: true } : {}
6045
+ };
6046
+ }
6047
+
6048
+ function generateSessionTrashId() {
6049
+ if (crypto.randomUUID) {
6050
+ return `trash-${crypto.randomUUID()}`;
6051
+ }
6052
+ return `trash-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
6053
+ }
6054
+
6055
+ function allocateSessionTrashTarget() {
6056
+ ensureDir(SESSION_TRASH_FILES_DIR);
6057
+ for (let attempt = 0; attempt < 6; attempt += 1) {
6058
+ const trashId = generateSessionTrashId();
6059
+ const trashFileName = `${trashId}.jsonl`;
6060
+ const trashFilePath = path.join(SESSION_TRASH_FILES_DIR, trashFileName);
6061
+ if (!fs.existsSync(trashFilePath)) {
6062
+ return { trashId, trashFileName, trashFilePath };
6063
+ }
6064
+ }
6065
+ const fallbackId = `trash-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
6066
+ return {
6067
+ trashId: fallbackId,
6068
+ trashFileName: `${fallbackId}.jsonl`,
6069
+ trashFilePath: path.join(SESSION_TRASH_FILES_DIR, `${fallbackId}.jsonl`)
6070
+ };
6071
+ }
6072
+
6073
+ function normalizeSessionTrashEntry(entry) {
6074
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
6075
+ return null;
6076
+ }
6077
+ const source = entry.source === 'claude' ? 'claude' : (entry.source === 'codex' ? 'codex' : '');
6078
+ const trashId = typeof entry.trashId === 'string' ? entry.trashId.trim() : '';
6079
+ if (!source || !trashId || trashId.includes('/') || trashId.includes('\\') || trashId.includes('\0')) {
6080
+ return null;
6081
+ }
6082
+ const sessionId = typeof entry.sessionId === 'string' ? entry.sessionId.trim() : '';
6083
+ const trashFileNameRaw = typeof entry.trashFileName === 'string' ? entry.trashFileName.trim() : '';
6084
+ const trashFileName = path.basename(trashFileNameRaw || `${trashId}.jsonl`);
6085
+ if (!trashFileName || trashFileName === '.' || trashFileName === '..' || trashFileName.includes('\0')) {
6086
+ return null;
6087
+ }
6088
+ return {
6089
+ trashId,
6090
+ trashFileName,
6091
+ source,
6092
+ sourceLabel: source === 'claude' ? 'Claude Code' : 'Codex',
6093
+ sessionId: sessionId || trashId,
6094
+ title: typeof entry.title === 'string' && entry.title.trim() ? entry.title.trim() : (sessionId || trashId),
6095
+ cwd: typeof entry.cwd === 'string' ? entry.cwd : '',
6096
+ createdAt: typeof entry.createdAt === 'string' ? entry.createdAt : '',
6097
+ updatedAt: typeof entry.updatedAt === 'string' ? entry.updatedAt : '',
6098
+ deletedAt: typeof entry.deletedAt === 'string' ? entry.deletedAt : '',
6099
+ messageCount: Number.isFinite(Number(entry.messageCount))
6100
+ ? Math.max(0, Math.floor(Number(entry.messageCount)))
6101
+ : 0,
6102
+ messageCountMtimeMs: Number.isFinite(Number(entry.messageCountMtimeMs))
6103
+ ? Math.max(0, Math.floor(Number(entry.messageCountMtimeMs)))
6104
+ : 0,
6105
+ originalFilePath: typeof entry.originalFilePath === 'string' ? entry.originalFilePath : '',
6106
+ provider: typeof entry.provider === 'string' && entry.provider.trim()
6107
+ ? entry.provider.trim()
6108
+ : (source === 'claude' ? 'claude' : 'codex'),
6109
+ keywords: normalizeKeywords(entry.keywords),
6110
+ capabilities: normalizeCapabilities(entry.capabilities),
6111
+ claudeIndexPath: typeof entry.claudeIndexPath === 'string' ? entry.claudeIndexPath : '',
6112
+ claudeIndexEntry: entry.claudeIndexEntry && typeof entry.claudeIndexEntry === 'object' && !Array.isArray(entry.claudeIndexEntry)
6113
+ ? entry.claudeIndexEntry
6114
+ : null
6115
+ };
6116
+ }
6117
+
6118
+ function resolveSessionTrashFilePath(entry) {
6119
+ const normalized = normalizeSessionTrashEntry(entry);
6120
+ if (!normalized) {
6121
+ return '';
6122
+ }
6123
+ const filePath = path.join(SESSION_TRASH_FILES_DIR, normalized.trashFileName);
6124
+ return isPathInside(filePath, SESSION_TRASH_FILES_DIR) ? filePath : '';
6125
+ }
6126
+
6127
+ function writeSessionTrashEntries(entries) {
6128
+ writeJsonAtomic(SESSION_TRASH_INDEX_FILE, {
6129
+ version: 1,
6130
+ updatedAt: new Date().toISOString(),
6131
+ entries
6132
+ });
6133
+ }
6134
+
6135
+ function readSessionTrashEntries(options = {}) {
6136
+ const cleanup = options.cleanup !== false;
6137
+ const parsed = readJsonFile(SESSION_TRASH_INDEX_FILE, null);
6138
+ if (!parsed || !Array.isArray(parsed.entries)) {
6139
+ return [];
6140
+ }
6141
+
6142
+ const normalizedEntries = [];
6143
+ let dirty = false;
6144
+ for (const rawEntry of parsed.entries) {
6145
+ const entry = normalizeSessionTrashEntry(rawEntry);
6146
+ if (!entry) {
6147
+ dirty = true;
6148
+ continue;
6149
+ }
6150
+ const trashFilePath = resolveSessionTrashFilePath(entry);
6151
+ if (!trashFilePath || !fs.existsSync(trashFilePath)) {
6152
+ dirty = true;
6153
+ continue;
6154
+ }
6155
+ normalizedEntries.push(entry);
6156
+ }
6157
+
6158
+ if (dirty && cleanup) {
6159
+ writeSessionTrashEntries(normalizedEntries);
6160
+ }
6161
+
6162
+ return normalizedEntries;
6163
+ }
6164
+
6165
+ function buildSessionTrashEntry(summary, options = {}) {
6166
+ const source = options.source === 'claude' ? 'claude' : 'codex';
6167
+ const sessionId = options.sessionId || summary.sessionId || path.basename(options.originalFilePath || summary.filePath || '', '.jsonl');
6168
+ const claudeIndexEntry = options.claudeIndexEntry && typeof options.claudeIndexEntry === 'object' && !Array.isArray(options.claudeIndexEntry)
6169
+ ? options.claudeIndexEntry
6170
+ : null;
6171
+ const deletedAt = typeof options.deletedAt === 'string' && options.deletedAt
6172
+ ? options.deletedAt
6173
+ : new Date().toISOString();
6174
+ const sourceLabel = source === 'claude' ? 'Claude Code' : 'Codex';
6175
+ const fallbackTitle = truncateText(
6176
+ (claudeIndexEntry && (claudeIndexEntry.summary || claudeIndexEntry.firstPrompt)) || sessionId,
6177
+ 120
6178
+ );
6179
+ const rawFallbackMessageCount = claudeIndexEntry && claudeIndexEntry.messageCount;
6180
+ const fallbackMessageCount = Number.isFinite(Number(rawFallbackMessageCount))
6181
+ ? Math.max(0, Number(rawFallbackMessageCount))
6182
+ : 0;
6183
+ const resolvedMessageCount = Number.isFinite(Number(summary && summary.messageCount))
6184
+ ? Math.max(0, Math.floor(Number(summary.messageCount)))
6185
+ : fallbackMessageCount;
6186
+ const messageCountMtimeMs = getFileMtimeMs(options.trashFilePath);
6187
+ const normalizedClaudeKeywords = claudeIndexEntry && Array.isArray(claudeIndexEntry.keywords)
6188
+ ? normalizeKeywords(claudeIndexEntry.keywords)
6189
+ : [];
6190
+ const normalizedClaudeCapabilities = claudeIndexEntry
6191
+ ? normalizeCapabilities(claudeIndexEntry.capabilities)
6192
+ : {};
6193
+ const normalizedSummaryKeywords = normalizeKeywords(summary.keywords);
6194
+ const normalizedSummaryCapabilities = normalizeCapabilities(summary.capabilities);
6195
+ return {
6196
+ trashId: options.trashId,
6197
+ trashFileName: options.trashFileName,
6198
+ source,
6199
+ sourceLabel,
6200
+ sessionId,
6201
+ title: summary.title || fallbackTitle || sessionId,
6202
+ cwd: summary.cwd || (claudeIndexEntry && typeof claudeIndexEntry.projectPath === 'string' ? claudeIndexEntry.projectPath : ''),
6203
+ createdAt: summary.createdAt || toIsoTime(claudeIndexEntry && claudeIndexEntry.created, ''),
6204
+ updatedAt: summary.updatedAt || toIsoTime(claudeIndexEntry && (claudeIndexEntry.modified || claudeIndexEntry.fileMtime), ''),
6205
+ deletedAt,
6206
+ messageCount: resolvedMessageCount,
6207
+ messageCountMtimeMs,
6208
+ originalFilePath: options.originalFilePath || summary.filePath || '',
6209
+ provider: (claudeIndexEntry && typeof claudeIndexEntry.provider === 'string' && claudeIndexEntry.provider.trim())
6210
+ ? claudeIndexEntry.provider.trim()
6211
+ : (summary.provider || (source === 'claude' ? 'claude' : 'codex')),
6212
+ keywords: normalizedClaudeKeywords.length > 0 ? normalizedClaudeKeywords : normalizedSummaryKeywords,
6213
+ capabilities: Object.keys(normalizedClaudeCapabilities).length > 0
6214
+ ? normalizedClaudeCapabilities
6215
+ : normalizedSummaryCapabilities,
6216
+ claudeIndexPath: typeof options.claudeIndexPath === 'string' ? options.claudeIndexPath : '',
6217
+ claudeIndexEntry
6218
+ };
6219
+ }
6220
+
6221
+ function resolveSessionRestoreTarget(entry) {
6222
+ const normalized = normalizeSessionTrashEntry(entry);
6223
+ if (!normalized) {
6224
+ return '';
6225
+ }
6226
+ const root = normalized.source === 'claude' ? getClaudeProjectsDir() : getCodexSessionsDir();
6227
+ const originalFilePath = typeof normalized.originalFilePath === 'string' ? normalized.originalFilePath.trim() : '';
6228
+ if (!root || !originalFilePath) {
6229
+ return '';
6230
+ }
6231
+ const expanded = expandHomePath(originalFilePath);
6232
+ const resolved = expanded ? path.resolve(expanded) : '';
6233
+ if (!resolved || !isPathInside(resolved, root)) {
6234
+ return '';
6235
+ }
6236
+ return resolved;
6237
+ }
6238
+
6239
+ function resolveClaudeSessionRestoreIndexPath(entry, targetFilePath) {
6240
+ const fallbackIndexPath = findClaudeSessionIndexPath(targetFilePath) || path.join(path.dirname(targetFilePath), 'sessions-index.json');
6241
+ const fallbackResolved = fallbackIndexPath ? path.resolve(fallbackIndexPath) : '';
6242
+ const candidateRaw = entry && typeof entry.claudeIndexPath === 'string' ? entry.claudeIndexPath.trim() : '';
6243
+ if (!candidateRaw) {
6244
+ return fallbackResolved;
6245
+ }
6246
+ const claudeProjectsDir = getClaudeProjectsDir();
6247
+ if (!claudeProjectsDir) {
6248
+ return fallbackResolved;
6249
+ }
6250
+ const candidateIndexPath = path.resolve(candidateRaw);
6251
+ if (path.basename(candidateIndexPath).toLowerCase() !== 'sessions-index.json') {
6252
+ return fallbackResolved;
6253
+ }
6254
+ if (!isPathInside(candidateIndexPath, claudeProjectsDir)) {
6255
+ return fallbackResolved;
6256
+ }
6257
+ if (!isPathInside(targetFilePath, path.dirname(candidateIndexPath))) {
6258
+ return fallbackResolved;
6259
+ }
6260
+ return candidateIndexPath;
6261
+ }
6262
+
6263
+ function buildClaudeSessionIndexEntry(entry, sessionFilePath) {
6264
+ const normalized = normalizeSessionTrashEntry(entry);
6265
+ const stored = normalized && normalized.claudeIndexEntry && typeof normalized.claudeIndexEntry === 'object'
6266
+ ? JSON.parse(JSON.stringify(normalized.claudeIndexEntry))
6267
+ : {};
6268
+ const storedCapabilities = stored && stored.capabilities && typeof stored.capabilities === 'object' && !Array.isArray(stored.capabilities)
6269
+ ? stored.capabilities
6270
+ : null;
6271
+ const storedKeywords = Array.isArray(stored && stored.keywords)
6272
+ ? stored.keywords
6273
+ : null;
6274
+ const normalizedMessageCount = Number(normalized && normalized.messageCount);
6275
+ const storedMessageCount = Number(stored && stored.messageCount);
6276
+ let modifiedAt = '';
6277
+ try {
6278
+ modifiedAt = fs.statSync(sessionFilePath).mtime.toISOString();
6279
+ } catch (e) {
6280
+ modifiedAt = normalized && normalized.updatedAt ? normalized.updatedAt : new Date().toISOString();
6281
+ }
6282
+ const projectDir = path.dirname(sessionFilePath);
6283
+ return {
6284
+ ...stored,
6285
+ sessionId: normalized.sessionId,
6286
+ fullPath: sessionFilePath,
6287
+ projectPath: (stored && typeof stored.projectPath === 'string' && stored.projectPath.trim())
6288
+ ? stored.projectPath.trim()
6289
+ : projectDir,
6290
+ created: (stored && typeof stored.created === 'string' && stored.created.trim())
6291
+ ? stored.created.trim()
6292
+ : (normalized.createdAt || modifiedAt),
6293
+ modified: modifiedAt,
6294
+ summary: (stored && typeof stored.summary === 'string' && stored.summary.trim())
6295
+ ? stored.summary.trim()
6296
+ : (normalized.title || normalized.sessionId),
6297
+ provider: (stored && typeof stored.provider === 'string' && stored.provider.trim())
6298
+ ? stored.provider.trim()
6299
+ : (normalized.provider || 'claude'),
6300
+ capabilities: normalizeCapabilities(
6301
+ storedCapabilities && Object.keys(storedCapabilities).length > 0
6302
+ ? storedCapabilities
6303
+ : normalized.capabilities
6304
+ ),
6305
+ keywords: normalizeKeywords(
6306
+ storedKeywords && storedKeywords.length > 0
6307
+ ? storedKeywords
6308
+ : normalized.keywords
6309
+ ),
6310
+ messageCount: Number.isFinite(normalizedMessageCount)
6311
+ ? buildClaudeStoredIndexMessageCount(normalizedMessageCount)
6312
+ : (
6313
+ Number.isFinite(storedMessageCount)
6314
+ ? Math.max(0, Math.floor(storedMessageCount))
6315
+ : buildClaudeStoredIndexMessageCount(normalized && normalized.messageCount)
6316
+ )
6317
+ };
6318
+ }
6319
+
6320
+ function upsertClaudeSessionIndexEntry(indexPath, sessionFilePath, entry) {
6321
+ if (!indexPath) {
6322
+ return;
6323
+ }
6324
+ const parsed = readJsonFile(indexPath, null);
6325
+ const index = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
6326
+ ? parsed
6327
+ : {};
6328
+ const entries = Array.isArray(index.entries) ? index.entries : [];
6329
+ const ignoreCase = process.platform === 'win32';
6330
+ const resolvedFile = normalizePathForCompare(sessionFilePath, { ignoreCase });
6331
+ const normalizedEntry = normalizeSessionTrashEntry(entry);
6332
+ const filtered = entries.filter((item) => {
6333
+ if (!item || typeof item !== 'object') {
6334
+ return false;
6335
+ }
6336
+ if (typeof item.fullPath === 'string' && item.fullPath) {
6337
+ const expanded = expandHomePath(item.fullPath);
6338
+ const itemPath = expanded
6339
+ ? normalizePathForCompare(expanded, { ignoreCase })
6340
+ : '';
6341
+ if (itemPath && itemPath === resolvedFile) {
6342
+ return false;
6343
+ }
6344
+ }
6345
+ const itemSessionId = typeof item.sessionId === 'string' ? item.sessionId : '';
6346
+ if (!resolvedFile && normalizedEntry.sessionId && itemSessionId === normalizedEntry.sessionId) {
6347
+ return false;
6348
+ }
6349
+ return true;
6350
+ });
6351
+ filtered.unshift(buildClaudeSessionIndexEntry(normalizedEntry, sessionFilePath));
6352
+ index.entries = filtered;
6353
+ if (!index.originalPath) {
6354
+ index.originalPath = path.dirname(indexPath);
6355
+ }
6356
+ writeJsonAtomic(indexPath, index);
6357
+ }
6358
+
6359
+ async function listSessionTrashItems(params = {}) {
6360
+ const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : 'all');
6361
+ const countOnly = params.countOnly === true;
6362
+ const rawLimit = Number(params.limit);
6363
+ const limit = Number.isFinite(rawLimit)
6364
+ ? Math.max(1, Math.min(rawLimit, MAX_SESSION_TRASH_LIST_SIZE))
6365
+ : 200;
6366
+ const allEntries = readSessionTrashEntries();
6367
+ let items = source === 'codex' || source === 'claude'
6368
+ ? allEntries.filter((entry) => entry.source === source)
6369
+ : allEntries.slice();
6370
+ items.sort((a, b) => {
6371
+ const aTime = Date.parse(a.deletedAt || a.updatedAt || '') || 0;
6372
+ const bTime = Date.parse(b.deletedAt || b.updatedAt || '') || 0;
6373
+ return bTime - aTime;
6374
+ });
6375
+ const totalCount = items.length;
6376
+ if (countOnly) {
6377
+ return {
6378
+ totalCount,
6379
+ items: []
6380
+ };
5088
6381
  }
5089
-
5090
- const switchToProxy = params.switchToProxy !== false;
5091
- let targetModel = '';
5092
- if (switchToProxy) {
5093
- try {
5094
- targetModel = cmdSwitch(BUILTIN_PROXY_PROVIDER_NAME, true) || '';
5095
- } catch (e) {
5096
- return { error: `写入代理 provider 成功,但切换失败: ${e.message}` };
6382
+ const visibleEntries = items.slice(0, limit);
6383
+ const hydratedVisibleEntries = await hydrateSessionTrashEntries(visibleEntries, { source });
6384
+ const updatedEntriesById = new Map();
6385
+ for (let index = 0; index < visibleEntries.length; index += 1) {
6386
+ const originalEntry = visibleEntries[index];
6387
+ const hydratedEntry = hydratedVisibleEntries[index];
6388
+ if (!originalEntry || !hydratedEntry) {
6389
+ continue;
6390
+ }
6391
+ if (
6392
+ originalEntry.messageCount !== hydratedEntry.messageCount
6393
+ || originalEntry.messageCountMtimeMs !== hydratedEntry.messageCountMtimeMs
6394
+ ) {
6395
+ updatedEntriesById.set(originalEntry.trashId, hydratedEntry);
5097
6396
  }
5098
6397
  }
5099
-
6398
+ if (updatedEntriesById.size > 0) {
6399
+ const latestEntries = readSessionTrashEntries({ cleanup: false });
6400
+ writeSessionTrashEntries(latestEntries.map((entry) => updatedEntriesById.get(entry.trashId) || entry));
6401
+ }
5100
6402
  return {
5101
- success: true,
5102
- provider: BUILTIN_PROXY_PROVIDER_NAME,
5103
- baseUrl,
5104
- switched: switchToProxy,
5105
- model: targetModel
6403
+ totalCount,
6404
+ items: hydratedVisibleEntries.map((item) => ({
6405
+ ...item,
6406
+ trashFilePath: resolveSessionTrashFilePath(item)
6407
+ }))
5106
6408
  };
5107
6409
  }
5108
6410
 
5109
- async function ensureBuiltinProxyForCodexDefault(params = {}) {
5110
- const payload = isPlainObject(params) ? { ...params } : {};
5111
- const switchToProxy = payload.switchToProxy !== false;
5112
- delete payload.switchToProxy;
5113
- payload.enabled = true;
5114
-
5115
- const saveResult = saveBuiltinProxySettings(payload);
5116
- if (saveResult.error) {
5117
- return { error: saveResult.error };
6411
+ async function restoreSessionTrashItem(params = {}) {
6412
+ const trashId = typeof params.trashId === 'string' ? params.trashId.trim() : '';
6413
+ if (!trashId) {
6414
+ return { error: '请先选择要恢复的回收站记录' };
5118
6415
  }
5119
- let nextSettings = saveResult.settings;
5120
6416
 
5121
- let upstreamResult = resolveBuiltinProxyUpstream(nextSettings);
5122
- if (upstreamResult.error) {
5123
- return { error: upstreamResult.error };
6417
+ const entries = readSessionTrashEntries();
6418
+ const entry = entries.find((item) => item.trashId === trashId);
6419
+ if (!entry) {
6420
+ return { error: '回收站记录不存在' };
6421
+ }
6422
+ const hydratedEntry = await resolveSessionTrashEntryExactMessageCount(entry);
6423
+ if (!hydratedEntry) {
6424
+ return { error: '回收站记录不存在' };
5124
6425
  }
5125
6426
 
5126
- const runtime = g_builtinProxyRuntime;
5127
- const shouldRestart = !!runtime && (
5128
- runtime.settings.host !== nextSettings.host
5129
- || runtime.settings.port !== nextSettings.port
5130
- || runtime.settings.authSource !== nextSettings.authSource
5131
- || runtime.settings.timeoutMs !== nextSettings.timeoutMs
5132
- || runtime.upstream.providerName !== upstreamResult.providerName
5133
- || runtime.upstream.baseUrl !== upstreamResult.baseUrl
5134
- || runtime.upstream.authHeader !== upstreamResult.authHeader
5135
- );
6427
+ const trashFilePath = resolveSessionTrashFilePath(hydratedEntry);
6428
+ if (!trashFilePath || !fs.existsSync(trashFilePath)) {
6429
+ return { error: '回收站文件不存在' };
6430
+ }
5136
6431
 
5137
- if (shouldRestart) {
5138
- await stopBuiltinProxyRuntime();
6432
+ const targetFilePath = resolveSessionRestoreTarget(hydratedEntry);
6433
+ if (!targetFilePath) {
6434
+ return { error: '原始会话路径非法,无法恢复' };
6435
+ }
6436
+ if (fs.existsSync(targetFilePath)) {
6437
+ return { error: '原始会话路径已存在同名文件,请先手动处理冲突' };
5139
6438
  }
5140
6439
 
5141
- if (!g_builtinProxyRuntime) {
5142
- let startRes = await startBuiltinProxyRuntime(nextSettings);
5143
- if (!startRes.success && /EADDRINUSE/i.test(String(startRes.error || ''))) {
5144
- const fallbackPort = await findAvailablePort(nextSettings.host, nextSettings.port + 1, 30);
5145
- if (fallbackPort > 0) {
5146
- const retrySave = saveBuiltinProxySettings({
5147
- ...nextSettings,
5148
- port: fallbackPort,
5149
- enabled: true
5150
- });
5151
- if (retrySave.success) {
5152
- nextSettings = retrySave.settings;
5153
- upstreamResult = resolveBuiltinProxyUpstream(nextSettings);
5154
- if (upstreamResult.error) {
5155
- return { error: upstreamResult.error };
5156
- }
5157
- startRes = await startBuiltinProxyRuntime(nextSettings);
5158
- }
5159
- }
6440
+ let claudeIndexPath = '';
6441
+ try {
6442
+ const latestEntries = readSessionTrashEntries({ cleanup: false });
6443
+ const latestEntry = latestEntries.find((item) => item && item.trashId === trashId);
6444
+ if (!latestEntry) {
6445
+ return { error: '回收站记录不存在' };
6446
+ }
6447
+ const remainingEntries = latestEntries.filter((item) => item.trashId !== trashId);
6448
+ moveFileSync(trashFilePath, targetFilePath);
6449
+ if (hydratedEntry.source === 'claude') {
6450
+ claudeIndexPath = resolveClaudeSessionRestoreIndexPath(hydratedEntry, targetFilePath);
6451
+ upsertClaudeSessionIndexEntry(claudeIndexPath, targetFilePath, hydratedEntry);
6452
+ }
6453
+ writeSessionTrashEntries(remainingEntries);
6454
+ } catch (e) {
6455
+ let rollbackSucceeded = false;
6456
+ if (fs.existsSync(targetFilePath) && !fs.existsSync(trashFilePath)) {
6457
+ try {
6458
+ moveFileSync(targetFilePath, trashFilePath);
6459
+ rollbackSucceeded = true;
6460
+ } catch (_) {}
5160
6461
  }
5161
- if (!startRes.success) {
5162
- return { error: startRes.error || '启动内建代理失败' };
6462
+ if (rollbackSucceeded && entry.source === 'claude' && claudeIndexPath && fs.existsSync(claudeIndexPath)) {
6463
+ try {
6464
+ removeClaudeSessionIndexEntry(claudeIndexPath, targetFilePath, entry.sessionId);
6465
+ } catch (_) {}
5163
6466
  }
6467
+ return { error: `恢复会话失败: ${e.message}` };
5164
6468
  }
5165
6469
 
5166
- let applyRes = {
6470
+ invalidateSessionListCache();
6471
+
6472
+ return {
5167
6473
  success: true,
5168
- provider: BUILTIN_PROXY_PROVIDER_NAME,
5169
- baseUrl: buildProxyListenUrl(nextSettings),
5170
- switched: false,
5171
- model: ''
6474
+ restored: true,
6475
+ trashId,
6476
+ source: entry.source,
6477
+ sessionId: entry.sessionId,
6478
+ filePath: targetFilePath
5172
6479
  };
5173
- if (switchToProxy) {
5174
- applyRes = applyBuiltinProxyProvider({ switchToProxy: true });
5175
- if (applyRes.error) {
5176
- return applyRes;
6480
+ }
6481
+
6482
+ async function purgeSessionTrashItems(params = {}) {
6483
+ const entries = readSessionTrashEntries();
6484
+ if (entries.length === 0) {
6485
+ return { success: true, purged: [], count: 0 };
6486
+ }
6487
+
6488
+ const all = params.all === true;
6489
+ const trashIds = Array.isArray(params.trashIds)
6490
+ ? params.trashIds
6491
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
6492
+ .filter(Boolean)
6493
+ : [];
6494
+ const singleTrashId = typeof params.trashId === 'string' ? params.trashId.trim() : '';
6495
+ const targetIds = all
6496
+ ? new Set(entries.map((item) => item.trashId))
6497
+ : new Set(singleTrashId ? [singleTrashId, ...trashIds] : trashIds);
6498
+
6499
+ if (targetIds.size === 0) {
6500
+ return { error: '请先选择要彻底删除的回收站记录' };
6501
+ }
6502
+
6503
+ const purged = [];
6504
+ const remaining = [];
6505
+ let purgeError = null;
6506
+ for (let index = 0; index < entries.length; index += 1) {
6507
+ const entry = entries[index];
6508
+ if (!targetIds.has(entry.trashId)) {
6509
+ remaining.push(entry);
6510
+ continue;
5177
6511
  }
6512
+ const trashFilePath = resolveSessionTrashFilePath(entry);
6513
+ if (trashFilePath && fs.existsSync(trashFilePath)) {
6514
+ try {
6515
+ fs.unlinkSync(trashFilePath);
6516
+ } catch (e) {
6517
+ if (!purgeError) purgeError = e;
6518
+ remaining.push(entry);
6519
+ continue;
6520
+ }
6521
+ }
6522
+ purged.push({
6523
+ trashId: entry.trashId,
6524
+ source: entry.source,
6525
+ sessionId: entry.sessionId
6526
+ });
6527
+ }
6528
+
6529
+ try {
6530
+ writeSessionTrashEntries(remaining);
6531
+ } catch (e) {
6532
+ return { error: `回收站索引更新失败: ${e.message}` };
6533
+ }
6534
+
6535
+ if (purgeError) {
6536
+ return { error: `彻底删除失败: ${purgeError.message}` };
5178
6537
  }
5179
6538
 
5180
- const status = getBuiltinProxyStatus();
5181
6539
  return {
5182
6540
  success: true,
5183
- provider: applyRes.provider,
5184
- baseUrl: applyRes.baseUrl,
5185
- switched: applyRes.switched,
5186
- model: applyRes.model || '',
5187
- settings: status.settings,
5188
- runtime: status.runtime
6541
+ purged,
6542
+ count: purged.length
5189
6543
  };
5190
6544
  }
5191
6545
 
5192
- function updateClaudeSessionIndex(indexPath, sessionFilePath, sessionId) {
5193
- if (!indexPath || !fs.existsSync(indexPath)) {
5194
- return;
6546
+ async function trashSessionData(params = {}) {
6547
+ const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
6548
+ if (!source) {
6549
+ return { error: 'Invalid source' };
5195
6550
  }
5196
- const index = readJsonFile(indexPath, null);
5197
- if (!index || !Array.isArray(index.entries)) {
5198
- return;
6551
+
6552
+ const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
6553
+ if (!filePath) {
6554
+ return { error: 'Session file not found' };
5199
6555
  }
5200
- const resolvedFile = sessionFilePath ? path.resolve(sessionFilePath) : '';
5201
- const resolvedLower = resolvedFile ? resolvedFile.toLowerCase() : '';
5202
- const filtered = index.entries.filter((entry) => {
5203
- if (!entry || typeof entry !== 'object') {
5204
- return false;
6556
+
6557
+ const summary = (source === 'claude' ? parseClaudeSessionSummary(filePath) : parseCodexSessionSummary(filePath))
6558
+ || buildSessionSummaryFallback(source, filePath, params.sessionId);
6559
+ const exactMessageCount = await countConversationMessagesInFile(filePath, source);
6560
+ if (Number.isFinite(Number(exactMessageCount))) {
6561
+ summary.messageCount = Math.max(0, Math.floor(Number(exactMessageCount)));
6562
+ }
6563
+ const sessionId = summary.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
6564
+ const { trashId, trashFileName, trashFilePath } = allocateSessionTrashTarget();
6565
+ const deletedAt = new Date().toISOString();
6566
+ const claudeIndexPath = source === 'claude' ? findClaudeSessionIndexPath(filePath) : '';
6567
+ let removedClaudeIndexEntry = null;
6568
+
6569
+ try {
6570
+ moveFileSync(filePath, trashFilePath);
6571
+ } catch (e) {
6572
+ return { error: `移入回收站失败: ${e.message}` };
6573
+ }
6574
+
6575
+ try {
6576
+ if (source === 'claude' && claudeIndexPath) {
6577
+ const removal = removeClaudeSessionIndexEntry(claudeIndexPath, filePath, sessionId);
6578
+ removedClaudeIndexEntry = removal && removal.entry ? removal.entry : null;
6579
+ }
6580
+ const entry = buildSessionTrashEntry(summary, {
6581
+ trashId,
6582
+ trashFileName,
6583
+ trashFilePath,
6584
+ source,
6585
+ sessionId,
6586
+ deletedAt,
6587
+ originalFilePath: filePath,
6588
+ claudeIndexPath,
6589
+ claudeIndexEntry: removedClaudeIndexEntry
6590
+ });
6591
+ const entries = readSessionTrashEntries({ cleanup: false });
6592
+ const totalCount = entries.length + 1;
6593
+ const nextEntries = [entry, ...entries].slice(0, MAX_SESSION_TRASH_LIST_SIZE);
6594
+ writeSessionTrashEntries(nextEntries);
6595
+ summary.totalCount = Math.min(totalCount, MAX_SESSION_TRASH_LIST_SIZE);
6596
+ } catch (e) {
6597
+ let rollbackSucceeded = false;
6598
+ if (fs.existsSync(trashFilePath) && !fs.existsSync(filePath)) {
6599
+ try {
6600
+ moveFileSync(trashFilePath, filePath);
6601
+ rollbackSucceeded = true;
6602
+ } catch (_) {}
5205
6603
  }
5206
- const entrySessionId = typeof entry.sessionId === 'string' ? entry.sessionId : '';
5207
- if (sessionId && entrySessionId === sessionId) {
5208
- return false;
6604
+ if (rollbackSucceeded && source === 'claude' && claudeIndexPath && removedClaudeIndexEntry) {
6605
+ try {
6606
+ upsertClaudeSessionIndexEntry(claudeIndexPath, filePath, {
6607
+ source,
6608
+ sessionId,
6609
+ title: summary.title,
6610
+ messageCount: summary.messageCount,
6611
+ capabilities: summary.capabilities,
6612
+ keywords: summary.keywords,
6613
+ updatedAt: summary.updatedAt,
6614
+ createdAt: summary.createdAt,
6615
+ claudeIndexEntry: removedClaudeIndexEntry,
6616
+ originalFilePath: filePath,
6617
+ trashId,
6618
+ trashFileName
6619
+ });
6620
+ } catch (_) {}
5209
6621
  }
5210
- if (entry.fullPath) {
5211
- const expanded = expandHomePath(entry.fullPath);
5212
- const entryPath = expanded ? path.resolve(expanded) : '';
5213
- if (entryPath && resolvedLower && entryPath.toLowerCase() === resolvedLower) {
5214
- return false;
5215
- }
6622
+ if (!rollbackSucceeded && fs.existsSync(trashFilePath)) {
6623
+ try { fs.unlinkSync(trashFilePath); } catch (_) {}
5216
6624
  }
5217
- return true;
5218
- });
5219
- if (filtered.length === index.entries.length) {
5220
- return;
6625
+ return { error: `移入回收站失败: ${e.message}` };
5221
6626
  }
5222
- index.entries = filtered;
5223
- try {
5224
- fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
5225
- } catch (e) {}
6627
+
6628
+ invalidateSessionListCache();
6629
+
6630
+ return {
6631
+ success: true,
6632
+ source,
6633
+ sessionId,
6634
+ filePath,
6635
+ trashed: true,
6636
+ trashId,
6637
+ deletedAt,
6638
+ totalCount: Number.isFinite(Number(summary && summary.totalCount))
6639
+ ? Math.max(0, Math.floor(Number(summary.totalCount)))
6640
+ : undefined,
6641
+ messageCount: Number.isFinite(Number(summary && summary.messageCount))
6642
+ ? Math.max(0, Math.floor(Number(summary.messageCount)))
6643
+ : 0
6644
+ };
5226
6645
  }
5227
6646
 
5228
6647
  async function deleteSessionData(params = {}) {
@@ -5231,14 +6650,16 @@ async function deleteSessionData(params = {}) {
5231
6650
  return { error: 'Invalid source' };
5232
6651
  }
5233
6652
 
5234
- const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
6653
+ const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
5235
6654
  if (!filePath) {
5236
6655
  return { error: 'Session file not found' };
5237
6656
  }
5238
6657
 
5239
6658
  const sessionId = params.sessionId || path.basename(filePath, '.jsonl');
6659
+ let fileDeleted = false;
5240
6660
  try {
5241
6661
  fs.unlinkSync(filePath);
6662
+ fileDeleted = true;
5242
6663
  } catch (e) {
5243
6664
  return { error: `删除会话失败: ${e.message}` };
5244
6665
  }
@@ -5246,7 +6667,14 @@ async function deleteSessionData(params = {}) {
5246
6667
  if (source === 'claude') {
5247
6668
  const indexPath = findClaudeSessionIndexPath(filePath);
5248
6669
  if (indexPath) {
5249
- updateClaudeSessionIndex(indexPath, filePath, sessionId);
6670
+ try {
6671
+ removeClaudeSessionIndexEntry(indexPath, filePath, sessionId);
6672
+ } catch (e) {
6673
+ console.warn('删除会话索引失败:', e && e.message ? e.message : e);
6674
+ if (!fileDeleted) {
6675
+ return { error: `删除会话失败: ${e.message || e}` };
6676
+ }
6677
+ }
5250
6678
  }
5251
6679
  }
5252
6680
 
@@ -5256,7 +6684,8 @@ async function deleteSessionData(params = {}) {
5256
6684
  success: true,
5257
6685
  source,
5258
6686
  sessionId,
5259
- filePath
6687
+ filePath,
6688
+ deleted: true
5260
6689
  };
5261
6690
  }
5262
6691
 
@@ -5311,7 +6740,7 @@ async function cloneCodexSession(params = {}) {
5311
6740
  return { error: '仅支持 Codex 会话克隆' };
5312
6741
  }
5313
6742
 
5314
- const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
6743
+ const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
5315
6744
  if (!filePath) {
5316
6745
  return { error: 'Session file not found' };
5317
6746
  }
@@ -5690,26 +7119,26 @@ async function readSessionDetail(params = {}) {
5690
7119
  return { error: 'Invalid source' };
5691
7120
  }
5692
7121
 
5693
- const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
7122
+ const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
5694
7123
  if (!filePath) {
5695
7124
  return { error: 'Session file not found' };
5696
7125
  }
5697
7126
 
5698
- const rawLimit = Number(params.messageLimit);
7127
+ const rawMaxMessages = Number(params.maxMessages);
7128
+ const rawLimit = Number.isFinite(rawMaxMessages) ? rawMaxMessages : Number(params.messageLimit);
5699
7129
  const messageLimit = Number.isFinite(rawLimit)
5700
7130
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_DETAIL_MESSAGES))
5701
7131
  : DEFAULT_SESSION_DETAIL_MESSAGES;
5702
7132
 
5703
- const extracted = await extractMessagesFromFile(filePath, source);
7133
+ const extracted = await extractSessionDetailPreviewFromFile(filePath, source, messageLimit);
5704
7134
  const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
5705
7135
  const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
5706
- const allMessages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : [])
5707
- .map((message, messageIndex) => ({
5708
- ...message,
5709
- messageIndex
5710
- }));
5711
- const startIndex = Math.max(0, allMessages.length - messageLimit);
5712
- const clippedMessages = allMessages.slice(startIndex);
7136
+ const clippedMessages = Array.isArray(extracted.messages) ? extracted.messages : [];
7137
+ const startIndex = Math.max(0, extracted.totalMessages - clippedMessages.length);
7138
+ const indexedMessages = clippedMessages.map((message, messageIndex) => ({
7139
+ ...message,
7140
+ messageIndex: startIndex + messageIndex
7141
+ }));
5713
7142
 
5714
7143
  return {
5715
7144
  source,
@@ -5717,10 +7146,10 @@ async function readSessionDetail(params = {}) {
5717
7146
  sessionId,
5718
7147
  cwd: extracted.cwd || '',
5719
7148
  updatedAt: extracted.updatedAt || '',
5720
- totalMessages: allMessages.length,
5721
- clipped: allMessages.length > clippedMessages.length,
7149
+ totalMessages: extracted.totalMessages,
7150
+ clipped: extracted.totalMessages > indexedMessages.length,
5722
7151
  messageLimit,
5723
- messages: clippedMessages,
7152
+ messages: indexedMessages,
5724
7153
  filePath
5725
7154
  };
5726
7155
  }
@@ -5731,7 +7160,7 @@ async function readSessionPlain(params = {}) {
5731
7160
  return { error: 'Invalid source' };
5732
7161
  }
5733
7162
 
5734
- const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
7163
+ const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
5735
7164
  if (!filePath) {
5736
7165
  return { error: 'Session file not found' };
5737
7166
  }
@@ -5777,7 +7206,7 @@ async function exportSessionData(params = {}) {
5777
7206
  }
5778
7207
 
5779
7208
  const maxMessages = resolveMaxMessagesValue(params.maxMessages, MAX_EXPORT_MESSAGES);
5780
- const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
7209
+ const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
5781
7210
  if (!filePath) {
5782
7211
  return { error: 'Session file not found' };
5783
7212
  }
@@ -6163,7 +7592,8 @@ async function cmdSetup() {
6163
7592
  const { config } = readConfigOrVirtualDefault();
6164
7593
  const providers = config.model_providers || {};
6165
7594
  const providerNames = Object.keys(providers);
6166
- const defaultProvider = config.model_provider || providerNames[0] || '';
7595
+ const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
7596
+ const defaultProvider = currentProvider || providerNames[0] || '';
6167
7597
  let availableModels = [];
6168
7598
  let defaultModel = config.model || '';
6169
7599
  let modelFetchUnlimited = false;
@@ -6449,7 +7879,7 @@ async function cmdModels() {
6449
7879
 
6450
7880
  // 切换提供商
6451
7881
  function cmdSwitch(providerName, silent = false) {
6452
- const config = readConfig();
7882
+ const config = sanitizeRemovedBuiltinProxyProvider(readConfig());
6453
7883
  const providers = config.model_providers || {};
6454
7884
 
6455
7885
  if (!providers[providerName]) {
@@ -6552,6 +7982,10 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) {
6552
7982
  if (!silent) console.error('错误: local provider 为系统保留名称,不可新增');
6553
7983
  throw new Error('local provider 为系统保留名称,不可新增');
6554
7984
  }
7985
+ if (isBuiltinProxyProvider(providerName)) {
7986
+ if (!silent) console.error('错误: codexmate-proxy 为保留名称,不可手动添加');
7987
+ throw new Error('codexmate-proxy 为保留名称,不可手动添加');
7988
+ }
6555
7989
 
6556
7990
  const config = readConfig();
6557
7991
  if (config.model_providers && config.model_providers[providerName]) {
@@ -6616,7 +8050,7 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
6616
8050
  if (isNonEditableProvider(name) && !allowManaged) {
6617
8051
  const msg = isDefaultLocalProvider(name)
6618
8052
  ? 'local provider 为系统保留项,不可编辑'
6619
- : '本地代理配置为系统内建项,不可编辑';
8053
+ : 'codexmate-proxy 为保留名称,不可编辑';
6620
8054
  if (!silent) console.error(`错误: ${msg}`);
6621
8055
  throw new Error(msg);
6622
8056
  }
@@ -8448,22 +9882,55 @@ function resolveUploadFileNameFromRequest(req, fallbackName = 'codex-skills.zip'
8448
9882
  return normalized || fallback;
8449
9883
  }
8450
9884
 
8451
- async function handleImportCodexSkillsZipUpload(req, res) {
9885
+ function resolveSkillTargetAppFromRequest(req, fallbackApp = 'codex') {
9886
+ const fallbackTarget = resolveSkillTarget({}, fallbackApp);
9887
+ const fallback = fallbackTarget ? fallbackTarget.app : 'codex';
9888
+ try {
9889
+ const parsed = new URL(req.url || '/', 'http://localhost');
9890
+ const hasTargetApp = parsed.searchParams.has('targetApp');
9891
+ const hasTarget = parsed.searchParams.has('target');
9892
+ if (hasTargetApp || hasTarget) {
9893
+ const target = resolveSkillTarget({
9894
+ ...(hasTargetApp ? { targetApp: parsed.searchParams.get('targetApp') } : {}),
9895
+ ...(hasTarget ? { target: parsed.searchParams.get('target') } : {})
9896
+ }, fallback);
9897
+ return target ? target.app : null;
9898
+ }
9899
+ return fallback;
9900
+ } catch (_) {
9901
+ return fallback;
9902
+ }
9903
+ }
9904
+
9905
+ async function handleImportSkillsZipUpload(req, res, options = {}) {
8452
9906
  if (req.method !== 'POST') {
9907
+ if (req && typeof req.resume === 'function') {
9908
+ req.resume();
9909
+ }
8453
9910
  writeJsonResponse(res, 405, { error: 'Method Not Allowed' });
8454
9911
  return;
8455
9912
  }
8456
9913
  try {
8457
- const fileName = resolveUploadFileNameFromRequest(req, 'codex-skills.zip');
9914
+ const forcedTargetApp = normalizeSkillTargetApp(options && options.targetApp ? options.targetApp : '');
9915
+ const targetApp = forcedTargetApp || resolveSkillTargetAppFromRequest(req, 'codex');
9916
+ if (!targetApp) {
9917
+ if (req && typeof req.resume === 'function') {
9918
+ req.resume();
9919
+ }
9920
+ writeJsonResponse(res, 400, { error: '目标宿主不支持' });
9921
+ return;
9922
+ }
9923
+ const fileName = resolveUploadFileNameFromRequest(req, `${targetApp}-skills.zip`);
8458
9924
  const upload = await writeUploadZipStream(
8459
9925
  req,
8460
9926
  'codex-skills-import',
8461
9927
  fileName,
8462
9928
  MAX_SKILLS_ZIP_UPLOAD_SIZE
8463
9929
  );
8464
- const result = await importCodexSkillsFromZipFile(upload.zipPath, {
9930
+ const result = await importSkillsFromZipFile(upload.zipPath, {
8465
9931
  tempDir: upload.tempDir,
8466
- fallbackName: fileName
9932
+ fallbackName: fileName,
9933
+ targetApp
8467
9934
  });
8468
9935
  writeJsonResponse(res, 200, result || {});
8469
9936
  } catch (e) {
@@ -8477,14 +9944,33 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8477
9944
 
8478
9945
  const server = http.createServer((req, res) => {
8479
9946
  const requestPath = (req.url || '/').split('?')[0];
9947
+ if (requestPath === '/api/import-skills-zip') {
9948
+ void handleImportSkillsZipUpload(req, res);
9949
+ return;
9950
+ }
8480
9951
  if (requestPath === '/api/import-codex-skills-zip') {
8481
- void handleImportCodexSkillsZipUpload(req, res);
9952
+ void handleImportSkillsZipUpload(req, res, { targetApp: 'codex' });
8482
9953
  return;
8483
9954
  }
8484
9955
  if (requestPath === '/api') {
8485
9956
  let body = '';
8486
- req.on('data', chunk => body += chunk);
9957
+ let bodySize = 0;
9958
+ let bodyTooLarge = false;
9959
+ req.on('data', chunk => {
9960
+ if (bodyTooLarge) return;
9961
+ bodySize += chunk.length;
9962
+ if (bodySize > MAX_API_BODY_SIZE) {
9963
+ bodyTooLarge = true;
9964
+ writeJsonResponse(res, 413, {
9965
+ error: `请求体过大(>${Math.floor(MAX_API_BODY_SIZE / 1024 / 1024)}MB)`
9966
+ });
9967
+ req.destroy();
9968
+ return;
9969
+ }
9970
+ body += chunk;
9971
+ });
8487
9972
  req.on('end', async () => {
9973
+ if (bodyTooLarge) return;
8488
9974
  try {
8489
9975
  const { action, params } = JSON.parse(body);
8490
9976
  let result;
@@ -8592,6 +10078,24 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8592
10078
  case 'apply-agents-file':
8593
10079
  result = applyAgentsFile(params || {});
8594
10080
  break;
10081
+ case 'preview-agents-diff':
10082
+ result = buildAgentsDiff(params || {});
10083
+ break;
10084
+ case 'list-skills':
10085
+ result = listSkills(params || {});
10086
+ break;
10087
+ case 'delete-skills':
10088
+ result = deleteSkills(params || {});
10089
+ break;
10090
+ case 'scan-unmanaged-skills':
10091
+ result = scanUnmanagedSkills(params || {});
10092
+ break;
10093
+ case 'import-skills':
10094
+ result = importSkills(params || {});
10095
+ break;
10096
+ case 'export-skills':
10097
+ result = await exportSkills(params || {});
10098
+ break;
8595
10099
  case 'list-codex-skills':
8596
10100
  result = listCodexSkills();
8597
10101
  break;
@@ -8679,7 +10183,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8679
10183
  result = { error: 'Invalid source. Must be codex, claude, or all' };
8680
10184
  } else {
8681
10185
  result = {
8682
- sessions: listAllSessions(params),
10186
+ sessions: await listAllSessionsData(params),
8683
10187
  source: source || 'all'
8684
10188
  };
8685
10189
  }
@@ -8697,6 +10201,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8697
10201
  }
8698
10202
  }
8699
10203
  break;
10204
+ case 'list-session-trash':
10205
+ result = await listSessionTrashItems(params || {});
10206
+ break;
10207
+ case 'restore-session-trash':
10208
+ result = await restoreSessionTrashItem(params || {});
10209
+ break;
10210
+ case 'purge-session-trash':
10211
+ result = await purgeSessionTrashItems(params || {});
10212
+ break;
10213
+ case 'trash-session':
10214
+ result = await trashSessionData(params || {});
10215
+ break;
8700
10216
  case 'export-session':
8701
10217
  result = await exportSessionData(params);
8702
10218
  break;
@@ -8939,7 +10455,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8939
10455
  process.exit(1);
8940
10456
  });
8941
10457
 
8942
- const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
10458
+ const openHost = host === '::'
10459
+ ? '::1'
10460
+ : (host === '0.0.0.0' ? DEFAULT_WEB_OPEN_HOST : host);
8943
10461
  const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
8944
10462
  server.listen(port, host, () => {
8945
10463
  console.log('\n✓ Web UI 已启动:', openUrl);
@@ -8995,6 +10513,69 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8995
10513
  return { server, stop };
8996
10514
  }
8997
10515
 
10516
+ // Region markers are used by unit tests that extract these helpers directly.
10517
+ // #region createSerializedWebUiRestartHandler
10518
+ function createSerializedWebUiRestartHandler(runRestart) {
10519
+ let restartQueued = false;
10520
+ let latestRestartInfo = null;
10521
+ let restartInFlight = null;
10522
+
10523
+ const drainRestartQueue = async () => {
10524
+ try {
10525
+ while (restartQueued) {
10526
+ restartQueued = false;
10527
+ await runRestart(latestRestartInfo);
10528
+ }
10529
+ } finally {
10530
+ restartInFlight = null;
10531
+ if (restartQueued) {
10532
+ restartInFlight = drainRestartQueue();
10533
+ return restartInFlight;
10534
+ }
10535
+ }
10536
+ };
10537
+
10538
+ return (info) => {
10539
+ latestRestartInfo = info;
10540
+ restartQueued = true;
10541
+ if (!restartInFlight) {
10542
+ restartInFlight = drainRestartQueue();
10543
+ }
10544
+ return restartInFlight;
10545
+ };
10546
+ }
10547
+ // #endregion createSerializedWebUiRestartHandler
10548
+
10549
+ // #region restartWebUiServerAfterFrontendChange
10550
+ async function restartWebUiServerAfterFrontendChange({
10551
+ serverHandle,
10552
+ serverOptions,
10553
+ createServer = createWebServer,
10554
+ delayMs = 3000,
10555
+ wait = setTimeout,
10556
+ logger = console
10557
+ }) {
10558
+ logger.log(' 正在停止旧服务...');
10559
+ try {
10560
+ await serverHandle.stop();
10561
+ logger.log(' 旧服务已停止');
10562
+ } catch (e) {
10563
+ logger.warn('! 停止旧服务失败:', e.message || e);
10564
+ }
10565
+
10566
+ await new Promise((resolve) => wait(resolve, delayMs));
10567
+
10568
+ try {
10569
+ const nextServerHandle = await createServer(serverOptions);
10570
+ logger.log('✓ 已重启 Web UI 服务\n');
10571
+ return nextServerHandle;
10572
+ } catch (e) {
10573
+ logger.error('! 重启失败:', e.message || e);
10574
+ return serverHandle;
10575
+ }
10576
+ }
10577
+ // #endregion restartWebUiServerAfterFrontendChange
10578
+
8998
10579
  // 打开 Web UI
8999
10580
  function cmdStart(options = {}) {
9000
10581
  const webDir = path.join(__dirname, 'web-ui');
@@ -9019,51 +10600,28 @@ function cmdStart(options = {}) {
9019
10600
  openBrowser: !options.noBrowser
9020
10601
  });
9021
10602
 
9022
- const proxySettings = readBuiltinProxySettings();
9023
- const shouldAutoStartProxy = proxySettings.enabled || hasCodexConfigReadyForProxy();
9024
- if (shouldAutoStartProxy) {
9025
- ensureBuiltinProxyForCodexDefault({
9026
- ...proxySettings,
9027
- switchToProxy: false
9028
- }).then((res) => {
9029
- if (res && res.success && res.runtime && res.runtime.listenUrl) {
9030
- const entryProvider = res.runtime.provider || DEFAULT_LOCAL_PROVIDER_NAME;
9031
- const upstreamLabel = res.runtime.upstreamProvider ? `(上游: ${res.runtime.upstreamProvider})` : '';
9032
- console.log(`~ 内建代理已启动(${entryProvider}): ${res.runtime.listenUrl}${upstreamLabel}`);
9033
- } else if (res && res.error) {
9034
- console.warn(`! 内建代理启动失败: ${res.error}`);
9035
- }
9036
- }).catch((err) => {
9037
- console.warn(`! 内建代理启动失败: ${err && err.message ? err.message : err}`);
9038
- });
9039
- }
9040
-
9041
- const stopWatch = watchPathsForRestart(
9042
- [webDir, legacyHtmlPath],
9043
- async (info) => {
10603
+ const requestWebUiRestart = createSerializedWebUiRestartHandler(async (info) => {
9044
10604
  const fileLabel = info && info.filename ? info.filename : (info && info.target ? path.basename(info.target) : 'unknown');
9045
10605
  console.log(`\n~ 侦测到前端变更 (${fileLabel}),重启中...`);
9046
- console.log(' 正在停止旧服务...');
9047
- try {
9048
- await serverHandle.stop();
9049
- console.log(' 旧服务已停止');
9050
- } catch (e) {
9051
- console.warn('! 停止旧服务失败:', e.message || e);
9052
- }
9053
- await new Promise((resolve) => setTimeout(resolve, 80));
9054
- try {
9055
- serverHandle = createWebServer({
10606
+ serverHandle = await restartWebUiServerAfterFrontendChange({
10607
+ serverHandle,
10608
+ serverOptions: {
9056
10609
  htmlPath,
9057
10610
  assetsDir,
9058
10611
  webDir,
9059
10612
  host,
9060
10613
  port,
9061
10614
  openBrowser: false
9062
- });
9063
- console.log('✓ 已重启 Web UI 服务\n');
9064
- } catch (e) {
9065
- console.error('! 重启失败:', e.message || e);
9066
- }
10615
+ }
10616
+ });
10617
+ });
10618
+
10619
+ const stopWatch = watchPathsForRestart(
10620
+ [webDir, legacyHtmlPath],
10621
+ (info) => {
10622
+ void requestWebUiRestart(info).catch((err) => {
10623
+ console.error('! 重启 Web UI 失败:', err && err.message ? err.message : err);
10624
+ });
9067
10625
  }
9068
10626
  );
9069
10627
 
@@ -9233,111 +10791,8 @@ function parseProxyCliOptions(args = []) {
9233
10791
  }
9234
10792
 
9235
10793
  async function cmdProxy(args = []) {
9236
- const subcommand = (args[0] || 'status').toLowerCase();
9237
- const optionResult = parseProxyCliOptions(args.slice(1));
9238
- if (optionResult.error) {
9239
- throw new Error(optionResult.error);
9240
- }
9241
- const options = optionResult.payload || {};
9242
-
9243
- if (subcommand === 'status') {
9244
- const status = getBuiltinProxyStatus();
9245
- const settings = status.settings || DEFAULT_BUILTIN_PROXY_SETTINGS;
9246
- console.log('\n内建代理状态:');
9247
- console.log(' 运行中:', status.running ? '是' : '否');
9248
- console.log(' 启用:', settings.enabled ? '是' : '否');
9249
- console.log(' 监听:', buildProxyListenUrl(settings));
9250
- console.log(' 上游 provider:', settings.provider || '(自动)');
9251
- console.log(' 鉴权来源:', settings.authSource);
9252
- if (status.runtime) {
9253
- console.log(' 实际上游:', status.runtime.upstreamProvider);
9254
- console.log(' 启动时间:', status.runtime.startedAt);
9255
- }
9256
- console.log();
9257
- return;
9258
- }
9259
-
9260
- if (subcommand === 'set' || subcommand === 'config') {
9261
- const result = saveBuiltinProxySettings(options);
9262
- if (result.error) {
9263
- throw new Error(result.error);
9264
- }
9265
- const settings = result.settings;
9266
- console.log('✓ 内建代理配置已保存');
9267
- console.log(' 监听:', buildProxyListenUrl(settings));
9268
- console.log(' 上游 provider:', settings.provider || '(自动)');
9269
- console.log(' 鉴权来源:', settings.authSource);
9270
- console.log();
9271
- return;
9272
- }
9273
-
9274
- if (subcommand === 'apply' || subcommand === 'apply-provider') {
9275
- const result = applyBuiltinProxyProvider({
9276
- switchToProxy: options.switchToProxy !== false
9277
- });
9278
- if (result.error) {
9279
- throw new Error(result.error);
9280
- }
9281
- console.log(`✓ 已写入本地代理 provider: ${result.provider}`);
9282
- console.log(` URL: ${result.baseUrl}`);
9283
- if (result.switched) {
9284
- console.log(` 已切换到 ${result.provider}${result.model ? ` / ${result.model}` : ''}`);
9285
- }
9286
- console.log();
9287
- return;
9288
- }
9289
-
9290
- if (subcommand === 'enable' || subcommand === 'default-codex') {
9291
- const result = await ensureBuiltinProxyForCodexDefault(options);
9292
- if (result.error) {
9293
- throw new Error(result.error);
9294
- }
9295
- const listenUrl = result.runtime && result.runtime.listenUrl
9296
- ? result.runtime.listenUrl
9297
- : buildProxyListenUrl(result.settings || DEFAULT_BUILTIN_PROXY_SETTINGS);
9298
- console.log('✓ 已启用 Codex 内建代理默认模式');
9299
- console.log(` 监听: ${listenUrl}`);
9300
- if (result.runtime && result.runtime.upstreamProvider) {
9301
- console.log(` 上游 provider: ${result.runtime.upstreamProvider}`);
9302
- }
9303
- console.log(` 当前 provider: ${result.provider}${result.model ? ` / ${result.model}` : ''}`);
9304
- console.log();
9305
- return;
9306
- }
9307
-
9308
- if (subcommand === 'start') {
9309
- const result = await startBuiltinProxyRuntime({
9310
- ...options,
9311
- enabled: true
9312
- });
9313
- if (result.error) {
9314
- throw new Error(result.error);
9315
- }
9316
- console.log(`✓ 内建代理已启动: ${result.listenUrl}`);
9317
- console.log(` 上游 provider: ${result.upstreamProvider}`);
9318
- console.log(' 按 Ctrl+C 停止代理\n');
9319
-
9320
- await new Promise((resolve) => {
9321
- let stopping = false;
9322
- const gracefulStop = async () => {
9323
- if (stopping) return;
9324
- stopping = true;
9325
- await stopBuiltinProxyRuntime();
9326
- resolve();
9327
- };
9328
- process.once('SIGINT', gracefulStop);
9329
- process.once('SIGTERM', gracefulStop);
9330
- });
9331
- return;
9332
- }
9333
-
9334
- if (subcommand === 'stop') {
9335
- await stopBuiltinProxyRuntime();
9336
- console.log('✓ 内建代理已停止\n');
9337
- return;
9338
- }
9339
-
9340
- throw new Error(`未知 proxy 子命令: ${subcommand}`);
10794
+ void args;
10795
+ throw new Error('该功能已移除');
9341
10796
  }
9342
10797
 
9343
10798
  function parseWorkflowInputArg(rawInput) {
@@ -10389,7 +11844,7 @@ function createWorkflowToolCatalog() {
10389
11844
  }
10390
11845
  return {
10391
11846
  source: source || 'all',
10392
- sessions: listAllSessions({
11847
+ sessions: await listAllSessionsData({
10393
11848
  ...args,
10394
11849
  source: source || 'all'
10395
11850
  })
@@ -10756,7 +12211,7 @@ function createMcpTools(options = {}) {
10756
12211
  source: source || 'all'
10757
12212
  };
10758
12213
  return {
10759
- sessions: listAllSessions(normalizedInput),
12214
+ sessions: await listAllSessionsData(normalizedInput),
10760
12215
  source: source || 'all'
10761
12216
  };
10762
12217
  }
@@ -10992,18 +12447,34 @@ function createMcpTools(options = {}) {
10992
12447
  handler: async (args = {}) => applyOpenclawConfig(args || {})
10993
12448
  });
10994
12449
 
12450
+ pushTool({
12451
+ name: 'codexmate.session.trash',
12452
+ description: 'Move one entire session file into session trash.',
12453
+ readOnly: false,
12454
+ inputSchema: {
12455
+ type: 'object',
12456
+ properties: {
12457
+ source: { type: 'string' },
12458
+ sessionId: { type: 'string' },
12459
+ filePath: { type: 'string' },
12460
+ file: { type: 'string' }
12461
+ },
12462
+ additionalProperties: true
12463
+ },
12464
+ handler: async (args = {}) => trashSessionData(args || {})
12465
+ });
12466
+
10995
12467
  pushTool({
10996
12468
  name: 'codexmate.session.delete',
10997
- description: 'Delete one session or selected records in a session.',
12469
+ description: 'Permanently delete one entire session file.',
10998
12470
  readOnly: false,
10999
12471
  inputSchema: {
11000
12472
  type: 'object',
11001
12473
  properties: {
11002
12474
  source: { type: 'string' },
11003
12475
  sessionId: { type: 'string' },
11004
- file: { type: 'string' },
11005
- recordLineIndex: { type: 'number' },
11006
- recordLineIndices: { type: 'array', items: { type: 'number' } }
12476
+ filePath: { type: 'string' },
12477
+ file: { type: 'string' }
11007
12478
  },
11008
12479
  additionalProperties: true
11009
12480
  },
@@ -11153,7 +12624,7 @@ function createMcpResources() {
11153
12624
  }
11154
12625
  const payload = {
11155
12626
  source: normalizedSource || 'all',
11156
- sessions: listAllSessions({
12627
+ sessions: await listAllSessionsData({
11157
12628
  source: normalizedSource || 'all',
11158
12629
  query,
11159
12630
  pathFilter,
@@ -11395,11 +12866,9 @@ async function main() {
11395
12866
  console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
11396
12867
  console.log(' codexmate add-model <模型> 添加模型');
11397
12868
  console.log(' codexmate delete-model <模型> 删除模型');
11398
- console.log(' codexmate auth <list|import|switch|delete|status> 认证文件管理');
11399
- console.log(' codexmate proxy <status|set|apply|enable|start|stop> 内建代理');
11400
12869
  console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
11401
12870
  console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
11402
- console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo(不会自动启用内建代理)');
12871
+ console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo');
11403
12872
  console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
11404
12873
  console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
11405
12874
  console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');