codexmate 0.0.7 → 0.0.9

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.
Files changed (47) hide show
  1. package/.github/workflows/release.yml +122 -8
  2. package/.planning/.fix-attempts +1 -0
  3. package/.planning/.lock +6 -0
  4. package/.planning/.verify-cache.json +14 -0
  5. package/.planning/CHECKPOINT.json +46 -0
  6. package/.planning/DESIGN.md +26 -0
  7. package/.planning/HISTORY.json +124 -0
  8. package/.planning/PLAN.md +69 -0
  9. package/.planning/REVIEW.md +41 -0
  10. package/.planning/STATE.md +12 -0
  11. package/.planning/STATS.json +13 -0
  12. package/.planning/VERIFICATION.md +70 -0
  13. package/.planning/daude-code-plan.md +51 -0
  14. package/.planning/research/architecture.md +32 -0
  15. package/.planning/research/conventions.md +36 -0
  16. package/.planning/task_1-REVIEW.md +29 -0
  17. package/.planning/task_1-SUMMARY.md +32 -0
  18. package/.planning/task_2-REVIEW.md +24 -0
  19. package/.planning/task_2-SUMMARY.md +37 -0
  20. package/.planning/task_3-REVIEW.md +25 -0
  21. package/.planning/task_3-SUMMARY.md +31 -0
  22. package/README.md +58 -52
  23. package/README.zh-CN.md +68 -56
  24. package/cli.js +1142 -1427
  25. package/lib/cli-file-utils.js +151 -0
  26. package/lib/cli-models-utils.js +152 -0
  27. package/lib/cli-network-utils.js +148 -0
  28. package/lib/cli-session-utils.js +121 -0
  29. package/lib/cli-utils.js +139 -0
  30. package/package.json +4 -2
  31. package/res/json5.min.js +1 -0
  32. package/res/vue.global.js +18552 -0
  33. package/tests/e2e/helpers.js +214 -0
  34. package/tests/e2e/recent-health.e2e.js +6 -0
  35. package/tests/e2e/run.js +103 -306
  36. package/tests/e2e/test-claude.js +21 -0
  37. package/tests/e2e/test-config.js +124 -0
  38. package/tests/e2e/test-health-speed.js +79 -0
  39. package/tests/e2e/test-openclaw.js +47 -0
  40. package/tests/e2e/test-session-search.js +114 -0
  41. package/tests/e2e/test-sessions.js +69 -0
  42. package/tests/e2e/test-setup.js +159 -0
  43. package/tests/unit/run.mjs +29 -0
  44. package/tests/unit/web-ui-logic.test.mjs +186 -0
  45. package/web-ui/app.js +2841 -0
  46. package/web-ui/logic.mjs +157 -0
  47. package/web-ui.html +1045 -2996
package/cli.js CHANGED
@@ -6,13 +6,55 @@ const crypto = require('crypto');
6
6
  const toml = require('@iarna/toml');
7
7
  const JSON5 = require('json5');
8
8
  const zipLib = require('zip-lib');
9
- const { exec, execSync, spawn } = require('child_process');
9
+ const { exec, execSync, spawn } = require('child_process');
10
10
  const http = require('http');
11
11
  const https = require('https');
12
12
  const readline = require('readline');
13
-
14
- const DEFAULT_WEB_PORT = 3737;
15
- const DEFAULT_WEB_HOST = '127.0.0.1';
13
+ const {
14
+ expandHomePath,
15
+ resolveExistingDir,
16
+ resolveHomePath,
17
+ hasUtf8Bom,
18
+ stripUtf8Bom,
19
+ ensureUtf8Bom,
20
+ detectLineEnding,
21
+ normalizeLineEnding,
22
+ isValidProviderName,
23
+ buildModelsCandidates,
24
+ isValidHttpUrl,
25
+ normalizeBaseUrl,
26
+ joinApiUrl
27
+ } = require('./lib/cli-utils');
28
+ const {
29
+ ensureDir,
30
+ readJsonFile,
31
+ readJsonArrayFile,
32
+ readJsonObjectFromFile,
33
+ backupFileIfNeededOnce,
34
+ writeJsonAtomic,
35
+ formatTimestampForFileName
36
+ } = require('./lib/cli-file-utils');
37
+ const {
38
+ extractModelNames,
39
+ hasModelsListPayload,
40
+ extractModelIds,
41
+ buildModelsProbeUrl,
42
+ buildModelProbeSpec,
43
+ buildModelsCacheKey
44
+ } = require('./lib/cli-models-utils');
45
+ const { probeUrl, probeJsonPost } = require('./lib/cli-network-utils');
46
+ const {
47
+ toIsoTime,
48
+ updateLatestIso,
49
+ truncateText,
50
+ extractMessageText,
51
+ normalizeRole,
52
+ parseMaxMessagesValue,
53
+ resolveMaxMessagesValue
54
+ } = require('./lib/cli-session-utils');
55
+
56
+ const DEFAULT_WEB_PORT = 3737;
57
+ const DEFAULT_WEB_HOST = '127.0.0.1';
16
58
 
17
59
  // ============================================================================
18
60
  // 配置
@@ -31,6 +73,7 @@ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
31
73
  const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
32
74
  const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
33
75
  const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
76
+ const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
34
77
 
35
78
  const DEFAULT_MODELS = ['gpt-5.3-codex', 'gpt-5.1-codex-max', 'gpt-4-turbo', 'gpt-4'];
36
79
  const SPEED_TEST_TIMEOUT_MS = 8000;
@@ -42,14 +85,13 @@ const MAX_SESSION_DETAIL_MESSAGES = 1000;
42
85
  const SESSION_TITLE_READ_BYTES = 64 * 1024;
43
86
  const CODEXMATE_MANAGED_MARKER = '# codexmate-managed: true';
44
87
  const SESSION_LIST_CACHE_TTL_MS = 4000;
45
- const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
46
- const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
47
- const DEFAULT_CONTENT_SCAN_LIMIT = 10;
88
+ const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
89
+ const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
90
+ const DEFAULT_CONTENT_SCAN_LIMIT = 50;
48
91
  const SESSION_SCAN_FACTOR = 4;
49
92
  const SESSION_SCAN_MIN_FILES = 800;
50
93
  const MAX_SESSION_PATH_LIST_SIZE = 2000;
51
94
  const AGENTS_FILE_NAME = 'AGENTS.md';
52
- const UTF8_BOM = '\ufeff';
53
95
  const MODELS_CACHE_TTL_MS = 60 * 1000;
54
96
  const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
55
97
  const MODELS_CACHE_MAX_ENTRIES = 50;
