coding-tool-x 3.6.12 → 3.7.0

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 (80) hide show
  1. package/README.md +46 -33
  2. package/dist/web/assets/{Analytics-BuBlsbcI.js → Analytics-Dma_j2Rq.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-DQczxuOX.js → ConfigTemplates-C49Y1PLH.js} +1 -1
  4. package/dist/web/assets/Home-DGGC8H4g.js +1 -0
  5. package/dist/web/assets/Home-ewgeIhF7.css +1 -0
  6. package/dist/web/assets/{PluginManager-C8gAlaps.js → PluginManager-B1ECTHyY.js} +1 -1
  7. package/dist/web/assets/{ProjectList-DL4JK6ci.css → ProjectList-B_L55-l9.css} +1 -1
  8. package/dist/web/assets/ProjectList-DyiGyT-H.js +1 -0
  9. package/dist/web/assets/SessionList-CH5GwwiJ.js +1 -0
  10. package/dist/web/assets/{SkillManager-BDbNbNkK.js → SkillManager-CRyVhjYQ.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-DGGaGW2c.js → WorkspaceManager-CO9tclVt.js} +1 -1
  12. package/dist/web/assets/{icons-QFXzEgqM.js → icons-Dop5j7Bf.js} +1 -1
  13. package/dist/web/assets/index-CM26UrnJ.css +1 -0
  14. package/dist/web/assets/index-EQq56pRa.js +2 -0
  15. package/dist/web/assets/{naive-ui-kcWDeJwl.js → naive-ui-CVyvdmg1.js} +1 -1
  16. package/dist/web/index.html +4 -4
  17. package/package.json +2 -1
  18. package/src/commands/channels.js +85 -24
  19. package/src/commands/daemon.js +7 -4
  20. package/src/commands/doctor.js +17 -8
  21. package/src/commands/logs.js +48 -6
  22. package/src/commands/port-config.js +81 -75
  23. package/src/commands/proxy-control.js +52 -19
  24. package/src/commands/stats.js +37 -19
  25. package/src/commands/toggle-proxy.js +131 -53
  26. package/src/commands/ui.js +3 -4
  27. package/src/config/default.js +14 -3
  28. package/src/config/paths.js +99 -11
  29. package/src/index.js +11 -16
  30. package/src/server/api/commands.js +1 -1
  31. package/src/server/api/config-export.js +4 -2
  32. package/src/server/api/config-registry.js +40 -34
  33. package/src/server/api/config.js +2 -1
  34. package/src/server/api/dashboard.js +22 -6
  35. package/src/server/api/mcp.js +5 -3
  36. package/src/server/api/pi-channels.js +349 -0
  37. package/src/server/api/pi-config.js +164 -0
  38. package/src/server/api/pi-projects.js +76 -0
  39. package/src/server/api/pi-proxy.js +154 -0
  40. package/src/server/api/pi-sessions.js +334 -0
  41. package/src/server/api/pi-statistics.js +40 -0
  42. package/src/server/api/plugins.js +1 -1
  43. package/src/server/api/prompts.js +3 -2
  44. package/src/server/api/skills.js +1 -1
  45. package/src/server/api/workspaces.js +2 -2
  46. package/src/server/index.js +41 -0
  47. package/src/server/pi-proxy-server.js +66 -0
  48. package/src/server/services/agents-service.js +2 -1
  49. package/src/server/services/base/proxy-utils.js +1 -0
  50. package/src/server/services/channel-balance.js +151 -1
  51. package/src/server/services/channel-scheduler.js +7 -1
  52. package/src/server/services/commands-service.js +11 -2
  53. package/src/server/services/config-export-service.js +275 -18
  54. package/src/server/services/config-registry-service.js +20 -18
  55. package/src/server/services/config-sync-manager.js +115 -6
  56. package/src/server/services/config-sync-service.js +4 -2
  57. package/src/server/services/config-templates-service.js +183 -45
  58. package/src/server/services/mcp-service.js +217 -33
  59. package/src/server/services/pi-auth-providers.js +294 -0
  60. package/src/server/services/pi-channels.js +94 -0
  61. package/src/server/services/pi-config.js +287 -0
  62. package/src/server/services/pi-sessions.js +490 -0
  63. package/src/server/services/pi-settings-manager.js +571 -0
  64. package/src/server/services/pi-statistics-service.js +317 -0
  65. package/src/server/services/plugins-service.js +673 -11
  66. package/src/server/services/prompts-service.js +112 -7
  67. package/src/server/services/proxy-log-helper.js +1 -0
  68. package/src/server/services/proxy-runtime.js +5 -1
  69. package/src/server/services/skill-service.js +13 -3
  70. package/src/server/services/speed-test.js +9 -3
  71. package/src/server/services/ui-config.js +50 -12
  72. package/src/server/services/workspace-service.js +19 -5
  73. package/src/server/websocket-server.js +7 -1
  74. package/dist/web/assets/Home-BvAzzWnB.js +0 -1
  75. package/dist/web/assets/Home-CIua-b-o.css +0 -1
  76. package/dist/web/assets/ProjectList-nqgi6_cR.js +0 -1
  77. package/dist/web/assets/SessionList-D8AxMYy6.js +0 -1
  78. package/dist/web/assets/index-Dsh4Q5Cg.css +0 -1
  79. package/dist/web/assets/index-y-eEayxR.js +0 -2
  80. package/src/utils/cli-flags.js +0 -7
