coding-tool-x 3.8.8 → 3.8.9

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 (60) hide show
  1. package/README.md +2 -2
  2. package/dist/web/assets/{AgentsDrawer-DRQuwi0m.js → AgentsDrawer-Cj5CJ4d9.js} +1 -1
  3. package/dist/web/assets/{Analytics-Cf5KwEdj.js → Analytics-1doS-8uC.js} +1 -1
  4. package/dist/web/assets/{AssetPathField-DyniM0pF.js → AssetPathField-C9TOPl7o.js} +1 -1
  5. package/dist/web/assets/{ChatHistoryDrawer-CrfS0KYV.js → ChatHistoryDrawer-BTmQ1aQr.js} +1 -1
  6. package/dist/web/assets/{CollapseItem-ClMsJuUu.js → CollapseItem-CwqJk3bE.js} +1 -1
  7. package/dist/web/assets/{CommandsDrawer-CzHelf56.js → CommandsDrawer-CaZWDI_G.js} +1 -1
  8. package/dist/web/assets/{ConfigExportDrawer-BEZ8wTby.js → ConfigExportDrawer-C3vSZEsS.js} +1 -1
  9. package/dist/web/assets/ConfigTemplates-gyERg9iC.js +1 -0
  10. package/dist/web/assets/{ConfigTemplatesDrawer-BqM32Im7.js → ConfigTemplatesDrawer-B17RqFi4.js} +1 -1
  11. package/dist/web/assets/{ConfigTemplatesPanel-lTdutC8H.js → ConfigTemplatesPanel-DKIC_FqG.js} +1 -1
  12. package/dist/web/assets/{FavoritesDrawer-DDyWUBJR.js → FavoritesDrawer-DaziQmBp.js} +1 -1
  13. package/dist/web/assets/{GatewayConvertDrawer-Rigtt_5J.js → GatewayConvertDrawer-OMgWUUSc.js} +1 -1
  14. package/dist/web/assets/{Home-tCSKmSHV.js → Home-DOSUTfrE.js} +1 -1
  15. package/dist/web/assets/{MarkdownEditor-BMMUufSc.js → MarkdownEditor-9TTwTS-U.js} +1 -1
  16. package/dist/web/assets/{McpDrawer-CmE-d7VW.js → McpDrawer-I8rl8lGH.js} +1 -1
  17. package/dist/web/assets/{McpFormDrawer-ByvS18od.js → McpFormDrawer-DEP3C7ut.js} +1 -1
  18. package/dist/web/assets/{PluginManager-C4kJlpVX.js → PluginManager-BuRcWuBj.js} +1 -1
  19. package/dist/web/assets/{PluginsDrawer-05R4k15x.js → PluginsDrawer-_zZENaQn.js} +1 -1
  20. package/dist/web/assets/{PluginsPanel-BCoEs7Og.js → PluginsPanel-Bs5qZ-TK.js} +1 -1
  21. package/dist/web/assets/ProjectList-BSxuL2td.js +1 -0
  22. package/dist/web/assets/ProjectList-VbIYSHbH.css +1 -0
  23. package/dist/web/assets/{PromptsDrawer-DnwBf_oW.js → PromptsDrawer-CoZ-ZtsY.js} +1 -1
  24. package/dist/web/assets/{RecentSessionsDrawer-B7d-38wH.js → RecentSessionsDrawer-B_17aJBG.js} +1 -1
  25. package/dist/web/assets/{SessionCard-BUgjHQwX.js → SessionCard-BL_x0d3t.js} +1 -1
  26. package/dist/web/assets/SessionList-DhRYZ6vl.js +1 -0
  27. package/dist/web/assets/{SettingsDrawer-BITzjQLl.js → SettingsDrawer-fYb0DU7X.js} +1 -1
  28. package/dist/web/assets/{SkillManager-D9jkNuRM.js → SkillManager-S698Hns8.js} +1 -1
  29. package/dist/web/assets/{SkillsDrawer-0Z4IefxC.js → SkillsDrawer-Ms4lPCow.js} +1 -1
  30. package/dist/web/assets/{SkillsPanel-BKIVOj7n.js → SkillsPanel-rqnqAOGL.js} +1 -1
  31. package/dist/web/assets/{SpeedTestDrawer-Bx1bYf5i.js → SpeedTestDrawer-Bbn38SfV.js} +1 -1
  32. package/dist/web/assets/{Upload-CzC4rGI4.js → Upload-CdyVHxCI.js} +1 -1
  33. package/dist/web/assets/{WorkspaceDrawer-eujtWpoq.js → WorkspaceDrawer-L7HJRc6u.js} +1 -1
  34. package/dist/web/assets/{WorkspaceManager-CQ-8Ej68.js → WorkspaceManager-DsoNg2z8.js} +1 -1
  35. package/dist/web/assets/{WorkspacePanel-Cql5qE2R.js → WorkspacePanel-Be8PVZah.js} +1 -1
  36. package/dist/web/assets/headers-B5JWGn3v.js +1 -0
  37. package/dist/web/assets/{index-Cnbe696C.css → index-3Lp6z8TQ.css} +1 -1
  38. package/dist/web/assets/index-BKlAWTH-.js +2 -0
  39. package/dist/web/assets/{sessions-DqlbocLJ.js → sessions-CC0j-KH_.js} +1 -1
  40. package/dist/web/assets/{useResponsiveDrawer-DZd5rNk5.js → useResponsiveDrawer-V_onl9NV.js} +1 -1
  41. package/dist/web/index.html +2 -2
  42. package/package.json +1 -1
  43. package/src/commands/daemon.js +27 -4
  44. package/src/platforms/channel-auth-registry.js +36 -6
  45. package/src/platforms/channel-auth-service.js +12 -2
  46. package/src/platforms/drivers/omp/prompts.js +58 -2
  47. package/src/platforms/drivers/omp/proxy-implementation.js +25 -1
  48. package/src/platforms/drivers/opencode/channels-implementation.js +25 -22
  49. package/src/platforms/drivers/opencode/channels.js +6 -1
  50. package/src/platforms/manifests/omp.json +2 -1
  51. package/src/platforms/manifests/opencode.json +28 -0
  52. package/src/platforms/native-oauth-adapters.js +318 -18
  53. package/src/platforms/oauth-credentials-service.js +344 -13
  54. package/src/server/index.js +35 -1
  55. package/dist/web/assets/ConfigTemplates-Crg3xWe5.js +0 -1
  56. package/dist/web/assets/ProjectList-BJt1OIaI.js +0 -1
  57. package/dist/web/assets/ProjectList-DG3yu8xZ.css +0 -1
  58. package/dist/web/assets/SessionList-B-bXZRFk.js +0 -1
  59. package/dist/web/assets/headers-BqDmkMjy.js +0 -1
  60. package/dist/web/assets/index-Cmmwj3Fx.js +0 -2
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
  const crypto = require('crypto');
