coding-tool-x 3.7.11 → 3.7.12

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 (42) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +1 -1
  3. package/dist/web/assets/{Analytics-Ck_00Brj.js → Analytics-8dRv1aYW.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-Cpw2vUa8.js → ConfigTemplates-BQRXSICO.js} +1 -1
  5. package/dist/web/assets/Home-BtzWhKIa.js +1 -0
  6. package/dist/web/assets/Home-DYVy4cGZ.css +1 -0
  7. package/dist/web/assets/{PluginManager-2RlvEJCT.js → PluginManager-BTa-Ygf6.js} +1 -1
  8. package/dist/web/assets/{ProjectList-_ecCngpl.js → ProjectList-F_cB8ldA.js} +1 -1
  9. package/dist/web/assets/{SessionList-DlWcw3hO.js → SessionList-LOrbPQvu.js} +1 -1
  10. package/dist/web/assets/SkillManager-BsX8Qpj0.css +1 -0
  11. package/dist/web/assets/SkillManager-BzI_t-yV.js +1 -0
  12. package/dist/web/assets/{WorkspaceManager-3mqghi09.js → WorkspaceManager-Db9RGiz7.js} +1 -1
  13. package/dist/web/assets/index-DIWJmyL6.js +2 -0
  14. package/dist/web/assets/{index-BceXgSSe.css → index-bJRG3RXH.css} +1 -1
  15. package/dist/web/index.html +2 -2
  16. package/docs/superpowers/plans/2026-08-01-omp-skill-scan-settings.md +759 -0
  17. package/docs/superpowers/specs/2026-08-01-omp-skill-scan-settings-design.md +224 -0
  18. package/package.json +1 -1
  19. package/src/commands/proxy-control.js +12 -12
  20. package/src/commands/toggle-proxy.js +9 -9
  21. package/src/config/paths.js +52 -29
  22. package/src/index.js +1 -1
  23. package/src/server/api/plugins.js +119 -47
  24. package/src/server/api/skills.js +63 -10
  25. package/src/server/omp-proxy-server.js +14 -1
  26. package/src/server/services/model-definition-schema.js +2 -1
  27. package/src/server/services/omp-channels.js +71 -12
  28. package/src/server/services/omp-config.js +71 -6
  29. package/src/server/services/omp-native-plugin-adapter.js +456 -0
  30. package/src/server/services/omp-settings-manager.js +3 -1
  31. package/src/server/services/omp-skill-discovery.js +358 -0
  32. package/src/server/services/omp-skill-settings-service.js +97 -0
  33. package/src/server/services/pi-omp-migration.js +273 -0
  34. package/src/server/services/platform-resolution.js +44 -0
  35. package/src/server/services/plugins-service.js +85 -561
  36. package/src/server/services/project-path-validation.js +65 -0
  37. package/src/server/services/skill-service.js +169 -77
  38. package/dist/web/assets/Home-Bft3Kztl.js +0 -1
  39. package/dist/web/assets/Home-CHZJkkjH.css +0 -1
  40. package/dist/web/assets/SkillManager-CnY7IXkX.js +0 -1
  41. package/dist/web/assets/SkillManager-D7pd-d_P.css +0 -1
  42. package/dist/web/assets/index-k5oP7c-A.js +0 -2
@@ -5,6 +5,11 @@ const yaml = require('js-yaml');
5
5
  const { HOME_DIR, PATHS = {} } = require('../../config/paths');
6
6
 
7
7
  const DEFAULT_OMP_COMMAND = 'omp';
8
+ const YAML_DUMP_OPTIONS = Object.freeze({
9
+ lineWidth: 120,
10
+ noRefs: true,
11
+ sortKeys: false
12
+ });
8
13
 
