codexmate 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -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 } = 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;
@@ -49,7 +92,6 @@ 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,252 +774,6 @@ 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}`;
1000
- }
1001
- return `${trimmed}/v1/${safeSuffix}`;
1002
- }
1003
-
1004
- function buildModelsProbeUrl(baseUrl) {
1005
- return joinApiUrl(baseUrl, 'models');
1006
- }
1007
-
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
- }
1013
-
1014
- function buildModelProbeSpec(provider, modelName, baseUrl) {
1015
- const model = typeof modelName === 'string' ? modelName.trim() : '';
1016
- if (!model) return null;
1017
-
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
- };
1029
- }
1030
-
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
- };
1041
- }
1042
-
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'
1066
- };
1067
- if (options.apiKey) {
1068
- headers['Authorization'] = `Bearer ${options.apiKey}`;
1069
- }
1070
-
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
- }
1083
- });
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
- });
1092
- });
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
777
  async function runRemoteHealthCheck(provider, modelName, options = {}) {
1225
778
  const issues = [];
1226
779
  const results = {};
@@ -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;
@@ -3179,7 +2663,7 @@ async function cloneCodexSession(params = {}) {
3179
2663
  };
3180
2664
  }
3181
2665
 
3182
- function buildSessionMarkdown(payload) {
2666
+ function buildSessionMarkdown(payload) {
3183
2667
  const lines = [
3184
2668
  '# AI Session Export',
3185
2669
  '',
@@ -3208,75 +2692,37 @@ function buildSessionMarkdown(payload) {
3208
2692
  lines.push('');
3209
2693
  });
3210
2694
 
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
- }
2695
+ return lines.join('\n');
2696
+ }
2697
+
2698
+ function buildSessionPlainText(messages) {
2699
+ if (!Array.isArray(messages) || messages.length === 0) {
2700
+ return '';
2701
+ }
2702
+
2703
+ const lines = [];
2704
+ messages.forEach((message) => {
2705
+ const role = normalizeRole(message && message.role) || 'unknown';
2706
+ const text = message && typeof message.text === 'string' ? message.text : '';
2707
+ lines.push(role);
2708
+ lines.push(text);
2709
+ lines.push('');
2710
+ });
2711
+
2712
+ while (lines.length > 0 && lines[lines.length - 1] === '') {
2713
+ lines.pop();
2714
+ }
2715
+
2716
+ return lines.join('\n');
2717
+ }
2718
+
2719
+ function resolveStateMaxMessages(state) {
2720
+ if (!state || typeof state !== 'object') {
2721
+ return MAX_EXPORT_MESSAGES;
2722
+ }
2723
+
2724
+ return resolveMaxMessagesValue(state.maxMessages, MAX_EXPORT_MESSAGES);
2725
+ }
3280
2726
 
3281
2727
  function canAppendMessage(state) {
3282
2728
  const maxMessages = resolveStateMaxMessages(state);
@@ -3313,7 +2759,7 @@ function extractCodexMessageFromRecord(record, state, lineIndex = -1) {
3313
2759
  }
3314
2760
  }
3315
2761
 
3316
- function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
2762
+ function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
3317
2763
  if (record.timestamp) {
3318
2764
  state.updatedAt = toIsoTime(record.timestamp, state.updatedAt);
3319
2765
  }
@@ -3338,130 +2784,130 @@ function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
3338
2784
  recordLineIndex: Number.isInteger(lineIndex) ? lineIndex : -1
3339
2785
  });
3340
2786
  }
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
- }
3410
-
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
- };
3421
-
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;
2787
+ }
2788
+ }
2789
+
2790
+ function recordHasCodexMessage(record) {
2791
+ if (!record || record.type !== 'response_item' || !record.payload) {
2792
+ return false;
2793
+ }
2794
+ if (record.payload.type !== 'message') {
2795
+ return false;
2796
+ }
2797
+ const role = normalizeRole(record.payload.role);
2798
+ if (role !== 'user' && role !== 'assistant' && role !== 'system') {
2799
+ return false;
2800
+ }
2801
+ const text = extractMessageText(record.payload.content);
2802
+ return !!text;
2803
+ }
2804
+
2805
+ function recordHasClaudeMessage(record) {
2806
+ if (!record) {
2807
+ return false;
2808
+ }
2809
+ const role = normalizeRole(record.type);
2810
+ if (role !== 'user' && role !== 'assistant' && role !== 'system') {
2811
+ return false;
2812
+ }
2813
+ const content = record.message ? record.message.content : '';
2814
+ const text = extractMessageText(content);
2815
+ return !!text;
2816
+ }
2817
+
2818
+ function recordHasMessage(record, source) {
2819
+ return source === 'codex'
2820
+ ? recordHasCodexMessage(record)
2821
+ : recordHasClaudeMessage(record);
2822
+ }
2823
+
2824
+ function extractMessagesFromRecords(records, source, options = {}) {
2825
+ const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
2826
+ const state = {
2827
+ sessionId: '',
2828
+ cwd: '',
2829
+ updatedAt: '',
2830
+ messages: [],
2831
+ maxMessages,
2832
+ truncated: false
2833
+ };
2834
+
2835
+ for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
2836
+ const record = records[lineIndex];
2837
+ if (source === 'codex') {
2838
+ extractCodexMessageFromRecord(record, state, lineIndex);
2839
+ } else {
2840
+ extractClaudeMessageFromRecord(record, state, lineIndex);
2841
+ }
2842
+
2843
+ if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
2844
+ for (let i = lineIndex + 1; i < records.length; i++) {
2845
+ if (recordHasMessage(records[i], source)) {
2846
+ state.truncated = true;
2847
+ break;
2848
+ }
2849
+ }
2850
+ break;
2851
+ }
2852
+ }
2853
+
2854
+ return state;
2855
+ }
2856
+
2857
+ async function extractMessagesFromFile(filePath, source, options = {}) {
2858
+ const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
2859
+ const state = {
2860
+ sessionId: '',
2861
+ cwd: '',
2862
+ updatedAt: '',
2863
+ messages: [],
2864
+ maxMessages,
2865
+ truncated: false
2866
+ };
2867
+
2868
+ let stream;
2869
+ let rl;
2870
+ try {
2871
+ stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
2872
+ rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
2873
+
2874
+ let lineIndex = 0;
2875
+ let limitReached = false;
2876
+ for await (const line of rl) {
2877
+ const currentLineIndex = lineIndex;
2878
+ lineIndex += 1;
2879
+
2880
+ const trimmed = line.trim();
2881
+ if (!trimmed) continue;
2882
+
2883
+ let record;
2884
+ try {
2885
+ record = JSON.parse(trimmed);
2886
+ } catch (e) {
2887
+ continue;
2888
+ }
3433
2889
 
3434
- const trimmed = line.trim();
3435
- if (!trimmed) continue;
2890
+ if (limitReached) {
2891
+ if (recordHasMessage(record, source)) {
2892
+ state.truncated = true;
2893
+ break;
2894
+ }
2895
+ continue;
2896
+ }
3436
2897
 
3437
- 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 });
2898
+ if (source === 'codex') {
2899
+ extractCodexMessageFromRecord(record, state, currentLineIndex);
2900
+ } else {
2901
+ extractClaudeMessageFromRecord(record, state, currentLineIndex);
2902
+ }
2903
+
2904
+ if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
2905
+ limitReached = true;
2906
+ }
2907
+ }
2908
+ } catch (e) {
2909
+ const fallbackRecords = readJsonlRecords(filePath);
2910
+ return extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
3465
2911
  } finally {
3466
2912
  if (rl) {
3467
2913
  try { rl.close(); } catch (e) {}
@@ -3474,7 +2920,7 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
3474
2920
  return state;
3475
2921
  }
3476
2922
 
3477
- async function readSessionDetail(params = {}) {
2923
+ async function readSessionDetail(params = {}) {
3478
2924
  const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
3479
2925
  if (!source) {
3480
2926
  return { error: 'Invalid source' };
@@ -3501,83 +2947,83 @@ async function readSessionDetail(params = {}) {
3501
2947
  const startIndex = Math.max(0, allMessages.length - messageLimit);
3502
2948
  const clippedMessages = allMessages.slice(startIndex);
3503
2949
 
3504
- return {
3505
- source,
3506
- sourceLabel,
3507
- sessionId,
2950
+ return {
2951
+ source,
2952
+ sourceLabel,
2953
+ sessionId,
3508
2954
  cwd: extracted.cwd || '',
3509
2955
  updatedAt: extracted.updatedAt || '',
3510
2956
  totalMessages: allMessages.length,
3511
2957
  clipped: allMessages.length > clippedMessages.length,
3512
2958
  messageLimit,
3513
2959
  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
- }
2960
+ filePath
2961
+ };
2962
+ }
2963
+
2964
+ async function readSessionPlain(params = {}) {
2965
+ const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
2966
+ if (!source) {
2967
+ return { error: 'Invalid source' };
2968
+ }
2969
+
2970
+ const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
2971
+ if (!filePath) {
2972
+ return { error: 'Session file not found' };
2973
+ }
2974
+
2975
+ let extracted;
2976
+ try {
2977
+ extracted = await extractMessagesFromFile(filePath, source, { maxMessages: Infinity });
2978
+ } catch (e) {
2979
+ extracted = null;
2980
+ }
2981
+
2982
+ if (!extracted) {
2983
+ return { error: 'Failed to parse session file' };
2984
+ }
2985
+
2986
+ if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
2987
+ const fallbackRecords = readJsonlRecords(filePath);
2988
+ if (fallbackRecords.length === 0) {
2989
+ return { error: 'Session file is empty' };
2990
+ }
2991
+ extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
2992
+ }
2993
+
2994
+ const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
2995
+ const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
2996
+ const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
2997
+ const text = buildSessionPlainText(messages);
2998
+
2999
+ return {
3000
+ source,
3001
+ sourceLabel,
3002
+ sessionId,
3003
+ title: sessionId,
3004
+ filePath,
3005
+ text
3006
+ };
3007
+ }
3008
+
3009
+ async function exportSessionData(params = {}) {
3010
+ const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
3011
+ if (!source) {
3012
+ return { error: 'Invalid source' };
3013
+ }
3014
+
3015
+ const maxMessages = resolveMaxMessagesValue(params.maxMessages, MAX_EXPORT_MESSAGES);
3016
+ const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
3017
+ if (!filePath) {
3018
+ return { error: 'Session file not found' };
3019
+ }
3020
+
3021
+ let extracted;
3022
+ try {
3023
+ extracted = await extractMessagesFromFile(filePath, source, { maxMessages });
3024
+ } catch (e) {
3025
+ extracted = null;
3026
+ }
3581
3027
 
3582
3028
  if (!extracted) {
3583
3029
  return { error: 'Failed to parse session file' };
@@ -3585,11 +3031,11 @@ async function exportSessionData(params = {}) {
3585
3031
 
3586
3032
  if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
3587
3033
  const fallbackRecords = readJsonlRecords(filePath);
3588
- if (fallbackRecords.length === 0) {
3589
- return { error: 'Session file is empty' };
3590
- }
3591
- extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
3592
- }
3034
+ if (fallbackRecords.length === 0) {
3035
+ return { error: 'Session file is empty' };
3036
+ }
3037
+ extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
3038
+ }
3593
3039
 
3594
3040
  extracted.messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
3595
3041
 
@@ -3601,29 +3047,29 @@ async function exportSessionData(params = {}) {
3601
3047
  }
3602
3048
 
3603
3049
  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,
3050
+ const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
3051
+ const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
3052
+ const truncated = !!extracted.truncated;
3053
+ const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
3054
+ const markdown = buildSessionMarkdown({
3055
+ sourceLabel,
3056
+ sessionId,
3057
+ updatedAt: extracted.updatedAt,
3058
+ cwd: extracted.cwd,
3613
3059
  filePath,
3614
3060
  messages: extracted.messages
3615
3061
  });
3616
3062
 
3617
- return {
3618
- source,
3619
- sourceLabel,
3620
- sessionId,
3621
- fileName: `${source}-session-${safeSessionId}.md`,
3622
- content: markdown,
3623
- truncated,
3624
- maxMessages: maxMessagesLabel
3625
- };
3626
- }
3063
+ return {
3064
+ source,
3065
+ sourceLabel,
3066
+ sessionId,
3067
+ fileName: `${source}-session-${safeSessionId}.md`,
3068
+ content: markdown,
3069
+ truncated,
3070
+ maxMessages: maxMessagesLabel
3071
+ };
3072
+ }
3627
3073
 
3628
3074
  function buildExportPayload(includeKeys) {
3629
3075
  const { config } = readConfigOrVirtualDefault();
@@ -3646,6 +3092,54 @@ function buildExportPayload(includeKeys) {
3646
3092
  };
3647
3093
  }
3648
3094
 
3095
+ function buildClaudeSharePayload(config = {}) {
3096
+ const apiKey = typeof config.apiKey === 'string' ? config.apiKey : '';
3097
+ const baseUrl = typeof config.baseUrl === 'string' ? config.baseUrl : '';
3098
+ const model = typeof config.model === 'string' ? config.model : '';
3099
+
3100
+ if (!baseUrl) return { error: 'Claude Base URL 未设置' };
3101
+ if (!apiKey) return { error: 'Claude API 密钥未设置' };
3102
+
3103
+ return {
3104
+ payload: {
3105
+ baseUrl: baseUrl.trim(),
3106
+ apiKey: apiKey.trim(),
3107
+ model: (model && model.trim()) || DEFAULT_CLAUDE_MODEL
3108
+ }
3109
+ };
3110
+ }
3111
+
3112
+ function buildProviderSharePayload(params = {}) {
3113
+ const name = typeof params.name === 'string' ? params.name.trim() : '';
3114
+ if (!name) {
3115
+ return { error: '缺少提供商名称' };
3116
+ }
3117
+
3118
+ const { config } = readConfigOrVirtualDefault();
3119
+ const providers = config.model_providers || {};
3120
+ const provider = providers[name];
3121
+ if (!provider || typeof provider !== 'object') {
3122
+ return { error: `提供商不存在: ${name}` };
3123
+ }
3124
+
3125
+ const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
3126
+ const apiKey = typeof provider.preferred_auth_method === 'string'
3127
+ ? provider.preferred_auth_method
3128
+ : '';
3129
+
3130
+ if (!baseUrl) {
3131
+ return { error: `提供商 ${name} 缺少 base_url` };
3132
+ }
3133
+
3134
+ return {
3135
+ payload: {
3136
+ name,
3137
+ baseUrl,
3138
+ apiKey
3139
+ }
3140
+ };
3141
+ }
3142
+
3649
3143
  function normalizeImportPayload(payload) {
3650
3144
  if (!payload || typeof payload !== 'object') {
3651
3145
  return { error: 'Invalid import payload' };
@@ -4464,7 +3958,7 @@ function applyToClaudeSettings(config = {}) {
4464
3958
  }
4465
3959
 
4466
3960
  const baseUrl = (config.baseUrl || 'https://open.bigmodel.cn/api/anthropic').trim();
4467
- const model = (config.model || 'glm-4.7').trim();
3961
+ const model = (config.model || DEFAULT_CLAUDE_MODEL).trim();
4468
3962
  const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
4469
3963
  if (!readResult.ok) {
4470
3964
  return { success: false, mode: 'settings-file', error: readResult.error };
@@ -4516,6 +4010,76 @@ function applyToClaudeSettings(config = {}) {
4516
4010
  }
4517
4011
  }
4518
4012
 
4013
+ function readClaudeSettingsInfo() {
4014
+ const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
4015
+ if (!readResult.ok) {
4016
+ return {
4017
+ error: readResult.error || '读取 Claude 配置失败',
4018
+ exists: !!readResult.exists,
4019
+ path: CLAUDE_SETTINGS_FILE
4020
+ };
4021
+ }
4022
+
4023
+ const settings = readResult.data || {};
4024
+ const env = (settings.env && typeof settings.env === 'object' && !Array.isArray(settings.env))
4025
+ ? settings.env
4026
+ : {};
4027
+
4028
+ return {
4029
+ exists: !!readResult.exists,
4030
+ path: CLAUDE_SETTINGS_FILE,
4031
+ apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
4032
+ baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
4033
+ model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
4034
+ env
4035
+ };
4036
+ }
4037
+
4038
+ // CLI: 一行写入 Claude Code 配置
4039
+ function cmdClaude(baseUrl, apiKey, model, silent = false) {
4040
+ const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
4041
+ const normalizedKey = typeof apiKey === 'string' ? apiKey.trim() : '';
4042
+ const normalizedModel = typeof model === 'string' && model.trim()
4043
+ ? model.trim()
4044
+ : DEFAULT_CLAUDE_MODEL;
4045
+
4046
+ if (!normalizedBaseUrl || !normalizedKey) {
4047
+ if (!silent) {
4048
+ console.error('用法: codexmate claude <BaseURL> <API密钥> [模型]');
4049
+ console.log('\n示例:');
4050
+ console.log(' codexmate claude https://open.bigmodel.cn/api/anthropic sk-ant-xxx glm-4.7');
4051
+ }
4052
+ throw new Error('BaseURL 和 API 密钥必填');
4053
+ }
4054
+
4055
+ const result = applyToClaudeSettings({
4056
+ baseUrl: normalizedBaseUrl,
4057
+ apiKey: normalizedKey,
4058
+ model: normalizedModel
4059
+ });
4060
+
4061
+ if (!result || result.success === false) {
4062
+ const message = (result && result.error) || '应用 Claude 配置失败';
4063
+ if (!silent) console.error('错误:', message);
4064
+ throw new Error(message);
4065
+ }
4066
+
4067
+ if (!silent) {
4068
+ console.log('✓ 已写入 Claude Code 配置');
4069
+ console.log(' Base URL:', normalizedBaseUrl);
4070
+ console.log(' 模型:', normalizedModel);
4071
+ if (result.targetPath) {
4072
+ console.log(' 目标文件:', result.targetPath);
4073
+ }
4074
+ if (result.backupPath) {
4075
+ console.log(' 已自动备份:', result.backupPath);
4076
+ }
4077
+ console.log();
4078
+ }
4079
+
4080
+ return result;
4081
+ }
4082
+
4519
4083
  function commandExists(command, args = '') {
4520
4084
  try {
4521
4085
  execSync(`${command} ${args}`, { stdio: 'ignore' });
@@ -4649,7 +4213,7 @@ async function cmdZip(targetPath, options = {}) {
4649
4213
  }
4650
4214
 
4651
4215
  // 解压(7-Zip 优先)
4652
- async function cmdUnzip(zipPath, outputDir) {
4216
+ async function cmdUnzip(zipPath, outputDir) {
4653
4217
  if (!zipPath) {
4654
4218
  console.error('用法: codexmate unzip <zip文件路径> [输出目录]');
4655
4219
  console.log('\n示例:');
@@ -4705,224 +4269,224 @@ async function cmdUnzip(zipPath, outputDir) {
4705
4269
  } catch (e) {
4706
4270
  console.error('解压失败:', e.message);
4707
4271
  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
- }
4272
+ }
4273
+ }
4274
+
4275
+ function resolveExportOutputPath(outputPath, defaultFileName) {
4276
+ const fallback = path.resolve(process.cwd(), defaultFileName);
4277
+ if (typeof outputPath !== 'string' || !outputPath.trim()) {
4278
+ return fallback;
4279
+ }
4280
+
4281
+ const trimmed = outputPath.trim();
4282
+ const resolved = path.resolve(trimmed);
4283
+ const hasTrailingSep = /[\\\/]$/.test(trimmed);
4284
+ if (hasTrailingSep) {
4285
+ ensureDir(resolved);
4286
+ return path.join(resolved, defaultFileName);
4287
+ }
4288
+
4289
+ if (fs.existsSync(resolved)) {
4290
+ try {
4291
+ const stat = fs.statSync(resolved);
4292
+ if (stat.isDirectory()) {
4293
+ return path.join(resolved, defaultFileName);
4294
+ }
4295
+ } catch (e) {}
4296
+ }
4297
+
4298
+ return resolved;
4299
+ }
4300
+
4301
+ function printExportSessionUsage() {
4302
+ console.log('\n用法: codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
4303
+ console.log('\n示例:');
4304
+ console.log(' codexmate export-session --source codex --session-id 123456');
4305
+ console.log(' codexmate export-session --source claude --file "~/.claude/projects/demo/session.jsonl"');
4306
+ console.log(' codexmate export-session --source codex --session-id 123456 --max-messages=all');
4307
+ }
4308
+
4309
+ function parseExportSessionArgs(args = []) {
4310
+ const options = {
4311
+ source: '',
4312
+ sessionId: '',
4313
+ filePath: '',
4314
+ output: '',
4315
+ maxMessages: undefined
4316
+ };
4317
+ const errors = [];
4318
+
4319
+ for (let i = 0; i < args.length; i++) {
4320
+ const arg = args[i];
4321
+ if (!arg) continue;
4322
+
4323
+ if (arg.startsWith('--source=')) {
4324
+ options.source = arg.slice('--source='.length);
4325
+ continue;
4326
+ }
4327
+ if (arg === '--source') {
4328
+ options.source = args[i + 1] || '';
4329
+ i += 1;
4330
+ continue;
4331
+ }
4332
+ if (arg.startsWith('--session-id=')) {
4333
+ options.sessionId = arg.slice('--session-id='.length);
4334
+ continue;
4335
+ }
4336
+ if (arg === '--session-id') {
4337
+ options.sessionId = args[i + 1] || '';
4338
+ i += 1;
4339
+ continue;
4340
+ }
4341
+ if (arg.startsWith('--file=')) {
4342
+ options.filePath = arg.slice('--file='.length);
4343
+ continue;
4344
+ }
4345
+ if (arg === '--file') {
4346
+ options.filePath = args[i + 1] || '';
4347
+ i += 1;
4348
+ continue;
4349
+ }
4350
+ if (arg.startsWith('--output=')) {
4351
+ options.output = arg.slice('--output='.length);
4352
+ continue;
4353
+ }
4354
+ if (arg === '--output') {
4355
+ options.output = args[i + 1] || '';
4356
+ i += 1;
4357
+ continue;
4358
+ }
4359
+ if (arg.startsWith('--max-messages=')) {
4360
+ options.maxMessages = arg.slice('--max-messages='.length);
4361
+ continue;
4362
+ }
4363
+ if (arg === '--max-messages') {
4364
+ options.maxMessages = args[i + 1] || '';
4365
+ i += 1;
4366
+ continue;
4367
+ }
4368
+
4369
+ errors.push(`未知参数: ${arg}`);
4370
+ }
4371
+
4372
+ const normalizedSource = options.source.trim().toLowerCase();
4373
+ if (normalizedSource && normalizedSource !== 'codex' && normalizedSource !== 'claude') {
4374
+ errors.push('参数 --source 仅支持 codex 或 claude');
4375
+ }
4376
+ options.source = normalizedSource;
4377
+
4378
+ if (!options.source) {
4379
+ errors.push('缺少 --source');
4380
+ }
4381
+
4382
+ if (!options.sessionId && !options.filePath) {
4383
+ errors.push('必须指定 --session-id 或 --file');
4384
+ }
4385
+
4386
+ if (options.maxMessages !== undefined) {
4387
+ const parsed = parseMaxMessagesValue(options.maxMessages);
4388
+ if (parsed === null) {
4389
+ errors.push('参数 --max-messages 无效');
4390
+ } else {
4391
+ options.maxMessages = parsed === Infinity ? Infinity : Math.max(1, Math.floor(parsed));
4392
+ }
4393
+ }
4394
+
4395
+ return {
4396
+ options,
4397
+ error: errors.length > 0 ? errors.join(';') : ''
4398
+ };
4399
+ }
4400
+
4401
+ async function cmdExportSession(args = []) {
4402
+ const parsed = parseExportSessionArgs(args);
4403
+ if (parsed.error) {
4404
+ console.error('错误:', parsed.error);
4405
+ printExportSessionUsage();
4406
+ process.exit(1);
4407
+ }
4408
+
4409
+ const options = parsed.options;
4410
+ const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
4411
+ let result;
4412
+ try {
4413
+ result = await exportSessionData({
4414
+ source: options.source,
4415
+ sessionId: options.sessionId,
4416
+ filePath: options.filePath,
4417
+ maxMessages
4418
+ });
4419
+ } catch (e) {
4420
+ console.error('导出失败:', e.message || e);
4421
+ process.exit(1);
4422
+ }
4423
+
4424
+ if (result && result.error) {
4425
+ console.error('导出失败:', result.error);
4426
+ process.exit(1);
4427
+ }
4428
+
4429
+ const defaultFileName = (result && result.fileName)
4430
+ ? result.fileName
4431
+ : `${options.source}-session-${options.sessionId || Date.now()}.md`;
4432
+ const outputPath = resolveExportOutputPath(options.output, defaultFileName);
4433
+ ensureDir(path.dirname(outputPath));
4434
+ fs.writeFileSync(outputPath, (result && result.content) ? result.content : '', 'utf-8');
4435
+
4436
+ console.log('\n✓ 会话已导出:', outputPath);
4437
+ if (result && result.truncated) {
4438
+ const label = maxMessages === Infinity ? 'all' : maxMessages;
4439
+ console.log(`! 已截断: 仅导出前 ${label} 条消息`);
4440
+ console.log(' 可使用 --max-messages=all 导出完整内容');
4441
+ }
4442
+ console.log();
4443
+ }
4444
+
4445
+ function parseStartOptions(args = []) {
4446
+ const options = { host: '' };
4447
+ if (!Array.isArray(args)) {
4448
+ return options;
4449
+ }
4450
+
4451
+ for (let i = 0; i < args.length; i++) {
4452
+ const arg = args[i];
4453
+ if (!arg) continue;
4454
+ if (arg.startsWith('--host=')) {
4455
+ options.host = arg.slice('--host='.length);
4456
+ continue;
4457
+ }
4458
+ if (arg === '--host') {
4459
+ options.host = args[i + 1] || '';
4460
+ i += 1;
4461
+ }
4462
+ }
4463
+
4464
+ return options;
4465
+ }
4466
+
4467
+ function isAnyAddressHost(host) {
4468
+ return host === '0.0.0.0' || host === '::';
4469
+ }
4470
+
4471
+ function formatHostForUrl(host) {
4472
+ const value = typeof host === 'string' ? host.trim() : '';
4473
+ if (!value) return '';
4474
+ if (value.startsWith('[') && value.endsWith(']')) {
4475
+ return value;
4476
+ }
4477
+ if (value.includes(':')) {
4478
+ return `[${value}]`;
4479
+ }
4480
+ return value;
4481
+ }
4482
+
4483
+ // 打开 Web UI
4484
+ function cmdStart(options = {}) {
4485
+ const htmlPath = path.join(__dirname, 'web-ui.html');
4486
+ if (!fs.existsSync(htmlPath)) {
4487
+ console.error('错误: web-ui.html 不存在');
4488
+ process.exit(1);
4489
+ }
4926
4490
 
4927
4491
  const server = http.createServer((req, res) => {
4928
4492
  if (req.url === '/api') {
@@ -4937,9 +4501,11 @@ function cmdStart(options = {}) {
4937
4501
  case 'status':
4938
4502
  const statusConfigResult = readConfigOrVirtualDefault();
4939
4503
  const config = statusConfigResult.config;
4504
+ const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
4940
4505
  result = {
4941
4506
  provider: config.model_provider || '未设置',
4942
4507
  model: config.model || '未设置',
4508
+ serviceTier,
4943
4509
  configReady: !statusConfigResult.isVirtual,
4944
4510
  configNotice: statusConfigResult.reason || '',
4945
4511
  initNotice: consumeInitNotice()
@@ -5039,9 +4605,18 @@ function cmdStart(options = {}) {
5039
4605
  cmdDeleteModel(params.model, true);
5040
4606
  result = { success: true };
5041
4607
  break;
4608
+ case 'get-claude-settings':
4609
+ result = readClaudeSettingsInfo();
4610
+ break;
5042
4611
  case 'apply-claude-config':
5043
4612
  result = applyToClaudeSettings(params.config);
5044
4613
  break;
4614
+ case 'export-claude-share':
4615
+ result = buildClaudeSharePayload(params && params.config ? params.config : {});
4616
+ break;
4617
+ case 'export-provider':
4618
+ result = buildProviderSharePayload(params || {});
4619
+ break;
5045
4620
  case 'export-config':
5046
4621
  result = {
5047
4622
  data: buildExportPayload(!!params.includeKeys)
@@ -5075,18 +4650,18 @@ function cmdStart(options = {}) {
5075
4650
  case 'delete-session':
5076
4651
  result = await deleteSessionData(params || {});
5077
4652
  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
- }
4653
+ case 'clone-session':
4654
+ result = await cloneCodexSession(params || {});
4655
+ break;
4656
+ case 'session-detail':
4657
+ result = await readSessionDetail(params);
4658
+ break;
4659
+ case 'session-plain':
4660
+ result = await readSessionPlain(params);
4661
+ break;
4662
+ default:
4663
+ result = { error: '未知操作' };
4664
+ }
5090
4665
 
5091
4666
  res.writeHead(200, { 'Content-Type': 'application/json' });
5092
4667
  res.end(JSON.stringify(result));
@@ -5102,28 +4677,28 @@ function cmdStart(options = {}) {
5102
4677
  }
5103
4678
  });
5104
4679
 
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}"`;
4680
+ const port = resolveWebPort();
4681
+ const host = resolveWebHost(options);
4682
+ const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
4683
+ const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
4684
+ server.listen(port, host, () => {
4685
+ console.log('\n✓ Web UI 已启动:', openUrl);
4686
+ if (host && host !== openHost) {
4687
+ console.log(' 监听地址:', host);
4688
+ }
4689
+ console.log(' 按 Ctrl+C 退出\n');
4690
+ if (isAnyAddressHost(host)) {
4691
+ console.warn('! 安全提示: 当前监听所有网卡(无鉴权)。');
4692
+ console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
4693
+ }
4694
+
4695
+ // 打开浏览器
4696
+ const platform = process.platform;
4697
+ let command;
4698
+ const url = openUrl;
4699
+
4700
+ if (platform === 'win32') {
4701
+ command = `start "" "${url}"`;
5127
4702
  } else if (platform === 'darwin') {
5128
4703
  command = `open "${url}"`;
5129
4704
  } else {
@@ -5139,6 +4714,39 @@ function cmdStart(options = {}) {
5139
4714
  });
5140
4715
  }
5141
4716
 
4717
+ async function cmdCodex(args = []) {
4718
+ const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
4719
+ const hasYolo = extraArgs.includes('--yolo');
4720
+ const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
4721
+
4722
+ return new Promise((resolve, reject) => {
4723
+ const child = spawn('codex', finalArgs, {
4724
+ stdio: 'inherit',
4725
+ shell: process.platform === 'win32'
4726
+ });
4727
+
4728
+ child.on('error', (err) => {
4729
+ reject(new Error(`无法启动 codex,请确认已安装并在 PATH 中: ${err.message}`));
4730
+ });
4731
+
4732
+ child.on('exit', (code, signal) => {
4733
+ if (typeof code === 'number') {
4734
+ resolve(code);
4735
+ return;
4736
+ }
4737
+ if (signal === 'SIGINT') {
4738
+ resolve(130);
4739
+ return;
4740
+ }
4741
+ if (signal === 'SIGTERM') {
4742
+ resolve(143);
4743
+ return;
4744
+ }
4745
+ resolve(1);
4746
+ });
4747
+ });
4748
+ }
4749
+
5142
4750
  // ============================================================================
5143
4751
  // 主程序
5144
4752
  // ============================================================================
@@ -5160,15 +4768,17 @@ async function main() {
5160
4768
  console.log(' codexmate use <模型> 切换模型');
5161
4769
  console.log(' codexmate add <名称> <URL> [密钥]');
5162
4770
  console.log(' codexmate delete <名称> 删除提供商');
5163
- console.log(' codexmate add-model <模型> 添加模型');
5164
- console.log(' codexmate delete-model <模型> 删除模型');
5165
- console.log(' codexmate start [--host <HOST>] 启动 Web 界面');
5166
- console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
5167
- console.log(' codexmate zip <路径> [--max:级别] 压缩(7-Zip 优先)');
5168
- console.log(' codexmate unzip <zip文件> [输出目录] 解压(7-Zip 优先)');
5169
- console.log('');
5170
- process.exit(0);
5171
- }
4771
+ console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
4772
+ console.log(' codexmate add-model <模型> 添加模型');
4773
+ console.log(' codexmate delete-model <模型> 删除模型');
4774
+ console.log(' codexmate run [--host <HOST>] 启动 Web 界面');
4775
+ console.log(' codexmate codex [参数...] 等同于 codex --yolo');
4776
+ console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
4777
+ console.log(' codexmate zip <路径> [--max:级别] 压缩(7-Zip 优先)');
4778
+ console.log(' codexmate unzip <zip文件> [输出目录] 解压(7-Zip 优先)');
4779
+ console.log('');
4780
+ process.exit(0);
4781
+ }
5172
4782
 
