coding-tool-x 3.9.0 → 3.9.2

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 (84) hide show
  1. package/README.md +2 -0
  2. package/dist/web/assets/AgentsDrawer-Dubq1kQI.css +1 -0
  3. package/dist/web/assets/AgentsDrawer-x29vGIIi.js +1 -0
  4. package/dist/web/assets/{Analytics-BXRwqC-P.js → Analytics-BJbR6XJO.js} +1 -1
  5. package/dist/web/assets/{AssetPathField-B-LHvOxq.js → AssetPathField-2W-16zgR.js} +1 -1
  6. package/dist/web/assets/{ChatHistoryDrawer-CoHlGAxb.js → ChatHistoryDrawer-C1URzQ_4.js} +1 -1
  7. package/dist/web/assets/{CollapseItem-kJkiHxFl.js → CollapseItem-DjkOkbwE.js} +1 -1
  8. package/dist/web/assets/{CommandsDrawer-DLQrikcw.js → CommandsDrawer-DcnfrHC4.js} +1 -1
  9. package/dist/web/assets/{ConfigExportDrawer-BItwo0FQ.js → ConfigExportDrawer-CKwG_RGJ.js} +1 -1
  10. package/dist/web/assets/{ConfigTemplates-R3YzzBT3.js → ConfigTemplates-C1tNpfzi.js} +1 -1
  11. package/dist/web/assets/{ConfigTemplatesDrawer-C_nPKrhx.js → ConfigTemplatesDrawer-gZNPbcUR.js} +1 -1
  12. package/dist/web/assets/{ConfigTemplatesPanel-BiwrNyYN.js → ConfigTemplatesPanel-C-Fy53Qo.js} +1 -1
  13. package/dist/web/assets/{FavoritesDrawer-BAJ-AOpN.js → FavoritesDrawer-Bp8FYHPk.js} +1 -1
  14. package/dist/web/assets/GatewayConvertDrawer-CvUkvNQy.css +1 -0
  15. package/dist/web/assets/GatewayConvertDrawer-j5IJzb2Y.js +1 -0
  16. package/dist/web/assets/Home-2MAvBnml.js +1 -0
  17. package/dist/web/assets/Home-BRNWE0Qy.css +1 -0
  18. package/dist/web/assets/{MarkdownEditor-D75GGt5X.js → MarkdownEditor-BA_PcLed.js} +1 -1
  19. package/dist/web/assets/{McpDrawer-xNzJtT4X.js → McpDrawer-DAep5PP_.js} +1 -1
  20. package/dist/web/assets/{McpFormDrawer-B240mJ5y.js → McpFormDrawer-BRm-hzv1.js} +1 -1
  21. package/dist/web/assets/{PluginManager-CgLjBdlr.js → PluginManager-C2yY8-FP.js} +1 -1
  22. package/dist/web/assets/{PluginsDrawer-DvJdwMXx.js → PluginsDrawer-CxKnWzTO.js} +1 -1
  23. package/dist/web/assets/{PluginsPanel-CzGMK-Ap.css → PluginsPanel-DsmiW9TU.css} +1 -1
  24. package/dist/web/assets/PluginsPanel-DtbXSoXu.js +1 -0
  25. package/dist/web/assets/{ProjectList-CRtc3Hhx.js → ProjectList-Cb25pa2s.js} +1 -1
  26. package/dist/web/assets/{PromptsDrawer-CvT9dA2N.js → PromptsDrawer-D39OJVRP.js} +1 -1
  27. package/dist/web/assets/{RecentSessionsDrawer-DM1gf584.js → RecentSessionsDrawer-DkCjc4kS.js} +1 -1
  28. package/dist/web/assets/{SessionCard-cZR2V4Br.js → SessionCard-5Mwig8zy.js} +1 -1
  29. package/dist/web/assets/{SessionList-D7-P4Zme.js → SessionList-DVkIlraF.js} +1 -1
  30. package/dist/web/assets/{SettingsDrawer-gW8kh-S8.js → SettingsDrawer-Cr9qZDgO.js} +1 -1
  31. package/dist/web/assets/{SkillManager-D0BlI7CG.js → SkillManager-D935sPSV.js} +1 -1
  32. package/dist/web/assets/{SkillsDrawer-BMB6uzqP.js → SkillsDrawer-D1_3j74b.js} +1 -1
  33. package/dist/web/assets/{SkillsPanel-BzR828De.js → SkillsPanel-6SehnNG8.js} +1 -1
  34. package/dist/web/assets/{Upload-3fNyOQrx.js → Upload-38eKB6wv.js} +1 -1
  35. package/dist/web/assets/{WorkspaceDrawer-DvtBA58Q.js → WorkspaceDrawer-Bkfmlr5M.js} +1 -1
  36. package/dist/web/assets/{WorkspaceManager-BW5KtsHO.js → WorkspaceManager-DFVQ5YoE.js} +1 -1
  37. package/dist/web/assets/{WorkspacePanel-BC_xpAEZ.js → WorkspacePanel-CEnogXEv.js} +1 -1
  38. package/dist/web/assets/client-oU6RT-6I.js +1 -0
  39. package/dist/web/assets/dsh-agent-presets-DjJRVMT6.js +1 -0
  40. package/dist/web/assets/index-CCATqHAD.js +2 -0
  41. package/dist/web/assets/index-p-yggtLj.css +1 -0
  42. package/dist/web/assets/{platforms-DrzAtnjY.js → platforms-DILqyP72.js} +1 -1
  43. package/dist/web/assets/platforms-JV36QBCw.js +1 -0
  44. package/dist/web/assets/{project-config-BVt5s-pH.js → project-config-CzFKBw_m.js} +1 -1
  45. package/dist/web/assets/{sessions-CaWUaysM.js → sessions-WXODMvLY.js} +1 -1
  46. package/dist/web/assets/{sessions-53-Fr68H.js → sessions-wvj5oaYr.js} +1 -1
  47. package/dist/web/assets/{useResponsiveDrawer-Ckgj760z.js → useResponsiveDrawer-BWRBXImg.js} +1 -1
  48. package/dist/web/assets/{workspaces-DQuZd4qw.js → workspaces-D2yLnT-B.js} +1 -1
  49. package/dist/web/index.html +5 -5
  50. package/package.json +4 -4
  51. package/src/platforms/agents-service.js +50 -12
  52. package/src/platforms/drivers/dsh/api-operations.js +60 -2
  53. package/src/platforms/drivers/dsh/common.js +222 -11
  54. package/src/platforms/drivers/dsh/native-config.js +2 -2
  55. package/src/platforms/drivers/dsh/native-logs.js +16 -1
  56. package/src/platforms/drivers/dsh/plugin-manager.js +64 -6
  57. package/src/platforms/drivers/omp/channels-implementation.js +1 -1
  58. package/src/platforms/drivers/omp/gateway.js +28 -2
  59. package/src/platforms/drivers/omp/model-role-settings.js +235 -0
  60. package/src/platforms/drivers/opencode/conversion.js +60 -8
  61. package/src/platforms/manifest-schema.js +4 -0
  62. package/src/platforms/manifests/dsh.json +29 -4
  63. package/src/platforms/manifests/omp.json +5 -4
  64. package/src/platforms/registry.js +1 -0
  65. package/src/server/api/convert.js +26 -76
  66. package/src/server/api/platforms.js +1 -0
  67. package/src/server/api/settings.js +27 -0
  68. package/src/server/services/model-definition-schema.js +2 -2
  69. package/src/server/services/model-detector.js +13 -22
  70. package/src/server/services/native-log-observer.js +1 -0
  71. package/src/server/services/plugins-service.js +13 -14
  72. package/src/server/services/request-logger.js +95 -0
  73. package/src/server/services/usage-log-utils.js +2 -1
  74. package/dist/web/assets/AgentsDrawer-DooV5Qhx.js +0 -1
  75. package/dist/web/assets/AgentsDrawer-Pw2k-ZAa.css +0 -1
  76. package/dist/web/assets/GatewayConvertDrawer-Av3yzwhP.js +0 -1
  77. package/dist/web/assets/GatewayConvertDrawer-CuNlf7AC.css +0 -1
  78. package/dist/web/assets/Home-B01aL_SR.js +0 -1
  79. package/dist/web/assets/Home-CSuFuJ9Y.css +0 -1
  80. package/dist/web/assets/PluginsPanel-kX14E_DT.js +0 -1
  81. package/dist/web/assets/client-BgcNg4Bt.js +0 -1
  82. package/dist/web/assets/index-BFqnddoI.js +0 -2
  83. package/dist/web/assets/index-Bn8BUnwP.css +0 -1
  84. package/dist/web/assets/platforms-mqqe_82d.js +0 -1
