codexmate 0.1.25 → 0.1.28

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 (41) hide show
  1. package/cli/config-bootstrap.js +2 -9
  2. package/cli/session-usage.js +10 -4
  3. package/cli.js +772 -225
  4. package/lib/cli-sessions.js +1 -1
  5. package/package.json +3 -3
  6. package/tools/dev/launch.js +147 -0
  7. package/web-ui/app.js +17 -3
  8. package/web-ui/index.html +12 -1
  9. package/web-ui/logic.prompts-token-count.mjs +49 -0
  10. package/web-ui/logic.sessions.mjs +30 -15
  11. package/web-ui/modules/app.computed.dashboard.mjs +1 -0
  12. package/web-ui/modules/app.computed.prompts.mjs +18 -0
  13. package/web-ui/modules/app.computed.session.mjs +3 -1
  14. package/web-ui/modules/app.methods.index.mjs +2 -0
  15. package/web-ui/modules/app.methods.session-actions.mjs +10 -4
  16. package/web-ui/modules/app.methods.session-browser.mjs +34 -45
  17. package/web-ui/modules/app.methods.session-timeline.mjs +1 -0
  18. package/web-ui/modules/app.methods.theme.mjs +38 -0
  19. package/web-ui/modules/app.methods.web-ui-preferences.mjs +9 -0
  20. package/web-ui/modules/i18n/locales/en.mjs +15 -2
  21. package/web-ui/modules/i18n/locales/ja.mjs +15 -2
  22. package/web-ui/modules/i18n/locales/vi.mjs +15 -2
  23. package/web-ui/modules/i18n/locales/zh-tw.mjs +15 -2
  24. package/web-ui/modules/i18n/locales/zh.mjs +15 -2
  25. package/web-ui/partials/index/layout-header.html +4 -2
  26. package/web-ui/partials/index/panel-prompts.html +14 -0
  27. package/web-ui/partials/index/panel-sessions.html +21 -6
  28. package/web-ui/partials/index/panel-settings.html +24 -0
  29. package/web-ui/res/web-ui-render.precompiled.js +115 -23
  30. package/web-ui/session-helpers.mjs +1 -1
  31. package/web-ui/styles/base-theme.css +25 -0
  32. package/web-ui/styles/controls-forms.css +1 -1
  33. package/web-ui/styles/modals-core.css +48 -0
  34. package/web-ui/styles/sessions-list.css +1 -1
  35. package/web-ui/styles/sessions-preview.css +56 -7
  36. package/web-ui/styles/sessions-toolbar-trash.css +2 -8
  37. package/web-ui/styles/sessions-usage.css +1 -1
  38. package/web-ui/styles/settings-panel.css +2 -2
  39. package/web-ui/styles/theme-dark-tokens.css +83 -0
  40. package/web-ui/styles/theme-dark.css +1355 -0
  41. package/web-ui/styles.css +1 -0
package/cli.js CHANGED
@@ -64,6 +64,18 @@ const {
64
64
  parseMaxMessagesValue,
65
65
  resolveMaxMessagesValue
66
66
  } = require('./lib/cli-session-utils');
67
+ let DatabaseSync = null;
68
+ function getNodeSqliteDatabaseSync() {
69
+ if (DatabaseSync !== null) {
70
+ return DatabaseSync;
71
+ }
72
+ try {
73
+ DatabaseSync = require('node:sqlite').DatabaseSync || false;
74
+ } catch (_) {
75
+ DatabaseSync = false;
76
+ }
77
+ return DatabaseSync;
78
+ }
67
79
  const { createMcpStdioServer } = require('./lib/mcp-stdio');