9
14
  function expandHome(input = '') {
10
15
  const value = String(input || '').trim();
@@ -43,7 +48,9 @@ function getDefaultOmpAgentDir(env = process.env) {
43
48
  }
44
49
 
45
50
  function getFallbackOmpAgentDir(runtime, env = process.env) {
46
- const configured = expandHome(env.OMP_CODING_AGENT_DIR || '');
51
+ const configured = expandHome(
52
+ env.PI_CODING_AGENT_DIR || env.OMP_CODING_AGENT_DIR || ''
53
+ );
47
54
  if (configured) return configured;
48
55
  return getDefaultOmpAgentDir(env);
49
56
  }
@@ -121,6 +128,14 @@ function ensureOmpDir(dirPath) {
121
128
  }
122
129
  }
123
130
 
131
+ function isPlainObject(value) {
132
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
133
+ return false;
134
+ }
135
+ const prototype = Object.getPrototypeOf(value);
136
+ return prototype === Object.prototype || prototype === null;
137
+ }
138
+
124
139
  function readJsonFile(filePath, fallback = {}) {
125
140
  try {
126
141
  if (!fs.existsSync(filePath)) return fallback;
@@ -150,11 +165,7 @@ function readYamlFile(filePath, fallback = {}) {
150
165
 
151
166
  function writeYamlFile(filePath, data) {
152
167
  ensureOmpDir(path.dirname(filePath));
153
- fs.writeFileSync(filePath, yaml.dump(data || {}, {
154
- lineWidth: 120,
155
- noRefs: true,
156
- sortKeys: false
157
- }), 'utf8');
168
+ fs.writeFileSync(filePath, yaml.dump(data || {}, YAML_DUMP_OPTIONS), 'utf8');
158
169
  }
159
170
 
160
171
  function readOmpSettings() {
@@ -166,6 +177,58 @@ function writeOmpSettings(settings) {
166
177
  writeYamlFile(getOmpPaths().settings, settings || {});
167
178
  }
168
179
 
180
+ function readOmpSettingsStrict() {
181
+ const filePath = getOmpPaths().settings;
182
+ let source;
183
+ try {
184
+ source = fs.readFileSync(filePath, 'utf8');
185
+ } catch (error) {
186
+ if (error?.code === 'ENOENT') return {};
187
+ throw error;
188
+ }
189
+
190
+ const settings = yaml.load(source);
191
+ if (!isPlainObject(settings)) {
192
+ throw new Error('Invalid OMP config');
193
+ }
194
+ return settings;
195
+ }
196
+
197
+ function writeOmpSettingsAtomic(settings) {
198
+ const filePath = getOmpPaths().settings;
199
+ const directory = path.dirname(filePath);
200
+ ensureOmpDir(directory);
201
+ let mode = 0o600;
202
+ try {
203
+ mode = fs.statSync(filePath).mode & 0o777;
204
+ } catch (error) {
205
+ if (error?.code !== 'ENOENT') throw error;
206
+ }
207
+ const temporaryPath = path.join(
208
+ directory,
209
+ `.${path.basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`
210
+ );
211
+
212
+ try {
213
+ fs.writeFileSync(temporaryPath, yaml.dump(settings, YAML_DUMP_OPTIONS), {
214
+ encoding: 'utf8',
215
+ flag: 'wx',
216
+ mode
217
+ });
218
+ fs.chmodSync(temporaryPath, mode);
219
+ fs.renameSync(temporaryPath, filePath);
220
+ } catch (error) {
221
+ try {
222
+ if (fs.existsSync(temporaryPath)) {
223
+ fs.unlinkSync(temporaryPath);
224
+ }
225
+ } catch {
226
+ // Preserve the original write, chmod, or rename failure.
227
+ }
228
+ throw error;
229
+ }
230
+ }
231
+
169
232
  function getOmpCommand(env = process.env) {
170
233
  return String(env.OMP_COMMAND || DEFAULT_OMP_COMMAND).trim() || DEFAULT_OMP_COMMAND;
171
234
  }
@@ -286,6 +349,8 @@ module.exports = {
286
349
  writeYamlFile,
287
350
  readOmpSettings,
288
351
  writeOmpSettings,
352
+ readOmpSettingsStrict,
353
+ writeOmpSettingsAtomic,
289
354
  getOmpCommand,
290
355
  getOmpProfile,
291
356
  resolveOmpRuntime,
@@ -0,0 +1,456 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execFileSync } = require('child_process');
4
+ const { getOmpCommand, getOmpPaths } = require('./omp-config');
5
+
6
+ const VALID_SCOPES = new Set(['user', 'project']);
7
+ const GENERIC_SOURCE_KINDS = new Set(['npm', 'link', 'linked', 'marketplace']);
8
+
9
+ function normalizeScope(scope, fallback = 'user') {
10
+ return VALID_SCOPES.has(scope) ? scope : fallback;
11
+ }
12
+
13
+ function parseJsonOutput(output, commandLabel, emptyMessages = []) {
14
+ const text = stripAnsi(output).trim();
15
+ if (!text || emptyMessages.some(message => text.includes(message))) return null;
16
+ try {
17
+ return JSON.parse(text);
18
+ } catch {
19
+ throw new Error(`OMP returned invalid JSON for "${commandLabel}"`);
20
+ }
21
+ }
22
+
23
+ function stripAnsi(output) {
24
+ return String(output || '').replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '');
25
+ }
26
+
27
+ function splitMarketplaceId(pluginId = '') {
28
+ const value = String(pluginId || '');
29
+ const index = value.lastIndexOf('@');
30
+ if (index <= 0) return { name: value, marketplace: '' };
31
+ return {
32
+ name: value.slice(0, index),
33
+ marketplace: value.slice(index + 1)
34
+ };
35
+ }
36
+
37
+ function collectEntries(value, inherited = {}, result = []) {
38
+ if (Array.isArray(value)) {
39
+ value.forEach(item => collectEntries(item, inherited, result));
40
+ return result;
41
+ }
42
+ if (!value || typeof value !== 'object') return result;
43
+
44
+ const looksLikePlugin = Boolean(
45
+ value.name || value.id || value.pluginId || value.package || value.installPath
46
+ );
47
+ if (looksLikePlugin) {
48
+ result.push({ ...inherited, ...value });
49
+ return result;
50
+ }
51
+
52
+ for (const [key, nested] of Object.entries(value)) {
53
+ const next = { ...inherited };
54
+ if (key === 'user' || key === 'project') next.scope = key;
55
+ if (key === 'npm') next.pluginKind = 'npm';
56
+ if (key === 'linked' || key === 'link') next.pluginKind = 'link';
57
+ if (key === 'marketplace') next.pluginKind = 'marketplace';
58
+ collectEntries(nested, next, result);
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function normalizeInstalledPlugin(entry, kindHint = '') {
64
+ const firstEntry = Array.isArray(entry.entries) ? entry.entries[0] || {} : {};
65
+ const pluginKind = entry.pluginKind
66
+ || entry.kind
67
+ || entry.sourceKind
68
+ || (entry.marketplace ? 'marketplace' : kindHint)
69
+ || (entry.linked ? 'link' : 'npm');
70
+ const isMarketplace = pluginKind === 'marketplace';
71
+ const sourceIdentity = String(entry.source || '').trim();
72
+ const rawIdentity = String(
73
+ entry.pluginId
74
+ || entry.id
75
+ || entry.package
76
+ || entry.installTarget
77
+ || (!GENERIC_SOURCE_KINDS.has(sourceIdentity.toLowerCase()) ? sourceIdentity : '')
78
+ || entry.name
79
+ || ''
80
+ ).trim();
81
+ const marketplace = String(entry.marketplace || '').trim();
82
+ const pluginId = isMarketplace && marketplace && !rawIdentity.endsWith(`@${marketplace}`)
83
+ ? `${rawIdentity}@${marketplace}`
84
+ : rawIdentity;
85
+ const parsed = isMarketplace ? splitMarketplaceId(pluginId) : { name: entry.name || pluginId, marketplace };
86
+ const scope = normalizeScope(entry.scope, 'user');
87
+
88
+ if (!pluginId) {
89
+ throw new Error('OMP plugin list returned an entry without a native identity');
90
+ }
91
+
92
+ return {
93
+ ...entry,
94
+ key: `omp:${pluginKind}:${pluginId}:${scope}`,
95
+ pluginId,
96
+ id: pluginId,
97
+ name: String(entry.displayName || entry.name || parsed.name || pluginId),
98
+ marketplace: marketplace || parsed.marketplace,
99
+ installTarget: pluginId,
100
+ directory: entry.directory || entry.installPath || pluginId,
101
+ installPath: entry.installPath || entry.path || firstEntry.installPath || firstEntry.path || '',
102
+ installSource: entry.installSource || entry.source || pluginId,
103
+ source: `omp-${pluginKind}`,
104
+ scope,
105
+ pluginKind,
106
+ pluginType: pluginKind,
107
+ version: entry.version || firstEntry.version || 'latest',
108
+ description: entry.description || entry.manifest?.description || firstEntry.description || '',
109
+ installed: true,
110
+ enabled: entry.enabled !== false && firstEntry.enabled !== false,
111
+ readonly: false
112
+ };
113
+ }
114
+
115
+ function normalizeMarketplacePlugin(entry, inheritedMarketplace = '') {
116
+ const marketplace = String(entry.marketplace || inheritedMarketplace || '').trim();
117
+ const rawName = String(entry.name || entry.id || entry.pluginId || '').trim();
118
+ if (!rawName) return null;
119
+ const parsed = splitMarketplaceId(rawName);
120
+ const name = parsed.name;
121
+ const marketplaceName = marketplace || parsed.marketplace;
122
+ const pluginId = marketplaceName ? `${name}@${marketplaceName}` : rawName;
123
+ return {
124
+ ...entry,
125
+ key: `omp-market:${pluginId}`,
126
+ pluginId,
127
+ id: pluginId,
128
+ name,
129
+ marketplace: marketplaceName,
130
+ installTarget: pluginId,
131
+ installSource: pluginId,
132
+ source: entry.source || pluginId,
133
+ directory: entry.directory || name,
134
+ version: entry.version || 'latest',
135
+ description: entry.description || '',
136
+ pluginKind: 'marketplace',
137
+ pluginType: 'marketplace',
138
+ readonly: false,
139
+ isInstalled: entry.installed === true
140
+ };
141
+ }
142
+
143
+ class OmpNativePluginAdapter {
144
+ constructor(options = {}) {
145
+ this.commandRunner = options.commandRunner || execFileSync;
146
+ }
147
+
148
+ run(args, options = {}) {
149
+ const command = getOmpCommand();
150
+ try {
151
+ return this.commandRunner(command, args, {
152
+ cwd: options.cwd || process.cwd(),
153
+ encoding: 'utf8',
154
+ stdio: ['ignore', 'pipe', 'pipe'],
155
+ timeout: options.timeout || 120000,
156
+ windowsHide: true
157
+ });
158
+ } catch (error) {
159
+ const detail = String(error?.stderr || error?.message || error).trim();
160
+ throw new Error(
161
+ `OMP 17.1+ plugin CLI command failed (${command} ${args.join(' ')}): ${detail}`
162
+ );
163
+ }
164
+ }
165
+
166
+ runJson(args, options = {}, emptyMessages = []) {
167
+ const output = this.run(args, options);
168
+ return parseJsonOutput(output, args.join(' '), emptyMessages);
169
+ }
170
+
171
+ listPlugins(options = {}) {
172
+ const payload = this.runJson(['plugin', 'list', '--json'], options);
173
+ if (!payload || typeof payload !== 'object') {
174
+ throw new Error('OMP plugin list returned an invalid JSON payload');
175
+ }
176
+
177
+ const plugins = [];
178
+ for (const entry of collectEntries(payload.npm || [])) {
179
+ plugins.push(normalizeInstalledPlugin(entry, 'npm'));
180
+ }
181
+ for (const entry of collectEntries(payload.linked || payload.link || [])) {
182
+ plugins.push(normalizeInstalledPlugin(entry, 'link'));
183
+ }
184
+ for (const entry of collectEntries(payload.marketplace || [])) {
185
+ plugins.push(normalizeInstalledPlugin(entry, 'marketplace'));
186
+ }
187
+ plugins.push(...this.listLooseExtensions());
188
+ return {
189
+ plugins: options.cwd
190
+ ? plugins
191
+ : plugins.filter(plugin => plugin.scope !== 'project')
192
+ };
193
+ }
194
+
195
+ listLooseExtensions() {
196
+ const extensionsDir = getOmpPaths().extensions;
197
+ if (!fs.existsSync(extensionsDir)) return [];
198
+ const plugins = [];
199
+
200
+ for (const entry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
201
+ if (entry.name.startsWith('.')) continue;
202
+ const extension = path.extname(entry.name).toLowerCase();
203
+ if (!entry.isDirectory() && !['.js', '.mjs', '.cjs', '.ts'].includes(extension)) {
204
+ continue;
205
+ }
206
+ const installPath = path.join(extensionsDir, entry.name);
207
+ let manifest = {};
208
+ if (entry.isDirectory()) {
209
+ for (const manifestName of ['omp.json', 'extension.json', 'plugin.json', 'package.json']) {
210
+ const manifestPath = path.join(installPath, manifestName);
211
+ if (!fs.existsSync(manifestPath)) continue;
212
+ try {
213
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
214
+ } catch {
215
+ manifest = {};
216
+ }
217
+ break;
218
+ }
219
+ }
220
+ let realPath = installPath;
221
+ try {
222
+ realPath = fs.realpathSync(installPath);
223
+ } catch {
224
+ // The absolute install path remains a stable local identity.
225
+ }
226
+ const name = manifest.name || (entry.isDirectory()
227
+ ? entry.name
228
+ : entry.name.slice(0, -extension.length));
229
+ plugins.push({
230
+ key: `omp:extension:${realPath}`,
231
+ pluginId: `extension:${realPath}`,
232
+ id: `extension:${realPath}`,
233
+ name,
234
+ directory: entry.name,
235
+ installPath,
236
+ installTarget: '',
237
+ installSource: installPath,
238
+ source: 'omp-extension',
239
+ scope: 'user',
240
+ version: manifest.version || 'local',
241
+ description: manifest.description || '',
242
+ installed: true,
243
+ enabled: true,
244
+ pluginType: 'extension',
245
+ pluginKind: 'extension',
246
+ readonly: true
247
+ });
248
+ }
249
+ return plugins;
250
+ }
251
+
252
+ installPlugin(target, metadata = {}, options = {}) {
253
+ const pluginId = String(target || metadata.pluginId || metadata.installSource || '').trim();
254
+ if (!pluginId) throw new Error('Missing OMP plugin target');
255
+ const args = ['plugin', 'install', pluginId];
256
+ if (options.force) args.push('--force');
257
+ if (options.scope) args.push('--scope', normalizeScope(options.scope));
258
+ this.run(args, options);
259
+ const parsed = splitMarketplaceId(pluginId);
260
+ return {
261
+ success: true,
262
+ plugin: normalizeInstalledPlugin({
263
+ ...metadata,
264
+ pluginId,
265
+ name: metadata.name || parsed.name || pluginId,
266
+ marketplace: metadata.marketplace || parsed.marketplace,
267
+ scope: options.scope || metadata.scope,
268
+ pluginKind: metadata.pluginKind || (parsed.marketplace ? 'marketplace' : 'npm')
269
+ })
270
+ };
271
+ }
272
+
273
+ uninstallPlugin(pluginId, options = {}) {
274
+ this.runMutation('uninstall', pluginId, options);
275
+ return { success: true, message: 'Plugin uninstalled successfully' };
276
+ }
277
+
278
+ togglePlugin(pluginId, enabled, options = {}) {
279
+ this.runMutation(enabled ? 'enable' : 'disable', pluginId, options);
280
+ return {
281
+ pluginId,
282
+ name: splitMarketplaceId(pluginId).name,
283
+ scope: options.scope || 'user',
284
+ enabled,
285
+ success: true
286
+ };
287
+ }
288
+
289
+ updatePluginConfig(pluginId, config, options = {}) {
290
+ const target = String(pluginId || '').trim();
291
+ if (!target) throw new Error('Missing OMP pluginId');
292
+ for (const [key, value] of Object.entries(config || {})) {
293
+ const serialized = typeof value === 'string' ? value : JSON.stringify(value);
294
+ const args = ['plugin', 'config', 'set', target, key, serialized];
295
+ if (options.scope) args.push('--scope', normalizeScope(options.scope));
296
+ this.run(args, options);
297
+ }
298
+ return {
299
+ success: true,
300
+ message: `Configuration updated for plugin "${target}"`
301
+ };
302
+ }
303
+
304
+ runMutation(action, pluginId, options = {}) {
305
+ const target = String(pluginId || '').trim();
306
+ if (!target) throw new Error('Missing OMP pluginId');
307
+ if (target.startsWith('extension:')) {
308
+ throw new Error('OMP loose extensions are readonly and cannot be managed by the plugin CLI');
309
+ }
310
+ const args = ['plugin', action, target];
311
+ if (options.scope) args.push('--scope', normalizeScope(options.scope));
312
+ this.run(args, options);
313
+ }
314
+
315
+ discover(options = {}) {
316
+ const output = this.run(['plugin', 'discover', '--json'], options);
317
+ const text = stripAnsi(output).trim();
318
+ if (!text || text.includes('No plugins available')) return [];
319
+ let payload;
320
+ try {
321
+ payload = JSON.parse(text);
322
+ } catch {
323
+ return this.discoverFromText(text, options);
324
+ }
325
+
326
+ const entries = [];
327
+ if (Array.isArray(payload)) {
328
+ entries.push(...payload);
329
+ } else if (Array.isArray(payload.plugins)) {
330
+ entries.push(...payload.plugins);
331
+ } else if (Array.isArray(payload.marketplaces)) {
332
+ for (const marketplace of payload.marketplaces) {
333
+ for (const plugin of marketplace.plugins || []) {
334
+ entries.push({ ...plugin, marketplace: plugin.marketplace || marketplace.name });
335
+ }
336
+ }
337
+ } else {
338
+ entries.push(...collectEntries(payload));
339
+ }
340
+
341
+ const seen = new Set();
342
+ return entries
343
+ .map(entry => normalizeMarketplacePlugin(entry, entry.marketplace))
344
+ .filter(plugin => {
345
+ if (!plugin || seen.has(plugin.pluginId)) return false;
346
+ seen.add(plugin.pluginId);
347
+ return true;
348
+ });
349
+ }
350
+
351
+ discoverFromText(initialOutput, options = {}) {
352
+ const marketplaces = this.listMarketplaces(options);
353
+ if (marketplaces.length === 0) return [];
354
+ const results = [];
355
+ for (const marketplace of marketplaces) {
356
+ const output = marketplaces.length === 1
357
+ ? initialOutput
358
+ : this.run(['plugin', 'discover', marketplace.id, '--json'], options);
359
+ const lines = stripAnsi(output).split(/\r?\n/);
360
+ let current = null;
361
+ for (const rawLine of lines) {
362
+ const line = rawLine.trim();
363
+ if (!line || line.startsWith('Available Plugins')) continue;
364
+ if (/^No plugins (available|found)/i.test(line)) continue;
365
+ if (/^\S/.test(rawLine)) continue;
366
+ if (/^\s{4,}\S/.test(rawLine) && current) {
367
+ current.description = line;
368
+ continue;
369
+ }
370
+ const match = line.match(/^(.+?)(?:@([^@\s]+))?$/);
371
+ if (!match) continue;
372
+ current = normalizeMarketplacePlugin({
373
+ name: match[1],
374
+ version: match[2] || 'latest',
375
+ marketplace: marketplace.id
376
+ }, marketplace.id);
377
+ if (current) results.push(current);
378
+ }
379
+ }
380
+ return results;
381
+ }
382
+
383
+ listMarketplaces(options = {}) {
384
+ const output = this.run(['plugin', 'marketplace', 'list', '--json'], options);
385
+ const text = stripAnsi(output).trim();
386
+ if (!text || text.includes('No marketplaces configured')) return [];
387
+ let payload;
388
+ try {
389
+ payload = JSON.parse(text);
390
+ } catch {
391
+ const entries = text.split(/\r?\n/)
392
+ .filter(line => /^\s{2,}\S/.test(line))
393
+ .map(line => {
394
+ const match = line.trim().match(/^(\S+)\s+(.+)$/);
395
+ return match ? { name: match[1], sourceUri: match[2], source: match[2] } : null;
396
+ })
397
+ .filter(Boolean);
398
+ if (entries.length === 0) {
399
+ throw new Error('OMP returned invalid marketplace list output');
400
+ }
401
+ payload = entries;
402
+ }
403
+ const entries = Array.isArray(payload)
404
+ ? payload
405
+ : (payload.marketplaces || collectEntries(payload));
406
+ return entries.map(entry => {
407
+ const name = String(entry.name || entry.id || entry.marketplace || '').trim();
408
+ const source = entry.source || entry.url || entry.repo || entry.repository || '';
409
+ return {
410
+ ...entry,
411
+ id: name,
412
+ name,
413
+ label: name,
414
+ source,
415
+ sourceUri: typeof source === 'string' ? source : JSON.stringify(source),
416
+ provider: 'omp-marketplace',
417
+ enabled: true,
418
+ mutable: {
419
+ toggle: false,
420
+ auth: false,
421
+ remove: true,
422
+ update: true
423
+ }
424
+ };
425
+ }).filter(entry => entry.id);
426
+ }
427
+
428
+ addMarketplace(source, options = {}) {
429
+ const target = String(source || '').trim();
430
+ if (!target) throw new Error('Missing marketplace source');
431
+ this.run(['plugin', 'marketplace', 'add', target], options);
432
+ return this.listMarketplaces(options);
433
+ }
434
+
435
+ removeMarketplace(name, options = {}) {
436
+ const target = String(name || '').trim();
437
+ if (!target) throw new Error('Missing marketplace name');
438
+ this.run(['plugin', 'marketplace', 'remove', target], options);
439
+ return this.listMarketplaces(options);
440
+ }
441
+
442
+ updateMarketplaces(name = '', options = {}) {
443
+ const args = ['plugin', 'marketplace', 'update'];
444
+ if (name) args.push(name);
445
+ this.run(args, options);
446
+ return this.listMarketplaces(options);
447
+ }
448
+ }
449
+
450
+ module.exports = {
451
+ OmpNativePluginAdapter,
452
+ collectEntries,
453
+ normalizeInstalledPlugin,
454
+ normalizeMarketplacePlugin,
455
+ parseJsonOutput
456
+ };
@@ -220,7 +220,9 @@ function normalizeModels(channel = {}, options = {}) {
220
220
  }
221
221
 
222
222
  function readOmpCatalogModels(command, providerId, options = {}) {
223
- const requestedModelIds = uniqueStrings(options.requestedModelIds);
223
+ const requestedModelIds = uniqueStrings(options.requestedModelIds)
224
+ .map(modelId => splitModelSelector(modelId).modelId)
225
+ .filter(Boolean);
224
226
  const requested = new Set(requestedModelIds.map(modelId => modelId.toLowerCase()));
225
227
  const useFullCatalog = requested.size > 0;
226
228
  const cacheKey = `${command}\u0000${useFullCatalog ? `requested:${providerId}:${[...requested].sort().join(',')}` : providerId}`;