codexmate 0.0.24 → 0.0.25

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 (35) hide show
  1. package/README.md +28 -6
  2. package/README.zh.md +29 -7
  3. package/cli/builtin-proxy.js +35 -0
  4. package/cli/claude-proxy.js +24 -0
  5. package/cli/import-skills-url.js +23 -1
  6. package/cli/openai-bridge.js +51 -4
  7. package/cli/session-usage.js +8 -2
  8. package/cli.js +1543 -117
  9. package/lib/automation.js +404 -0
  10. package/lib/cli-path-utils.js +21 -5
  11. package/lib/cli-sessions.js +32 -1
  12. package/lib/download-artifacts.js +17 -2
  13. package/lib/mcp-stdio.js +13 -0
  14. package/package.json +2 -5
  15. package/web-ui/app.js +6 -3
  16. package/web-ui/logic.sessions.mjs +2 -2
  17. package/web-ui/modules/app.computed.dashboard.mjs +2 -0
  18. package/web-ui/modules/app.computed.session.mjs +17 -0
  19. package/web-ui/modules/app.methods.install.mjs +28 -0
  20. package/web-ui/modules/app.methods.session-actions.mjs +8 -1
  21. package/web-ui/modules/app.methods.session-browser.mjs +28 -4
  22. package/web-ui/modules/app.methods.session-trash.mjs +4 -2
  23. package/web-ui/modules/i18n.dict.mjs +24 -8
  24. package/web-ui/partials/index/layout-header.html +12 -2
  25. package/web-ui/partials/index/panel-sessions.html +4 -2
  26. package/web-ui/partials/index/panel-usage.html +7 -0
  27. package/web-ui/styles/controls-forms.css +49 -2
  28. package/web-ui/styles/layout-shell.css +1 -0
  29. package/web-ui/styles/responsive.css +0 -2
  30. package/web-ui/styles/sessions-list.css +2 -4
  31. package/web-ui/styles/sessions-toolbar-trash.css +4 -4
  32. package/web-ui/styles/sessions-usage.css +24 -0
  33. /package/{res → web-ui/res}/json5.min.js +0 -0
  34. /package/{res → web-ui/res}/logo-pack.webp +0 -0
  35. /package/{res → web-ui/res}/vue.global.prod.js +0 -0
package/cli.js CHANGED
@@ -10,6 +10,7 @@ const yauzl = require('yauzl');
10
10
  const { exec, execSync, spawn, spawnSync } = require('child_process');
11
11
  const http = require('http');
12
12
  const https = require('https');
13
+ const net = require('net');
13
14
  const readline = require('readline');