@@ -66,26 +108,26 @@ const BOOTSTRAP_TEXT_MARKERS = [
66
108
  const HTTP_KEEP_ALIVE_AGENT = new http.Agent({ keepAlive: true });
67
109
  const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
68
110
 
69
- function resolveWebPort() {
70
- const raw = process.env.CODEXMATE_PORT;
71
- if (!raw) return DEFAULT_WEB_PORT;
72
- const parsed = parseInt(raw, 10);
73
- if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_WEB_PORT;
74
- return parsed;
75
- }
76
-
77
- function resolveWebHost(options = {}) {
78
- const optionHost = typeof options.host === 'string' ? options.host.trim() : '';
79
- if (optionHost) {
80
- return optionHost;
81
- }
82
- const envHost = typeof process.env.CODEXMATE_HOST === 'string' ? process.env.CODEXMATE_HOST.trim() : '';
83
- if (envHost) {
84
- return envHost;
85
- }
86
- return DEFAULT_WEB_HOST;
87
- }
88
-
111
+ function resolveWebPort() {
112
+ const raw = process.env.CODEXMATE_PORT;
113
+ if (!raw) return DEFAULT_WEB_PORT;
114
+ const parsed = parseInt(raw, 10);
115
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_WEB_PORT;
116
+ return parsed;
117
+ }
118
+
119
+ function resolveWebHost(options = {}) {
120
+ const optionHost = typeof options.host === 'string' ? options.host.trim() : '';
121
+ if (optionHost) {
122
+ return optionHost;
123
+ }
124
+ const envHost = typeof process.env.CODEXMATE_HOST === 'string' ? process.env.CODEXMATE_HOST.trim() : '';
125
+ if (envHost) {
126
+ return envHost;
127
+ }
128
+ return DEFAULT_WEB_HOST;
129
+ }
130
+
89
131
  const EMPTY_CONFIG_FALLBACK_TEMPLATE = `model = "gpt-5.3-codex"
90
132
  model_reasoning_effort = "high"
91
133
  disable_response_storage = true
@@ -178,69 +220,6 @@ function updateAuthJson(apiKey) {
178
220
  fs.writeFileSync(AUTH_FILE, JSON.stringify(authData, null, 2), 'utf-8');
179
221
  }
180
222
 
181
- function readJsonFile(filePath, fallback = null) {
182
- if (!fs.existsSync(filePath)) {
183
- return fallback;
184
- }
185
- try {
186
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
187
- } catch (e) {
188
- return fallback;
189
- }
190
- }
191
-
192
- function readJsonArrayFile(filePath, fallback = []) {
193
- if (!fs.existsSync(filePath)) {
194
- return Array.isArray(fallback) ? [...fallback] : [];
195
- }
196
- try {
197
- const content = fs.readFileSync(filePath, 'utf-8');
198
- if (!content.trim()) {
199
- return Array.isArray(fallback) ? [...fallback] : [];
200
- }
201
- const parsed = JSON.parse(content);
202
- return Array.isArray(parsed) ? parsed : (Array.isArray(fallback) ? [...fallback] : []);
203
- } catch (e) {
204
- return Array.isArray(fallback) ? [...fallback] : [];
205
- }
206
- }
207
-
208
- function ensureDir(dirPath) {
209
- if (!fs.existsSync(dirPath)) {
210
- fs.mkdirSync(dirPath, { recursive: true });
211
- }
212
- }
213
-
214
- function expandHomePath(value) {
215
- if (typeof value !== 'string') {
216
- return '';
217
- }
218
- const trimmed = value.trim();
219
- if (!trimmed) {
220
- return '';
221
- }
222
- if (trimmed === '~') {
223
- return os.homedir();
224
- }
225
- if (trimmed.startsWith(`~${path.sep}`) || trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
226
- return path.resolve(os.homedir(), trimmed.slice(2));
227
- }
228
- return trimmed;
229
- }
230
-
231
- function resolveExistingDir(candidates = [], fallback = '') {
232
- for (const raw of candidates) {
233
- const candidate = expandHomePath(raw);
234
- if (!candidate) continue;
235
- try {
236
- if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
237
- return candidate;
238
- }
239
- } catch (e) {}
240
- }
241
- return fallback;
242
- }
243
-
244
223
  function getCodexSessionsDir() {
245
224
  const candidates = [];
246
225
  const envCodexHome = process.env.CODEX_HOME;
@@ -271,100 +250,6 @@ function getClaudeProjectsDir() {
271
250
  return resolveExistingDir(candidates, CLAUDE_PROJECTS_DIR);
272
251
  }
273
252
 
274
- function hasUtf8Bom(text) {
275
- return typeof text === 'string' && text.charCodeAt(0) === 0xfeff;
276
- }
277
-
278
- function stripUtf8Bom(text) {
279
- if (!text) return '';
280
- return hasUtf8Bom(text) ? text.slice(1) : text;
281
- }
282
-
283
- function ensureUtf8Bom(text) {
284
- const content = typeof text === 'string' ? text : '';
285
- return hasUtf8Bom(content) ? content : UTF8_BOM + content;
286
- }
287
-
288
- function detectLineEnding(text) {
289
- return typeof text === 'string' && text.includes('\r\n') ? '\r\n' : '\n';
290
- }
291
-
292
- function normalizeLineEnding(text, lineEnding) {
293
- const normalized = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
294
- return lineEnding === '\r\n' ? normalized.replace(/\n/g, '\r\n') : normalized;
295
- }
296
-
297
- function isValidProviderName(name) {
298
- return typeof name === 'string' && /^[a-zA-Z0-9._-]+$/.test(name.trim());
299
- }
300
-
301
- function buildModelsCandidates(baseUrl) {
302
- const trimmed = typeof baseUrl === 'string' ? baseUrl.trim() : '';
303
- if (!trimmed) return [];
304
- if (/\/models\/?$/.test(trimmed)) {
305
- return [trimmed];
306
- }
307
- const normalized = trimmed.replace(/\/+$/, '');
308
- const candidates = [];
309
- const pushUnique = (url) => {
310
- if (url && !candidates.includes(url)) {
311
- candidates.push(url);
312
- }
313
- };
314
-
315
- if (/\/v1$/i.test(normalized)) {
316
- pushUnique(normalized + '/models');
317
- } else {
318
- pushUnique(normalized + '/v1/models');
319
- pushUnique(normalized + '/models');
320
- }
321
-
322
- pushUnique(trimmed);
323
- return candidates;
324
- }
325
-
326
- function extractModelNames(payload) {
327
- if (!payload || typeof payload !== 'object') return [];
328
- const data = Array.isArray(payload.data)
329
- ? payload.data
330
- : (Array.isArray(payload.models) ? payload.models : []);
331
- const names = [];
332
- for (const item of data) {
333
- if (typeof item === 'string') {
334
- if (item.trim()) names.push(item.trim());
335
- continue;
336
- }
337
- if (!item || typeof item !== 'object') continue;
338
- const name = item.id || item.name || item.model || '';
339
- if (typeof name === 'string' && name.trim()) {
340
- names.push(name.trim());
341
- }
342
- }
343
- return Array.from(new Set(names));
344
- }
345
-
346
- function hasModelsListPayload(payload) {
347
- if (!payload || typeof payload !== 'object') return false;
348
- return Array.isArray(payload.data) || Array.isArray(payload.models);
349
- }
350
-
351
- function hashModelsCacheValue(value) {
352
- if (!value) return '';
353
- try {
354
- return crypto.createHash('sha256').update(String(value)).digest('hex');
355
- } catch (e) {
356
- return '';
357
- }
358
- }
359
-
360
- function buildModelsCacheKey(baseUrl, apiKey) {
361
- const normalizedUrl = typeof baseUrl === 'string'
362
- ? baseUrl.trim().replace(/\/+$/, '')
363
- : '';
364
- const apiKeyHash = hashModelsCacheValue(apiKey);
365
- return `${normalizedUrl}|${apiKeyHash}`;
366
- }
367
-
368
253
  function readModelsCacheEntry(cacheKey) {
369
254
  if (!cacheKey) return null;
370
255
  const entry = g_modelsCache.get(cacheKey);
@@ -439,6 +324,7 @@ async function fetchModelsFromBaseUrlCore(baseUrl, apiKey) {
439
324
  };
440
325
  if (apiKey) {
441
326
  headers['Authorization'] = `Bearer ${apiKey}`;
327
+ headers['x-api-key'] = apiKey;
442
328
  }
443
329
 
444
330
  const result = await new Promise((innerResolve) => {
@@ -596,16 +482,6 @@ function applyAgentsFile(params = {}) {
596
482
  }
597
483
  }
598
484
 
599
- function resolveHomePath(input) {
600
- const raw = typeof input === 'string' ? input.trim() : '';
601
- if (!raw) return '';
602
- if (raw === '~') return os.homedir();
603
- if (raw.startsWith('~/') || raw.startsWith('~\\')) {
604
- return path.join(os.homedir(), raw.slice(2));
605
- }
606
- return raw;
607
- }
608
-
609
485
  function resolveOpenclawWorkspaceDir(config) {
610
486
  const workspace = config
611
487
  && config.agents
@@ -847,83 +723,6 @@ function applyOpenclawConfig(params = {}) {
847
723
  }
848
724
  }
849
725
 
850
- function readJsonObjectFromFile(filePath, fallback = {}) {
851
- if (!fs.existsSync(filePath)) {
852
- return { ok: true, exists: false, data: { ...fallback } };
853
- }
854
-
855
- try {
856
- const content = fs.readFileSync(filePath, 'utf-8');
857
- if (!content.trim()) {
858
- return { ok: true, exists: true, data: { ...fallback } };
859
- }
860
-
861
- const parsed = JSON.parse(content);
862
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
863
- return {
864
- ok: false,
865
- exists: true,
866
- error: `配置文件格式错误(根节点必须是对象): ${filePath}`
867
- };
868
- }
869
- return { ok: true, exists: true, data: parsed };
870
- } catch (e) {
871
- return {
872
- ok: false,
873
- exists: true,
874
- error: `配置文件解析失败: ${e.message}`
875
- };
876
- }
877
- }
878
-
879
- function backupFileIfNeededOnce(filePath, backupPrefix = 'codexmate-backup') {
880
- if (!fs.existsSync(filePath)) {
881
- return '';
882
- }
883
-
884
- const dirPath = path.dirname(filePath);
885
- const baseName = path.basename(filePath);
886
- const existingPrefix = `${baseName}.${backupPrefix}-`;
887
- const hasBackup = fs.readdirSync(dirPath).some(fileName =>
888
- fileName.startsWith(existingPrefix) && fileName.endsWith('.bak')
889
- );
890
-
891
- if (hasBackup) {
892
- return '';
893
- }
894
-
895
- const backupPath = path.join(dirPath, `${existingPrefix}${formatTimestampForFileName()}.bak`);
896
- fs.copyFileSync(filePath, backupPath);
897
- return backupPath;
898
- }
899
-
900
- function writeJsonAtomic(filePath, data) {
901
- const dirPath = path.dirname(filePath);
902
- ensureDir(dirPath);
903
-
904
- const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
905
- const content = `${JSON.stringify(data, null, 2)}\n`;
906
-
907
- try {
908
- fs.writeFileSync(tmpPath, content, 'utf-8');
909
- try {
910
- fs.renameSync(tmpPath, filePath);
911
- } catch (renameError) {
912
- if (process.platform === 'win32') {
913
- fs.copyFileSync(tmpPath, filePath);
914
- fs.unlinkSync(tmpPath);
915
- } else {
916
- throw renameError;
917
- }
918
- }
919
- } catch (e) {
920
- if (fs.existsSync(tmpPath)) {
921
- try { fs.unlinkSync(tmpPath); } catch (_) {}
922
- }
923
- throw new Error(`写入 JSON 文件失败: ${e.message}`);
924
- }
925
- }
926
-
927
726
  function normalizeRecentConfigs(items) {
928
727
  if (!Array.isArray(items)) return [];
929
728
  const output = [];
@@ -975,330 +774,84 @@ function recordRecentConfig(provider, model) {
975
774
  writeRecentConfigs(trimmed);
976
775
  }
977
776
 
978
- function isValidHttpUrl(value) {
979
- if (typeof value !== 'string' || !value.trim()) return false;
980
- try {
981
- const parsed = new URL(value);
982
- return parsed.protocol === 'http:' || parsed.protocol === 'https:';
983
- } catch (e) {
984
- return false;
985
- }
986
- }
987
-
988
- function normalizeBaseUrl(value) {
989
- if (typeof value !== 'string') return '';
990
- return value.trim().replace(/\/+$/g, '');
991
- }
992
-
993
- function joinApiUrl(baseUrl, pathSuffix) {
994
- const trimmed = normalizeBaseUrl(baseUrl);
995
- if (!trimmed) return '';
996
- const safeSuffix = String(pathSuffix || '').replace(/^\/+/g, '');
997
- if (!safeSuffix) return trimmed;
998
- if (/\/v1$/i.test(trimmed)) {
999
- return `${trimmed}/${safeSuffix}`;
777
+ async function runRemoteHealthCheck(provider, modelName, options = {}) {
778
+ const issues = [];
779
+ const results = {};
780
+ const baseUrl = normalizeBaseUrl(provider && provider.base_url ? provider.base_url : '');
781
+ if (!baseUrl) {
782
+ issues.push({
783
+ code: 'remote-skip-base-url',
784
+ message: '无法进行远程探测:base_url 为空',
785
+ suggestion: '补全 base_url 或关闭远程探测'
786
+ });
787
+ return { issues, results };
1000
788
  }
1001
- return `${trimmed}/v1/${safeSuffix}`;
1002
- }
1003
-
1004
- function buildModelsProbeUrl(baseUrl) {
1005
- return joinApiUrl(baseUrl, 'models');
1006
- }
1007
789
 
1008
- function normalizeWireApi(value) {
1009
- const raw = typeof value === 'string' ? value.trim().toLowerCase() : '';
1010
- if (!raw) return 'responses';
1011
- return raw.replace(/[\s-]/g, '_');
1012
- }
790
+ const requiresAuth = provider && provider.requires_openai_auth !== false;
791
+ const apiKey = typeof provider.preferred_auth_method === 'string'
792
+ ? provider.preferred_auth_method.trim()
793
+ : '';
794
+ const authValue = requiresAuth ? apiKey : (apiKey || '');
795
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
1013
796
 
1014
- function buildModelProbeSpec(provider, modelName, baseUrl) {
1015
- const model = typeof modelName === 'string' ? modelName.trim() : '';
1016
- if (!model) return null;
797
+ const baseProbe = await probeUrl(baseUrl, { apiKey: authValue, timeoutMs });
798
+ results.base = {
799
+ url: baseUrl,
800
+ status: baseProbe.status || 0,
801
+ ok: baseProbe.ok,
802
+ durationMs: baseProbe.durationMs || 0
803
+ };
1017
804
 
1018
- const wireApi = normalizeWireApi(provider && provider.wire_api);
1019
- if (wireApi === 'chat_completions' || wireApi === 'chat') {
1020
- return {
1021
- url: joinApiUrl(baseUrl, 'chat/completions'),
1022
- body: {
1023
- model,
1024
- messages: [{ role: 'user', content: 'ping' }],
1025
- max_tokens: 1,
1026
- temperature: 0
1027
- }
1028
- };
805
+ if (!baseProbe.ok) {
806
+ issues.push({
807
+ code: 'remote-unreachable',
808
+ message: `远程探测失败:${baseProbe.error || '无法连接'}`,
809
+ suggestion: '检查网络与 base_url 可达性'
810
+ });
811
+ return { issues, results };
1029
812
  }
1030
813
 
1031
- if (wireApi === 'completions') {
1032
- return {
1033
- url: joinApiUrl(baseUrl, 'completions'),
1034
- body: {
1035
- model,
1036
- prompt: 'ping',
1037
- max_tokens: 1,
1038
- temperature: 0
1039
- }
1040
- };
814
+ if (baseProbe.status === 401 || baseProbe.status === 403) {
815
+ issues.push({
816
+ code: 'remote-auth-failed',
817
+ message: '远程探测鉴权失败(401/403)',
818
+ suggestion: '检查 API Key 或认证方式'
819
+ });
820
+ } else if (baseProbe.status >= 400) {
821
+ issues.push({
822
+ code: 'remote-http-error',
823
+ message: `远程探测返回异常状态: ${baseProbe.status}`,
824
+ suggestion: '检查 base_url 是否正确'
825
+ });
1041
826
  }
1042
827
 
1043
- return {
1044
- url: joinApiUrl(baseUrl, 'responses'),
1045
- body: {
1046
- model,
1047
- input: 'ping',
1048
- max_output_tokens: 1
1049
- }
1050
- };
1051
- }
1052
-
1053
- function probeUrl(targetUrl, options = {}) {
1054
- return new Promise((resolve) => {
1055
- let parsed;
1056
- try {
1057
- parsed = new URL(targetUrl);
1058
- } catch (e) {
1059
- return resolve({ ok: false, error: 'Invalid URL' });
1060
- }
1061
-
1062
- const transport = parsed.protocol === 'https:' ? https : http;
1063
- const headers = {
1064
- 'User-Agent': 'codexmate-health-check',
1065
- 'Accept': 'application/json'
828
+ const modelsUrl = buildModelsProbeUrl(baseUrl);
829
+ if (modelsUrl) {
830
+ const modelsProbe = await probeUrl(modelsUrl, { apiKey: authValue, timeoutMs, maxBytes: 256 * 1024 });
831
+ results.models = {
832
+ url: modelsUrl,
833
+ status: modelsProbe.status || 0,
834
+ ok: modelsProbe.ok,
835
+ durationMs: modelsProbe.durationMs || 0
1066
836
  };
1067
- if (options.apiKey) {
1068
- headers['Authorization'] = `Bearer ${options.apiKey}`;
1069
- }
1070
837
 
1071
- const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
1072
- const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 256 * 1024;
1073
- const start = Date.now();
1074
- const req = transport.request(parsed, { method: 'GET', headers }, (res) => {
1075
- const chunks = [];
1076
- let size = 0;
1077
- res.on('data', (chunk) => {
1078
- if (!chunk) return;
1079
- size += chunk.length;
1080
- if (size <= maxBytes) {
1081
- chunks.push(chunk);
1082
- }
838
+ if (!modelsProbe.ok) {
839
+ issues.push({
840
+ code: 'remote-models-unreachable',
841
+ message: `模型列表探测失败:${modelsProbe.error || '无法连接'}`,
842
+ suggestion: '检查 base_url 是否包含 /v1 或关闭远程探测'
1083
843
  });
1084
- res.on('end', () => {
1085
- const body = chunks.length > 0 ? Buffer.concat(chunks).toString('utf-8') : '';
1086
- resolve({
1087
- ok: true,
1088
- status: res.statusCode || 0,
1089
- durationMs: Date.now() - start,
1090
- body
1091
- });
844
+ } else if (modelsProbe.status === 401 || modelsProbe.status === 403) {
845
+ issues.push({
846
+ code: 'remote-models-auth-failed',
847
+ message: '模型列表鉴权失败(401/403)',
848
+ suggestion: '检查 API Key 或认证方式'
1092
849
  });
1093
- });
1094
-
1095
- req.setTimeout(timeoutMs, () => {
1096
- req.destroy(new Error('timeout'));
1097
- });
1098
-
1099
- req.on('error', (err) => {
1100
- resolve({
1101
- ok: false,
1102
- error: err.message || 'request failed',
1103
- durationMs: Date.now() - start
1104
- });
1105
- });
1106
-
1107
- req.end();
1108
- });
1109
- }
1110
-
1111
- function probeJsonPost(targetUrl, body, options = {}) {
1112
- return new Promise((resolve) => {
1113
- let parsed;
1114
- try {
1115
- parsed = new URL(targetUrl);
1116
- } catch (e) {
1117
- return resolve({ ok: false, error: 'Invalid URL' });
1118
- }
1119
-
1120
- const transport = parsed.protocol === 'https:' ? https : http;
1121
- const headers = {
1122
- 'User-Agent': 'codexmate-health-check',
1123
- 'Accept': 'application/json',
1124
- 'Content-Type': 'application/json'
1125
- };
1126
- if (options.apiKey) {
1127
- headers['Authorization'] = `Bearer ${options.apiKey}`;
1128
- }
1129
-
1130
- const payload = JSON.stringify(body || {});
1131
- headers['Content-Length'] = Buffer.byteLength(payload);
1132
-
1133
- const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
1134
- const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 256 * 1024;
1135
- const start = Date.now();
1136
- const req = transport.request(parsed, { method: 'POST', headers }, (res) => {
1137
- const chunks = [];
1138
- let size = 0;
1139
- res.on('data', (chunk) => {
1140
- if (!chunk) return;
1141
- size += chunk.length;
1142
- if (size <= maxBytes) {
1143
- chunks.push(chunk);
1144
- }
1145
- });
1146
- res.on('end', () => {
1147
- const bodyText = chunks.length > 0 ? Buffer.concat(chunks).toString('utf-8') : '';
1148
- resolve({
1149
- ok: true,
1150
- status: res.statusCode || 0,
1151
- durationMs: Date.now() - start,
1152
- body: bodyText
1153
- });
1154
- });
1155
- });
1156
-
1157
- req.setTimeout(timeoutMs, () => {
1158
- req.destroy(new Error('timeout'));
1159
- });
1160
-
1161
- req.on('error', (err) => {
1162
- resolve({
1163
- ok: false,
1164
- error: err.message || 'request failed',
1165
- durationMs: Date.now() - start
1166
- });
1167
- });
1168
-
1169
- req.write(payload);
1170
- req.end();
1171
- });
1172
- }
1173
-
1174
- function extractModelIds(payload) {
1175
- const ids = [];
1176
- const pushValue = (value) => {
1177
- if (typeof value === 'string' && value.trim()) {
1178
- ids.push(value.trim());
1179
- }
1180
- };
1181
-
1182
- if (!payload) return ids;
1183
-
1184
- if (Array.isArray(payload)) {
1185
- for (const item of payload) {
1186
- if (item && typeof item === 'object') {
1187
- pushValue(item.id);
1188
- pushValue(item.model);
1189
- pushValue(item.name);
1190
- } else {
1191
- pushValue(item);
1192
- }
1193
- }
1194
- return ids;
1195
- }
1196
-
1197
- if (Array.isArray(payload.data)) {
1198
- for (const item of payload.data) {
1199
- if (item && typeof item === 'object') {
1200
- pushValue(item.id);
1201
- pushValue(item.model);
1202
- pushValue(item.name);
1203
- } else {
1204
- pushValue(item);
1205
- }
1206
- }
1207
- }
1208
-
1209
- if (Array.isArray(payload.models)) {
1210
- for (const item of payload.models) {
1211
- if (item && typeof item === 'object') {
1212
- pushValue(item.id);
1213
- pushValue(item.model);
1214
- pushValue(item.name);
1215
- } else {
1216
- pushValue(item);
1217
- }
1218
- }
1219
- }
1220
-
1221
- return ids;
1222
- }
1223
-
1224
- async function runRemoteHealthCheck(provider, modelName, options = {}) {
1225
- const issues = [];
1226
- const results = {};
1227
- const baseUrl = normalizeBaseUrl(provider && provider.base_url ? provider.base_url : '');
1228
- if (!baseUrl) {
1229
- issues.push({
1230
- code: 'remote-skip-base-url',
1231
- message: '无法进行远程探测:base_url 为空',
1232
- suggestion: '补全 base_url 或关闭远程探测'
1233
- });
1234
- return { issues, results };
1235
- }
1236
-
1237
- const requiresAuth = provider && provider.requires_openai_auth !== false;
1238
- const apiKey = typeof provider.preferred_auth_method === 'string'
1239
- ? provider.preferred_auth_method.trim()
1240
- : '';
1241
- const authValue = requiresAuth ? apiKey : (apiKey || '');
1242
- const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
1243
-
1244
- const baseProbe = await probeUrl(baseUrl, { apiKey: authValue, timeoutMs });
1245
- results.base = {
1246
- url: baseUrl,
1247
- status: baseProbe.status || 0,
1248
- ok: baseProbe.ok,
1249
- durationMs: baseProbe.durationMs || 0
1250
- };
1251
-
1252
- if (!baseProbe.ok) {
1253
- issues.push({
1254
- code: 'remote-unreachable',
1255
- message: `远程探测失败:${baseProbe.error || '无法连接'}`,
1256
- suggestion: '检查网络与 base_url 可达性'
1257
- });
1258
- return { issues, results };
1259
- }
1260
-
1261
- if (baseProbe.status === 401 || baseProbe.status === 403) {
1262
- issues.push({
1263
- code: 'remote-auth-failed',
1264
- message: '远程探测鉴权失败(401/403)',
1265
- suggestion: '检查 API Key 或认证方式'
1266
- });
1267
- } else if (baseProbe.status >= 400) {
1268
- issues.push({
1269
- code: 'remote-http-error',
1270
- message: `远程探测返回异常状态: ${baseProbe.status}`,
1271
- suggestion: '检查 base_url 是否正确'
1272
- });
1273
- }
1274
-
1275
- const modelsUrl = buildModelsProbeUrl(baseUrl);
1276
- if (modelsUrl) {
1277
- const modelsProbe = await probeUrl(modelsUrl, { apiKey: authValue, timeoutMs, maxBytes: 256 * 1024 });
1278
- results.models = {
1279
- url: modelsUrl,
1280
- status: modelsProbe.status || 0,
1281
- ok: modelsProbe.ok,
1282
- durationMs: modelsProbe.durationMs || 0
1283
- };
1284
-
1285
- if (!modelsProbe.ok) {
1286
- issues.push({
1287
- code: 'remote-models-unreachable',
1288
- message: `模型列表探测失败:${modelsProbe.error || '无法连接'}`,
1289
- suggestion: '检查 base_url 是否包含 /v1 或关闭远程探测'
1290
- });
1291
- } else if (modelsProbe.status === 401 || modelsProbe.status === 403) {
1292
- issues.push({
1293
- code: 'remote-models-auth-failed',
1294
- message: '模型列表鉴权失败(401/403)',
1295
- suggestion: '检查 API Key 或认证方式'
1296
- });
1297
- } else if (modelsProbe.status >= 400) {
1298
- issues.push({
1299
- code: 'remote-models-http-error',
1300
- message: `模型列表返回异常状态: ${modelsProbe.status}`,
1301
- suggestion: '确认 /v1/models 可用'
850
+ } else if (modelsProbe.status >= 400) {
851
+ issues.push({
852
+ code: 'remote-models-http-error',
853
+ message: `模型列表返回异常状态: ${modelsProbe.status}`,
854
+ suggestion: '确认 /v1/models 可用'
1302
855
  });
1303
856
  } else {
1304
857
  let payload = null;
@@ -1558,84 +1111,6 @@ async function buildConfigHealthReport(params = {}) {
1558
1111
  };
1559
1112
  }
1560
1113
 
1561
- function formatTimestampForFileName(value) {
1562
- const date = value ? new Date(value) : new Date();
1563
- const normalized = Number.isNaN(date.getTime()) ? new Date() : date;
1564
- const pad = (num) => String(num).padStart(2, '0');
1565
- return [
1566
- normalized.getFullYear(),
1567
- pad(normalized.getMonth() + 1),
1568
- pad(normalized.getDate()),
1569
- '-',
1570
- pad(normalized.getHours()),
1571
- pad(normalized.getMinutes()),
1572
- pad(normalized.getSeconds())
1573
- ].join('');
1574
- }
1575
-
1576
- function toIsoTime(value, fallback = '') {
1577
- if (value === undefined || value === null || value === '') {
1578
- return fallback;
1579
- }
1580
- const date = new Date(value);
1581
- if (Number.isNaN(date.getTime())) {
1582
- return fallback;
1583
- }
1584
- return date.toISOString();
1585
- }
1586
-
1587
- function updateLatestIso(currentIso, candidate) {
1588
- const currentTime = Date.parse(currentIso || '') || 0;
1589
- const candidateIso = toIsoTime(candidate, '');
1590
- const candidateTime = Date.parse(candidateIso || '') || 0;
1591
- if (!candidateTime) {
1592
- return currentIso;
1593
- }
1594
- return candidateTime > currentTime ? candidateIso : currentIso;
1595
- }
1596
-
1597
- function truncateText(text, maxLength = 90) {
1598
- if (!text) return '';
1599
- const normalized = String(text).replace(/\s+/g, ' ').trim();
1600
- if (normalized.length <= maxLength) return normalized;
1601
- return normalized.slice(0, maxLength - 1) + '…';
1602
- }
1603
-
1604
- function extractMessageText(content) {
1605
- if (typeof content === 'string') {
1606
- return content.trim();
1607
- }
1608
-
1609
- if (Array.isArray(content)) {
1610
- const parts = content
1611
- .map(item => extractMessageText(item))
1612
- .filter(Boolean);
1613
- return parts.join('\n').trim();
1614
- }
1615
-
1616
- if (!content || typeof content !== 'object') {
1617
- return '';
1618
- }
1619
-
1620
- if (typeof content.text === 'string') {
1621
- return content.text.trim();
1622
- }
1623
-
1624
- if (typeof content.value === 'string') {
1625
- return content.value.trim();
1626
- }
1627
-
1628
- if (content.content !== undefined) {
1629
- return extractMessageText(content.content);
1630
- }
1631
-
1632
- if (typeof content.output === 'string') {
1633
- return content.output.trim();
1634
- }
1635
-
1636
- return '';
1637
- }
1638
-
1639
1114
  function buildDefaultConfigContent(initializedAt) {
1640
1115
  const defaultModel = DEFAULT_MODELS[0] || 'gpt-4';
1641
1116
  return `${CODEXMATE_MANAGED_MARKER}
@@ -1712,6 +1187,22 @@ function normalizeTopLevelConfigWithTemplate(template, selectedProvider, selecte
1712
1187
  return content;
1713
1188
  }
1714
1189
 
1190
+ function applyServiceTierToTemplate(template, serviceTier) {
1191
+ let content = typeof template === 'string' ? template : '';
1192
+ const tier = typeof serviceTier === 'string' ? serviceTier.trim().toLowerCase() : '';
1193
+ if (!tier) {
1194
+ return content;
1195
+ }
1196
+
1197
+ content = content.replace(/^\s*service_tier\s*=\s*["'][^"']*["']\s*\n?/gmi, '');
1198
+ if (tier !== 'fast') {
1199
+ return content;
1200
+ }
1201
+
1202
+ content = content.replace(/^\s*\n*/, '');
1203
+ return `service_tier = "fast"\n${content}`;
1204
+ }
1205
+
1715
1206
  function getConfigTemplate(params = {}) {
1716
1207
  let content = EMPTY_CONFIG_FALLBACK_TEMPLATE;
1717
1208
  if (fs.existsSync(CONFIG_FILE)) {
@@ -1724,8 +1215,12 @@ function getConfigTemplate(params = {}) {
1724
1215
  }
1725
1216
  const selectedProvider = params.provider || '';
1726
1217
  const selectedModel = params.model || '';
1218
+ let template = normalizeTopLevelConfigWithTemplate(content, selectedProvider, selectedModel);
1219
+ if (typeof params.serviceTier === 'string') {
1220
+ template = applyServiceTierToTemplate(template, params.serviceTier);
1221
+ }
1727
1222
  return {
1728
- template: normalizeTopLevelConfigWithTemplate(content, selectedProvider, selectedModel)
1223
+ template
1729
1224
  };
1730
1225
  }
1731
1226
 
@@ -2022,17 +1517,6 @@ function parseJsonlHeadRecords(filePath, maxBytes = SESSION_SUMMARY_READ_BYTES)
2022
1517
  return parseJsonlContent(headText);
2023
1518
  }
2024
1519
 
2025
- function normalizeRole(value) {
2026
- if (typeof value !== 'string') {
2027
- return '';
2028
- }
2029
- const role = value.trim().toLowerCase();
2030
- if (role === 'assistant' || role === 'user' || role === 'system') {
2031
- return role;
2032
- }
2033
- return '';
2034
- }
2035
-
2036
1520
  function isBootstrapLikeText(text) {
2037
1521
  if (!text || typeof text !== 'string') {
2038
1522
  return false;
@@ -2046,17 +1530,17 @@ function isBootstrapLikeText(text) {
2046
1530
  return BOOTSTRAP_TEXT_MARKERS.some(marker => normalized.includes(marker));
2047
1531
  }
2048
1532
 
2049
- function removeLeadingSystemMessage(messages) {
2050
- if (!Array.isArray(messages) || messages.length === 0) {
2051
- return [];
2052
- }
2053
-
2054
- let startIndex = 1;
2055
- while (startIndex < messages.length) {
2056
- const item = messages[startIndex];
2057
- const role = item ? normalizeRole(item.role) : '';
2058
- const text = item && typeof item.text === 'string' ? item.text : '';
2059
- const isSystemRole = role === 'system';
1533
+ function removeLeadingSystemMessage(messages) {
1534
+ if (!Array.isArray(messages) || messages.length === 0) {
1535
+ return [];
1536
+ }
1537
+
1538
+ let startIndex = 0;
1539
+ while (startIndex < messages.length) {
1540
+ const item = messages[startIndex];
1541
+ const role = item ? normalizeRole(item.role) : '';
1542
+ const text = item && typeof item.text === 'string' ? item.text : '';
1543
+ const isSystemRole = role === 'system';
2060
1544
  const isBootstrapText = isBootstrapLikeText(text);
2061
1545
  if (!item || isSystemRole || isBootstrapText) {
2062
1546
  startIndex += 1;
@@ -2143,16 +1627,85 @@ function matchesSessionPathFilter(session, normalizedFilter) {
2143
1627
  return cwd.includes(normalizedFilter);
2144
1628
  }
2145
1629
 
2146
- function normalizeQueryTokens(query) {
2147
- if (typeof query !== 'string') {
2148
- return [];
2149
- }
2150
- return query
2151
- .split(/\s+/)
2152
- .map(item => item.trim())
2153
- .map(item => item.toLowerCase())
2154
- .filter(Boolean);
2155
- }
1630
+ function normalizeQueryTokens(query) {
1631
+ if (typeof query !== 'string') {
1632
+ return [];
1633
+ }
1634
+ return query
1635
+ .split(/\s+/)
1636
+ .map(item => item.trim())
1637
+ .map(item => item.toLowerCase())
1638
+ .filter(Boolean);
1639
+ }
1640
+
1641
+ function expandSessionQueryTokens(tokens) {
1642
+ const base = Array.isArray(tokens) ? tokens.map(t => String(t || '').toLowerCase()).filter(Boolean) : [];
1643
+ const result = [];
1644
+ const seen = new Set();
1645
+ let hasClaudeAlias = false;
1646
+ let hasDaudeAlias = false;
1647
+
1648
+ for (const token of base) {
1649
+ if (/^claude[-_ ]?code$/.test(token) || token === 'claudecode') {
1650
+ hasClaudeAlias = true;
1651
+ continue;
1652
+ }
1653
+ if (/^daude[-_ ]?code$/.test(token) || token === 'daudecode') {
1654
+ hasDaudeAlias = true;
1655
+ continue;
1656
+ }
1657
+ if (!seen.has(token)) {
1658
+ seen.add(token);
1659
+ result.push(token);
1660
+ }
1661
+ }
1662
+
1663
+ const push = (token) => {
1664
+ const normalized = String(token || '').toLowerCase();
1665
+ if (!normalized || seen.has(normalized)) return;
1666
+ seen.add(normalized);
1667
+ result.push(normalized);
1668
+ };
1669
+
1670
+ if (hasClaudeAlias) {
1671
+ push('claude');
1672
+ push('code');
1673
+ }
1674
+ if (hasDaudeAlias) {
1675
+ push('daude');
1676
+ push('code');
1677
+ }
1678
+
1679
+ return result;
1680
+ }
1681
+
1682
+ function normalizeKeywords(value) {
1683
+ if (!Array.isArray(value)) {
1684
+ return [];
1685
+ }
1686
+ const seen = new Set();
1687
+ const result = [];
1688
+ for (const item of value) {
1689
+ const normalized = typeof item === 'string' ? item.trim() : String(item || '').trim();
1690
+ if (!normalized) continue;
1691
+ const lower = normalized.toLowerCase();
1692
+ if (seen.has(lower)) continue;
1693
+ seen.add(lower);
1694
+ result.push(normalized);
1695
+ }
1696
+ return result;
1697
+ }
1698
+
1699
+ function normalizeCapabilities(value) {
1700
+ const result = {};
1701
+ if (!value || typeof value !== 'object') {
1702
+ return result;
1703
+ }
1704
+ if (value.code === true) {
1705
+ result.code = true;
1706
+ }
1707
+ return result;
1708
+ }
2156
1709
 
2157
1710
  function normalizeQueryMode(mode) {
2158
1711
  return mode === 'or' ? 'or' : 'and';
@@ -2187,18 +1740,22 @@ function matchTokensInText(text, tokens, mode = 'and') {
2187
1740
  return tokens.every(token => haystack.includes(token));
2188
1741
  }
2189
1742
 
2190
- function buildSessionSummaryText(session) {
2191
- if (!session) {
2192
- return '';
2193
- }
2194
- return [
2195
- session.title,
2196
- session.sessionId,
2197
- session.cwd,
2198
- session.filePath,
2199
- session.sourceLabel
2200
- ].filter(Boolean).join(' ');
2201
- }
1743
+ function buildSessionSummaryText(session) {
1744
+ if (!session) {
1745
+ return '';
1746
+ }
1747
+ const keywords = Array.isArray(session.keywords) ? session.keywords.join(' ') : '';
1748
+ const provider = typeof session.provider === 'string' ? session.provider : '';
1749
+ return [
1750
+ session.title,
1751
+ session.sessionId,
1752
+ session.cwd,
1753
+ session.filePath,
1754
+ session.sourceLabel,
1755
+ provider,
1756
+ keywords
1757
+ ].filter(Boolean).join(' ');
1758
+ }
2202
1759
 
2203
1760
  function extractMessageFromRecord(record, source) {
2204
1761
  if (!record) {
@@ -2308,39 +1865,39 @@ function applySessionQueryFilter(sessions, options = {}) {
2308
1865
  ? Math.max(1024, Number(options.contentScanBytes))
2309
1866
  : SESSION_CONTENT_READ_BYTES;
2310
1867
 
2311
- let scanned = 0;
2312
- const results = [];
2313
-
2314
- for (const session of sessions) {
2315
- if (scope === 'content' && scanned >= contentScanLimit) {
1868
+ let scanned = 0;
1869
+ const results = [];
1870
+
1871
+ for (const session of sessions) {
1872
+ if (scope === 'content' && scanned >= contentScanLimit) {
2316
1873
  break;
2317
1874
  }
1875
+
1876
+ const summaryText = buildSessionSummaryText(session);
1877
+ const summaryHit = scope !== 'content' && matchTokensInText(summaryText, tokens, mode);
1878
+ let contentHit = false;
1879
+ let contentInfo = null;
1880
+
1881
+ const shouldScanContent = scope === 'content' || scope === 'all' || !summaryHit;
1882
+ if (shouldScanContent && scanned < contentScanLimit) {
1883
+ scanned += 1;
1884
+ contentInfo = scanSessionContentForQuery(session, tokens, {
1885
+ mode,
1886
+ roleFilter,
1887
+ maxBytes: contentScanBytes,
1888
+ maxMatches: 1,
1889
+ snippetLimit: 2
1890
+ });
1891
+ contentHit = contentInfo.hit;
1892
+ }
2318
1893
 
2319
- const summaryText = buildSessionSummaryText(session);
2320
- const summaryHit = scope !== 'content' && matchTokensInText(summaryText, tokens, mode);
2321
- let contentHit = false;
2322
- let contentInfo = null;
2323
-
2324
- if (scope !== 'summary' && (!summaryHit || scope === 'content')) {
2325
- if (scanned < contentScanLimit) {
2326
- scanned += 1;
2327
- contentInfo = scanSessionContentForQuery(session, tokens, {
2328
- mode,
2329
- roleFilter,
2330
- maxBytes: contentScanBytes,
2331
- maxMatches: 1,
2332
- snippetLimit: 2
2333
- });
2334
- contentHit = contentInfo.hit;
2335
- }
2336
- }
2337
-
2338
- const hit = scope === 'summary'
2339
- ? summaryHit
2340
- : (scope === 'content' ? contentHit : (summaryHit || contentHit));
2341
- if (!hit) {
2342
- continue;
2343
- }
1894
+ const hit = scope === 'summary'
1895
+ ? summaryHit
1896
+ : (scope === 'content' ? contentHit : (summaryHit || contentHit));
1897
+
1898
+ if (!hit) {
1899
+ continue;
1900
+ }
2344
1901
 
2345
1902
  const matchInfo = contentInfo && contentInfo.hit
2346
1903
  ? contentInfo
@@ -2515,23 +2072,26 @@ function parseCodexSessionSummary(filePath) {
2515
2072
  }
2516
2073
  }
2517
2074
 
2518
- messageCount = Math.max(0, messageCount);
2519
-
2520
- return {
2521
- source: 'codex',
2522
- sourceLabel: 'Codex',
2523
- sessionId,
2524
- title: firstPrompt || sessionId,
2525
- cwd,
2526
- createdAt,
2527
- updatedAt,
2528
- messageCount,
2529
- filePath
2530
- };
2531
- }
2532
-
2533
- function parseClaudeSessionSummary(filePath) {
2534
- const records = parseJsonlHeadRecords(filePath);
2075
+ messageCount = Math.max(0, messageCount);
2076
+
2077
+ return {
2078
+ source: 'codex',
2079
+ sourceLabel: 'Codex',
2080
+ provider: 'codex',
2081
+ sessionId,
2082
+ title: firstPrompt || sessionId,
2083
+ cwd,
2084
+ createdAt,
2085
+ updatedAt,
2086
+ messageCount,
2087
+ filePath,
2088
+ keywords: [],
2089
+ capabilities: {}
2090
+ };
2091
+ }
2092
+
2093
+ function parseClaudeSessionSummary(filePath) {
2094
+ const records = parseJsonlHeadRecords(filePath);
2535
2095
  if (records.length === 0) {
2536
2096
  return null;
2537
2097
  }
@@ -2601,20 +2161,23 @@ function parseClaudeSessionSummary(filePath) {
2601
2161
  }
2602
2162
  }
2603
2163
 
2604
- messageCount = Math.max(0, messageCount);
2605
-
2606
- return {
2607
- source: 'claude',
2608
- sourceLabel: 'Claude Code',
2609
- sessionId,
2610
- title: firstPrompt || sessionId,
2611
- cwd,
2612
- createdAt,
2613
- updatedAt,
2614
- messageCount,
2615
- filePath
2616
- };
2617
- }
2164
+ messageCount = Math.max(0, messageCount);
2165
+
2166
+ return {
2167
+ source: 'claude',
2168
+ sourceLabel: 'Claude Code',
2169
+ provider: 'claude',
2170
+ sessionId,
2171
+ title: firstPrompt || sessionId,
2172
+ cwd,
2173
+ createdAt,
2174
+ updatedAt,
2175
+ messageCount,
2176
+ filePath,
2177
+ keywords: [],
2178
+ capabilities: { code: true }
2179
+ };
2180
+ }
2618
2181
 
2619
2182
  function listCodexSessions(limit, options = {}) {
2620
2183
  const codexSessionsDir = getCodexSessionsDir();
@@ -2715,12 +2278,12 @@ function listClaudeSessions(limit, options = {}) {
2715
2278
  let title = truncateText(entry.summary || entry.firstPrompt || sessionId, 120);
2716
2279
  let messageCount = Number.isFinite(entry.messageCount) ? Math.max(0, entry.messageCount - 1) : 0;
2717
2280
 
2718
- const quickRecords = parseJsonlHeadRecords(filePath, SESSION_TITLE_READ_BYTES);
2719
- if (quickRecords.length > 0) {
2720
- const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
2721
- if (filteredCount > 0 || messageCount === 0) {
2722
- messageCount = filteredCount;
2723
- }
2281
+ const quickRecords = parseJsonlHeadRecords(filePath, SESSION_TITLE_READ_BYTES);
2282
+ if (quickRecords.length > 0) {
2283
+ const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
2284
+ if (filteredCount > 0 || messageCount === 0) {
2285
+ messageCount = filteredCount;
2286
+ }
2724
2287
 
2725
2288
  const quickMessages = [];
2726
2289
  for (const record of quickRecords) {
@@ -2729,29 +2292,38 @@ function listClaudeSessions(limit, options = {}) {
2729
2292
  const content = record.message ? record.message.content : '';
2730
2293
  quickMessages.push({ role, text: extractMessageText(content) });
2731
2294
  }
2732
- }
2733
- const filteredQuickMessages = removeLeadingSystemMessage(quickMessages);
2734
- const firstUser = filteredQuickMessages.find(item => item.role === 'user' && item.text);
2735
- if (firstUser) {
2736
- title = truncateText(firstUser.text, 120);
2737
- }
2738
- }
2739
-
2740
- sessions.push({
2741
- source: 'claude',
2742
- sourceLabel: 'Claude Code',
2743
- sessionId,
2744
- title,
2745
- cwd: entry.projectPath || index.originalPath || '',
2746
- createdAt,
2747
- updatedAt,
2748
- messageCount,
2749
- filePath
2750
- });
2751
-
2752
- if (sessions.length >= targetCount) {
2753
- break;
2754
- }
2295
+ }
2296
+ const filteredQuickMessages = removeLeadingSystemMessage(quickMessages);
2297
+ const firstUser = filteredQuickMessages.find(item => item.role === 'user' && item.text);
2298
+ if (firstUser) {
2299
+ title = truncateText(firstUser.text, 120);
2300
+ }
2301
+ }
2302
+
2303
+ const provider = typeof entry.provider === 'string' && entry.provider.trim()
2304
+ ? entry.provider.trim()
2305
+ : 'claude';
2306
+ const keywords = normalizeKeywords(entry.keywords);
2307
+ const capabilities = normalizeCapabilities(entry.capabilities);
2308
+
2309
+ sessions.push({
2310
+ source: 'claude',
2311
+ sourceLabel: 'Claude Code',
2312
+ provider,
2313
+ sessionId,
2314
+ title,
2315
+ cwd: entry.projectPath || index.originalPath || '',
2316
+ createdAt,
2317
+ updatedAt,
2318
+ messageCount,
2319
+ filePath,
2320
+ keywords,
2321
+ capabilities
2322
+ });
2323
+
2324
+ if (sessions.length >= targetCount) {
2325
+ break;
2326
+ }
2755
2327
  }
2756
2328
 
2757
2329
  if (sessions.length >= targetCount) {
@@ -2784,15 +2356,15 @@ function listAllSessions(params = {}) {
2784
2356
  const source = params.source === 'codex' || params.source === 'claude'
2785
2357
  ? params.source
2786
2358
  : 'all';
2787
- const rawLimit = Number(params.limit);
2788
- const limit = Number.isFinite(rawLimit)
2789
- ? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
2790
- : 120;
2791
- const forceRefresh = !!params.forceRefresh;
2792
- const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
2793
- const hasPathFilter = !!normalizedPathFilter;
2794
- const queryTokens = normalizeQueryTokens(params.query);
2795
- const hasQuery = queryTokens.length > 0;
2359
+ const rawLimit = Number(params.limit);
2360
+ const limit = Number.isFinite(rawLimit)
2361
+ ? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
2362
+ : 120;
2363
+ const forceRefresh = !!params.forceRefresh;
2364
+ const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
2365
+ const hasPathFilter = !!normalizedPathFilter;
2366
+ const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
2367
+ const hasQuery = queryTokens.length > 0;
2796
2368
  const cacheKey = hasQuery ? '' : `${source}:${limit}:${normalizedPathFilter}`;
2797
2369
  if (!hasQuery) {
2798
2370
  const cached = getSessionListCache(cacheKey, forceRefresh);
@@ -2809,16 +2381,16 @@ function listAllSessions(params = {}) {
2809
2381
  : {};
2810
2382
 
2811
2383
  let sessions = [];
2812
- if (source === 'all' || source === 'codex') {
2813
- sessions = sessions.concat(listCodexSessions(limit, scanOptions));
2814
- }
2815
- if (source === 'all' || source === 'claude') {
2816
- sessions = sessions.concat(listClaudeSessions(limit, scanOptions));
2817
- }
2818
-
2819
- if (hasPathFilter) {
2820
- sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
2821
- }
2384
+ if (source === 'all' || source === 'codex') {
2385
+ sessions = sessions.concat(listCodexSessions(limit, scanOptions));
2386
+ }
2387
+ if (source === 'all' || source === 'claude') {
2388
+ sessions = sessions.concat(listClaudeSessions(limit, scanOptions));
2389
+ }
2390
+
2391
+ if (hasPathFilter) {
2392
+ sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
2393
+ }
2822
2394
 
2823
2395
  let result = sessions;
2824
2396
  if (hasQuery) {
@@ -2905,15 +2477,15 @@ function resolveSessionFilePath(source, filePath, sessionId) {
2905
2477
  }
2906
2478
  }
2907
2479
 
2908
- if (typeof sessionId === 'string' && sessionId.trim()) {
2909
- const targetId = sessionId.trim().toLowerCase();
2910
- const files = collectJsonlFiles(root, 5000);
2911
- const matchedFile = files.find(item => path.basename(item).toLowerCase().includes(targetId));
2912
- if (matchedFile && fs.existsSync(matchedFile)) {
2913
- return matchedFile;
2914
- }
2915
- }
2916
-
2480
+ if (typeof sessionId === 'string' && sessionId.trim()) {
2481
+ const targetId = sessionId.trim().toLowerCase();
2482
+ const files = collectJsonlFiles(root, 5000);
2483
+ const matchedFile = files.find(item => path.basename(item, '.jsonl').toLowerCase() === targetId);
2484
+ if (matchedFile && fs.existsSync(matchedFile)) {
2485
+ return matchedFile;
2486
+ }
2487
+ }
2488
+
2917
2489
  return '';
2918
2490
  }
2919
2491
 
@@ -3179,7 +2751,7 @@ async function cloneCodexSession(params = {}) {
3179
2751
  };
3180
2752
  }
3181
2753
 
3182
- function buildSessionMarkdown(payload) {
2754
+ function buildSessionMarkdown(payload) {
3183
2755
  const lines = [
3184
2756
  '# AI Session Export',
3185
2757
  '',
@@ -3208,75 +2780,37 @@ function buildSessionMarkdown(payload) {
3208
2780
  lines.push('');
3209
2781
  });
3210
2782
 
3211
- return lines.join('\n');
3212
- }
3213
-
3214
- function buildSessionPlainText(messages) {
3215
- if (!Array.isArray(messages) || messages.length === 0) {
3216
- return '';
3217
- }
3218
-
3219
- const lines = [];
3220
- messages.forEach((message) => {
3221
- const role = normalizeRole(message && message.role) || 'unknown';
3222
- const text = message && typeof message.text === 'string' ? message.text : '';
3223
- lines.push(role);
3224
- lines.push(text);
3225
- lines.push('');
3226
- });
3227
-
3228
- while (lines.length > 0 && lines[lines.length - 1] === '') {
3229
- lines.pop();
3230
- }
3231
-
3232
- return lines.join('\n');
3233
- }
3234
-
3235
- function parseMaxMessagesValue(value) {
3236
- if (value === Infinity) {
3237
- return Infinity;
3238
- }
3239
-
3240
- if (typeof value === 'string') {
3241
- const trimmed = value.trim();
3242
- if (!trimmed) {
3243
- return null;
3244
- }
3245
- const lower = trimmed.toLowerCase();
3246
- if (lower === 'all' || lower === 'infinity' || lower === 'inf') {
3247
- return Infinity;
3248
- }
3249
- const parsed = Number(trimmed);
3250
- if (Number.isFinite(parsed)) {
3251
- return parsed;
3252
- }
3253
- return null;
3254
- }
3255
-
3256
- if (Number.isFinite(value)) {
3257
- return value;
3258
- }
3259
- return null;
3260
- }
3261
-
3262
- function resolveMaxMessagesValue(value, fallback) {
3263
- const parsed = parseMaxMessagesValue(value);
3264
- if (parsed === null) {
3265
- return fallback;
3266
- }
3267
- if (parsed === Infinity) {
3268
- return Infinity;
3269
- }
3270
- return Math.max(1, Math.floor(parsed));
3271
- }
3272
-
3273
- function resolveStateMaxMessages(state) {
3274
- if (!state || typeof state !== 'object') {
3275
- return MAX_EXPORT_MESSAGES;
3276
- }
3277
-
3278
- return resolveMaxMessagesValue(state.maxMessages, MAX_EXPORT_MESSAGES);
3279
- }
2783
+ return lines.join('\n');
2784
+ }
2785
+
2786
+ function buildSessionPlainText(messages) {
2787
+ if (!Array.isArray(messages) || messages.length === 0) {
2788
+ return '';
2789
+ }
2790
+
2791
+ const lines = [];
2792
+ messages.forEach((message) => {
2793
+ const role = normalizeRole(message && message.role) || 'unknown';
2794
+ const text = message && typeof message.text === 'string' ? message.text : '';
2795
+ lines.push(role);
2796
+ lines.push(text);
2797
+ lines.push('');
2798
+ });
2799
+
2800
+ while (lines.length > 0 && lines[lines.length - 1] === '') {
2801
+ lines.pop();
2802
+ }
2803
+
2804
+ return lines.join('\n');
2805
+ }
2806
+
2807
+ function resolveStateMaxMessages(state) {
2808
+ if (!state || typeof state !== 'object') {
2809
+ return MAX_EXPORT_MESSAGES;
2810
+ }
2811
+
2812
+ return resolveMaxMessagesValue(state.maxMessages, MAX_EXPORT_MESSAGES);
2813
+ }
3280
2814
 
3281
2815
  function canAppendMessage(state) {
3282
2816
  const maxMessages = resolveStateMaxMessages(state);
@@ -3313,7 +2847,7 @@ function extractCodexMessageFromRecord(record, state, lineIndex = -1) {
3313
2847
  }
3314
2848
  }
3315
2849
 
3316
- function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
2850
+ function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
3317
2851
  if (record.timestamp) {
3318
2852
  state.updatedAt = toIsoTime(record.timestamp, state.updatedAt);
3319
2853
  }
@@ -3338,130 +2872,130 @@ function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
3338
2872
  recordLineIndex: Number.isInteger(lineIndex) ? lineIndex : -1
3339
2873
  });
3340
2874
  }
3341
- }
3342
- }
3343
-
3344
- function recordHasCodexMessage(record) {
3345
- if (!record || record.type !== 'response_item' || !record.payload) {
3346
- return false;
3347
- }
3348
- if (record.payload.type !== 'message') {
3349
- return false;
3350
- }
3351
- const role = normalizeRole(record.payload.role);
3352
- if (role !== 'user' && role !== 'assistant' && role !== 'system') {
3353
- return false;
3354
- }
3355
- const text = extractMessageText(record.payload.content);
3356
- return !!text;
3357
- }
3358
-
3359
- function recordHasClaudeMessage(record) {
3360
- if (!record) {
3361
- return false;
3362
- }
3363
- const role = normalizeRole(record.type);
3364
- if (role !== 'user' && role !== 'assistant' && role !== 'system') {
3365
- return false;
3366
- }
3367
- const content = record.message ? record.message.content : '';
3368
- const text = extractMessageText(content);
3369
- return !!text;
3370
- }
3371
-
3372
- function recordHasMessage(record, source) {
3373
- return source === 'codex'
3374
- ? recordHasCodexMessage(record)
3375
- : recordHasClaudeMessage(record);
3376
- }
3377
-
3378
- function extractMessagesFromRecords(records, source, options = {}) {
3379
- const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
3380
- const state = {
3381
- sessionId: '',
3382
- cwd: '',
3383
- updatedAt: '',
3384
- messages: [],
3385
- maxMessages,
3386
- truncated: false
3387
- };
3388
-
3389
- for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
3390
- const record = records[lineIndex];
3391
- if (source === 'codex') {
3392
- extractCodexMessageFromRecord(record, state, lineIndex);
3393
- } else {
3394
- extractClaudeMessageFromRecord(record, state, lineIndex);
3395
- }
3396
-
3397
- if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
3398
- for (let i = lineIndex + 1; i < records.length; i++) {
3399
- if (recordHasMessage(records[i], source)) {
3400
- state.truncated = true;
3401
- break;
3402
- }
3403
- }
3404
- break;
3405
- }
3406
- }
3407
-
3408
- return state;
3409
- }
2875
+ }
2876
+ }
3410
2877
 
3411
- async function extractMessagesFromFile(filePath, source, options = {}) {
3412
- const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
3413
- const state = {
3414
- sessionId: '',
3415
- cwd: '',
3416
- updatedAt: '',
3417
- messages: [],
3418
- maxMessages,
3419
- truncated: false
3420
- };
2878
+ function recordHasCodexMessage(record) {
2879
+ if (!record || record.type !== 'response_item' || !record.payload) {
2880
+ return false;
2881
+ }
2882
+ if (record.payload.type !== 'message') {
2883
+ return false;
2884
+ }
2885
+ const role = normalizeRole(record.payload.role);
2886
+ if (role !== 'user' && role !== 'assistant' && role !== 'system') {
2887
+ return false;
2888
+ }
2889
+ const text = extractMessageText(record.payload.content);
2890
+ return !!text;
2891
+ }
3421
2892
 
3422
- let stream;
3423
- let rl;
3424
- try {
3425
- stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
3426
- rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
3427
-
3428
- let lineIndex = 0;
3429
- let limitReached = false;
3430
- for await (const line of rl) {
3431
- const currentLineIndex = lineIndex;
3432
- lineIndex += 1;
2893
+ function recordHasClaudeMessage(record) {
2894
+ if (!record) {
2895
+ return false;
2896
+ }
2897
+ const role = normalizeRole(record.type);
2898
+ if (role !== 'user' && role !== 'assistant' && role !== 'system') {
2899
+ return false;
2900
+ }
2901
+ const content = record.message ? record.message.content : '';
2902
+ const text = extractMessageText(content);
2903
+ return !!text;
2904
+ }
2905
+
2906
+ function recordHasMessage(record, source) {
2907
+ return source === 'codex'
2908
+ ? recordHasCodexMessage(record)
2909
+ : recordHasClaudeMessage(record);
2910
+ }
2911
+
2912
+ function extractMessagesFromRecords(records, source, options = {}) {
2913
+ const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
2914
+ const state = {
2915
+ sessionId: '',
2916
+ cwd: '',
2917
+ updatedAt: '',
2918
+ messages: [],
2919
+ maxMessages,
2920
+ truncated: false
2921
+ };
2922
+
2923
+ for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
2924
+ const record = records[lineIndex];
2925
+ if (source === 'codex') {
2926
+ extractCodexMessageFromRecord(record, state, lineIndex);
2927
+ } else {
2928
+ extractClaudeMessageFromRecord(record, state, lineIndex);
2929
+ }
2930
+
2931
+ if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
2932
+ for (let i = lineIndex + 1; i < records.length; i++) {
2933
+ if (recordHasMessage(records[i], source)) {
2934
+ state.truncated = true;
2935
+ break;
2936
+ }
2937
+ }
2938
+ break;
2939
+ }
2940
+ }
2941
+
2942
+ return state;
2943
+ }
2944
+
2945
+ async function extractMessagesFromFile(filePath, source, options = {}) {
2946
+ const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
2947
+ const state = {
2948
+ sessionId: '',
2949
+ cwd: '',
2950
+ updatedAt: '',
2951
+ messages: [],
2952
+ maxMessages,
2953
+ truncated: false
2954
+ };
2955
+
2956
+ let stream;
2957
+ let rl;
2958
+ try {
2959
+ stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
2960
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
2961
+
2962
+ let lineIndex = 0;
2963
+ let limitReached = false;
2964
+ for await (const line of rl) {
2965
+ const currentLineIndex = lineIndex;
2966
+ lineIndex += 1;
3433
2967
 
3434
2968
  const trimmed = line.trim();
3435
2969
  if (!trimmed) continue;
3436
2970
 
3437
2971
  let record;
3438
- try {
3439
- record = JSON.parse(trimmed);
3440
- } catch (e) {
3441
- continue;
3442
- }
3443
-
3444
- if (limitReached) {
3445
- if (recordHasMessage(record, source)) {
3446
- state.truncated = true;
3447
- break;
3448
- }
3449
- continue;
3450
- }
3451
-
3452
- if (source === 'codex') {
3453
- extractCodexMessageFromRecord(record, state, currentLineIndex);
3454
- } else {
3455
- extractClaudeMessageFromRecord(record, state, currentLineIndex);
3456
- }
3457
-
3458
- if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
3459
- limitReached = true;
3460
- }
3461
- }
3462
- } catch (e) {
3463
- const fallbackRecords = readJsonlRecords(filePath);
3464
- return extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
2972
+ try {
2973
+ record = JSON.parse(trimmed);
2974
+ } catch (e) {
2975
+ continue;
2976
+ }
2977
+
2978
+ if (limitReached) {
2979
+ if (recordHasMessage(record, source)) {
2980
+ state.truncated = true;
2981
+ break;
2982
+ }
2983
+ continue;
2984
+ }
2985
+
2986
+ if (source === 'codex') {
2987
+ extractCodexMessageFromRecord(record, state, currentLineIndex);
2988
+ } else {
2989
+ extractClaudeMessageFromRecord(record, state, currentLineIndex);
2990
+ }
2991
+
2992
+ if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
2993
+ limitReached = true;
2994
+ }
2995
+ }
2996
+ } catch (e) {
2997
+ const fallbackRecords = readJsonlRecords(filePath);
2998
+ return extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
3465
2999
  } finally {
3466
3000
  if (rl) {
3467
3001
  try { rl.close(); } catch (e) {}
@@ -3474,7 +3008,7 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
3474
3008
  return state;
3475
3009
  }
3476
3010
 
3477
- async function readSessionDetail(params = {}) {
3011
+ async function readSessionDetail(params = {}) {
3478
3012
  const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
3479
3013
  if (!source) {
3480
3014
  return { error: 'Invalid source' };
@@ -3501,83 +3035,37 @@ async function readSessionDetail(params = {}) {
3501
3035
  const startIndex = Math.max(0, allMessages.length - messageLimit);
3502
3036
  const clippedMessages = allMessages.slice(startIndex);
3503
3037
 
3504
- return {
3505
- source,
3506
- sourceLabel,
3507
- sessionId,
3038
+ return {
3039
+ source,
3040
+ sourceLabel,
3041
+ sessionId,
3508
3042
  cwd: extracted.cwd || '',
3509
3043
  updatedAt: extracted.updatedAt || '',
3510
3044
  totalMessages: allMessages.length,
3511
3045
  clipped: allMessages.length > clippedMessages.length,
3512
3046
  messageLimit,
3513
3047
  messages: clippedMessages,
3514
- filePath
3515
- };
3516
- }
3517
-
3518
- async function readSessionPlain(params = {}) {
3519
- const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
3520
- if (!source) {
3521
- return { error: 'Invalid source' };
3522
- }
3523
-
3524
- const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
3525
- if (!filePath) {
3526
- return { error: 'Session file not found' };
3527
- }
3528
-
3529
- let extracted;
3530
- try {
3531
- extracted = await extractMessagesFromFile(filePath, source, { maxMessages: Infinity });
3532
- } catch (e) {
3533
- extracted = null;
3534
- }
3535
-
3536
- if (!extracted) {
3537
- return { error: 'Failed to parse session file' };
3538
- }
3539
-
3540
- if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
3541
- const fallbackRecords = readJsonlRecords(filePath);
3542
- if (fallbackRecords.length === 0) {
3543
- return { error: 'Session file is empty' };
3544
- }
3545
- extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
3546
- }
3547
-
3548
- const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
3549
- const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
3550
- const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
3551
- const text = buildSessionPlainText(messages);
3552
-
3553
- return {
3554
- source,
3555
- sourceLabel,
3556
- sessionId,
3557
- title: sessionId,
3558
- filePath,
3559
- text
3560
- };
3561
- }
3562
-
3563
- async function exportSessionData(params = {}) {
3564
- const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
3565
- if (!source) {
3566
- return { error: 'Invalid source' };
3567
- }
3568
-
3569
- const maxMessages = resolveMaxMessagesValue(params.maxMessages, MAX_EXPORT_MESSAGES);
3570
- const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
3571
- if (!filePath) {
3572
- return { error: 'Session file not found' };
3573
- }
3574
-
3575
- let extracted;
3576
- try {
3577
- extracted = await extractMessagesFromFile(filePath, source, { maxMessages });
3578
- } catch (e) {
3579
- extracted = null;
3580
- }
3048
+ filePath
3049
+ };
3050
+ }
3051
+
3052
+ async function readSessionPlain(params = {}) {
3053
+ const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
3054
+ if (!source) {
3055
+ return { error: 'Invalid source' };
3056
+ }
3057
+
3058
+ const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
3059
+ if (!filePath) {
3060
+ return { error: 'Session file not found' };
3061
+ }
3062
+
3063
+ let extracted;
3064
+ try {
3065
+ extracted = await extractMessagesFromFile(filePath, source, { maxMessages: Infinity });
3066
+ } catch (e) {
3067
+ extracted = null;
3068
+ }
3581
3069
 
3582
3070
  if (!extracted) {
3583
3071
  return { error: 'Failed to parse session file' };
@@ -3585,11 +3073,57 @@ async function exportSessionData(params = {}) {
3585
3073
 
3586
3074
  if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
3587
3075
  const fallbackRecords = readJsonlRecords(filePath);
3588
- if (fallbackRecords.length === 0) {
3589
- return { error: 'Session file is empty' };
3590
- }
3591
- extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
3592
- }
3076
+ if (fallbackRecords.length === 0) {
3077
+ return { error: 'Session file is empty' };
3078
+ }
3079
+ extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
3080
+ }
3081
+
3082
+ const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
3083
+ const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
3084
+ const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
3085
+ const text = buildSessionPlainText(messages);
3086
+
3087
+ return {
3088
+ source,
3089
+ sourceLabel,
3090
+ sessionId,
3091
+ title: sessionId,
3092
+ filePath,
3093
+ text
3094
+ };
3095
+ }
3096
+
3097
+ async function exportSessionData(params = {}) {
3098
+ const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
3099
+ if (!source) {
3100
+ return { error: 'Invalid source' };
3101
+ }
3102
+
3103
+ const maxMessages = resolveMaxMessagesValue(params.maxMessages, MAX_EXPORT_MESSAGES);
3104
+ const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
3105
+ if (!filePath) {
3106
+ return { error: 'Session file not found' };
3107
+ }
3108
+
3109
+ let extracted;
3110
+ try {
3111
+ extracted = await extractMessagesFromFile(filePath, source, { maxMessages });
3112
+ } catch (e) {
3113
+ extracted = null;
3114
+ }
3115
+
3116
+ if (!extracted) {
3117
+ return { error: 'Failed to parse session file' };
3118
+ }
3119
+
3120
+ if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
3121
+ const fallbackRecords = readJsonlRecords(filePath);
3122
+ if (fallbackRecords.length === 0) {
3123
+ return { error: 'Session file is empty' };
3124
+ }
3125
+ extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
3126
+ }
3593
3127
 
3594
3128
  extracted.messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
3595
3129
 
@@ -3601,29 +3135,29 @@ async function exportSessionData(params = {}) {
3601
3135
  }
3602
3136
 
3603
3137
  const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
3604
- const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
3605
- const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
3606
- const truncated = !!extracted.truncated;
3607
- const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
3608
- const markdown = buildSessionMarkdown({
3609
- sourceLabel,
3610
- sessionId,
3611
- updatedAt: extracted.updatedAt,
3612
- cwd: extracted.cwd,
3138
+ const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
3139
+ const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
3140
+ const truncated = !!extracted.truncated;
3141
+ const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
3142
+ const markdown = buildSessionMarkdown({
3143
+ sourceLabel,
3144
+ sessionId,
3145
+ updatedAt: extracted.updatedAt,
3146
+ cwd: extracted.cwd,
3613
3147
  filePath,
3614
3148
  messages: extracted.messages
3615
3149
  });
3616
3150
 
3617
- return {
3618
- source,
3619
- sourceLabel,
3620
- sessionId,
3621
- fileName: `${source}-session-${safeSessionId}.md`,
3622
- content: markdown,
3623
- truncated,
3624
- maxMessages: maxMessagesLabel
3625
- };
3626
- }
3151
+ return {
3152
+ source,
3153
+ sourceLabel,
3154
+ sessionId,
3155
+ fileName: `${source}-session-${safeSessionId}.md`,
3156
+ content: markdown,
3157
+ truncated,
3158
+ maxMessages: maxMessagesLabel
3159
+ };
3160
+ }
3627
3161
 
3628
3162
  function buildExportPayload(includeKeys) {
3629
3163
  const { config } = readConfigOrVirtualDefault();
@@ -3646,6 +3180,54 @@ function buildExportPayload(includeKeys) {
3646
3180
  };
3647
3181
  }
3648
3182
 
3183
+ function buildClaudeSharePayload(config = {}) {
3184
+ const apiKey = typeof config.apiKey === 'string' ? config.apiKey : '';
3185
+ const baseUrl = typeof config.baseUrl === 'string' ? config.baseUrl : '';
3186
+ const model = typeof config.model === 'string' ? config.model : '';
3187
+
3188
+ if (!baseUrl) return { error: 'Claude Base URL 未设置' };
3189
+ if (!apiKey) return { error: 'Claude API 密钥未设置' };
3190
+
3191
+ return {
3192
+ payload: {
3193
+ baseUrl: baseUrl.trim(),
3194
+ apiKey: apiKey.trim(),
3195
+ model: (model && model.trim()) || DEFAULT_CLAUDE_MODEL
3196
+ }
3197
+ };
3198
+ }
3199
+
3200
+ function buildProviderSharePayload(params = {}) {
3201
+ const name = typeof params.name === 'string' ? params.name.trim() : '';
3202
+ if (!name) {
3203
+ return { error: '缺少提供商名称' };
3204
+ }
3205
+
3206
+ const { config } = readConfigOrVirtualDefault();
3207
+ const providers = config.model_providers || {};
3208
+ const provider = providers[name];
3209
+ if (!provider || typeof provider !== 'object') {
3210
+ return { error: `提供商不存在: ${name}` };
3211
+ }
3212
+
3213
+ const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
3214
+ const apiKey = typeof provider.preferred_auth_method === 'string'
3215
+ ? provider.preferred_auth_method
3216
+ : '';
3217
+
3218
+ if (!baseUrl) {
3219
+ return { error: `提供商 ${name} 缺少 base_url` };
3220
+ }
3221
+
3222
+ return {
3223
+ payload: {
3224
+ name,
3225
+ baseUrl,
3226
+ apiKey
3227
+ }
3228
+ };
3229
+ }
3230
+
3649
3231
  function normalizeImportPayload(payload) {
3650
3232
  if (!payload || typeof payload !== 'object') {
3651
3233
  return { error: 'Invalid import payload' };
@@ -4464,7 +4046,7 @@ function applyToClaudeSettings(config = {}) {
4464
4046
  }
4465
4047
 
4466
4048
  const baseUrl = (config.baseUrl || 'https://open.bigmodel.cn/api/anthropic').trim();
4467
- const model = (config.model || 'glm-4.7').trim();
4049
+ const model = (config.model || DEFAULT_CLAUDE_MODEL).trim();
4468
4050
  const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
4469
4051
  if (!readResult.ok) {
4470
4052
  return { success: false, mode: 'settings-file', error: readResult.error };
@@ -4516,6 +4098,76 @@ function applyToClaudeSettings(config = {}) {
4516
4098
  }
4517
4099
  }
4518
4100
 
4101
+ function readClaudeSettingsInfo() {
4102
+ const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
4103
+ if (!readResult.ok) {
4104
+ return {
4105
+ error: readResult.error || '读取 Claude 配置失败',
4106
+ exists: !!readResult.exists,
4107
+ path: CLAUDE_SETTINGS_FILE
4108
+ };
4109
+ }
4110
+
4111
+ const settings = readResult.data || {};
4112
+ const env = (settings.env && typeof settings.env === 'object' && !Array.isArray(settings.env))
4113
+ ? settings.env
4114
+ : {};
4115
+
4116
+ return {
4117
+ exists: !!readResult.exists,
4118
+ path: CLAUDE_SETTINGS_FILE,
4119
+ apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
4120
+ baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
4121
+ model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
4122
+ env
4123
+ };
4124
+ }
4125
+
4126
+ // CLI: 一行写入 Claude Code 配置
4127
+ function cmdClaude(baseUrl, apiKey, model, silent = false) {
4128
+ const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
4129
+ const normalizedKey = typeof apiKey === 'string' ? apiKey.trim() : '';
4130
+ const normalizedModel = typeof model === 'string' && model.trim()
4131
+ ? model.trim()
4132
+ : DEFAULT_CLAUDE_MODEL;
4133
+
4134
+ if (!normalizedBaseUrl || !normalizedKey) {
4135
+ if (!silent) {
4136
+ console.error('用法: codexmate claude <BaseURL> <API密钥> [模型]');
4137
+ console.log('\n示例:');
4138
+ console.log(' codexmate claude https://open.bigmodel.cn/api/anthropic sk-ant-xxx glm-4.7');
4139
+ }
4140
+ throw new Error('BaseURL 和 API 密钥必填');
4141
+ }
4142
+
4143
+ const result = applyToClaudeSettings({
4144
+ baseUrl: normalizedBaseUrl,
4145
+ apiKey: normalizedKey,
4146
+ model: normalizedModel
4147
+ });
4148
+
4149
+ if (!result || result.success === false) {
4150
+ const message = (result && result.error) || '应用 Claude 配置失败';
4151
+ if (!silent) console.error('错误:', message);
4152
+ throw new Error(message);
4153
+ }
4154
+
4155
+ if (!silent) {
4156
+ console.log('✓ 已写入 Claude Code 配置');
4157
+ console.log(' Base URL:', normalizedBaseUrl);
4158
+ console.log(' 模型:', normalizedModel);
4159
+ if (result.targetPath) {
4160
+ console.log(' 目标文件:', result.targetPath);
4161
+ }
4162
+ if (result.backupPath) {
4163
+ console.log(' 已自动备份:', result.backupPath);
4164
+ }
4165
+ console.log();
4166
+ }
4167
+
4168
+ return result;
4169
+ }
4170
+
4519
4171
  function commandExists(command, args = '') {
4520
4172
  try {
4521
4173
  execSync(`${command} ${args}`, { stdio: 'ignore' });
@@ -4649,7 +4301,7 @@ async function cmdZip(targetPath, options = {}) {
4649
4301
  }
4650
4302
 
4651
4303
  // 解压(7-Zip 优先)
4652
- async function cmdUnzip(zipPath, outputDir) {
4304
+ async function cmdUnzip(zipPath, outputDir) {
4653
4305
  if (!zipPath) {
4654
4306
  console.error('用法: codexmate unzip <zip文件路径> [输出目录]');
4655
4307
  console.log('\n示例:');
@@ -4705,227 +4357,230 @@ async function cmdUnzip(zipPath, outputDir) {
4705
4357
  } catch (e) {
4706
4358
  console.error('解压失败:', e.message);
4707
4359
  process.exit(1);
4708
- }
4709
- }
4710
-
4711
- function resolveExportOutputPath(outputPath, defaultFileName) {
4712
- const fallback = path.resolve(process.cwd(), defaultFileName);
4713
- if (typeof outputPath !== 'string' || !outputPath.trim()) {
4714
- return fallback;
4715
- }
4716
-
4717
- const trimmed = outputPath.trim();
4718
- const resolved = path.resolve(trimmed);
4719
- const hasTrailingSep = /[\\\/]$/.test(trimmed);
4720
- if (hasTrailingSep) {
4721
- ensureDir(resolved);
4722
- return path.join(resolved, defaultFileName);
4723
- }
4724
-
4725
- if (fs.existsSync(resolved)) {
4726
- try {
4727
- const stat = fs.statSync(resolved);
4728
- if (stat.isDirectory()) {
4729
- return path.join(resolved, defaultFileName);
4730
- }
4731
- } catch (e) {}
4732
- }
4733
-
4734
- return resolved;
4735
- }
4736
-
4737
- function printExportSessionUsage() {
4738
- console.log('\n用法: codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
4739
- console.log('\n示例:');
4740
- console.log(' codexmate export-session --source codex --session-id 123456');
4741
- console.log(' codexmate export-session --source claude --file "~/.claude/projects/demo/session.jsonl"');
4742
- console.log(' codexmate export-session --source codex --session-id 123456 --max-messages=all');
4743
- }
4744
-
4745
- function parseExportSessionArgs(args = []) {
4746
- const options = {
4747
- source: '',
4748
- sessionId: '',
4749
- filePath: '',
4750
- output: '',
4751
- maxMessages: undefined
4752
- };
4753
- const errors = [];
4754
-
4755
- for (let i = 0; i < args.length; i++) {
4756
- const arg = args[i];
4757
- if (!arg) continue;
4758
-
4759
- if (arg.startsWith('--source=')) {
4760
- options.source = arg.slice('--source='.length);
4761
- continue;
4762
- }
4763
- if (arg === '--source') {
4764
- options.source = args[i + 1] || '';
4765
- i += 1;
4766
- continue;
4767
- }
4768
- if (arg.startsWith('--session-id=')) {
4769
- options.sessionId = arg.slice('--session-id='.length);
4770
- continue;
4771
- }
4772
- if (arg === '--session-id') {
4773
- options.sessionId = args[i + 1] || '';
4774
- i += 1;
4775
- continue;
4776
- }
4777
- if (arg.startsWith('--file=')) {
4778
- options.filePath = arg.slice('--file='.length);
4779
- continue;
4780
- }
4781
- if (arg === '--file') {
4782
- options.filePath = args[i + 1] || '';
4783
- i += 1;
4784
- continue;
4785
- }
4786
- if (arg.startsWith('--output=')) {
4787
- options.output = arg.slice('--output='.length);
4788
- continue;
4789
- }
4790
- if (arg === '--output') {
4791
- options.output = args[i + 1] || '';
4792
- i += 1;
4793
- continue;
4794
- }
4795
- if (arg.startsWith('--max-messages=')) {
4796
- options.maxMessages = arg.slice('--max-messages='.length);
4797
- continue;
4798
- }
4799
- if (arg === '--max-messages') {
4800
- options.maxMessages = args[i + 1] || '';
4801
- i += 1;
4802
- continue;
4803
- }
4804
-
4805
- errors.push(`未知参数: ${arg}`);
4806
- }
4807
-
4808
- const normalizedSource = options.source.trim().toLowerCase();
4809
- if (normalizedSource && normalizedSource !== 'codex' && normalizedSource !== 'claude') {
4810
- errors.push('参数 --source 仅支持 codex 或 claude');
4811
- }
4812
- options.source = normalizedSource;
4813
-
4814
- if (!options.source) {
4815
- errors.push('缺少 --source');
4816
- }
4817
-
4818
- if (!options.sessionId && !options.filePath) {
4819
- errors.push('必须指定 --session-id 或 --file');
4820
- }
4821
-
4822
- if (options.maxMessages !== undefined) {
4823
- const parsed = parseMaxMessagesValue(options.maxMessages);
4824
- if (parsed === null) {
4825
- errors.push('参数 --max-messages 无效');
4826
- } else {
4827
- options.maxMessages = parsed === Infinity ? Infinity : Math.max(1, Math.floor(parsed));
4828
- }
4829
- }
4830
-
4831
- return {
4832
- options,
4833
- error: errors.length > 0 ? errors.join(';') : ''
4834
- };
4835
- }
4836
-
4837
- async function cmdExportSession(args = []) {
4838
- const parsed = parseExportSessionArgs(args);
4839
- if (parsed.error) {
4840
- console.error('错误:', parsed.error);
4841
- printExportSessionUsage();
4842
- process.exit(1);
4843
- }
4844
-
4845
- const options = parsed.options;
4846
- const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
4847
- let result;
4848
- try {
4849
- result = await exportSessionData({
4850
- source: options.source,
4851
- sessionId: options.sessionId,
4852
- filePath: options.filePath,
4853
- maxMessages
4854
- });
4855
- } catch (e) {
4856
- console.error('导出失败:', e.message || e);
4857
- process.exit(1);
4858
- }
4859
-
4860
- if (result && result.error) {
4861
- console.error('导出失败:', result.error);
4862
- process.exit(1);
4863
- }
4864
-
4865
- const defaultFileName = (result && result.fileName)
4866
- ? result.fileName
4867
- : `${options.source}-session-${options.sessionId || Date.now()}.md`;
4868
- const outputPath = resolveExportOutputPath(options.output, defaultFileName);
4869
- ensureDir(path.dirname(outputPath));
4870
- fs.writeFileSync(outputPath, (result && result.content) ? result.content : '', 'utf-8');
4871
-
4872
- console.log('\n✓ 会话已导出:', outputPath);
4873
- if (result && result.truncated) {
4874
- const label = maxMessages === Infinity ? 'all' : maxMessages;
4875
- console.log(`! 已截断: 仅导出前 ${label} 条消息`);
4876
- console.log(' 可使用 --max-messages=all 导出完整内容');
4877
- }
4878
- console.log();
4879
- }
4880
-
4881
- function parseStartOptions(args = []) {
4882
- const options = { host: '' };
4883
- if (!Array.isArray(args)) {
4884
- return options;
4885
- }
4886
-
4887
- for (let i = 0; i < args.length; i++) {
4888
- const arg = args[i];
4889
- if (!arg) continue;
4890
- if (arg.startsWith('--host=')) {
4891
- options.host = arg.slice('--host='.length);
4892
- continue;
4893
- }
4894
- if (arg === '--host') {
4895
- options.host = args[i + 1] || '';
4896
- i += 1;
4897
- }
4898
- }
4899
-
4900
- return options;
4901
- }
4902
-
4903
- function isAnyAddressHost(host) {
4904
- return host === '0.0.0.0' || host === '::';
4905
- }
4906
-
4907
- function formatHostForUrl(host) {
4908
- const value = typeof host === 'string' ? host.trim() : '';
4909
- if (!value) return '';
4910
- if (value.startsWith('[') && value.endsWith(']')) {
4911
- return value;
4912
- }
4913
- if (value.includes(':')) {
4914
- return `[${value}]`;
4915
- }
4916
- return value;
4917
- }
4918
-
4919
- // 打开 Web UI
4920
- function cmdStart(options = {}) {
4921
- const htmlPath = path.join(__dirname, 'web-ui.html');
4922
- if (!fs.existsSync(htmlPath)) {
4923
- console.error('错误: web-ui.html 不存在');
4924
- process.exit(1);
4925
- }
4360
+ }
4361
+ }
4362
+
4363
+ function resolveExportOutputPath(outputPath, defaultFileName) {
4364
+ const fallback = path.resolve(process.cwd(), defaultFileName);
4365
+ if (typeof outputPath !== 'string' || !outputPath.trim()) {
4366
+ return fallback;
4367
+ }
4368
+
4369
+ const trimmed = outputPath.trim();
4370
+ const resolved = path.resolve(trimmed);
4371
+ const hasTrailingSep = /[\\\/]$/.test(trimmed);
4372
+ if (hasTrailingSep) {
4373
+ ensureDir(resolved);
4374
+ return path.join(resolved, defaultFileName);
4375
+ }
4376
+
4377
+ if (fs.existsSync(resolved)) {
4378
+ try {
4379
+ const stat = fs.statSync(resolved);
4380
+ if (stat.isDirectory()) {
4381
+ return path.join(resolved, defaultFileName);
4382
+ }
4383
+ } catch (e) {}
4384
+ }
4385
+
4386
+ return resolved;
4387
+ }
4388
+
4389
+ function printExportSessionUsage() {
4390
+ console.log('\n用法: codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
4391
+ console.log('\n示例:');
4392
+ console.log(' codexmate export-session --source codex --session-id 123456');
4393
+ console.log(' codexmate export-session --source claude --file "~/.claude/projects/demo/session.jsonl"');
4394
+ console.log(' codexmate export-session --source codex --session-id 123456 --max-messages=all');
4395
+ }
4396
+
4397
+ function parseExportSessionArgs(args = []) {
4398
+ const options = {
4399
+ source: '',
4400
+ sessionId: '',
4401
+ filePath: '',
4402
+ output: '',
4403
+ maxMessages: undefined
4404
+ };
4405
+ const errors = [];
4406
+
4407
+ for (let i = 0; i < args.length; i++) {
4408
+ const arg = args[i];
4409
+ if (!arg) continue;
4410
+
4411
+ if (arg.startsWith('--source=')) {
4412
+ options.source = arg.slice('--source='.length);
4413
+ continue;
4414
+ }
4415
+ if (arg === '--source') {
4416
+ options.source = args[i + 1] || '';
4417
+ i += 1;
4418
+ continue;
4419
+ }
4420
+ if (arg.startsWith('--session-id=')) {
4421
+ options.sessionId = arg.slice('--session-id='.length);
4422
+ continue;
4423
+ }
4424
+ if (arg === '--session-id') {
4425
+ options.sessionId = args[i + 1] || '';
4426
+ i += 1;
4427
+ continue;
4428
+ }
4429
+ if (arg.startsWith('--file=')) {
4430
+ options.filePath = arg.slice('--file='.length);
4431
+ continue;
4432
+ }
4433
+ if (arg === '--file') {
4434
+ options.filePath = args[i + 1] || '';
4435
+ i += 1;
4436
+ continue;
4437
+ }
4438
+ if (arg.startsWith('--output=')) {
4439
+ options.output = arg.slice('--output='.length);
4440
+ continue;
4441
+ }
4442
+ if (arg === '--output') {
4443
+ options.output = args[i + 1] || '';
4444
+ i += 1;
4445
+ continue;
4446
+ }
4447
+ if (arg.startsWith('--max-messages=')) {
4448
+ options.maxMessages = arg.slice('--max-messages='.length);
4449
+ continue;
4450
+ }
4451
+ if (arg === '--max-messages') {
4452
+ options.maxMessages = args[i + 1] || '';
4453
+ i += 1;
4454
+ continue;
4455
+ }
4456
+
4457
+ errors.push(`未知参数: ${arg}`);
4458
+ }
4459
+
4460
+ const normalizedSource = options.source.trim().toLowerCase();
4461
+ if (normalizedSource && normalizedSource !== 'codex' && normalizedSource !== 'claude') {
4462
+ errors.push('参数 --source 仅支持 codex 或 claude');
4463
+ }
4464
+ options.source = normalizedSource;
4465
+
4466
+ if (!options.source) {
4467
+ errors.push('缺少 --source');
4468
+ }
4469
+
4470
+ if (!options.sessionId && !options.filePath) {
4471
+ errors.push('必须指定 --session-id 或 --file');
4472
+ }
4473
+
4474
+ if (options.maxMessages !== undefined) {
4475
+ const parsed = parseMaxMessagesValue(options.maxMessages);
4476
+ if (parsed === null) {
4477
+ errors.push('参数 --max-messages 无效');
4478
+ } else {
4479
+ options.maxMessages = parsed === Infinity ? Infinity : Math.max(1, Math.floor(parsed));
4480
+ }
4481
+ }
4482
+
4483
+ return {
4484
+ options,
4485
+ error: errors.length > 0 ? errors.join(';') : ''
4486
+ };
4487
+ }
4488
+
4489
+ async function cmdExportSession(args = []) {
4490
+ const parsed = parseExportSessionArgs(args);
4491
+ if (parsed.error) {
4492
+ console.error('错误:', parsed.error);
4493
+ printExportSessionUsage();
4494
+ process.exit(1);
4495
+ }
4496
+
4497
+ const options = parsed.options;
4498
+ const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
4499
+ let result;
4500
+ try {
4501
+ result = await exportSessionData({
4502
+ source: options.source,
4503
+ sessionId: options.sessionId,
4504
+ filePath: options.filePath,
4505
+ maxMessages
4506
+ });
4507
+ } catch (e) {
4508
+ console.error('导出失败:', e.message || e);
4509
+ process.exit(1);
4510
+ }
4511
+
4512
+ if (result && result.error) {
4513
+ console.error('导出失败:', result.error);
4514
+ process.exit(1);
4515
+ }
4516
+
4517
+ const defaultFileName = (result && result.fileName)
4518
+ ? result.fileName
4519
+ : `${options.source}-session-${options.sessionId || Date.now()}.md`;
4520
+ const outputPath = resolveExportOutputPath(options.output, defaultFileName);
4521
+ ensureDir(path.dirname(outputPath));
4522
+ fs.writeFileSync(outputPath, (result && result.content) ? result.content : '', 'utf-8');
4523
+
4524
+ console.log('\n✓ 会话已导出:', outputPath);
4525
+ if (result && result.truncated) {
4526
+ const label = maxMessages === Infinity ? 'all' : maxMessages;
4527
+ console.log(`! 已截断: 仅导出前 ${label} 条消息`);
4528
+ console.log(' 可使用 --max-messages=all 导出完整内容');
4529
+ }
4530
+ console.log();
4531
+ }
4532
+
4533
+ function parseStartOptions(args = []) {
4534
+ const options = { host: '' };
4535
+ if (!Array.isArray(args)) {
4536
+ return options;
4537
+ }
4538
+
4539
+ for (let i = 0; i < args.length; i++) {
4540
+ const arg = args[i];
4541
+ if (!arg) continue;
4542
+ if (arg.startsWith('--host=')) {
4543
+ options.host = arg.slice('--host='.length);
4544
+ continue;
4545
+ }
4546
+ if (arg === '--host') {
4547
+ options.host = args[i + 1] || '';
4548
+ i += 1;
4549
+ }
4550
+ }
4551
+
4552
+ return options;
4553
+ }
4554
+
4555
+ function isAnyAddressHost(host) {
4556
+ return host === '0.0.0.0' || host === '::';
4557
+ }
4558
+
4559
+ function formatHostForUrl(host) {
4560
+ const value = typeof host === 'string' ? host.trim() : '';
4561
+ if (!value) return '';
4562
+ if (value.startsWith('[') && value.endsWith(']')) {
4563
+ return value;
4564
+ }
4565
+ if (value.includes(':')) {
4566
+ return `[${value}]`;
4567
+ }
4568
+ return value;
4569
+ }
4570
+
4571
+ // 打开 Web UI
4572
+ function cmdStart(options = {}) {
4573
+ const htmlPath = path.join(__dirname, 'web-ui.html');
4574
+ const assetsDir = path.join(__dirname, 'res');
4575
+ const webDir = path.join(__dirname, 'web-ui');
4576
+ if (!fs.existsSync(htmlPath)) {
4577
+ console.error('错误: web-ui.html 不存在');
4578
+ process.exit(1);
4579
+ }
4926
4580
 
4927
4581
  const server = http.createServer((req, res) => {
4928
- if (req.url === '/api') {
4582
+ const requestPath = (req.url || '/').split('?')[0];
4583
+ if (requestPath === '/api') {
4929
4584
  let body = '';
4930
4585
  req.on('data', chunk => body += chunk);
4931
4586
  req.on('end', async () => {
@@ -4937,9 +4592,11 @@ function cmdStart(options = {}) {
4937
4592
  case 'status':
4938
4593
  const statusConfigResult = readConfigOrVirtualDefault();
4939
4594
  const config = statusConfigResult.config;
4595
+ const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
4940
4596
  result = {
4941
4597
  provider: config.model_provider || '未设置',
4942
4598
  model: config.model || '未设置',
4599
+ serviceTier,
4943
4600
  configReady: !statusConfigResult.isVirtual,
4944
4601
  configNotice: statusConfigResult.reason || '',
4945
4602
  initNotice: consumeInitNotice()
@@ -5039,9 +4696,18 @@ function cmdStart(options = {}) {
5039
4696
  cmdDeleteModel(params.model, true);
5040
4697
  result = { success: true };
5041
4698
  break;
4699
+ case 'get-claude-settings':
4700
+ result = readClaudeSettingsInfo();
4701
+ break;
5042
4702
  case 'apply-claude-config':
5043
4703
  result = applyToClaudeSettings(params.config);
5044
4704
  break;
4705
+ case 'export-claude-share':
4706
+ result = buildClaudeSharePayload(params && params.config ? params.config : {});
4707
+ break;
4708
+ case 'export-provider':
4709
+ result = buildProviderSharePayload(params || {});
4710
+ break;
5045
4711
  case 'export-config':
5046
4712
  result = {
5047
4713
  data: buildExportPayload(!!params.includeKeys)
@@ -5075,18 +4741,18 @@ function cmdStart(options = {}) {
5075
4741
  case 'delete-session':
5076
4742
  result = await deleteSessionData(params || {});
5077
4743
  break;
5078
- case 'clone-session':
5079
- result = await cloneCodexSession(params || {});
5080
- break;
5081
- case 'session-detail':
5082
- result = await readSessionDetail(params);
5083
- break;
5084
- case 'session-plain':
5085
- result = await readSessionPlain(params);
5086
- break;
5087
- default:
5088
- result = { error: '未知操作' };
5089
- }
4744
+ case 'clone-session':
4745
+ result = await cloneCodexSession(params || {});
4746
+ break;
4747
+ case 'session-detail':
4748
+ result = await readSessionDetail(params);
4749
+ break;
4750
+ case 'session-plain':
4751
+ result = await readSessionPlain(params);
4752
+ break;
4753
+ default:
4754
+ result = { error: '未知操作' };
4755
+ }
5090
4756
 
5091
4757
  res.writeHead(200, { 'Content-Type': 'application/json' });
5092
4758
  res.end(JSON.stringify(result));
@@ -5095,6 +4761,50 @@ function cmdStart(options = {}) {
5095
4761
  res.end(JSON.stringify({ error: e.message }));
5096
4762
  }
5097
4763
  });
4764
+ } else if (requestPath.startsWith('/web-ui/')) {
4765
+ const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
4766
+ const filePath = path.join(__dirname, normalized);
4767
+ if (!isPathInside(filePath, webDir)) {
4768
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
4769
+ res.end('Forbidden');
4770
+ return;
4771
+ }
4772
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
4773
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
4774
+ res.end('Not Found');
4775
+ return;
4776
+ }
4777
+ const ext = path.extname(filePath).toLowerCase();
4778
+ const mime = ext === '.js' || ext === '.mjs'
4779
+ ? 'application/javascript; charset=utf-8'
4780
+ : ext === '.css'
4781
+ ? 'text/css; charset=utf-8'
4782
+ : ext === '.json'
4783
+ ? 'application/json; charset=utf-8'
4784
+ : 'application/octet-stream';
4785
+ res.writeHead(200, { 'Content-Type': mime });
4786
+ fs.createReadStream(filePath).pipe(res);
4787
+ } else if (requestPath.startsWith('/res/')) {
4788
+ const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
4789
+ const filePath = path.join(__dirname, normalized);
4790
+ if (!isPathInside(filePath, assetsDir)) {
4791
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
4792
+ res.end('Forbidden');
4793
+ return;
4794
+ }
4795
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
4796
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
4797
+ res.end('Not Found');
4798
+ return;
4799
+ }
4800
+ const ext = path.extname(filePath).toLowerCase();
4801
+ const mime = ext === '.js'
4802
+ ? 'application/javascript; charset=utf-8'
4803
+ : ext === '.json'
4804
+ ? 'application/json; charset=utf-8'
4805
+ : 'application/octet-stream';
4806
+ res.writeHead(200, { 'Content-Type': mime });
4807
+ fs.createReadStream(filePath).pipe(res);
5098
4808
  } else {
5099
4809
  const html = fs.readFileSync(htmlPath, 'utf-8');
5100
4810
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
@@ -5102,28 +4812,28 @@ function cmdStart(options = {}) {
5102
4812
  }
5103
4813
  });
5104
4814
 
5105
- const port = resolveWebPort();
5106
- const host = resolveWebHost(options);
5107
- const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
5108
- const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
5109
- server.listen(port, host, () => {
5110
- console.log('\n✓ Web UI 已启动:', openUrl);
5111
- if (host && host !== openHost) {
5112
- console.log(' 监听地址:', host);
5113
- }
5114
- console.log(' 按 Ctrl+C 退出\n');
5115
- if (isAnyAddressHost(host)) {
5116
- console.warn('! 安全提示: 当前监听所有网卡(无鉴权)。');
5117
- console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
5118
- }
5119
-
5120
- // 打开浏览器
5121
- const platform = process.platform;
5122
- let command;
5123
- const url = openUrl;
5124
-
5125
- if (platform === 'win32') {
5126
- command = `start "" "${url}"`;
4815
+ const port = resolveWebPort();
4816
+ const host = resolveWebHost(options);
4817
+ const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
4818
+ const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
4819
+ server.listen(port, host, () => {
4820
+ console.log('\n✓ Web UI 已启动:', openUrl);
4821
+ if (host && host !== openHost) {
4822
+ console.log(' 监听地址:', host);
4823
+ }
4824
+ console.log(' 按 Ctrl+C 退出\n');
4825
+ if (isAnyAddressHost(host)) {
4826
+ console.warn('! 安全提示: 当前监听所有网卡(无鉴权)。');
4827
+ console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
4828
+ }
4829
+
4830
+ // 打开浏览器
4831
+ const platform = process.platform;
4832
+ let command;
4833
+ const url = openUrl;
4834
+
4835
+ if (platform === 'win32') {
4836
+ command = `start "" "${url}"`;
5127
4837
  } else if (platform === 'darwin') {
5128
4838
  command = `open "${url}"`;
5129
4839
  } else {
@@ -5136,46 +4846,46 @@ function cmdStart(options = {}) {
5136
4846
  if (error) console.warn('无法自动打开浏览器,请手动访问:', url);
5137
4847
  });
5138
4848
  }
5139
- });
5140
- }
5141
-
5142
- async function cmdCodex(args = []) {
5143
- const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
5144
- const hasYolo = extraArgs.includes('--yolo');
5145
- const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
5146
-
5147
- return new Promise((resolve, reject) => {
5148
- const child = spawn('codex', finalArgs, {
5149
- stdio: 'inherit',
5150
- shell: process.platform === 'win32'
5151
- });
5152
-
5153
- child.on('error', (err) => {
5154
- reject(new Error(`无法启动 codex,请确认已安装并在 PATH 中: ${err.message}`));
5155
- });
5156
-
5157
- child.on('exit', (code, signal) => {
5158
- if (typeof code === 'number') {
5159
- resolve(code);
5160
- return;
5161
- }
5162
- if (signal === 'SIGINT') {
5163
- resolve(130);
5164
- return;
5165
- }
5166
- if (signal === 'SIGTERM') {
5167
- resolve(143);
5168
- return;
5169
- }
5170
- resolve(1);
5171
- });
5172
- });
5173
- }
5174
-
5175
- // ============================================================================
5176
- // 主程序
5177
- // ============================================================================
5178
- async function main() {
4849
+ });
4850
+ }
4851
+
4852
+ async function cmdCodex(args = []) {
4853
+ const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
4854
+ const hasYolo = extraArgs.includes('--yolo');
4855
+ const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
4856
+
4857
+ return new Promise((resolve, reject) => {
4858
+ const child = spawn('codex', finalArgs, {
4859
+ stdio: 'inherit',
4860
+ shell: process.platform === 'win32'
4861
+ });
4862
+
4863
+ child.on('error', (err) => {
4864
+ reject(new Error(`无法启动 codex,请确认已安装并在 PATH 中: ${err.message}`));
4865
+ });
4866
+
4867
+ child.on('exit', (code, signal) => {
4868
+ if (typeof code === 'number') {
4869
+ resolve(code);
4870
+ return;
4871
+ }
4872
+ if (signal === 'SIGINT') {
4873
+ resolve(130);
4874
+ return;
4875
+ }
4876
+ if (signal === 'SIGTERM') {
4877
+ resolve(143);
4878
+ return;
4879
+ }
4880
+ resolve(1);
4881
+ });
4882
+ });
4883
+ }
4884
+
4885
+ // ============================================================================
4886
+ // 主程序
4887
+ // ============================================================================
4888
+ async function main() {
5179
4889
  const bootstrap = ensureManagedConfigBootstrap();
5180
4890
  if (bootstrap && bootstrap.notice) {
5181
4891
  console.log(`\n[Init] ${bootstrap.notice}`);
@@ -5193,16 +4903,17 @@ async function main() {
5193
4903
  console.log(' codexmate use <模型> 切换模型');
5194
4904
  console.log(' codexmate add <名称> <URL> [密钥]');
5195
4905
  console.log(' codexmate delete <名称> 删除提供商');
5196
- console.log(' codexmate add-model <模型> 添加模型');
5197
- console.log(' codexmate delete-model <模型> 删除模型');
5198
- console.log(' codexmate start [--host <HOST>] 启动 Web 界面');
5199
- console.log(' codexmate codex [参数...] 等同于 codex --yolo');
5200
- console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
5201
- console.log(' codexmate zip <路径> [--max:级别] 压缩(7-Zip 优先)');
5202
- console.log(' codexmate unzip <zip文件> [输出目录] 解压(7-Zip 优先)');
5203
- console.log('');
5204
- process.exit(0);
5205
- }
4906
+ console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
4907
+ console.log(' codexmate add-model <模型> 添加模型');
4908
+ console.log(' codexmate delete-model <模型> 删除模型');
4909
+ console.log(' codexmate run [--host <HOST>] 启动 Web 界面');
4910
+ console.log(' codexmate codex [参数...] 等同于 codex --yolo');
4911
+ console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
4912
+ console.log(' codexmate zip <路径> [--max:级别] 压缩(7-Zip 优先)');
4913
+ console.log(' codexmate unzip <zip文件> [输出目录] 解压(7-Zip 优先)');
4914
+ console.log('');
4915
+ process.exit(0);
4916
+ }
5206
4917
 
5207
4918
  const command = args[0];
5208
4919
 
@@ -5215,18 +4926,23 @@ async function main() {
5215
4926
  case 'use': cmdUseModel(args[1]); break;
5216
4927
  case 'add': cmdAdd(args[1], args[2], args[3]); break;
5217
4928
  case 'delete': cmdDelete(args[1]); break;
5218
- case 'add-model': cmdAddModel(args[1]); break;
5219
- case 'delete-model': cmdDeleteModel(args[1]); break;
5220
- case 'start': cmdStart(parseStartOptions(args.slice(1))); break;
5221
- case 'codex': {
5222
- const exitCode = await cmdCodex(args.slice(1));
5223
- process.exit(exitCode);
5224
- break;
5225
- }
5226
- case 'export-session': await cmdExportSession(args.slice(1)); break;
5227
- case 'zip': {
5228
- // 解析 --max:N 参数
5229
- const zipOptions = {};
4929
+ case 'claude': cmdClaude(args[1], args[2], args[3]); break;
4930
+ case 'add-model': cmdAddModel(args[1]); break;
4931
+ case 'delete-model': cmdDeleteModel(args[1]); break;
4932
+ case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
4933
+ case 'start':
4934
+ console.error('错误: 命令已更名为 "run",请使用: codexmate run');
4935
+ process.exit(1);
4936
+ break;
4937
+ case 'codex': {
4938
+ const exitCode = await cmdCodex(args.slice(1));
4939
+ process.exit(exitCode);
4940
+ break;
4941
+ }
4942
+ case 'export-session': await cmdExportSession(args.slice(1)); break;
4943
+ case 'zip': {
4944
+ // 解析 --max:N 参数
4945
+ const zipOptions = {};
5230
4946
  let targetPath = null;
5231
4947
  for (let i = 1; i < args.length; i++) {
5232
4948
  const arg = args[i];
@@ -5251,4 +4967,3 @@ main().catch((err) => {
5251
4967
  console.error('错误:', err && err.message ? err.message : err);
5252
4968
  process.exit(1);
5253
4969
  });
5254
-