@@ -0,0 +1,235 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ getOmpPaths,
5
+ readOmpSettingsStrict,
6
+ readYamlFile,
7
+ writeOmpSettingsAtomic
8
+ } = require('./config');
9
+ const { getPlatformCatalog } = require('../../../server/services/platform-catalog');
10
+
11
+ const OMP_MODEL_ROLE_KEYS = Object.freeze([
12
+ 'default',
13
+ 'smol',
14
+ 'slow',
15
+ 'plan',
16
+ 'commit',
17
+ 'vision',
18
+ 'designer',
19
+ 'task',
20
+ 'advisor',
21
+ 'tiny'
22
+ ]);
23
+
24
+ const OMP_THINKING_LEVELS = new Set([
25
+ 'minimal',
26
+ 'low',
27
+ 'medium',
28
+ 'high',
29
+ 'xhigh',
30
+ 'max'
31
+ ]);
32
+
33
+ function isPlainObject(value) {
34
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
35
+ }
36
+
37
+ function parseModelRoleSelector(value) {
38
+ const selector = typeof value === 'string' ? value.trim() : '';
39
+ const suffix = selector.match(/:(minimal|low|medium|high|xhigh|max)$/i);
40
+ if (!suffix) return { model: selector, thinkingLevel: '' };
41
+
42
+ return {
43
+ model: selector.slice(0, -suffix[0].length),
44
+ thinkingLevel: suffix[1].toLowerCase()
45
+ };
46
+ }
47
+
48
+ function normalizeProviderId(value = '') {
49
+ return String(value || '')
50
+ .trim()
51
+ .toLowerCase()
52
+ .replace(/[^a-z0-9_-]/g, '-')
53
+ .replace(/-+/g, '-')
54
+ .replace(/^-|-$/g, '')
55
+ || 'coding-tool-x';
56
+ }
57
+
58
+ function readEnabledOmpChannels() {
59
+ const driver = getPlatformCatalog().driver('omp', 'channels');
60
+ const result = driver?.getEnabled?.();
61
+ if (!result || result.status !== 'ok') {
62
+ throw new Error(result?.error || 'Unable to read enabled OMP channels');
63
+ }
64
+ const channels = Array.isArray(result.data)
65
+ ? result.data
66
+ : (Array.isArray(result.data?.channels) ? result.data.channels : []);
67
+ return channels.filter(channel => channel && channel.enabled !== false);
68
+ }
69
+
70
+ function modelIdFromConfig(value) {
71
+ const candidate = typeof value === 'string' ? value : (value?.id || value?.name || '');
72
+ return String(candidate || '').trim().replace(/:(minimal|low|medium|high|xhigh|max|off)$/i, '');
73
+ }
74
+
75
+ function uniqueModelIds(values = []) {
76
+ const seen = new Set();
77
+ const result = [];
78
+ for (const value of values) {
79
+ const modelId = modelIdFromConfig(value);
80
+ const key = modelId.toLowerCase();
81
+ if (!modelId || seen.has(key)) continue;
82
+ seen.add(key);
83
+ result.push(modelId);
84
+ }
85
+ return result;
86
+ }
87
+
88
+ function getManagedProviderId(channel = {}) {
89
+ const rawId = channel.managedProviderId
90
+ || channel.extra?.managedProviderId
91
+ || channel.providerKey
92
+ || channel.provider
93
+ || channel.name
94
+ || channel.id;
95
+ const normalizedId = normalizeProviderId(rawId);
96
+ return normalizedId.startsWith('ctx-') ? normalizedId : `ctx-${normalizedId}`;
97
+ }
98
+
99
+ function findConfiguredProviderId(providers, managedProviderId) {
100
+ const target = normalizeProviderId(managedProviderId);
101
+ return Object.keys(providers).find(providerId => normalizeProviderId(providerId) === target)
102
+ || managedProviderId;
103
+ }
104
+
105
+ function collectNativeOAuthModelOptions(channel) {
106
+ const rawProviderId = String(channel.oauthProviderId || channel.authRef?.providerId || '').trim();
107
+ if (!rawProviderId) return [];
108
+ const providerId = normalizeProviderId(rawProviderId);
109
+ const allowedModels = Array.isArray(channel.allowedModels) ? channel.allowedModels : [];
110
+ const candidates = allowedModels.length > 0
111
+ ? allowedModels
112
+ : [
113
+ ...(Array.isArray(channel.models) ? channel.models : []),
114
+ channel.model
115
+ ];
116
+ return uniqueModelIds(candidates).map(modelId => `${providerId}/${modelId}`);
117
+ }
118
+
119
+ function collectManagedChannelModelOptions(channel, providers) {
120
+ const managedProviderId = getManagedProviderId(channel);
121
+ const configuredProviderId = findConfiguredProviderId(providers, managedProviderId);
122
+ const configuredModels = providers[configuredProviderId]?.models;
123
+ const channelModels = [
124
+ ...(Array.isArray(channel.models) ? channel.models : []),
125
+ ...(Array.isArray(channel.allowedModels) ? channel.allowedModels : []),
126
+ channel.model,
127
+ channel.speedTestModel,
128
+ ...(Array.isArray(channel.modelBindings)
129
+ ? channel.modelBindings.map(binding => binding?.modelId || binding?.id || binding)
130
+ : [])
131
+ ];
132
+ const modelIds = uniqueModelIds(Array.isArray(configuredModels) && configuredModels.length > 0
133
+ ? configuredModels
134
+ : channelModels);
135
+ return modelIds.map(modelId => `${configuredProviderId}/${modelId}`);
136
+ }
137
+
138
+ function collectModelOptions(modelsConfig = {}, channels = []) {
139
+ const options = new Set();
140
+ const providers = isPlainObject(modelsConfig.providers) ? modelsConfig.providers : {};
141
+ for (const channel of channels) {
142
+ const isNativeOAuth = channel.authMode === 'oauth' && !String(channel.baseUrl || '').trim();
143
+ const channelOptions = isNativeOAuth
144
+ ? collectNativeOAuthModelOptions(channel)
145
+ : collectManagedChannelModelOptions(channel, providers);
146
+ channelOptions.forEach(option => options.add(option));
147
+ }
148
+ return [...options].sort((left, right) => left.localeCompare(right));
149
+ }
150
+
151
+ function readOmpModelRoleSettings() {
152
+ const settings = readOmpSettingsStrict();
153
+ const modelRoles = isPlainObject(settings.modelRoles) ? settings.modelRoles : {};
154
+ const roles = Object.fromEntries(OMP_MODEL_ROLE_KEYS.map((key) => [
155
+ key,
156
+ parseModelRoleSelector(modelRoles[key])
157
+ ]));
158
+ const modelsConfig = readYamlFile(getOmpPaths().modelsYml, {});
159
+ const modelOptions = collectModelOptions(modelsConfig, readEnabledOmpChannels());
160
+ const optionSet = new Set(modelOptions);
161
+
162
+ return {
163
+ roles,
164
+ modelOptions,
165
+ unavailableRoles: Object.fromEntries(Object.entries(roles)
166
+ .filter(([, role]) => role.model && !optionSet.has(role.model))
167
+ .map(([key]) => [key, true]))
168
+ };
169
+ }
170
+
171
+ function updateOmpModelRoleSettings(input) {
172
+ if (!isPlainObject(input)) {
173
+ throw new Error('roles must be an object');
174
+ }
175
+
176
+ const settings = readOmpSettingsStrict();
177
+ const modelRoles = isPlainObject(settings.modelRoles) ? { ...settings.modelRoles } : {};
178
+ const currentModelRoles = isPlainObject(settings.modelRoles) ? settings.modelRoles : {};
179
+ const modelOptions = new Set(collectModelOptions(
180
+ readYamlFile(getOmpPaths().modelsYml, {}),
181
+ readEnabledOmpChannels()
182
+ ));
183
+ for (const [key, role] of Object.entries(input)) {
184
+ if (!OMP_MODEL_ROLE_KEYS.includes(key)) {
185
+ throw new Error(`roles.${key} is not a supported OMP model role`);
186
+ }
187
+ if (!isPlainObject(role) || typeof role.model !== 'string') {
188
+ throw new Error(`roles.${key}.model must be a string`);
189
+ }
190
+
191
+ const suppliedModel = role.model.trim();
192
+ if (suppliedModel.length > 500) {
193
+ throw new Error(`roles.${key}.model must be 500 characters or fewer`);
194
+ }
195
+
196
+ const suppliedThinkingLevel = role.thinkingLevel == null ? '' : role.thinkingLevel;
197
+ if (typeof suppliedThinkingLevel !== 'string'
198
+ || (suppliedThinkingLevel && !OMP_THINKING_LEVELS.has(suppliedThinkingLevel))) {
199
+ throw new Error(`roles.${key}.thinkingLevel must be one of ${[...OMP_THINKING_LEVELS].join(', ')}`);
200
+ }
201
+
202
+ if (!suppliedModel) {
203
+ delete modelRoles[key];
204
+ continue;
205
+ }
206
+
207
+ const parsed = parseModelRoleSelector(suppliedModel);
208
+ if (!parsed.model) {
209
+ throw new Error(`roles.${key}.model must include a model selector`);
210
+ }
211
+ const thinkingLevel = suppliedThinkingLevel || parsed.thinkingLevel;
212
+ const nextSelector = thinkingLevel ? `${parsed.model}:${thinkingLevel}` : parsed.model;
213
+ const currentSelector = String(currentModelRoles[key] || '').trim();
214
+ if (!modelOptions.has(parsed.model) && nextSelector !== currentSelector) {
215
+ throw new Error(`roles.${key}.model must use a model from an enabled OMP channel`);
216
+ }
217
+ modelRoles[key] = nextSelector;
218
+ }
219
+
220
+ if (Object.keys(modelRoles).length > 0) settings.modelRoles = modelRoles;
221
+ else delete settings.modelRoles;
222
+
223
+ writeOmpSettingsAtomic(settings);
224
+ return readOmpModelRoleSettings();
225
+ }
226
+
227
+ module.exports = {
228
+ OMP_MODEL_ROLE_KEYS,
229
+ OMP_THINKING_LEVELS,
230
+ collectModelOptions,
231
+ readEnabledOmpChannels,
232
+ parseModelRoleSelector,
233
+ readOmpModelRoleSettings,
234
+ updateOmpModelRoleSettings
235
+ };
@@ -2,22 +2,69 @@
2
2
 