@@ -19,7 +19,7 @@ const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homed
19
19
  const MCP_SERVERS_FILE = PATHS.mcpServers;
20
20
 
21
21
  // 各平台配置文件路径
22
- const CLAUDE_CONFIG_PATH = path.join(HOME_DIR, '.claude.json');
22
+ const CLAUDE_CONFIG_PATH = NATIVE_PATHS.claude.mcp || path.join(HOME_DIR, '.claude.json');
23
23
  const CODEX_CONFIG_PATH = NATIVE_PATHS.codex.config;
24
24
  const GEMINI_CONFIG_PATH = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'settings.json');
25
25
  const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
@@ -28,6 +28,8 @@ const OPENCODE_CONFIG_PATHS = {
28
28
  json: path.join(OPENCODE_CONFIG_DIR, 'opencode.json'),
29
29
  legacy: path.join(OPENCODE_CONFIG_DIR, 'config.json')
30
30
  };
31
+ const PI_MCP_CONFIG_PATH = NATIVE_PATHS.pi?.mcp
32
+ || path.join(NATIVE_PATHS.pi?.dir || path.join(HOME_DIR, '.omp', 'agent'), 'mcp.json');
31
33
 
32
34
  // MCP 客户端连接池
33
35
  // serverId -> { client, timestamp }
@@ -36,6 +38,9 @@ const POOL_TTL = 5 * 60 * 1000; // 5 minutes
36
38
  const STREAMABLE_HTTP_TYPE = 'streamable_http';
37
39
  const MCP_SERVER_TYPES = ['stdio', STREAMABLE_HTTP_TYPE, 'sse'];
38
40
  const REMOTE_MCP_SERVER_TYPES = [STREAMABLE_HTTP_TYPE, 'sse'];
41
+ const MCP_PLATFORM_KEYS = ['claude', 'codex', 'gemini', 'opencode', 'pi'];
42
+ const OMP_MCP_SCHEMA_URL = 'https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json';
43
+ const OMP_SERVER_NAME_PATTERN = /^[a-zA-Z0-9_.-]{1,100}$/;
39
44
 
