codexmate 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -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;
@@ -752,6 +770,7 @@ let g_sessionFileLookupCache = {
752
770
  claude: new Map(),
753
771
  gemini: new Map(),
754
772
  codebuddy: new Map(),
773
+ workbuddy: new Map(),
755
774
  pi: new Map()
756
775
  };
757
776
  let g_exactMessageCountCache = new Map();
@@ -1012,7 +1031,7 @@ function normalizeSidebarCollapsedPreference(value) {
1012
1031
 
1013
1032
  function normalizeSessionFilterSourcePreference(value) {
1014
1033
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
1015
- if (normalized === 'all' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'pi') return normalized;
1034
+ if (normalized === 'all' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'workbuddy' || normalized === 'pi') return normalized;
1016
1035
  return 'codex';
1017
1036
  }
1018
1037
 
@@ -1195,7 +1214,6 @@ function getApiToolConfigWriteTarget(action) {
1195
1214
  const opencodeWriteActions = new Set([
1196
1215
  'apply-opencode-config',
1197
1216
  'update-opencode-selection',
1198
- 'apply-opencode-agents-file'
1199
1217
  ]);
1200
1218
  const kilocodeWriteActions = new Set([
1201
1219
  'apply-kilocode-config',
@@ -2114,6 +2132,16 @@ function getCodeBuddyProjectsDir() {
2114
2132
  return resolveExistingDir(candidates, CODEBUDDY_PROJECTS_DIR);
2115
2133
  }
2116
2134
 
2135
+ function getWorkBuddyProjectsDir() {
2136
+ const candidates = [];
2137
+ const envHome = process.env.WORKBUDDY_AI_HOME_DIR || process.env.WORKBUDDY_HOME;
2138
+ if (envHome) {
2139
+ candidates.push(path.join(envHome, 'projects'));
2140
+ }
2141
+ candidates.push(WORKBUDDY_PROJECTS_DIR);
2142
+ return resolveExistingDir(candidates, WORKBUDDY_PROJECTS_DIR);
2143
+ }
2144
+
2117
2145
  function getPiSessionsDir() {
2118
2146
  const candidates = [];
2119
2147
  const envPiHome = process.env.PI_HOME;
@@ -2128,6 +2156,250 @@ function getPiSessionsDir() {
2128
2156
  return resolveExistingDir(candidates, PI_SESSIONS_DIR);
2129
2157
  }
2130
2158
 
2159
+ function getOpencodeDataDir() {
2160
+ const candidates = [];
2161
+ const envOpencodeData = process.env.OPENCODE_DATA_HOME || process.env.OPENCODE_DATA_DIR;
2162
+ if (envOpencodeData) {
2163
+ candidates.push(envOpencodeData);
2164
+ }
2165
+ const xdgData = process.env.XDG_DATA_HOME;
2166
+ if (xdgData) {
2167
+ candidates.push(path.join(xdgData, 'opencode'));
2168
+ }
2169
+ candidates.push(OPENCODE_DATA_DIR);
2170
+ return resolveExistingDir(candidates, OPENCODE_DATA_DIR);
2171
+ }
2172
+
2173
+ function getOpencodeDbFile() {
2174
+ const dataDir = getOpencodeDataDir();
2175
+ return path.join(dataDir, 'opencode.db');
2176
+ }
2177
+
2178
+ function buildOpencodeVirtualSessionPath(sessionId) {
2179
+ const id = typeof sessionId === 'string' ? sessionId.trim() : '';
2180
+ if (!id) return '';
2181
+ return path.join(OPENCODE_DB_VIRTUAL_ROOT, `${id}.jsonl`);
2182
+ }
2183
+
2184
+ function parseOpencodeVirtualSessionId(filePath) {
2185
+ const raw = typeof filePath === 'string' ? filePath.trim() : '';
2186
+ if (!raw) return '';
2187
+ const base = path.basename(raw, path.extname(raw));
2188
+ return base && base.startsWith('ses_') ? base : '';
2189
+ }
2190
+
2191
+ function isOpencodeDbAvailable() {
2192
+ const Database = getNodeSqliteDatabaseSync();
2193
+ const dbFile = getOpencodeDbFile();
2194
+ return !!(Database && dbFile && fs.existsSync(dbFile));
2195
+ }
2196
+
2197
+ function withOpencodeDb(callback, fallback = null) {
2198
+ const Database = getNodeSqliteDatabaseSync();
2199
+ const dbFile = getOpencodeDbFile();
2200
+ if (!Database || !dbFile || !fs.existsSync(dbFile)) {
2201
+ return fallback;
2202
+ }
2203
+ let db;
2204
+ try {
2205
+ db = new Database(dbFile, { readOnly: true });
2206
+ return callback(db, dbFile);
2207
+ } catch (_) {
2208
+ return fallback;
2209
+ } finally {
2210
+ if (db) {
2211
+ try { db.close(); } catch (_) { }
2212
+ }
2213
+ }
2214
+ }
2215
+
2216
+ function parseJsonObjectSafe(value) {
2217
+ if (!value || typeof value !== 'string') return null;
2218
+ try {
2219
+ const parsed = JSON.parse(value);
2220
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
2221
+ } catch (_) {
2222
+ return null;
2223
+ }
2224
+ }
2225
+
2226
+ function toOpencodeIsoTime(value, fallback = '') {
2227
+ const numeric = Number(value);
2228
+ if (Number.isFinite(numeric) && numeric > 0) {
2229
+ return toIsoTime(numeric, fallback);
2230
+ }
2231
+ return toIsoTime(value, fallback);
2232
+ }
2233
+
2234
+ function readOpencodeModelName(value) {
2235
+ const parsed = typeof value === 'string' ? parseJsonObjectSafe(value) : (value && typeof value === 'object' ? value : null);
2236
+ if (!parsed) return '';
2237
+ return typeof parsed.id === 'string' && parsed.id.trim()
2238
+ ? parsed.id.trim()
2239
+ : (typeof parsed.modelID === 'string' ? parsed.modelID.trim() : '');
2240
+ }
2241
+
2242
+ function readOpencodeProviderName(value) {
2243
+ const parsed = typeof value === 'string' ? parseJsonObjectSafe(value) : (value && typeof value === 'object' ? value : null);
2244
+ if (!parsed) return 'opencode';
2245
+ return typeof parsed.providerID === 'string' && parsed.providerID.trim() ? parsed.providerID.trim() : 'opencode';
2246
+ }
2247
+
2248
+ function extractOpencodeTextFromPartData(data) {
2249
+ const part = typeof data === 'string' ? parseJsonObjectSafe(data) : (data && typeof data === 'object' ? data : null);
2250
+ if (!part) return '';
2251
+ const type = typeof part.type === 'string' ? part.type.trim().toLowerCase() : '';
2252
+ if (type !== 'text' && type !== 'reasoning') return '';
2253
+ return typeof part.text === 'string' ? part.text.trim() : '';
2254
+ }
2255
+
2256
+ function normalizeOpencodeMessageRow(row) {
2257
+ if (!row || typeof row !== 'object') return null;
2258
+ const messageData = parseJsonObjectSafe(row.message_data || row.data || '');
2259
+ const role = normalizeRole((messageData && messageData.role) || row.role || '');
2260
+ if (role !== 'user' && role !== 'assistant' && role !== 'system') return null;
2261
+ const rawParts = typeof row.parts === 'string' && row.parts ? row.parts.split('\n') : [];
2262
+ const textParts = rawParts.map(extractOpencodeTextFromPartData).filter(Boolean);
2263
+ const text = textParts.join('\n').trim();
2264
+ if (!text) return null;
2265
+ const model = messageData ? (messageData.modelID || readOpencodeModelName(messageData.model)) : '';
2266
+ const provider = messageData ? (messageData.providerID || readOpencodeProviderName(messageData.model)) : 'opencode';
2267
+ return {
2268
+ role,
2269
+ text,
2270
+ timestamp: toOpencodeIsoTime(row.time_created || (messageData && messageData.time && messageData.time.created), ''),
2271
+ model: typeof model === 'string' ? model : '',
2272
+ provider: typeof provider === 'string' && provider ? provider : 'opencode'
2273
+ };
2274
+ }
2275
+
2276
+ function queryOpencodeSessionMessages(sessionId, options = {}) {
2277
+ const id = typeof sessionId === 'string' ? sessionId.trim() : '';
2278
+ if (!id) return null;
2279
+ const maxMessages = resolveMaxMessagesValue(options.maxMessages, DEFAULT_SESSION_DETAIL_MESSAGES);
2280
+ const limit = maxMessages === Infinity ? OPENCODE_DB_READ_LIMIT : Math.min(OPENCODE_DB_READ_LIMIT, Math.max(1, Math.floor(maxMessages)));
2281
+ return withOpencodeDb((db) => {
2282
+ const totalRow = db.prepare('select count(*) as count from message where session_id = ?').get(id) || {};
2283
+ const totalMessages = Math.max(0, Math.floor(Number(totalRow.count) || 0));
2284
+ const rows = db.prepare(`
2285
+ select m.id as message_id, m.session_id, m.time_created, m.time_updated, m.data as message_data,
2286
+ group_concat(p.data, char(10)) as parts
2287
+ from (
2288
+ select * from message where session_id = ? order by time_created desc limit ?
2289
+ ) m
2290
+ left join part p on p.message_id = m.id
2291
+ group by m.id
2292
+ order by m.time_created asc
2293
+ `).all(id, limit);
2294
+ const messages = rows.map(normalizeOpencodeMessageRow).filter(Boolean);
2295
+ const sessionRow = db.prepare('select id, directory, path, time_updated from session where id = ?').get(id) || null;
2296
+ return {
2297
+ sessionId: id,
2298
+ cwd: sessionRow && (sessionRow.path || sessionRow.directory) ? (sessionRow.path || sessionRow.directory) : '',
2299
+ updatedAt: sessionRow ? toOpencodeIsoTime(sessionRow.time_updated, '') : '',
2300
+ messages,
2301
+ totalMessages,
2302
+ clipped: maxMessages !== Infinity && totalMessages > messages.length,
2303
+ truncated: maxMessages !== Infinity && totalMessages > messages.length
2304
+ };
2305
+ }, null);
2306
+ }
2307
+
2308
+ function countOpencodeMessages(sessionId) {
2309
+ const id = typeof sessionId === 'string' ? sessionId.trim() : '';
2310
+ if (!id) return null;
2311
+ return withOpencodeDb((db) => {
2312
+ const row = db.prepare('select count(*) as count from message where session_id = ?').get(id) || {};
2313
+ return Math.max(0, Math.floor(Number(row.count) || 0));
2314
+ }, null);
2315
+ }
2316
+
2317
+ function parseOpencodeSessionSummaryRow(row, db) {
2318
+ if (!row || typeof row !== 'object') return null;
2319
+ const sessionId = typeof row.id === 'string' ? row.id.trim() : '';
2320
+ if (!sessionId) return null;
2321
+ const model = readOpencodeModelName(row.model);
2322
+ const provider = readOpencodeProviderName(row.model);
2323
+ let title = typeof row.title === 'string' && row.title.trim() ? row.title.trim() : (row.slug || sessionId);
2324
+ const firstRow = db.prepare(`
2325
+ select m.data as message_data, group_concat(p.data, char(10)) as parts
2326
+ from message m left join part p on p.message_id = m.id
2327
+ where m.session_id = ?
2328
+ group by m.id
2329
+ order by m.time_created asc
2330
+ limit 12
2331
+ `).all(sessionId).map(normalizeOpencodeMessageRow).filter(Boolean).find(item => item.role === 'user' && item.text);
2332
+ if (firstRow && firstRow.text) {
2333
+ title = truncateText(firstRow.text, 120);
2334
+ }
2335
+ const messageCount = Math.max(0, Math.floor(Number(row.message_count) || 0));
2336
+ const inputTokens = readNonNegativeInteger(row.tokens_input) || 0;
2337
+ const outputTokens = readNonNegativeInteger(row.tokens_output) || 0;
2338
+ const reasoningOutputTokens = readNonNegativeInteger(row.tokens_reasoning) || 0;
2339
+ const cachedInputTokens = readNonNegativeInteger(row.tokens_cache_read) || 0;
2340
+ const cacheCreationInputTokens = readNonNegativeInteger(row.tokens_cache_write) || 0;
2341
+ const totalTokens = inputTokens + outputTokens + reasoningOutputTokens + cachedInputTokens + cacheCreationInputTokens;
2342
+ return {
2343
+ source: 'opencode',
2344
+ sourceLabel: 'OpenCode',
2345
+ provider,
2346
+ model,
2347
+ models: model ? [model] : [],
2348
+ sessionId,
2349
+ title,
2350
+ cwd: row.path || row.directory || '',
2351
+ createdAt: toOpencodeIsoTime(row.time_created, ''),
2352
+ updatedAt: toOpencodeIsoTime(row.time_updated, ''),
2353
+ messageCount,
2354
+ totalTokens,
2355
+ contextWindow: 0,
2356
+ inputTokens,
2357
+ cachedInputTokens,
2358
+ cacheCreationInputTokens,
2359
+ outputTokens,
2360
+ reasoningOutputTokens,
2361
+ __messageCountExact: true,
2362
+ filePath: buildOpencodeVirtualSessionPath(sessionId),
2363
+ keywords: [],
2364
+ capabilities: { code: true }
2365
+ };
2366
+ }
2367
+
2368
+ function parseOpencodeSessionSummary(sessionId) {
2369
+ const id = typeof sessionId === 'string' ? sessionId.trim() : '';
2370
+ if (!id) return null;
2371
+ return withOpencodeDb((db) => {
2372
+ const row = db.prepare(`
2373
+ select s.*, (select count(*) from message m where m.session_id = s.id) as message_count
2374
+ from session s where s.id = ?
2375
+ `).get(id);
2376
+ return parseOpencodeSessionSummaryRow(row, db);
2377
+ }, null);
2378
+ }
2379
+
2380
+ function listOpencodeSessions(limit, options = {}) {
2381
+ if (!isOpencodeDbAvailable()) return [];
2382
+ const scanFactor = Number.isFinite(Number(options.scanFactor)) ? Math.max(1, Number(options.scanFactor)) : SESSION_SCAN_FACTOR;
2383
+ const targetCount = Number.isFinite(Number(options.targetCount)) ? Math.max(1, Math.floor(Number(options.targetCount))) : Math.max(1, Math.floor(limit * scanFactor));
2384
+ 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));
2385
+ return withOpencodeDb((db) => {
2386
+ const rows = db.prepare(`
2387
+ select s.*, (select count(*) from message m where m.session_id = s.id) as message_count
2388
+ from session s
2389
+ where s.time_archived is null
2390
+ order by s.time_updated desc
2391
+ limit ?
2392
+ `).all(Math.min(OPENCODE_DB_READ_LIMIT, Math.max(scanCount, targetCount)));
2393
+ const sessions = [];
2394
+ for (const row of rows) {
2395
+ const summary = parseOpencodeSessionSummaryRow(row, db);
2396
+ if (summary) sessions.push(summary);
2397
+ if (sessions.length >= targetCount) break;
2398
+ }
2399
+ return mergeAndLimitSessions(sessions, limit);
2400
+ }, []);
2401
+ }
2402
+
2131
2403
  function getCodexmateDerivedSessionsRoot(target) {
2132
2404
  if (target === 'claude') {
2133
2405
  return CODEXMATE_DERIVED_CLAUDE_DIR;
@@ -4084,7 +4356,7 @@ function countConversationMessagesInRecords(records, source) {
4084
4356
  }
4085
4357
  continue;
4086
4358
  }
4087
- if (source === 'codebuddy') {
4359
+ if (source === 'codebuddy' || source === 'workbuddy') {
4088
4360
  if (record && record.type === 'message') {
4089
4361
  const role = normalizeRole(record.role);
4090
4362
  if (role === 'assistant' || role === 'user' || role === 'system') {
@@ -4167,7 +4439,7 @@ async function countConversationMessagesInFile(filePath, source) {
4167
4439
  role = normalizeRole(record.payload.role);
4168
4440
  text = extractMessageText(record.payload.content);
4169
4441
  }
4170
- } else if (source === 'codebuddy') {
4442
+ } else if (source === 'codebuddy' || source === 'workbuddy') {
4171
4443
  if (record && record.type === 'message') {
4172
4444
  role = normalizeRole(record.role);
4173
4445
  if (role === 'assistant' || role === 'user' || role === 'system') {
@@ -4347,15 +4619,7 @@ async function resolveSessionTrashEntryExactMessageCount(entry) {
4347
4619
  }
4348
4620
 
4349
4621
  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'))));
4622
+ const source = normalizeSessionSourceName(options.source, '') || 'all';
4359
4623
  const hydratedEntries = await mapWithConcurrency(Array.isArray(entries) ? entries : [], 8, async (entry) => {
4360
4624
  const normalizedEntry = normalizeSessionTrashEntry(entry);
4361
4625
  if (!normalizedEntry) {
@@ -4364,7 +4628,7 @@ async function hydrateSessionTrashEntries(entries, options = {}) {
4364
4628
  return await resolveSessionTrashEntryExactMessageCount(normalizedEntry);
4365
4629
  });
4366
4630
 
4367
- if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi') {
4631
+ if (normalizeSessionSourceName(source, '')) {
4368
4632
  return hydratedEntries.filter((entry) => entry.source === source);
4369
4633
  }
4370
4634
  return hydratedEntries;
@@ -4378,15 +4642,7 @@ async function hydrateSessionItemsExactMessageCount(items) {
4378
4642
  if (item.__messageCountExact === true) {
4379
4643
  return item;
4380
4644
  }
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' : ''))));
4645
+ const source = normalizeSessionSourceName(item.source, '');
4390
4646
  const filePath = typeof item.filePath === 'string' ? item.filePath : '';
4391
4647
  if (!source || !filePath || !fs.existsSync(filePath)) {
4392
4648
  return item;
@@ -4413,6 +4669,40 @@ async function hydrateSessionItemsExactMessageCount(items) {
4413
4669
  });
4414
4670
  }
4415
4671
 
4672
+
4673
+ function normalizeSessionSourceName(value, fallback = '') {
4674
+ const source = typeof value === 'string' ? value.trim().toLowerCase() : '';
4675
+ if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'pi' || source === 'opencode') {
4676
+ return source;
4677
+ }
4678
+ return fallback;
4679
+ }
4680
+
4681
+ function isSessionSourceOrAll(value) {
4682
+ const source = typeof value === 'string' ? value.trim().toLowerCase() : '';
4683
+ return source === 'all' || !!normalizeSessionSourceName(source, '');
4684
+ }
4685
+
4686
+ function getSessionSourceLabel(source) {
4687
+ if (source === 'claude') return 'Claude Code';
4688
+ if (source === 'gemini') return 'Gemini CLI';
4689
+ if (source === 'codebuddy') return 'CodeBuddy Code';
4690
+ if (source === 'workbuddy') return 'WorkBuddy AI';
4691
+ if (source === 'pi') return 'Pi';
4692
+ if (source === 'opencode') return 'OpenCode';
4693
+ return 'Codex';
4694
+ }
4695
+
4696
+ function getSessionSourceProvider(source) {
4697
+ if (source === 'claude') return 'claude';
4698
+ if (source === 'gemini') return 'gemini';
4699
+ if (source === 'codebuddy') return 'codebuddy';
4700
+ if (source === 'workbuddy') return 'workbuddy';
4701
+ if (source === 'pi') return 'pi';
4702
+ if (source === 'opencode') return 'opencode';
4703
+ return 'codex';
4704
+ }
4705
+
4416
4706
  function getSessionExportKeyForApi(item) {
4417
4707
  const source = item && item.source ? String(item.source).trim() : '';
4418
4708
  const sessionId = item && item.sessionId ? String(item.sessionId) : '';
@@ -4430,20 +4720,21 @@ async function readSessionMessageCounts(params = {}) {
4430
4720
  return undefined;
4431
4721
  }
4432
4722
  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' : ''))));
4723
+ const source = normalizeSessionSourceName(item.source, '');
4442
4724
  const filePath = typeof item.filePath === 'string' ? item.filePath : '';
4443
- if (!source || !filePath || !fs.existsSync(filePath)) {
4725
+ if (!source) {
4444
4726
  return { key };
4445
4727
  }
4446
- const exactMessageCount = await countConversationMessagesInFile(filePath, source);
4728
+ let exactMessageCount;
4729
+ if (source === 'opencode') {
4730
+ const opencodeSessionId = item.sessionId || parseOpencodeVirtualSessionId(filePath);
4731
+ exactMessageCount = countOpencodeMessages(opencodeSessionId);
4732
+ } else {
4733
+ if (!filePath || !fs.existsSync(filePath)) {
4734
+ return { key };
4735
+ }
4736
+ exactMessageCount = await countConversationMessagesInFile(filePath, source);
4737
+ }
4447
4738
  if (!Number.isFinite(Number(exactMessageCount))) {
4448
4739
  return { key };
4449
4740
  }
@@ -4573,6 +4864,10 @@ function createSessionQueryScanState(tokens, options = {}) {
4573
4864
  const snippetLimit = Number.isFinite(Number(options.snippetLimit))
4574
4865
  ? Math.max(0, Number(options.snippetLimit))
4575
4866
  : 0;
4867
+ const positionsLimitRaw = Number(options.positionsLimit);
4868
+ const positionsLimit = Number.isFinite(positionsLimitRaw) && positionsLimitRaw > 0
4869
+ ? Math.floor(positionsLimitRaw)
4870
+ : 0;
4576
4871
 
4577
4872
  return {
4578
4873
  tokens,
@@ -4580,8 +4875,10 @@ function createSessionQueryScanState(tokens, options = {}) {
4580
4875
  roleFilter,
4581
4876
  maxMatches,
4582
4877
  snippetLimit,
4878
+ positionsLimit,
4583
4879
  count: 0,
4584
4880
  snippets: [],
4881
+ positions: [],
4585
4882
  leadingSystem: roleFilter !== 'system'
4586
4883
  };
4587
4884
  }
@@ -4613,6 +4910,13 @@ function consumeSessionQueryMessage(state, message) {
4613
4910
  if (state.snippetLimit > 0 && state.snippets.length < state.snippetLimit) {
4614
4911
  state.snippets.push(truncateText(text));
4615
4912
  }
4913
+ if (state.positionsLimit > 0 && state.positions.length < state.positionsLimit) {
4914
+ state.positions.push({
4915
+ lineIndex: Number.isInteger(message.recordLineIndex) ? message.recordLineIndex : -1,
4916
+ role,
4917
+ timestamp: typeof message.timestamp === 'string' ? message.timestamp : ''
4918
+ });
4919
+ }
4616
4920
  return state.count >= state.maxMatches;
4617
4921
  }
4618
4922
 
@@ -4620,7 +4924,8 @@ function buildSessionQueryScanResult(state) {
4620
4924
  return {
4621
4925
  hit: !!(state && state.count > 0),
4622
4926
  count: state && Number.isFinite(state.count) ? state.count : 0,
4623
- snippets: state && Array.isArray(state.snippets) ? state.snippets : []
4927
+ snippets: state && Array.isArray(state.snippets) ? state.snippets : [],
4928
+ positions: state && Array.isArray(state.positions) ? state.positions : []
4624
4929
  };
4625
4930
  }
4626
4931
 
@@ -4628,12 +4933,14 @@ function scanSessionContentForQueryInRecords(records, source, state) {
4628
4933
  if (!Array.isArray(records) || !state) {
4629
4934
  return buildSessionQueryScanResult(state);
4630
4935
  }
4631
-
4632
- for (const record of records) {
4936
+ for (let recordIndex = 0; recordIndex < records.length; recordIndex++) {
4937
+ const record = records[recordIndex];
4633
4938
  const message = extractMessageFromRecord(record, source);
4634
4939
  if (!message) {
4635
4940
  continue;
4636
4941
  }
4942
+ message.recordLineIndex = recordIndex;
4943
+ message.timestamp = toIsoTime(record && record.timestamp, '');
4637
4944
  if (consumeSessionQueryMessage(state, message)) {
4638
4945
  break;
4639
4946
  }
@@ -4644,12 +4951,12 @@ function scanSessionContentForQueryInRecords(records, source, state) {
4644
4951
 
4645
4952
  async function scanSessionContentForQuery(session, tokens, options = {}) {
4646
4953
  if (!session || !Array.isArray(tokens) || tokens.length === 0) {
4647
- return { hit: false, count: 0, snippets: [] };
4954
+ return { hit: false, count: 0, snippets: [], positions: [] };
4648
4955
  }
4649
4956
 
4650
4957
  const filePath = resolveSessionFilePath(session.source, session.filePath, session.sessionId);
4651
4958
  if (!filePath) {
4652
- return { hit: false, count: 0, snippets: [] };
4959
+ return { hit: false, count: 0, snippets: [], positions: [] };
4653
4960
  }
4654
4961
 
4655
4962
  const rawMaxBytes = Number(options.maxBytes);
@@ -4657,8 +4964,17 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
4657
4964
  ? Math.max(1024, rawMaxBytes)
4658
4965
  : 0;
4659
4966
  const state = createSessionQueryScanState(tokens, options);
4967
+ if (session.source === 'opencode') {
4968
+ const extracted = queryOpencodeSessionMessages(session.sessionId || parseOpencodeVirtualSessionId(filePath), { maxMessages: Infinity });
4969
+ const messages = extracted && Array.isArray(extracted.messages) ? extracted.messages : [];
4970
+ for (let i = 0; i < messages.length; i += 1) {
4971
+ const message = { ...messages[i], recordLineIndex: i };
4972
+ if (consumeSessionQueryMessage(state, message)) break;
4973
+ }
4974
+ return buildSessionQueryScanResult(state);
4975
+ }
4660
4976
  if (session.source === 'gemini') {
4661
- if (state.roleFilter !== 'all') {
4977
+ if (state.roleFilter !== 'all' || state.positionsLimit > 0) {
4662
4978
  let json;
4663
4979
  try {
4664
4980
  json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
@@ -4672,7 +4988,12 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
4672
4988
  if (!role) continue;
4673
4989
  const text = extractMessageText(extractGeminiMessageText(entry.content ?? entry.message ?? entry.text));
4674
4990
  if (!text) continue;
4675
- if (consumeSessionQueryMessage(state, { role, text })) {
4991
+ if (consumeSessionQueryMessage(state, {
4992
+ role,
4993
+ text,
4994
+ timestamp: toIsoTime(entry.timestamp ?? entry.time ?? entry.at, ''),
4995
+ recordLineIndex: -1
4996
+ })) {
4676
4997
  break;
4677
4998
  }
4678
4999
  }
@@ -4712,7 +5033,10 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
4712
5033
  rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
4713
5034
 
4714
5035
  let bytesRead = 0;
5036
+ let lineIndex = 0;
4715
5037
  for await (const line of rl) {
5038
+ const currentLineIndex = lineIndex;
5039
+ lineIndex += 1;
4716
5040
  if (maxBytes > 0 && bytesRead >= maxBytes) {
4717
5041
  break;
4718
5042
  }
@@ -4734,6 +5058,8 @@ async function scanSessionContentForQuery(session, tokens, options = {}) {
4734
5058
  if (!message) {
4735
5059
  continue;
4736
5060
  }
5061
+ message.recordLineIndex = currentLineIndex;
5062
+ message.timestamp = toIsoTime(record.timestamp, '');
4737
5063
  if (consumeSessionQueryMessage(state, message)) {
4738
5064
  break;
4739
5065
  }
@@ -4952,13 +5278,7 @@ function setSessionListCache(cacheKey, value) {
4952
5278
  }
4953
5279
 
4954
5280
  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')));
5281
+ const normalizedSource = normalizeSessionSourceName(source, 'codex');
4962
5282
  const normalizedLimit = Number.isFinite(Number(limit))
4963
5283
  ? Math.max(1, Math.floor(Number(limit)))
4964
5284
  : 1;
@@ -5044,7 +5364,7 @@ function getSessionInventoryCache(cacheKey, forceRefresh = false) {
5044
5364
  }
5045
5365
 
5046
5366
  function registerSessionFileLookupEntries(source, sessions = []) {
5047
- const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi'
5367
+ const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'pi' || source === 'opencode'
5048
5368
  ? source
5049
5369
  : 'codex';
5050
5370
  const store = g_sessionFileLookupCache[normalizedSource];
@@ -5085,7 +5405,7 @@ function setSessionInventoryCache(cacheKey, source, value) {
5085
5405
  }
5086
5406
 
5087
5407
  function listSessionInventoryBySource(source, limit, scanOptions = {}, options = {}) {
5088
- const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi'
5408
+ const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'pi' || source === 'opencode'
5089
5409
  ? source
5090
5410
  : 'codex';
5091
5411
  const forceRefresh = !!options.forceRefresh;
@@ -5101,9 +5421,13 @@ function listSessionInventoryBySource(source, limit, scanOptions = {}, options =
5101
5421
  ? listGeminiSessions(limit, scanOptions)
5102
5422
  : (normalizedSource === 'codebuddy'
5103
5423
  ? listCodeBuddySessions(limit, scanOptions)
5104
- : (normalizedSource === 'pi'
5105
- ? listPiSessions(limit, scanOptions)
5106
- : listCodexSessions(limit, scanOptions))));
5424
+ : (normalizedSource === 'workbuddy'
5425
+ ? listWorkBuddySessions(limit, scanOptions)
5426
+ : (normalizedSource === 'pi'
5427
+ ? listPiSessions(limit, scanOptions)
5428
+ : (normalizedSource === 'opencode'
5429
+ ? listOpencodeSessions(limit, scanOptions)
5430
+ : listCodexSessions(limit, scanOptions))))));
5107
5431
  setSessionInventoryCache(cacheKey, normalizedSource, sessions);
5108
5432
  return sessions;
5109
5433
  }
@@ -5116,7 +5440,9 @@ function invalidateSessionListCache() {
5116
5440
  claude: new Map(),
5117
5441
  gemini: new Map(),
5118
5442
  codebuddy: new Map(),
5119
- pi: new Map()
5443
+ workbuddy: new Map(),
5444
+ pi: new Map(),
5445
+ opencode: new Map()
5120
5446
  };
5121
5447
  }
5122
5448
 
@@ -5350,13 +5676,7 @@ function readSessionProviderFromRecord(record, source = '') {
5350
5676
  if (provider) {
5351
5677
  return provider;
5352
5678
  }
5353
- return source === 'claude'
5354
- ? 'claude'
5355
- : (source === 'gemini'
5356
- ? 'gemini'
5357
- : (source === 'codebuddy'
5358
- ? 'codebuddy'
5359
- : (source === 'pi' ? 'pi' : 'codex')));
5679
+ return getSessionSourceProvider(source);
5360
5680
  }
5361
5681
 
5362
5682
  function applySessionUsageSummaryFromRecord(state, record, source) {
@@ -5901,6 +6221,175 @@ function parseCodeBuddySessionSummary(filePath, options = {}) {
5901
6221
  };
5902
6222
  }
5903
6223
 
6224
+ // WorkBuddy AI 会话记录与 CodeBuddy 同族(type:"message" + 顶层 role/content,
6225
+ // 且存在 __codebuddyLocal 字段),标题来自 ai-title 记录,模型名位于 providerData.model,
6226
+ // token 用量位于嵌套 message.usage。
6227
+ function parseWorkBuddySessionSummary(filePath, options = {}) {
6228
+ const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
6229
+ ? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
6230
+ : SESSION_SUMMARY_READ_BYTES;
6231
+ const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
6232
+ ? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
6233
+ : SESSION_TITLE_READ_BYTES;
6234
+ const records = parseJsonlHeadRecords(filePath, summaryReadBytes);
6235
+ if (records.length === 0) {
6236
+ return null;
6237
+ }
6238
+
6239
+ let stat;
6240
+ try {
6241
+ stat = fs.statSync(filePath);
6242
+ } catch (_) {
6243
+ return null;
6244
+ }
6245
+
6246
+ let sessionId = path.basename(filePath, '.jsonl');
6247
+ let cwd = '';
6248
+ let firstPrompt = '';
6249
+ let aiTitle = '';
6250
+ let messageCount = 0;
6251
+ let totalTokens = 0;
6252
+ let contextWindow = 0;
6253
+ let inputTokens = 0;
6254
+ let cachedInputTokens = 0;
6255
+ let cacheCreationInputTokens = 0;
6256
+ let outputTokens = 0;
6257
+ let reasoningOutputTokens = 0;
6258
+ let provider = 'workbuddy';
6259
+ let model = '';
6260
+ const models = [];
6261
+ const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, cacheCreationInputTokens, outputTokens, reasoningOutputTokens };
6262
+ const previewMessages = [];
6263
+ let createdAt = '';
6264
+ let updatedAt = stat.mtime.toISOString();
6265
+
6266
+ for (const record of records) {
6267
+ if (!createdAt && record && record.timestamp) {
6268
+ createdAt = toIsoTime(record.timestamp, createdAt);
6269
+ }
6270
+ if (record && record.timestamp) {
6271
+ updatedAt = updateLatestIso(updatedAt, record.timestamp);
6272
+ }
6273
+
6274
+ applySessionUsageSummaryFromRecord(usageState, record, 'workbuddy');
6275
+ totalTokens = usageState.totalTokens || 0;
6276
+ contextWindow = usageState.contextWindow || 0;
6277
+ inputTokens = usageState.inputTokens || 0;
6278
+ cachedInputTokens = usageState.cachedInputTokens || 0;
6279
+ cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
6280
+ outputTokens = usageState.outputTokens || 0;
6281
+ reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
6282
+
6283
+ if (record && typeof record.sessionId === 'string' && record.sessionId.trim()) {
6284
+ sessionId = record.sessionId.trim();
6285
+ }
6286
+ if (!cwd && record && typeof record.cwd === 'string' && record.cwd.trim()) {
6287
+ cwd = record.cwd.trim();
6288
+ }
6289
+
6290
+ provider = readExplicitSessionProviderFromRecord(record) || provider;
6291
+ const recordModels = readSessionModelsFromRecord(record);
6292
+ for (const recordModel of recordModels) {
6293
+ if (!models.includes(recordModel)) {
6294
+ models.push(recordModel);
6295
+ }
6296
+ }
6297
+ model = recordModels[0] || model;
6298
+
6299
+ if (record && record.type === 'ai-title' && typeof record.aiTitle === 'string' && record.aiTitle.trim()) {
6300
+ aiTitle = record.aiTitle.trim();
6301
+ }
6302
+
6303
+ if (record && record.type === 'message') {
6304
+ const role = normalizeRole(record.role);
6305
+ if (role === 'assistant' || role === 'user' || role === 'system') {
6306
+ const content = record.message?.content ?? record.content ?? '';
6307
+ previewMessages.push({
6308
+ role,
6309
+ text: extractMessageText(content)
6310
+ });
6311
+ }
6312
+ }
6313
+ }
6314
+
6315
+ const tailRecords = parseJsonlTailRecords(filePath, summaryReadBytes);
6316
+ for (const record of tailRecords) {
6317
+ applySessionUsageSummaryFromRecord(usageState, record, 'workbuddy');
6318
+ totalTokens = usageState.totalTokens || 0;
6319
+ contextWindow = usageState.contextWindow || 0;
6320
+ inputTokens = usageState.inputTokens || 0;
6321
+ cachedInputTokens = usageState.cachedInputTokens || 0;
6322
+ cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
6323
+ outputTokens = usageState.outputTokens || 0;
6324
+ reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
6325
+ provider = readExplicitSessionProviderFromRecord(record) || provider;
6326
+ const recordModels = readSessionModelsFromRecord(record);
6327
+ for (const recordModel of recordModels) {
6328
+ if (!models.includes(recordModel)) {
6329
+ models.push(recordModel);
6330
+ }
6331
+ }
6332
+ model = recordModels[0] || model;
6333
+ }
6334
+
6335
+ const filteredPreviewMessages = removeLeadingSystemMessage(previewMessages);
6336
+ messageCount = filteredPreviewMessages.length;
6337
+ const firstUser = filteredPreviewMessages.find(item => item.role === 'user' && item.text);
6338
+ if (firstUser) {
6339
+ firstPrompt = truncateText(firstUser.text);
6340
+ }
6341
+
6342
+ if (!firstPrompt) {
6343
+ const titleRecords = parseJsonlHeadRecords(filePath, titleReadBytes);
6344
+ const titleMessages = [];
6345
+ for (const record of titleRecords) {
6346
+ if (record && record.type === 'message') {
6347
+ const role = normalizeRole(record.role);
6348
+ if (role === 'assistant' || role === 'user' || role === 'system') {
6349
+ const content = record.message?.content ?? record.content ?? '';
6350
+ titleMessages.push({
6351
+ role,
6352
+ text: extractMessageText(content)
6353
+ });
6354
+ }
6355
+ }
6356
+ }
6357
+
6358
+ const filteredTitleMessages = removeLeadingSystemMessage(titleMessages);
6359
+ const titleUser = filteredTitleMessages.find(item => item.role === 'user' && item.text);
6360
+ if (titleUser) {
6361
+ firstPrompt = truncateText(titleUser.text);
6362
+ }
6363
+ }
6364
+
6365
+ messageCount = Math.max(0, messageCount);
6366
+
6367
+ return {
6368
+ source: 'workbuddy',
6369
+ sourceLabel: 'WorkBuddy AI',
6370
+ provider,
6371
+ model,
6372
+ models,
6373
+ sessionId,
6374
+ title: aiTitle || firstPrompt || sessionId,
6375
+ cwd,
6376
+ createdAt,
6377
+ updatedAt,
6378
+ messageCount,
6379
+ totalTokens,
6380
+ contextWindow,
6381
+ inputTokens,
6382
+ cachedInputTokens,
6383
+ cacheCreationInputTokens,
6384
+ outputTokens,
6385
+ reasoningOutputTokens,
6386
+ __messageCountExact: isSessionSummaryMessageCountExact(stat, summaryReadBytes),
6387
+ filePath,
6388
+ keywords: [],
6389
+ capabilities: { code: true }
6390
+ };
6391
+ }
6392
+
5904
6393
  function extractGeminiMessageText(content) {
5905
6394
  if (typeof content === 'string') {
5906
6395
  return content;
@@ -6665,6 +7154,54 @@ function listCodeBuddySessions(limit, options = {}) {
6665
7154
  return mergeAndLimitSessions(sessions, limit);
6666
7155
  }
6667
7156
 
7157
+ function listWorkBuddySessions(limit, options = {}) {
7158
+ const projectsDir = getWorkBuddyProjectsDir();
7159
+ if (!fs.existsSync(projectsDir)) {
7160
+ return [];
7161
+ }
7162
+
7163
+ const scanFactor = Number.isFinite(Number(options.scanFactor))
7164
+ ? Math.max(1, Number(options.scanFactor))
7165
+ : SESSION_SCAN_FACTOR;
7166
+ const minFiles = Number.isFinite(Number(options.minFiles))
7167
+ ? Math.max(1, Number(options.minFiles))
7168
+ : Math.min(SESSION_SCAN_MIN_FILES, MAX_SESSION_LIST_SIZE * SESSION_SCAN_FACTOR);
7169
+ const targetCount = Number.isFinite(Number(options.targetCount))
7170
+ ? Math.max(1, Math.floor(Number(options.targetCount)))
7171
+ : Math.max(1, Math.floor(limit * scanFactor));
7172
+ const scanCount = Number.isFinite(Number(options.scanCount))
7173
+ ? Math.max(targetCount, Math.floor(Number(options.scanCount)))
7174
+ : Math.max(targetCount, minFiles);
7175
+ const maxFilesScanned = Number.isFinite(Number(options.maxFilesScanned))
7176
+ ? Math.max(scanCount, Math.floor(Number(options.maxFilesScanned)))
7177
+ : Math.max(scanCount * 2, minFiles);
7178
+ const summaryReadBytes = Number.isFinite(Number(options.summaryReadBytes))
7179
+ ? Math.max(1024, Math.floor(Number(options.summaryReadBytes)))
7180
+ : SESSION_SUMMARY_READ_BYTES;
7181
+ const titleReadBytes = Number.isFinite(Number(options.titleReadBytes))
7182
+ ? Math.max(1024, Math.floor(Number(options.titleReadBytes)))
7183
+ : SESSION_TITLE_READ_BYTES;
7184
+
7185
+ const files = collectRecentJsonlFiles(projectsDir, {
7186
+ returnCount: scanCount,
7187
+ maxFilesScanned
7188
+ });
7189
+ const sessions = [];
7190
+ for (const filePath of files) {
7191
+ const summary = parseWorkBuddySessionSummary(filePath, {
7192
+ summaryReadBytes,
7193
+ titleReadBytes
7194
+ });
7195
+ if (summary) {
7196
+ sessions.push(summary);
7197
+ }
7198
+ if (sessions.length >= targetCount) {
7199
+ break;
7200
+ }
7201
+ }
7202
+ return mergeAndLimitSessions(sessions, limit);
7203
+ }
7204
+
6668
7205
  function listPiSessions(limit, options = {}) {
6669
7206
  const sessionsDir = getPiSessionsDir();
6670
7207
  if (!fs.existsSync(sessionsDir)) {
@@ -6714,9 +7251,7 @@ function listPiSessions(limit, options = {}) {
6714
7251
  }
6715
7252
 
6716
7253
  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';
7254
+ const source = normalizeSessionSourceName(params.source, '') || 'all';
6720
7255
  const rawLimit = Number(params.limit);
6721
7256
  const limit = Number.isFinite(rawLimit)
6722
7257
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
@@ -6764,9 +7299,15 @@ async function listAllSessions(params = {}) {
6764
7299
  if (source === 'all' || source === 'codebuddy') {
6765
7300
  sessions = sessions.concat(listSessionInventoryBySource('codebuddy', limit, scanOptions, { forceRefresh }));
6766
7301
  }
7302
+ if (source === 'all' || source === 'workbuddy') {
7303
+ sessions = sessions.concat(listSessionInventoryBySource('workbuddy', limit, scanOptions, { forceRefresh }));
7304
+ }
6767
7305
  if (source === 'all' || source === 'pi') {
6768
7306
  sessions = sessions.concat(listSessionInventoryBySource('pi', limit, scanOptions, { forceRefresh }));
6769
7307
  }
7308
+ if (source === 'all' || source === 'opencode') {
7309
+ sessions = sessions.concat(listSessionInventoryBySource('opencode', limit, scanOptions, { forceRefresh }));
7310
+ }
6770
7311
 
6771
7312
  if (hasPathFilter) {
6772
7313
  sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
@@ -6789,9 +7330,7 @@ async function listAllSessions(params = {}) {
6789
7330
  }
6790
7331
 
6791
7332
  async function listAllSessionsData(params = {}) {
6792
- const source = params.source === 'codex' || params.source === 'claude'
6793
- ? params.source
6794
- : 'all';
7333
+ const source = normalizeSessionSourceName(params.source, '') || 'all';
6795
7334
  const rawLimit = Number(params.limit);
6796
7335
  const limit = Number.isFinite(rawLimit)
6797
7336
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
@@ -6848,6 +7387,7 @@ async function listSessionUsage(params = {}) {
6848
7387
  parseCodexSessionSummary,
6849
7388
  parseClaudeSessionSummary,
6850
7389
  parseCodeBuddySessionSummary,
7390
+ parseWorkBuddySessionSummary,
6851
7391
  parseGeminiSessionSummary,
6852
7392
  parsePiSessionSummary,
6853
7393
  MAX_SESSION_USAGE_LIST_SIZE,
@@ -6863,10 +7403,10 @@ async function exportSessionUsage(params = {}) {
6863
7403
 
6864
7404
  function listSessionPaths(params = {}) {
6865
7405
  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') {
7406
+ if (source && !isSessionSourceOrAll(source)) {
6867
7407
  return [];
6868
7408
  }
6869
- const validSource = source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi' ? source : 'all';
7409
+ const validSource = normalizeSessionSourceName(source, '') || (source === 'all' ? 'all' : 'all');
6870
7410
  const rawLimit = Number(params.limit);
6871
7411
  const limit = Number.isFinite(rawLimit)
6872
7412
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_PATH_LIST_SIZE))
@@ -6900,9 +7440,15 @@ function listSessionPaths(params = {}) {
6900
7440
  if (validSource === 'all' || validSource === 'codebuddy') {
6901
7441
  sessions = sessions.concat(listSessionInventoryBySource('codebuddy', gatherLimit, scanOptions, { forceRefresh }));
6902
7442
  }
7443
+ if (validSource === 'all' || validSource === 'workbuddy') {
7444
+ sessions = sessions.concat(listSessionInventoryBySource('workbuddy', gatherLimit, scanOptions, { forceRefresh }));
7445
+ }
6903
7446
  if (validSource === 'all' || validSource === 'pi') {
6904
7447
  sessions = sessions.concat(listSessionInventoryBySource('pi', gatherLimit, scanOptions, { forceRefresh }));
6905
7448
  }
7449
+ if (validSource === 'all' || validSource === 'opencode') {
7450
+ sessions = sessions.concat(listSessionInventoryBySource('opencode', gatherLimit, scanOptions, { forceRefresh }));
7451
+ }
6906
7452
 
6907
7453
  const dedupedPaths = [];
6908
7454
  const seen = new Set();
@@ -6928,9 +7474,13 @@ function listSessionPaths(params = {}) {
6928
7474
  }
6929
7475
 
6930
7476
  function resolveSessionFilePath(source, filePath, sessionId) {
6931
- const normalizedSource = source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi'
6932
- ? source
6933
- : 'codex';
7477
+ const normalizedSource = normalizeSessionSourceName(source, 'codex');
7478
+ if (normalizedSource === 'opencode') {
7479
+ const id = typeof sessionId === 'string' && sessionId.trim()
7480
+ ? sessionId.trim()
7481
+ : parseOpencodeVirtualSessionId(filePath);
7482
+ return id && parseOpencodeSessionSummary(id) ? buildOpencodeVirtualSessionPath(id) : '';
7483
+ }
6934
7484
  const homeDir = process && process.env && process.env.HOME ? process.env.HOME : '';
6935
7485
  const derivedCodexDir = homeDir ? `${homeDir}/.codexmate/sessions/derived/codex` : '';
6936
7486
  const derivedClaudeDir = homeDir ? `${homeDir}/.codexmate/sessions/derived/claude` : '';
@@ -6940,9 +7490,11 @@ function resolveSessionFilePath(source, filePath, sessionId) {
6940
7490
  ? [getGeminiTmpDir()]
6941
7491
  : (normalizedSource === 'codebuddy'
6942
7492
  ? [getCodeBuddyProjectsDir()]
6943
- : (normalizedSource === 'pi'
6944
- ? [getPiSessionsDir()]
6945
- : [getCodexSessionsDir(), derivedCodexDir])));
7493
+ : (normalizedSource === 'workbuddy'
7494
+ ? [getWorkBuddyProjectsDir()]
7495
+ : (normalizedSource === 'pi'
7496
+ ? [getPiSessionsDir()]
7497
+ : [getCodexSessionsDir(), derivedCodexDir]))));
6946
7498
  const availableRoots = roots.filter((dirPath) => dirPath && fs.existsSync(dirPath));
6947
7499
  if (availableRoots.length === 0) {
6948
7500
  return '';
@@ -7488,23 +8040,11 @@ function moveFileSync(sourcePath, targetPath) {
7488
8040
 
7489
8041
  function buildSessionSummaryFallback(source, filePath, sessionId = '') {
7490
8042
  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')));
8043
+ const sourceLabel = getSessionSourceLabel(source);
7498
8044
  return {
7499
8045
  source,
7500
8046
  sourceLabel,
7501
- provider: source === 'claude'
7502
- ? 'claude'
7503
- : (source === 'gemini'
7504
- ? 'gemini'
7505
- : (source === 'codebuddy'
7506
- ? 'codebuddy'
7507
- : (source === 'pi' ? 'pi' : 'codex'))),
8047
+ provider: getSessionSourceProvider(source),
7508
8048
  sessionId: resolvedSessionId,
7509
8049
  title: resolvedSessionId,
7510
8050
  cwd: '',
@@ -7515,7 +8055,7 @@ function buildSessionSummaryFallback(source, filePath, sessionId = '') {
7515
8055
  contextWindow: 0,
7516
8056
  filePath,
7517
8057
  keywords: [],
7518
- capabilities: source === 'claude' || source === 'gemini' || source === 'codebuddy' ? { code: true } : {}
8058
+ capabilities: source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'workbuddy' || source === 'opencode' ? { code: true } : {}
7519
8059
  };
7520
8060
  }
7521
8061
 
@@ -7559,7 +8099,9 @@ function normalizeSessionTrashEntry(entry) {
7559
8099
  ? 'gemini'
7560
8100
  : (entry.source === 'codebuddy'
7561
8101
  ? 'codebuddy'
7562
- : (entry.source === 'pi' ? 'pi' : ''))));
8102
+ : (entry.source === 'workbuddy'
8103
+ ? 'workbuddy'
8104
+ : (entry.source === 'pi' ? 'pi' : '')))));
7563
8105
  const trashId = typeof entry.trashId === 'string' ? entry.trashId.trim() : '';
7564
8106
  if (!source || !trashId || trashId.includes('/') || trashId.includes('\\') || trashId.includes('\0')) {
7565
8107
  return null;
@@ -7580,7 +8122,9 @@ function normalizeSessionTrashEntry(entry) {
7580
8122
  ? 'Gemini CLI'
7581
8123
  : (source === 'codebuddy'
7582
8124
  ? 'CodeBuddy Code'
7583
- : (source === 'pi' ? 'Pi' : 'Codex'))),
8125
+ : (source === 'workbuddy'
8126
+ ? 'WorkBuddy AI'
8127
+ : (source === 'pi' ? 'Pi' : 'Codex')))),
7584
8128
  sessionId: sessionId || trashId,
7585
8129
  title: typeof entry.title === 'string' && entry.title.trim() ? entry.title.trim() : (sessionId || trashId),
7586
8130
  cwd: typeof entry.cwd === 'string' ? entry.cwd : '',
@@ -7596,7 +8140,7 @@ function normalizeSessionTrashEntry(entry) {
7596
8140
  originalFilePath: typeof entry.originalFilePath === 'string' ? entry.originalFilePath : '',
7597
8141
  provider: typeof entry.provider === 'string' && entry.provider.trim()
7598
8142
  ? entry.provider.trim()
7599
- : (source === 'claude' ? 'claude' : (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : (source === 'pi' ? 'pi' : 'codex')))),
8143
+ : (source === 'claude' ? 'claude' : (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : (source === 'workbuddy' ? 'workbuddy' : (source === 'pi' ? 'pi' : 'codex'))))),
7600
8144
  keywords: normalizeKeywords(entry.keywords),
7601
8145
  capabilities: normalizeCapabilities(entry.capabilities),
7602
8146
  claudeIndexPath: typeof entry.claudeIndexPath === 'string' ? entry.claudeIndexPath : '',
@@ -7689,7 +8233,9 @@ function buildSessionTrashEntry(summary, options = {}) {
7689
8233
  ? 'gemini'
7690
8234
  : (options.source === 'codebuddy'
7691
8235
  ? 'codebuddy'
7692
- : (options.source === 'pi' ? 'pi' : 'codex')));
8236
+ : (options.source === 'workbuddy'
8237
+ ? 'workbuddy'
8238
+ : (options.source === 'pi' ? 'pi' : 'codex'))));
7693
8239
  const sessionId = options.sessionId || summary.sessionId || path.basename(options.originalFilePath || summary.filePath || '', '.jsonl');
7694
8240
  const claudeIndexEntry = options.claudeIndexEntry && typeof options.claudeIndexEntry === 'object' && !Array.isArray(options.claudeIndexEntry)
7695
8241
  ? options.claudeIndexEntry
@@ -7703,7 +8249,9 @@ function buildSessionTrashEntry(summary, options = {}) {
7703
8249
  ? 'Gemini CLI'
7704
8250
  : (source === 'codebuddy'
7705
8251
  ? 'CodeBuddy Code'
7706
- : (source === 'pi' ? 'Pi' : 'Codex')));
8252
+ : (source === 'workbuddy'
8253
+ ? 'WorkBuddy AI'
8254
+ : (source === 'pi' ? 'Pi' : 'Codex'))));
7707
8255
  const fallbackTitle = truncateText(
7708
8256
  (claudeIndexEntry && (claudeIndexEntry.summary || claudeIndexEntry.firstPrompt)) || sessionId,
7709
8257
  120
@@ -7740,7 +8288,7 @@ function buildSessionTrashEntry(summary, options = {}) {
7740
8288
  originalFilePath: options.originalFilePath || summary.filePath || '',
7741
8289
  provider: (claudeIndexEntry && typeof claudeIndexEntry.provider === 'string' && claudeIndexEntry.provider.trim())
7742
8290
  ? claudeIndexEntry.provider.trim()
7743
- : (summary.provider || (source === 'claude' ? 'claude' : (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : (source === 'pi' ? 'pi' : 'codex'))))),
8291
+ : (summary.provider || (source === 'claude' ? 'claude' : (source === 'gemini' ? 'gemini' : (source === 'codebuddy' ? 'codebuddy' : (source === 'workbuddy' ? 'workbuddy' : (source === 'pi' ? 'pi' : 'codex')))))),
7744
8292
  keywords: normalizedClaudeKeywords.length > 0 ? normalizedClaudeKeywords : normalizedSummaryKeywords,
7745
8293
  capabilities: Object.keys(normalizedClaudeCapabilities).length > 0
7746
8294
  ? normalizedClaudeCapabilities
@@ -7761,7 +8309,9 @@ function resolveSessionRestoreTarget(entry) {
7761
8309
  ? getGeminiTmpDir()
7762
8310
  : (normalized.source === 'codebuddy'
7763
8311
  ? getCodeBuddyProjectsDir()
7764
- : (normalized.source === 'pi' ? getPiSessionsDir() : getCodexSessionsDir())));
8312
+ : (normalized.source === 'workbuddy'
8313
+ ? getWorkBuddyProjectsDir()
8314
+ : (normalized.source === 'pi' ? getPiSessionsDir() : getCodexSessionsDir()))));
7765
8315
  const originalFilePath = typeof normalized.originalFilePath === 'string' ? normalized.originalFilePath.trim() : '';
7766
8316
  if (!root || !originalFilePath) {
7767
8317
  return '';
@@ -7895,15 +8445,7 @@ function upsertClaudeSessionIndexEntry(indexPath, sessionFilePath, entry) {
7895
8445
  }
7896
8446
 
7897
8447
  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'))));
8448
+ const source = normalizeSessionSourceName(params.source, '') || 'all';
7907
8449
  const countOnly = params.countOnly === true;
7908
8450
  const rawLimit = Number(params.limit);
7909
8451
  const limit = Number.isFinite(rawLimit)
@@ -7913,7 +8455,7 @@ async function listSessionTrashItems(params = {}) {
7913
8455
  purgeExpiredSessionTrashEntries(params.retentionDays);
7914
8456
  }
7915
8457
  const allEntries = readSessionTrashEntries();
7916
- let items = source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi'
8458
+ let items = normalizeSessionSourceName(source, '')
7917
8459
  ? allEntries.filter((entry) => entry.source === source)
7918
8460
  : allEntries.slice();
7919
8461
  items.sort((a, b) => {
@@ -8093,18 +8635,13 @@ async function purgeSessionTrashItems(params = {}) {
8093
8635
  }
8094
8636
 
8095
8637
  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' : ''))));
8638
+ const source = normalizeSessionSourceName(params.source, '');
8105
8639
  if (!source) {
8106
8640
  return { error: 'Invalid source' };
8107
8641
  }
8642
+ if (source === 'opencode') {
8643
+ return { error: 'OpenCode DB-backed sessions cannot be deleted from CodexMate' };
8644
+ }
8108
8645
 
8109
8646
  const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
8110
8647
  if (!filePath) {
@@ -8117,7 +8654,9 @@ async function trashSessionData(params = {}) {
8117
8654
  ? parseGeminiSessionSummary(filePath)
8118
8655
  : (source === 'codebuddy'
8119
8656
  ? parseCodeBuddySessionSummary(filePath)
8120
- : (source === 'pi' ? parsePiSessionSummary(filePath) : parseCodexSessionSummary(filePath)))))
8657
+ : (source === 'workbuddy'
8658
+ ? parseWorkBuddySessionSummary(filePath)
8659
+ : (source === 'pi' ? parsePiSessionSummary(filePath) : parseCodexSessionSummary(filePath))))))
8121
8660
  || buildSessionSummaryFallback(source, filePath, params.sessionId);
8122
8661
  const exactMessageCount = await countConversationMessagesInFile(filePath, source);
8123
8662
  if (Number.isFinite(Number(exactMessageCount))) {
@@ -8208,18 +8747,13 @@ async function trashSessionData(params = {}) {
8208
8747
  }
8209
8748
 
8210
8749
  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' : ''))));
8750
+ const source = normalizeSessionSourceName(params.source, '');
8220
8751
  if (!source) {
8221
8752
  return { error: 'Invalid source' };
8222
8753
  }
8754
+ if (source === 'opencode') {
8755
+ return { error: 'OpenCode DB-backed sessions cannot be deleted from CodexMate' };
8756
+ }
8223
8757
 
8224
8758
  const filePath = resolveSessionFilePath(source, getSessionFileArg(params), params.sessionId);
8225
8759
  if (!filePath) {
@@ -8767,7 +9301,7 @@ function recordHasPiMessage(record) {
8767
9301
 
8768
9302
  function recordHasMessage(record, source) {
8769
9303
  if (source === 'codex') return recordHasCodexMessage(record);
8770
- if (source === 'codebuddy') return recordHasCodeBuddyMessage(record);
9304
+ if (source === 'codebuddy' || source === 'workbuddy') return recordHasCodeBuddyMessage(record);
8771
9305
  if (source === 'pi') return recordHasPiMessage(record);
8772
9306
  return recordHasClaudeMessage(record);
8773
9307
  }
@@ -8787,7 +9321,7 @@ function extractMessagesFromRecords(records, source, options = {}) {
8787
9321
  const record = records[lineIndex];
8788
9322
  if (source === 'codex') {
8789
9323
  extractCodexMessageFromRecord(record, state, lineIndex);
8790
- } else if (source === 'codebuddy') {
9324
+ } else if (source === 'codebuddy' || source === 'workbuddy') {
8791
9325
  extractCodeBuddyMessageFromRecord(record, state, lineIndex);
8792
9326
  } else if (source === 'pi') {
8793
9327
  extractPiMessageFromRecord(record, state, lineIndex);
@@ -8852,7 +9386,7 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
8852
9386
 
8853
9387
  if (source === 'codex') {
8854
9388
  extractCodexMessageFromRecord(record, state, currentLineIndex);
8855
- } else if (source === 'codebuddy') {
9389
+ } else if (source === 'codebuddy' || source === 'workbuddy') {
8856
9390
  extractCodeBuddyMessageFromRecord(record, state, currentLineIndex);
8857
9391
  } else if (source === 'pi') {
8858
9392
  extractPiMessageFromRecord(record, state, currentLineIndex);
@@ -8880,15 +9414,7 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
8880
9414
  }
8881
9415
 
8882
9416
  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' : ''))));
9417
+ const source = normalizeSessionSourceName(params.source, '');
8892
9418
  if (!source) {
8893
9419
  return { error: 'Invalid source' };
8894
9420
  }
@@ -8904,9 +9430,30 @@ async function readSessionDetail(params = {}) {
8904
9430
  ? Math.max(1, Math.min(rawLimit, MAX_SESSION_DETAIL_MESSAGES))
8905
9431
  : DEFAULT_SESSION_DETAIL_MESSAGES;
8906
9432
  const preview = params.preview === true || params.preview === 'true';
9433
+ const rawSessionQuery = typeof params.query === 'string' ? params.query.trim() : '';
9434
+ const sessionQueryTokens = rawSessionQuery
9435
+ ? expandSessionQueryTokens(normalizeQueryTokens(rawSessionQuery))
9436
+ : [];
9437
+ let sessionQueryMatch = null;
9438
+ if (sessionQueryTokens.length > 0) {
9439
+ sessionQueryMatch = await scanSessionContentForQuery(
9440
+ { source, filePath, sessionId: params.sessionId },
9441
+ sessionQueryTokens,
9442
+ {
9443
+ maxMatches: Number.MAX_SAFE_INTEGER,
9444
+ positionsLimit: SESSION_QUERY_MATCH_POSITIONS_LIMIT
9445
+ }
9446
+ );
9447
+ }
8907
9448
 
8908
9449
  let extracted;
8909
- if (source === 'gemini') {
9450
+ if (source === 'opencode') {
9451
+ const opencodeSessionId = params.sessionId || parseOpencodeVirtualSessionId(filePath);
9452
+ extracted = queryOpencodeSessionMessages(opencodeSessionId, { maxMessages: messageLimit });
9453
+ if (!extracted) {
9454
+ return { error: 'Failed to parse session file' };
9455
+ }
9456
+ } else if (source === 'gemini') {
8910
9457
  let json;
8911
9458
  try {
8912
9459
  json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
@@ -8946,13 +9493,7 @@ async function readSessionDetail(params = {}) {
8946
9493
  extracted = await extractSessionDetailPreviewFromFile(filePath, source, messageLimit, { preview });
8947
9494
  }
8948
9495
  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')));
9496
+ const sourceLabel = getSessionSourceLabel(source);
8956
9497
  const clippedMessages = Array.isArray(extracted.messages) ? extracted.messages : [];
8957
9498
  const hasExactTotalMessages = Number.isFinite(extracted.totalMessages);
8958
9499
  const startIndex = hasExactTotalMessages
@@ -8996,6 +9537,13 @@ async function readSessionDetail(params = {}) {
8996
9537
  ? extracted.clipped
8997
9538
  : (hasExactTotalMessages ? extracted.totalMessages > indexedMessages.length : false),
8998
9539
  messageLimit,
9540
+ ...(sessionQueryMatch ? {
9541
+ match: {
9542
+ hit: !!sessionQueryMatch.hit,
9543
+ count: Number.isFinite(sessionQueryMatch.count) ? sessionQueryMatch.count : 0,
9544
+ positions: Array.isArray(sessionQueryMatch.positions) ? sessionQueryMatch.positions : []
9545
+ }
9546
+ } : {}),
8999
9547
  messages: indexedMessages,
9000
9548
  filePath,
9001
9549
  ...(typeof buildSessionNativeStatus === 'function'
@@ -9013,15 +9561,7 @@ async function readSessionDetail(params = {}) {
9013
9561
  }
9014
9562
 
9015
9563
  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' : ''))));
9564
+ const source = normalizeSessionSourceName(params.source, '');
9025
9565
  if (!source) {
9026
9566
  return { error: 'Invalid source' };
9027
9567
  }
@@ -9041,7 +9581,13 @@ async function readSessionPlain(params = {}) {
9041
9581
  );
9042
9582
 
9043
9583
  let extracted;
9044
- if (source === 'gemini') {
9584
+ if (source === 'opencode') {
9585
+ const opencodeSessionId = params.sessionId || parseOpencodeVirtualSessionId(filePath);
9586
+ extracted = queryOpencodeSessionMessages(opencodeSessionId, { maxMessages });
9587
+ if (!extracted) {
9588
+ return { error: 'Failed to parse session file' };
9589
+ }
9590
+ } else if (source === 'gemini') {
9045
9591
  let json;
9046
9592
  try {
9047
9593
  json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
@@ -9091,13 +9637,7 @@ async function readSessionPlain(params = {}) {
9091
9637
  }
9092
9638
 
9093
9639
  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')));
9640
+ const sourceLabel = getSessionSourceLabel(source);
9101
9641
  const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
9102
9642
  const text = buildSessionPlainText(messages);
9103
9643
 
@@ -9113,15 +9653,7 @@ async function readSessionPlain(params = {}) {
9113
9653
  }
9114
9654
 
9115
9655
  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' : ''))));
9656
+ const source = normalizeSessionSourceName(params.source, '');
9125
9657
  if (!source) {
9126
9658
  return { error: 'Invalid source' };
9127
9659
  }
@@ -9133,7 +9665,13 @@ async function exportSessionData(params = {}) {
9133
9665
  }
9134
9666
 
9135
9667
  let extracted;
9136
- if (source === 'gemini') {
9668
+ if (source === 'opencode') {
9669
+ const opencodeSessionId = params.sessionId || parseOpencodeVirtualSessionId(filePath);
9670
+ extracted = queryOpencodeSessionMessages(opencodeSessionId, { maxMessages });
9671
+ if (!extracted) {
9672
+ return { error: 'Failed to parse session file' };
9673
+ }
9674
+ } else if (source === 'gemini') {
9137
9675
  let json;
9138
9676
  try {
9139
9677
  json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
@@ -9191,13 +9729,7 @@ async function exportSessionData(params = {}) {
9191
9729
 
9192
9730
  const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, source === 'gemini' ? '.json' : '.jsonl');
9193
9731
  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')));
9732
+ const sourceLabel = getSessionSourceLabel(source);
9201
9733
  const truncated = !!extracted.truncated;
9202
9734
  const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
9203
9735
  const markdown = buildSessionMarkdown({
@@ -11085,7 +11617,8 @@ function runKilocodeCommand(args = [], options = {}) {
11085
11617
  const child = spawn(bin, finalArgs, {
11086
11618
  detached: true,
11087
11619
  stdio: 'ignore',
11088
- windowsHide: true
11620
+ windowsHide: true,
11621
+ shell: process.platform === 'win32'
11089
11622
  });
11090
11623
  child.unref();
11091
11624
  return { success: true, bin, args: finalArgs, pid: child.pid };
@@ -11093,7 +11626,8 @@ function runKilocodeCommand(args = [], options = {}) {
11093
11626
  return new Promise((resolve, reject) => {
11094
11627
  const child = spawn(bin, finalArgs, {
11095
11628
  stdio: 'inherit',
11096
- windowsHide: false
11629
+ windowsHide: false,
11630
+ shell: process.platform === 'win32'
11097
11631
  });
11098
11632
  child.on('error', reject);
11099
11633
  child.on('exit', (code, signal) => {
@@ -12382,7 +12916,7 @@ function resolveExportOutputPath(outputPath, defaultFileName) {
12382
12916
  }
12383
12917
 
12384
12918
  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>]');
12919
+ 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
12920
  console.log('\n示例:');
12387
12921
  console.log(' codexmate export-session --source codex --session-id 123456');
12388
12922
  console.log(' codexmate export-session --source claude --file "~/.claude/projects/demo/session.jsonl"');
@@ -12454,8 +12988,8 @@ function parseExportSessionArgs(args = []) {
12454
12988
  }
12455
12989
 
12456
12990
  const normalizedSource = options.source.trim().toLowerCase();
12457
- if (normalizedSource && normalizedSource !== 'codex' && normalizedSource !== 'claude' && normalizedSource !== 'pi') {
12458
- errors.push('参数 --source 仅支持 codex、claude 或 pi');
12991
+ if (normalizedSource && !isSessionSourceOrAll(normalizedSource)) {
12992
+ errors.push('参数 --source 仅支持 codex、claude、gemini、codebuddy、workbuddy、pi 或 opencode');
12459
12993
  }
12460
12994
  options.source = normalizedSource;
12461
12995
 
@@ -12528,7 +13062,7 @@ async function cmdExportSession(args = []) {
12528
13062
 
12529
13063
  function printAnalyticsUsage() {
12530
13064
  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|->]');
13065
+ 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
13066
  console.log('');
12533
13067
  }
12534
13068
 
@@ -13883,8 +14417,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13883
14417
  case 'list-sessions':
13884
14418
  {
13885
14419
  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' };
14420
+ if (source && !isSessionSourceOrAll(source)) {
14421
+ result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' };
13888
14422
  } else {
13889
14423
  result = {
13890
14424
  sessions: await listSessionBrowse(params),
@@ -13897,8 +14431,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13897
14431
  {
13898
14432
  const usageParams = isPlainObject(params) ? params : {};
13899
14433
  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' };
14434
+ if (source && !isSessionSourceOrAll(source)) {
14435
+ result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' };
13902
14436
  } else {
13903
14437
  result = {
13904
14438
  sessions: await listSessionUsage({
@@ -13914,8 +14448,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13914
14448
  {
13915
14449
  const usageParams = isPlainObject(params) ? params : {};
13916
14450
  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' };
14451
+ if (source && !isSessionSourceOrAll(source)) {
14452
+ result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' };
13919
14453
  } else {
13920
14454
  result = await exportSessionUsage({
13921
14455
  ...usageParams,
@@ -13927,8 +14461,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13927
14461
  case 'list-session-paths':
13928
14462
  {
13929
14463
  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' };
14464
+ if (source && !isSessionSourceOrAll(source)) {
14465
+ result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' };
13932
14466
  } else {
13933
14467
  result = {
13934
14468
  paths: listSessionPaths(params)
@@ -15439,7 +15973,7 @@ function buildMcpClaudeSettingsPayload() {
15439
15973
  function normalizeMcpSource(value) {
15440
15974
  const source = typeof value === 'string' ? value.trim().toLowerCase() : '';
15441
15975
  if (!source) return '';
15442
- if (source === 'codex' || source === 'claude' || source === 'gemini' || source === 'codebuddy' || source === 'pi' || source === 'all') {
15976
+ if (normalizeSessionSourceName(source, '') || source === 'all') {
15443
15977
  return source;
15444
15978
  }
15445
15979
  return null;
@@ -15752,7 +16286,7 @@ function createWorkflowToolCatalog() {
15752
16286
  handler: async (args = {}) => {
15753
16287
  const source = normalizeMcpSource(args.source);
15754
16288
  if (source === null) {
15755
- return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, pi, or all' };
16289
+ return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' };
15756
16290
  }
15757
16291
  return {
15758
16292
  source: source || 'all',
@@ -16118,7 +16652,7 @@ function createMcpTools(options = {}) {
16118
16652
  const input = args && typeof args === 'object' ? args : {};
16119
16653
  const source = normalizeMcpSource(input.source);
16120
16654
  if (source === null) {
16121
- return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, pi, or all' };
16655
+ return { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' };
16122
16656
  }
16123
16657
  const normalizedInput = {
16124
16658
  ...input,
@@ -16578,7 +17112,7 @@ function createMcpResources() {
16578
17112
  contents: [{
16579
17113
  uri,
16580
17114
  mimeType: 'application/json',
16581
- text: JSON.stringify({ error: 'Invalid source. Must be codex, claude, gemini, codebuddy, pi, or all' }, null, 2)
17115
+ text: JSON.stringify({ error: 'Invalid source. Must be codex, claude, gemini, codebuddy, workbuddy, pi, opencode, or all' }, null, 2)
16582
17116
  }]
16583
17117
  };
16584
17118
  }
@@ -16827,7 +17361,7 @@ function printMainHelp() {
16827
17361
  console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错');
16828
17362
  console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
16829
17363
  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>]');
17364
+ 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
17365
  console.log(' codexmate convert-session --from <codex|claude> --to <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
16832
17366
  console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
16833
17367
  console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');