3
3
  function createDriver(context = {}) {
4
4
  const converter = require('./gateway-converter');
5
+ const {
6
+ loadLatestProxyRequestHeaders,
7
+ loadLatestProxyRequestSnapshot
8
+ } = require('../../../server/services/request-logger');
9
+ const target = context.platform || 'opencode';
10
+
11
+ function getOmpTargetContext() {
12
+ const snapshot = loadLatestProxyRequestSnapshot('omp');
13
+ const providerApi = snapshot?.route?.providerApi;
14
+ const targetApi = providerApi === 'openai-completions'
15
+ ? 'chat.completions'
16
+ : ['openai-responses', 'openai-codex-responses', 'azure-openai-responses'].includes(providerApi)
17
+ ? 'responses'
18
+ : null;
19
+ return { snapshot, providerApi, targetApi };
20
+ }
21
+
22
+ function convertPayload({ sourceType, payload, options = {} } = {}) {
23
+ const ompContext = target === 'omp' ? getOmpTargetContext() : null;
24
+ if (ompContext && !ompContext.targetApi) {
25
+ throw new Error('The latest OMP request log does not identify a supported OpenAI-compatible provider API. Send an OMP request through an OpenAI-compatible route, then retry.');
26
+ }
27
+ const requestedTargetApi = options.targetApi || options.api;
28
+ if (ompContext && requestedTargetApi && requestedTargetApi !== ompContext.targetApi) {
29
+ throw new Error(`The latest OMP request uses ${ompContext.providerApi}; conversion only supports ${ompContext.targetApi}.`);
30
+ }
31
+ const result = converter.convertToOpenCodePayload({
32
+ sourceType,
33
+ payload,
34
+ options: ompContext ? { ...options, targetApi: ompContext.targetApi } : options
35
+ });
36
+ return {
37
+ ...result,
38
+ target,
39
+ ...(ompContext?.snapshot?.route?.path ? { endpoint: ompContext.snapshot.route.path } : {}),
40
+ requestHeaders: loadLatestProxyRequestHeaders(target)
41
+ };
42
+ }
43
+
5
44
  return {
6
45
  platform: context.platform,
7
46
  capability: 'conversion',
8
47
  formats() {
48
+ const ompContext = target === 'omp' ? getOmpTargetContext() : null;
49
+ const targetApis = target === 'omp'
50
+ ? (ompContext.targetApi ? [ompContext.targetApi] : [])
51
+ : converter.SUPPORTED_TARGET_APIS;
9
52
  return {
10
53
  sourceTypes: converter.SUPPORTED_SOURCE_TYPES,
11
54
  formats: converter.SUPPORTED_SOURCE_TYPES.map(type => ({
12
55
  id: type,
13
56
  name: type === 'claude' ? 'Claude Code' : type === 'codex' ? 'Codex' : 'Gemini'
14
57
  })),
15
- targetApis: converter.SUPPORTED_TARGET_APIS,
16
- defaultTargetApi: 'responses',
17
- endpoints: {
18
- responses: '/v1/responses',
19
- 'chat.completions': '/v1/chat/completions'
20
- }
58
+ targetApis,
59
+ defaultTargetApi: targetApis[0] || null,
60
+ endpoints: ompContext
61
+ ? (ompContext.targetApi && ompContext.snapshot?.route?.path
62
+ ? { [ompContext.targetApi]: ompContext.snapshot.route.path }
63
+ : {})
64
+ : {
65
+ responses: '/v1/responses',
66
+ 'chat.completions': '/v1/chat/completions'
67
+ }
21
68
  };
22
69
  },
23
70
  getFormats() {
@@ -30,10 +77,15 @@ function createDriver(context = {}) {
30
77
  codex: converter.convertCodexToOpenCodePayload,
31
78
  gemini: converter.convertGeminiToOpenCodePayload
32
79
  };
33
- return (handlers[sourceType] || converter.convertToOpenCodePayload)({ sourceType, payload, options });
80
+ if (target === 'omp') return convertPayload({ sourceType, payload, options });
81
+ return {
82
+ ...(handlers[sourceType] || converter.convertToOpenCodePayload)({ sourceType, payload, options }),
83
+ target,
84
+ requestHeaders: loadLatestProxyRequestHeaders(target)
85
+ };
34
86
  },
35
87
  convert({ sourceType, payload, options = {} } = {}) {
36
- return converter.convertToOpenCodePayload({ sourceType, payload, options });
88
+ return convertPayload({ sourceType, payload, options });
37
89
  }
38
90
  };
39
91
  }