5173
4783
  const command = args[0];
5174
4784
 
@@ -5181,13 +4791,23 @@ async function main() {
5181
4791
  case 'use': cmdUseModel(args[1]); break;
5182
4792
  case 'add': cmdAdd(args[1], args[2], args[3]); break;
5183
4793
  case 'delete': cmdDelete(args[1]); break;
5184
- case 'add-model': cmdAddModel(args[1]); break;
5185
- case 'delete-model': cmdDeleteModel(args[1]); break;
5186
- case 'start': cmdStart(parseStartOptions(args.slice(1))); break;
5187
- case 'export-session': await cmdExportSession(args.slice(1)); break;
5188
- case 'zip': {
5189
- // 解析 --max:N 参数
5190
- const zipOptions = {};
4794
+ case 'claude': cmdClaude(args[1], args[2], args[3]); break;
4795
+ case 'add-model': cmdAddModel(args[1]); break;
4796
+ case 'delete-model': cmdDeleteModel(args[1]); break;
4797
+ case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
4798
+ case 'start':
4799
+ console.error('错误: 命令已更名为 "run",请使用: codexmate run');
4800
+ process.exit(1);
4801
+ break;
4802
+ case 'codex': {
4803
+ const exitCode = await cmdCodex(args.slice(1));
4804
+ process.exit(exitCode);
4805
+ break;
4806
+ }
4807
+ case 'export-session': await cmdExportSession(args.slice(1)); break;
4808
+ case 'zip': {
4809
+ // 解析 --max:N 参数
4810
+ const zipOptions = {};
5191
4811
  let targetPath = null;
5192
4812
  for (let i = 1; i < args.length; i++) {
5193
4813
  const arg = args[i];
@@ -5212,4 +4832,3 @@ main().catch((err) => {
5212
4832
  console.error('错误:', err && err.message ? err.message : err);
5213
4833
  process.exit(1);
5214
4834
  });
5215
-