coding-tool-x 3.6.13 → 3.7.1

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 (88) hide show
  1. package/README.md +45 -33
  2. package/dist/web/assets/{Analytics-CnA75GxO.js → Analytics-Dpje9L84.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-D8XEscQY.js → ConfigTemplates-Cz6kasek.js} +1 -1
  4. package/dist/web/assets/Home-BwTaYu3q.js +1 -0
  5. package/dist/web/assets/Home-Jom_ZcdY.css +1 -0
  6. package/dist/web/assets/{PluginManager-BzKZ8TYu.js → PluginManager-DsN-xNXx.js} +1 -1
  7. package/dist/web/assets/ProjectList-4BCObimU.js +1 -0
  8. package/dist/web/assets/{ProjectList-DL4JK6ci.css → ProjectList-D3a0oRum.css} +1 -1
  9. package/dist/web/assets/SessionList-D4c8FRzq.js +1 -0
  10. package/dist/web/assets/{SkillManager-fxOvFTYD.js → SkillManager-C-KYbXB5.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-Ch2mM8jm.js → WorkspaceManager-B-BlPJHX.js} +1 -1
  12. package/dist/web/assets/{icons-QFXzEgqM.js → icons-Dop5j7Bf.js} +1 -1
  13. package/dist/web/assets/index-Btfvh0GG.js +2 -0
  14. package/dist/web/assets/index-CLNa6DhH.css +1 -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 +3 -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 +46 -6
  22. package/src/commands/port-config.js +83 -75
  23. package/src/commands/proxy-control.js +59 -26
  24. package/src/commands/stats.js +37 -19
  25. package/src/commands/switch.js +5 -1
  26. package/src/commands/toggle-proxy.js +131 -53
  27. package/src/commands/ui.js +3 -4
  28. package/src/commands/workspace.js +3 -35
  29. package/src/config/default.js +19 -2
  30. package/src/config/loader.js +5 -76
  31. package/src/config/paths.js +96 -11
  32. package/src/index.js +10 -16
  33. package/src/server/api/commands.js +1 -1
  34. package/src/server/api/config-export.js +4 -2
  35. package/src/server/api/config-registry.js +40 -34
  36. package/src/server/api/config.js +5 -1
  37. package/src/server/api/dashboard.js +22 -6
  38. package/src/server/api/mcp.js +5 -3
  39. package/src/server/api/omp-channels.js +349 -0
  40. package/src/server/api/omp-config.js +164 -0
  41. package/src/server/api/omp-projects.js +76 -0
  42. package/src/server/api/omp-proxy.js +154 -0
  43. package/src/server/api/omp-sessions.js +334 -0
  44. package/src/server/api/omp-statistics.js +40 -0
  45. package/src/server/api/plugins.js +1 -1
  46. package/src/server/api/prompts.js +3 -2
  47. package/src/server/api/settings.js +2 -0
  48. package/src/server/api/skills.js +1 -1
  49. package/src/server/api/workspaces.js +3 -41
  50. package/src/server/index.js +41 -0
  51. package/src/server/omp-proxy-server.js +66 -0
  52. package/src/server/services/agents-service.js +2 -1
  53. package/src/server/services/base/proxy-utils.js +1 -0
  54. package/src/server/services/channel-balance.js +151 -1
  55. package/src/server/services/channel-scheduler.js +7 -1
  56. package/src/server/services/commands-service.js +11 -2
  57. package/src/server/services/config-export-service.js +296 -39
  58. package/src/server/services/config-registry-service.js +20 -18
  59. package/src/server/services/config-sync-manager.js +115 -6
  60. package/src/server/services/config-sync-service.js +4 -2
  61. package/src/server/services/config-templates-service.js +183 -45
  62. package/src/server/services/mcp-service.js +217 -33
  63. package/src/server/services/notification-hooks.js +138 -6
  64. package/src/server/services/omp-auth-providers.js +294 -0
  65. package/src/server/services/omp-channels.js +94 -0
  66. package/src/server/services/omp-config.js +269 -0
  67. package/src/server/services/omp-sessions.js +490 -0
  68. package/src/server/services/omp-settings-manager.js +975 -0
  69. package/src/server/services/omp-statistics-service.js +317 -0
  70. package/src/server/services/plugins-service.js +673 -11
  71. package/src/server/services/prompts-service.js +112 -7
  72. package/src/server/services/proxy-log-helper.js +1 -0
  73. package/src/server/services/proxy-runtime.js +5 -1
  74. package/src/server/services/sessions.js +19 -35
  75. package/src/server/services/skill-service.js +13 -3
  76. package/src/server/services/speed-test.js +34 -28
  77. package/src/server/services/ui-config.js +50 -12
  78. package/src/server/services/workspace-service.js +117 -225
  79. package/src/server/websocket-server.js +7 -1
  80. package/src/shared/platforms.js +201 -0
  81. package/src/utils/session.js +4 -10
  82. package/dist/web/assets/Home-BiaKZMah.js +0 -1
  83. package/dist/web/assets/Home-CIua-b-o.css +0 -1
  84. package/dist/web/assets/ProjectList-BPfG_92X.js +0 -1
  85. package/dist/web/assets/SessionList-BIVtw00v.js +0 -1
  86. package/dist/web/assets/index-BpBFF3b2.js +0 -2
  87. package/dist/web/assets/index-Dsh4Q5Cg.css +0 -1
  88. package/src/utils/cli-flags.js +0 -7