@@ -68,6 +68,7 @@ const CAPABILITY_OPERATIONS = Object.freeze({
68
68
  'listProfileCapabilities',
69
69
  'listProfileMcp',
70
70
  'listProfilePrompts',
71
+ 'listProfileAgentPresets',
71
72
  'installPlugin',
72
73
  'uninstallPlugin',
73
74
  'updatePlugin',
@@ -75,6 +76,8 @@ const CAPABILITY_OPERATIONS = Object.freeze({
75
76
  'deleteMcp',
76
77
  'upsertPrompt',
77
78
  'deletePrompt',
79
+ 'upsertAgentPreset',
80
+ 'deleteAgentPreset',
78
81
  'listSkills',
79
82
  'getSkill',
80
83
  'createSkill',
@@ -113,6 +116,7 @@ const schema = {
113
116
  portLabel: { type: 'string', minLength: 1 },
114
117
  apiBasePath: { type: 'string', minLength: 1 },
115
118
  logFile: { type: 'string', minLength: 1 },
119
+ logLayout: { enum: ['basic'] },
116
120
  logAliases: { type: 'array', items: { type: 'string', minLength: 1 } },
117
121
  logNote: { type: 'string' },
118
122
  logColor: { type: 'string', minLength: 1 },
@@ -5,6 +5,7 @@
5
5
  "command": "dsh",
6
6
  "iconToken": "terminal",
7
7
  "color": "#2563eb",
8
+ "logLayout": "basic",
8
9
  "defaultVisible": true,
9
10
  "defaultEnabled": false,
10
11
  "sortOrder": 50,
@@ -25,7 +26,7 @@
25
26
  "resourceTypes": {
26
27
  "skills": true,
27
28
  "commands": false,
28
- "agents": false,
29
+ "agents": true,
29
30
  "plugins": true,
30
31
  "mcp": true,
31
32
  "prompts": true
@@ -38,10 +39,10 @@
38
39
  "refresh": true
39
40
  },
40
41
  "plugins": {
41
- "repositories": true,
42
+ "repositories": false,
42
43
  "market": false,
43
- "install": false,
44
- "uninstall": false,
44
+ "install": true,
45
+ "uninstall": true,
45
46
  "import": false,
46
47
  "syncRepos": false
47
48
  }
@@ -477,6 +478,30 @@
477
478
  "request": "default",
478
479
  "response": "default"
479
480
  },
481
+ {
482
+ "method": "GET",
483
+ "path": "/profiles/:profileName/agent-presets",
484
+ "capability": "api",
485
+ "operation": "listProfileAgentPresets",
486
+ "request": "default",
487
+ "response": "default"
488
+ },
489
+ {
490
+ "method": "POST",
491
+ "path": "/profiles/:profileName/agent-presets",
492
+ "capability": "api",
493
+ "operation": "upsertAgentPreset",
494
+ "request": "default",
495
+ "response": "default"
496
+ },
497
+ {
498
+ "method": "DELETE",
499
+ "path": "/profiles/:profileName/agent-presets/:presetId",
500
+ "capability": "api",
501
+ "operation": "deleteAgentPreset",
502
+ "request": "default",
503
+ "response": "default"
504
+ },
480
505
  {
481
506
  "method": "GET",
482
507
  "path": "/skills",
@@ -45,11 +45,11 @@
45
45
  "resourceTypes": {
46
46
  "skills": true,
47
47
  "commands": true,
48
- "agents": false,
48
+ "agents": true,
49
49
  "plugins": true
50
50
  },
51
51
  "agentCapabilities": {
52
- "projectScope": false,
52
+ "projectScope": true,
53
53
  "repoOperations": true
54
54
  },
55
55
  "skillActivation": {
@@ -73,7 +73,7 @@
73
73
  ]
74
74
  },
75
75
  "agents": {
76
- "canonicalRoot": null
76
+ "canonicalRoot": ".omp/agents"
77
77
  },
78
78
  "commands": {
79
79
  "canonicalRoot": ".omp/commands",
@@ -484,6 +484,7 @@
484
484
  "hooks": "legacy:omp",
485
485
  "resourceSync": "legacy:omp",
486
486
  "mcp": "legacy:omp",
487
- "prompts": "legacy:omp"
487
+ "prompts": "legacy:omp",
488
+ "conversion": "opencode-conversion"
488
489
  }
489
490
  }
@@ -188,6 +188,7 @@ function createPlatformRegistry({
188
188
  portLabel: platform.portLabel || null,
189
189
  apiPrefix: platform.api?.prefix || null,
190
190
  modelCatalogKey: platform.modelConfig?.catalogKey || platform.key,
191
+ ...(platform.logLayout ? { logLayout: platform.logLayout } : {}),
191
192
  capabilities
192
193
  };
193
194
  const resourceTypes = publicResourceTypes(platform.resourceTypes);
@@ -1,40 +1,28 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
3
  const { getPlatformCatalog } = require('../services/platform-catalog');
4
+ const { createSameOriginGuard } = require('../services/network-access');
4
5
 
5
- /**
6
- * 获取支持的格式列表
7
- * GET /api/convert/formats
8
- */
9
- router.get('/formats', (req, res) => {
10
- const driver = getPlatformCatalog().driver('opencode', 'conversion');
6
+ router.use(createSameOriginGuard({
7
+ message: '禁止跨站访问请求转换接口'
8
+ }));
9
+
10
+ function getConversionDriver(target) {
11
+ return getPlatformCatalog().driver(target, 'conversion');
12
+ }
13
+
14
+ function getConversionFormats(driver) {
11
15
  const formats = driver?.formats?.() || driver?.getFormats?.();
12
- if (!formats) {
13
- return res.status(404).json({ success: false, error: 'Conversion is not supported' });
14
- }
15
- const sourceTypes = formats.sourceTypes || [];
16
- res.json({
17
- formats: sourceTypes.map(type => ({
18
- id: type,
19
- name: (formats.formats || []).find(item => item.id === type)?.name
20
- || String(type).replace(/[-_]/g, ' ').replace(/\b\w/g, char => char.toUpperCase()),
21
- description: `${type} session format`,
22
- extension: '.jsonl',
23
- icon: type
24
- })),
25
- conversions: sourceTypes.flatMap(sourceType => sourceTypes
26
- .filter(targetType => targetType !== sourceType)
27
- .map(targetType => ({ from: sourceType, to: targetType })))
28
- });
29
- });
16
+ return formats;
17
+ }
30
18
 
31
19
  /**
32
- * 获取 OpenCode 网关支持格式
33
- * GET /api/convert/opencode/formats
20
+ * Read conversion capabilities for a target CLI.
21
+ * GET /api/convert/:target/formats
34
22
  */
35
- router.get('/opencode/formats', (req, res) => {
36
- const driver = getConversionDriver();
37
- const formats = driver?.formats?.() || driver?.getFormats?.();
23
+ router.get('/:target/formats', (req, res) => {
24
+ const driver = getConversionDriver(req.params.target);
25
+ const formats = getConversionFormats(driver);
38
26
  if (!formats) {
39
27
  return res.status(404).json({ success: false, error: 'Conversion is not supported' });
40
28
  }
@@ -45,62 +33,24 @@ router.get('/opencode/formats', (req, res) => {
45
33
  name: (formats.formats || []).find(item => item.id === type)?.name
46
34
  || String(type).replace(/[-_]/g, ' ').replace(/\b\w/g, char => char.toUpperCase())
47
35
  })),
48
- target: 'opencode',
36
+ target: req.params.target,
49
37
  targetApis: formats.targetApis || [],
50
38
  defaultTargetApi: formats.defaultTargetApi,
51
- endpoints: formats.endpoints || {},
52
- sourceEndpoints: Object.fromEntries(sourceTypes.map(type => [type, `/api/convert/opencode/${type}`]))
39
+ endpoints: formats.endpoints || {}
53
40
  });
54
41
  });
55
42
 
56
- function getConversionDriver() {
57
- return getPlatformCatalog().driver('opencode', 'conversion');
58
- }
59
-
60
- function handleOpenCodeConvert(req, res, sourceType) {
61
- try {
62
- const { payload, options = {} } = req.body || {};
63
- const driver = getConversionDriver();
64
-
65
- if (!driver) {
66
- return res.status(404).json({ success: false, error: 'Conversion is not supported' });
67
- }
68
- if (!payload) {
69
- return res.status(400).json({
70
- success: false,
71
- error: 'Missing required parameters: payload'
72
- });
73
- }
74
- const result = driver.convertSource
75
- ? driver.convertSource(sourceType, payload, options)
76
- : driver.convert({ sourceType, payload, options });
77
- return res.json({
78
- success: true,
79
- ...result
80
- });
81
- } catch (error) {
82
- console.error(`[Convert API] OpenCode ${sourceType} convert error:`, error);
83
- return res.status(500).json({
84
- success: false,
85
- error: error.message
86
- });
87
- }
88
- }
89
-
90
- router.post('/opencode/:sourceType', (req, res) => {
91
- return handleOpenCodeConvert(req, res, req.params.sourceType);
92
- });
93
-
94
43
  /**
95
- * 在线转换为 OpenCode 可处理格式
96
- * POST /api/convert/opencode
44
+ * Online conversion to a CLI-compatible request format.
45
+ * POST /api/convert/:target
97
46
  * Body: { sourceType, payload, options? }
98
47
  */
99
- router.post('/opencode', (req, res) => {
48
+ router.post('/:target', (req, res) => {
100
49
  try {
101
50
  const { sourceType, payload, options = {} } = req.body || {};
102
- const driver = getConversionDriver();
103
- const sourceTypes = driver?.formats?.().sourceTypes || driver?.getFormats?.().sourceTypes || [];
51
+ const driver = getConversionDriver(req.params.target);
52
+ const formats = getConversionFormats(driver);
53
+ const sourceTypes = formats?.sourceTypes || [];
104
54
  const normalized = driver?.normalizeSourceType?.(sourceType) || sourceType;
105
55
 
106
56
  if (!driver) {
@@ -124,7 +74,7 @@ router.post('/opencode', (req, res) => {
124
74
  ...driver.convert({ sourceType: normalized, payload, options })
125
75
  });
126
76
  } catch (error) {
127
- console.error('[Convert API] OpenCode gateway convert error:', error);
77
+ console.error(`[Convert API] ${req.params.target} convert error:`, error);
128
78
  return res.status(500).json({
129
79
  success: false,
130
80
  error: error.message
@@ -18,6 +18,7 @@ const PUBLIC_FIELDS = Object.freeze([
18
18
  'portLabel',
19
19
  'apiPrefix',
20
20
  'modelCatalogKey',
21
+ 'logLayout',
21
22
  'promptLabel',
22
23
  'resourceTypes',
23
24
  'resourceActions'
@@ -9,6 +9,10 @@ const {
9
9
  } = require('../../config/model-metadata');
10
10
  const { getPlatformCatalog } = require('../services/platform-catalog');
11
11
  const { loadConfig, saveConfig } = require('../../config/loader');
12
+ const {
13
+ readOmpModelRoleSettings,
14
+ updateOmpModelRoleSettings
15
+ } = require('../../platforms/drivers/omp/model-role-settings');
12
16
  const {
13
17
  MODEL_SCHEMA_VERSION,
14
18
  getPublicModelFieldSchema,
@@ -269,4 +273,27 @@ function handleDeleteModelOverride(req, res) {
269
273
  router.delete('/model-settings/:modelId', handleDeleteModelOverride);
270
274
  router.delete('/model-metadata/:modelId', handleDeleteModelOverride);
271
275
 
276
+ router.get('/omp-model-roles', (req, res) => {
277
+ try {
278
+ res.json({ success: true, ...readOmpModelRoleSettings() });
279
+ } catch (error) {
280
+ console.error('Error getting OMP model roles:', error);
281
+ res.status(500).json({ error: error.message });
282
+ }
283
+ });
284
+
285
+ router.put('/omp-model-roles', (req, res) => {
286
+ try {
287
+ const { roles } = req.body || {};
288
+ if (!roles || typeof roles !== 'object' || Array.isArray(roles)) {
289
+ return res.status(400).json({ error: 'roles must be an object' });
290
+ }
291
+ res.json({ success: true, ...updateOmpModelRoleSettings(roles) });
292
+ } catch (error) {
293
+ console.error('Error saving OMP model roles:', error);
294
+ const isValidationError = /^roles(?:\.| must be)/.test(error.message);
295
+ res.status(isValidationError ? 400 : 500).json({ error: error.message });
296
+ }
297
+ });
298
+
272
299
  module.exports = router;
@@ -3,7 +3,7 @@
3
3
  const MODEL_SCHEMA_VERSION = 2;
4
4
  const MODEL_METADATA_MODES = ['auto', 'hybrid', 'manual'];
5
5
  const THINKING_EFFORT_LEVELS = ['minimal', 'low', 'medium', 'high', 'xhigh'];
6
- const THINKING_SELECTOR_LEVELS = [...THINKING_EFFORT_LEVELS, 'off'];
6
+ const THINKING_SELECTOR_LEVELS = [...THINKING_EFFORT_LEVELS, 'max', 'off'];
7
7
  const THINKING_MODES = [
8
8
  'effort',
9
9
  'budget',
@@ -173,7 +173,7 @@ function compactObject(value = {}) {
173
173
 
174
174
  function splitModelSelector(value = '') {
175
175
  const raw = String(value || '').trim();
176
- const match = raw.match(/:(minimal|low|medium|high|xhigh|off)$/i);
176
+ const match = raw.match(/:(minimal|low|medium|high|xhigh|max|off)$/i);
177
177
  if (!match) return { modelId: raw, thinkingLevel: null };
178
178
  return {
179
179
  modelId: raw.slice(0, -match[0].length),