5
- const { spawnSync } = require('child_process');
5
+ const { execFileSync, spawnSync } = require('child_process');
6
6
  const toml = require('toml');
7
7
  const tomlStringify = require('@iarna/toml').stringify;
8
8
  const pathsModule = require('../config/paths');
@@ -17,7 +17,7 @@ const { syncCodexUserEnvironment } = require('./drivers/codex/env-manager');
17
17
  const nativeKeychain = require('../server/services/native-keychain');
18
18
  const { maskToken, decodeJwtPayload, removeFileIfExists, sha256 } = require('../server/services/oauth-utils');
19
19
 
20
- const SUPPORTED_TOOLS = ['claude', 'codex', 'gemini', 'omp'];
20
+ const SUPPORTED_TOOLS = ['claude', 'codex', 'gemini', 'omp', 'opencode'];
21
21
  const GEMINI_MAIN_ACCOUNT_KEY = 'main-account';
22
22
  const GEMINI_KEYCHAIN_SERVICE = 'gemini-cli-oauth';
23
23
  const CODEX_KEYCHAIN_SERVICE = 'Codex Auth';
@@ -295,6 +295,43 @@ function readJsonFileFromString(raw) {
295
295
  }
296
296
  }
297
297
 
298
+ function firstString(...values) {
299
+ return values.find(value => typeof value === 'string' && value.trim())?.trim() || '';
300
+ }
301
+
302
+ function extractTokenMetadata(...tokens) {
303
+ const payloads = tokens
304
+ .filter(Boolean)
305
+ .map(token => decodeJwtPayload(token))
306
+ .filter(payload => payload && typeof payload === 'object');
307
+ const payload = payloads.find(item => (
308
+ item.chatgpt_account_id
309
+ || item['https://api.openai.com/auth.chatgpt_account_id']
310
+ || item['https://api.openai.com/auth']?.chatgpt_account_id
311
+ || item.email
312
+ )) || payloads[0] || {};
313
+ const auth = payload['https://api.openai.com/auth'] || {};
314
+ const profile = payload['https://api.openai.com/profile'] || {};
315
+ const accountId = firstString(
316
+ payload.chatgpt_account_id,
317
+ payload['https://api.openai.com/auth.chatgpt_account_id'],
318
+ auth.chatgpt_account_id,
319
+ payload.account_id,
320
+ payload.organizations?.[0]?.id
321
+ );
322
+ const accountEmail = firstString(
323
+ payload.email,
324
+ profile.email,
325
+ auth.email
326
+ );
327
+ const exp = Number(payload.exp || 0);
328
+ return {
329
+ accountId,
330
+ accountEmail,
331
+ expiresAt: Number.isFinite(exp) && exp > 0 ? exp * 1000 : null
332
+ };
333
+ }
334
+
298
335
  function getCodexKeychainAccount() {
299
336
  const codexHome = NATIVE_PATHS.codex.dir;
300
337
  const resolvedPath = fs.existsSync(codexHome)
@@ -320,7 +357,13 @@ function parseCodexAuthPayload(raw) {
320
357
 
321
358
  const idTokenPayload = decodeJwtPayload(tokens.id_token);
322
359
  const accessTokenPayload = decodeJwtPayload(tokens.access_token);
323
- const exp = Number(idTokenPayload?.exp || accessTokenPayload?.exp || 0) || null;
360
+ const tokenMetadata = extractTokenMetadata(tokens.id_token, tokens.access_token);
361
+ // The id token can expire before the access token. Use the newest usable
362
+ // expiry so a still-valid access token is not unnecessarily refreshed.
363
+ const exp = Math.max(
364
+ Number(idTokenPayload?.exp || 0),
365
+ Number(accessTokenPayload?.exp || 0)
366
+ ) || null;
324
367
  const accountId = tokens.account_id
325
368
  || idTokenPayload?.chatgpt_account_id
326
369
  || accessTokenPayload?.chatgpt_account_id
@@ -337,8 +380,8 @@ function parseCodexAuthPayload(raw) {
337
380
  refreshToken: String(tokens.refresh_token || '').trim() || '',
338
381
  idToken: String(tokens.id_token || '').trim() || '',
339
382
  accountId: String(accountId).trim(),
340
- accountEmail: String(idTokenPayload?.email || '').trim() || '',
341
- expiresAt: exp ? exp * 1000 : null,
383
+ accountEmail: tokenMetadata.accountEmail,
384
+ expiresAt: exp ? exp * 1000 : tokenMetadata.expiresAt,
342
385
  lastRefresh: auth.last_refresh || null,
343
386
  primaryToken: String(tokens.access_token || '').trim()
344
387
  };
@@ -362,7 +405,21 @@ function readCodexFileAuth() {
362
405
  }
363
406
 
364
407
  function readCodexNativeOAuth() {
365
- return readCodexKeychainAuth() || readCodexFileAuth();
408
+ const keychain = readCodexKeychainAuth();
409
+ const file = readCodexFileAuth();
410
+ if (!keychain) return file;
411
+ if (!file) return keychain;
412
+
413
+ const recency = (credential) => {
414
+ const lastRefresh = Date.parse(credential.lastRefresh || '') || 0;
415
+ const expiresAt = Number(credential.expiresAt || 0);
416
+ return lastRefresh || (Number.isFinite(expiresAt) ? expiresAt : 0);
417
+ };
418
+
419
+ // Codex stores the same auth payload in the keychain and auth.json. When
420
+ // they diverge, prefer the copy refreshed most recently instead of blindly
421
+ // selecting a stale keychain entry.
422
+ return recency(file) > recency(keychain) ? file : keychain;
366
423
  }
367
424
 
368
425
  function clearCodexOAuth() {
@@ -447,6 +504,45 @@ function applyCodexOAuth(credential) {
447
504
  return { storage: wroteKeychain ? 'auth-file+keychain' : 'auth-file' };
448
505
  }
449
506
 
507
+ // Refreshing an OAuth token must not clear the user's configured channels.
508
+ // `applyCodexOAuth` is intentionally broader because it switches the active
509
+ // auth mode; this helper only replaces the rotating token pair in place.
510
+ function updateCodexOAuthTokens(credential = {}) {
511
+ const current = readCodexNativeOAuth();
512
+ const currentAuth = (() => {
513
+ try {
514
+ return codexSettingsManager.readAuth() || {};
515
+ } catch {
516
+ return {};
517
+ }
518
+ })();
519
+ const currentTokens = currentAuth.tokens && typeof currentAuth.tokens === 'object'
520
+ ? currentAuth.tokens
521
+ : {};
522
+ const authPayload = {
523
+ ...currentAuth,
524
+ auth_mode: credential.authMode || current?.authMode || currentAuth.auth_mode || 'chatgpt',
525
+ tokens: {
526
+ ...currentTokens,
527
+ access_token: credential.accessToken || current?.accessToken || currentTokens.access_token || '',
528
+ refresh_token: credential.refreshToken || current?.refreshToken || currentTokens.refresh_token || '',
529
+ id_token: credential.idToken || current?.idToken || currentTokens.id_token || '',
530
+ account_id: credential.accountId || current?.accountId || currentTokens.account_id || ''
531
+ },
532
+ last_refresh: credential.lastRefresh || new Date().toISOString()
533
+ };
534
+
535
+ if (!authPayload.tokens.access_token) {
536
+ throw new Error('Codex OAuth access token is missing');
537
+ }
538
+
539
+ writeJsonFile(NATIVE_PATHS.codex.auth, authPayload);
540
+ const wroteKeychain = nativeKeychain.isSupported()
541
+ ? nativeKeychain.setPassword(CODEX_KEYCHAIN_SERVICE, getCodexKeychainAccount(), JSON.stringify(authPayload))
542
+ : false;
543
+ return { storage: wroteKeychain ? 'auth-file+keychain' : 'auth-file' };
544
+ }
545
+
450
546
  function inspectCodexState() {
451
547
  const { getCodexProxyStatus } = require('./drivers/codex/proxy-implementation');
452
548
  const proxyStatus = getCodexProxyStatus();
@@ -749,23 +845,184 @@ function getOmpAuthSnapshot(options = {}) {
749
845
  }
750
846
 
751
847
  function readAllOmpNativeOAuth() {
848
+ const storedOAuth = readOmpOAuthCredentialsFromDb();
752
849
  const snapshot = getOmpAuthSnapshot({ forceRefresh: true });
753
- if (!snapshot?.available || !Array.isArray(snapshot.providers)) {
754
- return [];
755
- }
850
+ if (!snapshot?.available || !Array.isArray(snapshot.providers)) return storedOAuth;
756
851
 
757
- return snapshot.providers
852
+ const snapshotCredentials = snapshot.providers
758
853
  .filter(provider => provider?.loggedIn || Number(provider?.accountCount || 0) > 0)
759
854
  .flatMap((provider) => {
760
855
  const accounts = Array.isArray(provider.accounts) ? provider.accounts : [];
761
- return accounts.map((account) => ({
762
- providerId: provider.id,
763
- accountId: String(account.index || account.id || account.accountId || '').trim(),
764
- accountEmail: String(account.identity || account.email || account.accountEmail || '').trim(),
765
- storage: 'auth-broker',
766
- primaryToken: ''
767
- }));
856
+ const providerCredentials = storedOAuth.filter(item => item.providerId === provider.id);
857
+ return accounts.map((account, index) => {
858
+ const accountId = String(account.index || account.id || account.accountId || '').trim();
859
+ const accountEmail = String(account.identity || account.email || account.accountEmail || '').trim();
860
+ const stored = providerCredentials.find(item => (
861
+ (item.accountId && item.accountId === accountId)
862
+ || (item.accountEmail && item.accountEmail === accountEmail)
863
+ )) || providerCredentials[index] || null;
864
+ return {
865
+ providerId: provider.id,
866
+ accountId: stored?.accountId || accountId,
867
+ accountEmail: stored?.accountEmail || accountEmail,
868
+ identityKey: stored?.identityKey || '',
869
+ accessToken: stored?.accessToken || '',
870
+ refreshToken: stored?.refreshToken || '',
871
+ expiresAt: stored?.expiresAt || null,
872
+ storage: stored?.storage || 'auth-broker',
873
+ primaryToken: stored?.primaryToken || ''
874
+ };
875
+ });
876
+ });
877
+ return snapshotCredentials.length > 0 ? snapshotCredentials : storedOAuth;
878
+ }
879
+
880
+ function getOmpAgentDatabasePath() {
881
+ const agentDir = NATIVE_PATHS.omp?.dir;
882
+ return agentDir ? path.join(agentDir, 'agent.db') : '';
883
+ }
884
+
885
+ function readSqliteRows(databasePath, sql) {
886
+ if (!databasePath || !fs.existsSync(databasePath)) return [];
887
+ try {
888
+ const output = execFileSync('sqlite3', ['-json', databasePath, sql], {
889
+ encoding: 'utf8',
890
+ stdio: ['ignore', 'pipe', 'pipe'],
891
+ maxBuffer: 4 * 1024 * 1024,
892
+ windowsHide: true
893
+ }).trim();
894
+ if (!output) return [];
895
+ const parsed = JSON.parse(output);
896
+ return Array.isArray(parsed) ? parsed : [];
897
+ } catch {
898
+ return [];
899
+ }
900
+ }
901
+
902
+ function parseOAuthCredentialData(value) {
903
+ if (!value) return null;
904
+ try {
905
+ const parsed = typeof value === 'string' ? JSON.parse(value) : value;
906
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
907
+ } catch {
908
+ return null;
909
+ }
910
+ }
911
+
912
+ function normalizeCredentialExpiry(value) {
913
+ const numeric = Number(value);
914
+ if (!Number.isFinite(numeric) || numeric <= 0) return null;
915
+ return numeric < 1e12 ? numeric * 1000 : numeric;
916
+ }
917
+
918
+ function readOmpOAuthCredentialsFromDb() {
919
+ const rows = readSqliteRows(
920
+ getOmpAgentDatabasePath(),
921
+ "select provider, credential_type, data, identity_key, disabled_cause from auth_credentials where credential_type = 'oauth' order by updated_at desc"
922
+ );
923
+ return rows.map(row => {
924
+ if (row?.disabled_cause) return null;
925
+ const data = parseOAuthCredentialData(row?.data) || {};
926
+ const accessToken = firstString(data.access, data.accessToken, data.access_token, data.token);
927
+ if (!accessToken) return null;
928
+ const tokenMetadata = extractTokenMetadata(accessToken, data.id_token, data.idToken);
929
+ const identityKey = firstString(data.identityKey, data.identity_key, row?.identity_key);
930
+ const identityAccountId = identityKey.match(/(?:^|[|:])org:([^|]+)/)?.[1] || '';
931
+ return {
932
+ providerId: firstString(row?.provider),
933
+ accountId: firstString(data.accountId, data.account_id, data.orgId, data.org_id, tokenMetadata.accountId, identityAccountId),
934
+ accountEmail: firstString(data.accountEmail, data.account_email, data.email, tokenMetadata.accountEmail),
935
+ identityKey,
936
+ accessToken,
937
+ refreshToken: firstString(data.refresh, data.refreshToken, data.refresh_token),
938
+ expiresAt: normalizeCredentialExpiry(data.expires || data.expiresAt || data.expires_at) || tokenMetadata.expiresAt,
939
+ storage: 'auth-broker',
940
+ primaryToken: accessToken
941
+ };
942
+ }).filter(Boolean);
943
+ }
944
+
945
+ function normalizeOpenCodeProviderId(value = '') {
946
+ const text = String(value || '').trim().toLowerCase();
947
+ if (text.includes('openai') || text.includes('codex')) return 'openai-codex';
948
+ try {
949
+ return new URL(text).hostname.replace(/^www\./, '') || text;
950
+ } catch {
951
+ return text || 'opencode';
952
+ }
953
+ }
954
+
955
+ function parseOpenCodeCredential(row = {}, storage = 'opencode') {
956
+ const accessToken = firstString(row.accessToken, row.access_token, row.access, row.token);
957
+ if (!accessToken) return null;
958
+ const tokenMetadata = extractTokenMetadata(accessToken, row.idToken, row.id_token);
959
+ const providerId = normalizeOpenCodeProviderId(row.providerId || row.provider || row.url);
960
+ return {
961
+ providerId,
962
+ accountId: firstString(row.accountId, row.account_id, tokenMetadata.accountId),
963
+ accountEmail: firstString(row.accountEmail, row.account_email, row.email, tokenMetadata.accountEmail),
964
+ identityKey: firstString(row.identityKey, row.identity_key),
965
+ accessToken,
966
+ idToken: firstString(row.idToken, row.id_token),
967
+ refreshToken: firstString(row.refreshToken, row.refresh_token, row.refresh),
968
+ expiresAt: normalizeCredentialExpiry(row.expiresAt || row.expires_at || row.token_expiry) || tokenMetadata.expiresAt,
969
+ storage,
970
+ primaryToken: accessToken
971
+ };
972
+ }
973
+
974
+ function readOpenCodeAuthFile() {
975
+ const filePath = NATIVE_PATHS.opencode?.auth;
976
+ const payload = filePath ? readJsonFile(filePath, null) : null;
977
+ if (!payload || typeof payload !== 'object') return [];
978
+ const entries = payload.access_token || payload.accessToken || payload.access
979
+ ? [{ ...payload }]
980
+ : Object.entries(payload).map(([providerId, value]) => ({
981
+ ...(value && typeof value === 'object' ? value : {}),
982
+ providerId
983
+ }));
984
+ return entries.map(entry => parseOpenCodeCredential(entry, 'opencode-auth-file')).filter(Boolean);
985
+ }
986
+
987
+ function readOpenCodeDatabaseCredentials() {
988
+ const dataDir = NATIVE_PATHS.opencode?.data;
989
+ if (!dataDir || !fs.existsSync(dataDir)) return [];
990
+ const databases = fs.readdirSync(dataDir)
991
+ .filter(name => /^opencode-.*\.db$/i.test(name) || /^opencode\.db$/i.test(name))
992
+ .map(name => path.join(dataDir, name))
993
+ .filter(filePath => {
994
+ try { return fs.statSync(filePath).isFile(); } catch { return false; }
995
+ })
996
+ .sort((left, right) => {
997
+ try { return fs.statSync(right).mtimeMs - fs.statSync(left).mtimeMs; } catch { return 0; }
768
998
  });
999
+ const rows = [];
1000
+ for (const databasePath of databases) {
1001
+ rows.push(...readSqliteRows(
1002
+ databasePath,
1003
+ 'select email, url, access_token, refresh_token, token_expiry from account'
1004
+ ));
1005
+ rows.push(...readSqliteRows(
1006
+ databasePath,
1007
+ 'select email, url, access_token, refresh_token, token_expiry from control_account'
1008
+ ));
1009
+ }
1010
+ return rows.map(row => parseOpenCodeCredential(row, 'opencode-sqlite')).filter(Boolean);
1011
+ }
1012
+
1013
+ function readAllOpenCodeNativeOAuth() {
1014
+ const entries = [...readOpenCodeAuthFile(), ...readOpenCodeDatabaseCredentials()];
1015
+ const seen = new Set();
1016
+ return entries.filter(entry => {
1017
+ const key = `${entry.providerId}:${entry.accountId || entry.accountEmail || entry.primaryToken}`;
1018
+ if (seen.has(key)) return false;
1019
+ seen.add(key);
1020
+ return true;
1021
+ });
1022
+ }
1023
+
1024
+ function readOpenCodeNativeOAuth() {
1025
+ return readAllOpenCodeNativeOAuth()[0] || null;
769
1026
  }
770
1027
 
771
1028
  function readOmpNativeOAuth() {
@@ -805,17 +1062,32 @@ function buildOmpImportPayload(credential = {}) {
805
1062
  }
806
1063
 
807
1064
  const access = String(credential.accessToken || credential.primaryToken || '').trim();
1065
+ const refresh = String(credential.refreshToken || '').trim();
1066
+ const expiresAt = Number(credential.expiresAt || 0);
1067
+ const expired = Number.isFinite(expiresAt) && expiresAt > 0
1068
+ ? new Date(expiresAt).toISOString()
1069
+ : undefined;
808
1070
  if (!access) {
809
1071
  return null;
810
1072
  }
811
1073
 
812
1074
  return {
813
1075
  provider: credential.providerId,
1076
+ type: credential.credentialType || 'oauth',
814
1077
  credential_type: credential.credentialType || 'oauth',
1078
+ // OMP keeps a logged-out credential row in the broker database. Explicitly
1079
+ // marking the imported row active is required to revive a token after a
1080
+ // restart or token rotation.
1081
+ disabled: false,
815
1082
  identity_key: credential.identityKey || credential.accountId || credential.accountEmail || undefined,
1083
+ access_token: access,
1084
+ refresh_token: refresh,
1085
+ expired,
1086
+ account_id: credential.accountId || undefined,
1087
+ email: credential.accountEmail || undefined,
816
1088
  data: {
817
1089
  access,
818
- refresh: credential.refreshToken || '',
1090
+ refresh,
819
1091
  expires: credential.expiresAt || null,
820
1092
  accountId: credential.accountId || undefined,
821
1093
  accountEmail: credential.accountEmail || undefined
@@ -853,6 +1125,9 @@ function applyOmpOAuth(credential = {}) {
853
1125
  const detail = result.stderr.trim() || result.stdout.trim() || result.error?.message || `exit code ${result.status}`;
854
1126
  throw new Error(`OMP auth-broker import failed: ${detail}`);
855
1127
  }
1128
+ if (/no importable credentials|missing access_token|cannot parse expired|^skip\b/im.test(result.stdout)) {
1129
+ throw new Error(`OMP auth-broker import rejected the credential: ${result.stdout.trim()}`);
1130
+ }
856
1131
  try {
857
1132
  require('./drivers/omp/auth-providers').clearOmpAuthProviderCache();
858
1133
  } catch {
@@ -894,6 +1169,24 @@ function inspectOmpState() {
894
1169
  };
895
1170
  }
896
1171
 
1172
+ function inspectOpenCodeState() {
1173
+ let proxyRunning = false;
1174
+ try {
1175
+ proxyRunning = Boolean(require('./drivers/opencode/proxy-implementation').getOpenCodeProxyStatus()?.running);
1176
+ } catch {
1177
+ proxyRunning = false;
1178
+ }
1179
+ const nativeCredentials = readAllOpenCodeNativeOAuth();
1180
+ return {
1181
+ tool: 'opencode',
1182
+ mode: proxyRunning ? 'proxy' : (nativeCredentials.length > 0 ? 'oauth' : 'idle'),
1183
+ proxyRunning,
1184
+ oauthPresent: nativeCredentials.length > 0,
1185
+ channelConfigured: false,
1186
+ nativeCredential: nativeCredentials[0] ? buildNativeSummary(nativeCredentials[0]) : null
1187
+ };
1188
+ }
1189
+
897
1190
  function inspectTool(tool) {
898
1191
  switch (tool) {
899
1192
  case 'claude':
@@ -904,6 +1197,8 @@ function inspectTool(tool) {
904
1197
  return inspectGeminiState();
905
1198
  case 'omp':
906
1199
  return inspectOmpState();
1200
+ case 'opencode':
1201
+ return inspectOpenCodeState();
907
1202
  default:
908
1203
  throw new Error(`Unsupported OAuth tool: ${tool}`);
909
1204
  }
@@ -919,6 +1214,8 @@ function readNativeOAuth(tool) {
919
1214
  return readGeminiNativeOAuth();
920
1215
  case 'omp':
921
1216
  return readOmpNativeOAuth();
1217
+ case 'opencode':
1218
+ return readOpenCodeNativeOAuth();
922
1219
  default:
923
1220
  throw new Error(`Unsupported OAuth tool: ${tool}`);
924
1221
  }
@@ -940,6 +1237,8 @@ function readAllNativeOAuth(tool) {
940
1237
  }
941
1238
  case 'omp':
942
1239
  return readAllOmpNativeOAuth();
1240
+ case 'opencode':
1241
+ return readAllOpenCodeNativeOAuth();
943
1242
  default:
944
1243
  throw new Error(`Unsupported OAuth tool: ${tool}`);
945
1244
  }
@@ -1008,6 +1307,7 @@ module.exports = {
1008
1307
  clearClaudeChannelConfig,
1009
1308
  clearGeminiChannelConfig,
1010
1309
  clearCodexChannelConfig,
1310
+ updateCodexOAuthTokens,
1011
1311
  clearNativeOAuth,
1012
1312
  disableNativeOAuthCredential,
1013
1313
  applyOAuthCredential