14
15
  const {
15
16
  expandHomePath,
@@ -74,6 +75,14 @@ const {
74
75
  validateTaskPlan,
75
76
  executeTaskPlan
76
77
  } = require('./lib/task-orchestrator');
78
+ const {
79
+ readAutomationConfig,
80
+ matchAutomationRule,
81
+ buildAutomationEventKey,
82
+ isCronMatch,
83
+ dispatchAutomationNotifiers,
84
+ formatTaskRunNotificationPayload
85
+ } = require('./lib/automation');
77
86
  const { buildConfigHealthReport: buildConfigHealthReportCore } = require('./cli/config-health');
78
87
  const { buildDoctorReport, buildDoctorLegacyPayload, renderDoctorMarkdown } = require('./cli/doctor-core');
79
88
  const {
@@ -185,6 +194,10 @@ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
185
194
  const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
186
195
  const CLAUDE_MD_FILE_NAME = 'CLAUDE.md';
187
196
  const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
197
+ const CODEBUDDY_DIR = path.join(os.homedir(), '.codebuddy');
198
+ const CODEBUDDY_PROJECTS_DIR = path.join(CODEBUDDY_DIR, 'projects');
199
+ const GEMINI_DIR = path.join(os.homedir(), '.gemini');
200
+ const GEMINI_TMP_DIR = path.join(GEMINI_DIR, 'tmp');
188
201
  const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
189
202
  const WORKFLOW_DEFINITIONS_FILE = path.join(CONFIG_DIR, 'codexmate-workflows.json');
190
203
  const WORKFLOW_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-workflow-runs.jsonl');
@@ -192,6 +205,8 @@ const TASK_QUEUE_FILE = path.join(CONFIG_DIR, 'codexmate-task-queue.json');
192
205
  const TASK_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-task-runs.jsonl');
193
206
  const TASK_RUN_DETAILS_DIR = path.join(CONFIG_DIR, 'codexmate-task-runs');
194
207
  const TASK_QUEUE_WORKER_FILE = path.join(CONFIG_DIR, 'codexmate-task-queue-worker.json');
208
+ const TASK_ARTIFACTS_DIR = path.join(CONFIG_DIR, 'codexmate-task-artifacts');
209
+ const AUTOMATION_CONFIG_FILE = path.join(CONFIG_DIR, 'codexmate-automation.json');
195
210
  const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
196
211
  const DEFAULT_MODEL_CONTEXT_WINDOW = 190000;
197
212
  const DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT = 185000;
@@ -262,6 +277,18 @@ const CLI_INSTALL_TARGETS = Object.freeze([
262
277
  packageName: '@anthropic-ai/claude-code',
263
278
  bins: ['claude']
264
279
  },
280
+ {
281
+ id: 'codebuddy',
282
+ name: 'CodeBuddy Code',
283
+ packageName: '@tencent-ai/codebuddy-code',
284
+ bins: ['codebuddy']
285
+ },
286
+ {
287
+ id: 'gemini',
288
+ name: 'Gemini CLI',
289
+ packageName: '@google/gemini-cli',
290
+ bins: ['gemini']
291
+ },
265
292
  {
266
293
  id: 'codex',
267
294
  name: 'Codex CLI',
@@ -275,7 +302,7 @@ const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
275
302
 
276
303
  const openaiBridgeHandler = createOpenaiBridgeHttpHandler({
277
304
  settingsFile: OPENAI_BRIDGE_SETTINGS_FILE,
278
- expectedToken: 'codexmate',
305
+ expectedToken: typeof process.env.CODEXMATE_HTTP_TOKEN === 'string' ? process.env.CODEXMATE_HTTP_TOKEN.trim() : '',
279
306
  maxBodySize: MAX_API_BODY_SIZE,
280
307
  httpAgent: HTTP_KEEP_ALIVE_AGENT,
281
308
  httpsAgent: HTTPS_KEEP_ALIVE_AGENT
@@ -559,7 +586,9 @@ let g_sessionListCache = new Map();
559
586
  let g_sessionInventoryCache = new Map();
560
587
  let g_sessionFileLookupCache = {
561
588
  codex: new Map(),
562
- claude: new Map()
589
+ claude: new Map(),
590
+ gemini: new Map(),
591
+ codebuddy: new Map()
563
592
  };
564
593
  let g_exactMessageCountCache = new Map();
565
594
  let g_modelsCache = new Map();
@@ -1254,6 +1283,31 @@ function getClaudeProjectsDir() {
1254
1283
  return resolveExistingDir(candidates, CLAUDE_PROJECTS_DIR);
1255
1284
  }
1256
1285
 
1286
+ function getGeminiTmpDir() {
1287
+ const candidates = [];
1288
+ const envGeminiHome = process.env.GEMINI_HOME;
1289
+ if (envGeminiHome) {
1290
+ candidates.push(path.join(envGeminiHome, 'tmp'));
1291
+ }
1292
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
1293
+ if (xdgConfig) {
1294
+ candidates.push(path.join(xdgConfig, 'gemini', 'tmp'));
1295
+ }
1296
+ candidates.push(path.join(os.homedir(), '.config', 'gemini', 'tmp'));
1297
+ candidates.push(GEMINI_TMP_DIR);
1298
+ return resolveExistingDir(candidates, GEMINI_TMP_DIR);
1299
+ }
1300
+
1301
+ function getCodeBuddyProjectsDir() {
1302
+ const candidates = [];
1303
+ const envHome = process.env.CODEBUDDY_CODE_HOME_DIR || process.env.CODEBUDDY_HOME;
1304
+ if (envHome) {
1305
+ candidates.push(path.join(envHome, 'projects'));
1306
+ }
1307
+ candidates.push(CODEBUDDY_PROJECTS_DIR);
1308
+ return resolveExistingDir(candidates, CODEBUDDY_PROJECTS_DIR);
1309
+ }
1310
+
1257
1311
  function readModelsCacheEntry(cacheKey) {
1258
1312
  if (!cacheKey) return null;
1259
1313
  const entry = g_modelsCache.get(cacheKey);
@@ -2501,6 +2555,19 @@ function countConversationMessagesInRecords(records, source) {
2501
2555
  }
2502
2556
  continue;
2503
2557
  }
2558
+ if (source === 'codebuddy') {
2559
+ if (record && record.type === 'message') {
2560
+ const role = normalizeRole(record.role);
2561
+ if (role === 'assistant' || role === 'user' || role === 'system') {
2562
+ const content = record.message?.content ?? record.content ?? '';
2563
+ messages.push({
2564
+ role,
2565
+ text: extractMessageText(content)
2566
+ });
2567
+ }
2568
+ }
2569
+ continue;
2570
+ }
2504
2571
 
2505
2572
  const role = normalizeRole(record.type);
2506
2573
  if (role === 'assistant' || role === 'user' || role === 'system') {
@@ -2522,6 +2589,28 @@ async function countConversationMessagesInFile(filePath, source) {
2522
2589
  return cached;
2523
2590
  }
2524
2591
 
2592
+ if (source === 'gemini') {
2593
+ let json;
2594
+ try {
2595
+ json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
2596
+ } catch (_) {
2597
+ json = null;
2598
+ }
2599
+ const rawMessages = json && Array.isArray(json.messages) ? json.messages : [];
2600
+ const messages = [];
2601
+ for (const entry of rawMessages) {
2602
+ if (!entry || typeof entry !== 'object') continue;
2603
+ const role = normalizeGeminiMessageRole(entry.type);
2604
+ if (!role) continue;
2605
+ const text = extractMessageText(extractGeminiMessageText(entry.content ?? entry.message ?? entry.text));
2606
+ if (!text && role !== 'system') continue;
2607
+ messages.push({ role, text });
2608
+ }
2609
+ const safeCount = removeLeadingSystemMessage(messages).length;
2610
+ writeExactMessageCountCache(filePath, source, safeCount, fileStat);
2611
+ return safeCount;
2612
+ }
2613
+
2525
2614
  let stream;
2526
2615
  let rl;
2527
2616
  let messageCount = 0;
@@ -2549,6 +2638,15 @@ async function countConversationMessagesInFile(filePath, source) {
2549
2638
  role = normalizeRole(record.payload.role);
2550
2639
  text = extractMessageText(record.payload.content);
2551
2640
  }
2641
+ } else if (source === 'codebuddy') {
2642
+ if (record && record.type === 'message') {
2643
+ role = normalizeRole(record.role);
2644
+ if (role === 'assistant' || role === 'user' || role === 'system') {
2645
+ text = extractMessageText(record.message?.content ?? record.content ?? '');
2646
+ } else {
2647
+ role = '';
2648
+ }
2649
+ }
2552
2650
  } else {
2553
2651
  role = normalizeRole(record.type);
2554
2652
  if (role === 'assistant' || role === 'user' || role === 'system') {
@@ -2711,7 +2809,13 @@ async function resolveSessionTrashEntryExactMessageCount(entry) {
2711
2809
  }
2712
2810
 
2713
2811
  async function hydrateSessionTrashEntries(entries, options = {}) {
2714
- const source = options.source === 'claude' ? 'claude' : (options.source === 'codex' ? 'codex' : 'all');
2812
+ const source = options.source === 'claude'
2813
+ ? 'claude'
2814
+ : (options.source === 'codex'
2815
+ ? 'codex'
2816
+ : (options.source === 'gemini'
2817
+ ? 'gemini'
2818
+ : (options.source === 'codebuddy' ? 'codebuddy' : 'all')));
2715
2819
  const hydratedEntries = await mapWithConcurrency(Array.isArray(entries) ? entries : [], 8, async (entry) => {
2716
2820
  const normalizedEntry = normalizeSessionTrashEntry(entry);
2717
2821
  if (!normalizedEntry) {
@@ -2720,7 +2824,7 @@ async function hydrateSessionTrashEntries(entries, options = {}) {
2720
2824
  return await resolveSessionTrashEntryExactMessageCount(normalizedEntry);
2721
2825
  });
2722
2826
 
2723
- if (source === 'codex' || source === 'claude') {
2827
+ if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy') {
2724
2828
  return hydratedEntries.filter((entry) => entry.source === source);
2725
2829
  }
2726
2830
  return hydratedEntries;
@@ -2734,7 +2838,11 @@ async function hydrateSessionItemsExactMessageCount(items) {
2734
2838
  if (item.__messageCountExact === true) {
2735
2839
  return item;
2736
2840
  }
2737
- const source = item.source === 'claude' ? 'claude' : (item.source === 'codex' ? 'codex' : '');
2841
+ const source = item.source === 'claude'
2842
+ ? 'claude'
2843
+ : (item.source === 'codex'
2844
+ ? 'codex'
2845
+ : (item.source === 'gemini' ? 'gemini' : (item.source === 'codebuddy' ? 'codebuddy' : '')));
2738
2846
  const filePath = typeof item.filePath === 'string' ? item.filePath : '';
2739
2847
  if (!source || !filePath || !fs.existsSync(filePath)) {
2740
2848
  return item;
@@ -2961,6 +3069,54 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
2961
3069
  ? Math.max(1024, rawMaxBytes)
2962
3070
  : 0;
2963
3071
  const state = createSessionQueryScanState(tokens, options);
3072
+ if (session.source === 'gemini') {
3073
+ if (state.roleFilter !== 'all') {
3074
+ let json;
3075
+ try {
3076
+ json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
3077
+ } catch (_) {
3078
+ json = null;
3079
+ }
3080
+ const rawMessages = json && Array.isArray(json.messages) ? json.messages : [];
3081
+ for (const entry of rawMessages) {
3082
+ if (!entry || typeof entry !== 'object') continue;
3083
+ const role = normalizeGeminiMessageRole(entry.type);
3084
+ if (!role) continue;
3085
+ const text = extractMessageText(extractGeminiMessageText(entry.content ?? entry.message ?? entry.text));
3086
+ if (!text) continue;
3087
+ if (consumeSessionQueryMessage(state, { role, text })) {
3088
+ break;
3089
+ }
3090
+ }
3091
+ return buildSessionQueryScanResult(state);
3092
+ }
3093
+
3094
+ let text = '';
3095
+ try {
3096
+ const stat = fs.statSync(filePath);
3097
+ const targetBytes = maxBytes > 0 ? Math.min(maxBytes, stat.size || 0) : Math.min(stat.size || 0, 512 * 1024);
3098
+ const fd = fs.openSync(filePath, 'r');
3099
+ const buf = Buffer.alloc(targetBytes);
3100
+ const bytes = fs.readSync(fd, buf, 0, targetBytes, 0);
3101
+ fs.closeSync(fd);
3102
+ text = bytes > 0 ? buf.slice(0, bytes).toString('utf-8') : '';
3103
+ } catch (_) {
3104
+ try {
3105
+ text = fs.readFileSync(filePath, 'utf-8');
3106
+ } catch (_) {
3107
+ text = '';
3108
+ }
3109
+ }
3110
+
3111
+ if (!matchTokensInText(text, state.tokens, state.mode)) {
3112
+ return buildSessionQueryScanResult(state);
3113
+ }
3114
+ state.count = 1;
3115
+ if (state.snippetLimit > 0) {
3116
+ state.snippets.push(truncateText(text));
3117
+ }
3118
+ return buildSessionQueryScanResult(state);
3119
+ }
2964
3120
  let stream;
2965
3121
  let rl;
2966
3122
  try {
@@ -3243,7 +3399,9 @@ function getSessionInventoryCache(cacheKey, forceRefresh = false) {
3243
3399
  }
3244
3400
 
3245
3401
  function registerSessionFileLookupEntries(source, sessions = []) {
3246
- const normalizedSource = source === 'claude' ? 'claude' : 'codex';
3402
+ const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy'
3403
+ ? source
3404
+ : 'codex';
3247
3405
  const store = g_sessionFileLookupCache[normalizedSource];
3248
3406
  if (!(store instanceof Map) || !Array.isArray(sessions)) {
3249
3407
  return;
@@ -3282,7 +3440,9 @@ function setSessionInventoryCache(cacheKey, source, value) {
3282
3440
  }
3283
3441
 
3284
3442
  function listSessionInventoryBySource(source, limit, scanOptions = {}, options = {}) {
3285
- const normalizedSource = source === 'claude' ? 'claude' : 'codex';
3443
+ const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy'
3444
+ ? source
3445
+ : 'codex';
3286
3446
  const forceRefresh = !!options.forceRefresh;
3287
3447
  const cacheKey = buildSessionInventoryCacheKey(normalizedSource, limit, scanOptions);
3288
3448
  const cached = getSessionInventoryCache(cacheKey, forceRefresh);
@@ -3292,7 +3452,11 @@ function listSessionInventoryBySource(source, limit, scanOptions = {}, options =
3292
3452
 
3293
3453
  const sessions = normalizedSource === 'claude'
3294
3454
  ? listClaudeSessions(limit, scanOptions)
3295
- : listCodexSessions(limit, scanOptions);
3455
+ : (normalizedSource === 'gemini'
3456
+ ? listGeminiSessions(limit, scanOptions)
3457
+ : (normalizedSource === 'codebuddy'
3458
+ ? listCodeBuddySessions(limit, scanOptions)
3459
+ : listCodexSessions(limit, scanOptions)));
3296
3460
  setSessionInventoryCache(cacheKey, normalizedSource, sessions);
3297
3461
  return sessions;
3298
3462
  }
@@ -3302,7 +3466,9 @@ function invalidateSessionListCache() {
3302
3466
  g_sessionInventoryCache.clear();
3303
3467
  g_sessionFileLookupCache = {
3304
3468
  codex: new Map(),
3305
- claude: new Map()
3469
+ claude: new Map(),
3470
+ gemini: new Map(),
3471
+ codebuddy: new Map()
3306
3472
  };
3307
3473
  }
3308
3474
 
@@ -3894,6 +4060,317 @@ function parseClaudeSessionSummary(filePath, options = {}) {
3894
4060
  };
3895
4061
  }
3896
4062
 
4063
+ function parseCodeBuddySessionSummary(filePath, options = {}) {
4064
+ const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
4065
+ ? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
4066
+ : SESSION_SUMMARY_READ_BYTES;
4067
+ const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
4068
+ ? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
4069
+ : SESSION_TITLE_READ_BYTES;
4070
+ const records = parseJsonlHeadRecords(filePath, summaryReadBytes);
4071
+ if (records.length === 0) {
4072
+ return null;
4073
+ }
4074
+
4075
+ let stat;
4076
+ try {
4077
+ stat = fs.statSync(filePath);
4078
+ } catch (_) {
4079
+ return null;
4080
+ }
4081
+
4082
+ let sessionId = path.basename(filePath, '.jsonl');
4083
+ let cwd = '';
4084
+ let firstPrompt = '';
4085
+ let messageCount = 0;
4086
+ let totalTokens = 0;
4087
+ let contextWindow = 0;
4088
+ let inputTokens = 0;
4089
+ let cachedInputTokens = 0;
4090
+ let outputTokens = 0;
4091
+ let reasoningOutputTokens = 0;
4092
+ let provider = 'codebuddy';
4093
+ let model = '';
4094
+ const models = [];
4095
+ const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, outputTokens, reasoningOutputTokens };
4096
+ const previewMessages = [];
4097
+ let createdAt = '';
4098
+ let updatedAt = stat.mtime.toISOString();
4099
+
4100
+ for (const record of records) {
4101
+ if (!createdAt && record && record.timestamp) {
4102
+ createdAt = toIsoTime(record.timestamp, createdAt);
4103
+ }
4104
+ if (record && record.timestamp) {
4105
+ updatedAt = updateLatestIso(updatedAt, record.timestamp);
4106
+ }
4107
+
4108
+ applySessionUsageSummaryFromRecord(usageState, record, 'codebuddy');
4109
+ totalTokens = usageState.totalTokens || 0;
4110
+ contextWindow = usageState.contextWindow || 0;
4111
+ inputTokens = usageState.inputTokens || 0;
4112
+ cachedInputTokens = usageState.cachedInputTokens || 0;
4113
+ outputTokens = usageState.outputTokens || 0;
4114
+ reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
4115
+
4116
+ if (record && typeof record.sessionId === 'string' && record.sessionId.trim()) {
4117
+ sessionId = record.sessionId.trim();
4118
+ }
4119
+ if (!cwd && record && typeof record.cwd === 'string' && record.cwd.trim()) {
4120
+ cwd = record.cwd.trim();
4121
+ }
4122
+
4123
+ provider = readExplicitSessionProviderFromRecord(record) || provider;
4124
+ const recordModels = readSessionModelsFromRecord(record);
4125
+ for (const recordModel of recordModels) {
4126
+ if (!models.includes(recordModel)) {
4127
+ models.push(recordModel);
4128
+ }
4129
+ }
4130
+ model = recordModels[0] || model;
4131
+
4132
+ if (record && record.type === 'message') {
4133
+ const role = normalizeRole(record.role);
4134
+ if (role === 'assistant' || role === 'user' || role === 'system') {
4135
+ const content = record.message?.content ?? record.content ?? '';
4136
+ previewMessages.push({
4137
+ role,
4138
+ text: extractMessageText(content)
4139
+ });
4140
+ }
4141
+ }
4142
+ }
4143
+
4144
+ const tailRecords = parseJsonlTailRecords(filePath, summaryReadBytes);
4145
+ for (const record of tailRecords) {
4146
+ applySessionUsageSummaryFromRecord(usageState, record, 'codebuddy');
4147
+ totalTokens = usageState.totalTokens || 0;
4148
+ contextWindow = usageState.contextWindow || 0;
4149
+ inputTokens = usageState.inputTokens || 0;
4150
+ cachedInputTokens = usageState.cachedInputTokens || 0;
4151
+ outputTokens = usageState.outputTokens || 0;
4152
+ reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
4153
+ provider = readExplicitSessionProviderFromRecord(record) || provider;
4154
+ const recordModels = readSessionModelsFromRecord(record);
4155
+ for (const recordModel of recordModels) {
4156
+ if (!models.includes(recordModel)) {
4157
+ models.push(recordModel);
4158
+ }
4159
+ }
4160
+ model = recordModels[0] || model;
4161
+ }
4162
+
4163
+ const filteredPreviewMessages = removeLeadingSystemMessage(previewMessages);
4164
+ messageCount = filteredPreviewMessages.length;
4165
+ const firstUser = filteredPreviewMessages.find(item => item.role === 'user' && item.text);
4166
+ if (firstUser) {
4167
+ firstPrompt = truncateText(firstUser.text);
4168
+ }
4169
+
4170
+ if (!firstPrompt) {
4171
+ const titleRecords = parseJsonlHeadRecords(filePath, titleReadBytes);
4172
+ const titleMessages = [];
4173
+ for (const record of titleRecords) {
4174
+ if (record && record.type === 'message') {
4175
+ const role = normalizeRole(record.role);
4176
+ if (role === 'assistant' || role === 'user' || role === 'system') {
4177
+ const content = record.message?.content ?? record.content ?? '';
4178
+ titleMessages.push({
4179
+ role,
4180
+ text: extractMessageText(content)
4181
+ });
4182
+ }
4183
+ }
4184
+ }
4185
+
4186
+ const filteredTitleMessages = removeLeadingSystemMessage(titleMessages);
4187
+ const titleUser = filteredTitleMessages.find(item => item.role === 'user' && item.text);
4188
+ if (titleUser) {
4189
+ firstPrompt = truncateText(titleUser.text);
4190
+ }
4191
+ }
4192
+
4193
+ messageCount = Math.max(0, messageCount);
4194
+
4195
+ return {
4196
+ source: 'codebuddy',
4197
+ sourceLabel: 'CodeBuddy Code',
4198
+ provider,
4199
+ model,
4200
+ models,
4201
+ sessionId,
4202
+ title: firstPrompt || sessionId,
4203
+ cwd,
4204
+ createdAt,
4205
+ updatedAt,
4206
+ messageCount,
4207
+ totalTokens,
4208
+ contextWindow,
4209
+ inputTokens,
4210
+ cachedInputTokens,
4211
+ outputTokens,
4212
+ reasoningOutputTokens,
4213
+ __messageCountExact: isSessionSummaryMessageCountExact(stat, summaryReadBytes),
4214
+ filePath,
4215
+ keywords: [],
4216
+ capabilities: { code: true }
4217
+ };
4218
+ }
4219
+
4220
+ function extractGeminiMessageText(content) {
4221
+ if (typeof content === 'string') {
4222
+ return content;
4223
+ }
4224
+ if (Array.isArray(content)) {
4225
+ const parts = [];
4226
+ for (const item of content) {
4227
+ if (!item) continue;
4228
+ if (typeof item === 'string') {
4229
+ parts.push(item);
4230
+ continue;
4231
+ }
4232
+ if (typeof item.text === 'string' && item.text.trim()) {
4233
+ parts.push(item.text);
4234
+ continue;
4235
+ }
4236
+ if (typeof item.content === 'string' && item.content.trim()) {
4237
+ parts.push(item.content);
4238
+ }
4239
+ }
4240
+ return parts.filter(Boolean).join('\n');
4241
+ }
4242
+ if (content && typeof content === 'object') {
4243
+ if (typeof content.text === 'string') {
4244
+ return content.text;
4245
+ }
4246
+ if (typeof content.content === 'string') {
4247
+ return content.content;
4248
+ }
4249
+ if (Array.isArray(content.parts)) {
4250
+ return extractGeminiMessageText(content.parts);
4251
+ }
4252
+ if (Array.isArray(content.content)) {
4253
+ return extractGeminiMessageText(content.content);
4254
+ }
4255
+ }
4256
+ return '';
4257
+ }
4258
+
4259
+ function normalizeGeminiMessageRole(type) {
4260
+ const t = typeof type === 'string' ? type.trim().toLowerCase() : '';
4261
+ if (t === 'user') return 'user';
4262
+ if (t === 'gemini' || t === 'assistant' || t === 'model') return 'assistant';
4263
+ if (t === 'system' || t === 'info' || t === 'warning' || t === 'error') return 'system';
4264
+ return '';
4265
+ }
4266
+
4267
+ function parseGeminiSessionSummary(filePath, options = {}) {
4268
+ const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
4269
+ ? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
4270
+ : SESSION_SUMMARY_READ_BYTES;
4271
+ const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
4272
+ ? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
4273
+ : SESSION_TITLE_READ_BYTES;
4274
+ let stat;
4275
+ try {
4276
+ stat = fs.statSync(filePath);
4277
+ } catch (_) {
4278
+ return null;
4279
+ }
4280
+
4281
+ const fileName = path.basename(filePath);
4282
+ const projectHash = path.basename(path.dirname(path.dirname(filePath)));
4283
+ let sessionId = path.basename(filePath, '.json');
4284
+ let createdAt = '';
4285
+ let updatedAt = stat.mtime.toISOString();
4286
+ let provider = 'gemini';
4287
+ let model = '';
4288
+ const models = [];
4289
+ let firstPrompt = '';
4290
+ let messageCount = 0;
4291
+
4292
+ let headText = '';
4293
+ try {
4294
+ const fd = fs.openSync(filePath, 'r');
4295
+ const buf = Buffer.alloc(summaryReadBytes);
4296
+ const bytes = fs.readSync(fd, buf, 0, summaryReadBytes, 0);
4297
+ fs.closeSync(fd);
4298
+ headText = bytes > 0 ? buf.slice(0, bytes).toString('utf-8') : '';
4299
+ } catch (_) {
4300
+ headText = '';
4301
+ }
4302
+
4303
+ if (headText) {
4304
+ const sessionIdMatch = headText.match(/"sessionId"\s*:\s*"([^"]+)"/);
4305
+ if (sessionIdMatch) {
4306
+ sessionId = sessionIdMatch[1] || sessionId;
4307
+ }
4308
+ const startMatch = headText.match(/"startTime"\s*:\s*"([^"]+)"/);
4309
+ if (startMatch) {
4310
+ createdAt = toIsoTime(startMatch[1], createdAt);
4311
+ }
4312
+ const updatedMatch = headText.match(/"lastUpdated"\s*:\s*"([^"]+)"/);
4313
+ if (updatedMatch) {
4314
+ updatedAt = toIsoTime(updatedMatch[1], updatedAt);
4315
+ }
4316
+ const modelMatch = headText.match(/"model"\s*:\s*"([^"]+)"/);
4317
+ if (modelMatch && modelMatch[1]) {
4318
+ model = modelMatch[1];
4319
+ models.push(model);
4320
+ }
4321
+ const summaryMatch = headText.match(/"summary"\s*:\s*"([^"]+)"/);
4322
+ if (summaryMatch && summaryMatch[1]) {
4323
+ firstPrompt = truncateText(summaryMatch[1]);
4324
+ }
4325
+ if (!firstPrompt) {
4326
+ const userIdx = headText.search(/"type"\s*:\s*"user"/);
4327
+ if (userIdx >= 0) {
4328
+ const slice = headText.slice(userIdx, Math.min(headText.length, userIdx + titleReadBytes));
4329
+ const contentStringMatch = slice.match(/"content"\s*:\s*"((?:\\\\.|[^\"\\\\])*)"/);
4330
+ const textMatch = slice.match(/"text"\s*:\s*"((?:\\\\.|[^\"\\\\])*)"/);
4331
+ const raw = (contentStringMatch && contentStringMatch[1]) || (textMatch && textMatch[1]) || '';
4332
+ if (raw) {
4333
+ try {
4334
+ firstPrompt = truncateText(JSON.parse(`"${raw}"`));
4335
+ } catch (_) {
4336
+ firstPrompt = truncateText(raw);
4337
+ }
4338
+ }
4339
+ }
4340
+ }
4341
+ }
4342
+
4343
+ if (!createdAt) {
4344
+ createdAt = stat.mtime.toISOString();
4345
+ }
4346
+
4347
+ const cwd = projectHash ? path.join(getGeminiTmpDir(), projectHash) : '';
4348
+
4349
+ return {
4350
+ source: 'gemini',
4351
+ sourceLabel: 'Gemini CLI',
4352
+ provider,
4353
+ model,
4354
+ models,
4355
+ sessionId,
4356
+ title: firstPrompt || sessionId || fileName,
4357
+ cwd,
4358
+ createdAt,
4359
+ updatedAt,
4360
+ messageCount,
4361
+ totalTokens: 0,
4362
+ contextWindow: 0,
4363
+ inputTokens: 0,
4364
+ cachedInputTokens: 0,
4365
+ outputTokens: 0,
4366
+ reasoningOutputTokens: 0,
4367
+ __messageCountExact: false,
4368
+ filePath,
4369
+ keywords: [],
4370
+ capabilities: { code: true }
4371
+ };
4372
+ }
4373
+
3897
4374
  function listCodexSessions(limit, options = {}) {
3898
4375
  const codexSessionsDir = getCodexSessionsDir();
3899
4376
  const scanFactor = Number.isFinite(Number(options.scanFactor))
@@ -4144,8 +4621,144 @@ function listClaudeSessions(limit, options = {}) {
4144
4621
  return mergeAndLimitSessions(sessions, limit);
4145
4622
  }
4146
4623
 
4624
+ function listGeminiSessions(limit, options = {}) {
4625
+ const geminiTmpDir = getGeminiTmpDir();
4626
+ if (!fs.existsSync(geminiTmpDir)) {
4627
+ return [];
4628
+ }
4629
+
4630
+ const scanFactor = Number.isFinite(Number(options.scanFactor))
4631
+ ? Math.max(1, Number(options.scanFactor))
4632
+ : SESSION_SCAN_FACTOR;
4633
+ const minFiles = Number.isFinite(Number(options.minFiles))
4634
+ ? Math.max(1, Number(options.minFiles))
4635
+ : Math.min(SESSION_SCAN_MIN_FILES, MAX_SESSION_LIST_SIZE * SESSION_SCAN_FACTOR);
4636
+ const targetCount = Number.isFinite(Number(options.targetCount))
4637
+ ? Math.max(1, Math.floor(Number(options.targetCount)))
4638
+ : Math.max(1, Math.floor(limit * scanFactor));
4639
+ const scanCount = Number.isFinite(Number(options.scanCount))
4640
+ ? Math.max(targetCount, Math.floor(Number(options.scanCount)))
4641
+ : Math.max(targetCount, minFiles);
4642
+ const maxFilesScanned = Number.isFinite(Number(options.maxFilesScanned))
4643
+ ? Math.max(scanCount, Math.floor(Number(options.maxFilesScanned)))
4644
+ : Math.max(scanCount * 2, minFiles);
4645
+ const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
4646
+ ? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
4647
+ : SESSION_SUMMARY_READ_BYTES;
4648
+ const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
4649
+ ? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
4650
+ : SESSION_TITLE_READ_BYTES;
4651
+
4652
+ const sessions = [];
4653
+ const filesMeta = [];
4654
+ let scanned = 0;
4655
+ let projectDirs = [];
4656
+ try {
4657
+ projectDirs = fs.readdirSync(geminiTmpDir, { withFileTypes: true })
4658
+ .filter(entry => entry.isDirectory())
4659
+ .map(entry => path.join(geminiTmpDir, entry.name));
4660
+ } catch (_) {
4661
+ projectDirs = [];
4662
+ }
4663
+
4664
+ for (const projectDir of projectDirs) {
4665
+ const chatsDir = path.join(projectDir, 'chats');
4666
+ if (!fs.existsSync(chatsDir)) {
4667
+ continue;
4668
+ }
4669
+ let entries = [];
4670
+ try {
4671
+ entries = fs.readdirSync(chatsDir, { withFileTypes: true });
4672
+ } catch (_) {
4673
+ entries = [];
4674
+ }
4675
+ for (const entry of entries) {
4676
+ if (!entry.isFile() || !entry.name.endsWith('.json')) {
4677
+ continue;
4678
+ }
4679
+ const fullPath = path.join(chatsDir, entry.name);
4680
+ try {
4681
+ const stat = fs.statSync(fullPath);
4682
+ filesMeta.push({ filePath: fullPath, mtimeMs: stat.mtimeMs || 0 });
4683
+ } catch (_) {}
4684
+ scanned += 1;
4685
+ if (scanned >= maxFilesScanned) {
4686
+ break;
4687
+ }
4688
+ }
4689
+ if (scanned >= maxFilesScanned) {
4690
+ break;
4691
+ }
4692
+ }
4693
+
4694
+ filesMeta.sort((a, b) => b.mtimeMs - a.mtimeMs);
4695
+ for (const item of filesMeta.slice(0, scanCount)) {
4696
+ const summary = parseGeminiSessionSummary(item.filePath, { summaryReadBytes, titleReadBytes });
4697
+ if (summary) {
4698
+ sessions.push(summary);
4699
+ }
4700
+ if (sessions.length >= targetCount) {
4701
+ break;
4702
+ }
4703
+ }
4704
+
4705
+ return mergeAndLimitSessions(sessions, limit);
4706
+ }
4707
+
4708
+ function listCodeBuddySessions(limit, options = {}) {
4709
+ const projectsDir = getCodeBuddyProjectsDir();
4710
+ if (!fs.existsSync(projectsDir)) {
4711
+ return [];
4712
+ }
4713
+
4714
+ const scanFactor = Number.isFinite(Number(options.scanFactor))
4715
+ ? Math.max(1, Number(options.scanFactor))
4716
+ : SESSION_SCAN_FACTOR;
4717
+ const minFiles = Number.isFinite(Number(options.minFiles))
4718
+ ? Math.max(1, Number(options.minFiles))
4719
+ : Math.min(SESSION_SCAN_MIN_FILES, MAX_SESSION_LIST_SIZE * SESSION_SCAN_FACTOR);
4720
+ const targetCount = Number.isFinite(Number(options.targetCount))
4721
+ ? Math.max(1, Math.floor(Number(options.targetCount)))
4722
+ : Math.max(1, Math.floor(limit * scanFactor));
4723
+ const scanCount = Number.isFinite(Number(options.scanCount))
4724
+ ? Math.max(targetCount, Math.floor(Number(options.scanCount)))
4725
+ : Math.max(targetCount, minFiles);
4726
+ const maxFilesScanned = Number.isFinite(Number(options.maxFilesScanned))
4727
+ ? Math.max(scanCount, Math.floor(Number(options.maxFilesScanned)))
4728
+ : Math.max(scanCount * 2, minFiles);
4729
+ const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
4730
+ ? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
4731
+ : SESSION_SUMMARY_READ_BYTES;
4732
+ const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
4733
+ ? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
4734
+ : SESSION_TITLE_READ_BYTES;
4735
+
4736
+ const files = collectRecentJsonlFiles(projectsDir, {
4737
+ returnCount: scanCount,
4738
+ maxFilesScanned,
4739
+ ignoreSubPath: `${path.sep}subagents${path.sep}`
4740
+ });
4741
+ const sessions = [];
4742
+ for (const filePath of files) {
4743
+ if (path.basename(filePath) === 'history.jsonl') {
4744
+ continue;
4745
+ }
4746
+ const summary = parseCodeBuddySessionSummary(filePath, {
4747
+ summaryReadBytes,
4748
+ titleReadBytes
4749
+ });
4750
+ if (summary) {
4751
+ sessions.push(summary);
4752
+ }
4753
+ if (sessions.length >= targetCount) {
4754
+ break;
4755
+ }
4756
+ }
4757
+ return mergeAndLimitSessions(sessions, limit);
4758
+ }
4759
+
4147
4760
  async function listAllSessions(params = {}) {
4148
- const source = params.source === 'codex' || params.source === 'claude'
4761
+ const source = params.source === 'codex' || params.source === 'claude' || params.source === 'gemini' || params.source === 'codebuddy'
4149
4762
  ? params.source
4150
4763
  : 'all';
4151
4764
  const rawLimit = Number(params.limit);
@@ -4189,6 +4802,12 @@ async function listAllSessions(params = {}) {
4189
4802
  if (source === 'all' || source === 'claude') {
4190
4803
  sessions = sessions.concat(listSessionInventoryBySource('claude', limit, scanOptions, { forceRefresh }));
4191
4804
  }
4805
+ if (source === 'all' || source === 'gemini') {
4806
+ sessions = sessions.concat(listSessionInventoryBySource('gemini', limit, scanOptions, { forceRefresh }));
4807
+ }
4808
+ if (source === 'all' || source === 'codebuddy') {
4809
+ sessions = sessions.concat(listSessionInventoryBySource('codebuddy', limit, scanOptions, { forceRefresh }));
4810
+ }
4192
4811
 
4193
4812
  if (hasPathFilter) {
4194
4813
  sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
@@ -4269,6 +4888,8 @@ async function listSessionUsage(params = {}) {
4269
4888
  listSessionBrowse,
4270
4889
  parseCodexSessionSummary,
4271
4890
  parseClaudeSessionSummary,
4891
+ parseCodeBuddySessionSummary,
4892
+ parseGeminiSessionSummary,
4272
4893
  MAX_SESSION_USAGE_LIST_SIZE,
4273
4894
  SESSION_BROWSE_SUMMARY_READ_BYTES
4274
4895
  });
@@ -4276,10 +4897,10 @@ async function listSessionUsage(params = {}) {
4276
4897
 
4277
4898
  function listSessionPaths(params = {}) {
4278
4899
  const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
4279
- if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
4900
+ if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
4280
4901
  return [];
4281
4902
  }
4282
- const validSource = source === 'codex' || source === 'claude' ? source : 'all';
4903
+ const validSource = source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' ? source : 'all';
4283
4904
  const rawLimit = Number(params.limit);
4284
4905
  const limit = Number.isFinite(rawLimit)
4285
4906
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_PATH_LIST_SIZE))
@@ -4307,6 +4928,12 @@ function listSessionPaths(params = {}) {
4307
4928
  if (validSource === 'all' || validSource === 'claude') {
4308
4929
  sessions = sessions.concat(listSessionInventoryBySource('claude', gatherLimit, scanOptions, { forceRefresh }));
4309
4930
  }
4931
+ if (validSource === 'all' || validSource === 'gemini') {
4932
+ sessions = sessions.concat(listSessionInventoryBySource('gemini', gatherLimit, scanOptions, { forceRefresh }));
4933
+ }
4934
+ if (validSource === 'all' || validSource === 'codebuddy') {
4935
+ sessions = sessions.concat(listSessionInventoryBySource('codebuddy', gatherLimit, scanOptions, { forceRefresh }));
4936
+ }
4310
4937
 
4311
4938
  const dedupedPaths = [];
4312
4939
  const seen = new Set();
@@ -4332,7 +4959,14 @@ function listSessionPaths(params = {}) {
4332
4959
  }
4333
4960
 
4334
4961
  function resolveSessionFilePath(source, filePath, sessionId) {
4335
- const root = source === 'claude' ? getClaudeProjectsDir() : getCodexSessionsDir();
4962
+ const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy'
4963
+ ? source
4964
+ : 'codex';
4965
+ const root = normalizedSource === 'claude'
4966
+ ? getClaudeProjectsDir()
4967
+ : (normalizedSource === 'gemini'
4968
+ ? getGeminiTmpDir()
4969
+ : (normalizedSource === 'codebuddy' ? getCodeBuddyProjectsDir() : getCodexSessionsDir()));
4336
4970
  if (!root || !fs.existsSync(root)) {
4337
4971
  return '';
4338
4972
  }
@@ -4347,7 +4981,7 @@ function resolveSessionFilePath(source, filePath, sessionId) {
4347
4981
 
4348
4982
  if (typeof sessionId === 'string' && sessionId.trim()) {
4349
4983
  const targetId = sessionId.trim().toLowerCase();
4350
- const lookupStore = g_sessionFileLookupCache[source === 'claude' ? 'claude' : 'codex'];
4984
+ const lookupStore = g_sessionFileLookupCache[normalizedSource];
4351
4985
  if (lookupStore instanceof Map && lookupStore.has(targetId)) {
4352
4986
  const cachedPath = lookupStore.get(targetId);
4353
4987
  if (cachedPath && fs.existsSync(cachedPath) && isPathInside(cachedPath, root)) {
@@ -4355,8 +4989,39 @@ function resolveSessionFilePath(source, filePath, sessionId) {
4355
4989
  }
4356
4990
  lookupStore.delete(targetId);
4357
4991
  }
4358
- const files = collectJsonlFiles(root, 5000);
4359
- const matchedFile = files.find(item => path.basename(item, '.jsonl').toLowerCase() === targetId);
4992
+ let matchedFile = '';
4993
+ if (normalizedSource === 'gemini') {
4994
+ const filesMeta = [];
4995
+ let projectDirs = [];
4996
+ try {
4997
+ projectDirs = fs.readdirSync(root, { withFileTypes: true })
4998
+ .filter(entry => entry.isDirectory())
4999
+ .map(entry => path.join(root, entry.name));
5000
+ } catch (_) {
5001
+ projectDirs = [];
5002
+ }
5003
+ for (const projectDir of projectDirs) {
5004
+ const chatsDir = path.join(projectDir, 'chats');
5005
+ if (!fs.existsSync(chatsDir)) continue;
5006
+ let entries = [];
5007
+ try {
5008
+ entries = fs.readdirSync(chatsDir, { withFileTypes: true });
5009
+ } catch (_) {
5010
+ entries = [];
5011
+ }
5012
+ for (const entry of entries) {
5013
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
5014
+ const fullPath = path.join(chatsDir, entry.name);
5015
+ filesMeta.push(fullPath);
5016
+ if (filesMeta.length >= 5000) break;
5017
+ }
5018
+ if (filesMeta.length >= 5000) break;
5019
+ }
5020
+ matchedFile = filesMeta.find(item => path.basename(item, '.json').toLowerCase() === targetId) || '';
5021
+ } else {
5022
+ const files = collectJsonlFiles(root, 5000);
5023
+ matchedFile = files.find(item => path.basename(item, '.jsonl').toLowerCase() === targetId) || '';
5024
+ }
4360
5025
  if (matchedFile && fs.existsSync(matchedFile)) {
4361
5026
  return matchedFile;
4362
5027
  }
@@ -4533,11 +5198,15 @@ function moveFileSync(sourcePath, targetPath) {
4533
5198
 
4534
5199
  function buildSessionSummaryFallback(source, filePath, sessionId = '') {
4535
5200
  const resolvedSessionId = sessionId || path.basename(filePath, '.jsonl');
4536
- const sourceLabel = source === 'claude' ? 'Claude Code' : 'Codex';
5201
+ const sourceLabel = source === 'claude'
5202
+ ? 'Claude Code'
5203
+ : (source === 'gemini' ? 'Gemini CLI' : (source === 'codebuddy' ? 'CodeBuddy Code' : 'Codex'));
4537
5204
  return {
4538
5205
  source,
4539
5206
  sourceLabel,
4540
- provider: source === 'claude' ? 'claude' : 'codex',
5207
+ provider: source === 'claude'
5208
+ ? 'claude'
5209
+ : (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : 'codex')),
4541
5210
  sessionId: resolvedSessionId,
4542
5211
  title: resolvedSessionId,
4543
5212
  cwd: '',
@@ -4548,7 +5217,7 @@ function buildSessionSummaryFallback(source, filePath, sessionId = '') {
4548
5217
  contextWindow: 0,
4549
5218
  filePath,
4550
5219
  keywords: [],
4551
- capabilities: source === 'claude' ? { code: true } : {}
5220
+ capabilities: source === 'claude' || source === 'gemini' || source === 'codebuddy' ? { code: true } : {}
4552
5221
  };
4553
5222
  }
4554
5223
 
@@ -4559,11 +5228,14 @@ function generateSessionTrashId() {
4559
5228
  return `trash-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
4560
5229
  }
4561
5230
 
4562
- function allocateSessionTrashTarget() {
5231
+ function allocateSessionTrashTarget(extension = 'jsonl') {
4563
5232
  ensureDir(SESSION_TRASH_FILES_DIR);
5233
+ const safeExt = typeof extension === 'string' && extension.trim()
5234
+ ? extension.trim().replace(/^\./, '')
5235
+ : 'jsonl';
4564
5236
  for (let attempt = 0; attempt < 6; attempt += 1) {
4565
5237
  const trashId = generateSessionTrashId();
4566
- const trashFileName = `${trashId}.jsonl`;
5238
+ const trashFileName = `${trashId}.${safeExt}`;
4567
5239
  const trashFilePath = path.join(SESSION_TRASH_FILES_DIR, trashFileName);
4568
5240
  if (!fs.existsSync(trashFilePath)) {
4569
5241
  return { trashId, trashFileName, trashFilePath };
@@ -4572,8 +5244,8 @@ function allocateSessionTrashTarget() {
4572
5244
  const fallbackId = `trash-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
4573
5245
  return {
4574
5246
  trashId: fallbackId,
4575
- trashFileName: `${fallbackId}.jsonl`,
4576
- trashFilePath: path.join(SESSION_TRASH_FILES_DIR, `${fallbackId}.jsonl`)
5247
+ trashFileName: `${fallbackId}.${safeExt}`,
5248
+ trashFilePath: path.join(SESSION_TRASH_FILES_DIR, `${fallbackId}.${safeExt}`)
4577
5249
  };
4578
5250
  }
4579
5251
 
@@ -4581,7 +5253,13 @@ function normalizeSessionTrashEntry(entry) {
4581
5253
  if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
4582
5254
  return null;
4583
5255
  }
4584
- const source = entry.source === 'claude' ? 'claude' : (entry.source === 'codex' ? 'codex' : '');
5256
+ const source = entry.source === 'claude'
5257
+ ? 'claude'
5258
+ : (entry.source === 'codex'
5259
+ ? 'codex'
5260
+ : (entry.source === 'gemini'
5261
+ ? 'gemini'
5262
+ : (entry.source === 'codebuddy' ? 'codebuddy' : '')));
4585
5263
  const trashId = typeof entry.trashId === 'string' ? entry.trashId.trim() : '';
4586
5264
  if (!source || !trashId || trashId.includes('/') || trashId.includes('\\') || trashId.includes('\0')) {
4587
5265
  return null;
@@ -4596,7 +5274,9 @@ function normalizeSessionTrashEntry(entry) {
4596
5274
  trashId,
4597
5275
  trashFileName,
4598
5276
  source,
4599
- sourceLabel: source === 'claude' ? 'Claude Code' : 'Codex',
5277
+ sourceLabel: source === 'claude'
5278
+ ? 'Claude Code'
5279
+ : (source === 'gemini' ? 'Gemini CLI' : (source === 'codebuddy' ? 'CodeBuddy Code' : 'Codex')),
4600
5280
  sessionId: sessionId || trashId,
4601
5281
  title: typeof entry.title === 'string' && entry.title.trim() ? entry.title.trim() : (sessionId || trashId),
4602
5282
  cwd: typeof entry.cwd === 'string' ? entry.cwd : '',
@@ -4612,7 +5292,7 @@ function normalizeSessionTrashEntry(entry) {
4612
5292
  originalFilePath: typeof entry.originalFilePath === 'string' ? entry.originalFilePath : '',
4613
5293
  provider: typeof entry.provider === 'string' && entry.provider.trim()
4614
5294
  ? entry.provider.trim()
4615
- : (source === 'claude' ? 'claude' : 'codex'),
5295
+ : (source === 'claude' ? 'claude' : (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : 'codex'))),
4616
5296
  keywords: normalizeKeywords(entry.keywords),
4617
5297
  capabilities: normalizeCapabilities(entry.capabilities),
4618
5298
  claudeIndexPath: typeof entry.claudeIndexPath === 'string' ? entry.claudeIndexPath : '',
@@ -4730,7 +5410,11 @@ function resolveSessionRestoreTarget(entry) {
4730
5410
  if (!normalized) {
4731
5411
  return '';
4732
5412
  }
4733
- const root = normalized.source === 'claude' ? getClaudeProjectsDir() : getCodexSessionsDir();
5413
+ const root = normalized.source === 'claude'
5414
+ ? getClaudeProjectsDir()
5415
+ : (normalized.source === 'gemini'
5416
+ ? getGeminiTmpDir()
5417
+ : (normalized.source === 'codebuddy' ? getCodeBuddyProjectsDir() : getCodexSessionsDir()));
4734
5418
  const originalFilePath = typeof normalized.originalFilePath === 'string' ? normalized.originalFilePath.trim() : '';
4735
5419
  if (!root || !originalFilePath) {
4736
5420
  return '';
@@ -4864,14 +5548,20 @@ function upsertClaudeSessionIndexEntry(indexPath, sessionFilePath, entry) {
4864
5548
  }
4865
5549
 
4866
5550
  async function listSessionTrashItems(params = {}) {
4867
- const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : 'all');
5551
+ const source = params.source === 'claude'
5552
+ ? 'claude'
5553
+ : (params.source === 'codex'
5554
+ ? 'codex'
5555
+ : (params.source === 'gemini'
5556
+ ? 'gemini'
5557
+ : (params.source === 'codebuddy' ? 'codebuddy' : 'all')));
4868
5558
  const countOnly = params.countOnly === true;
4869
5559
  const rawLimit = Number(params.limit);
4870
5560
  const limit = Number.isFinite(rawLimit)
4871
5561
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_TRASH_LIST_SIZE))
4872
5562
  : 200;
4873
5563
  const allEntries = readSessionTrashEntries();
4874
- let items = source === 'codex' || source === 'claude'
5564
+ let items = source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy'
4875
5565
  ? allEntries.filter((entry) => entry.source === source)
4876
5566
  : allEntries.slice();
4877
5567
  items.sort((a, b) => {
@@ -5051,7 +5741,13 @@ async function purgeSessionTrashItems(params = {}) {
5051
5741
  }
5052
5742
 
5053
5743
  async function trashSessionData(params = {}) {
5054
- const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
5744
+ const source = params.source === 'claude'
5745
+ ? 'claude'
5746
+ : (params.source === 'codex'
5747
+ ? 'codex'
5748
+ : (params.source === 'gemini'
5749
+ ? 'gemini'
5750
+ : (params.source === 'codebuddy' ? 'codebuddy' : '')));
5055
5751
  if (!source) {
5056
5752
  return { error: 'Invalid source' };
5057
5753
  }
@@ -5061,14 +5757,18 @@ async function trashSessionData(params = {}) {
5061
5757
  return { error: 'Session file not found' };
5062
5758
  }
5063
5759
 
5064
- const summary = (source === 'claude' ? parseClaudeSessionSummary(filePath) : parseCodexSessionSummary(filePath))
5760
+ const summary = (source === 'claude'
5761
+ ? parseClaudeSessionSummary(filePath)
5762
+ : (source === 'gemini'
5763
+ ? parseGeminiSessionSummary(filePath)
5764
+ : (source === 'codebuddy' ? parseCodeBuddySessionSummary(filePath) : parseCodexSessionSummary(filePath))))
5065
5765
  || buildSessionSummaryFallback(source, filePath, params.sessionId);
5066
5766
  const exactMessageCount = await countConversationMessagesInFile(filePath, source);
5067
5767
  if (Number.isFinite(Number(exactMessageCount))) {
5068
5768
  summary.messageCount = Math.max(0, Math.floor(Number(exactMessageCount)));
5069
5769
  }
5070
- const sessionId = summary.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
5071
- const { trashId, trashFileName, trashFilePath } = allocateSessionTrashTarget();
5770
+ const sessionId = summary.sessionId || params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
5771
+ const { trashId, trashFileName, trashFilePath } = allocateSessionTrashTarget(source === 'gemini' ? 'json' : 'jsonl');
5072
5772
  const deletedAt = new Date().toISOString();
5073
5773
  const claudeIndexPath = source === 'claude' ? findClaudeSessionIndexPath(filePath) : '';
5074
5774
  let removedClaudeIndexEntry = null;
@@ -5152,7 +5852,13 @@ async function trashSessionData(params = {}) {
5152
5852
  }
5153
5853
 
5154
5854
  async function deleteSessionData(params = {}) {
5155
- const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
5855
+ const source = params.source === 'claude'
5856
+ ? 'claude'
5857
+ : (params.source === 'codex'
5858
+ ? 'codex'
5859
+ : (params.source === 'gemini'
5860
+ ? 'gemini'
5861
+ : (params.source === 'codebuddy' ? 'codebuddy' : '')));
5156
5862
  if (!source) {
5157
5863
  return { error: 'Invalid source' };
5158
5864
  }
@@ -5162,7 +5868,7 @@ async function deleteSessionData(params = {}) {
5162
5868
  return { error: 'Session file not found' };
5163
5869
  }
5164
5870
 
5165
- const sessionId = params.sessionId || path.basename(filePath, '.jsonl');
5871
+ const sessionId = params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
5166
5872
  let fileDeleted = false;
5167
5873
  try {
5168
5874
  fs.unlinkSync(filePath);
@@ -5487,6 +6193,38 @@ function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
5487
6193
  }
5488
6194
  }
5489
6195
 
6196
+ function extractCodeBuddyMessageFromRecord(record, state, lineIndex = -1) {
6197
+ if (record && record.timestamp) {
6198
+ state.updatedAt = toIsoTime(record.timestamp, state.updatedAt);
6199
+ }
6200
+
6201
+ if (record && typeof record.sessionId === 'string' && record.sessionId.trim()) {
6202
+ state.sessionId = record.sessionId.trim();
6203
+ }
6204
+
6205
+ if (!state.cwd && record && typeof record.cwd === 'string' && record.cwd.trim()) {
6206
+ state.cwd = record.cwd.trim();
6207
+ }
6208
+
6209
+ if (!record || record.type !== 'message') {
6210
+ return;
6211
+ }
6212
+
6213
+ const role = normalizeRole(record.role);
6214
+ if (role === 'user' || role === 'assistant' || role === 'system') {
6215
+ const content = record.message?.content ?? record.content ?? '';
6216
+ const text = extractMessageText(content);
6217
+ if (text && canAppendMessage(state)) {
6218
+ state.messages.push({
6219
+ role,
6220
+ text,
6221
+ timestamp: toIsoTime(record.timestamp, ''),
6222
+ recordLineIndex: Number.isInteger(lineIndex) ? lineIndex : -1
6223
+ });
6224
+ }
6225
+ }
6226
+ }
6227
+
5490
6228
  function recordHasCodexMessage(record) {
5491
6229
  if (!record || record.type !== 'response_item' || !record.payload) {
5492
6230
  return false;
@@ -5515,10 +6253,23 @@ function recordHasClaudeMessage(record) {
5515
6253
  return !!text;
5516
6254
  }
5517
6255
 
6256
+ function recordHasCodeBuddyMessage(record) {
6257
+ if (!record || record.type !== 'message') {
6258
+ return false;
6259
+ }
6260
+ const role = normalizeRole(record.role);
6261
+ if (role !== 'user' && role !== 'assistant' && role !== 'system') {
6262
+ return false;
6263
+ }
6264
+ const content = record.message?.content ?? record.content ?? '';
6265
+ const text = extractMessageText(content);
6266
+ return !!text;
6267
+ }
6268
+
5518
6269
  function recordHasMessage(record, source) {
5519
- return source === 'codex'
5520
- ? recordHasCodexMessage(record)
5521
- : recordHasClaudeMessage(record);
6270
+ if (source === 'codex') return recordHasCodexMessage(record);
6271
+ if (source === 'codebuddy') return recordHasCodeBuddyMessage(record);
6272
+ return recordHasClaudeMessage(record);
5522
6273
  }
5523
6274
 
5524
6275
  function extractMessagesFromRecords(records, source, options = {}) {
@@ -5536,6 +6287,8 @@ function extractMessagesFromRecords(records, source, options = {}) {
5536
6287
  const record = records[lineIndex];
5537
6288
  if (source === 'codex') {
5538
6289
  extractCodexMessageFromRecord(record, state, lineIndex);
6290
+ } else if (source === 'codebuddy') {
6291
+ extractCodeBuddyMessageFromRecord(record, state, lineIndex);
5539
6292
  } else {
5540
6293
  extractClaudeMessageFromRecord(record, state, lineIndex);
5541
6294
  }
@@ -5597,6 +6350,8 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
5597
6350
 
5598
6351
  if (source === 'codex') {
5599
6352
  extractCodexMessageFromRecord(record, state, currentLineIndex);
6353
+ } else if (source === 'codebuddy') {
6354
+ extractCodeBuddyMessageFromRecord(record, state, currentLineIndex);
5600
6355
  } else {
5601
6356
  extractClaudeMessageFromRecord(record, state, currentLineIndex);
5602
6357
  }
@@ -5621,7 +6376,13 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
5621
6376
  }
5622
6377
 
5623
6378
  async function readSessionDetail(params = {}) {
5624
- const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
6379
+ const source = params.source === 'claude'
6380
+ ? 'claude'
6381
+ : (params.source === 'codex'
6382
+ ? 'codex'
6383
+ : (params.source === 'gemini'
6384
+ ? 'gemini'
6385
+ : (params.source === 'codebuddy' ? 'codebuddy' : '')));
5625
6386
  if (!source) {
5626
6387
  return { error: 'Invalid source' };
5627
6388
  }
@@ -5638,9 +6399,52 @@ async function readSessionDetail(params = {}) {
5638
6399
  : DEFAULT_SESSION_DETAIL_MESSAGES;
5639
6400
  const preview = params.preview === true || params.preview === 'true';
5640
6401
 
5641
- const extracted = await extractSessionDetailPreviewFromFile(filePath, source, messageLimit, { preview });
5642
- const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
5643
- const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
6402
+ let extracted;
6403
+ if (source === 'gemini') {
6404
+ let json;
6405
+ try {
6406
+ json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
6407
+ } catch (_) {
6408
+ json = null;
6409
+ }
6410
+ if (!json || typeof json !== 'object') {
6411
+ return { error: 'Failed to parse session file' };
6412
+ }
6413
+ const rawMessages = Array.isArray(json.messages) ? json.messages : [];
6414
+ const messages = [];
6415
+ for (const entry of rawMessages) {
6416
+ if (!entry || typeof entry !== 'object') continue;
6417
+ const role = normalizeGeminiMessageRole(entry.type);
6418
+ if (!role) continue;
6419
+ const text = extractMessageText(extractGeminiMessageText(entry.content ?? entry.message ?? entry.text));
6420
+ if (!text && role !== 'system') continue;
6421
+ messages.push({
6422
+ role,
6423
+ text,
6424
+ timestamp: toIsoTime(entry.timestamp ?? entry.time ?? entry.at, '')
6425
+ });
6426
+ }
6427
+ const filtered = removeLeadingSystemMessage(messages);
6428
+ const totalMessages = filtered.length;
6429
+ const clipped = totalMessages > messageLimit;
6430
+ const sliced = clipped ? filtered.slice(Math.max(0, totalMessages - messageLimit)) : filtered;
6431
+ extracted = {
6432
+ sessionId: typeof json.sessionId === 'string' && json.sessionId.trim() ? json.sessionId.trim() : path.basename(filePath, '.json'),
6433
+ cwd: typeof json.projectRoot === 'string' ? json.projectRoot : (typeof json.cwd === 'string' ? json.cwd : ''),
6434
+ updatedAt: toIsoTime(json.lastUpdated ?? json.updatedAt, ''),
6435
+ totalMessages,
6436
+ clipped,
6437
+ messages: sliced
6438
+ };
6439
+ } else {
6440
+ extracted = await extractSessionDetailPreviewFromFile(filePath, source, messageLimit, { preview });
6441
+ }
6442
+ const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
6443
+ const sourceLabel = source === 'codex'
6444
+ ? 'Codex'
6445
+ : (source === 'claude'
6446
+ ? 'Claude Code'
6447
+ : (source === 'gemini' ? 'Gemini CLI' : 'CodeBuddy Code'));
5644
6448
  const clippedMessages = Array.isArray(extracted.messages) ? extracted.messages : [];
5645
6449
  const hasExactTotalMessages = Number.isFinite(extracted.totalMessages);
5646
6450
  const startIndex = hasExactTotalMessages
@@ -5674,7 +6478,13 @@ async function readSessionDetail(params = {}) {
5674
6478
  }
5675
6479
 
5676
6480
  async function readSessionPlain(params = {}) {
5677
- const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
6481
+ const source = params.source === 'claude'
6482
+ ? 'claude'
6483
+ : (params.source === 'codex'
6484
+ ? 'codex'
6485
+ : (params.source === 'gemini'
6486
+ ? 'gemini'
6487
+ : (params.source === 'codebuddy' ? 'codebuddy' : '')));
5678
6488
  if (!source) {
5679
6489
  return { error: 'Invalid source' };
5680
6490
  }
@@ -5685,26 +6495,57 @@ async function readSessionPlain(params = {}) {
5685
6495
  }
5686
6496
 
5687
6497
  let extracted;
5688
- try {
5689
- extracted = await extractMessagesFromFile(filePath, source, { maxMessages: Infinity });
5690
- } catch (e) {
5691
- extracted = null;
5692
- }
6498
+ if (source === 'gemini') {
6499
+ let json;
6500
+ try {
6501
+ json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
6502
+ } catch (_) {
6503
+ json = null;
6504
+ }
6505
+ if (!json || typeof json !== 'object') {
6506
+ return { error: 'Failed to parse session file' };
6507
+ }
6508
+ const rawMessages = Array.isArray(json.messages) ? json.messages : [];
6509
+ const messages = [];
6510
+ for (const entry of rawMessages) {
6511
+ if (!entry || typeof entry !== 'object') continue;
6512
+ const role = normalizeGeminiMessageRole(entry.type);
6513
+ if (!role) continue;
6514
+ const text = extractMessageText(extractGeminiMessageText(entry.content ?? entry.message ?? entry.text));
6515
+ if (!text && role !== 'system') continue;
6516
+ messages.push({ role, text });
6517
+ }
6518
+ extracted = {
6519
+ sessionId: typeof json.sessionId === 'string' && json.sessionId.trim() ? json.sessionId.trim() : path.basename(filePath, '.json'),
6520
+ cwd: typeof json.projectRoot === 'string' ? json.projectRoot : '',
6521
+ messages
6522
+ };
6523
+ } else {
6524
+ try {
6525
+ extracted = await extractMessagesFromFile(filePath, source, { maxMessages: Infinity });
6526
+ } catch (e) {
6527
+ extracted = null;
6528
+ }
5693
6529
 
5694
- if (!extracted) {
5695
- return { error: 'Failed to parse session file' };
5696
- }
6530
+ if (!extracted) {
6531
+ return { error: 'Failed to parse session file' };
6532
+ }
5697
6533
 
5698
- if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
5699
- const fallbackRecords = readJsonlRecords(filePath);
5700
- if (fallbackRecords.length === 0) {
5701
- return { error: 'Session file is empty' };
6534
+ if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
6535
+ const fallbackRecords = readJsonlRecords(filePath);
6536
+ if (fallbackRecords.length === 0) {
6537
+ return { error: 'Session file is empty' };
6538
+ }
6539
+ extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
5702
6540
  }
5703
- extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
5704
6541
  }
5705
6542
 
5706
- const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
5707
- const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
6543
+ const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
6544
+ const sourceLabel = source === 'codex'
6545
+ ? 'Codex'
6546
+ : (source === 'claude'
6547
+ ? 'Claude Code'
6548
+ : (source === 'gemini' ? 'Gemini CLI' : 'CodeBuddy Code'));
5708
6549
  const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
5709
6550
  const text = buildSessionPlainText(messages);
5710
6551
 
@@ -5719,7 +6560,13 @@ async function readSessionPlain(params = {}) {
5719
6560
  }
5720
6561
 
5721
6562
  async function exportSessionData(params = {}) {
5722
- const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
6563
+ const source = params.source === 'claude'
6564
+ ? 'claude'
6565
+ : (params.source === 'codex'
6566
+ ? 'codex'
6567
+ : (params.source === 'gemini'
6568
+ ? 'gemini'
6569
+ : (params.source === 'codebuddy' ? 'codebuddy' : '')));
5723
6570
  if (!source) {
5724
6571
  return { error: 'Invalid source' };
5725
6572
  }
@@ -5731,22 +6578,51 @@ async function exportSessionData(params = {}) {
5731
6578
  }
5732
6579
 
5733
6580
  let extracted;
5734
- try {
5735
- extracted = await extractMessagesFromFile(filePath, source, { maxMessages });
5736
- } catch (e) {
5737
- extracted = null;
5738
- }
6581
+ if (source === 'gemini') {
6582
+ let json;
6583
+ try {
6584
+ json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
6585
+ } catch (_) {
6586
+ json = null;
6587
+ }
6588
+ if (!json || typeof json !== 'object') {
6589
+ return { error: 'Failed to parse session file' };
6590
+ }
6591
+ const rawMessages = Array.isArray(json.messages) ? json.messages : [];
6592
+ const messages = [];
6593
+ for (const entry of rawMessages) {
6594
+ if (!entry || typeof entry !== 'object') continue;
6595
+ const role = normalizeGeminiMessageRole(entry.type);
6596
+ if (!role) continue;
6597
+ const text = extractMessageText(extractGeminiMessageText(entry.content ?? entry.message ?? entry.text));
6598
+ if (!text && role !== 'system') continue;
6599
+ messages.push({ role, text, timestamp: toIsoTime(entry.timestamp ?? entry.time ?? entry.at, '') });
6600
+ }
6601
+ extracted = {
6602
+ sessionId: typeof json.sessionId === 'string' && json.sessionId.trim() ? json.sessionId.trim() : path.basename(filePath, '.json'),
6603
+ cwd: typeof json.projectRoot === 'string' ? json.projectRoot : '',
6604
+ updatedAt: toIsoTime(json.lastUpdated ?? json.updatedAt, ''),
6605
+ messages: maxMessages === Infinity ? messages : messages.slice(-maxMessages),
6606
+ truncated: maxMessages !== Infinity && messages.length > maxMessages
6607
+ };
6608
+ } else {
6609
+ try {
6610
+ extracted = await extractMessagesFromFile(filePath, source, { maxMessages });
6611
+ } catch (e) {
6612
+ extracted = null;
6613
+ }
5739
6614
 
5740
- if (!extracted) {
5741
- return { error: 'Failed to parse session file' };
5742
- }
6615
+ if (!extracted) {
6616
+ return { error: 'Failed to parse session file' };
6617
+ }
5743
6618
 
5744
- if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
5745
- const fallbackRecords = readJsonlRecords(filePath);
5746
- if (fallbackRecords.length === 0) {
5747
- return { error: 'Session file is empty' };
6619
+ if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
6620
+ const fallbackRecords = readJsonlRecords(filePath);
6621
+ if (fallbackRecords.length === 0) {
6622
+ return { error: 'Session file is empty' };
6623
+ }
6624
+ extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
5748
6625
  }
5749
- extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
5750
6626
  }
5751
6627
 
5752
6628
  extracted.messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
@@ -5758,9 +6634,13 @@ async function exportSessionData(params = {}) {
5758
6634
  }
5759
6635
  }
5760
6636
 
5761
- const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
6637
+ const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
5762
6638
  const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
5763
- const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
6639
+ const sourceLabel = source === 'codex'
6640
+ ? 'Codex'
6641
+ : (source === 'claude'
6642
+ ? 'Claude Code'
6643
+ : (source === 'gemini' ? 'Gemini CLI' : 'CodeBuddy Code'));
5764
6644
  const truncated = !!extracted.truncated;
5765
6645
  const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
5766
6646
  const markdown = buildSessionMarkdown({
@@ -7303,12 +8183,57 @@ function cmdClaude(baseUrl, apiKey, model, silent = false) {
7303
8183
  }
7304
8184
 
7305
8185
  function commandExists(command, args = '') {
8186
+ const cmd = typeof command === 'string' ? command.trim() : '';
8187
+ const argText = typeof args === 'string' ? args.trim() : '';
8188
+ if (!cmd || cmd.includes('\0') || /[\r\n]/.test(cmd)) {
8189
+ return false;
8190
+ }
8191
+ const argv = argText ? argText.split(/\s+/g).filter(Boolean) : [];
8192
+ const hasSeparators = cmd.includes('/') || cmd.includes('\\');
8193
+ const useShell = process.platform === 'win32' && !hasSeparators;
8194
+ if (useShell) {
8195
+ if (!/^[A-Za-z0-9._-]+$/.test(cmd)) return false;
8196
+ if (argText && /[\r\n;&|<>`$]/.test(argText)) return false;
8197
+ }
7306
8198
  try {
7307
- execSync(`${command} ${args}`, { stdio: 'ignore', shell: process.platform === 'win32' });
7308
- return true;
7309
- } catch (e) {
8199
+ const probe = spawnSync(cmd, argv, {
8200
+ stdio: 'ignore',
8201
+ windowsHide: true,
8202
+ timeout: 5000,
8203
+ shell: useShell
8204
+ });
8205
+ return probe.status === 0;
8206
+ } catch (_) {
8207
+ return false;
8208
+ }
8209
+ }
8210
+
8211
+ function isPrivateNetworkHost(hostname) {
8212
+ const host = typeof hostname === 'string' ? hostname.trim().toLowerCase() : '';
8213
+ if (!host) return true;
8214
+ if (host === 'localhost') return true;
8215
+ const ipVer = net.isIP(host);
8216
+ if (!ipVer) {
8217
+ return false;
8218
+ }
8219
+ if (ipVer === 4) {
8220
+ const parts = host.split('.').map((x) => parseInt(x, 10));
8221
+ if (parts.length !== 4 || parts.some((x) => !Number.isFinite(x))) return true;
8222
+ const [a, b] = parts;
8223
+ if (a === 10) return true;
8224
+ if (a === 127) return true;
8225
+ if (a === 169 && b === 254) return true;
8226
+ if (a === 192 && b === 168) return true;
8227
+ if (a === 172 && b >= 16 && b <= 31) return true;
7310
8228
  return false;
7311
8229
  }
8230
+ if (ipVer === 6) {
8231
+ if (host === '::1') return true;
8232
+ if (host.startsWith('fe80:')) return true;
8233
+ if (host.startsWith('fc') || host.startsWith('fd')) return true;
8234
+ return false;
8235
+ }
8236
+ return false;
7312
8237
  }
7313
8238
 
7314
8239
  function detectPreferredPackageManager() {
@@ -7527,10 +8452,11 @@ function resolveExportOutputPath(outputPath, defaultFileName) {
7527
8452
  }
7528
8453
 
7529
8454
  function printExportSessionUsage() {
7530
- console.log('\n用法: codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
8455
+ console.log('\n用法: codexmate export-session --source <codex|claude|gemini|codebuddy> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
7531
8456
  console.log('\n示例:');
7532
8457
  console.log(' codexmate export-session --source codex --session-id 123456');
7533
8458
  console.log(' codexmate export-session --source claude --file "~/.claude/projects/demo/session.jsonl"');
8459
+ console.log(' codexmate export-session --source codebuddy --file "~/.codebuddy/projects/demo/session.jsonl"');
7534
8460
  console.log(' codexmate export-session --source codex --session-id 123456 --max-messages=all');
7535
8461
  }
7536
8462
 
@@ -7943,6 +8869,234 @@ function writeJsonResponse(res, statusCode, payload) {
7943
8869
  res.end(body, 'utf-8');
7944
8870
  }
7945
8871
 
8872
+ function readJsonRequestBody(req, res, options = {}) {
8873
+ const maxBytes = Number.isFinite(options.maxBytes) ? Math.max(1024, Math.floor(options.maxBytes)) : MAX_API_BODY_SIZE;
8874
+ return new Promise((resolve) => {
8875
+ const chunks = [];
8876
+ let bodySize = 0;
8877
+ let bodyTooLarge = false;
8878
+ req.on('data', (chunk) => {
8879
+ if (bodyTooLarge) return;
8880
+ bodySize += chunk.length;
8881
+ if (bodySize > maxBytes) {
8882
+ bodyTooLarge = true;
8883
+ writeJsonResponse(res, 413, {
8884
+ error: `请求体过大(>${Math.floor(maxBytes / 1024 / 1024)}MB)`
8885
+ });
8886
+ req.destroy();
8887
+ resolve({ ok: false, error: 'payload-too-large' });
8888
+ return;
8889
+ }
8890
+ chunks.push(chunk);
8891
+ });
8892
+ req.on('end', () => {
8893
+ if (bodyTooLarge) return;
8894
+ const rawBuffer = chunks.length ? Buffer.concat(chunks) : Buffer.alloc(0);
8895
+ const rawText = rawBuffer.length ? rawBuffer.toString('utf-8') : '';
8896
+ try {
8897
+ resolve({ ok: true, body: JSON.parse(rawText || '{}'), rawText, rawBuffer });
8898
+ } catch (error) {
8899
+ resolve({ ok: false, error: error && error.message ? error.message : 'invalid json' });
8900
+ }
8901
+ });
8902
+ });
8903
+ }
8904
+
8905
+ function isLoopbackRemoteAddress(value) {
8906
+ const addr = typeof value === 'string' ? value.trim() : '';
8907
+ if (!addr) return false;
8908
+ if (addr === '127.0.0.1' || addr === '::1') return true;
8909
+ if (addr === '::ffff:127.0.0.1') return true;
8910
+ return false;
8911
+ }
8912
+
8913
+ function extractRequestToken(req) {
8914
+ const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
8915
+ const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
8916
+ if (rawAuth) {
8917
+ const match = rawAuth.match(/^bearer\s+(.+)$/i);
8918
+ if (match && match[1]) return match[1].trim();
8919
+ return rawAuth;
8920
+ }
8921
+ const raw = typeof headers['x-codexmate-token'] === 'string' ? headers['x-codexmate-token'].trim() : '';
8922
+ return raw;
8923
+ }
8924
+
8925
+ function readServerToken() {
8926
+ const raw = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string' ? process.env.CODEXMATE_HTTP_TOKEN.trim() : '';
8927
+ return raw;
8928
+ }
8929
+
8930
+ function assertRequestAuthorized(req, res) {
8931
+ const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
8932
+ if (isLoopbackRemoteAddress(remoteAddr)) {
8933
+ return { ok: true, mode: 'loopback' };
8934
+ }
8935
+ const expected = readServerToken();
8936
+ if (!expected) {
8937
+ writeJsonResponse(res, 403, {
8938
+ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN or use --host 127.0.0.1)'
8939
+ });
8940
+ return { ok: false, mode: 'missing-token' };
8941
+ }
8942
+ const actual = extractRequestToken(req);
8943
+ if (!actual || actual !== expected) {
8944
+ writeJsonResponse(res, 401, { error: 'Unauthorized' });
8945
+ return { ok: false, mode: 'unauthorized' };
8946
+ }
8947
+ return { ok: true, mode: 'token' };
8948
+ }
8949
+
8950
+ const g_webhookDeliveryCache = new Map();
8951
+
8952
+ function pruneWebhookDeliveryCache() {
8953
+ const now = Date.now();
8954
+ for (const [key, expiresAt] of g_webhookDeliveryCache.entries()) {
8955
+ if (now >= expiresAt) {
8956
+ g_webhookDeliveryCache.delete(key);
8957
+ }
8958
+ }
8959
+ }
8960
+
8961
+ function rememberWebhookDeliveryId(value, ttlMs = 10 * 60 * 1000) {
8962
+ const id = typeof value === 'string' ? value.trim() : '';
8963
+ if (!id) return { ok: true, seen: false };
8964
+ pruneWebhookDeliveryCache();
8965
+ if (g_webhookDeliveryCache.has(id)) {
8966
+ return { ok: true, seen: true };
8967
+ }
8968
+ g_webhookDeliveryCache.set(id, Date.now() + ttlMs);
8969
+ while (g_webhookDeliveryCache.size > 2000) {
8970
+ const firstKey = g_webhookDeliveryCache.keys().next().value;
8971
+ if (!firstKey) break;
8972
+ g_webhookDeliveryCache.delete(firstKey);
8973
+ }
8974
+ return { ok: true, seen: false };
8975
+ }
8976
+
8977
+ function safeTimingEqual(a, b) {
8978
+ try {
8979
+ const ba = Buffer.isBuffer(a) ? a : Buffer.from(String(a || ''), 'utf-8');
8980
+ const bb = Buffer.isBuffer(b) ? b : Buffer.from(String(b || ''), 'utf-8');
8981
+ if (ba.length !== bb.length) return false;
8982
+ return crypto.timingSafeEqual(ba, bb);
8983
+ } catch (_) {
8984
+ return false;
8985
+ }
8986
+ }
8987
+
8988
+ function verifyGithubWebhookSignature(secret, signatureHeader, rawBuffer) {
8989
+ const key = typeof secret === 'string' ? secret : '';
8990
+ const signature = typeof signatureHeader === 'string' ? signatureHeader.trim() : '';
8991
+ if (!key || !signature || !signature.startsWith('sha256=')) return false;
8992
+ const expected = 'sha256=' + crypto.createHmac('sha256', key).update(rawBuffer || Buffer.alloc(0)).digest('hex');
8993
+ return safeTimingEqual(signature, expected);
8994
+ }
8995
+
8996
+ async function handleAutomationHook(req, res, source) {
8997
+ const method = (req.method || 'GET').toUpperCase();
8998
+ if (method !== 'POST') {
8999
+ writeJsonResponse(res, 405, { error: 'Method Not Allowed' });
9000
+ return;
9001
+ }
9002
+ const deliveryId = typeof (req.headers || {})['x-github-delivery'] === 'string'
9003
+ ? String(req.headers['x-github-delivery'] || '')
9004
+ : (typeof (req.headers || {})['x-gitlab-event-uuid'] === 'string' ? String(req.headers['x-gitlab-event-uuid'] || '') : '');
9005
+ const remember = rememberWebhookDeliveryId(deliveryId);
9006
+ if (remember.seen) {
9007
+ writeJsonResponse(res, 200, { ok: true, deduped: true });
9008
+ return;
9009
+ }
9010
+ const parsedBody = await readJsonRequestBody(req, res);
9011
+ if (!parsedBody.ok) {
9012
+ if (parsedBody.error !== 'payload-too-large') {
9013
+ writeJsonResponse(res, 400, { error: parsedBody.error || 'invalid request body' });
9014
+ }
9015
+ return;
9016
+ }
9017
+ const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
9018
+ const isLoopback = !remoteAddr || isLoopbackRemoteAddress(remoteAddr);
9019
+ const normalizedSource = typeof source === 'string' ? source.trim().toLowerCase() : '';
9020
+ if (normalizedSource === 'github') {
9021
+ const secret = typeof process.env.CODEXMATE_GITHUB_WEBHOOK_SECRET === 'string'
9022
+ ? process.env.CODEXMATE_GITHUB_WEBHOOK_SECRET
9023
+ : '';
9024
+ if (!secret && !isLoopback) {
9025
+ writeJsonResponse(res, 403, { error: 'Remote GitHub webhook is disabled (set CODEXMATE_GITHUB_WEBHOOK_SECRET)' });
9026
+ return;
9027
+ }
9028
+ if (secret) {
9029
+ const signature = (req.headers || {})['x-hub-signature-256'];
9030
+ if (!verifyGithubWebhookSignature(secret, signature, parsedBody.rawBuffer)) {
9031
+ writeJsonResponse(res, 401, { error: 'Invalid webhook signature' });
9032
+ return;
9033
+ }
9034
+ }
9035
+ } else if (normalizedSource === 'gitlab') {
9036
+ const secret = typeof process.env.CODEXMATE_GITLAB_WEBHOOK_SECRET === 'string'
9037
+ ? process.env.CODEXMATE_GITLAB_WEBHOOK_SECRET.trim()
9038
+ : '';
9039
+ if (!secret && !isLoopback) {
9040
+ writeJsonResponse(res, 403, { error: 'Remote GitLab webhook is disabled (set CODEXMATE_GITLAB_WEBHOOK_SECRET)' });
9041
+ return;
9042
+ }
9043
+ if (secret) {
9044
+ const tokenHeader = typeof (req.headers || {})['x-gitlab-token'] === 'string'
9045
+ ? String(req.headers['x-gitlab-token']).trim()
9046
+ : '';
9047
+ if (!tokenHeader || tokenHeader !== secret) {
9048
+ writeJsonResponse(res, 401, { error: 'Invalid webhook token' });
9049
+ return;
9050
+ }
9051
+ }
9052
+ }
9053
+ const payload = parsedBody.body && typeof parsedBody.body === 'object' ? parsedBody.body : {};
9054
+ const eventKey = buildAutomationEventKey(source, req.headers || {}, payload);
9055
+ if (!eventKey) {
9056
+ writeJsonResponse(res, 400, { error: 'unknown event' });
9057
+ return;
9058
+ }
9059
+ const cfg = readAutomationConfig(AUTOMATION_CONFIG_FILE, { env: process.env });
9060
+ if (!cfg.ok) {
9061
+ writeJsonResponse(res, 500, { error: cfg.error || 'failed to load automation config' });
9062
+ return;
9063
+ }
9064
+ const rule = matchAutomationRule(cfg.config, { source, event: eventKey });
9065
+ if (!rule) {
9066
+ writeJsonResponse(res, 404, { error: 'no matching rule', source, event: eventKey });
9067
+ return;
9068
+ }
9069
+ const action = rule.action && typeof rule.action === 'object' ? rule.action : {};
9070
+ const actionType = typeof action.type === 'string' ? action.type.trim().toLowerCase() : '';
9071
+ if (actionType !== 'task.queue.add') {
9072
+ writeJsonResponse(res, 400, { error: 'unsupported rule action', action: actionType || '' });
9073
+ return;
9074
+ }
9075
+ const taskPayload = action.task && typeof action.task === 'object' ? action.task : {};
9076
+ const enqueue = addTaskToQueue(taskPayload);
9077
+ if (enqueue.error) {
9078
+ writeJsonResponse(res, 400, { error: enqueue.error, issues: enqueue.issues || [], warnings: enqueue.warnings || [] });
9079
+ return;
9080
+ }
9081
+ const taskId = enqueue.task && enqueue.task.taskId ? enqueue.task.taskId : '';
9082
+ const shouldStart = action.startQueue === true;
9083
+ const queueResult = shouldStart
9084
+ ? await startTaskQueueProcessing({ taskId: '', detach: true })
9085
+ : { ok: true, started: false };
9086
+ writeJsonResponse(res, 200, {
9087
+ ok: true,
9088
+ ruleId: rule.id,
9089
+ source,
9090
+ event: eventKey,
9091
+ taskId,
9092
+ queue: {
9093
+ started: !!queueResult.started,
9094
+ alreadyRunning: !!queueResult.alreadyRunning,
9095
+ detached: !!queueResult.detached
9096
+ }
9097
+ });
9098
+ }
9099
+
7946
9100
  function streamZipDownloadResponse(res, filePath, options = {}) {
7947
9101
  if (!filePath || !fs.existsSync(filePath)) {
7948
9102
  res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
@@ -8245,9 +9399,50 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8245
9399
 
8246
9400
  const server = http.createServer((req, res) => {
8247
9401
  const requestPath = (req.url || '/').split('?')[0];
9402
+ const sendJson = (statusCode, payload) => {
9403
+ const body = JSON.stringify(payload || {}, null, 2);
9404
+ res.writeHead(statusCode, {
9405
+ 'Content-Type': 'application/json; charset=utf-8',
9406
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
9407
+ });
9408
+ res.end(body, 'utf-8');
9409
+ };
8248
9410
  if (typeof openaiBridgeHandler === 'function' && openaiBridgeHandler(req, res)) {
8249
9411
  return;
8250
9412
  }
9413
+ if (
9414
+ requestPath === '/api'
9415
+ || requestPath.startsWith('/api/import-')
9416
+ || requestPath.startsWith('/hooks/')
9417
+ || requestPath.startsWith('/download/')
9418
+ ) {
9419
+ const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
9420
+ const isLoopback = !remoteAddr
9421
+ || remoteAddr === '127.0.0.1'
9422
+ || remoteAddr === '::1'
9423
+ || remoteAddr === '::ffff:127.0.0.1';
9424
+ if (!isLoopback) {
9425
+ const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
9426
+ ? process.env.CODEXMATE_HTTP_TOKEN.trim()
9427
+ : '';
9428
+ if (!expected) {
9429
+ sendJson(403, {
9430
+ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN or use --host 127.0.0.1)'
9431
+ });
9432
+ return;
9433
+ }
9434
+ const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
9435
+ const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
9436
+ const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
9437
+ const actual = match && match[1]
9438
+ ? match[1].trim()
9439
+ : (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
9440
+ if (!actual || actual !== expected) {
9441
+ sendJson(401, { error: 'Unauthorized' });
9442
+ return;
9443
+ }
9444
+ }
9445
+ }
8251
9446
  if (requestPath === '/api/import-skills-zip') {
8252
9447
  void handleImportSkillsZipUpload(req, res);
8253
9448
  return;
@@ -8256,7 +9451,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8256
9451
  void handleImportSkillsZipUpload(req, res, { targetApp: 'codex' });
8257
9452
  return;
8258
9453
  }
9454
+ if (requestPath.startsWith('/hooks/')) {
9455
+ const segments = requestPath.split('/').filter(Boolean);
9456
+ const source = segments[1] ? String(segments[1]) : '';
9457
+ void handleAutomationHook(req, res, source);
9458
+ return;
9459
+ }
8259
9460
  if (requestPath === '/api') {
9461
+ const method = (req.method ? String(req.method) : 'POST').toUpperCase();
9462
+ if (method !== 'POST') {
9463
+ sendJson(405, { error: 'Method Not Allowed' });
9464
+ return;
9465
+ }
8260
9466
  let body = '';
8261
9467
  let bodySize = 0;
8262
9468
  let bodyTooLarge = false;
@@ -8265,7 +9471,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8265
9471
  bodySize += chunk.length;
8266
9472
  if (bodySize > MAX_API_BODY_SIZE) {
8267
9473
  bodyTooLarge = true;
8268
- writeJsonResponse(res, 413, {
9474
+ sendJson(413, {
8269
9475
  error: `请求体过大(>${Math.floor(MAX_API_BODY_SIZE / 1024 / 1024)}MB)`
8270
9476
  });
8271
9477
  req.destroy();
@@ -8276,7 +9482,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8276
9482
  req.on('end', async () => {
8277
9483
  if (bodyTooLarge) return;
8278
9484
  try {
8279
- const { action, params } = JSON.parse(body);
9485
+ const { action, params } = JSON.parse(body || '{}');
8280
9486
  let result;
8281
9487
 
8282
9488
  switch (action) {
@@ -8345,6 +9551,20 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8345
9551
  if (!baseUrl) {
8346
9552
  result = { error: 'Base URL is required' };
8347
9553
  } else {
9554
+ const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
9555
+ const requesterIsLoopback = !remoteAddr
9556
+ || remoteAddr === '127.0.0.1'
9557
+ || remoteAddr === '::1'
9558
+ || remoteAddr === '::ffff:127.0.0.1';
9559
+ if (!requesterIsLoopback) {
9560
+ try {
9561
+ const parsedUrl = new URL(baseUrl);
9562
+ if (isPrivateNetworkHost(parsedUrl.hostname || '')) {
9563
+ result = { error: 'Refusing to access private network baseUrl from non-loopback request' };
9564
+ break;
9565
+ }
9566
+ } catch (_) {}
9567
+ }
8348
9568
  const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
8349
9569
  if (res.error) {
8350
9570
  result = { error: res.error, models: [], source: 'remote' };
@@ -8557,8 +9777,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8557
9777
  case 'list-sessions':
8558
9778
  {
8559
9779
  const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
8560
- if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
8561
- result = { error: 'Invalid source. Must be codex, claude, or all' };
9780
+ if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
9781
+ result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
8562
9782
  } else {
8563
9783
  result = {
8564
9784
  sessions: await listSessionBrowse(params),
@@ -8571,8 +9791,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8571
9791
  {
8572
9792
  const usageParams = isPlainObject(params) ? params : {};
8573
9793
  const source = typeof usageParams.source === 'string' ? usageParams.source.trim().toLowerCase() : '';
8574
- if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
8575
- result = { error: 'Invalid source. Must be codex, claude, or all' };
9794
+ if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
9795
+ result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
8576
9796
  } else {
8577
9797
  result = {
8578
9798
  sessions: await listSessionUsage({
@@ -8587,8 +9807,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8587
9807
  case 'list-session-paths':
8588
9808
  {
8589
9809
  const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
8590
- if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
8591
- result = { error: 'Invalid source. Must be codex, claude, or all' };
9810
+ if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
9811
+ result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
8592
9812
  } else {
8593
9813
  result = {
8594
9814
  paths: listSessionPaths(params)
@@ -8932,7 +10152,14 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8932
10152
  });
8933
10153
  return;
8934
10154
  }
8935
-
10155
+ const allowLegacy = process.env.CODEXMATE_ALLOW_LEGACY_DOWNLOAD === '1';
10156
+ const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
10157
+ const isLoopback = !remoteAddr || remoteAddr === '127.0.0.1' || remoteAddr === '::1' || remoteAddr === '::ffff:127.0.0.1';
10158
+ if (!allowLegacy || !isLoopback) {
10159
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
10160
+ res.end('Not Found');
10161
+ return;
10162
+ }
8936
10163
  const tempDir = os.tmpdir();
8937
10164
  const legacyFilePath = path.join(tempDir, decodedFileName);
8938
10165
  if (!isPathInside(legacyFilePath, tempDir)) {
@@ -8945,8 +10172,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
8945
10172
  deleteAfterDownload: false
8946
10173
  });
8947
10174
  } else if (requestPath.startsWith('/res/')) {
8948
- const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
8949
- const filePath = path.join(__dirname, normalized);
10175
+ const normalized = path.normalize(requestPath.slice('/res/'.length)).replace(/^([\\.\\/])+/, '');
10176
+ const filePath = path.join(assetsDir, normalized);
8950
10177
  if (!isPathInside(filePath, assetsDir)) {
8951
10178
  res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
8952
10179
  res.end('Forbidden');
@@ -9006,8 +10233,12 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
9006
10233
  }
9007
10234
  console.log(' 退出: Ctrl+C\n');
9008
10235
  if (isAnyAddressHost(host)) {
9009
- console.warn('! 安全提示: 当前监听所有网卡(无鉴权)。');
9010
- console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
10236
+ const tokenEnabled = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string' && process.env.CODEXMATE_HTTP_TOKEN.trim().length > 0;
10237
+ console.warn(`! 安全提示: 当前监听所有网卡(${tokenEnabled ? '已启用鉴权' : '无鉴权'})。`);
10238
+ if (!tokenEnabled) {
10239
+ console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
10240
+ console.warn(' 如需远程访问,请设置 CODEXMATE_HTTP_TOKEN。');
10241
+ }
9011
10242
  }
9012
10243
 
9013
10244
  if (willOpenBrowser) {
@@ -9108,7 +10339,7 @@ function cmdStart(options = {}) {
9108
10339
  const newHtmlPath = path.join(webDir, 'index.html');
9109
10340
  const legacyHtmlPath = path.join(__dirname, 'web-ui.html');
9110
10341
  const htmlPath = fs.existsSync(newHtmlPath) ? newHtmlPath : legacyHtmlPath;
9111
- const assetsDir = path.join(__dirname, 'res');
10342
+ const assetsDir = path.join(webDir, 'res');
9112
10343
  if (!fs.existsSync(htmlPath)) {
9113
10344
  console.error('错误: Web UI 页面不存在(尝试路径: web-ui/index.html, web-ui.html)');
9114
10345
  process.exit(1);
@@ -9133,12 +10364,15 @@ function cmdStart(options = {}) {
9133
10364
  openBrowser: shouldOpenBrowser
9134
10365
  });
9135
10366
 
10367
+ const stopAutomationScheduler = startAutomationScheduler();
10368
+
9136
10369
  // 禁止前端变更侦测与自动重启:避免终端输出噪音与访问时短暂 Connection Refused。
9137
10370
  // 如需热重启,请由开发者自行使用外部 watcher / nodemon 等工具。
9138
10371
  const stopWatch = () => {};
9139
10372
 
9140
10373
  const handleExit = () => {
9141
10374
  stopWatch();
10375
+ stopAutomationScheduler();
9142
10376
  Promise.allSettled([
9143
10377
  serverHandle.stop(),
9144
10378
  stopBuiltinProxyRuntime(),
@@ -10678,7 +11912,7 @@ function buildMcpClaudeSettingsPayload() {
10678
11912
  function normalizeMcpSource(value) {
10679
11913
  const source = typeof value === 'string' ? value.trim().toLowerCase() : '';
10680
11914
  if (!source) return '';
10681
- if (source === 'codex' || source === 'claude' || source === 'all') {
11915
+ if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'all') {
10682
11916
  return source;
10683
11917
  }
10684
11918
  return null;
@@ -10991,7 +12225,7 @@ function createWorkflowToolCatalog() {
10991
12225
  handler: async (args = {}) => {
10992
12226
  const source = normalizeMcpSource(args.source);
10993
12227
  if (source === null) {
10994
- return { error: 'Invalid source. Must be codex, claude, or all' };
12228
+ return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
10995
12229
  }
10996
12230
  return {
10997
12231
  source: source || 'all',
@@ -11363,14 +12597,21 @@ function normalizeTaskQueueItem(raw = {}) {
11363
12597
 
11364
12598
  function readTaskQueueState() {
11365
12599
  const parsed = readJsonObjectFromFile(TASK_QUEUE_FILE, {});
12600
+ if (!parsed.ok && parsed.exists) {
12601
+ return {
12602
+ tasks: [],
12603
+ error: parsed.error || 'failed to read task queue'
12604
+ };
12605
+ }
11366
12606
  if (!parsed.ok || !parsed.exists) {
11367
12607
  return {
11368
- tasks: []
12608
+ tasks: [],
12609
+ error: ''
11369
12610
  };
11370
12611
  }
11371
12612
  const source = parsed.data && typeof parsed.data === 'object' ? parsed.data : {};
11372
12613
  const tasks = Array.isArray(source.tasks) ? source.tasks.map((item) => normalizeTaskQueueItem(item)) : [];
11373
- return { tasks };
12614
+ return { tasks, error: '' };
11374
12615
  }
11375
12616
 
11376
12617
  function writeTaskQueueState(state = {}) {
@@ -11380,17 +12621,58 @@ function writeTaskQueueState(state = {}) {
11380
12621
  });
11381
12622
  }
11382
12623
 
11383
- function upsertTaskQueueItem(item) {
11384
- const state = readTaskQueueState();
11385
- const next = normalizeTaskQueueItem(item || {});
11386
- const index = state.tasks.findIndex((entry) => entry.taskId === next.taskId);
11387
- if (index >= 0) {
11388
- state.tasks[index] = next;
11389
- } else {
11390
- state.tasks.push(next);
12624
+ function withTaskQueueLock(fn) {
12625
+ const lockPath = `${TASK_QUEUE_FILE}.lock`;
12626
+ ensureDir(path.dirname(lockPath));
12627
+ let lockFd = null;
12628
+ try {
12629
+ lockFd = fs.openSync(lockPath, 'wx', 0o600);
12630
+ } catch (error) {
12631
+ const code = error && error.code ? error.code : '';
12632
+ if (code === 'EEXIST') {
12633
+ try {
12634
+ const stat = fs.statSync(lockPath);
12635
+ const ageMs = Date.now() - stat.mtimeMs;
12636
+ if (ageMs > 5000) {
12637
+ try {
12638
+ fs.unlinkSync(lockPath);
12639
+ } catch (_) {}
12640
+ lockFd = fs.openSync(lockPath, 'wx', 0o600);
12641
+ }
12642
+ } catch (_) {}
12643
+ }
12644
+ }
12645
+ if (!lockFd) {
12646
+ return { error: 'task queue is busy' };
11391
12647
  }
11392
- writeTaskQueueState(state);
11393
- return next;
12648
+ try {
12649
+ return fn();
12650
+ } finally {
12651
+ try {
12652
+ fs.closeSync(lockFd);
12653
+ } catch (_) {}
12654
+ try {
12655
+ fs.unlinkSync(lockPath);
12656
+ } catch (_) {}
12657
+ }
12658
+ }
12659
+
12660
+ function upsertTaskQueueItem(item) {
12661
+ return withTaskQueueLock(() => {
12662
+ const state = readTaskQueueState();
12663
+ if (state.error) {
12664
+ return { error: state.error };
12665
+ }
12666
+ const next = normalizeTaskQueueItem(item || {});
12667
+ const index = state.tasks.findIndex((entry) => entry.taskId === next.taskId);
12668
+ if (index >= 0) {
12669
+ state.tasks[index] = next;
12670
+ } else {
12671
+ state.tasks.push(next);
12672
+ }
12673
+ writeTaskQueueState(state);
12674
+ return next;
12675
+ });
11394
12676
  }
11395
12677
 
11396
12678
  function getTaskQueueItem(taskId) {
@@ -11425,15 +12707,19 @@ function appendTaskRunRecord(record) {
11425
12707
  fs.appendFileSync(TASK_RUNS_FILE, `${JSON.stringify(record)}\n`, { encoding: 'utf-8', mode: 0o600 });
11426
12708
  }
11427
12709
 
12710
+ let g_taskRunRecordsLastParseErrors = 0;
12711
+
11428
12712
  function listTaskRunRecords(limit = 20) {
11429
12713
  const max = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 20;
11430
12714
  if (!fs.existsSync(TASK_RUNS_FILE)) {
12715
+ g_taskRunRecordsLastParseErrors = 0;
11431
12716
  return [];
11432
12717
  }
11433
12718
  let content = '';
11434
12719
  try {
11435
12720
  content = fs.readFileSync(TASK_RUNS_FILE, 'utf-8');
11436
12721
  } catch (_) {
12722
+ g_taskRunRecordsLastParseErrors = 0;
11437
12723
  return [];
11438
12724
  }
11439
12725
  const rows = content
@@ -11441,14 +12727,18 @@ function listTaskRunRecords(limit = 20) {
11441
12727
  .map((line) => line.trim())
11442
12728
  .filter(Boolean);
11443
12729
  const parsed = [];
12730
+ let parseErrors = 0;
11444
12731
  for (let i = rows.length - 1; i >= 0; i -= 1) {
11445
12732
  try {
11446
12733
  parsed.push(JSON.parse(rows[i]));
11447
12734
  if (parsed.length >= max) {
11448
12735
  break;
11449
12736
  }
11450
- } catch (_) {}
12737
+ } catch (_) {
12738
+ parseErrors += 1;
12739
+ }
11451
12740
  }
12741
+ g_taskRunRecordsLastParseErrors = parseErrors;
11452
12742
  return parsed;
11453
12743
  }
11454
12744
 
@@ -11510,18 +12800,117 @@ function collectTaskRunSummary(detail = {}) {
11510
12800
  };
11511
12801
  }
11512
12802
 
12803
+ function writeTaskRunArtifacts(detail = {}) {
12804
+ const validation = validateTaskRunId(detail && typeof detail.runId === 'string' ? detail.runId : '');
12805
+ if (!validation.ok) {
12806
+ return;
12807
+ }
12808
+ const run = detail.run && typeof detail.run === 'object' ? detail.run : {};
12809
+ const nodes = Array.isArray(run.nodes) ? run.nodes : [];
12810
+ const dir = path.join(TASK_ARTIFACTS_DIR, validation.runId);
12811
+ ensureDir(dir);
12812
+ writeJsonAtomic(path.join(dir, 'summary.json'), collectTaskRunSummary(detail));
12813
+ const runLogText = summarizeTaskLogs(run.logs || [], 200);
12814
+ const nodeLogText = nodes
12815
+ .map((node) => {
12816
+ const header = node && node.id ? `\n# ${node.id}\n` : '\n# node\n';
12817
+ const text = summarizeTaskLogs(node && node.logs ? node.logs : [], 200);
12818
+ return `${header}${text}`.trimEnd();
12819
+ })
12820
+ .filter(Boolean)
12821
+ .join('\n');
12822
+ const combined = `${runLogText}${nodeLogText ? `\n\n${nodeLogText}` : ''}`.trim();
12823
+ try {
12824
+ fs.writeFileSync(path.join(dir, 'logs.txt'), combined, { encoding: 'utf-8', mode: 0o600 });
12825
+ } catch (_) {}
12826
+ }
12827
+
12828
+ async function notifyAutomationOnTaskRun(detail = {}) {
12829
+ const cfg = readAutomationConfig(AUTOMATION_CONFIG_FILE, { env: process.env });
12830
+ if (!cfg.ok || !cfg.config) {
12831
+ return [];
12832
+ }
12833
+ const payload = formatTaskRunNotificationPayload(detail);
12834
+ const status = String(payload.status || '').toLowerCase();
12835
+ const eventType = status === 'success'
12836
+ ? 'task.completed'
12837
+ : (status === 'failed' ? 'task.failed' : 'task.finished');
12838
+ return await dispatchAutomationNotifiers(cfg.config, eventType, payload);
12839
+ }
12840
+
12841
+ function startAutomationScheduler() {
12842
+ const lastTicks = new Map();
12843
+ let tickInFlight = false;
12844
+ let timer = setInterval(async () => {
12845
+ if (tickInFlight) {
12846
+ return;
12847
+ }
12848
+ tickInFlight = true;
12849
+ try {
12850
+ const cfg = readAutomationConfig(AUTOMATION_CONFIG_FILE, { env: process.env });
12851
+ if (!cfg.ok || !cfg.config) {
12852
+ return;
12853
+ }
12854
+ const schedules = Array.isArray(cfg.config.schedules) ? cfg.config.schedules : [];
12855
+ if (schedules.length === 0) {
12856
+ return;
12857
+ }
12858
+ const now = new Date();
12859
+ const tickKey = now.toISOString().slice(0, 16);
12860
+ for (const schedule of schedules) {
12861
+ if (!schedule || schedule.enabled === false) continue;
12862
+ if (!schedule.id || !schedule.cron) continue;
12863
+ if (!isCronMatch(schedule.cron, now)) continue;
12864
+ if (lastTicks.get(schedule.id) === tickKey) continue;
12865
+ lastTicks.set(schedule.id, tickKey);
12866
+ const action = schedule.action && typeof schedule.action === 'object' ? schedule.action : {};
12867
+ const actionType = typeof action.type === 'string' ? action.type.trim().toLowerCase() : '';
12868
+ if (actionType !== 'task.queue.add') continue;
12869
+ const taskPayload = action.task && typeof action.task === 'object' ? action.task : {};
12870
+ try {
12871
+ const enqueue = addTaskToQueue(taskPayload);
12872
+ if (enqueue && enqueue.error) continue;
12873
+ if (action.startQueue === true) {
12874
+ await startTaskQueueProcessing({ taskId: '', detach: true });
12875
+ }
12876
+ } catch (_) {}
12877
+ }
12878
+ } finally {
12879
+ tickInFlight = false;
12880
+ }
12881
+ }, 30000);
12882
+ if (timer && typeof timer.unref === 'function') {
12883
+ timer.unref();
12884
+ }
12885
+ return () => {
12886
+ if (!timer) return;
12887
+ clearInterval(timer);
12888
+ timer = null;
12889
+ };
12890
+ }
12891
+
11513
12892
  function buildTaskOverviewPayload(options = {}) {
11514
12893
  const queueLimit = Number.isFinite(options.queueLimit) ? Math.max(1, Math.floor(options.queueLimit)) : 20;
11515
12894
  const runLimit = Number.isFinite(options.runLimit) ? Math.max(1, Math.floor(options.runLimit)) : 20;
11516
12895
  const workflowCatalog = buildTaskWorkflowCatalog();
11517
- const queue = listTaskQueueItems({ limit: queueLimit });
12896
+ const queueState = readTaskQueueState();
12897
+ const queue = queueState.error ? [] : listTaskQueueItems({ limit: queueLimit });
11518
12898
  const runs = listTaskRunRecords(runLimit);
12899
+ const warnings = Array.isArray(workflowCatalog.warnings) ? [...workflowCatalog.warnings] : [];
12900
+ if (queueState.error) {
12901
+ warnings.push(`task queue read error: ${queueState.error}`);
12902
+ }
12903
+ if (g_taskRunRecordsLastParseErrors > 0) {
12904
+ warnings.push(`task run history parse errors: ${g_taskRunRecordsLastParseErrors}`);
12905
+ }
11519
12906
  return {
11520
12907
  workflows: workflowCatalog.workflows,
11521
- warnings: workflowCatalog.warnings,
12908
+ warnings,
11522
12909
  queue,
11523
12910
  runs,
11524
- activeRunIds: Array.from(g_taskRunControllers.keys())
12911
+ activeRunIds: Array.from(g_taskRunControllers.keys()),
12912
+ queueError: queueState.error || '',
12913
+ runParseErrors: g_taskRunRecordsLastParseErrors
11525
12914
  };
11526
12915
  }
11527
12916
 
@@ -11819,7 +13208,7 @@ async function runTaskPlanInternal(plan, options = {}) {
11819
13208
  }
11820
13209
  });
11821
13210
  if (options.queueItem) {
11822
- upsertTaskQueueItem({
13211
+ const queued = upsertTaskQueueItem({
11823
13212
  ...options.queueItem,
11824
13213
  taskId,
11825
13214
  status: 'running',
@@ -11829,6 +13218,7 @@ async function runTaskPlanInternal(plan, options = {}) {
11829
13218
  updatedAt: toIsoTime(Date.now()),
11830
13219
  plan
11831
13220
  });
13221
+ if (queued && queued.error) {}
11832
13222
  }
11833
13223
  try {
11834
13224
  const run = await executeTaskPlan(plan, {
@@ -11852,7 +13242,7 @@ async function runTaskPlanInternal(plan, options = {}) {
11852
13242
  };
11853
13243
  writeTaskRunDetail(nextDetail);
11854
13244
  if (options.queueItem) {
11855
- upsertTaskQueueItem({
13245
+ const queued = upsertTaskQueueItem({
11856
13246
  ...options.queueItem,
11857
13247
  taskId,
11858
13248
  status: snapshot.status === 'success'
@@ -11864,6 +13254,7 @@ async function runTaskPlanInternal(plan, options = {}) {
11864
13254
  updatedAt: toIsoTime(Date.now()),
11865
13255
  plan
11866
13256
  });
13257
+ if (queued && queued.error) {}
11867
13258
  }
11868
13259
  }
11869
13260
  });
@@ -11875,8 +13266,12 @@ async function runTaskPlanInternal(plan, options = {}) {
11875
13266
  };
11876
13267
  writeTaskRunDetail(detail);
11877
13268
  appendTaskRunRecord(collectTaskRunSummary(detail));
13269
+ writeTaskRunArtifacts(detail);
13270
+ try {
13271
+ await notifyAutomationOnTaskRun(detail);
13272
+ } catch (_) {}
11878
13273
  if (options.queueItem) {
11879
- upsertTaskQueueItem({
13274
+ const queued = upsertTaskQueueItem({
11880
13275
  ...options.queueItem,
11881
13276
  taskId,
11882
13277
  status: run.status === 'success'
@@ -11888,6 +13283,7 @@ async function runTaskPlanInternal(plan, options = {}) {
11888
13283
  updatedAt: toIsoTime(Date.now()),
11889
13284
  plan
11890
13285
  });
13286
+ if (queued && queued.error) {}
11891
13287
  }
11892
13288
  return detail;
11893
13289
  } finally {
@@ -11923,6 +13319,9 @@ function addTaskToQueue(params = {}) {
11923
13319
  runStatus: '',
11924
13320
  plan
11925
13321
  });
13322
+ if (item && item.error) {
13323
+ return { error: item.error };
13324
+ }
11926
13325
  return {
11927
13326
  ok: true,
11928
13327
  task: item,
@@ -12093,6 +13492,9 @@ function cancelTaskRunOrQueue(params = {}) {
12093
13492
  updatedAt: toIsoTime(Date.now()),
12094
13493
  lastSummary: queueItem.lastSummary || '已取消'
12095
13494
  });
13495
+ if (next && next.error) {
13496
+ return { error: next.error };
13497
+ }
12096
13498
  return {
12097
13499
  ok: true,
12098
13500
  cancelled: true,
@@ -12231,13 +13633,37 @@ function isLiveProcessId(value) {
12231
13633
  }
12232
13634
  }
12233
13635
 
13636
+ function isTaskWorkerProcessId(value) {
13637
+ const pid = Number.isFinite(Number(value)) ? Math.floor(Number(value)) : 0;
13638
+ if (!isLiveProcessId(pid)) return false;
13639
+ if (process.platform === 'linux') {
13640
+ try {
13641
+ const raw = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf-8');
13642
+ return raw.includes('__task-worker');
13643
+ } catch (_) {
13644
+ return true;
13645
+ }
13646
+ }
13647
+ if (process.platform === 'darwin') {
13648
+ try {
13649
+ const probe = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf-8', timeout: 1500 });
13650
+ if (probe.error || probe.status !== 0) return true;
13651
+ const cmd = String(probe.stdout || '');
13652
+ return cmd.includes('__task-worker');
13653
+ } catch (_) {
13654
+ return true;
13655
+ }
13656
+ }
13657
+ return true;
13658
+ }
13659
+
12234
13660
  function readTaskQueueWorkerState() {
12235
13661
  const parsed = readJsonObjectFromFile(TASK_QUEUE_WORKER_FILE, {});
12236
13662
  if (!parsed.ok || !parsed.exists || !parsed.data || typeof parsed.data !== 'object') {
12237
13663
  return null;
12238
13664
  }
12239
13665
  const state = parsed.data;
12240
- if (!isLiveProcessId(state.pid)) {
13666
+ if (!isTaskWorkerProcessId(state.pid)) {
12241
13667
  try {
12242
13668
  fs.unlinkSync(TASK_QUEUE_WORKER_FILE);
12243
13669
  } catch (_) {}
@@ -12492,7 +13918,7 @@ function createMcpTools(options = {}) {
12492
13918
  const input = args && typeof args === 'object' ? args : {};
12493
13919
  const source = normalizeMcpSource(input.source);
12494
13920
  if (source === null) {
12495
- return { error: 'Invalid source. Must be codex, claude, or all' };
13921
+ return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
12496
13922
  }
12497
13923
  const normalizedInput = {
12498
13924
  ...input,
@@ -12941,7 +14367,7 @@ function createMcpResources() {
12941
14367
  contents: [{
12942
14368
  uri,
12943
14369
  mimeType: 'application/json',
12944
- text: JSON.stringify({ error: 'Invalid source. Must be codex, claude, or all' }, null, 2)
14370
+ text: JSON.stringify({ error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' }, null, 2)
12945
14371
  }]
12946
14372
  };
12947
14373
  }
@@ -13187,7 +14613,7 @@ function printMainHelp() {
13187
14613
  console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
13188
14614
  console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
13189
14615
  console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
13190
- console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
14616
+ console.log(' codexmate export-session --source <codex|claude|gemini|codebuddy> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
13191
14617
  console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
13192
14618
  console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
13193
14619
  console.log(' codexmate unzip-ext <zip目录> [输出目录] [--ext:后缀[,后缀...]] [--no-recursive] 批量提取 ZIP 指定后缀文件(默认递归)');