coding-tool-x 3.8.0 → 3.8.2

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 (53) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +4 -6
  3. package/dist/web/assets/{Analytics-8dRv1aYW.js → Analytics-COEUXmd4.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-BQRXSICO.js → ConfigTemplates-DLLkS53H.js} +1 -1
  5. package/dist/web/assets/{Home-BtzWhKIa.js → Home-DCEyLzwX.js} +1 -1
  6. package/dist/web/assets/{PluginManager-BTa-Ygf6.js → PluginManager-Oe1ZHoMO.js} +1 -1
  7. package/dist/web/assets/{ProjectList-F_cB8ldA.js → ProjectList-CWjuAFof.js} +1 -1
  8. package/dist/web/assets/{SessionList-LOrbPQvu.js → SessionList-v3ehGg9U.js} +1 -1
  9. package/dist/web/assets/{SkillManager-BzI_t-yV.js → SkillManager-DUF_qP5S.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-Db9RGiz7.js → WorkspaceManager-CjAWBosO.js} +1 -1
  11. package/dist/web/assets/index-D5hyHPj7.js +2 -0
  12. package/dist/web/index.html +1 -1
  13. package/docs/superpowers/plans/2026-08-10-codex-conversion-compatibility.md +251 -0
  14. package/docs/superpowers/plans/2026-08-11-windows-command-window-hiding.md +90 -0
  15. package/docs/superpowers/specs/2026-08-10-codex-conversion-compatibility-design.md +61 -0
  16. package/docs/superpowers/specs/2026-08-11-windows-command-window-hiding-design.md +44 -0
  17. package/package.json +2 -2
  18. package/src/commands/update.js +1 -1
  19. package/src/config/paths.js +1 -0
  20. package/src/server/api/codex-sessions.js +14 -14
  21. package/src/server/api/gemini-sessions.js +52 -14
  22. package/src/server/api/omp-projects.js +2 -2
  23. package/src/server/api/omp-sessions.js +61 -23
  24. package/src/server/api/sessions.js +44 -2
  25. package/src/server/opencode-proxy-server.js +55 -1037
  26. package/src/server/services/claude-wire.js +694 -0
  27. package/src/server/services/codex-sessions.js +41 -233
  28. package/src/server/services/codex-wire.js +219 -0
  29. package/src/server/services/dashboard-snapshot-worker.js +5 -5
  30. package/src/server/services/gemini-sessions.js +67 -157
  31. package/src/server/services/gemini-wire.js +759 -0
  32. package/src/server/services/model-detector.js +49 -131
  33. package/src/server/services/omp-channels.js +23 -4
  34. package/src/server/services/omp-gateway.js +29 -1
  35. package/src/server/services/omp-sessions.js +102 -105
  36. package/src/server/services/omp-settings-manager.js +27 -3
  37. package/src/server/services/opencode-gateway-adapters.js +22 -1147
  38. package/src/server/services/opencode-normalization.js +546 -0
  39. package/src/server/services/opencode-sessions.js +339 -546
  40. package/src/server/services/session-converter.js +4 -4
  41. package/src/server/services/session-history-adapters/claude.js +235 -0
  42. package/src/server/services/session-history-adapters/codex.js +158 -0
  43. package/src/server/services/session-history-adapters/gemini.js +292 -0
  44. package/src/server/services/session-history-adapters/index.js +20 -0
  45. package/src/server/services/session-history-adapters/omp.js +256 -0
  46. package/src/server/services/session-history-index.js +930 -0
  47. package/src/server/services/session-history-worker.js +99 -0
  48. package/src/server/services/session-snapshot-worker.js +7 -7
  49. package/src/server/services/sessions.js +167 -775
  50. package/src/server/services/speed-test.js +85 -303
  51. package/src/server/services/sqlite-connection.js +78 -0
  52. package/src/server/services/workspace-service.js +3 -3
  53. package/dist/web/assets/index-DIWJmyL6.js +0 -2
@@ -11,6 +11,13 @@ const { URL } = require('url');
11
11
  const crypto = require('crypto');
12
12
  const zlib = require('zlib');
13
13
  const { loadConfig } = require('../../config/loader');
