coding-tool-x 3.7.10 → 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 (54) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +1 -1
  3. package/dist/web/assets/{Analytics-CkUtnoz9.js → Analytics-8dRv1aYW.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-BwPqhE-N.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-JMXWl37b.js → PluginManager-BTa-Ygf6.js} +1 -1
  8. package/dist/web/assets/{ProjectList-CZwDgVvt.js → ProjectList-F_cB8ldA.js} +1 -1
  9. package/dist/web/assets/{SessionList-2fZmN7Au.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-B3lhhBD0.js → WorkspaceManager-Db9RGiz7.js} +1 -1
  13. package/dist/web/assets/index-DIWJmyL6.js +2 -0
  14. package/dist/web/assets/{index-C5nLTbOk.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 +16 -17
  21. package/src/commands/ui.js +11 -1
  22. package/src/config/default.js +1 -1
  23. package/src/config/paths.js +52 -29
  24. package/src/index.js +42 -16
  25. package/src/server/api/omp-channels.js +6 -4
  26. package/src/server/api/omp-config.js +1 -1
  27. package/src/server/api/omp-proxy.js +13 -42
  28. package/src/server/api/plugins.js +119 -47
  29. package/src/server/api/skills.js +63 -10
  30. package/src/server/index.js +3 -3
  31. package/src/server/omp-proxy-server.js +142 -10
  32. package/src/server/services/channel-scheduler.js +59 -9
  33. package/src/server/services/model-definition-schema.js +2 -1
  34. package/src/server/services/omp-auth-gateway-client.js +62 -0
  35. package/src/server/services/omp-channels.js +168 -12
  36. package/src/server/services/omp-config.js +71 -6
  37. package/src/server/services/omp-gateway-routing.js +295 -0
  38. package/src/server/services/omp-gateway.js +919 -0
  39. package/src/server/services/omp-native-plugin-adapter.js +456 -0
  40. package/src/server/services/omp-session-log-observer.js +138 -0
  41. package/src/server/services/omp-sessions.js +80 -0
  42. package/src/server/services/omp-settings-manager.js +338 -87
  43. package/src/server/services/omp-skill-discovery.js +358 -0
  44. package/src/server/services/omp-skill-settings-service.js +97 -0
  45. package/src/server/services/pi-omp-migration.js +273 -0
  46. package/src/server/services/platform-resolution.js +44 -0
  47. package/src/server/services/plugins-service.js +85 -561
  48. package/src/server/services/project-path-validation.js +65 -0
  49. package/src/server/services/skill-service.js +169 -77
  50. package/dist/web/assets/Home-BANl91zG.js +0 -1
  51. package/dist/web/assets/Home-CqnBXFxj.css +0 -1
  52. package/dist/web/assets/SkillManager-Bhzhkeis.js +0 -1
  53. package/dist/web/assets/SkillManager-D7pd-d_P.css +0 -1
  54. package/dist/web/assets/index-LYvv7wtx.js +0 -2
@@ -1,6 +1,4 @@
1
1
  const express = require('express');
2
- const fs = require('fs');
3
- const path = require('path');
4
2
  const router = express.Router();
5
3
  const {
6
4
  startOmpProxyServer,
@@ -10,10 +8,10 @@ const {
10
8
  const {
11
9
  getChannels,
12
10
  getEnabledChannels,
13
- markChannelAsRecentlyUsed
11
+ markChannelAsRecentlyUsed,
12
+ loadManagedOmpActiveChannelId
14
13
  } = require('../services/omp-channels');
15
14
  const { getSchedulerState } = require('../services/channel-scheduler');
16
- const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
17
15
 
18
16
  function sanitizeChannel(channel) {
19
17
  if (!channel) return null;
@@ -21,7 +19,10 @@ function sanitizeChannel(channel) {
21
19
  id: channel.id,
22
20
  name: channel.name,
23
21
  baseUrl: channel.baseUrl,
24
- websiteUrl: channel.websiteUrl
22
+ websiteUrl: channel.websiteUrl,
23
+ providerKey: channel.providerKey,
24
+ providerApi: channel.providerApi,
25
+ routingGroup: channel.routingGroup || ''
25
26
  };
26
27
  }
27
28
 
@@ -35,34 +36,6 @@ function selectLatestEnabledChannel(channels) {
35
36
  }, enabledChannels[0]);
36
37
  }
37
38
 
38
- function saveActiveChannelId(channelId) {
39
- ensureStorageDirMigrated();
40
- const filePath = PATHS.activeChannel.omp;
41
- const dir = path.dirname(filePath);
42
- if (!fs.existsSync(dir)) {
43
- fs.mkdirSync(dir, { recursive: true });
44
- }
45
- fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
46
- }
47
-
48
- function loadActiveChannelId() {
49
- ensureStorageDirMigrated();
50
- try {
51
- if (!fs.existsSync(PATHS.activeChannel.omp)) return null;
52
- const data = JSON.parse(fs.readFileSync(PATHS.activeChannel.omp, 'utf8'));
53
- return data.activeChannelId || null;
54
- } catch {
55
- return null;
56
- }
57
- }
58
-
59
- function removeActiveChannelFile() {
60
- ensureStorageDirMigrated();
61
- if (fs.existsSync(PATHS.activeChannel.omp)) {
62
- fs.unlinkSync(PATHS.activeChannel.omp);
63
- }
64
- }
65
-
66
39
  function resolveActiveChannel(channels, activeChannelId = null) {
67
40
  if (!Array.isArray(channels) || channels.length === 0) return null;
68
41
  if (activeChannelId) {
@@ -80,12 +53,12 @@ router.get('/status', (req, res) => {
80
53
  const proxy = getOmpProxyStatus();
81
54
  const { channels } = getChannels();
82
55
  const enabledChannels = channels.filter(ch => ch.enabled !== false);
83
- const activeChannel = resolveActiveChannel(channels, loadActiveChannelId());
56
+ const activeChannel = resolveActiveChannel(channels, loadManagedOmpActiveChannelId());
84
57
  res.json({
85
58
  proxy,
86
59
  config: {
87
- mode: 'models-yml-provider-config',
88
- nativeProxyProtocol: false
60
+ mode: 'http-gateway',
61
+ nativeProxyProtocol: true
89
62
  },
90
63
  activeChannel: sanitizeChannel(activeChannel),
91
64
  enabledChannelsCount: enabledChannels.length,
@@ -107,11 +80,10 @@ router.post('/start', async (req, res) => {
107
80
 
108
81
  let currentChannel = selectLatestEnabledChannel(enabledChannels) || enabledChannels[0];
109
82
  currentChannel = markChannelAsRecentlyUsed(currentChannel.id);
110
- saveActiveChannelId(currentChannel.id);
111
83
 
112
- const proxyResult = await startOmpProxyServer();
84
+ const proxyResult = await startOmpProxyServer({ activeChannelId: currentChannel.id });
113
85
  if (!proxyResult.success) {
114
- return res.status(500).json({ error: 'Failed to enable OMP models.yml provider config' });
86
+ return res.status(500).json({ error: 'Failed to start OMP gateway' });
115
87
  }
116
88
 
117
89
  const { broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
@@ -125,10 +97,10 @@ router.post('/start', async (req, res) => {
125
97
  success: true,
126
98
  port: proxyResult.port,
127
99
  activeChannel: sanitizeChannel(activeChannel),
128
- mode: 'models-yml-provider-config',
100
+ mode: 'http-gateway',
129
101
  sync: proxyResult.sync || null,
130
102
  warnings: proxyResult.warnings || [],
131
- message: `OMP models.yml provider config enabled, active channel: ${activeChannel.name}`
103
+ message: `OMP gateway and managed providers enabled, active channel: ${activeChannel.name}`
132
104
  });
133
105
  } catch (error) {
134
106
  console.error('[OMP Proxy] Start failed:', error);
@@ -139,7 +111,6 @@ router.post('/start', async (req, res) => {
139
111
  router.post('/stop', async (req, res) => {
140
112
  try {
141
113
  const result = await stopOmpProxyServer();
142
- removeActiveChannelFile();
143
114
  const { broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
144
115
  const { channels } = getChannels();
145
116
  broadcastProxyState('omp', getOmpProxyStatus(), null, channels);
@@ -8,13 +8,14 @@ const express = require('express');
8
8
  const { PluginsService } = require('../services/plugins-service');
9
9
  const { maskToken } = require('../services/oauth-utils');
10
10
  const { sendApiError } = require('./validation-errors');
11
+ const { resolveManagedPlatform } = require('../services/platform-resolution');
12
+ const { validateKnownProjectCwd } = require('../services/project-path-validation');
11
13
 
12
14
  const router = express.Router();
13
- const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode', 'omp'];
14
15
  const pluginServices = new Map();
15
16
 
16
17
  function resolvePlatform(rawPlatform) {
17
- return SUPPORTED_PLATFORMS.includes(rawPlatform) ? rawPlatform : 'claude';
18
+ return resolveManagedPlatform(rawPlatform);
18
19
  }
19
20
 
20
21
  function getPlatform(req) {
@@ -22,11 +23,31 @@ function getPlatform(req) {
22
23
  }
23
24
 
24
25
  function getPluginsService(req) {
25
- const platform = getPlatform(req);
26
+ const resolution = getPlatform(req);
27
+ const platform = resolution.platform;
26
28
  if (!pluginServices.has(platform)) {
27
29
  pluginServices.set(platform, new PluginsService(platform));
28
30
  }
29
- return { platform, service: pluginServices.get(platform) };
31
+ return {
32
+ platform,
33
+ service: pluginServices.get(platform),
34
+ warning: resolution.warning
35
+ };
36
+ }
37
+
38
+ async function getRequestOptions(req) {
39
+ const scope = String(req.query?.scope || req.body?.scope || '').trim();
40
+ if (scope && scope !== 'user' && scope !== 'project') {
41
+ throw new Error('Invalid scope: expected "user" or "project"');
42
+ }
43
+ const cwd = await validateKnownProjectCwd(req.query?.cwd || req.body?.cwd);
44
+ if (scope === 'project' && !cwd) {
45
+ throw new Error('Project scope requires a valid cwd');
46
+ }
47
+ return {
48
+ ...(cwd ? { cwd } : {}),
49
+ ...(scope ? { scope } : {})
50
+ };
30
51
  }
31
52
 
32
53
  function extractRepoPayload(source = {}) {
@@ -43,7 +64,8 @@ function extractRepoPayload(source = {}) {
43
64
  localPath: repo.localPath || source.localPath || '',
44
65
  repoUrl: repo.repoUrl || repo.url || source.repoUrl || source.url || '',
45
66
  token: repo.token || source.token || '',
46
- marketplace: repo.marketplace || source.marketplace || ''
67
+ marketplace: repo.marketplace || source.marketplace || '',
68
+ source: repo.source || repo.sourceUri || source.source || source.sourceUri || ''
47
69
  };
48
70
  }
49
71
 
@@ -65,6 +87,10 @@ function sanitizeRepos(service, repos = []) {
65
87
  return (Array.isArray(repos) ? repos : []).map(sanitizeRepo);
66
88
  }
67
89
 
90
+ function hasRequestOptions(options = {}) {
91
+ return Object.keys(options).length > 0;
92
+ }
93
+
68
94
  /**
69
95
  * 获取平台插件能力
70
96
  * GET /api/plugins/capabilities
@@ -91,15 +117,19 @@ router.get('/capabilities', (req, res) => {
91
117
  * 获取插件列表
92
118
  * GET /api/plugins
93
119
  */
94
- router.get('/', (req, res) => {
120
+ router.get('/', async (req, res) => {
95
121
  try {
96
- const { platform, service } = getPluginsService(req);
97
- const result = service.listPlugins();
122
+ const { platform, service, warning } = getPluginsService(req);
123
+ const options = await getRequestOptions(req);
124
+ const result = platform === 'omp' || hasRequestOptions(options)
125
+ ? service.listPlugins(options)
126
+ : service.listPlugins();
98
127
 
99
128
  res.json({
100
129
  success: true,
101
130
  platform,
102
- ...result
131
+ ...result,
132
+ ...(warning ? { warnings: [warning] } : {})
103
133
  });
104
134
  } catch (err) {
105
135
  console.error('[Plugins API] List plugins error:', err);
@@ -113,18 +143,22 @@ router.get('/', (req, res) => {
113
143
  */
114
144
  router.get('/market', async (req, res) => {
115
145
  try {
116
- const { platform, service } = getPluginsService(req);
146
+ const { platform, service, warning } = getPluginsService(req);
147
+ const options = await getRequestOptions(req);
117
148
  const forceRefresh = req.query.refresh === '1';
118
149
  if (forceRefresh) {
119
150
  console.log(`[Plugins API] Refreshing market plugins for ${platform}...`);
120
151
  }
121
- const plugins = await service.getMarketPlugins(forceRefresh);
152
+ const plugins = platform === 'omp' || hasRequestOptions(options)
153
+ ? await service.getMarketPlugins(forceRefresh, options)
154
+ : await service.getMarketPlugins(forceRefresh);
122
155
  console.log(`[Plugins API] ${platform}: ${plugins.length} market plugins loaded (refresh=${forceRefresh})`);
123
156
 
124
157
  res.json({
125
158
  success: true,
126
159
  platform,
127
- plugins
160
+ plugins,
161
+ ...(warning ? { warnings: [warning] } : {})
128
162
  });
129
163
  } catch (err) {
130
164
  console.error('[Plugins API] Get market plugins error:', err);
@@ -139,14 +173,15 @@ router.get('/market', async (req, res) => {
139
173
  */
140
174
  router.post('/install', async (req, res) => {
141
175
  try {
142
- const { platform, service } = getPluginsService(req);
143
- const { directory, repo, gitUrl, source } = req.body;
176
+ const { platform, service, warning } = getPluginsService(req);
177
+ const options = await getRequestOptions(req);
178
+ const { directory, repo, gitUrl, source, pluginId } = req.body;
144
179
  const hasDirectoryField = Object.prototype.hasOwnProperty.call(req.body, 'directory');
145
180
 
146
181
  // Support both new format (directory + repo) and legacy format (gitUrl)
147
182
  let installUrl;
148
- if (source) {
149
- installUrl = source;
183
+ if (pluginId || source) {
184
+ installUrl = pluginId || source;
150
185
  } else if (repo && hasDirectoryField) {
151
186
  installUrl = '';
152
187
  } else if (gitUrl) {
@@ -158,15 +193,25 @@ router.post('/install', async (req, res) => {
158
193
  });
159
194
  }
160
195
 
161
- const result = await service.installPlugin(
162
- installUrl,
163
- repo && hasDirectoryField
164
- ? {
165
- ...extractRepoPayload({ repo }),
166
- directory: directory || ''
167
- }
168
- : null
169
- );
196
+ const repoMetadata = repo && hasDirectoryField
197
+ ? {
198
+ ...extractRepoPayload({ repo }),
199
+ directory: directory || ''
200
+ }
201
+ : platform === 'omp' ? {
202
+ ...(req.body.metadata && typeof req.body.metadata === 'object' ? req.body.metadata : {}),
203
+ name: req.body.name || req.body.metadata?.name,
204
+ pluginId: pluginId || req.body.metadata?.pluginId,
205
+ pluginKind: req.body.pluginKind || req.body.metadata?.pluginKind,
206
+ marketplace: req.body.marketplace || req.body.metadata?.marketplace,
207
+ installSource: req.body.installSource || req.body.metadata?.installSource,
208
+ version: req.body.version || req.body.metadata?.version,
209
+ description: req.body.description || req.body.metadata?.description,
210
+ resourceTypes: req.body.resourceTypes || req.body.metadata?.resourceTypes
211
+ } : null;
212
+ const result = platform === 'omp' || hasRequestOptions(options)
213
+ ? await service.installPlugin(installUrl, repoMetadata, options)
214
+ : await service.installPlugin(installUrl, repoMetadata);
170
215
 
171
216
  if (!result.success) {
172
217
  return res.status(400).json({
@@ -179,7 +224,8 @@ router.post('/install', async (req, res) => {
179
224
  success: true,
180
225
  platform,
181
226
  plugin: result.plugin,
182
- message: `Plugin "${result.plugin.name}" installed successfully`
227
+ message: `Plugin "${result.plugin.name}" installed successfully`,
228
+ ...(warning ? { warnings: [warning] } : {})
183
229
  });
184
230
  } catch (err) {
185
231
  console.error('[Plugins API] Install plugin error:', err);
@@ -193,14 +239,23 @@ router.post('/install', async (req, res) => {
193
239
  * 获取插件仓库列表
194
240
  * GET /api/plugins/repos
195
241
  */
196
- router.get('/repos', (req, res) => {
242
+ router.get('/repos', async (req, res) => {
197
243
  try {
198
- const { platform, service } = getPluginsService(req);
199
- const repos = service.getRepos();
244
+ const { platform, service, warning } = getPluginsService(req);
245
+ const options = await getRequestOptions(req);
246
+ const repos = platform === 'omp' || hasRequestOptions(options)
247
+ ? service.getRepos(options)
248
+ : service.getRepos();
200
249
  res.json({
201
250
  success: true,
202
251
  platform,
203
- repos: sanitizeRepos(service, repos)
252
+ repos: sanitizeRepos(service, repos),
253
+ ...((warning || service.getMigrationWarnings?.().length) ? {
254
+ warnings: [
255
+ ...(warning ? [warning] : []),
256
+ ...(service.getMigrationWarnings?.() || [])
257
+ ]
258
+ } : {})
204
259
  });
205
260
  } catch (err) {
206
261
  console.error('[Plugins API] Get repos error:', err);
@@ -213,20 +268,23 @@ router.get('/repos', (req, res) => {
213
268
  * POST /api/plugins/repos
214
269
  * Body: { url, name, description }
215
270
  */
216
- router.post('/repos', (req, res) => {
271
+ router.post('/repos', async (req, res) => {
217
272
  try {
218
273
  const { platform, service } = getPluginsService(req);
274
+ const options = await getRequestOptions(req);
219
275
  const repo = extractRepoPayload(req.body);
220
276
  repo.enabled = req.body.enabled !== false;
221
277
 
222
- if (!repo.localPath && !repo.projectPath && (!repo.owner || !repo.name) && !repo.repoUrl) {
278
+ if (!repo.source && !repo.localPath && !repo.projectPath && (!repo.owner || !repo.name) && !repo.repoUrl) {
223
279
  return res.status(400).json({
224
280
  success: false,
225
281
  message: 'Missing repo info'
226
282
  });
227
283
  }
228
284
 
229
- const repos = service.addRepo(repo);
285
+ const repos = platform === 'omp' || hasRequestOptions(options)
286
+ ? service.addRepo(repo, options)
287
+ : service.addRepo(repo);
230
288
 
231
289
  res.json({
232
290
  success: true,
@@ -240,11 +298,14 @@ router.post('/repos', (req, res) => {
240
298
  }
241
299
  });
242
300
 
243
- router.delete('/repos', (req, res) => {
301
+ router.delete('/repos', async (req, res) => {
244
302
  try {
245
303
  const { platform, service } = getPluginsService(req);
304
+ const options = await getRequestOptions(req);
246
305
  const { id = '', owner = '', name = '' } = req.query;
247
- const repos = service.removeRepo(owner, name, id);
306
+ const repos = platform === 'omp' || hasRequestOptions(options)
307
+ ? service.removeRepo(owner, name, id, options)
308
+ : service.removeRepo(owner, name, id);
248
309
 
249
310
  res.json({
250
311
  success: true,
@@ -376,7 +437,8 @@ router.put('/repos/auth', (req, res) => {
376
437
  router.post('/repos/sync', async (req, res) => {
377
438
  try {
378
439
  const { platform, service } = getPluginsService(req);
379
- const result = await service.syncRepos();
440
+ const options = await getRequestOptions(req);
441
+ const result = await service.syncRepos(options);
380
442
 
381
443
  res.json({
382
444
  success: true,
@@ -397,7 +459,8 @@ router.post('/repos/sync', async (req, res) => {
397
459
  router.post('/sync', async (req, res) => {
398
460
  try {
399
461
  const { platform, service } = getPluginsService(req);
400
- const result = await service.syncPlugins();
462
+ const options = await getRequestOptions(req);
463
+ const result = await service.syncPlugins(options);
401
464
 
402
465
  res.json({
403
466
  success: true,
@@ -472,12 +535,13 @@ router.get('/:name/readme', async (req, res) => {
472
535
  * 获取单个插件详情
473
536
  * GET /api/plugins/:name
474
537
  */
475
- router.get('/:name', (req, res) => {
538
+ router.get('/:name', async (req, res) => {
476
539
  try {
477
540
  const { platform, service } = getPluginsService(req);
478
541
  const { name } = req.params;
542
+ const options = await getRequestOptions(req);
479
543
 
480
- const plugin = service.getPlugin(name);
544
+ const plugin = service.getPlugin(req.query.pluginId || name, options);
481
545
 
482
546
  if (!plugin) {
483
547
  return res.status(404).json({
@@ -501,12 +565,14 @@ router.get('/:name', (req, res) => {
501
565
  * 卸载插件
502
566
  * DELETE /api/plugins/:name
503
567
  */
504
- router.delete('/:name', (req, res) => {
568
+ router.delete('/:name', async (req, res) => {
505
569
  try {
506
570
  const { platform, service } = getPluginsService(req);
507
571
  const { name } = req.params;
572
+ const options = await getRequestOptions(req);
573
+ const pluginId = req.query.pluginId || name;
508
574
 
509
- const result = service.uninstallPlugin(name);
575
+ const result = service.uninstallPlugin(pluginId, options);
510
576
 
511
577
  if (!result.success) {
512
578
  return res.status(400).json({
@@ -531,11 +597,12 @@ router.delete('/:name', (req, res) => {
531
597
  * PUT /api/plugins/:name/toggle
532
598
  * Body: { enabled }
533
599
  */
534
- router.put('/:name/toggle', (req, res) => {
600
+ router.put('/:name/toggle', async (req, res) => {
535
601
  try {
536
602
  const { platform, service } = getPluginsService(req);
537
603
  const { name } = req.params;
538
- const { enabled } = req.body;
604
+ const { enabled, pluginId = name } = req.body;
605
+ const options = await getRequestOptions(req);
539
606
 
540
607
  if (typeof enabled !== 'boolean') {
541
608
  return res.status(400).json({
@@ -544,7 +611,9 @@ router.put('/:name/toggle', (req, res) => {
544
611
  });
545
612
  }
546
613
 
547
- const plugin = service.togglePlugin(name, enabled);
614
+ const plugin = platform === 'omp' || hasRequestOptions(options)
615
+ ? service.togglePlugin(pluginId, enabled, options)
616
+ : service.togglePlugin(pluginId, enabled);
548
617
 
549
618
  res.json({
550
619
  success: true,
@@ -563,11 +632,12 @@ router.put('/:name/toggle', (req, res) => {
563
632
  * PUT /api/plugins/:name/config
564
633
  * Body: { config }
565
634
  */
566
- router.put('/:name/config', (req, res) => {
635
+ router.put('/:name/config', async (req, res) => {
567
636
  try {
568
637
  const { platform, service } = getPluginsService(req);
569
638
  const { name } = req.params;
570
- const { config } = req.body;
639
+ const { config, pluginId = name } = req.body;
640
+ const options = await getRequestOptions(req);
571
641
 
572
642
  if (!config || typeof config !== 'object') {
573
643
  return res.status(400).json({
@@ -576,7 +646,9 @@ router.put('/:name/config', (req, res) => {
576
646
  });
577
647
  }
578
648
 
579
- const result = service.updatePluginConfig(name, config);
649
+ const result = platform === 'omp' || hasRequestOptions(options)
650
+ ? service.updatePluginConfig(pluginId, config, options)
651
+ : service.updatePluginConfig(pluginId, config);
580
652
 
581
653
  res.json({
582
654
  success: true,
@@ -6,13 +6,18 @@ const express = require('express');
6
6
  const { SkillService } = require('../services/skill-service');
7
7
  const { maskToken } = require('../services/oauth-utils');
8
8
  const { sendApiError } = require('./validation-errors');
9
+ const { resolveManagedPlatform } = require('../services/platform-resolution');
10
+ const { validateKnownProjectCwd } = require('../services/project-path-validation');
11
+ const {
12
+ readOmpSkillSettings,
13
+ updateOmpSkillSettings
14
+ } = require('../services/omp-skill-settings-service');
9
15
 
10
16
  const router = express.Router();
11
- const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode', 'omp'];
12
17
  const skillServices = new Map();
13
18
 
14
19
  function resolvePlatform(rawPlatform) {
15
- return SUPPORTED_PLATFORMS.includes(rawPlatform) ? rawPlatform : 'claude';
20
+ return resolveManagedPlatform(rawPlatform);
16
21
  }
17
22
 
18
23
  function getPlatform(req) {
@@ -20,11 +25,16 @@ function getPlatform(req) {
20
25
  }
21
26
 
22
27
  function getSkillService(req) {
23
- const platform = getPlatform(req);
28
+ const resolution = getPlatform(req);
29
+ const platform = resolution.platform;
24
30
  if (!skillServices.has(platform)) {
25
31
  skillServices.set(platform, new SkillService(platform));
26
32
  }
27
- return { platform, service: skillServices.get(platform) };
33
+ return {
34
+ platform,
35
+ service: skillServices.get(platform),
36
+ warning: resolution.warning
37
+ };
28
38
  }
29
39
 
30
40
  function extractRepoPayload(source = {}) {
@@ -69,19 +79,21 @@ function sanitizeRepos(service, repos = []) {
69
79
  */
70
80
  router.get('/', async (req, res) => {
71
81
  try {
72
- const { platform, service } = getSkillService(req);
82
+ const { platform, service, warning } = getSkillService(req);
73
83
  const forceRefresh = req.query.refresh === '1';
84
+ const cwd = await validateKnownProjectCwd(req.query.cwd);
74
85
  if (forceRefresh) {
75
86
  console.log(`[Skills API] Refreshing skills for ${platform}...`);
76
87
  }
77
- const skills = await service.listSkills(forceRefresh);
88
+ const skills = await service.listSkills(forceRefresh, { cwd });
78
89
  console.log(`[Skills API] ${platform}: ${skills.length} skills loaded (refresh=${forceRefresh})`);
79
90
  res.json({
80
91
  success: true,
81
92
  platform,
82
93
  skills,
83
94
  total: skills.length,
84
- installed: skills.filter(s => s.installed).length
95
+ installed: skills.filter(s => s.installed).length,
96
+ ...(warning ? { warnings: [warning] } : {})
85
97
  });
86
98
  } catch (err) {
87
99
  console.error('[Skills API] List skills error:', err);
@@ -89,6 +101,34 @@ router.get('/', async (req, res) => {
89
101
  }
90
102
  });
91
103
 
104
+ /**
105
+ * 获取 OMP 技能设置
106
+ * GET /api/skills/omp-settings
107
+ */
108
+ router.get('/omp-settings', (req, res) => {
109
+ try {
110
+ const settings = readOmpSkillSettings();
111
+ res.json({ success: true, settings });
112
+ } catch (err) {
113
+ console.error('[Skills API] Get OMP skill settings error:', err);
114
+ sendApiError(res, err);
115
+ }
116
+ });
117
+
118
+ /**
119
+ * 更新 OMP 技能设置
120
+ * PUT /api/skills/omp-settings
121
+ */
122
+ router.put('/omp-settings', (req, res) => {
123
+ try {
124
+ const settings = updateOmpSkillSettings(req.body);
125
+ res.json({ success: true, settings });
126
+ } catch (err) {
127
+ console.error('[Skills API] Update OMP skill settings error:', err);
128
+ sendApiError(res, err);
129
+ }
130
+ });
131
+
92
132
  /**
93
133
  * 获取技能详情(完整内容)
94
134
  * GET /api/skills/detail/:directory
@@ -257,10 +297,10 @@ router.post('/create', (req, res) => {
257
297
  * POST /api/skills/uninstall
258
298
  * Body: { directory }
259
299
  */
260
- router.post('/uninstall', (req, res) => {
300
+ router.post('/uninstall', async (req, res) => {
261
301
  try {
262
302
  const { platform, service } = getSkillService(req);
263
- const { directory } = req.body;
303
+ const { directory, scope } = req.body;
264
304
 
265
305
  if (!directory) {
266
306
  return res.status(400).json({
@@ -269,7 +309,20 @@ router.post('/uninstall', (req, res) => {
269
309
  });
270
310
  }
271
311
 
272
- const result = service.uninstallSkill(directory);
312
+ const cwd = await validateKnownProjectCwd(req.body.cwd);
313
+ if (scope && scope !== 'user' && scope !== 'project') {
314
+ throw new Error('Invalid scope: expected "user" or "project"');
315
+ }
316
+ if (scope === 'project' && !cwd) {
317
+ throw new Error('Project scope requires a valid cwd');
318
+ }
319
+ const options = {
320
+ ...(cwd ? { cwd } : {}),
321
+ ...(scope ? { scope } : {})
322
+ };
323
+ const result = platform === 'omp' && Object.keys(options).length > 0
324
+ ? service.uninstallSkill(directory, options)
325
+ : service.uninstallSkill(directory);
273
326
 
274
327
  res.json({
275
328
  success: true,
@@ -465,13 +465,13 @@ function autoRestoreProxies() {
465
465
  startOmpProxyServer({ preserveStartTime: true })
466
466
  .then((result) => {
467
467
  if (result.success) {
468
- console.log(chalk.green(`[OK] OMP models.yml 受管 provider 已自动启用,端口: ${result.port}`));
468
+ console.log(chalk.green(`[OK] OMP 动态网关已自动恢复: http://127.0.0.1:${result.port}`));
469
469
  } else {
470
- console.error(chalk.red(`[ERROR] OMP models.yml 受管 provider 恢复失败: ${result.error || 'Unknown error'}`));
470
+ console.error(chalk.red(`[ERROR] OMP 动态网关恢复失败: ${result.error || 'Unknown error'}`));
471
471
  }
472
472
  })
473
473
  .catch((err) => {
474
- console.error(chalk.red(`[ERROR] OMP models.yml 受管 provider 恢复失败: ${err.message}`));
474
+ console.error(chalk.red(`[ERROR] OMP 动态网关恢复失败: ${err.message}`));
475
475
  });
476
476
  }
477
477
  }