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
@@ -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,295 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const MANAGED_PREFIX = 'ctx-';
6
+ const SUPPORTED_PROVIDER_APIS = new Set([
7
+ 'openai-completions',
8
+ 'openai-responses',
9
+ 'openai-codex-responses',
10
+ 'azure-openai-responses',
11
+ 'anthropic-messages',
12
+ 'google-generative-ai',
13
+ 'google-gemini-cli',
14
+ 'google-vertex'
15
+ ]);
16
+
17
+ function normalizeText(value) {
18
+ return String(value || '').trim();
19
+ }
20
+
21
+ function normalizeProviderId(value) {
22
+ return normalizeText(value)
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9._-]+/g, '-')
25
+ .replace(/^-+|-+$/g, '') || 'provider';
26
+ }
27
+
28
+ function getProviderKey(channel = {}) {
29
+ return normalizeText(channel.providerKey || channel.provider || channel.name || channel.id);
30
+ }
31
+
32
+ function getProviderApi(channel = {}) {
33
+ return normalizeText(channel.providerApi || channel.api || channel.wireApi || 'openai-completions');
34
+ }
35
+
36
+ function getRoutingGroup(channel = {}) {
37
+ return normalizeText(channel.routingGroup) || normalizeText(channel.id);
38
+ }
39
+
40
+ function stableToken(secret, namespace, value, length = 24) {
41
+ return crypto
42
+ .createHmac('sha256', String(secret || ''))
43
+ .update(`${namespace}\0${value}`)
44
+ .digest('hex')
45
+ .slice(0, length);
46
+ }
47
+
48
+ function stablePublicId(namespace, value, length = 8) {
49
+ return crypto
50
+ .createHash('sha256')
51
+ .update(`${namespace}\0${value}`)
52
+ .digest('hex')
53
+ .slice(0, length);
54
+ }
55
+
56
+ function normalizeRedirects(channel = {}) {
57
+ const redirects = Array.isArray(channel.modelRedirects) ? channel.modelRedirects : [];
58
+ return JSON.stringify(redirects);
59
+ }
60
+
61
+ function canonicalize(value) {
62
+ if (Array.isArray(value)) return value.map(canonicalize);
63
+ if (value && typeof value === 'object') {
64
+ return Object.fromEntries(
65
+ Object.keys(value).sort().map(key => [key, canonicalize(value[key])])
66
+ );
67
+ }
68
+ return value;
69
+ }
70
+
71
+ function getModelCapabilityMap(channel = {}) {
72
+ const result = new Map();
73
+ const add = (raw) => {
74
+ const model = typeof raw === 'string' ? { id: raw } : raw;
75
+ const id = normalizeText(model?.id || model?.name);
76
+ if (!id || result.has(id)) return;
77
+ const capability = { ...(model || {}) };
78
+ delete capability.id;
79
+ delete capability.name;
80
+ delete capability.baseUrl;
81
+ delete capability.headers;
82
+ result.set(id, JSON.stringify(canonicalize(capability)));
83
+ };
84
+ (Array.isArray(channel.models) ? channel.models : []).forEach(add);
85
+ (Array.isArray(channel.allowedModels) ? channel.allowedModels : []).forEach(add);
86
+ add(channel.model);
87
+ return result;
88
+ }
89
+
90
+ function getGroupKey(channel = {}) {
91
+ return [
92
+ getProviderKey(channel),
93
+ getProviderApi(channel),
94
+ getRoutingGroup(channel)
95
+ ].join('\0');
96
+ }
97
+
98
+ function assertCompatibleGroup(channels = []) {
99
+ if (channels.length <= 1) return;
100
+ const first = channels[0];
101
+ const expectedApi = getProviderApi(first);
102
+ const expectedProvider = getProviderKey(first);
103
+ const expectedRedirects = normalizeRedirects(first);
104
+ const expectedAuthMode = normalizeText(first.authMode) || 'api_key';
105
+ const expectedCapabilities = getModelCapabilityMap(first);
106
+
107
+ channels.slice(1).forEach((channel) => {
108
+ if (getProviderKey(channel) !== expectedProvider) {
109
+ throw new Error(`OMP routing group "${getRoutingGroup(first)}" mixes providerKey values`);
110
+ }
111
+ if (getProviderApi(channel) !== expectedApi) {
112
+ throw new Error(`OMP routing group "${getRoutingGroup(first)}" mixes providerApi values`);
113
+ }
114
+ if (normalizeRedirects(channel) !== expectedRedirects) {
115
+ throw new Error(`OMP routing group "${getRoutingGroup(first)}" has inconsistent modelRedirects`);
116
+ }
117
+ if ((normalizeText(channel.authMode) || 'api_key') !== expectedAuthMode) {
118
+ throw new Error(`OMP routing group "${getRoutingGroup(first)}" mixes authMode values`);
119
+ }
120
+ const actualCapabilities = getModelCapabilityMap(channel);
121
+ expectedCapabilities.forEach((capability, modelId) => {
122
+ if (actualCapabilities.has(modelId) && actualCapabilities.get(modelId) !== capability) {
123
+ throw new Error(
124
+ `OMP routing group "${getRoutingGroup(first)}" has incompatible capability metadata for model "${modelId}"`
125
+ );
126
+ }
127
+ });
128
+ });
129
+ }
130
+
131
+ function sanitizeProviderConfig(channel = {}) {
132
+ const source = channel.providerConfig && typeof channel.providerConfig === 'object'
133
+ && !Array.isArray(channel.providerConfig)
134
+ ? channel.providerConfig
135
+ : {};
136
+ const next = { ...source };
137
+ for (const key of ['baseUrl', 'base_url', 'apiKey', 'api_key', 'headers', 'auth']) {
138
+ delete next[key];
139
+ }
140
+ return next;
141
+ }
142
+
143
+ function sanitizeModels(channel = {}) {
144
+ return (Array.isArray(channel.models) ? channel.models : []).map((model) => {
145
+ if (!model || typeof model !== 'object' || Array.isArray(model)) return model;
146
+ const next = { ...model };
147
+ delete next.baseUrl;
148
+ delete next.headers;
149
+ return next;
150
+ });
151
+ }
152
+
153
+ function mergeGroupField(channels = [], field) {
154
+ const seen = new Set();
155
+ const result = [];
156
+ channels.forEach((channel) => {
157
+ const values = Array.isArray(channel?.[field]) ? channel[field] : [];
158
+ values.forEach((value) => {
159
+ const id = normalizeText(typeof value === 'string' ? value : (value?.id || value?.name));
160
+ if (!id || seen.has(id.toLowerCase())) return;
161
+ seen.add(id.toLowerCase());
162
+ result.push(value);
163
+ });
164
+ });
165
+ return result;
166
+ }
167
+
168
+ function buildManagedBaseUrl(host, port, routeToken) {
169
+ return `http://${host}:${port}/omp/${routeToken}`;
170
+ }
171
+
172
+ function prepareManagedOmpChannels(channels = [], gateway = {}) {
173
+ const host = normalizeText(gateway.host) || '127.0.0.1';
174
+ const port = Number(gateway.port);
175
+ const secret = normalizeText(gateway.secret);
176
+ if (!Number.isInteger(port) || port <= 0) {
177
+ throw new Error('OMP gateway port is required before managed providers can be generated');
178
+ }
179
+ if (!secret) {
180
+ throw new Error('OMP gateway secret is required before managed providers can be generated');
181
+ }
182
+
183
+ const enabled = channels.filter(channel => channel && channel.enabled !== false && channel.baseUrl);
184
+ const groups = new Map();
185
+ enabled.forEach((channel) => {
186
+ const key = getGroupKey(channel);
187
+ if (!groups.has(key)) groups.set(key, []);
188
+ groups.get(key).push(channel);
189
+ });
190
+
191
+ const providerCounts = new Map();
192
+ groups.forEach((groupChannels) => {
193
+ const providerId = normalizeProviderId(getProviderKey(groupChannels[0]));
194
+ providerCounts.set(providerId, (providerCounts.get(providerId) || 0) + 1);
195
+ });
196
+
197
+ const routes = [];
198
+ const managedChannels = [];
199
+ const unsupportedChannels = [];
200
+
201
+ groups.forEach((groupChannels, groupKey) => {
202
+ assertCompatibleGroup(groupChannels);
203
+ const template = groupChannels[0];
204
+ if (!SUPPORTED_PROVIDER_APIS.has(getProviderApi(template))) {
205
+ groupChannels.forEach(channel => unsupportedChannels.push({
206
+ id: channel.id,
207
+ name: channel.name,
208
+ reason: 'unsupported-provider-api'
209
+ }));
210
+ return;
211
+ }
212
+ const authMode = normalizeText(template.authMode) || 'api_key';
213
+ const transport = normalizeText(template.transport || template.providerConfig?.transport);
214
+ const supportedOAuthIds = Array.isArray(gateway.supportedOAuthChannelIds)
215
+ ? new Set(gateway.supportedOAuthChannelIds)
216
+ : null;
217
+ const oauthGatewayUnavailable = authMode === 'oauth' && (
218
+ !normalizeText(template.apiKey)
219
+ || transport !== 'pi-native'
220
+ || (supportedOAuthIds && groupChannels.some(channel => !supportedOAuthIds.has(channel.id)))
221
+ );
222
+ if (oauthGatewayUnavailable) {
223
+ groupChannels.forEach(channel => unsupportedChannels.push({
224
+ id: channel.id,
225
+ name: channel.name,
226
+ reason: 'oauth-auth-gateway-unavailable'
227
+ }));
228
+ return;
229
+ }
230
+
231
+ const providerId = normalizeProviderId(getProviderKey(template));
232
+ const routeToken = stableToken(secret, 'route', groupKey);
233
+ const capability = `ctx_${stableToken(secret, 'capability', groupKey, 40)}`;
234
+ const multipleGroups = providerCounts.get(providerId) > 1;
235
+ const managedProviderId = multipleGroups
236
+ ? `${MANAGED_PREFIX}${providerId}-${stablePublicId('provider', groupKey)}`
237
+ : `${MANAGED_PREFIX}${providerId}`;
238
+ const routingGroup = getRoutingGroup(template);
239
+ const mergedModels = sanitizeModels({
240
+ models: mergeGroupField(groupChannels, 'models')
241
+ });
242
+ const mergedAllowedModels = mergeGroupField(groupChannels, 'allowedModels');
243
+
244
+ const route = {
245
+ token: routeToken,
246
+ capability,
247
+ routingGroup,
248
+ providerKey: getProviderKey(template),
249
+ providerApi: getProviderApi(template),
250
+ managedProviderId,
251
+ channelIds: groupChannels.map(channel => channel.id),
252
+ authMode
253
+ };
254
+ routes.push(route);
255
+
256
+ managedChannels.push({
257
+ ...template,
258
+ id: `omp-route-${routeToken}`,
259
+ managedProviderId,
260
+ originalProviderId: getProviderKey(template),
261
+ routingGroup,
262
+ baseUrl: buildManagedBaseUrl(host, port, routeToken),
263
+ apiKey: authMode === 'none' ? '' : capability,
264
+ authMode,
265
+ providerConfig: sanitizeProviderConfig(template),
266
+ headers: undefined,
267
+ auth: authMode === 'none' ? 'none' : undefined,
268
+ models: mergedModels,
269
+ allowedModels: mergedAllowedModels,
270
+ _ompGatewayRoute: route
271
+ });
272
+ });
273
+
274
+ return { routes, managedChannels, unsupportedChannels };
275
+ }
276
+
277
+ function resolveOmpGatewayRoute(pathname, channels, gateway) {
278
+ const match = String(pathname || '').match(/^\/omp\/([a-f0-9]{24})(\/.*|$)/);
279
+ if (!match) return null;
280
+ const prepared = prepareManagedOmpChannels(channels, gateway);
281
+ const route = prepared.routes.find(item => item.token === match[1]);
282
+ if (!route) return null;
283
+ return {
284
+ ...route,
285
+ upstreamPath: match[2] || '/'
286
+ };
287
+ }
288
+
289
+ module.exports = {
290
+ prepareManagedOmpChannels,
291
+ resolveOmpGatewayRoute,
292
+ getRoutingGroup,
293
+ getProviderKey,
294
+ getProviderApi
295
+ };