@@ -0,0 +1,975 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+ const yaml = require('js-yaml');
5
+ const ompConfig = require('./omp-config');
6
+
7
+ const MANAGED_PROVIDER_PREFIX = 'ctx-';
8
+ const MODELS_BACKUP_MARKER = '.ctx-backup-';
9
+ const VISIBILITY_STATE_VERSION = 1;
10
+ const MANAGED_VISIBILITY_STATE_FILE = 'coding-tool-x-omp-managed-visibility.json';
11
+ const NO_MANAGED_MODELS_SELECTOR = `${MANAGED_PROVIDER_PREFIX}coding-tool-x/__no_models_configured__`;
12
+ const THINKING_EFFORT_LEVELS = ['minimal', 'low', 'medium', 'high', 'xhigh'];
13
+ const THINKING_MODES = new Set([
14
+ 'effort',
15
+ 'budget',
16
+ 'google-level',
17
+ 'anthropic-adaptive',
18
+ 'anthropic-budget-effort'
19
+ ]);
20
+
21
+ let lastManagedOmpSyncResult = null;
22
+
23
+ function getOmpPaths(...args) {
24
+ return ompConfig.getOmpPaths(...args);
25
+ }
26
+
27
+ function ensureOmpDir(...args) {
28
+ return ompConfig.ensureOmpDir(...args);
29
+ }
30
+
31
+ function normalizeProviderId(value = '') {
32
+ return String(value || '')
33
+ .trim()
34
+ .toLowerCase()
35
+ .replace(/[^a-z0-9_-]/g, '-')
36
+ .replace(/-+/g, '-')
37
+ .replace(/^-|-$/g, '')
38
+ || 'coding-tool-x';
39
+ }
40
+
41
+ function getManagedProviderId(channel = {}) {
42
+ const baseId = normalizeProviderId(channel.providerKey || channel.provider || channel.name || channel.id);
43
+ return baseId.startsWith(MANAGED_PROVIDER_PREFIX) ? baseId : `${MANAGED_PROVIDER_PREFIX}${baseId}`;
44
+ }
45
+
46
+ function isManagedProviderId(providerId = '') {
47
+ return String(providerId || '').startsWith(MANAGED_PROVIDER_PREFIX);
48
+ }
49
+
50
+ function normalizeProviderApi(value = '') {
51
+ const normalized = String(value || '').trim();
52
+ if (!normalized || normalized === 'openai' || normalized === 'chat' || normalized === 'chat.completions') {
53
+ return 'openai-completions';
54
+ }
55
+ if (normalized === 'responses') {
56
+ return 'openai-responses';
57
+ }
58
+ return normalized;
59
+ }
60
+
61
+ function defaultCost() {
62
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
63
+ }
64
+
65
+ function normalizeCost(value) {
66
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
67
+ return defaultCost();
68
+ }
69
+ const numberOrZero = (item) => {
70
+ const number = Number(item);
71
+ return Number.isFinite(number) ? number : 0;
72
+ };
73
+ return {
74
+ input: numberOrZero(value.input),
75
+ output: numberOrZero(value.output),
76
+ cacheRead: numberOrZero(value.cacheRead ?? value.cache_read),
77
+ cacheWrite: numberOrZero(value.cacheWrite ?? value.cache_write)
78
+ };
79
+ }
80
+
81
+ function normalizePositiveNumber(value, fallback) {
82
+ const number = Number(value);
83
+ return Number.isFinite(number) && number > 0 ? number : fallback;
84
+ }
85
+
86
+ function normalizeModelInput(value) {
87
+ if (!Array.isArray(value)) return ['text', 'image'];
88
+ const filtered = value.filter(item => item === 'text' || item === 'image');
89
+ return filtered.length > 0 ? filtered : ['text', 'image'];
90
+ }
91
+
92
+ function normalizeThinkingEfforts(value) {
93
+ if (!Array.isArray(value)) return undefined;
94
+ const seen = new Set();
95
+ const filtered = value
96
+ .map(item => String(item || '').trim())
97
+ .filter(item => THINKING_EFFORT_LEVELS.includes(item))
98
+ .filter((item) => {
99
+ if (seen.has(item)) return false;
100
+ seen.add(item);
101
+ return true;
102
+ });
103
+ return filtered.length > 0 ? filtered : undefined;
104
+ }
105
+
106
+ function normalizeThinking(value) {
107
+ if (Array.isArray(value)) {
108
+ const efforts = normalizeThinkingEfforts(value);
109
+ return efforts ? { mode: 'effort', efforts } : undefined;
110
+ }
111
+ if (typeof value === 'string') {
112
+ const trimmed = value.trim();
113
+ if (THINKING_EFFORT_LEVELS.includes(trimmed)) {
114
+ return { mode: 'effort', efforts: [trimmed] };
115
+ }
116
+ return undefined;
117
+ }
118
+ if (value && typeof value === 'object') {
119
+ const mode = String(value.mode || '').trim();
120
+ if (!THINKING_MODES.has(mode)) return undefined;
121
+ const efforts = normalizeThinkingEfforts(value.efforts || value.levels);
122
+ const minLevel = String(value.minLevel || '').trim();
123
+ const maxLevel = String(value.maxLevel || '').trim();
124
+ const defaultLevel = String(value.defaultLevel || '').trim();
125
+ const hasRange = THINKING_EFFORT_LEVELS.includes(minLevel) && THINKING_EFFORT_LEVELS.includes(maxLevel);
126
+ if (!efforts && !hasRange) return undefined;
127
+ return compactObject({
128
+ mode,
129
+ efforts,
130
+ defaultLevel: THINKING_EFFORT_LEVELS.includes(defaultLevel) ? defaultLevel : undefined,
131
+ minLevel: efforts ? undefined : minLevel,
132
+ maxLevel: efforts ? undefined : maxLevel,
133
+ effortMap: value.effortMap && typeof value.effortMap === 'object' && !Array.isArray(value.effortMap)
134
+ ? value.effortMap
135
+ : undefined,
136
+ supportsDisplay: typeof value.supportsDisplay === 'boolean' ? value.supportsDisplay : undefined
137
+ });
138
+ }
139
+ return undefined;
140
+ }
141
+
142
+ function compactObject(value = {}) {
143
+ const next = { ...value };
144
+ Object.keys(next).forEach((key) => {
145
+ if (next[key] === undefined) delete next[key];
146
+ });
147
+ return next;
148
+ }
149
+
150
+ function uniqueStrings(values = []) {
151
+ const seen = new Set();
152
+ const result = [];
153
+ (Array.isArray(values) ? values : []).forEach((value) => {
154
+ const item = String(value || '').trim();
155
+ if (!item || seen.has(item)) return;
156
+ seen.add(item);
157
+ result.push(item);
158
+ });
159
+ return result;
160
+ }
161
+
162
+ function pushUnique(values, value) {
163
+ const item = String(value || '').trim();
164
+ if (item && !values.includes(item)) {
165
+ values.push(item);
166
+ }
167
+ }
168
+
169
+ function sameStringSet(left = [], right = []) {
170
+ const leftSet = new Set(uniqueStrings(left));
171
+ const rightSet = new Set(uniqueStrings(right));
172
+ if (leftSet.size !== rightSet.size) return false;
173
+ for (const item of leftSet) {
174
+ if (!rightSet.has(item)) return false;
175
+ }
176
+ return true;
177
+ }
178
+
179
+ function getOwnProperty(object, key) {
180
+ return object && typeof object === 'object' && !Array.isArray(object)
181
+ ? Object.prototype.hasOwnProperty.call(object, key)
182
+ : false;
183
+ }
184
+
185
+ function mergeDefined(base = {}, override = {}) {
186
+ const next = { ...base };
187
+ Object.entries(override || {}).forEach(([key, value]) => {
188
+ if (value !== undefined) {
189
+ next[key] = value;
190
+ }
191
+ });
192
+ return next;
193
+ }
194
+
195
+ function buildModelEntry(model = {}, fallbackId = '') {
196
+ const id = String(model.id || model.name || fallbackId || '').trim();
197
+ if (!id) return null;
198
+ return compactObject({
199
+ id,
200
+ name: model.name || id,
201
+ api: model.api ? normalizeProviderApi(model.api) : undefined,
202
+ reasoning: model.reasoning === true,
203
+ thinking: normalizeThinking(model.thinking),
204
+ input: normalizeModelInput(model.input),
205
+ cost: normalizeCost(model.cost),
206
+ contextWindow: normalizePositiveNumber(model.contextWindow || model.context_window, 128000),
207
+ maxTokens: normalizePositiveNumber(model.maxTokens || model.max_tokens, 4096),
208
+ headers: model.headers && typeof model.headers === 'object' && !Array.isArray(model.headers)
209
+ ? model.headers
210
+ : undefined,
211
+ compat: model.compat && typeof model.compat === 'object' && !Array.isArray(model.compat)
212
+ ? model.compat
213
+ : undefined
214
+ });
215
+ }
216
+
217
+ function normalizeModels(channel = {}, options = {}) {
218
+ const models = [];
219
+ const catalogIndex = options.catalogIndex instanceof Map ? options.catalogIndex : new Map();
220
+ const add = (value) => {
221
+ const id = String(value || '').trim();
222
+ if (!id || models.some(model => model.id === id)) return;
223
+ const catalogModel = catalogIndex.get(id.toLowerCase()) || {};
224
+ const entry = buildModelEntry({ ...catalogModel, id }, id);
225
+ if (entry) models.push(entry);
226
+ };
227
+
228
+ if (Array.isArray(channel.models)) {
229
+ channel.models.forEach((model) => {
230
+ if (typeof model === 'string') {
231
+ add(model);
232
+ return;
233
+ }
234
+ if (!model || typeof model !== 'object') return;
235
+ const id = String(model.id || model.name || '').trim();
236
+ if (!id || models.some(item => item.id === id)) return;
237
+ const catalogModel = catalogIndex.get(id.toLowerCase()) || {};
238
+ const entry = buildModelEntry(mergeDefined(catalogModel, model), id);
239
+ if (entry) models.push(entry);
240
+ });
241
+ }
242
+
243
+ add(channel.model);
244
+ add(channel.speedTestModel);
245
+ if (Array.isArray(channel.allowedModels)) {
246
+ channel.allowedModels.forEach(add);
247
+ }
248
+
249
+ return models.map(compactObject);
250
+ }
251
+
252
+ function readOmpCatalogModels(command, providerId, options = {}) {
253
+ const runner = options.catalogRunner || options.modelsRunner || spawnSync;
254
+ const result = runner(command, ['models', providerId, '--json'], {
255
+ encoding: 'utf8',
256
+ env: {
257
+ ...process.env,
258
+ ...(options.env || {})
259
+ },
260
+ timeout: options.catalogTimeout || 5000
261
+ });
262
+ const status = result?.status === undefined || result?.status === null ? 0 : result.status;
263
+ if (result?.error || status !== 0) return [];
264
+ try {
265
+ const parsed = JSON.parse(normalizeCommandOutput(result?.stdout).trim() || '{}');
266
+ return Array.isArray(parsed.models) ? parsed.models : [];
267
+ } catch {
268
+ return [];
269
+ }
270
+ }
271
+
272
+ function buildCatalogIndex(channel = {}, options = {}) {
273
+ const runtime = options.runtime
274
+ || (typeof ompConfig.resolveOmpRuntime === 'function'
275
+ ? ompConfig.resolveOmpRuntime(options.env || process.env, options.runtimeOptions || {})
276
+ : null);
277
+ if (!runtime || runtime.runtime !== 'omp' || !runtime.installed) {
278
+ return new Map();
279
+ }
280
+
281
+ const rawProviderId = normalizeProviderId(channel.providerKey || channel.provider || channel.name || '');
282
+ const providerId = rawProviderId.startsWith(MANAGED_PROVIDER_PREFIX)
283
+ ? rawProviderId.slice(MANAGED_PROVIDER_PREFIX.length)
284
+ : rawProviderId;
285
+ if (!providerId) return new Map();
286
+
287
+ const index = new Map();
288
+ readOmpCatalogModels(runtime.command, providerId, options).forEach((model) => {
289
+ const id = String(model?.id || model?.name || '').trim();
290
+ if (id) {
291
+ index.set(id.toLowerCase(), model);
292
+ }
293
+ });
294
+ return index;
295
+ }
296
+
297
+ function buildProviderEntry(channel = {}, options = {}) {
298
+ const catalogIndex = options.catalogIndex instanceof Map
299
+ ? options.catalogIndex
300
+ : buildCatalogIndex(channel, options);
301
+ const entry = {
302
+ baseUrl: String(channel.baseUrl || '').trim(),
303
+ api: normalizeProviderApi(channel.providerApi || channel.api || channel.wireApi),
304
+ models: normalizeModels(channel, { catalogIndex })
305
+ };
306
+
307
+ const apiKey = String(channel.apiKey || '').trim();
308
+ if (apiKey) {
309
+ entry.apiKey = apiKey;
310
+ }
311
+
312
+ if (channel.headers && typeof channel.headers === 'object' && !Array.isArray(channel.headers)) {
313
+ entry.headers = channel.headers;
314
+ }
315
+
316
+ return entry;
317
+ }
318
+
319
+ function readModelsConfig(filePath = getOmpPaths().modelsYml) {
320
+ try {
321
+ if (!fs.existsSync(filePath)) {
322
+ return { providers: {} };
323
+ }
324
+ const raw = fs.readFileSync(filePath, 'utf8');
325
+ if (!raw.trim()) {
326
+ return { providers: {} };
327
+ }
328
+ const parsed = yaml.load(raw);
329
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
330
+ return { providers: {} };
331
+ }
332
+ if (!parsed.providers || typeof parsed.providers !== 'object' || Array.isArray(parsed.providers)) {
333
+ parsed.providers = {};
334
+ }
335
+ return parsed;
336
+ } catch (error) {
337
+ throw new Error(`Failed to read OMP models.yml: ${error.message}`);
338
+ }
339
+ }
340
+
341
+ function writeModelsConfig(config, filePath = getOmpPaths().modelsYml) {
342
+ ensureOmpDir(path.dirname(filePath));
343
+ const doc = yaml.dump(config || { providers: {} }, {
344
+ lineWidth: 120,
345
+ noRefs: true,
346
+ sortKeys: false
347
+ });
348
+ fs.writeFileSync(filePath, doc, 'utf8');
349
+ }
350
+
351
+ function getOmpSettingsPath(paths = getOmpPaths()) {
352
+ if (paths.settings || paths.config) {
353
+ return paths.settings || paths.config;
354
+ }
355
+ const baseDir = paths.agentDir
356
+ || path.dirname(paths.modelsYml || paths.models || process.cwd());
357
+ return path.join(baseDir, 'config.yml');
358
+ }
359
+
360
+ function readOmpSettingsConfig(filePath = getOmpSettingsPath()) {
361
+ try {
362
+ if (!filePath || !fs.existsSync(filePath)) {
363
+ return {};
364
+ }
365
+ const raw = fs.readFileSync(filePath, 'utf8');
366
+ if (!raw.trim()) {
367
+ return {};
368
+ }
369
+ const parsed = yaml.load(raw);
370
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
371
+ return {};
372
+ }
373
+ return parsed;
374
+ } catch (error) {
375
+ throw new Error(`Failed to read OMP config.yml: ${error.message}`);
376
+ }
377
+ }
378
+
379
+ function writeOmpSettingsConfig(config, filePath = getOmpSettingsPath()) {
380
+ ensureOmpDir(path.dirname(filePath));
381
+ const doc = yaml.dump(config || {}, {
382
+ lineWidth: 120,
383
+ noRefs: true,
384
+ sortKeys: false
385
+ });
386
+ fs.writeFileSync(filePath, doc, 'utf8');
387
+ }
388
+
389
+ function getModelsBackupPrefix(filePath) {
390
+ return `${path.basename(filePath)}${MODELS_BACKUP_MARKER}`;
391
+ }
392
+
393
+ function hasExistingModelsBackup(filePath) {
394
+ const dirPath = path.dirname(filePath);
395
+ if (!fs.existsSync(dirPath)) return false;
396
+ const prefix = getModelsBackupPrefix(filePath);
397
+ return fs.readdirSync(dirPath).some(entry => entry.startsWith(prefix));
398
+ }
399
+
400
+ function formatBackupTimestamp(date = new Date()) {
401
+ return date.toISOString().replace(/[:.]/g, '-');
402
+ }
403
+
404
+ function createModelsBackupIfNeeded(filePath, options = {}) {
405
+ if (options.backup === false || !fs.existsSync(filePath) || hasExistingModelsBackup(filePath)) {
406
+ return null;
407
+ }
408
+
409
+ const dirPath = path.dirname(filePath);
410
+ const prefix = getModelsBackupPrefix(filePath);
411
+ const timestamp = formatBackupTimestamp(options.now instanceof Date ? options.now : new Date());
412
+ let backupPath = path.join(dirPath, `${prefix}${timestamp}`);
413
+ let counter = 1;
414
+ while (fs.existsSync(backupPath)) {
415
+ backupPath = path.join(dirPath, `${prefix}${timestamp}-${counter}`);
416
+ counter += 1;
417
+ }
418
+ fs.copyFileSync(filePath, backupPath);
419
+ return backupPath;
420
+ }
421
+
422
+ function getManagedVisibilityStatePath(paths = getOmpPaths()) {
423
+ if (paths.managedVisibilityState) {
424
+ return paths.managedVisibilityState;
425
+ }
426
+ const baseDir = paths.agentDir
427
+ || path.dirname(paths.settings || paths.config || paths.modelsYml || paths.models || process.cwd());
428
+ return path.join(baseDir, MANAGED_VISIBILITY_STATE_FILE);
429
+ }
430
+
431
+ function readManagedVisibilityState(paths = getOmpPaths()) {
432
+ const filePath = getManagedVisibilityStatePath(paths);
433
+ try {
434
+ if (!fs.existsSync(filePath)) return null;
435
+ const raw = fs.readFileSync(filePath, 'utf8');
436
+ if (!raw.trim()) return null;
437
+ const parsed = JSON.parse(raw);
438
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
439
+ } catch {
440
+ return null;
441
+ }
442
+ }
443
+
444
+ function writeManagedVisibilityState(state, paths = getOmpPaths()) {
445
+ const filePath = getManagedVisibilityStatePath(paths);
446
+ ensureOmpDir(path.dirname(filePath));
447
+ fs.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
448
+ return filePath;
449
+ }
450
+
451
+ function removeManagedVisibilityState(paths = getOmpPaths()) {
452
+ const filePath = getManagedVisibilityStatePath(paths);
453
+ if (fs.existsSync(filePath)) {
454
+ fs.unlinkSync(filePath);
455
+ return true;
456
+ }
457
+ return false;
458
+ }
459
+
460
+ function normalizeCommandOutput(value) {
461
+ if (value === undefined || value === null) return '';
462
+ return Buffer.isBuffer(value) ? value.toString('utf8') : String(value);
463
+ }
464
+
465
+ function pushUniqueWarning(warnings, value) {
466
+ const warning = String(value || '').trim();
467
+ if (warning && !warnings.includes(warning)) {
468
+ warnings.push(warning);
469
+ }
470
+ }
471
+
472
+ function collectValidationWarnings(stdout, stderr) {
473
+ const warnings = [];
474
+ normalizeCommandOutput(stderr)
475
+ .split(/\r?\n/)
476
+ .map(line => line.trim())
477
+ .filter(Boolean)
478
+ .forEach(line => pushUniqueWarning(warnings, line));
479
+
480
+ const rawStdout = normalizeCommandOutput(stdout).trim();
481
+ if (rawStdout) {
482
+ try {
483
+ const parsed = JSON.parse(rawStdout);
484
+ if (Array.isArray(parsed?.warnings)) {
485
+ parsed.warnings.forEach(warning => pushUniqueWarning(warnings, warning));
486
+ }
487
+ } catch {
488
+ // OMP's JSON payload is informational here; stderr carries schema warnings.
489
+ }
490
+ }
491
+ return warnings;
492
+ }
493
+
494
+ function runModelsJson(command, env, options = {}) {
495
+ const runner = options.modelsRunner || spawnSync;
496
+ const result = runner(command, ['models', '--json'], {
497
+ encoding: 'utf8',
498
+ env: {
499
+ ...process.env,
500
+ ...env
501
+ },
502
+ timeout: options.timeout || 5000
503
+ });
504
+ if (typeof result === 'string' || Buffer.isBuffer(result)) {
505
+ return { status: 0, stdout: result, stderr: '' };
506
+ }
507
+ return result || { status: 0, stdout: '', stderr: '' };
508
+ }
509
+
510
+ function collectVisibleProviderIds(command, env, options = {}) {
511
+ const runner = options.visibilityModelsRunner || options.modelsRunner || spawnSync;
512
+ const result = runner(command, ['models', '--json'], {
513
+ encoding: 'utf8',
514
+ env: {
515
+ ...process.env,
516
+ ...env
517
+ },
518
+ timeout: options.visibilityTimeout || options.timeout || 5000
519
+ });
520
+ const status = result?.status === undefined || result?.status === null ? 0 : result.status;
521
+ if (result?.error || status !== 0) return [];
522
+ try {
523
+ const parsed = JSON.parse(normalizeCommandOutput(result?.stdout).trim() || '{}');
524
+ const models = Array.isArray(parsed.models) ? parsed.models : [];
525
+ return uniqueStrings(models.map(model => model?.provider));
526
+ } catch {
527
+ return [];
528
+ }
529
+ }
530
+
531
+ function validateOmpModelsConfig(options = {}) {
532
+ const env = options.env || process.env;
533
+ const runtime = options.runtime
534
+ || (typeof ompConfig.resolveOmpRuntime === 'function'
535
+ ? ompConfig.resolveOmpRuntime(env, options.runtimeOptions || {})
536
+ : null);
537
+
538
+ if (!runtime || runtime.runtime !== 'omp' || !runtime.installed) {
539
+ return {
540
+ skipped: true,
541
+ reason: 'omp-not-available',
542
+ command: runtime?.command || 'omp',
543
+ warnings: []
544
+ };
545
+ }
546
+
547
+ const result = runModelsJson(runtime.command, env, options);
548
+ if (result.error) {
549
+ throw new Error(`Failed to validate OMP models.yml with "${runtime.command} models --json": ${result.error.message}`);
550
+ }
551
+
552
+ const status = result.status === undefined || result.status === null ? 0 : result.status;
553
+ const stdout = normalizeCommandOutput(result.stdout);
554
+ const stderr = normalizeCommandOutput(result.stderr);
555
+ const warnings = collectValidationWarnings(stdout, stderr);
556
+ if (status !== 0) {
557
+ const details = stderr.trim() || stdout.trim() || `exit code ${status}`;
558
+ throw new Error(`OMP models.yml validation failed: ${details}`);
559
+ }
560
+
561
+ return {
562
+ skipped: false,
563
+ command: runtime.command,
564
+ warnings,
565
+ validatedAt: new Date().toISOString()
566
+ };
567
+ }
568
+
569
+ function recordManagedOmpSyncResult(result) {
570
+ lastManagedOmpSyncResult = {
571
+ success: true,
572
+ warnings: [],
573
+ ...result
574
+ };
575
+ return lastManagedOmpSyncResult;
576
+ }
577
+
578
+ function getLastManagedOmpSyncResult() {
579
+ return lastManagedOmpSyncResult
580
+ ? {
581
+ ...lastManagedOmpSyncResult,
582
+ warnings: [...(lastManagedOmpSyncResult.warnings || [])],
583
+ validation: lastManagedOmpSyncResult.validation
584
+ ? {
585
+ ...lastManagedOmpSyncResult.validation,
586
+ warnings: [...(lastManagedOmpSyncResult.validation.warnings || [])]
587
+ }
588
+ : lastManagedOmpSyncResult.validation
589
+ }
590
+ : null;
591
+ }
592
+
593
+ function removeLegacyManagedExtension(paths = getOmpPaths()) {
594
+ const target = paths.managedProviderExtension;
595
+ if (target && fs.existsSync(target)) {
596
+ fs.unlinkSync(target);
597
+ }
598
+ }
599
+
600
+ function pruneManagedProviders(providers = {}) {
601
+ Object.keys(providers).forEach((providerId) => {
602
+ if (isManagedProviderId(providerId)) {
603
+ delete providers[providerId];
604
+ }
605
+ });
606
+ }
607
+
608
+ function buildManagedModelsConfig(channels = [], baseConfig = readModelsConfig(), options = {}) {
609
+ const next = {
610
+ ...baseConfig,
611
+ providers: {
612
+ ...(baseConfig.providers || {})
613
+ }
614
+ };
615
+ pruneManagedProviders(next.providers);
616
+
617
+ channels
618
+ .filter(channel => channel && channel.enabled !== false && channel.baseUrl)
619
+ .forEach((channel) => {
620
+ next.providers[getManagedProviderId(channel)] = buildProviderEntry(channel, options);
621
+ });
622
+
623
+ return next;
624
+ }
625
+
626
+ function getOriginalProviderId(channel = {}) {
627
+ const providerId = normalizeProviderId(channel.providerKey || channel.provider || channel.name || channel.id);
628
+ return isManagedProviderId(providerId)
629
+ ? providerId.slice(MANAGED_PROVIDER_PREFIX.length)
630
+ : providerId;
631
+ }
632
+
633
+ function collectManagedVisibility(channels = [], modelsConfig = { providers: {} }, options = {}) {
634
+ const managedEnabledModels = [];
635
+ const managedDisabledProviders = [];
636
+ const warnings = [];
637
+ const providers = modelsConfig.providers || {};
638
+
639
+ (channels || [])
640
+ .filter(channel => channel && channel.enabled !== false && channel.baseUrl)
641
+ .forEach((channel) => {
642
+ const managedProviderId = getManagedProviderId(channel);
643
+ const providerEntry = providers[managedProviderId] || {};
644
+ const models = Array.isArray(providerEntry.models) ? providerEntry.models : [];
645
+ models.forEach((model) => {
646
+ const modelId = String(model?.id || model?.name || '').trim();
647
+ if (modelId) {
648
+ pushUnique(managedEnabledModels, `${managedProviderId}/${modelId}`);
649
+ }
650
+ });
651
+
652
+ const originalProviderId = getOriginalProviderId(channel);
653
+ if (originalProviderId && !isManagedProviderId(originalProviderId)) {
654
+ pushUnique(managedDisabledProviders, originalProviderId);
655
+ }
656
+ if (models.length === 0) {
657
+ warnings.push(`OMP channel "${channel.name || channel.id || managedProviderId}" has no configured models; OMP model scope will resolve to no usable managed models.`);
658
+ }
659
+ });
660
+
661
+ const env = options.env || process.env;
662
+ const runtime = options.runtime
663
+ || (typeof ompConfig.resolveOmpRuntime === 'function'
664
+ ? ompConfig.resolveOmpRuntime(env, options.runtimeOptions || {})
665
+ : null);
666
+ if (runtime && runtime.runtime === 'omp' && runtime.installed && options.discoverDisabledProviders !== false) {
667
+ collectVisibleProviderIds(runtime.command, env, options)
668
+ .filter(providerId => providerId && !isManagedProviderId(providerId))
669
+ .forEach(providerId => pushUnique(managedDisabledProviders, providerId));
670
+ }
671
+
672
+ return {
673
+ managedEnabledModels,
674
+ managedEnabledModelsForSettings: managedEnabledModels.length > 0
675
+ ? managedEnabledModels
676
+ : [NO_MANAGED_MODELS_SELECTOR],
677
+ managedDisabledProviders,
678
+ managedDefaultModel: managedEnabledModels[0] || null,
679
+ warnings
680
+ };
681
+ }
682
+
683
+ function captureOriginalVisibility(settings = {}) {
684
+ const modelRoles = settings.modelRoles && typeof settings.modelRoles === 'object' && !Array.isArray(settings.modelRoles)
685
+ ? settings.modelRoles
686
+ : {};
687
+ return {
688
+ enabledModels: getOwnProperty(settings, 'enabledModels') ? uniqueStrings(settings.enabledModels) : null,
689
+ disabledProviders: getOwnProperty(settings, 'disabledProviders') ? uniqueStrings(settings.disabledProviders) : null,
690
+ modelRolesHadDefault: getOwnProperty(modelRoles, 'default'),
691
+ modelRolesDefault: getOwnProperty(modelRoles, 'default') ? String(modelRoles.default || '') : null
692
+ };
693
+ }
694
+
695
+ function collectUserDisabledProviders(settings = {}, previousState = null) {
696
+ const currentDisabled = uniqueStrings(settings.disabledProviders);
697
+ if (!previousState?.active || !previousState.original) {
698
+ return currentDisabled;
699
+ }
700
+ const originalDisabled = Array.isArray(previousState.original.disabledProviders)
701
+ ? previousState.original.disabledProviders
702
+ : [];
703
+ const previousManagedDisabled = uniqueStrings(previousState.managedDisabledProviders);
704
+ const userAddedDuringManagedMode = currentDisabled
705
+ .filter(providerId => !previousManagedDisabled.includes(providerId) && !originalDisabled.includes(providerId));
706
+ return uniqueStrings([...originalDisabled, ...userAddedDuringManagedMode]);
707
+ }
708
+
709
+ function applyManagedModelRole(settings, visibility, previousState = null) {
710
+ const roles = settings.modelRoles && typeof settings.modelRoles === 'object' && !Array.isArray(settings.modelRoles)
711
+ ? { ...settings.modelRoles }
712
+ : {};
713
+ const currentDefault = String(roles.default || '').trim();
714
+ const previousManagedDefault = String(previousState?.managedDefaultModel || '').trim();
715
+ if (!visibility.managedDefaultModel) {
716
+ return roles;
717
+ }
718
+ const defaultIsManaged = visibility.managedEnabledModels.includes(currentDefault);
719
+ if (!currentDefault || !defaultIsManaged || currentDefault === previousManagedDefault) {
720
+ roles.default = visibility.managedDefaultModel;
721
+ }
722
+ return roles;
723
+ }
724
+
725
+ function syncManagedOmpVisibility(channels = [], modelsConfig = { providers: {} }, options = {}) {
726
+ const paths = getOmpPaths();
727
+ const settingsPath = getOmpSettingsPath(paths);
728
+ const statePath = getManagedVisibilityStatePath(paths);
729
+ const settings = readOmpSettingsConfig(settingsPath);
730
+ const previousState = readManagedVisibilityState(paths);
731
+ const visibility = collectManagedVisibility(channels, modelsConfig, options);
732
+ const original = previousState?.active && previousState.original
733
+ ? previousState.original
734
+ : captureOriginalVisibility(settings);
735
+ const before = JSON.stringify(settings);
736
+ const settingsBackupPath = createModelsBackupIfNeeded(settingsPath, options);
737
+
738
+ settings.enabledModels = visibility.managedEnabledModelsForSettings;
739
+ settings.disabledProviders = uniqueStrings([
740
+ ...collectUserDisabledProviders(settings, previousState),
741
+ ...visibility.managedDisabledProviders
742
+ ]);
743
+ settings.modelRoles = applyManagedModelRole(settings, visibility, previousState);
744
+
745
+ const after = JSON.stringify(settings);
746
+ if (before !== after) {
747
+ writeOmpSettingsConfig(settings, settingsPath);
748
+ }
749
+
750
+ const state = {
751
+ version: VISIBILITY_STATE_VERSION,
752
+ active: true,
753
+ settingsPath,
754
+ managedEnabledModels: visibility.managedEnabledModels,
755
+ managedEnabledModelsForSettings: visibility.managedEnabledModelsForSettings,
756
+ managedDisabledProviders: visibility.managedDisabledProviders,
757
+ managedDefaultModel: visibility.managedDefaultModel,
758
+ original,
759
+ updatedAt: new Date().toISOString()
760
+ };
761
+ writeManagedVisibilityState(state, paths);
762
+
763
+ return {
764
+ settingsPath,
765
+ statePath,
766
+ settingsBackupPath,
767
+ changed: before !== after,
768
+ managedEnabledModels: visibility.managedEnabledModels,
769
+ managedEnabledModelsForSettings: visibility.managedEnabledModelsForSettings,
770
+ managedDisabledProviders: visibility.managedDisabledProviders,
771
+ managedDefaultModel: visibility.managedDefaultModel,
772
+ warnings: visibility.warnings
773
+ };
774
+ }
775
+
776
+ function restoreOriginalList(settings, key, originalValue) {
777
+ if (Array.isArray(originalValue)) {
778
+ settings[key] = uniqueStrings(originalValue);
779
+ return;
780
+ }
781
+ delete settings[key];
782
+ }
783
+
784
+ function removeManagedOmpVisibility(options = {}) {
785
+ const paths = getOmpPaths();
786
+ const settingsPath = getOmpSettingsPath(paths);
787
+ const state = readManagedVisibilityState(paths);
788
+ const settings = readOmpSettingsConfig(settingsPath);
789
+ const before = JSON.stringify(settings);
790
+ const warnings = [];
791
+ let settingsBackupPath = null;
792
+
793
+ if (state?.active) {
794
+ settingsBackupPath = createModelsBackupIfNeeded(settingsPath, options);
795
+ const managedEnabled = uniqueStrings(state.managedEnabledModelsForSettings || state.managedEnabledModels);
796
+ const currentEnabled = uniqueStrings(settings.enabledModels);
797
+ if (sameStringSet(currentEnabled, managedEnabled)) {
798
+ restoreOriginalList(settings, 'enabledModels', state.original?.enabledModels);
799
+ } else if (managedEnabled.length > 0) {
800
+ const nextEnabled = currentEnabled.filter(model => !managedEnabled.includes(model));
801
+ if (nextEnabled.length > 0) {
802
+ settings.enabledModels = nextEnabled;
803
+ } else {
804
+ restoreOriginalList(settings, 'enabledModels', state.original?.enabledModels);
805
+ }
806
+ warnings.push('OMP enabledModels was changed while coding-tool-x managed visibility was active; preserved non-managed entries during cleanup.');
807
+ }
808
+
809
+ const managedDisabled = uniqueStrings(state.managedDisabledProviders);
810
+ const originalDisabled = Array.isArray(state.original?.disabledProviders)
811
+ ? state.original.disabledProviders
812
+ : [];
813
+ const currentDisabled = uniqueStrings(settings.disabledProviders);
814
+ const expectedDisabled = uniqueStrings([...originalDisabled, ...managedDisabled]);
815
+ if (sameStringSet(currentDisabled, expectedDisabled)) {
816
+ restoreOriginalList(settings, 'disabledProviders', state.original?.disabledProviders);
817
+ } else if (managedDisabled.length > 0) {
818
+ const nextDisabled = currentDisabled.filter(providerId => !managedDisabled.includes(providerId));
819
+ if (nextDisabled.length > 0) {
820
+ settings.disabledProviders = nextDisabled;
821
+ } else {
822
+ restoreOriginalList(settings, 'disabledProviders', state.original?.disabledProviders);
823
+ }
824
+ warnings.push('OMP disabledProviders was changed while coding-tool-x managed visibility was active; removed only managed provider entries.');
825
+ }
826
+
827
+ if (settings.modelRoles && typeof settings.modelRoles === 'object' && !Array.isArray(settings.modelRoles)) {
828
+ const roles = { ...settings.modelRoles };
829
+ if (roles.default === state.managedDefaultModel) {
830
+ if (state.original?.modelRolesHadDefault) {
831
+ roles.default = state.original.modelRolesDefault;
832
+ } else {
833
+ delete roles.default;
834
+ }
835
+ settings.modelRoles = roles;
836
+ }
837
+ if (Object.keys(settings.modelRoles).length === 0) {
838
+ delete settings.modelRoles;
839
+ }
840
+ }
841
+ } else {
842
+ const managedEnabled = uniqueStrings(settings.enabledModels)
843
+ .filter(model => isManagedProviderId(String(model).split('/')[0]));
844
+ if (managedEnabled.length > 0) {
845
+ settingsBackupPath = createModelsBackupIfNeeded(settingsPath, options);
846
+ settings.enabledModels = uniqueStrings(settings.enabledModels)
847
+ .filter(model => !managedEnabled.includes(model));
848
+ if (settings.enabledModels.length === 0) {
849
+ delete settings.enabledModels;
850
+ }
851
+ warnings.push('Removed stale coding-tool-x managed OMP enabledModels entries without a visibility state file.');
852
+ }
853
+ }
854
+
855
+ const after = JSON.stringify(settings);
856
+ if (before !== after) {
857
+ writeOmpSettingsConfig(settings, settingsPath);
858
+ }
859
+ const stateRemoved = removeManagedVisibilityState(paths);
860
+
861
+ return {
862
+ settingsPath,
863
+ statePath: getManagedVisibilityStatePath(paths),
864
+ settingsBackupPath,
865
+ changed: before !== after || stateRemoved,
866
+ stateRemoved,
867
+ warnings
868
+ };
869
+ }
870
+
871
+ function writeManagedOmpProviders(channels = [], options = {}) {
872
+ const paths = getOmpPaths();
873
+ const config = buildManagedModelsConfig(channels, readModelsConfig(paths.modelsYml), options);
874
+ const backupPath = createModelsBackupIfNeeded(paths.modelsYml, options);
875
+ writeModelsConfig(config, paths.modelsYml);
876
+ const visibility = syncManagedOmpVisibility(channels, config, options);
877
+ const validation = validateOmpModelsConfig(options);
878
+ const warnings = [
879
+ ...(visibility.warnings || []),
880
+ ...(validation.warnings || [])
881
+ ];
882
+ recordManagedOmpSyncResult({
883
+ path: paths.modelsYml,
884
+ modelsPath: paths.modelsYml,
885
+ settingsPath: visibility.settingsPath,
886
+ statePath: visibility.statePath,
887
+ backupPath,
888
+ modelsBackupPath: backupPath,
889
+ settingsBackupPath: visibility.settingsBackupPath,
890
+ changed: true,
891
+ visibilityChanged: visibility.changed,
892
+ managedEnabledModels: visibility.managedEnabledModels,
893
+ managedEnabledModelsForSettings: visibility.managedEnabledModelsForSettings,
894
+ managedDisabledProviders: visibility.managedDisabledProviders,
895
+ managedDefaultModel: visibility.managedDefaultModel,
896
+ validation,
897
+ warnings
898
+ });
899
+ removeLegacyManagedExtension(paths);
900
+ return paths.modelsYml;
901
+ }
902
+
903
+ function removeManagedOmpProviders(options = {}) {
904
+ const paths = getOmpPaths();
905
+ const config = readModelsConfig(paths.modelsYml);
906
+ const visibility = removeManagedOmpVisibility(options);
907
+ const before = Object.keys(config.providers || {}).length;
908
+ pruneManagedProviders(config.providers);
909
+ const after = Object.keys(config.providers || {}).length;
910
+ let backupPath = null;
911
+ let validation = {
912
+ skipped: true,
913
+ reason: before === after ? 'no-managed-providers' : 'not-run',
914
+ warnings: []
915
+ };
916
+ if (before !== after) {
917
+ backupPath = createModelsBackupIfNeeded(paths.modelsYml, options);
918
+ writeModelsConfig(config, paths.modelsYml);
919
+ validation = validateOmpModelsConfig(options);
920
+ }
921
+ recordManagedOmpSyncResult({
922
+ path: paths.modelsYml,
923
+ modelsPath: paths.modelsYml,
924
+ settingsPath: visibility.settingsPath,
925
+ statePath: visibility.statePath,
926
+ backupPath,
927
+ modelsBackupPath: backupPath,
928
+ settingsBackupPath: visibility.settingsBackupPath,
929
+ changed: before !== after,
930
+ visibilityChanged: visibility.changed,
931
+ stateRemoved: visibility.stateRemoved,
932
+ validation,
933
+ warnings: [
934
+ ...(visibility.warnings || []),
935
+ ...(validation.warnings || [])
936
+ ]
937
+ });
938
+ removeLegacyManagedExtension(paths);
939
+ }
940
+
941
+ function isManagedOmpProvidersActive() {
942
+ const paths = getOmpPaths();
943
+ if (!fs.existsSync(paths.modelsYml)) {
944
+ return false;
945
+ }
946
+ const config = readModelsConfig(paths.modelsYml);
947
+ return Object.keys(config.providers || {}).some(isManagedProviderId);
948
+ }
949
+
950
+ function buildManagedExtensionSource(channels = []) {
951
+ return yaml.dump(buildManagedModelsConfig(channels, { providers: {} }), {
952
+ lineWidth: 120,
953
+ noRefs: true,
954
+ sortKeys: false
955
+ });
956
+ }
957
+
958
+ module.exports = {
959
+ MANAGED_PROVIDER_PREFIX,
960
+ buildManagedExtensionSource,
961
+ buildManagedModelsConfig,
962
+ buildProviderEntry,
963
+ getManagedProviderId,
964
+ getLastManagedOmpSyncResult,
965
+ isManagedOmpProvidersActive,
966
+ isManagedProviderExtensionActive: isManagedOmpProvidersActive,
967
+ normalizeProviderId,
968
+ readModelsConfig,
969
+ removeManagedOmpProviders,
970
+ removeManagedProviderExtension: removeManagedOmpProviders,
971
+ validateOmpModelsConfig,
972
+ writeManagedOmpProviders,
973
+ writeManagedProviderExtension: writeManagedOmpProviders,
974
+ writeModelsConfig
975
+ };