68
80
  const {
69
81
  validateWorkflowDefinition,
@@ -212,6 +224,8 @@ const CLAUDE_MD_FILE_NAME = 'CLAUDE.md';
212
224
  const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
213
225
  const CODEBUDDY_DIR = path.join(os.homedir(), '.codebuddy');
214
226
  const CODEBUDDY_PROJECTS_DIR = path.join(CODEBUDDY_DIR, 'projects');
227
+ const WORKBUDDY_DIR = path.join(os.homedir(), '.workbuddy-ai');
228
+ const WORKBUDDY_PROJECTS_DIR = path.join(WORKBUDDY_DIR, 'projects');
215
229
  const CODEXMATE_DIR = path.join(os.homedir(), '.codexmate');
216
230
  const PROVIDER_CACHE_FILE_GROUPS = Object.freeze({
217
231
  claude: [
@@ -247,6 +261,9 @@ const GEMINI_DIR = path.join(os.homedir(), '.gemini');
247
261
  const GEMINI_TMP_DIR = path.join(GEMINI_DIR, 'tmp');
248
262
  const PI_DIR = path.join(os.homedir(), '.pi');
249
263
  const PI_SESSIONS_DIR = path.join(PI_DIR, 'agent', 'sessions');
264
+ const OPENCODE_DATA_DIR = path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'opencode');
265
+ const OPENCODE_DB_VIRTUAL_ROOT = path.join(OPENCODE_DATA_DIR, 'sessions');
266
+ const OPENCODE_DB_READ_LIMIT = 2000;
250
267
  const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
251
268
  const WORKFLOW_DEFINITIONS_FILE = path.join(CONFIG_DIR, 'codexmate-workflows.json');
252
269
  const WORKFLOW_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-workflow-runs.jsonl');
@@ -271,6 +288,7 @@ const SESSION_LIST_CACHE_TTL_MS = 4000;
271
288
  const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
272
289
  const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
273
290
  const SESSION_PREVIEW_MESSAGE_TEXT_MAX_LENGTH = 4000;
291
+ const SESSION_QUERY_MATCH_POSITIONS_LIMIT = 500;
274
292
  const EXACT_MESSAGE_COUNT_CACHE_MAX_ENTRIES = 800;
275
293
  const DEFAULT_CONTENT_SCAN_LIMIT = 50;
276
294
  const SESSION_SCAN_FACTOR = 4;
@@ -739,6 +757,7 @@ base_url = "http://127.0.0.1:3737/bridge/local/v1"
739
757
  wire_api = "responses"
740
758
  requires_openai_auth = true
741
759
  preferred_auth_method = "codexmate"
760
+ experimental_bearer_token = "codexmate"
742
761
  codexmate_bridge = "local"
743
762
  request_max_retries = 4
744
763
  stream_max_retries = 10
@@ -752,6 +771,7 @@ let g_sessionFileLookupCache = {
752
771
  claude: new Map(),
753
772
  gemini: new Map(),
754
773
  codebuddy: new Map(),
774
+ workbuddy: new Map(),
755
775
  pi: new Map()
756
776
  };
757
777
  let g_exactMessageCountCache = new Map();
@@ -874,17 +894,27 @@ function writeCurrentModels(data) {
874
894
  fs.writeFileSync(CURRENT_MODELS_FILE, JSON.stringify(data, null, 2), 'utf-8');
875
895
  }
876
896
 
877
- function updateAuthJson(apiKey) {
878
- assertToolConfigWriteAllowed('codex');
879
- let authData = {};
880
- if (fs.existsSync(AUTH_FILE)) {
881
- try {
882
- const content = fs.readFileSync(AUTH_FILE, 'utf-8');
883
- if (content.trim()) authData = JSON.parse(content);
884
- } catch (e) { }
897
+ function ensureActiveProviderBearerToken(content, providerName, providerSegments = null) {
898
+ // 单文件认证:auth.json 不再写入,激活 provider 块缺 experimental_bearer_token 时
899
+ // 从 preferred_auth_method(内部键仓)惰性补写,保证 codex 仅凭 config.toml 取到密钥。
900
+ const ranges = findProviderSectionRanges(content, providerName, providerSegments);
901
+ if (!ranges.length) return content;
902
+ let updated = content;
903
+ const sorted = [...ranges].sort((a, b) => b.start - a.start);
904
+ for (const range of sorted) {
905
+ const block = updated.slice(range.start, range.end);
906
+ if (/(^|\n)\s*experimental_bearer_token\s*=/.test(block)) continue;
907
+ const prefMatch = block.match(/^(\s*)preferred_auth_method\s*=\s*"((?:\\.|[^"\\\n])*)"\s*(?:#.*)?$/m);
908
+ if (!prefMatch || !prefMatch[2].trim()) continue;
909
+ const insertLine = `${prefMatch[1]}experimental_bearer_token = "${prefMatch[2]}"`;
910
+ const lineEndIndex = block.indexOf('\n', prefMatch.index + prefMatch[0].length);
911
+ const insertAt = lineEndIndex === -1
912
+ ? range.start + block.length
913
+ : range.start + lineEndIndex + 1;
914
+ const prefix = lineEndIndex === -1 ? '\n' : '';
915
+ updated = updated.slice(0, insertAt) + prefix + insertLine + '\n' + updated.slice(insertAt);
885
916
  }
886
- authData['OPENAI_API_KEY'] = apiKey;
887
- fs.writeFileSync(AUTH_FILE, JSON.stringify(authData, null, 2), { encoding: 'utf-8', mode: 0o600 });
917
+ return updated;
888
918
  }
889
919
 
890
920
  function isPlainObject(value) {
@@ -1010,9 +1040,13 @@ function normalizeSidebarCollapsedPreference(value) {
1010
1040
  return normalizeBooleanPreference(value, false);
1011
1041
  }
1012
1042
 
1043
+ function normalizeThemePreference(value) {
1044
+ return value === 'dark' ? 'dark' : 'light';
1045
+ }
1046
+
1013
1047
  function normalizeSessionFilterSourcePreference(value) {
1014
1048
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
1015
- if (normalized === 'all' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'pi') return normalized;
1049
+ if (normalized === 'all' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'workbuddy' || normalized === 'pi') return normalized;
1016
1050
  return 'codex';
1017
1051
  }
1018
1052
 
@@ -1081,6 +1115,7 @@ function normalizeWebUiPreferences(value = {}) {
1081
1115
  sysPromptMode: normalizeSysPromptModePreference(source.sysPromptMode),
1082
1116
  projectClaudeMdPath: typeof source.projectClaudeMdPath === 'string' ? source.projectClaudeMdPath : '',
1083
1117
  sidebarCollapsed: normalizeSidebarCollapsedPreference(source.sidebarCollapsed),
1118
+ theme: normalizeThemePreference(source.theme),
1084
1119
  starPrompted: normalizeBooleanPreference(source.starPrompted, false),
1085
1120
  sessionLoadNativeDialog: normalizeBooleanPreference(source.sessionLoadNativeDialog, false),
1086
1121
  language: typeof source.language === 'string' ? source.language : '',
@@ -1195,7 +1230,6 @@ function getApiToolConfigWriteTarget(action) {
1195
1230
  const opencodeWriteActions = new Set([
1196
1231
  'apply-opencode-config',
1197
1232
  'update-opencode-selection',
1198
- 'apply-opencode-agents-file'
1199
1233
  ]);
1200
1234
  const kilocodeWriteActions = new Set([
1201
1235
  'apply-kilocode-config',
@@ -1531,6 +1565,7 @@ const PROVIDER_CONFIG_KEYS = new Set([
1531
1565
  'wire_api',
1532
1566
  'requires_openai_auth',
1533
1567
  'preferred_auth_method',
1568
+ 'experimental_bearer_token',
1534
1569
  'request_max_retries',
1535
1570
  'stream_max_retries',
1536
1571
  'stream_idle_timeout_ms'
@@ -2114,6 +2149,16 @@ function getCodeBuddyProjectsDir() {
2114
2149
  return resolveExistingDir(candidates, CODEBUDDY_PROJECTS_DIR);
2115
2150
  }
2116
2151
 
2152
+ function getWorkBuddyProjectsDir() {
2153
+ const candidates = [];
2154
+ const envHome = process.env.WORKBUDDY_AI_HOME_DIR || process.env.WORKBUDDY_HOME;
2155
+ if (envHome) {
2156
+ candidates.push(path.join(envHome, 'projects'));
2157
+ }
2158
+ candidates.push(WORKBUDDY_PROJECTS_DIR);
2159
+ return resolveExistingDir(candidates, WORKBUDDY_PROJECTS_DIR);
2160
+ }
2161
+
2117
2162
  function getPiSessionsDir() {
2118
2163
  const candidates = [];
2119
2164
  const envPiHome = process.env.PI_HOME;
@@ -2128,6 +2173,250 @@ function getPiSessionsDir() {
2128
2173
  return resolveExistingDir(candidates, PI_SESSIONS_DIR);
2129
2174
  }
2130
2175
 
2176
+ function getOpencodeDataDir() {
2177
+ const candidates = [];
2178
+ const envOpencodeData = process.env.OPENCODE_DATA_HOME || process.env.OPENCODE_DATA_DIR;
2179
+ if (envOpencodeData) {
2180
+ candidates.push(envOpencodeData);
2181
+ }
2182
+ const xdgData = process.env.XDG_DATA_HOME;
2183
+ if (xdgData) {
2184
+ candidates.push(path.join(xdgData, 'opencode'));
2185
+ }
2186
+ candidates.push(OPENCODE_DATA_DIR);
2187
+ return resolveExistingDir(candidates, OPENCODE_DATA_DIR);
2188
+ }
2189
+
2190
+ function getOpencodeDbFile() {
2191
+ const dataDir = getOpencodeDataDir();
2192
+ return path.join(dataDir, 'opencode.db');
2193
+ }
2194
+
2195
+ function buildOpencodeVirtualSessionPath(sessionId) {
2196
+ const id = typeof sessionId === 'string' ? sessionId.trim() : '';
2197
+ if (!id) return '';
2198
+ return path.join(OPENCODE_DB_VIRTUAL_ROOT, `${id}.jsonl`);
2199
+ }
2200
+
2201
+ function parseOpencodeVirtualSessionId(filePath) {
2202
+ const raw = typeof filePath === 'string' ? filePath.trim() : '';
2203
+ if (!raw) return '';
2204
+ const base = path.basename(raw, path.extname(raw));
2205
+ return base && base.startsWith('ses_') ? base : '';
2206
+ }
2207
+
2208
+ function isOpencodeDbAvailable() {
2209
+ const Database = getNodeSqliteDatabaseSync();
2210
+ const dbFile = getOpencodeDbFile();
2211
+ return !!(Database && dbFile && fs.existsSync(dbFile));
2212
+ }
2213
+
2214
+ function withOpencodeDb(callback, fallback = null) {
2215
+ const Database = getNodeSqliteDatabaseSync();
2216
+ const dbFile = getOpencodeDbFile();
2217
+ if (!Database || !dbFile || !fs.existsSync(dbFile)) {
2218
+ return fallback;
2219
+ }
2220
+ let db;
2221
+ try {
2222
+ db = new Database(dbFile, { readOnly: true });
2223
+ return callback(db, dbFile);
2224
+ } catch (_) {
2225
+ return fallback;
2226
+ } finally {
2227
+ if (db) {
2228
+ try { db.close(); } catch (_) { }
2229
+ }
2230
+ }
2231
+ }
2232
+
2233
+ function parseJsonObjectSafe(value) {
2234
+ if (!value || typeof value !== 'string') return null;
2235
+ try {
2236
+ const parsed = JSON.parse(value);
2237
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
2238
+ } catch (_) {
2239
+ return null;
2240
+ }
2241
+ }
2242
+
2243
+ function toOpencodeIsoTime(value, fallback = '') {
2244
+ const numeric = Number(value);
2245
+ if (Number.isFinite(numeric) && numeric > 0) {
2246
+ return toIsoTime(numeric, fallback);
2247
+ }
2248
+ return toIsoTime(value, fallback);
2249
+ }
2250
+
2251
+ function readOpencodeModelName(value) {
2252
+ const parsed = typeof value === 'string' ? parseJsonObjectSafe(value) : (value && typeof value === 'object' ? value : null);
2253
+ if (!parsed) return '';
2254
+ return typeof parsed.id === 'string' && parsed.id.trim()
2255
+ ? parsed.id.trim()
2256
+ : (typeof parsed.modelID === 'string' ? parsed.modelID.trim() : '');
2257
+ }
2258
+
2259
+ function readOpencodeProviderName(value) {
2260
+ const parsed = typeof value === 'string' ? parseJsonObjectSafe(value) : (value && typeof value === 'object' ? value : null);
2261
+ if (!parsed) return 'opencode';
2262
+ return typeof parsed.providerID === 'string' && parsed.providerID.trim() ? parsed.providerID.trim() : 'opencode';
2263
+ }
2264
+
2265
+ function extractOpencodeTextFromPartData(data) {
2266
+ const part = typeof data === 'string' ? parseJsonObjectSafe(data) : (data && typeof data === 'object' ? data : null);
2267
+ if (!part) return '';
2268
+ const type = typeof part.type === 'string' ? part.type.trim().toLowerCase() : '';
2269
+ if (type !== 'text' && type !== 'reasoning') return '';
2270
+ return typeof part.text === 'string' ? part.text.trim() : '';
2271
+ }
2272
+
2273
+ function normalizeOpencodeMessageRow(row) {
2274
+ if (!row || typeof row !== 'object') return null;
2275
+ const messageData = parseJsonObjectSafe(row.message_data || row.data || '');
2276
+ const role = normalizeRole((messageData && messageData.role) || row.role || '');
2277
+ if (role !== 'user' && role !== 'assistant' && role !== 'system') return null;
2278
+ const rawParts = typeof row.parts === 'string' && row.parts ? row.parts.split('\n') : [];
2279
+ const textParts = rawParts.map(extractOpencodeTextFromPartData).filter(Boolean);
2280
+ const text = textParts.join('\n').trim();
2281
+ if (!text) return null;
2282
+ const model = messageData ? (messageData.modelID || readOpencodeModelName(messageData.model)) : '';
2283
+ const provider = messageData ? (messageData.providerID || readOpencodeProviderName(messageData.model)) : 'opencode';
2284
+ return {
2285
+ role,
2286
+ text,
2287
+ timestamp: toOpencodeIsoTime(row.time_created || (messageData && messageData.time && messageData.time.created), ''),
2288
+ model: typeof model === 'string' ? model : '',
2289
+ provider: typeof provider === 'string' && provider ? provider : 'opencode'
2290
+ };
2291
+ }
2292
+
2293
+ function queryOpencodeSessionMessages(sessionId, options = {}) {
2294
+ const id = typeof sessionId === 'string' ? sessionId.trim() : '';
2295
+ if (!id) return null;
2296
+ const maxMessages = resolveMaxMessagesValue(options.maxMessages, DEFAULT_SESSION_DETAIL_MESSAGES);
2297
+ const limit = maxMessages === Infinity ? OPENCODE_DB_READ_LIMIT : Math.min(OPENCODE_DB_READ_LIMIT, Math.max(1, Math.floor(maxMessages)));
2298
+ return withOpencodeDb((db) => {
2299
+ const totalRow = db.prepare('select count(*) as count from message where session_id = ?').get(id) || {};
2300
+ const totalMessages = Math.max(0, Math.floor(Number(totalRow.count) || 0));
2301
+ const rows = db.prepare(`
2302
+ select m.id as message_id, m.session_id, m.time_created, m.time_updated, m.data as message_data,
2303
+ group_concat(p.data, char(10)) as parts
2304
+ from (
2305
+ select * from message where session_id = ? order by time_created desc limit ?
2306
+ ) m
2307
+ left join part p on p.message_id = m.id
2308
+ group by m.id
2309
+ order by m.time_created asc
2310
+ `).all(id, limit);
2311
+ const messages = rows.map(normalizeOpencodeMessageRow).filter(Boolean);
2312
+ const sessionRow = db.prepare('select id, directory, path, time_updated from session where id = ?').get(id) || null;
2313
+ return {
2314
+ sessionId: id,
2315
+ cwd: sessionRow && (sessionRow.path || sessionRow.directory) ? (sessionRow.path || sessionRow.directory) : '',
2316
+ updatedAt: sessionRow ? toOpencodeIsoTime(sessionRow.time_updated, '') : '',
2317
+ messages,
2318
+ totalMessages,
2319
+ clipped: maxMessages !== Infinity && totalMessages > messages.length,
2320
+ truncated: maxMessages !== Infinity && totalMessages > messages.length
2321
+ };
2322
+ }, null);
2323
+ }
2324
+
2325
+ function countOpencodeMessages(sessionId) {
2326
+ const id = typeof sessionId === 'string' ? sessionId.trim() : '';
2327
+ if (!id) return null;
2328
+ return withOpencodeDb((db) => {
2329
+ const row = db.prepare('select count(*) as count from message where session_id = ?').get(id) || {};
2330
+ return Math.max(0, Math.floor(Number(row.count) || 0));
2331
+ }, null);
2332
+ }
2333
+
2334
+ function parseOpencodeSessionSummaryRow(row, db) {
2335
+ if (!row || typeof row !== 'object') return null;
2336
+ const sessionId = typeof row.id === 'string' ? row.id.trim() : '';
2337
+ if (!sessionId) return null;
2338
+ const model = readOpencodeModelName(row.model);
2339
+ const provider = readOpencodeProviderName(row.model);
2340
+ let title = typeof row.title === 'string' && row.title.trim() ? row.title.trim() : (row.slug || sessionId);
2341
+ const firstRow = db.prepare(`
2342
+ select m.data as message_data, group_concat(p.data, char(10)) as parts
2343
+ from message m left join part p on p.message_id = m.id
2344
+ where m.session_id = ?
2345
+ group by m.id
2346
+ order by m.time_created asc
2347
+ limit 12
2348
+ `).all(sessionId).map(normalizeOpencodeMessageRow).filter(Boolean).find(item => item.role === 'user' && item.text);
2349
+ if (firstRow && firstRow.text) {
2350
+ title = truncateText(firstRow.text, 120);
2351
+ }
2352
+ const messageCount = Math.max(0, Math.floor(Number(row.message_count) || 0));
2353
+ const inputTokens = readNonNegativeInteger(row.tokens_input) || 0;
2354
+ const outputTokens = readNonNegativeInteger(row.tokens_output) || 0;
2355
+ const reasoningOutputTokens = readNonNegativeInteger(row.tokens_reasoning) || 0;
2356
+ const cachedInputTokens = readNonNegativeInteger(row.tokens_cache_read) || 0;
2357
+ const cacheCreationInputTokens = readNonNegativeInteger(row.tokens_cache_write) || 0;
2358
+ const totalTokens = inputTokens + outputTokens + reasoningOutputTokens + cachedInputTokens + cacheCreationInputTokens;
2359
+ return {
2360
+ source: 'opencode',
2361
+ sourceLabel: 'OpenCode',
2362
+ provider,
2363
+ model,
2364
+ models: model ? [model] : [],
2365
+ sessionId,
2366
+ title,
2367
+ cwd: row.path || row.directory || '',
2368
+ createdAt: toOpencodeIsoTime(row.time_created, ''),
2369
+ updatedAt: toOpencodeIsoTime(row.time_updated, ''),
2370
+ messageCount,
2371
+ totalTokens,
2372
+ contextWindow: 0,
2373
+ inputTokens,
2374
+ cachedInputTokens,
2375
+ cacheCreationInputTokens,
2376
+ outputTokens,
2377
+ reasoningOutputTokens,
2378
+ __messageCountExact: true,
2379
+ filePath: buildOpencodeVirtualSessionPath(sessionId),
2380
+ keywords: [],
2381
+ capabilities: { code: true }
2382
+ };
2383
+ }
2384
+
2385
+ function parseOpencodeSessionSummary(sessionId) {
2386
+ const id = typeof sessionId === 'string' ? sessionId.trim() : '';
2387
+ if (!id) return null;
2388
+ return withOpencodeDb((db) => {
2389
+ const row = db.prepare(`
2390
+ select s.*, (select count(*) from message m where m.session_id = s.id) as message_count
2391
+ from session s where s.id = ?
2392
+ `).get(id);
2393
+ return parseOpencodeSessionSummaryRow(row, db);
2394
+ }, null);
2395
+ }
2396
+
2397
+ function listOpencodeSessions(limit, options = {}) {
2398
+ if (!isOpencodeDbAvailable()) return [];
2399
+ const scanFactor = Number.isFinite(Number(options.scanFactor)) ? Math.max(1, Number(options.scanFactor)) : SESSION_SCAN_FACTOR;
2400
+ const targetCount = Number.isFinite(Number(options.targetCount)) ? Math.max(1, Math.floor(Number(options.targetCount))) : Math.max(1, Math.floor(limit * scanFactor));
2401
+ const scanCount = Number.isFinite(Number(options.scanCount)) ? Math.max(targetCount, Math.floor(Number(options.scanCount))) : Math.max(targetCount, Math.min(SESSION_SCAN_MIN_FILES, MAX_SESSION_LIST_SIZE * SESSION_SCAN_FACTOR));
2402
+ return withOpencodeDb((db) => {
2403
+ const rows = db.prepare(`
2404
+ select s.*, (select count(*) from message m where m.session_id = s.id) as message_count
2405
+ from session s
2406
+ where s.time_archived is null
2407
+ order by s.time_updated desc
2408
+ limit ?
2409
+ `).all(Math.min(OPENCODE_DB_READ_LIMIT, Math.max(scanCount, targetCount)));
2410
+ const sessions = [];
2411
+ for (const row of rows) {
2412
+ const summary = parseOpencodeSessionSummaryRow(row, db);
2413
+ if (summary) sessions.push(summary);
2414
+ if (sessions.length >= targetCount) break;
2415
+ }
2416
+ return mergeAndLimitSessions(sessions, limit);
2417
+ }, []);
2418
+ }
2419
+
2131
2420
  function getCodexmateDerivedSessionsRoot(target) {
2132
2421
  if (target === 'claude') {
2133
2422
  return CODEXMATE_DERIVED_CLAUDE_DIR;
@@ -2481,7 +2770,6 @@ const {
2481
2770
  writeModels,
2482
2771
  readCurrentModels,
2483
2772
  writeCurrentModels,
2484
- updateAuthJson,
2485
2773
  CONFIG_DIR,
2486
2774
  CONFIG_FILE,
2487
2775
  AUTH_FILE,
@@ -2799,8 +3087,8 @@ function applyConfigTemplate(params = {}) {
2799
3087
  return { error: `模板中找不到当前 provider: ${activeProvider}` };
2800
3088
  }
2801
3089
 
2802
- writeConfig(template.trim() + '\n');
2803
- updateAuthJson(activeProviderBlock.preferred_auth_method || '');
3090
+ const ensuredTemplate = ensureActiveProviderBearerToken(template.trim(), activeProvider);
3091
+ writeConfig(ensuredTemplate + '\n');
2804
3092
 
2805
3093
  const models = readModels();
2806
3094
  if (!models.includes(parsed.model)) {
@@ -3038,6 +3326,7 @@ function addProviderToConfig(params = {}) {
3038
3326
  `wire_api = "responses"`,
3039
3327
  `requires_openai_auth = true`,
3040
3328
  `preferred_auth_method = "${safeKey}"`,
3329
+ `experimental_bearer_token = "${safeKey}"`,
3041
3330
  `models = [{ id = "${escapeTomlBasicString(model)}", name = "${escapeTomlBasicString(model)}" }]`,
3042
3331
  ...extraLines,
3043
3332
  `request_max_retries = 4`,
@@ -4084,7 +4373,7 @@ function countConversationMessagesInRecords(records, source) {
4084
4373
  }
4085
4374
  continue;
4086
4375
  }
4087
- if (source === 'codebuddy') {
4376
+ if (source === 'codebuddy' || source === 'workbuddy') {
4088
4377
  if (record && record.type === 'message') {
4089
4378
  const role = normalizeRole(record.role);
4090
4379
  if (role === 'assistant' || role === 'user' || role === 'system') {
@@ -4167,7 +4456,7 @@ async function countConversationMessagesInFile(filePath, source) {
4167
4456
  role = normalizeRole(record.payload.role);
4168
4457
  text = extractMessageText(record.payload.content);
4169
4458
  }
4170
- } else if (source === 'codebuddy') {
4459
+ } else if (source === 'codebuddy' || source === 'workbuddy') {
4171
4460
  if (record && record.type === 'message') {
4172
4461
  role = normalizeRole(record.role);
4173
4462
  if (role === 'assistant' || role === 'user' || role === 'system') {
@@ -4347,15 +4636,7 @@ async function resolveSessionTrashEntryExactMessageCount(entry) {
4347
4636
  }
4348
4637
 
4349
4638
  async function hydrateSessionTrashEntries(entries, options = {}) {
4350
- const source = options.source === 'claude'
4351
- ? 'claude'
4352
- : (options.source === 'codex'
4353
- ? 'codex'
4354
- : (options.source === 'gemini'
4355
- ? 'gemini'
4356
- : (options.source === 'codebuddy'
4357
- ? 'codebuddy'
4358
- : (options.source === 'pi' ? 'pi' : 'all'))));
4639
+ const source = normalizeSessionSourceName(options.source, '') || 'all';
4359
4640
  const hydratedEntries = await mapWithConcurrency(Array.isArray(entries) ? entries : [], 8, async (entry) => {
4360
4641
  const normalizedEntry = normalizeSessionTrashEntry(entry);
4361
4642
  if (!normalizedEntry) {
@@ -4364,7 +4645,7 @@ async function hydrateSessionTrashEntries(entries, options = {}) {
4364
4645
  return await resolveSessionTrashEntryExactMessageCount(normalizedEntry);
4365
4646
  });
4366
4647
 
4367
- if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi') {
4648
+ if (normalizeSessionSourceName(source, '')) {
4368
4649
  return hydratedEntries.filter((entry) => entry.source === source);
4369
4650
  }
4370
4651
  return hydratedEntries;
@@ -4378,15 +4659,7 @@ async function hydrateSessionItemsExactMessageCount(items) {
4378
4659
  if (item.__messageCountExact === true) {
4379
4660
  return item;
4380
4661
  }
4381
- const source = item.source === 'claude'
4382
- ? 'claude'
4383
- : (item.source === 'codex'
4384
- ? 'codex'
4385
- : (item.source === 'gemini'
4386
- ? 'gemini'
4387
- : (item.source === 'codebuddy'
4388
- ? 'codebuddy'
4389
- : (item.source === 'pi' ? 'pi' : ''))));
4662
+ const source = normalizeSessionSourceName(item.source, '');
4390
4663
  const filePath = typeof item.filePath === 'string' ? item.filePath : '';
4391
4664
  if (!source || !filePath || !fs.existsSync(filePath)) {
4392
4665
  return item;
@@ -4413,6 +4686,40 @@ async function hydrateSessionItemsExactMessageCount(items) {
4413
4686
  });
4414
4687
  }
4415
4688
 
4689
+
4690
+ function normalizeSessionSourceName(value, fallback = '') {
4691
+ const source = typeof value === 'string' ? value.trim().toLowerCase() : '';
4692
+ if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'pi' || source === 'opencode') {
4693
+ return source;
4694
+ }
4695
+ return fallback;
4696
+ }
4697
+
4698
+ function isSessionSourceOrAll(value) {
4699
+ const source = typeof value === 'string' ? value.trim().toLowerCase() : '';
4700
+ return source === 'all' || !!normalizeSessionSourceName(source, '');
4701
+ }
4702
+
4703
+ function getSessionSourceLabel(source) {
4704
+ if (source === 'claude') return 'Claude Code';
4705
+ if (source === 'gemini') return 'Gemini CLI';
4706
+ if (source === 'codebuddy') return 'CodeBuddy Code';
4707
+ if (source === 'workbuddy') return 'WorkBuddy AI';
4708
+ if (source === 'pi') return 'Pi';
4709
+ if (source === 'opencode') return 'OpenCode';
4710
+ return 'Codex';
4711
+ }
4712
+
4713
+ function getSessionSourceProvider(source) {
4714
+ if (source === 'claude') return 'claude';
4715
+ if (source === 'gemini') return 'gemini';
4716
+ if (source === 'codebuddy') return 'codebuddy';
4717
+ if (source === 'workbuddy') return 'workbuddy';
4718
+ if (source === 'pi') return 'pi';
4719
+ if (source === 'opencode') return 'opencode';
4720
+ return 'codex';
4721
+ }
4722
+
4416
4723
  function getSessionExportKeyForApi(item) {
4417
4724
  const source = item && item.source ? String(item.source).trim() : '';
4418
4725
  const sessionId = item && item.sessionId ? String(item.sessionId) : '';
@@ -4430,20 +4737,21 @@ async function readSessionMessageCounts(params = {}) {
4430
4737
  return undefined;
4431
4738
  }
4432
4739
  const key = getSessionExportKeyForApi(item);
4433
- const source = item.source === 'claude'
4434
- ? 'claude'
4435
- : (item.source === 'codex'
4436
- ? 'codex'
4437
- : (item.source === 'gemini'
4438
- ? 'gemini'
4439
- : (item.source === 'codebuddy'
4440
- ? 'codebuddy'
4441
- : (item.source === 'pi' ? 'pi' : ''))));
4740
+ const source = normalizeSessionSourceName(item.source, '');
4442
4741
  const filePath = typeof item.filePath === 'string' ? item.filePath : '';
4443
- if (!source || !filePath || !fs.existsSync(filePath)) {
4742
+ if (!source) {
4444
4743
  return { key };
4445
4744
  }
4446
- const exactMessageCount = await countConversationMessagesInFile(filePath, source);
4745
+ let exactMessageCount;
4746
+ if (source === 'opencode') {
4747
+ const opencodeSessionId = item.sessionId || parseOpencodeVirtualSessionId(filePath);
4748
+ exactMessageCount = countOpencodeMessages(opencodeSessionId);
4749
+ } else {
4750
+ if (!filePath || !fs.existsSync(filePath)) {
4751
+ return { key };
4752
+ }
4753
+ exactMessageCount = await countConversationMessagesInFile(filePath, source);
4754
+ }
4447
4755
  if (!Number.isFinite(Number(exactMessageCount))) {
4448
4756
  return { key };
4449
4757
  }
@@ -4573,6 +4881,10 @@ function createSessionQueryScanState(tokens, options = {}) {
4573
4881
  const snippetLimit = Number.isFinite(Number(options.snippetLimit))
4574
4882
  ? Math.max(0, Number(options.snippetLimit))
4575
4883
  : 0;
4884
+ const positionsLimitRaw = Number(options.positionsLimit);
4885
+ const positionsLimit = Number.isFinite(positionsLimitRaw) && positionsLimitRaw > 0
4886
+ ? Math.floor(positionsLimitRaw)
4887
+ : 0;
4576
4888
 
4577
4889
  return {
4578
4890
  tokens,
@@ -4580,8 +4892,10 @@ function createSessionQueryScanState(tokens, options = {}) {
4580
4892
  roleFilter,
4581
4893
  maxMatches,
4582
4894
  snippetLimit,
4895
+ positionsLimit,
4583
4896
  count: 0,
4584
4897
  snippets: [],
4898
+ positions: [],
4585
4899
  leadingSystem: roleFilter !== 'system'
4586
4900
  };
4587
4901
  }
@@ -4613,6 +4927,13 @@ function consumeSessionQueryMessage(state, message) {
4613
4927
  if (state.snippetLimit > 0 && state.snippets.length < state.snippetLimit) {
4614
4928
  state.snippets.push(truncateText(text));
4615
4929
  }
4930
+ if (state.positionsLimit > 0 && state.positions.length < state.positionsLimit) {
4931
+ state.positions.push({
4932
+ lineIndex: Number.isInteger(message.recordLineIndex) ? message.recordLineIndex : -1,
4933
+ role,
4934
+ timestamp: typeof message.timestamp === 'string' ? message.timestamp : ''
4935
+ });
4936
+ }
4616
4937
  return state.count >= state.maxMatches;
4617
4938
  }
4618
4939
 
@@ -4620,7 +4941,8 @@ function buildSessionQueryScanResult(state) {
4620
4941
  return {
4621
4942
  hit: !!(state && state.count > 0),
4622
4943
  count: state && Number.isFinite(state.count) ? state.count : 0,
4623
- snippets: state && Array.isArray(state.snippets) ? state.snippets : []
4944
+ snippets: state && Array.isArray(state.snippets) ? state.snippets : [],
4945
+ positions: state && Array.isArray(state.positions) ? state.positions : []
4624
4946
  };
4625
4947
  }
4626
4948
 
@@ -4628,12 +4950,14 @@ function scanSessionContentForQueryInRecords(records, source, state) {
4628
4950
  if (!Array.isArray(records) || !state) {
4629
4951
  return buildSessionQueryScanResult(state);
4630
4952
  }
4631
-
4632
- for (const record of records) {
4953
+ for (let recordIndex = 0; recordIndex < records.length; recordIndex++) {
4954
+ const record = records[recordIndex];
4633
4955
  const message = extractMessageFromRecord(record, source);
4634
4956
  if (!message) {
4635
4957
  continue;
4636
4958
  }
4959
+ message.recordLineIndex = recordIndex;
4960
+ message.timestamp = toIsoTime(record && record.timestamp, '');
4637
4961
  if (consumeSessionQueryMessage(state, message)) {
4638
4962
  break;
4639
4963
  }
@@ -4644,12 +4968,12 @@ function scanSessionContentForQueryInRecords(records, source, state) {
4644
4968
 
4645
4969
  async function scanSessionContentForQuery(session, tokens, options = {}) {
4646
4970
  if (!session || !Array.isArray(tokens) || tokens.length === 0) {
4647
- return { hit: false, count: 0, snippets: [] };
4971
+ return { hit: false, count: 0, snippets: [], positions: [] };
4648
4972
  }
4649
4973
 
4650
4974
  const filePath = resolveSessionFilePath(session.source, session.filePath, session.sessionId);
4651
4975
  if (!filePath) {
4652
- return { hit: false, count: 0, snippets: [] };
4976
+ return { hit: false, count: 0, snippets: [], positions: [] };
4653
4977
  }
4654
4978
 
4655
4979
  const rawMaxBytes = Number(options.maxBytes);
@@ -4657,8 +4981,17 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
4657
4981
  ? Math.max(1024, rawMaxBytes)
4658
4982
  : 0;
4659
4983
  const state = createSessionQueryScanState(tokens, options);
4984
+ if (session.source === 'opencode') {
4985
+ const extracted = queryOpencodeSessionMessages(session.sessionId || parseOpencodeVirtualSessionId(filePath), { maxMessages: Infinity });
4986
+ const messages = extracted && Array.isArray(extracted.messages) ? extracted.messages : [];
4987
+ for (let i = 0; i < messages.length; i += 1) {
4988
+ const message = { ...messages[i], recordLineIndex: i };
4989
+ if (consumeSessionQueryMessage(state, message)) break;
4990
+ }
4991
+ return buildSessionQueryScanResult(state);
4992
+ }
4660
4993
  if (session.source === 'gemini') {
4661
- if (state.roleFilter !== 'all') {
4994
+ if (state.roleFilter !== 'all' || state.positionsLimit > 0) {
4662
4995
  let json;
4663
4996
  try {
4664
4997
  json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
@@ -4672,7 +5005,12 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
4672
5005
  if (!role) continue;
4673
5006
  const text = extractMessageText(extractGeminiMessageText(entry.content ?? entry.message ?? entry.text));
4674
5007
  if (!text) continue;
4675
- if (consumeSessionQueryMessage(state, { role, text })) {
5008
+ if (consumeSessionQueryMessage(state, {
5009
+ role,
5010
+ text,
5011
+ timestamp: toIsoTime(entry.timestamp ?? entry.time ?? entry.at, ''),
5012
+ recordLineIndex: -1
5013
+ })) {
4676
5014
  break;
4677
5015
  }
4678
5016
  }
@@ -4712,7 +5050,10 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
4712
5050
  rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
4713
5051
 
4714
5052
  let bytesRead = 0;
5053
+ let lineIndex = 0;
4715
5054
  for await (const line of rl) {
5055
+ const currentLineIndex = lineIndex;
5056
+ lineIndex += 1;
4716
5057
  if (maxBytes > 0 && bytesRead >= maxBytes) {
4717
5058
  break;
4718
5059
  }
@@ -4734,6 +5075,8 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
4734
5075
  if (!message) {
4735
5076
  continue;
4736
5077
  }
5078
+ message.recordLineIndex = currentLineIndex;
5079
+ message.timestamp = toIsoTime(record.timestamp, '');
4737
5080
  if (consumeSessionQueryMessage(state, message)) {
4738
5081
  break;
4739
5082
  }
@@ -4952,13 +5295,7 @@ function setSessionListCache(cacheKey, value) {
4952
5295
  }
4953
5296
 
4954
5297
  function buildSessionInventoryCacheKey(source, limit, options = {}) {
4955
- const normalizedSource = source === 'claude'
4956
- ? 'claude'
4957
- : (source === 'gemini'
4958
- ? 'gemini'
4959
- : (source === 'codebuddy'
4960
- ? 'codebuddy'
4961
- : (source === 'pi' ? 'pi' : 'codex')));
5298
+ const normalizedSource = normalizeSessionSourceName(source, 'codex');
4962
5299
  const normalizedLimit = Number.isFinite(Number(limit))
4963
5300
  ? Math.max(1, Math.floor(Number(limit)))
4964
5301
  : 1;
@@ -5044,7 +5381,7 @@ function getSessionInventoryCache(cacheKey, forceRefresh = false) {
5044
5381
  }
5045
5382
 
5046
5383
  function registerSessionFileLookupEntries(source, sessions = []) {
5047
- const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi'
5384
+ const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'pi' || source === 'opencode'
5048
5385
  ? source
5049
5386
  : 'codex';
5050
5387
  const store = g_sessionFileLookupCache[normalizedSource];
@@ -5085,7 +5422,7 @@ function setSessionInventoryCache(cacheKey, source, value) {
5085
5422
  }
5086
5423
 
5087
5424
  function listSessionInventoryBySource(source, limit, scanOptions = {}, options = {}) {
5088
- const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi'
5425
+ const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'pi' || source === 'opencode'
5089
5426
  ? source
5090
5427
  : 'codex';
5091
5428
  const forceRefresh = !!options.forceRefresh;
@@ -5101,9 +5438,13 @@ function listSessionInventoryBySource(source, limit, scanOptions = {}, options =
5101
5438
  ? listGeminiSessions(limit, scanOptions)
5102
5439
  : (normalizedSource === 'codebuddy'
5103
5440
  ? listCodeBuddySessions(limit, scanOptions)
5104
- : (normalizedSource === 'pi'
5105
- ? listPiSessions(limit, scanOptions)
5106
- : listCodexSessions(limit, scanOptions))));
5441
+ : (normalizedSource === 'workbuddy'
5442
+ ? listWorkBuddySessions(limit, scanOptions)
5443
+ : (normalizedSource === 'pi'
5444
+ ? listPiSessions(limit, scanOptions)
5445
+ : (normalizedSource === 'opencode'
5446
+ ? listOpencodeSessions(limit, scanOptions)
5447
+ : listCodexSessions(limit, scanOptions))))));
5107
5448
  setSessionInventoryCache(cacheKey, normalizedSource, sessions);
5108
5449
  return sessions;
5109
5450
  }
@@ -5116,7 +5457,9 @@ function invalidateSessionListCache() {
5116
5457
  claude: new Map(),
5117
5458
  gemini: new Map(),
5118
5459
  codebuddy: new Map(),
5119
- pi: new Map()
5460
+ workbuddy: new Map(),
5461
+ pi: new Map(),
5462
+ opencode: new Map()
5120
5463
  };
5121
5464
  }
5122
5465
 
@@ -5350,13 +5693,7 @@ function readSessionProviderFromRecord(record, source = '') {
5350
5693
  if (provider) {
5351
5694
  return provider;
5352
5695
  }
5353
- return source === 'claude'
5354
- ? 'claude'
5355
- : (source === 'gemini'
5356
- ? 'gemini'
5357
- : (source === 'codebuddy'
5358
- ? 'codebuddy'
5359
- : (source === 'pi' ? 'pi' : 'codex')));
5696
+ return getSessionSourceProvider(source);
5360
5697
  }
5361
5698
 
5362
5699
  function applySessionUsageSummaryFromRecord(state, record, source) {
@@ -5901,6 +6238,175 @@ function parseCodeBuddySessionSummary(filePath, options = {}) {
5901
6238
  };
5902
6239
  }
5903
6240
 
6241
+ // WorkBuddy AI 会话记录与 CodeBuddy 同族(type:"message" + 顶层 role/content,
6242
+ // 且存在 __codebuddyLocal 字段),标题来自 ai-title 记录,模型名位于 providerData.model,
6243
+ // token 用量位于嵌套 message.usage。
6244
+ function parseWorkBuddySessionSummary(filePath, options = {}) {
6245
+ const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
6246
+ ? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
6247
+ : SESSION_SUMMARY_READ_BYTES;
6248
+ const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
6249
+ ? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
6250
+ : SESSION_TITLE_READ_BYTES;
6251
+ const records = parseJsonlHeadRecords(filePath, summaryReadBytes);
6252
+ if (records.length === 0) {
6253
+ return null;
6254
+ }
6255
+
6256
+ let stat;
6257
+ try {
6258
+ stat = fs.statSync(filePath);
6259
+ } catch (_) {
6260
+ return null;
6261
+ }
6262
+
6263
+ let sessionId = path.basename(filePath, '.jsonl');
6264
+ let cwd = '';
6265
+ let firstPrompt = '';
6266
+ let aiTitle = '';
6267
+ let messageCount = 0;
6268
+ let totalTokens = 0;
6269
+ let contextWindow = 0;
6270
+ let inputTokens = 0;
6271
+ let cachedInputTokens = 0;
6272
+ let cacheCreationInputTokens = 0;
6273
+ let outputTokens = 0;
6274
+ let reasoningOutputTokens = 0;
6275
+ let provider = 'workbuddy';
6276
+ let model = '';
6277
+ const models = [];
6278
+ const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, cacheCreationInputTokens, outputTokens, reasoningOutputTokens };
6279
+ const previewMessages = [];
6280
+ let createdAt = '';
6281
+ let updatedAt = stat.mtime.toISOString();
6282
+
6283
+ for (const record of records) {
6284
+ if (!createdAt && record && record.timestamp) {
6285
+ createdAt = toIsoTime(record.timestamp, createdAt);
6286
+ }
6287
+ if (record && record.timestamp) {
6288
+ updatedAt = updateLatestIso(updatedAt, record.timestamp);
6289
+ }
6290
+
6291
+ applySessionUsageSummaryFromRecord(usageState, record, 'workbuddy');
6292
+ totalTokens = usageState.totalTokens || 0;
6293
+ contextWindow = usageState.contextWindow || 0;
6294
+ inputTokens = usageState.inputTokens || 0;
6295
+ cachedInputTokens = usageState.cachedInputTokens || 0;
6296
+ cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
6297
+ outputTokens = usageState.outputTokens || 0;
6298
+ reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
6299
+
6300
+ if (record && typeof record.sessionId === 'string' && record.sessionId.trim()) {
6301
+ sessionId = record.sessionId.trim();
6302
+ }
6303
+ if (!cwd && record && typeof record.cwd === 'string' && record.cwd.trim()) {
6304
+ cwd = record.cwd.trim();
6305
+ }
6306
+
6307
+ provider = readExplicitSessionProviderFromRecord(record) || provider;
6308
+ const recordModels = readSessionModelsFromRecord(record);
6309
+ for (const recordModel of recordModels) {
6310
+ if (!models.includes(recordModel)) {
6311
+ models.push(recordModel);
6312
+ }
6313
+ }
6314
+ model = recordModels[0] || model;
6315
+
6316
+ if (record && record.type === 'ai-title' && typeof record.aiTitle === 'string' && record.aiTitle.trim()) {
6317
+ aiTitle = record.aiTitle.trim();
6318
+ }
6319
+
6320
+ if (record && record.type === 'message') {
6321
+ const role = normalizeRole(record.role);
6322
+ if (role === 'assistant' || role === 'user' || role === 'system') {
6323
+ const content = record.message?.content ?? record.content ?? '';
6324
+ previewMessages.push({
6325
+ role,
6326
+ text: extractMessageText(content)
6327
+ });
6328
+ }
6329
+ }
6330
+ }
6331
+
6332
+ const tailRecords = parseJsonlTailRecords(filePath, summaryReadBytes);
6333
+ for (const record of tailRecords) {
6334
+ applySessionUsageSummaryFromRecord(usageState, record, 'workbuddy');
6335
+ totalTokens = usageState.totalTokens || 0;
6336
+ contextWindow = usageState.contextWindow || 0;
6337
+ inputTokens = usageState.inputTokens || 0;
6338
+ cachedInputTokens = usageState.cachedInputTokens || 0;
6339
+ cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
6340
+ outputTokens = usageState.outputTokens || 0;
6341
+ reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
6342
+ provider = readExplicitSessionProviderFromRecord(record) || provider;
6343
+ const recordModels = readSessionModelsFromRecord(record);
6344
+ for (const recordModel of recordModels) {
6345
+ if (!models.includes(recordModel)) {
6346
+ models.push(recordModel);
6347
+ }
6348
+ }
6349
+ model = recordModels[0] || model;
6350
+ }
6351
+
6352
+ const filteredPreviewMessages = removeLeadingSystemMessage(previewMessages);
6353
+ messageCount = filteredPreviewMessages.length;
6354
+ const firstUser = filteredPreviewMessages.find(item => item.role === 'user' && item.text);
6355
+ if (firstUser) {
6356
+ firstPrompt = truncateText(firstUser.text);
6357
+ }
6358
+
6359
+ if (!firstPrompt) {
6360
+ const titleRecords = parseJsonlHeadRecords(filePath, titleReadBytes);
6361
+ const titleMessages = [];
6362
+ for (const record of titleRecords) {
6363
+ if (record && record.type === 'message') {
6364
+ const role = normalizeRole(record.role);
6365
+ if (role === 'assistant' || role === 'user' || role === 'system') {
6366
+ const content = record.message?.content ?? record.content ?? '';
6367
+ titleMessages.push({
6368
+ role,
6369
+ text: extractMessageText(content)
6370
+ });
6371
+ }
6372
+ }
6373
+ }
6374
+
6375
+ const filteredTitleMessages = removeLeadingSystemMessage(titleMessages);
6376
+ const titleUser = filteredTitleMessages.find(item => item.role === 'user' && item.text);
6377
+ if (titleUser) {
6378
+ firstPrompt = truncateText(titleUser.text);
6379
+ }
6380
+ }
6381
+
6382
+ messageCount = Math.max(0, messageCount);
6383
+
6384
+ return {
6385
+ source: 'workbuddy',
6386
+ sourceLabel: 'WorkBuddy AI',
6387
+ provider,
6388
+ model,
6389
+ models,
6390
+ sessionId,
6391
+ title: aiTitle || firstPrompt || sessionId,
6392
+ cwd,
6393
+ createdAt,
6394
+ updatedAt,
6395
+ messageCount,
6396
+ totalTokens,
6397
+ contextWindow,
6398
+ inputTokens,
6399
+ cachedInputTokens,
6400
+ cacheCreationInputTokens,
6401
+ outputTokens,
6402
+ reasoningOutputTokens,
6403
+ __messageCountExact: isSessionSummaryMessageCountExact(stat, summaryReadBytes),
6404
+ filePath,
6405
+ keywords: [],
6406
+ capabilities: { code: true }
6407
+ };
6408
+ }
6409
+
5904
6410
  function extractGeminiMessageText(content) {
5905
6411
  if (typeof content === 'string') {
5906
6412
  return content;
@@ -6665,6 +7171,54 @@ function listCodeBuddySessions(limit, options = {}) {
6665
7171
  return mergeAndLimitSessions(sessions, limit);
6666
7172
  }
6667
7173
 
7174
+ function listWorkBuddySessions(limit, options = {}) {
7175
+ const projectsDir = getWorkBuddyProjectsDir();
7176
+ if (!fs.existsSync(projectsDir)) {
7177
+ return [];
7178
+ }
7179
+
7180
+ const scanFactor = Number.isFinite(Number(options.scanFactor))
7181
+ ? Math.max(1, Number(options.scanFactor))
7182
+ : SESSION_SCAN_FACTOR;
7183
+ const minFiles = Number.isFinite(Number(options.minFiles))
7184
+ ? Math.max(1, Number(options.minFiles))
7185
+ : Math.min(SESSION_SCAN_MIN_FILES, MAX_SESSION_LIST_SIZE * SESSION_SCAN_FACTOR);
7186
+ const targetCount = Number.isFinite(Number(options.targetCount))
7187
+ ? Math.max(1, Math.floor(Number(options.targetCount)))
7188
+ : Math.max(1, Math.floor(limit * scanFactor));
7189
+ const scanCount = Number.isFinite(Number(options.scanCount))
7190
+ ? Math.max(targetCount, Math.floor(Number(options.scanCount)))
7191
+ : Math.max(targetCount, minFiles);
7192
+ const maxFilesScanned = Number.isFinite(Number(options.maxFilesScanned))
7193
+ ? Math.max(scanCount, Math.floor(Number(options.maxFilesScanned)))
7194
+ : Math.max(scanCount * 2, minFiles);
7195
+ const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
7196
+ ? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
7197
+ : SESSION_SUMMARY_READ_BYTES;
7198
+ const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
7199
+ ? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
7200
+ : SESSION_TITLE_READ_BYTES;
7201
+
7202
+ const files = collectRecentJsonlFiles(projectsDir, {
7203
+ returnCount: scanCount,
7204
+ maxFilesScanned
7205
+ });
7206
+ const sessions = [];
7207
+ for (const filePath of files) {
7208
+ const summary = parseWorkBuddySessionSummary(filePath, {
7209
+ summaryReadBytes,
7210
+ titleReadBytes
7211
+ });
7212
+ if (summary) {
7213
+ sessions.push(summary);
7214
+ }
7215
+ if (sessions.length >= targetCount) {
7216
+ break;
7217
+ }
7218
+ }
7219
+ return mergeAndLimitSessions(sessions, limit);
7220
+ }
7221
+
6668
7222
  function listPiSessions(limit, options = {}) {
6669
7223
  const sessionsDir = getPiSessionsDir();
6670
7224
  if (!fs.existsSync(sessionsDir)) {
@@ -6714,9 +7268,7 @@ function listPiSessions(limit, options = {}) {
6714
7268
  }
6715
7269
 
6716
7270
  async function listAllSessions(params = {}) {
6717
- const source = params.source === 'codex' || params.source === 'claude' || params.source === 'gemini' || params.source === 'codebuddy' || params.source === 'pi'
6718
- ? params.source
6719
- : 'all';
7271
+ const source = normalizeSessionSourceName(params.source, '') || 'all';
6720
7272
  const rawLimit = Number(params.limit);
6721
7273
  const limit = Number.isFinite(rawLimit)
6722
7274
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
@@ -6764,9 +7316,15 @@ async function listAllSessions(params = {}) {
6764
7316
  if (source === 'all' || source === 'codebuddy') {
6765
7317
  sessions = sessions.concat(listSessionInventoryBySource('codebuddy', limit, scanOptions, { forceRefresh }));
6766
7318
  }
7319
+ if (source === 'all' || source === 'workbuddy') {
7320
+ sessions = sessions.concat(listSessionInventoryBySource('workbuddy', limit, scanOptions, { forceRefresh }));
7321
+ }
6767
7322
  if (source === 'all' || source === 'pi') {
6768
7323
  sessions = sessions.concat(listSessionInventoryBySource('pi', limit, scanOptions, { forceRefresh }));
6769
7324
  }
7325
+ if (source === 'all' || source === 'opencode') {
7326
+ sessions = sessions.concat(listSessionInventoryBySource('opencode', limit, scanOptions, { forceRefresh }));
7327
+ }
6770
7328
 
6771
7329
  if (hasPathFilter) {
6772
7330
  sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
@@ -6789,9 +7347,7 @@ async function listAllSessions(params = {}) {
6789
7347
  }
6790
7348
 
6791
7349
  async function listAllSessionsData(params = {}) {
6792
- const source = params.source === 'codex' || params.source === 'claude'
6793
- ? params.source
6794
- : 'all';
7350
+ const source = normalizeSessionSourceName(params.source, '') || 'all';
6795
7351
  const rawLimit = Number(params.limit);
6796
7352
  const limit = Number.isFinite(rawLimit)
6797
7353
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
@@ -6848,6 +7404,7 @@ async function listSessionUsage(params = {}) {
6848
7404
  parseCodexSessionSummary,
6849
7405
  parseClaudeSessionSummary,
6850
7406
  parseCodeBuddySessionSummary,
7407
+ parseWorkBuddySessionSummary,
6851
7408
  parseGeminiSessionSummary,
6852
7409
  parsePiSessionSummary,
6853
7410
  MAX_SESSION_USAGE_LIST_SIZE,
@@ -6863,10 +7420,10 @@ async function exportSessionUsage(params = {}) {
6863
7420
 
6864
7421
  function listSessionPaths(params = {}) {
6865
7422
  const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
6866
- if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'pi' && source !== 'all') {
7423
+ if (source && !isSessionSourceOrAll(source)) {
6867
7424
  return [];
6868
7425
  }
6869
- const validSource = source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi' ? source : 'all';
7426
+ const validSource = normalizeSessionSourceName(source, '') || (source === 'all' ? 'all' : 'all');
6870
7427
  const rawLimit = Number(params.limit);
6871
7428
  const limit = Number.isFinite(rawLimit)
6872
7429
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_PATH_LIST_SIZE))
@@ -6900,9 +7457,15 @@ function listSessionPaths(params = {}) {
6900
7457
  if (validSource === 'all' || validSource === 'codebuddy') {
6901
7458
  sessions = sessions.concat(listSessionInventoryBySource('codebuddy', gatherLimit, scanOptions, { forceRefresh }));
6902
7459
  }
7460
+ if (validSource === 'all' || validSource === 'workbuddy') {
7461
+ sessions = sessions.concat(listSessionInventoryBySource('workbuddy', gatherLimit, scanOptions, { forceRefresh }));
7462
+ }
6903
7463
  if (validSource === 'all' || validSource === 'pi') {
6904
7464
  sessions = sessions.concat(listSessionInventoryBySource('pi', gatherLimit, scanOptions, { forceRefresh }));
6905
7465
  }
7466
+ if (validSource === 'all' || validSource === 'opencode') {
7467
+ sessions = sessions.concat(listSessionInventoryBySource('opencode', gatherLimit, scanOptions, { forceRefresh }));
7468
+ }
6906
7469
 
6907
7470
  const dedupedPaths = [];
6908
7471
  const seen = new Set();
@@ -6928,9 +7491,13 @@ function listSessionPaths(params = {}) {
6928
7491
  }
6929
7492
 
6930
7493
  function resolveSessionFilePath(source, filePath, sessionId) {
6931
- const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi'
6932
- ? source
6933
- : 'codex';
7494
+ const normalizedSource = normalizeSessionSourceName(source, 'codex');
7495
+ if (normalizedSource === 'opencode') {
7496
+ const id = typeof sessionId === 'string' && sessionId.trim()
7497
+ ? sessionId.trim()
7498
+ : parseOpencodeVirtualSessionId(filePath);
7499
+ return id && parseOpencodeSessionSummary(id) ? buildOpencodeVirtualSessionPath(id) : '';
7500
+ }
6934
7501
  const homeDir = process && process.env && process.env.HOME ? process.env.HOME : '';
6935
7502
  const derivedCodexDir = homeDir ? `${homeDir}/.codexmate/sessions/derived/codex` : '';
6936
7503
  const derivedClaudeDir = homeDir ? `${homeDir}/.codexmate/sessions/derived/claude` : '';
@@ -6940,9 +7507,11 @@ function resolveSessionFilePath(source, filePath, sessionId) {
6940
7507
  ? [getGeminiTmpDir()]
6941
7508
  : (normalizedSource === 'codebuddy'
6942
7509
  ? [getCodeBuddyProjectsDir()]
6943
- : (normalizedSource === 'pi'
6944
- ? [getPiSessionsDir()]
6945
- : [getCodexSessionsDir(), derivedCodexDir])));
7510
+ : (normalizedSource === 'workbuddy'
7511
+ ? [getWorkBuddyProjectsDir()]
7512
+ : (normalizedSource === 'pi'
7513
+ ? [getPiSessionsDir()]
7514
+ : [getCodexSessionsDir(), derivedCodexDir]))));
6946
7515
  const availableRoots = roots.filter((dirPath) => dirPath && fs.existsSync(dirPath));
6947
7516
  if (availableRoots.length === 0) {
6948
7517
  return '';
@@ -7488,23 +8057,11 @@ function moveFileSync(sourcePath, targetPath) {
7488
8057
 
7489
8058
  function buildSessionSummaryFallback(source, filePath, sessionId = '') {
7490
8059
  const resolvedSessionId = sessionId || path.basename(filePath, '.jsonl');
7491
- const sourceLabel = source === 'claude'
7492
- ? 'Claude Code'
7493
- : (source === 'gemini'
7494
- ? 'Gemini CLI'
7495
- : (source === 'codebuddy'
7496
- ? 'CodeBuddy Code'
7497
- : (source === 'pi' ? 'Pi' : 'Codex')));
8060
+ const sourceLabel = getSessionSourceLabel(source);
7498
8061
  return {
7499
8062
  source,
7500
8063
  sourceLabel,
7501
- provider: source === 'claude'
7502
- ? 'claude'
7503
- : (source === 'gemini'
7504
- ? 'gemini'
7505
- : (source === 'codebuddy'
7506
- ? 'codebuddy'
7507
- : (source === 'pi' ? 'pi' : 'codex'))),
8064
+ provider: getSessionSourceProvider(source),
7508
8065
  sessionId: resolvedSessionId,
7509
8066
  title: resolvedSessionId,
7510
8067
  cwd: '',
@@ -7515,7 +8072,7 @@ function buildSessionSummaryFallback(source, filePath, sessionId = '') {
7515
8072
  contextWindow: 0,
7516
8073
  filePath,
7517
8074
  keywords: [],
7518
- capabilities: source === 'claude' || source === 'gemini' || source === 'codebuddy' ? { code: true } : {}
8075
+ capabilities: source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'opencode' ? { code: true } : {}
7519
8076
  };
7520
8077
  }
7521
8078
 
@@ -7559,7 +8116,9 @@ function normalizeSessionTrashEntry(entry) {
7559
8116
  ? 'gemini'
7560
8117
  : (entry.source === 'codebuddy'
7561
8118
  ? 'codebuddy'
7562
- : (entry.source === 'pi' ? 'pi' : ''))));
8119
+ : (entry.source === 'workbuddy'
8120
+ ? 'workbuddy'
8121
+ : (entry.source === 'pi' ? 'pi' : '')))));
7563
8122
  const trashId = typeof entry.trashId === 'string' ? entry.trashId.trim() : '';
7564
8123
  if (!source || !trashId || trashId.includes('/') || trashId.includes('\\') || trashId.includes('\0')) {
7565
8124
  return null;
@@ -7580,7 +8139,9 @@ function normalizeSessionTrashEntry(entry) {
7580
8139
  ? 'Gemini CLI'
7581
8140
  : (source === 'codebuddy'
7582
8141
  ? 'CodeBuddy Code'
7583
- : (source === 'pi' ? 'Pi' : 'Codex'))),
8142
+ : (source === 'workbuddy'
8143
+ ? 'WorkBuddy AI'
8144
+ : (source === 'pi' ? 'Pi' : 'Codex')))),
7584
8145
  sessionId: sessionId || trashId,
7585
8146
  title: typeof entry.title === 'string' && entry.title.trim() ? entry.title.trim() : (sessionId || trashId),
7586
8147
  cwd: typeof entry.cwd === 'string' ? entry.cwd : '',
@@ -7596,7 +8157,7 @@ function normalizeSessionTrashEntry(entry) {
7596
8157
  originalFilePath: typeof entry.originalFilePath === 'string' ? entry.originalFilePath : '',
7597
8158
  provider: typeof entry.provider === 'string' && entry.provider.trim()
7598
8159
  ? entry.provider.trim()
7599
- : (source === 'claude' ? 'claude' : (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : (source === 'pi' ? 'pi' : 'codex')))),
8160
+ : (source === 'claude' ? 'claude' : (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : (source === 'workbuddy' ? 'workbuddy' : (source === 'pi' ? 'pi' : 'codex'))))),
7600
8161
  keywords: normalizeKeywords(entry.keywords),
7601
8162
  capabilities: normalizeCapabilities(entry.capabilities),
7602
8163
  claudeIndexPath: typeof entry.claudeIndexPath === 'string' ? entry.claudeIndexPath : '',
@@ -7689,7 +8250,9 @@ function buildSessionTrashEntry(summary, options = {}) {
7689
8250
  ? 'gemini'
7690
8251
  : (options.source === 'codebuddy'
7691
8252
  ? 'codebuddy'
7692
- : (options.source === 'pi' ? 'pi' : 'codex')));
8253
+ : (options.source === 'workbuddy'
8254
+ ? 'workbuddy'
8255
+ : (options.source === 'pi' ? 'pi' : 'codex'))));
7693
8256
  const sessionId = options.sessionId || summary.sessionId || path.basename(options.originalFilePath || summary.filePath || '', '.jsonl');
7694
8257
  const claudeIndexEntry = options.claudeIndexEntry && typeof options.claudeIndexEntry === 'object' && !Array.isArray(options.claudeIndexEntry)
7695
8258
  ? options.claudeIndexEntry
@@ -7703,7 +8266,9 @@ function buildSessionTrashEntry(summary, options = {}) {
7703
8266
  ? 'Gemini CLI'
7704
8267
  : (source === 'codebuddy'
7705
8268
  ? 'CodeBuddy Code'
7706
- : (source === 'pi' ? 'Pi' : 'Codex')));
8269
+ : (source === 'workbuddy'
8270
+ ? 'WorkBuddy AI'
8271
+ : (source === 'pi' ? 'Pi' : 'Codex'))));
7707
8272
  const fallbackTitle = truncateText(
7708
8273
  (claudeIndexEntry && (claudeIndexEntry.summary || claudeIndexEntry.firstPrompt)) || sessionId,
7709
8274
  120
@@ -7740,7 +8305,7 @@ function buildSessionTrashEntry(summary, options = {}) {
7740
8305
  originalFilePath: options.originalFilePath || summary.filePath || '',
7741
8306
  provider: (claudeIndexEntry && typeof claudeIndexEntry.provider === 'string' && claudeIndexEntry.provider.trim())
7742
8307
  ? claudeIndexEntry.provider.trim()
7743
- : (summary.provider || (source === 'claude' ? 'claude' : (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : (source === 'pi' ? 'pi' : 'codex'))))),
8308
+ : (summary.provider || (source === 'claude' ? 'claude' : (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : (source === 'workbuddy' ? 'workbuddy' : (source === 'pi' ? 'pi' : 'codex')))))),
7744
8309
  keywords: normalizedClaudeKeywords.length > 0 ? normalizedClaudeKeywords : normalizedSummaryKeywords,
7745
8310
  capabilities: Object.keys(normalizedClaudeCapabilities).length > 0
7746
8311
  ? normalizedClaudeCapabilities
@@ -7761,7 +8326,9 @@ function resolveSessionRestoreTarget(entry) {
7761
8326
  ? getGeminiTmpDir()
7762
8327
  : (normalized.source === 'codebuddy'
7763
8328
  ? getCodeBuddyProjectsDir()
7764
- : (normalized.source === 'pi' ? getPiSessionsDir() : getCodexSessionsDir())));
8329
+ : (normalized.source === 'workbuddy'
8330
+ ? getWorkBuddyProjectsDir()
8331
+ : (normalized.source === 'pi' ? getPiSessionsDir() : getCodexSessionsDir()))));
7765
8332
  const originalFilePath = typeof normalized.originalFilePath === 'string' ? normalized.originalFilePath.trim() : '';
7766
8333
  if (!root || !originalFilePath) {
7767
8334
  return '';
@@ -7895,15 +8462,7 @@ function upsertClaudeSessionIndexEntry(indexPath, sessionFilePath, entry) {
7895
8462
  }
7896
8463
 
7897
8464
  async function listSessionTrashItems(params = {}) {
7898
- const source = params.source === 'claude'
7899
- ? 'claude'
7900
- : (params.source === 'codex'
7901
- ? 'codex'
7902
- : (params.source === 'gemini'
7903
- ? 'gemini'
7904
- : (params.source === 'codebuddy'
7905
- ? 'codebuddy'
7906
- : (params.source === 'pi' ? 'pi' : 'all'))));
8465
+ const source = normalizeSessionSourceName(params.source, '') || 'all';
7907
8466
  const countOnly = params.countOnly === true;
7908
8467
  const rawLimit = Number(params.limit);
7909
8468
  const limit = Number.isFinite(rawLimit)
@@ -7913,7 +8472,7 @@ async function listSessionTrashItems(params = {}) {
7913
8472
  purgeExpiredSessionTrashEntries(params.retentionDays);
7914
8473
  }
7915
8474
  const allEntries = readSessionTrashEntries();
7916
- let items = source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi'
8475
+ let items = normalizeSessionSourceName(source, '')
7917
8476
  ? allEntries.filter((entry) => entry.source === source)
7918
8477
  : allEntries.slice();
7919
8478
  items.sort((a, b) => {
@@ -8093,18 +8652,13 @@ async function purgeSessionTrashItems(params = {}) {
8093
8652
  }
8094
8653
 
8095
8654
  async function trashSessionData(params = {}) {
8096
- const source = params.source === 'claude'
8097
- ? 'claude'
8098
- : (params.source === 'codex'
8099
- ? 'codex'
8100
- : (params.source === 'gemini'
8101
- ? 'gemini'
8102
- : (params.source === 'codebuddy'
8103
- ? 'codebuddy'
8104
- : (params.source === 'pi' ? 'pi' : ''))));
8655
+ const source = normalizeSessionSourceName(params.source, '');
8105
8656
  if (!source) {
8106
8657
  return { error: 'Invalid source' };
8107
8658
  }
8659
+ if (source === 'opencode') {
8660
+ return { error: 'OpenCode DB-backed sessions cannot be deleted from CodexMate' };
8661
+ }
8108
8662
 
8109
8663
  const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
8110
8664
  if (!filePath) {
@@ -8117,7 +8671,9 @@ async function trashSessionData(params = {}) {
8117
8671
  ? parseGeminiSessionSummary(filePath)
8118
8672
  : (source === 'codebuddy'
8119
8673
  ? parseCodeBuddySessionSummary(filePath)
8120
- : (source === 'pi' ? parsePiSessionSummary(filePath) : parseCodexSessionSummary(filePath)))))
8674
+ : (source === 'workbuddy'
8675
+ ? parseWorkBuddySessionSummary(filePath)
8676
+ : (source === 'pi' ? parsePiSessionSummary(filePath) : parseCodexSessionSummary(filePath))))))
8121
8677
  || buildSessionSummaryFallback(source, filePath, params.sessionId);
8122
8678
  const exactMessageCount = await countConversationMessagesInFile(filePath, source);
8123
8679
  if (Number.isFinite(Number(exactMessageCount))) {
@@ -8208,18 +8764,13 @@ async function trashSessionData(params = {}) {
8208
8764
  }
8209
8765
 
8210
8766
  async function deleteSessionData(params = {}) {
8211
- const source = params.source === 'claude'
8212
- ? 'claude'
8213
- : (params.source === 'codex'
8214
- ? 'codex'
8215
- : (params.source === 'gemini'
8216
- ? 'gemini'
8217
- : (params.source === 'codebuddy'
8218
- ? 'codebuddy'
8219
- : (params.source === 'pi' ? 'pi' : ''))));
8767
+ const source = normalizeSessionSourceName(params.source, '');
8220
8768
  if (!source) {
8221
8769
  return { error: 'Invalid source' };
8222
8770
  }
8771
+ if (source === 'opencode') {
8772
+ return { error: 'OpenCode DB-backed sessions cannot be deleted from CodexMate' };
8773
+ }
8223
8774
 
8224
8775
  const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
8225
8776
  if (!filePath) {
@@ -8767,7 +9318,7 @@ function recordHasPiMessage(record) {
8767
9318
 
8768
9319
  function recordHasMessage(record, source) {
8769
9320
  if (source === 'codex') return recordHasCodexMessage(record);
8770
- if (source === 'codebuddy') return recordHasCodeBuddyMessage(record);
9321
+ if (source === 'codebuddy' || source === 'workbuddy') return recordHasCodeBuddyMessage(record);
8771
9322
  if (source === 'pi') return recordHasPiMessage(record);
8772
9323
  return recordHasClaudeMessage(record);
8773
9324
  }
@@ -8787,7 +9338,7 @@ function extractMessagesFromRecords(records, source, options = {}) {
8787
9338
  const record = records[lineIndex];
8788
9339
  if (source === 'codex') {
8789
9340
  extractCodexMessageFromRecord(record, state, lineIndex);
8790
- } else if (source === 'codebuddy') {
9341
+ } else if (source === 'codebuddy' || source === 'workbuddy') {
8791
9342
  extractCodeBuddyMessageFromRecord(record, state, lineIndex);
8792
9343
  } else if (source === 'pi') {
8793
9344
  extractPiMessageFromRecord(record, state, lineIndex);
@@ -8852,7 +9403,7 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
8852
9403
 
8853
9404
  if (source === 'codex') {
8854
9405
  extractCodexMessageFromRecord(record, state, currentLineIndex);
8855
- } else if (source === 'codebuddy') {
9406
+ } else if (source === 'codebuddy' || source === 'workbuddy') {
8856
9407
  extractCodeBuddyMessageFromRecord(record, state, currentLineIndex);
8857
9408
  } else if (source === 'pi') {
8858
9409
  extractPiMessageFromRecord(record, state, currentLineIndex);
@@ -8880,15 +9431,7 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
8880
9431
  }
8881
9432
 
8882
9433
  async function readSessionDetail(params = {}) {
8883
- const source = params.source === 'claude'
8884
- ? 'claude'
8885
- : (params.source === 'codex'
8886
- ? 'codex'
8887
- : (params.source === 'gemini'
8888
- ? 'gemini'
8889
- : (params.source === 'codebuddy'
8890
- ? 'codebuddy'
8891
- : (params.source === 'pi' ? 'pi' : ''))));
9434
+ const source = normalizeSessionSourceName(params.source, '');
8892
9435
  if (!source) {
8893
9436
  return { error: 'Invalid source' };
8894
9437
  }
@@ -8904,9 +9447,30 @@ async function readSessionDetail(params = {}) {
8904
9447
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_DETAIL_MESSAGES))
8905
9448
  : DEFAULT_SESSION_DETAIL_MESSAGES;
8906
9449
  const preview = params.preview === true || params.preview === 'true';
9450
+ const rawSessionQuery = typeof params.query === 'string' ? params.query.trim() : '';
9451
+ const sessionQueryTokens = rawSessionQuery
9452
+ ? expandSessionQueryTokens(normalizeQueryTokens(rawSessionQuery))
9453
+ : [];
9454
+ let sessionQueryMatch = null;
9455
+ if (sessionQueryTokens.length > 0) {
9456
+ sessionQueryMatch = await scanSessionContentForQuery(
9457
+ { source, filePath, sessionId: params.sessionId },
9458
+ sessionQueryTokens,
9459
+ {
9460
+ maxMatches: Number.MAX_SAFE_INTEGER,
9461
+ positionsLimit: SESSION_QUERY_MATCH_POSITIONS_LIMIT
9462
+ }
9463
+ );
9464
+ }
8907
9465
 
8908
9466
  let extracted;
8909
- if (source === 'gemini') {
9467
+ if (source === 'opencode') {
9468
+ const opencodeSessionId = params.sessionId || parseOpencodeVirtualSessionId(filePath);
9469
+ extracted = queryOpencodeSessionMessages(opencodeSessionId, { maxMessages: messageLimit });
9470
+ if (!extracted) {
9471
+ return { error: 'Failed to parse session file' };
9472
+ }
9473
+ } else if (source === 'gemini') {
8910
9474
  let json;
8911
9475
  try {
8912
9476
  json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
@@ -8946,13 +9510,7 @@ async function readSessionDetail(params = {}) {
8946
9510
  extracted = await extractSessionDetailPreviewFromFile(filePath, source, messageLimit, { preview });
8947
9511
  }
8948
9512
  const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
8949
- const sourceLabel = source === 'codex'
8950
- ? 'Codex'
8951
- : (source === 'claude'
8952
- ? 'Claude Code'
8953
- : (source === 'gemini'
8954
- ? 'Gemini CLI'
8955
- : (source === 'pi' ? 'Pi' : 'CodeBuddy Code')));
9513
+ const sourceLabel = getSessionSourceLabel(source);
8956
9514
  const clippedMessages = Array.isArray(extracted.messages) ? extracted.messages : [];
8957
9515
  const hasExactTotalMessages = Number.isFinite(extracted.totalMessages);
8958
9516
  const startIndex = hasExactTotalMessages
@@ -8996,6 +9554,13 @@ async function readSessionDetail(params = {}) {
8996
9554
  ? extracted.clipped
8997
9555
  : (hasExactTotalMessages ? extracted.totalMessages > indexedMessages.length : false),
8998
9556
  messageLimit,
9557
+ ...(sessionQueryMatch ? {
9558
+ match: {
9559
+ hit: !!sessionQueryMatch.hit,
9560
+ count: Number.isFinite(sessionQueryMatch.count) ? sessionQueryMatch.count : 0,
9561
+ positions: Array.isArray(sessionQueryMatch.positions) ? sessionQueryMatch.positions : []
9562
+ }
9563
+ } : {}),
8999
9564
  messages: indexedMessages,
9000
9565
  filePath,
9001
9566
  ...(typeof buildSessionNativeStatus === 'function'
@@ -9013,15 +9578,7 @@ async function readSessionDetail(params = {}) {
9013
9578
  }
9014
9579
 
9015
9580
  async function readSessionPlain(params = {}) {
9016
- const source = params.source === 'claude'
9017
- ? 'claude'
9018
- : (params.source === 'codex'
9019
- ? 'codex'
9020
- : (params.source === 'gemini'
9021
- ? 'gemini'
9022
- : (params.source === 'codebuddy'
9023
- ? 'codebuddy'
9024
- : (params.source === 'pi' ? 'pi' : ''))));
9581
+ const source = normalizeSessionSourceName(params.source, '');
9025
9582
  if (!source) {
9026
9583
  return { error: 'Invalid source' };
9027
9584
  }
@@ -9041,7 +9598,13 @@ async function readSessionPlain(params = {}) {
9041
9598
  );
9042
9599
 
9043
9600
  let extracted;
9044
- if (source === 'gemini') {
9601
+ if (source === 'opencode') {
9602
+ const opencodeSessionId = params.sessionId || parseOpencodeVirtualSessionId(filePath);
9603
+ extracted = queryOpencodeSessionMessages(opencodeSessionId, { maxMessages });
9604
+ if (!extracted) {
9605
+ return { error: 'Failed to parse session file' };
9606
+ }
9607
+ } else if (source === 'gemini') {
9045
9608
  let json;
9046
9609
  try {
9047
9610
  json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
@@ -9091,13 +9654,7 @@ async function readSessionPlain(params = {}) {
9091
9654
  }
9092
9655
 
9093
9656
  const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
9094
- const sourceLabel = source === 'codex'
9095
- ? 'Codex'
9096
- : (source === 'claude'
9097
- ? 'Claude Code'
9098
- : (source === 'gemini'
9099
- ? 'Gemini CLI'
9100
- : (source === 'pi' ? 'Pi' : 'CodeBuddy Code')));
9657
+ const sourceLabel = getSessionSourceLabel(source);
9101
9658
  const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
9102
9659
  const text = buildSessionPlainText(messages);
9103
9660
 
@@ -9113,15 +9670,7 @@ async function readSessionPlain(params = {}) {
9113
9670
  }
9114
9671
 
9115
9672
  async function exportSessionData(params = {}) {
9116
- const source = params.source === 'claude'
9117
- ? 'claude'
9118
- : (params.source === 'codex'
9119
- ? 'codex'
9120
- : (params.source === 'gemini'
9121
- ? 'gemini'
9122
- : (params.source === 'codebuddy'
9123
- ? 'codebuddy'
9124
- : (params.source === 'pi' ? 'pi' : ''))));
9673
+ const source = normalizeSessionSourceName(params.source, '');
9125
9674
  if (!source) {
9126
9675
  return { error: 'Invalid source' };
9127
9676
  }
@@ -9133,7 +9682,13 @@ async function exportSessionData(params = {}) {
9133
9682
  }
9134
9683
 
9135
9684
  let extracted;
9136
- if (source === 'gemini') {
9685
+ if (source === 'opencode') {
9686
+ const opencodeSessionId = params.sessionId || parseOpencodeVirtualSessionId(filePath);
9687
+ extracted = queryOpencodeSessionMessages(opencodeSessionId, { maxMessages });
9688
+ if (!extracted) {
9689
+ return { error: 'Failed to parse session file' };
9690
+ }
9691
+ } else if (source === 'gemini') {
9137
9692
  let json;
9138
9693
  try {
9139
9694
  json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
@@ -9191,13 +9746,7 @@ async function exportSessionData(params = {}) {
9191
9746
 
9192
9747
  const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
9193
9748
  const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
9194
- const sourceLabel = source === 'codex'
9195
- ? 'Codex'
9196
- : (source === 'claude'
9197
- ? 'Claude Code'
9198
- : (source === 'gemini'
9199
- ? 'Gemini CLI'
9200
- : (source === 'pi' ? 'Pi' : 'CodeBuddy Code')));
9749
+ const sourceLabel = getSessionSourceLabel(source);
9201
9750
  const truncated = !!extracted.truncated;
9202
9751
  const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
9203
9752
  const markdown = buildSessionMarkdown({
@@ -10389,17 +10938,16 @@ function cmdSwitch(providerName, silent = false) {
10389
10938
  throw new Error('提供商不存在');
10390
10939
  }
10391
10940
 
10392
- // 切换提供商
10941
+ // 切换提供商(单文件认证:同步确保目标块含 experimental_bearer_token)
10393
10942
  const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
10394
- const newContent = content.replace(
10943
+ const switchedContent = content.replace(
10395
10944
  /^(model_provider\s*=\s*)(["']).*?(["'])/m,
10396
10945
  `$1$2${providerName}$3`
10397
10946
  );
10398
- writeConfig(newContent);
10399
-
10400
- // 更新认证信息
10401
- const apiKey = providers[providerName].preferred_auth_method || '';
10402
- updateAuthJson(apiKey);
10947
+ const providerSegments = providers[providerName] && Array.isArray(providers[providerName].__codexmate_legacy_segments)
10948
+ ? providers[providerName].__codexmate_legacy_segments
10949
+ : null;
10950
+ writeConfig(ensureActiveProviderBearerToken(switchedContent, providerName, providerSegments));
10403
10951
 
10404
10952
  // 切换到该提供商的模型
10405
10953
  const currentModels = readCurrentModels();
@@ -10540,6 +11088,7 @@ base_url = "${safeBaseUrl}"
10540
11088
  wire_api = "responses"
10541
11089
  requires_openai_auth = true
10542
11090
  preferred_auth_method = "${safeApiKey}"
11091
+ experimental_bearer_token = "${safeApiKey}"
10543
11092
  request_max_retries = 4
10544
11093
  stream_max_retries = 10
10545
11094
  stream_idle_timeout_ms = 300000
@@ -10841,6 +11390,7 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
10841
11390
  updatedBlock = replaceTomlStringField(updatedBlock, 'base_url', computeLocalOpenaiBridgeBaseUrl());
10842
11391
  updatedBlock = replaceTomlBooleanField(updatedBlock, 'requires_openai_auth', true);
10843
11392
  updatedBlock = replaceTomlStringField(updatedBlock, 'preferred_auth_method', 'codexmate');
11393
+ updatedBlock = replaceTomlStringField(updatedBlock, 'experimental_bearer_token', 'codexmate');
10844
11394
  updatedBlock = replaceTomlStringField(updatedBlock, 'codexmate_bridge', 'openai');
10845
11395
  if (hasOpenaiBridgeMaxRetries) {
10846
11396
  updatedBlock = replaceTomlNumberField(updatedBlock, 'codexmate_bridge_max_retries', openaiBridgeMaxRetries);
@@ -10851,6 +11401,7 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
10851
11401
  }
10852
11402
  if (apiKey !== undefined) {
10853
11403
  updatedBlock = replaceTomlStringField(updatedBlock, 'preferred_auth_method', apiKey);
11404
+ updatedBlock = replaceTomlStringField(updatedBlock, 'experimental_bearer_token', apiKey);
10854
11405
  }
10855
11406
  }
10856
11407
 
@@ -10865,12 +11416,6 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
10865
11416
  }
10866
11417
  writeConfig(finalContent);
10867
11418
 
10868
- // 如果更新了 API Key 且该提供商是当前激活的,同步更新 auth.json
10869
- const currentProvider = config.model_provider;
10870
- if (apiKey !== undefined && name === currentProvider) {
10871
- updateAuthJson(apiKey);
10872
- }
10873
-
10874
11419
  if (!silent) {
10875
11420
  console.log('✓ 已更新提供商:', name);
10876
11421
  console.log();
@@ -11085,7 +11630,8 @@ function runKilocodeCommand(args = [], options = {}) {
11085
11630
  const child = spawn(bin, finalArgs, {
11086
11631
  detached: true,
11087
11632
  stdio: 'ignore',
11088
- windowsHide: true
11633
+ windowsHide: true,
11634
+ shell: process.platform === 'win32'
11089
11635
  });
11090
11636
  child.unref();
11091
11637
  return { success: true, bin, args: finalArgs, pid: child.pid };
@@ -11093,7 +11639,8 @@ function runKilocodeCommand(args = [], options = {}) {
11093
11639
  return new Promise((resolve, reject) => {
11094
11640
  const child = spawn(bin, finalArgs, {
11095
11641
  stdio: 'inherit',
11096
- windowsHide: false
11642
+ windowsHide: false,
11643
+ shell: process.platform === 'win32'
11097
11644
  });
11098
11645
  child.on('error', reject);
11099
11646
  child.on('exit', (code, signal) => {
@@ -12382,7 +12929,7 @@ function resolveExportOutputPath(outputPath, defaultFileName) {
12382
12929
  }
12383
12930
 
12384
12931
  function printExportSessionUsage() {
12385
- console.log('\n用法: codexmate export-session --source <codex|claude|gemini|codebuddy|pi> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
12932
+ console.log('\n用法: codexmate export-session --source <codex|claude|gemini|codebuddy|workbuddy|pi|opencode> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
12386
12933
  console.log('\n示例:');
12387
12934
  console.log(' codexmate export-session --source codex --session-id 123456');
12388
12935
  console.log(' codexmate export-session --source claude --file "~/.claude/projects/demo/session.jsonl"');
@@ -12454,8 +13001,8 @@ function parseExportSessionArgs(args = []) {
12454
13001
  }
12455
13002
 
12456
13003
  const normalizedSource = options.source.trim().toLowerCase();
12457
- if (normalizedSource && normalizedSource !== 'codex' && normalizedSource !== 'claude' && normalizedSource !== 'pi') {
12458
- errors.push('参数 --source 仅支持 codex、claude 或 pi');
13004
+ if (normalizedSource && !isSessionSourceOrAll(normalizedSource)) {
13005
+ errors.push('参数 --source 仅支持 codex、claude、gemini、codebuddy、workbuddy、pi 或 opencode');
12459
13006
  }
12460
13007
  options.source = normalizedSource;
12461
13008
 
@@ -12528,7 +13075,7 @@ async function cmdExportSession(args = []) {
12528
13075
 
12529
13076
  function printAnalyticsUsage() {
12530
13077
  console.log('\n用法:');
12531
- console.log(' codexmate analytics export [--format csv|json] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--model <MODEL>] [--source <codex|claude|gemini|codebuddy|pi|all>] [--output <PATH|->] [-o <PATH|->]');
13078
+ console.log(' codexmate analytics export [--format csv|json] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--model <MODEL>] [--source <codex|claude|gemini|codebuddy|workbuddy|pi|opencode|all>] [--output <PATH|->] [-o <PATH|->]');
12532
13079
  console.log('');
12533
13080
  }
12534
13081
 
@@ -13883,8 +14430,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13883
14430
  case 'list-sessions':
13884
14431
  {
13885
14432
  const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
13886
- if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'pi' && source !== 'all') {
13887
- result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, pi, or all' };
14433
+ if (source && !isSessionSourceOrAll(source)) {
14434
+ result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' };
13888
14435
  } else {
13889
14436
  result = {
13890
14437
  sessions: await listSessionBrowse(params),
@@ -13897,8 +14444,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13897
14444
  {
13898
14445
  const usageParams = isPlainObject(params) ? params : {};
13899
14446
  const source = typeof usageParams.source === 'string' ? usageParams.source.trim().toLowerCase() : '';
13900
- if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'pi' && source !== 'all') {
13901
- result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, pi, or all' };
14447
+ if (source && !isSessionSourceOrAll(source)) {
14448
+ result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' };
13902
14449
  } else {
13903
14450
  result = {
13904
14451
  sessions: await listSessionUsage({
@@ -13914,8 +14461,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13914
14461
  {
13915
14462
  const usageParams = isPlainObject(params) ? params : {};
13916
14463
  const source = typeof usageParams.source === 'string' ? usageParams.source.trim().toLowerCase() : '';
13917
- if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'pi' && source !== 'all') {
13918
- result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, pi, or all' };
14464
+ if (source && !isSessionSourceOrAll(source)) {
14465
+ result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' };
13919
14466
  } else {
13920
14467
  result = await exportSessionUsage({
13921
14468
  ...usageParams,
@@ -13927,8 +14474,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13927
14474
  case 'list-session-paths':
13928
14475
  {
13929
14476
  const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
13930
- if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'pi' && source !== 'all') {
13931
- result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, pi, or all' };
14477
+ if (source && !isSessionSourceOrAll(source)) {
14478
+ result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' };
13932
14479
  } else {
13933
14480
  result = {
13934
14481
  paths: listSessionPaths(params)
@@ -15439,7 +15986,7 @@ function buildMcpClaudeSettingsPayload() {
15439
15986
  function normalizeMcpSource(value) {
15440
15987
  const source = typeof value === 'string' ? value.trim().toLowerCase() : '';
15441
15988
  if (!source) return '';
15442
- if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi' || source === 'all') {
15989
+ if (normalizeSessionSourceName(source, '') || source === 'all') {
15443
15990
  return source;
15444
15991
  }
15445
15992
  return null;
@@ -15752,7 +16299,7 @@ function createWorkflowToolCatalog() {
15752
16299
  handler: async (args = {}) => {
15753
16300
  const source = normalizeMcpSource(args.source);
15754
16301
  if (source === null) {
15755
- return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, pi, or all' };
16302
+ return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' };
15756
16303
  }
15757
16304
  return {
15758
16305
  source: source || 'all',
@@ -16118,7 +16665,7 @@ function createMcpTools(options = {}) {
16118
16665
  const input = args && typeof args === 'object' ? args : {};
16119
16666
  const source = normalizeMcpSource(input.source);
16120
16667
  if (source === null) {
16121
- return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, pi, or all' };
16668
+ return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' };
16122
16669
  }
16123
16670
  const normalizedInput = {
16124
16671
  ...input,
@@ -16578,7 +17125,7 @@ function createMcpResources() {
16578
17125
  contents: [{
16579
17126
  uri,
16580
17127
  mimeType: 'application/json',
16581
- text: JSON.stringify({ error: 'Invalid source. Must be codex, claude, gemini, codebuddy, pi, or all' }, null, 2)
17128
+ text: JSON.stringify({ error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' }, null, 2)
16582
17129
  }]
16583
17130
  };
16584
17131
  }
@@ -16827,7 +17374,7 @@ function printMainHelp() {
16827
17374
  console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
16828
17375
  console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
16829
17376
  console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
16830
- console.log(' codexmate export-session --source <codex|claude|gemini|codebuddy|pi> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
17377
+ console.log(' codexmate export-session --source <codex|claude|gemini|codebuddy|workbuddy|pi|opencode> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
16831
17378
  console.log(' codexmate convert-session --from <codex|claude> --to <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
16832
17379
  console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
16833
17380
  console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');