14
+ const { createCodexRequest, buildCodexTargetUrl } = require('./codex-wire');
15
+ const { createClaudeRequest, buildClaudeTargetUrl } = require('./claude-wire');
16
+ const {
17
+ createGeminiRequest,
18
+ buildGeminiTargetUrl,
19
+ shouldUseGeminiCliFormat
20
+ } = require('./gemini-wire');
14
21
  const { PATHS } = require('../../config/paths');
15
22
 
16
23
  // 内置模型优先级(当配置缺失时兜底)
@@ -236,7 +243,6 @@ const TEST_TIMEOUT_MS = 10000; // 10 seconds per model test
236
243
  const MODEL_LIST_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
237
244
  const MODEL_LIST_FAILURE_TTL_MS = 5 * 60 * 1000;
238
245
  const MODEL_LIST_AUTH_FAILURE_TTL_MS = 60 * 60 * 1000;
239
- const CLAUDE_CODE_BETA_HEADER = 'claude-code-20250219,interleaved-thinking-2025-05-14';
240
246
 
241
247
  const MODEL_UNAVAILABLE_HINTS = [
242
248
  'not found',
@@ -347,170 +353,82 @@ function buildOpenAiCompatibleUrl(baseUrl, endpoint) {
347
353
  return `${trimmed}${normalizedEndpoint}`;
348
354
  }
349
355
 
350
- function buildClaudeMessagesUrl(baseUrl, options = {}) {
351
- const withBeta = options.withBeta !== false;
352
- const parsed = new URL(String(baseUrl || '').trim());
353
- let pathname = parsed.pathname.replace(/\/+$/, '');
354
-
355
- if (!pathname || pathname === '/') {
356
- pathname = '/v1/messages';
357
- } else if (pathname.endsWith('/messages')) {
358
- // noop
359
- } else if (pathname.endsWith('/v1')) {
360
- pathname = `${pathname}/messages`;
361
- } else {
362
- pathname = `${pathname}/v1/messages`;
363
- }
364
-
365
- parsed.pathname = pathname;
366
- if (withBeta) {
367
- parsed.searchParams.set('beta', 'true');
368
- }
369
- return parsed.toString();
370
- }
371
-
372
- function buildGeminiGenerateContentUrl(baseUrl, model, apiKey = '') {
373
- const modelName = String(model || '').trim();
374
- if (!modelName) {
375
- throw new Error('Model is required for Gemini probe');
376
- }
377
-
378
- const parsed = new URL(String(baseUrl || '').trim());
379
- let pathname = parsed.pathname.replace(/\/+$/, '');
380
- const modelsIndex = pathname.indexOf('/models');
381
- if (modelsIndex >= 0) {
382
- pathname = pathname.slice(0, modelsIndex);
383
- }
384
-
385
- let apiBasePath;
386
- if (!pathname || pathname === '/') {
387
- apiBasePath = '/v1beta';
388
- } else if (pathname.endsWith('/v1beta') || pathname.endsWith('/v1')) {
389
- apiBasePath = pathname;
390
- } else {
391
- apiBasePath = `${pathname}/v1beta`;
392
- }
393
356
 
394
- parsed.pathname = `${apiBasePath}/models/${encodeURIComponent(modelName)}:generateContent`;
395
- if (apiKey) {
396
- parsed.searchParams.set('key', apiKey);
397
- }
398
- return parsed.toString();
357
+ function resolveExplicitGeminiUseCli(channel) {
358
+ const providerApi = String(channel?.providerApi || channel?.wireApi || '').trim().toLowerCase();
359
+ if (providerApi === 'google-gemini-cli') return true;
360
+ if (providerApi === 'google-generative-ai' || providerApi === 'google-vertex') return false;
361
+ return shouldUseGeminiCliFormat(channel?.baseUrl || '');
399
362
  }
400
363
 
401
- function buildClaudeProbePayload(model, options = {}) {
402
- const includeSystem = options.includeSystem === true;
403
- const systemAsArray = options.systemAsArray === true;
404
- const includeMetadata = options.includeMetadata === true;
405
- const sessionId = Math.random().toString(36).substring(2, 15);
406
-
407
- const payload = {
364
+ function createClaudeProbeAttempts(channel, model) {
365
+ const apiKey = channel.apiKey || '';
366
+ const minimalPayload = {
408
367
  model,
409
368
  max_tokens: 1,
410
369
  stream: false,
411
370
  messages: [{ role: 'user', content: [{ type: 'text', text: 'ping' }] }]
412
371
  };
413
-
414
- if (includeSystem) {
415
- const systemPrompt = "You are Claude Code, Anthropic's official CLI for Claude.";
416
- payload.system = systemAsArray
417
- ? [{ type: 'text', text: systemPrompt }]
418
- : systemPrompt;
419
- }
420
-
421
- if (includeMetadata) {
422
- payload.metadata = {
423
- user_id: `user_0000000000000000000000000000000000000000000000000000000000000000_account__session_${sessionId}`
424
- };
425
- }
426
-
427
- return JSON.stringify(payload);
428
- }
429
-
430
- function createClaudeProbeAttempts(channel, model) {
431
- const apiKey = channel.apiKey || '';
432
- const commonHeaders = {
433
- ...buildRequestHeaders('claude', channel),
434
- 'Content-Type': 'application/json',
435
- 'x-api-key': apiKey,
436
- 'Authorization': `Bearer ${apiKey}`,
437
- 'anthropic-version': '2023-06-01',
438
- 'anthropic-beta': CLAUDE_CODE_BETA_HEADER,
439
- 'Accept-Encoding': 'gzip, deflate, br',
440
- 'User-Agent': 'claude-cli/2.0.53 (external, cli)'
441
- };
442
-
443
- const legacyBody = buildClaudeProbePayload(model, {
444
- includeSystem: true,
445
- systemAsArray: true,
446
- includeMetadata: true
372
+ const converted = createClaudeRequest('/v1/messages', minimalPayload, {
373
+ apiKey,
374
+ baseUrl: channel.baseUrl,
375
+ fallbackModel: model,
376
+ stream: false
447
377
  });
448
378
 
449
379
  return [
450
380
  {
451
- label: 'claude-code-legacy-beta',
452
- url: buildClaudeMessagesUrl(channel.baseUrl, { withBeta: true }),
453
- body: legacyBody,
454
- headers: {
455
- ...commonHeaders,
456
- 'anthropic-dangerous-direct-browser-access': 'true',
457
- 'x-app': 'cli',
458
- 'x-stainless-lang': 'js',
459
- 'x-stainless-runtime': 'node'
460
- }
381
+ label: 'claude-messages',
382
+ url: buildClaudeTargetUrl(channel.baseUrl),
383
+ body: JSON.stringify(converted.body),
384
+ headers: converted.headers
461
385
  }
462
386
  ];
463
387
  }
464
-
465
388
  function createCodexProbeAttempts(channel, model) {
466
389
  const apiKey = channel.apiKey || '';
467
- const commonHeaders = {
468
- ...buildRequestHeaders('codex', channel),
469
- 'Content-Type': 'application/json',
470
- 'Authorization': `Bearer ${apiKey}`,
471
- 'User-Agent': 'codex_cli_rs/0.65.0'
472
- };
473
-
474
- const responsesBody = JSON.stringify({
390
+ const sessionId = crypto.randomUUID();
391
+ const converted = createCodexRequest({
475
392
  model,
476
393
  instructions: 'You are Codex.',
477
394
  input: [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'ping' }] }],
478
- max_output_tokens: 1,
479
- stream: false,
480
- store: false
395
+ prompt_cache_key: sessionId
396
+ }, {
397
+ apiKey,
398
+ sessionId
481
399
  });
482
400
 
483
401
  return [
484
402
  {
485
403
  label: 'codex-responses',
486
- url: buildOpenAiCompatibleUrl(channel.baseUrl, '/v1/responses'),
487
- body: responsesBody,
488
- headers: {
489
- ...commonHeaders,
490
- 'openai-beta': 'responses=experimental'
491
- }
404
+ url: buildCodexTargetUrl(channel.baseUrl),
405
+ body: JSON.stringify(converted.body),
406
+ headers: converted.headers
492
407
  }
493
408
  ];
494
409
  }
495
410
 
496
411
  function createGeminiProbeAttempt(channel, model) {
497
412
  const apiKey = channel.apiKey || '';
498
- const body = JSON.stringify({
499
- contents: [{ role: 'user', parts: [{ text: 'test' }] }],
500
- generationConfig: { maxOutputTokens: 1, temperature: 0 }
413
+ const useCli = resolveExplicitGeminiUseCli(channel);
414
+ const minimalPayload = {
415
+ model,
416
+ max_output_tokens: 1,
417
+ temperature: 0,
418
+ input: [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'ping' }] }]
419
+ };
420
+ const converted = createGeminiRequest('/v1/responses', minimalPayload, {
421
+ apiKey,
422
+ fallbackModel: model,
423
+ stream: false,
424
+ useCli
501
425
  });
502
426
 
503
427
  return {
504
- label: 'gemini-generate-content',
505
- url: buildGeminiGenerateContentUrl(channel.baseUrl, model, apiKey),
506
- body,
507
- headers: {
508
- ...buildRequestHeaders('gemini', channel),
509
- 'Content-Type': 'application/json',
510
- 'Authorization': `Bearer ${apiKey}`,
511
- 'x-goog-api-key': apiKey,
512
- 'User-Agent': 'google-genai-sdk/0.8.0'
513
- }
428
+ label: useCli ? 'gemini-cli-generate-content' : 'gemini-generate-content',
429
+ url: buildGeminiTargetUrl(channel.baseUrl, converted.model, apiKey, { stream: false, useCli }),
430
+ body: JSON.stringify(converted.body),
431
+ headers: converted.headers
514
432
  };
515
433
  }
516
434
 
@@ -172,7 +172,10 @@ class OmpChannelService extends BaseChannelService {
172
172
  _applyDefaults(channel) {
173
173
  const normalized = super._applyDefaults(channel);
174
174
  normalized.providerKey = normalized.providerKey || normalized.provider || normalized.name || normalized.id;
175
- normalized.providerApi = normalized.providerApi || normalized.api || normalized.wireApi || 'openai-completions';
175
+ normalized.providerApi = normalizeProviderApi(
176
+ normalized.providerApi || normalized.api || normalized.wireApi || 'openai-completions',
177
+ { gatewaySourceType: normalized.gatewaySourceType }
178
+ );
176
179
  normalized.authMode = ['api_key', 'oauth', 'none'].includes(normalized.authMode)
177
180
  ? normalized.authMode
178
181
  : 'api_key';
@@ -605,19 +608,35 @@ function buildOmpSyncCandidate(modelsConfig, selection, channels) {
605
608
  };
606
609
  }
607
610
 
611
+ const rawProviderApi = sourceProvider.api || provider.api || 'openai-completions';
612
+ const apiVal = String(rawProviderApi).trim();
613
+ let gatewaySourceType;
614
+ if (existing?.gatewaySourceType) {
615
+ gatewaySourceType = existing.gatewaySourceType;
616
+ } else if (apiVal === 'anthropic-messages') {
617
+ gatewaySourceType = 'claude';
618
+ } else if (apiVal === 'google-generative-ai' || apiVal === 'google-gemini-cli' || apiVal === 'google-vertex') {
619
+ gatewaySourceType = 'gemini';
620
+ } else if (apiVal === 'openai-codex-responses') {
621
+ gatewaySourceType = 'codex';
622
+ } else {
623
+ gatewaySourceType = 'openai_compatible';
624
+ }
625
+ const providerApi = normalizeProviderApi(rawProviderApi, { gatewaySourceType });
626
+
608
627
  return {
609
628
  name: existing?.name || originalProviderId || providerId,
610
629
  providerKey: originalProviderId || providerId,
611
630
  baseUrl,
612
631
  apiKey: authMode === 'none' ? '' : apiKey,
613
- providerApi: normalizeProviderApi(sourceProvider.api || provider.api),
614
- wireApi: sourceProvider.api || provider.api || 'openai-completions',
632
+ providerApi,
633
+ wireApi: providerApi,
615
634
  authMode,
616
635
  oauthProviderId: authMode === 'oauth' ? (existing?.oauthProviderId || originalProviderId || providerId) : '',
617
636
  model: preferredModel,
618
637
  allowedModels,
619
638
  models: filterOmpProviderModels(provider.models, allowedModels),
620
- gatewaySourceType: existing?.gatewaySourceType || 'openai_compatible',
639
+ gatewaySourceType,
621
640
  credentialSource: credential.value ? credential.source : 'existing-channel'
622
641
  };
623
642
  }
@@ -58,6 +58,34 @@ function buildUpstreamRequestPath(baseUrl, requestUrl) {
58
58
  return `${pathname.startsWith('/') ? pathname : `/${pathname}`}${query ? `?${query}` : ''}`;
59
59
  }
60
60
 
61
+ function rewriteCodexResponsesRequestPath(baseUrl, requestUrl, providerApi = '') {
62
+ if (providerApi !== 'openai-codex-responses') return buildUpstreamRequestPath(baseUrl, requestUrl);
63
+
64
+ const base = new URL(baseUrl);
65
+ const request = new URL(requestUrl, 'http://127.0.0.1');
66
+ const requestedPath = request.pathname.replace(/\/+$/, '') || '/';
67
+ if (!requestedPath.endsWith('/codex/responses')) {
68
+ return buildUpstreamRequestPath(baseUrl, requestUrl);
69
+ }
70
+
71
+ const basePath = base.pathname === '/' ? '' : base.pathname.replace(/\/+$/, '');
72
+ let pathname;
73
+ if (!basePath) {
74
+ pathname = '/codex/responses';
75
+ } else if (basePath.endsWith('/responses')) {
76
+ pathname = basePath;
77
+ } else if (basePath.endsWith('/v1') || basePath.endsWith('/codex')) {
78
+ pathname = `${basePath}/responses`;
79
+ } else {
80
+ pathname = `${basePath}/codex/responses`;
81
+ }
82
+
83
+ const search = new URLSearchParams(base.searchParams);
84
+ request.searchParams.forEach((value, name) => search.set(name, value));
85
+ const query = search.toString();
86
+ return `${pathname.startsWith('/') ? pathname : `/${pathname}`}${query ? `?${query}` : ''}`;
87
+ }
88
+
61
89
  function rewriteCredential(headers, requestUrl, route, channel, options = {}) {
62
90
  const next = { ...headers };
63
91
  delete next.host;
@@ -428,7 +456,7 @@ function createOmpGateway(options = {}) {
428
456
  return;
429
457
  }
430
458
 
431
- const upstreamPath = buildUpstreamRequestPath(channel.baseUrl, route.upstreamPath);
459
+ const upstreamPath = rewriteCodexResponsesRequestPath(channel.baseUrl, route.upstreamPath, route.providerApi);
432
460
  const rewritten = rewriteCredential(req.headers, upstreamPath, route, channel);
433
461
  const transport = upstream.protocol === 'https:' ? https : http;
434
462
  let upstreamResponse = null;
@@ -1,6 +1,13 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { PATHS, HOME_DIR } = require('../../config/paths');
4
+ const {
5
+ listProjects: idxListProjects,
6
+ listSessions: idxListSessions,
7
+ getRecentSessions: idxGetRecent,
8
+ searchSessions: idxSearch,
9
+ getSessionStatus: idxGetSessionStatus,
10
+ } = require('./session-history-index');
4
11
  const { getOmpCommand, getOmpPaths, resolveOmpRuntime } = require('./omp-config');
5
12
 
6
13
  const PROJECT_ORDER_FILE = PATHS.ompProjectOrder;
@@ -286,18 +293,16 @@ function scanSessionFiles(rootDir = getOmpSessionPaths().sessions) {
286
293
  return files;
287
294
  }
288
295
 
289
- function getAllSessions() {
290
- return scanSessionFiles()
291
- .map((filePath) => {
292
- try {
293
- return parseSessionFile(filePath);
294
- } catch (error) {
295
- console.warn('[OMP Sessions] Failed to parse session:', filePath, error.message);
296
- return null;
297
- }
298
- })
299
- .filter(Boolean)
300
- .sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0));
296
+ async function getAllSessions(options = {}) {
297
+ const projects = await idxListProjects('omp', options);
298
+ const groups = await Promise.all(projects.map(project => idxListSessions('omp', project.name, options)));
299
+ return groups.flat().map(session => normalizeSession({
300
+ ...session,
301
+ mtime: new Date(session.mtime).toISOString(),
302
+ mtimeMs: session.mtime,
303
+ directory: session.projectFullPath,
304
+ usage: session.tokens
305
+ }));
301
306
  }
302
307
 
303
308
  function getOmpUsageEvents(rootDir = getOmpSessionPaths().sessions) {
@@ -350,32 +355,20 @@ function loadSessionOrder() {
350
355
  return safeReadJson(SESSION_ORDER_FILE, {});
351
356
  }
352
357
 
353
- function getProjects(_options = {}) {
354
- const sessions = getAllSessions();
355
- const projectMap = new Map();
356
- sessions.forEach((session) => {
357
- const key = session.projectName;
358
- const existing = projectMap.get(key) || {
359
- name: key,
360
- path: session.cwd,
361
- fullPath: session.cwd,
362
- displayName: getDisplayName(session.cwd),
363
- sessionCount: 0,
364
- latestSession: null,
365
- mtime: session.mtime,
366
- mtimeMs: 0,
367
- source: 'omp'
368
- };
369
- existing.sessionCount += 1;
370
- if ((session.mtimeMs || 0) > (existing.mtimeMs || 0)) {
371
- existing.latestSession = session.sessionId;
372
- existing.mtime = session.mtime;
373
- existing.mtimeMs = session.mtimeMs;
374
- }
375
- projectMap.set(key, existing);
376
- });
358
+ async function getProjects(_options = {}) {
359
+ const idxProjects = await idxListProjects('omp');
360
+ const projects = idxProjects.map(p => ({
361
+ name: p.name,
362
+ path: p.fullPath || p.path || '',
363
+ fullPath: p.fullPath || '',
364
+ displayName: p.displayName || p.name,
365
+ sessionCount: p.sessionCount || 0,
366
+ latestSession: p.latestSession || null,
367
+ mtime: p.lastUsed || null,
368
+ mtimeMs: p.lastUsed ? new Date(p.lastUsed).getTime() : 0,
369
+ source: 'omp'
370
+ }));
377
371
 
378
- const projects = Array.from(projectMap.values());
379
372
  const order = loadProjectOrder();
380
373
  projects.sort((a, b) => {
381
374
  const ai = order.indexOf(a.name);
@@ -390,10 +383,26 @@ function getProjects(_options = {}) {
390
383
  return projects;
391
384
  }
392
385
 
393
- function getSessionsByProject(projectName, _options = {}) {
386
+ async function getSessionsByProject(projectName, _options = {}) {
387
+ const idxSessions = await idxListSessions('omp', projectName);
388
+ const sessions = idxSessions.map(s => ({
389
+ sessionId: s.sessionId,
390
+ filePath: s.filePath,
391
+ firstMessage: s.firstMessage,
392
+ gitBranch: s.gitBranch,
393
+ provider: s.provider,
394
+ model: s.model,
395
+ size: s.size,
396
+ mtime: new Date(s.mtime).toISOString(),
397
+ mtimeMs: s.mtime,
398
+ directory: s.projectFullPath,
399
+ cwd: s.projectFullPath,
400
+ messageCount: s.messageCount,
401
+ usage: s.tokens
402
+ }));
403
+
394
404
  const orderMap = loadSessionOrder();
395
405
  const order = Array.isArray(orderMap[projectName]) ? orderMap[projectName] : [];
396
- const sessions = getAllSessions().filter(session => session.projectName === projectName);
397
406
  sessions.sort((a, b) => {
398
407
  const ai = order.indexOf(a.sessionId);
399
408
  const bi = order.indexOf(b.sessionId);
@@ -427,83 +436,71 @@ function normalizeSession(session) {
427
436
  };
428
437
  }
429
438
 
430
- function getSessionById(sessionId) {
431
- return getAllSessions().find(session => session.sessionId === sessionId || path.basename(session.filePath, '.jsonl') === sessionId) || null;
439
+ async function getSessionById(sessionId) {
440
+ const status = await idxGetSessionStatus('omp', sessionId);
441
+ if (!status) return null;
442
+ return {
443
+ sessionId: status.sessionId,
444
+ filePath: status.filePath,
445
+ size: status.size,
446
+ mtime: new Date(status.lastModified).toISOString(),
447
+ mtimeMs: status.lastModified,
448
+ source: 'omp',
449
+ firstMessage: null,
450
+ gitBranch: null,
451
+ forkedFrom: null,
452
+ provider: '',
453
+ model: '',
454
+ directory: null,
455
+ messageCount: 0,
456
+ tokens: { input: 0, output: 0, cached: 0, reasoning: 0, total: 0 }
457
+ };
432
458
  }
433
-
434
- function getSessionMessages(sessionId) {
435
- const session = getSessionById(sessionId);
459
+ async function getSessionMessages(sessionId) {
460
+ const session = await getSessionById(sessionId);
436
461
  if (!session) {
437
462
  throw new Error('Session not found');
438
463
  }
439
464
  return readJsonLines(session.filePath).map(convertOmpEntry).filter(Boolean);
440
465
  }
441
466
 
442
- function getRecentSessions(limit = 5) {
443
- return getAllSessions().slice(0, limit).map(normalizeSession);
444
- }
445
-
446
- function searchSessions(keyword, contextLength = 35, projectName = null) {
447
- const needle = String(keyword || '').toLowerCase();
448
- if (!needle) return [];
449
-
450
- return getAllSessions()
451
- .filter(session => !projectName || session.projectName === projectName)
452
- .map((session) => {
453
- const messages = readJsonLines(session.filePath).map(convertOmpEntry).filter(Boolean);
454
- const matches = [];
455
- messages.forEach((message) => {
456
- const text = String(message.content || '');
457
- const index = text.toLowerCase().indexOf(needle);
458
- if (index === -1) return;
459
- const start = Math.max(0, index - contextLength);
460
- const end = Math.min(text.length, index + needle.length + contextLength);
461
- matches.push({
462
- role: message.role || message.type,
463
- context: text.slice(start, end),
464
- timestamp: message.timestamp || null
465
- });
466
- });
467
- if (matches.length === 0) return null;
468
- return {
469
- ...normalizeSession(session),
470
- projectName: session.projectName,
471
- projectDisplayName: getDisplayName(session.cwd),
472
- matchCount: matches.length,
473
- matches
474
- };
475
- })
476
- .filter(Boolean);
477
- }
478
-
479
- function deleteSession(sessionId) {
480
- const session = getSessionById(sessionId);
481
- if (!session) {
482
- throw new Error('Session not found');
483
- }
467
+ async function getRecentSessions(limit = 5) {
468
+ const indexed = await idxGetRecent('omp', limit);
469
+ return indexed.map(session => normalizeSession({
470
+ ...session,
471
+ mtime: new Date(session.mtime).toISOString(),
472
+ mtimeMs: session.mtime,
473
+ directory: session.projectFullPath,
474
+ usage: session.tokens
475
+ }));
476
+ }
477
+
478
+ async function searchSessions(keyword, contextLength = 35, projectName = null) {
479
+ return idxSearch('omp', keyword, { contextLength, projectName });
480
+ }
481
+
482
+ async function deleteSession(sessionId) {
483
+ const session = await getSessionById(sessionId);
484
+ if (!session) throw new Error('Session not found');
484
485
  fs.unlinkSync(session.filePath);
485
486
  return { success: true };
486
487
  }
487
488
 
488
- function deleteProject(projectName) {
489
- const sessions = getAllSessions().filter(session => session.projectName === projectName);
490
- if (sessions.length === 0) {
491
- throw new Error('Project not found');
492
- }
489
+ async function deleteProject(projectName) {
490
+ const sessions = await getSessionsByProject(projectName);
491
+ if (sessions.length === 0) throw new Error('Project not found');
493
492
  sessions.forEach(session => fs.unlinkSync(session.filePath));
494
493
  return { success: true, deletedSessions: sessions.length };
495
494
  }
496
495
 
497
- function forkSession(sessionId) {
498
- const session = getSessionById(sessionId);
499
- if (!session) {
500
- throw new Error('Session not found');
501
- }
502
- const parsed = path.parse(session.filePath);
503
- const forkId = `fork-${Date.now()}`;
504
- const targetPath = path.join(parsed.dir, `${parsed.name}-${forkId}${parsed.ext}`);
505
- fs.copyFileSync(session.filePath, targetPath);
506
- return { success: true, newSessionId: parseOmpSessionId(targetPath), filePath: targetPath };
496
+ async function forkSession(sessionId) {
497
+ const session = await getSessionById(sessionId);
498
+ if (!session) throw new Error('Session not found');
499
+ const newSessionId = crypto.randomUUID();
500
+ const newFile = path.join(path.dirname(session.filePath), `${newSessionId}.jsonl`);
501
+ const content = fs.readFileSync(session.filePath, 'utf8');
502
+ fs.writeFileSync(newFile, content, 'utf8');
503
+ return { success: true, sessionId: newSessionId, filePath: newFile, forkedFrom: sessionId };
507
504
  }
508
505
 
509
506
  function saveProjectOrder(order) {
@@ -516,11 +513,11 @@ function saveSessionOrder(projectName, order) {
516
513
  safeWriteJson(SESSION_ORDER_FILE, data);
517
514
  }
518
515
 
519
- function getProjectAndSessionCounts(_options = {}) {
520
- const sessions = getAllSessions();
516
+ async function getProjectAndSessionCounts(options = {}) {
517
+ const projects = await idxListProjects('omp', options);
521
518
  return {
522
- projectCount: new Set(sessions.map(session => session.projectName)).size,
523
- sessionCount: sessions.length
519
+ projectCount: projects.length,
520
+ sessionCount: projects.reduce((sum, project) => sum + (project.sessionCount || 0), 0)
524
521
  };
525
522
  }
526
523
 
@@ -60,9 +60,30 @@ function isManagedProviderId(providerId = '') {
60
60
  return String(providerId || '').startsWith(MANAGED_PROVIDER_PREFIX);
61
61
  }
62
62
 
63
- function normalizeProviderApi(value = '') {
63
+ function normalizeProviderApi(value = '', options = {}) {
64
64
  const normalized = String(value || '').trim();
65
- if (!normalized || normalized === 'openai' || normalized === 'chat' || normalized === 'chat.completions') {
65
+ const gatewaySourceType = String(options.gatewaySourceType || '').trim().toLowerCase();
66
+
67
+ const isGeneric = !normalized
68
+ || normalized === 'openai'
69
+ || normalized === 'chat'
70
+ || normalized === 'chat.completions'
71
+ || normalized === 'openai-completions';
72
+
73
+ if (gatewaySourceType === 'codex' && (
74
+ isGeneric
75
+ || normalized === 'responses'
76
+ || normalized === 'openai-responses'
77
+ )) {
78
+ return 'openai-codex-responses';
79
+ }
80
+ if (gatewaySourceType === 'claude' && isGeneric) {
81
+ return 'anthropic-messages';
82
+ }
83
+ if (gatewaySourceType === 'gemini' && isGeneric) {
84
+ return 'google-generative-ai';
85
+ }
86
+ if (isGeneric) {
66
87
  return 'openai-completions';
67
88
  }
68
89
  if (normalized === 'responses') {
@@ -341,7 +362,9 @@ function buildProviderEntry(channel = {}, options = {}) {
341
362
  const entry = compactObject({
342
363
  ...normalizedProvider.config,
343
364
  baseUrl: String(channel.baseUrl || providerConfig.baseUrl || '').trim(),
344
- api: normalizeProviderApi(channel.providerApi || channel.api || channel.wireApi || providerConfig.api),
365
+ api: normalizeProviderApi(channel.providerApi || channel.api || channel.wireApi || providerConfig.api, {
366
+ gatewaySourceType: channel.gatewaySourceType
367
+ }),
345
368
  models
346
369
  });
347
370
 
@@ -1220,6 +1243,7 @@ module.exports = {
1220
1243
  getLastManagedOmpSyncResult,
1221
1244
  isManagedOmpProvidersActive,
1222
1245
  isManagedProviderExtensionActive: isManagedOmpProvidersActive,
1246
+ isManagedProviderId,
1223
1247
  normalizeProviderId,
1224
1248
  normalizeProviderApi,
1225
1249
  normalizeModels,