40
45
  function normalizeServerSpec(spec = {}) {
41
46
  if (!spec || typeof spec !== 'object') {
@@ -532,7 +537,8 @@ const DEFAULT_SERVER_APPS = {
532
537
  claude: true,
533
538
  codex: false,
534
539
  gemini: false,
535
- opencode: false
540
+ opencode: false,
541
+ pi: false
536
542
  };
537
543
 
538
544
  function normalizeServerApps(apps = {}, fallbackApps = DEFAULT_SERVER_APPS) {
@@ -540,7 +546,8 @@ function normalizeServerApps(apps = {}, fallbackApps = DEFAULT_SERVER_APPS) {
540
546
  claude: apps.claude !== undefined ? !!apps.claude : !!fallbackApps.claude,
541
547
  codex: apps.codex !== undefined ? !!apps.codex : !!fallbackApps.codex,
542
548
  gemini: apps.gemini !== undefined ? !!apps.gemini : !!fallbackApps.gemini,
543
- opencode: apps.opencode !== undefined ? !!apps.opencode : !!fallbackApps.opencode
549
+ opencode: apps.opencode !== undefined ? !!apps.opencode : !!fallbackApps.opencode,
550
+ pi: apps.pi !== undefined ? !!apps.pi : !!fallbackApps.pi
544
551
  };
545
552
  }
546
553
 
@@ -648,7 +655,7 @@ async function toggleServerApp(serverId, app, enabled) {
648
655
  throw new Error(`MCP 服务器 "${serverId}" 不存在`);
649
656
  }
650
657
 
651
- if (!['claude', 'codex', 'gemini', 'opencode'].includes(app)) {
658
+ if (!MCP_PLATFORM_KEYS.includes(app)) {
652
659
  throw new Error(`无效的平台: ${app}`);
653
660
  }
654
661
 
@@ -720,28 +727,12 @@ async function syncServerToAllPlatforms(server, previousApps = null) {
720
727
  return previous[platform] && !apps[platform];
721
728
  };
722
729
 
723
- if (apps.claude) {
724
- await syncServerToPlatform(server, 'claude');
725
- } else if (shouldRemoveFromPlatform('claude')) {
726
- await removeServerFromPlatform(server.id, 'claude');
727
- }
728
-
729
- if (apps.codex) {
730
- await syncServerToPlatform(server, 'codex');
731
- } else if (shouldRemoveFromPlatform('codex')) {
732
- await removeServerFromPlatform(server.id, 'codex');
733
- }
734
-
735
- if (apps.gemini) {
736
- await syncServerToPlatform(server, 'gemini');
737
- } else if (shouldRemoveFromPlatform('gemini')) {
738
- await removeServerFromPlatform(server.id, 'gemini');
739
- }
740
-
741
- if (apps.opencode) {
742
- await syncServerToPlatform(server, 'opencode');
743
- } else if (shouldRemoveFromPlatform('opencode')) {
744
- await removeServerFromPlatform(server.id, 'opencode');
730
+ for (const platform of MCP_PLATFORM_KEYS) {
731
+ if (apps[platform]) {
732
+ await syncServerToPlatform(server, platform);
733
+ } else if (shouldRemoveFromPlatform(platform)) {
734
+ await removeServerFromPlatform(server.id, platform);
735
+ }
745
736
  }
746
737
  }
747
738
 
@@ -749,10 +740,9 @@ async function syncServerToAllPlatforms(server, previousApps = null) {
749
740
  * 从所有平台移除服务器
750
741
  */
751
742
  async function removeServerFromAllPlatforms(serverId) {
752
- await removeServerFromPlatform(serverId, 'claude');
753
- await removeServerFromPlatform(serverId, 'codex');
754
- await removeServerFromPlatform(serverId, 'gemini');
755
- await removeServerFromPlatform(serverId, 'opencode');
743
+ for (const platform of MCP_PLATFORM_KEYS) {
744
+ await removeServerFromPlatform(serverId, platform);
745
+ }
756
746
  }
757
747
 
758
748
  /**
@@ -773,6 +763,9 @@ async function syncServerToPlatform(server, platform) {
773
763
  case 'opencode':
774
764
  syncToOpenCodeConfig(server);
775
765
  break;
766
+ case 'pi':
767
+ syncToOmpMcpConfig(server);
768
+ break;
776
769
  }
777
770
  console.log(`[MCP] Synced "${server.id}" to ${platform}`);
778
771
  } catch (err) {
@@ -799,6 +792,9 @@ async function removeServerFromPlatform(serverId, platform) {
799
792
  case 'opencode':
800
793
  removeFromOpenCodeConfig(serverId);
801
794
  break;
795
+ case 'pi':
796
+ removeFromOmpMcpConfig(serverId);
797
+ break;
802
798
  }
803
799
  console.log(`[MCP] Removed "${serverId}" from ${platform}`);
804
800
  } catch (err) {
@@ -947,6 +943,133 @@ function removeFromGeminiConfig(serverId) {
947
943
  }
948
944
  }
949
945
 
946
+ // ============================================================================
947
+ // OMP 配置同步
948
+ // ============================================================================
949
+
950
+ function validateOmpServerName(serverId) {
951
+ if (!OMP_SERVER_NAME_PATTERN.test(serverId)) {
952
+ throw new Error(`OMP MCP 服务器 ID "${serverId}" 无效。OMP 仅支持 1-100 个字母、数字、下划线、点或连字符。`);
953
+ }
954
+ }
955
+
956
+ function readOmpMcpConfig() {
957
+ const config = readJsonFile(PI_MCP_CONFIG_PATH, { mcpServers: {} });
958
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
959
+ return { mcpServers: {} };
960
+ }
961
+ if (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
962
+ config.mcpServers = {};
963
+ }
964
+ return config;
965
+ }
966
+
967
+ function copyCommonMcpFields(source, target) {
968
+ for (const key of ['timeout', 'auth', 'oauth']) {
969
+ if (source[key] !== undefined) {
970
+ target[key] = source[key];
971
+ }
972
+ }
973
+ return target;
974
+ }
975
+
976
+ function convertToOmpMcpFormat(spec = {}) {
977
+ const sourceType = spec.type || 'stdio';
978
+ let result;
979
+
980
+ if (sourceType === 'stdio') {
981
+ result = {
982
+ type: 'stdio',
983
+ command: spec.command || ''
984
+ };
985
+ if (Array.isArray(spec.args) && spec.args.length > 0) {
986
+ result.args = spec.args;
987
+ }
988
+ if (spec.env && Object.keys(spec.env).length > 0) {
989
+ result.env = spec.env;
990
+ }
991
+ if (spec.cwd) {
992
+ result.cwd = spec.cwd;
993
+ }
994
+ } else if (sourceType === STREAMABLE_HTTP_TYPE) {
995
+ result = {
996
+ type: 'http',
997
+ url: spec.url || ''
998
+ };
999
+ if (spec.headers && Object.keys(spec.headers).length > 0) {
1000
+ result.headers = spec.headers;
1001
+ }
1002
+ } else if (sourceType === 'sse') {
1003
+ result = {
1004
+ type: 'sse',
1005
+ url: spec.url || ''
1006
+ };
1007
+ if (spec.headers && Object.keys(spec.headers).length > 0) {
1008
+ result.headers = spec.headers;
1009
+ }
1010
+ } else {
1011
+ result = extractServerSpec(spec);
1012
+ }
1013
+
1014
+ return copyCommonMcpFields(spec, result);
1015
+ }
1016
+
1017
+ function convertFromOmpMcpFormat(spec = {}) {
1018
+ const sourceType = spec.type || 'stdio';
1019
+ let result;
1020
+
1021
+ if (sourceType === 'stdio') {
1022
+ result = {
1023
+ type: 'stdio',
1024
+ command: spec.command || ''
1025
+ };
1026
+ if (spec.args) {
1027
+ result.args = spec.args;
1028
+ }
1029
+ if (spec.env) {
1030
+ result.env = spec.env;
1031
+ }
1032
+ if (spec.cwd) {
1033
+ result.cwd = spec.cwd;
1034
+ }
1035
+ } else if (sourceType === 'http') {
1036
+ result = {
1037
+ type: STREAMABLE_HTTP_TYPE,
1038
+ url: spec.url || ''
1039
+ };
1040
+ if (spec.headers) {
1041
+ result.headers = spec.headers;
1042
+ }
1043
+ } else if (sourceType === 'sse') {
1044
+ result = {
1045
+ type: 'sse',
1046
+ url: spec.url || ''
1047
+ };
1048
+ if (spec.headers) {
1049
+ result.headers = spec.headers;
1050
+ }
1051
+ } else {
1052
+ result = convertFromCodexFormat(spec);
1053
+ }
1054
+
1055
+ return copyCommonMcpFields(spec, result);
1056
+ }
1057
+
1058
+ function syncToOmpMcpConfig(server) {
1059
+ validateOmpServerName(server.id);
1060
+ const config = readOmpMcpConfig();
1061
+ config.mcpServers[server.id] = convertToOmpMcpFormat(server.server);
1062
+ writeJsonFile(PI_MCP_CONFIG_PATH, config);
1063
+ }
1064
+
1065
+ function removeFromOmpMcpConfig(serverId) {
1066
+ const config = readOmpMcpConfig();
1067
+ if (config.mcpServers && config.mcpServers[serverId]) {
1068
+ delete config.mcpServers[serverId];
1069
+ writeJsonFile(PI_MCP_CONFIG_PATH, config);
1070
+ }
1071
+ }
1072
+
950
1073
  // ============================================================================
951
1074
  // OpenCode 配置同步
952
1075
  // ============================================================================
@@ -1108,6 +1231,9 @@ async function importFromPlatform(platform) {
1108
1231
  case 'opencode':
1109
1232
  importedCount = importFromOpenCode(servers);
1110
1233
  break;
1234
+ case 'pi':
1235
+ importedCount = importFromOmp(servers);
1236
+ break;
1111
1237
  default:
1112
1238
  throw new Error(`无效的平台: ${platform}`);
1113
1239
  }
@@ -1250,6 +1376,39 @@ function importFromOpenCode(servers) {
1250
1376
  return count;
1251
1377
  }
1252
1378
 
1379
+ /**
1380
+ * 从 OMP 导入
1381
+ */
1382
+ function importFromOmp(servers) {
1383
+ const config = readOmpMcpConfig();
1384
+ const mcpServers = config.mcpServers || {};
1385
+ let count = 0;
1386
+
1387
+ for (const [id, spec] of Object.entries(mcpServers)) {
1388
+ const convertedSpec = convertFromOmpMcpFormat(spec || {});
1389
+
1390
+ if (servers[id]) {
1391
+ servers[id].apps = normalizeServerApps(servers[id].apps);
1392
+ if (!servers[id].apps.pi) {
1393
+ servers[id].apps.pi = true;
1394
+ count++;
1395
+ }
1396
+ } else {
1397
+ servers[id] = {
1398
+ id,
1399
+ name: id,
1400
+ server: convertedSpec,
1401
+ apps: { claude: false, codex: false, gemini: false, opencode: false, pi: true },
1402
+ createdAt: Date.now(),
1403
+ updatedAt: Date.now()
1404
+ };
1405
+ count++;
1406
+ }
1407
+ }
1408
+
1409
+ return count;
1410
+ }
1411
+
1253
1412
  /**
1254
1413
  * 从 Codex 格式转换
1255
1414
  */
@@ -1311,7 +1470,8 @@ function getStats() {
1311
1470
  claude: serverList.filter(s => s.apps?.claude).length,
1312
1471
  codex: serverList.filter(s => s.apps?.codex).length,
1313
1472
  gemini: serverList.filter(s => s.apps?.gemini).length,
1314
- opencode: serverList.filter(s => s.apps?.opencode).length
1473
+ opencode: serverList.filter(s => s.apps?.opencode).length,
1474
+ pi: serverList.filter(s => s.apps?.pi).length
1315
1475
  };
1316
1476
  }
1317
1477
 
@@ -1763,7 +1923,7 @@ function updateServerOrder(serverIds) {
1763
1923
 
1764
1924
  /**
1765
1925
  * 导出所有 MCP 配置
1766
- * @param {string} format - 导出格式: 'json' | 'claude' | 'codex' | 'opencode' | 'gemini'
1926
+ * @param {string} format - 导出格式: 'json' | 'claude' | 'codex' | 'opencode' | 'gemini' | 'pi'
1767
1927
  */
1768
1928
  function exportServers(format = 'json') {
1769
1929
  const servers = getAllServers();
@@ -1777,6 +1937,8 @@ function exportServers(format = 'json') {
1777
1937
  return exportForOpenCode(servers);
1778
1938
  case 'gemini':
1779
1939
  return exportForGemini(servers);
1940
+ case 'pi':
1941
+ return exportForOmp(servers);
1780
1942
  case 'json':
1781
1943
  default:
1782
1944
  return exportAsJson(servers);
@@ -1876,6 +2038,26 @@ function exportForGemini(servers) {
1876
2038
  };
1877
2039
  }
1878
2040
 
2041
+ /**
2042
+ * 导出为 OMP 格式
2043
+ */
2044
+ function exportForOmp(servers) {
2045
+ const mcpServers = {};
2046
+
2047
+ for (const [id, server] of Object.entries(servers)) {
2048
+ if (server.apps?.pi) {
2049
+ validateOmpServerName(id);
2050
+ mcpServers[id] = convertToOmpMcpFormat(server.server);
2051
+ }
2052
+ }
2053
+
2054
+ return {
2055
+ format: 'pi',
2056
+ content: JSON.stringify({ $schema: OMP_MCP_SCHEMA_URL, mcpServers }, null, 2),
2057
+ filename: 'omp-mcp-config.json'
2058
+ };
2059
+ }
2060
+
1879
2061
  module.exports = {
1880
2062
  getAllServers,
1881
2063
  getServer,
@@ -1895,6 +2077,8 @@ module.exports = {
1895
2077
  exportServers,
1896
2078
  _test: {
1897
2079
  extractMcpHint,
1898
- buildMcpFailureResult
2080
+ buildMcpFailureResult,
2081
+ convertToOmpMcpFormat,
2082
+ convertFromOmpMcpFormat
1899
2083
  }
1900
2084
  };
@@ -0,0 +1,294 @@
1
+ const { spawnSync } = require('child_process');
2
+ const piConfig = require('./pi-config');
3
+
4
+ const COMMAND_TIMEOUT_MS = 5000;
5
+ const TOKEN_LIST_TIMEOUT_MS = 3000;
6
+ const CACHE_TTL_MS = 15000;
7
+
8
+ const LOGIN_PROVIDER_IDS = new Set([
9
+ 'openai-codex',
10
+ 'openai-codex-device',
11
+ 'anthropic',
12
+ 'github-copilot',
13
+ 'cursor',
14
+ 'devin',
15
+ 'google-antigravity',
16
+ 'google-gemini-cli',
17
+ 'xai-oauth',
18
+ 'gitlab-duo',
19
+ 'gitlab-duo-agent',
20
+ 'alibaba-coding-plan',
21
+ 'zhipu-coding-plan',
22
+ 'kimi-code',
23
+ 'zai',
24
+ 'qwen-portal',
25
+ 'minimax-code',
26
+ 'minimax-code-cn',
27
+ 'perplexity'
28
+ ]);
29
+
30
+ const ACCOUNT_CHECK_PROVIDER_IDS = new Set([
31
+ 'openai-codex',
32
+ 'openai-codex-device',
33
+ 'anthropic',
34
+ 'google-gemini-cli',
35
+ 'github-copilot'
36
+ ]);
37
+
38
+ const PROVIDER_ALIASES = {
39
+ codex: 'openai-codex',
40
+ openai: 'openai-codex',
41
+ 'openai-codex': 'openai-codex',
42
+ 'openai-codex-device': 'openai-codex-device',
43
+ claude: 'anthropic',
44
+ anthropic: 'anthropic',
45
+ gemini: 'google-gemini-cli',
46
+ 'gemini-cli': 'google-gemini-cli',
47
+ 'google-gemini-cli': 'google-gemini-cli',
48
+ copilot: 'github-copilot',
49
+ 'github-copilot': 'github-copilot'
50
+ };
51
+
52
+ let snapshotCache = {};
53
+
54
+ function normalizeProviderId(value = '') {
55
+ return String(value || '')
56
+ .trim()
57
+ .toLowerCase()
58
+ .replace(/[^a-z0-9_-]+/g, '-')
59
+ .replace(/-+/g, '-')
60
+ .replace(/^-|-$/g, '');
61
+ }
62
+
63
+ function resolveProviderAlias(value = '') {
64
+ const id = normalizeProviderId(value);
65
+ return PROVIDER_ALIASES[id] || id;
66
+ }
67
+
68
+ function normalizeCommandOutput(value) {
69
+ if (value === undefined || value === null) return '';
70
+ return Buffer.isBuffer(value) ? value.toString('utf8') : String(value);
71
+ }
72
+
73
+ function sanitizeIdentity(value = '') {
74
+ const text = String(value || '').trim();
75
+ if (!text) return '';
76
+ return text
77
+ .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, (email) => {
78
+ const [name, domain] = email.split('@');
79
+ if (!name || !domain) return '***';
80
+ const visible = name.length <= 2 ? `${name[0] || '*'}***` : `${name.slice(0, 2)}***${name.slice(-1)}`;
81
+ return `${visible}@${domain}`;
82
+ })
83
+ .replace(/sk-[A-Za-z0-9_-]{8,}/g, 'sk-***')
84
+ .replace(/nvapi-[A-Za-z0-9_-]{8,}/g, 'nvapi-***')
85
+ .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer ***')
86
+ .replace(/[A-Za-z0-9_-]{24,}\.[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,}/g, 'jwt.***')
87
+ .replace(/[A-Za-z0-9_=-]{32,}/g, '***');
88
+ }
89
+
90
+ function runCommand(command, args = [], options = {}) {
91
+ const runner = options.commandRunner || spawnSync;
92
+ const result = runner(command, args, {
93
+ encoding: 'utf8',
94
+ env: {
95
+ ...process.env,
96
+ ...(options.env || {})
97
+ },
98
+ timeout: options.timeout || COMMAND_TIMEOUT_MS
99
+ });
100
+ if (typeof result === 'string' || Buffer.isBuffer(result)) {
101
+ return { status: 0, stdout: normalizeCommandOutput(result), stderr: '' };
102
+ }
103
+ return result || { status: 0, stdout: '', stderr: '' };
104
+ }
105
+
106
+ function parseJsonCommand(command, args, options = {}) {
107
+ const result = runCommand(command, args, options);
108
+ const status = result.status === undefined || result.status === null ? 0 : result.status;
109
+ const stdout = normalizeCommandOutput(result.stdout).trim();
110
+ const stderr = normalizeCommandOutput(result.stderr).trim();
111
+ if (result.error) {
112
+ return { ok: false, status, error: result.error.message, stderr };
113
+ }
114
+ if (!stdout) {
115
+ return { ok: status === 0, status, value: null, stderr };
116
+ }
117
+ try {
118
+ return { ok: status === 0, status, value: JSON.parse(stdout), stderr };
119
+ } catch (error) {
120
+ return { ok: false, status, error: error.message, stderr };
121
+ }
122
+ }
123
+
124
+ function parseAccountLine(line = '', index) {
125
+ const sanitized = sanitizeIdentity(line)
126
+ .replace(/^\s*[-*]\s*/, '')
127
+ .replace(/^\s*\[?\d+\]?\s*[:.)-]?\s*/, '')
128
+ .trim();
129
+ if (!sanitized || /^no oauth accounts found/i.test(sanitized)) return null;
130
+ return {
131
+ index: index + 1,
132
+ identity: sanitized
133
+ };
134
+ }
135
+
136
+ function listProviderAccounts(command, providerId, options = {}) {
137
+ const result = runCommand(command, ['token', providerId, '--list'], {
138
+ ...options,
139
+ timeout: options.tokenListTimeout || TOKEN_LIST_TIMEOUT_MS
140
+ });
141
+ const status = result.status === undefined || result.status === null ? 0 : result.status;
142
+ const stdout = normalizeCommandOutput(result.stdout);
143
+ const stderr = normalizeCommandOutput(result.stderr);
144
+ const combined = `${stdout}\n${stderr}`;
145
+
146
+ if (result.error) {
147
+ return { checked: true, loggedIn: false, accountCount: 0, accounts: [], error: result.error.message };
148
+ }
149
+
150
+ const accounts = stdout
151
+ .split(/\r?\n/)
152
+ .map((line, index) => parseAccountLine(line, index))
153
+ .filter(Boolean);
154
+
155
+ const noAccounts = /no oauth accounts found/i.test(combined);
156
+ return {
157
+ checked: true,
158
+ loggedIn: accounts.length > 0,
159
+ accountCount: accounts.length,
160
+ accounts,
161
+ error: status !== 0 && !noAccounts && accounts.length === 0
162
+ ? sanitizeIdentity(stderr || stdout || `exit code ${status}`)
163
+ : null
164
+ };
165
+ }
166
+
167
+ function isLikelyLoginProvider(provider = {}) {
168
+ const id = normalizeProviderId(provider.id);
169
+ const name = String(provider.name || '').toLowerCase();
170
+ return LOGIN_PROVIDER_IDS.has(id)
171
+ || id.includes('oauth')
172
+ || id.includes('codex')
173
+ || id.includes('copilot')
174
+ || id.includes('gemini-cli')
175
+ || name.includes('subscription')
176
+ || name.includes('coding plan')
177
+ || name.includes('token plan')
178
+ || name.includes('claude pro')
179
+ || name.includes('chatgpt plus')
180
+ || name.includes('chatgpt pro');
181
+ }
182
+
183
+ function normalizeProvider(provider = {}) {
184
+ const id = normalizeProviderId(provider.id);
185
+ return {
186
+ id,
187
+ name: String(provider.name || provider.id || id),
188
+ loginCapable: isLikelyLoginProvider(provider)
189
+ };
190
+ }
191
+
192
+ function buildEmptySnapshot(runtime, reason) {
193
+ return {
194
+ available: false,
195
+ reason,
196
+ runtime,
197
+ brokerStatus: null,
198
+ gatewayStatus: null,
199
+ providers: [],
200
+ supportedProviders: [],
201
+ aliases: { ...PROVIDER_ALIASES },
202
+ checkedAt: new Date().toISOString()
203
+ };
204
+ }
205
+
206
+ function getOmpAuthProviderSnapshot(options = {}) {
207
+ const now = Date.now();
208
+ const cacheKey = options.accountCheck === false
209
+ ? (options.includeStatus === false ? 'metadata' : 'metadata-status')
210
+ : 'full';
211
+ const cached = snapshotCache[cacheKey];
212
+ if (!options.forceRefresh && cached && now - cached.loadedAt < CACHE_TTL_MS) {
213
+ return cached.value;
214
+ }
215
+
216
+ const env = options.env || process.env;
217
+ const runtime = options.runtime || piConfig.resolvePiRuntime(env, options.runtimeOptions || {});
218
+ if (!runtime || runtime.runtime !== 'omp' || !runtime.installed) {
219
+ const value = buildEmptySnapshot(runtime || null, 'omp-not-available');
220
+ snapshotCache[cacheKey] = { loadedAt: now, value };
221
+ return value;
222
+ }
223
+
224
+ const command = runtime.command;
225
+ const listResult = parseJsonCommand(command, ['auth-broker', 'list', '--json'], options);
226
+ if (!listResult.ok || !Array.isArray(listResult.value)) {
227
+ const value = buildEmptySnapshot(runtime, listResult.error || listResult.stderr || 'auth-provider-list-failed');
228
+ snapshotCache[cacheKey] = { loadedAt: now, value };
229
+ return value;
230
+ }
231
+
232
+ const supportedProviders = listResult.value
233
+ .map(normalizeProvider)
234
+ .filter(provider => provider.id);
235
+ const loginProviders = supportedProviders
236
+ .filter(provider => provider.loginCapable)
237
+ .map(provider => {
238
+ if (options.accountCheck === false || !ACCOUNT_CHECK_PROVIDER_IDS.has(provider.id)) {
239
+ return {
240
+ ...provider,
241
+ checked: false,
242
+ loggedIn: null,
243
+ accountCount: null,
244
+ accounts: [],
245
+ error: null
246
+ };
247
+ }
248
+ return {
249
+ ...provider,
250
+ ...listProviderAccounts(command, provider.id, options)
251
+ };
252
+ });
253
+
254
+ const brokerStatus = options.includeStatus === false
255
+ ? { value: null }
256
+ : parseJsonCommand(command, ['auth-broker', 'status', '--json'], options);
257
+ const gatewayStatus = options.includeStatus === false
258
+ ? { value: null }
259
+ : parseJsonCommand(command, ['auth-gateway', 'status', '--json'], options);
260
+
261
+ const value = {
262
+ available: true,
263
+ runtime,
264
+ brokerStatus: brokerStatus.value || null,
265
+ gatewayStatus: gatewayStatus.value || null,
266
+ providers: loginProviders,
267
+ supportedProviders,
268
+ aliases: { ...PROVIDER_ALIASES },
269
+ checkedAt: new Date().toISOString()
270
+ };
271
+ snapshotCache[cacheKey] = { loadedAt: now, value };
272
+ return value;
273
+ }
274
+
275
+ function findAuthProviderForKey(providerKey, snapshot) {
276
+ const resolved = resolveProviderAlias(providerKey);
277
+ const providers = Array.isArray(snapshot?.providers) ? snapshot.providers : [];
278
+ return providers.find(provider => provider.id === resolved) || null;
279
+ }
280
+
281
+ function clearOmpAuthProviderCache() {
282
+ snapshotCache = {};
283
+ }
284
+
285
+ module.exports = {
286
+ LOGIN_PROVIDER_IDS,
287
+ ACCOUNT_CHECK_PROVIDER_IDS,
288
+ clearOmpAuthProviderCache,
289
+ findAuthProviderForKey,
290
+ getOmpAuthProviderSnapshot,
291
+ normalizeProviderId,
292
+ resolveProviderAlias,
293
+ sanitizeIdentity
294
+ };