coding-tool-x 3.7.11 → 3.7.13

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 (44) hide show
  1. package/CHANGELOG.md +9 -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 +53 -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 +17 -4
  26. package/src/server/services/channel-health.js +5 -0
  27. package/src/server/services/channel-sync-utils.js +1 -1
  28. package/src/server/services/model-definition-schema.js +2 -1
  29. package/src/server/services/omp-channels.js +213 -26
  30. package/src/server/services/omp-config.js +71 -6
  31. package/src/server/services/omp-native-plugin-adapter.js +456 -0
  32. package/src/server/services/omp-settings-manager.js +3 -1
  33. package/src/server/services/omp-skill-discovery.js +358 -0
  34. package/src/server/services/omp-skill-settings-service.js +97 -0
  35. package/src/server/services/pi-omp-migration.js +273 -0
  36. package/src/server/services/platform-resolution.js +44 -0
  37. package/src/server/services/plugins-service.js +85 -561
  38. package/src/server/services/project-path-validation.js +65 -0
  39. package/src/server/services/skill-service.js +169 -77
  40. package/dist/web/assets/Home-Bft3Kztl.js +0 -1
  41. package/dist/web/assets/Home-CHZJkkjH.css +0 -1
  42. package/dist/web/assets/SkillManager-CnY7IXkX.js +0 -1
  43. package/dist/web/assets/SkillManager-D7pd-d_P.css +0 -1
  44. package/dist/web/assets/index-k5oP7c-A.js +0 -2
@@ -0,0 +1,358 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execFileSync } = require('child_process');
4
+ const {
5
+ getOmpCommand,
6
+ getOmpPaths,
7
+ readOmpSettings
8
+ } = require('./omp-config');
9
+ const { HOME_DIR, NATIVE_PATHS } = require('../../config/paths');
10
+
11
+ const DEFAULT_SKILL_SETTINGS = Object.freeze({
12
+ enabled: true,
13
+ enableCodexUser: true,
14
+ enableClaudeUser: true,
15
+ enableClaudeProject: true,
16
+ enablePiUser: true,
17
+ enablePiProject: true,
18
+ enableAgentsUser: true,
19
+ enableAgentsProject: true,
20
+ customDirectories: [],
21
+ ignoredSkills: [],
22
+ includeSkills: [],
23
+ disabledExtensions: []
24
+ });
25
+
26
+ function expandHome(value) {
27
+ const input = String(value || '').trim();
28
+ if (input === '~') return HOME_DIR;
29
+ if (input.startsWith('~/')) return path.join(HOME_DIR, input.slice(2));
30
+ return input;
31
+ }
32
+
33
+ function readEffectiveSettings(cwd) {
34
+ try {
35
+ const output = execFileSync(getOmpCommand(), ['config', 'list', '--json'], {
36
+ cwd: cwd || process.cwd(),
37
+ encoding: 'utf8',
38
+ stdio: ['ignore', 'pipe', 'pipe'],
39
+ timeout: 5000,
40
+ windowsHide: true
41
+ });
42
+ const parsed = JSON.parse(String(output || '').trim());
43
+ const flattened = {};
44
+ for (const [key, item] of Object.entries(parsed || {})) {
45
+ if (key.startsWith('skills.')) {
46
+ flattened[key.slice('skills.'.length)] = item?.value;
47
+ } else if (key === 'disabledExtensions' || key === 'disabledProviders') {
48
+ flattened[key] = item?.value;
49
+ }
50
+ }
51
+ return { ...DEFAULT_SKILL_SETTINGS, ...flattened };
52
+ } catch {
53
+ const persisted = readOmpSettings();
54
+ return {
55
+ ...DEFAULT_SKILL_SETTINGS,
56
+ ...(persisted.skills && typeof persisted.skills === 'object' ? persisted.skills : {}),
57
+ disabledExtensions: persisted.disabledExtensions || [],
58
+ disabledProviders: persisted.disabledProviders || []
59
+ };
60
+ }
61
+ }
62
+
63
+ function globMatches(value, pattern) {
64
+ const escaped = String(pattern || '')
65
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
66
+ .replace(/\*/g, '.*')
67
+ .replace(/\?/g, '.');
68
+ return new RegExp(`^${escaped}$`, 'i').test(value);
69
+ }
70
+
71
+ function isIncluded(name, settings) {
72
+ const disabled = Array.isArray(settings.disabledExtensions)
73
+ ? settings.disabledExtensions
74
+ : [];
75
+ if (disabled.some(item => item === `skill:${name}`)) return false;
76
+
77
+ const ignored = Array.isArray(settings.ignoredSkills) ? settings.ignoredSkills : [];
78
+ if (ignored.some(pattern => globMatches(name, pattern))) return false;
79
+
80
+ const included = Array.isArray(settings.includeSkills) ? settings.includeSkills : [];
81
+ return included.length === 0 || included.some(pattern => globMatches(name, pattern));
82
+ }
83
+
84
+ function parseSkill(service, skillFile, provider, scope, requireDescription) {
85
+ let metadata;
86
+ try {
87
+ metadata = service.parseSkillMd(fs.readFileSync(skillFile, 'utf8'));
88
+ } catch {
89
+ return null;
90
+ }
91
+
92
+ const directory = path.basename(path.dirname(skillFile));
93
+ const name = String(metadata.name || directory).trim();
94
+ const description = String(metadata.description || '').trim();
95
+ if (!name || (requireDescription && !description)) return null;
96
+
97
+ let realPath = skillFile;
98
+ try {
99
+ realPath = fs.realpathSync(skillFile);
100
+ } catch {
101
+ // Keep the absolute source path when a realpath cannot be resolved.
102
+ }
103
+
104
+ const native = provider === 'native';
105
+ return {
106
+ key: `omp:${provider}:${scope}:${realPath}`,
107
+ name,
108
+ description,
109
+ directory,
110
+ installed: true,
111
+ isLocal: false,
112
+ source: native ? 'native-installed' : 'provider-installed',
113
+ sourceProvider: provider,
114
+ sourceScope: scope,
115
+ sourcePath: skillFile,
116
+ realPath,
117
+ readonly: !native,
118
+ shadowedSources: [],
119
+ protected: false,
120
+ readmeUrl: null,
121
+ repoOwner: null,
122
+ repoName: null,
123
+ repoBranch: null,
124
+ license: metadata.license || null
125
+ };
126
+ }
127
+
128
+ function scanOneLevel(service, root, descriptor, settings) {
129
+ if (!root || !fs.existsSync(root)) return [];
130
+ const result = [];
131
+ let entries = [];
132
+ try {
133
+ entries = fs.readdirSync(root, { withFileTypes: true });
134
+ } catch {
135
+ return result;
136
+ }
137
+
138
+ for (const entry of entries) {
139
+ const entryPath = path.join(root, entry.name);
140
+ if (!entry.isDirectory()) {
141
+ if (!entry.isSymbolicLink()) continue;
142
+ try {
143
+ if (!fs.statSync(entryPath).isDirectory()) continue;
144
+ } catch {
145
+ continue;
146
+ }
147
+ }
148
+ const skillFile = path.join(entryPath, 'SKILL.md');
149
+ if (!fs.existsSync(skillFile)) continue;
150
+ const skill = parseSkill(
151
+ service,
152
+ skillFile,
153
+ descriptor.provider,
154
+ descriptor.scope,
155
+ descriptor.requireDescription
156
+ );
157
+ if (skill && isIncluded(skill.name, settings)) {
158
+ result.push(skill);
159
+ }
160
+ }
161
+ return result;
162
+ }
163
+
164
+ function getPluginInstallPaths(cwd) {
165
+ try {
166
+ const output = execFileSync(getOmpCommand(), ['plugin', 'list', '--json'], {
167
+ cwd: cwd || process.cwd(),
168
+ encoding: 'utf8',
169
+ stdio: ['ignore', 'pipe', 'pipe'],
170
+ timeout: 5000,
171
+ windowsHide: true
172
+ });
173
+ const parsed = JSON.parse(String(output || '').trim());
174
+ const paths = [];
175
+ const visit = (value, inheritedScope = 'user') => {
176
+ if (Array.isArray(value)) {
177
+ value.forEach(item => visit(item, inheritedScope));
178
+ return;
179
+ }
180
+ if (!value || typeof value !== 'object') return;
181
+ const scope = value.scope || inheritedScope;
182
+ if (value.installPath || value.path) {
183
+ paths.push({
184
+ path: value.installPath || value.path,
185
+ scope
186
+ });
187
+ }
188
+ for (const nested of Object.values(value)) {
189
+ if (nested && typeof nested === 'object') visit(nested, scope);
190
+ }
191
+ };
192
+ visit(parsed);
193
+ return paths;
194
+ } catch {
195
+ return [];
196
+ }
197
+ }
198
+
199
+ function getClaudePluginSkillRoots() {
200
+ const pluginsDir = NATIVE_PATHS.claude.plugins
201
+ || path.join(NATIVE_PATHS.claude.dir, 'plugins');
202
+ const installedFile = path.join(pluginsDir, 'installed_plugins.json');
203
+ if (!fs.existsSync(installedFile)) return [];
204
+
205
+ try {
206
+ const parsed = JSON.parse(fs.readFileSync(installedFile, 'utf8'));
207
+ const roots = [];
208
+ for (const installations of Object.values(parsed?.plugins || {})) {
209
+ if (!Array.isArray(installations)) continue;
210
+ for (const installation of installations) {
211
+ const installPath = String(installation?.installPath || '').trim();
212
+ if (!installPath) continue;
213
+ roots.push({
214
+ path: path.join(installPath, 'skills'),
215
+ scope: installation.scope === 'project' ? 'project' : 'user'
216
+ });
217
+ }
218
+ }
219
+ return roots;
220
+ } catch {
221
+ return [];
222
+ }
223
+ }
224
+
225
+ function providerEnabled(settings, provider, scope) {
226
+ if (Array.isArray(settings.disabledProviders) && settings.disabledProviders.includes(provider)) {
227
+ return false;
228
+ }
229
+ if (provider === 'native') {
230
+ return scope === 'project' ? settings.enablePiProject !== false : settings.enablePiUser !== false;
231
+ }
232
+ if (provider === 'omp-plugins') {
233
+ return scope === 'project' ? settings.enablePiProject !== false : settings.enablePiUser !== false;
234
+ }
235
+ if (provider === 'claude') {
236
+ return scope === 'project' ? settings.enableClaudeProject !== false : settings.enableClaudeUser !== false;
237
+ }
238
+ if (provider === 'claude-plugins') {
239
+ return scope === 'project'
240
+ ? settings.enableClaudeProject !== false
241
+ : settings.enableClaudeUser !== false;
242
+ }
243
+ if (provider === 'agents') {
244
+ return scope === 'project' ? settings.enableAgentsProject !== false : settings.enableAgentsUser !== false;
245
+ }
246
+ if (provider === 'codex' && scope === 'user') {
247
+ return settings.enableCodexUser !== false;
248
+ }
249
+ return [
250
+ settings.enablePiUser,
251
+ settings.enablePiProject,
252
+ settings.enableClaudeUser,
253
+ settings.enableClaudeProject,
254
+ settings.enableCodexUser,
255
+ settings.enableAgentsUser,
256
+ settings.enableAgentsProject
257
+ ].some(value => value !== false);
258
+ }
259
+
260
+ function compactShadow(skill) {
261
+ return {
262
+ sourceProvider: skill.sourceProvider,
263
+ sourceScope: skill.sourceScope,
264
+ sourcePath: skill.sourcePath
265
+ };
266
+ }
267
+
268
+ function deduplicateDiscoveredSkills(skills) {
269
+ const byName = new Map();
270
+ const byRealPath = new Map();
271
+ const result = [];
272
+
273
+ for (const skill of skills) {
274
+ const nameKey = skill.name.toLowerCase();
275
+ const pathKey = skill.realPath || skill.sourcePath;
276
+ const existing = byName.get(nameKey) || byRealPath.get(pathKey);
277
+ if (existing) {
278
+ existing.shadowedSources.push(compactShadow(skill));
279
+ continue;
280
+ }
281
+ byName.set(nameKey, skill);
282
+ byRealPath.set(pathKey, skill);
283
+ result.push(skill);
284
+ }
285
+
286
+ return result;
287
+ }
288
+
289
+ function discoverOmpSkills(service, options = {}) {
290
+ const cwd = options.cwd ? path.resolve(options.cwd) : null;
291
+ const settings = readEffectiveSettings(cwd);
292
+ if (settings.enabled === false) return [];
293
+
294
+ const ompPaths = getOmpPaths();
295
+ const userHome = HOME_DIR;
296
+ const descriptors = [
297
+ { provider: 'native', scope: 'user', root: ompPaths.skills, requireDescription: true },
298
+ ...(cwd ? [{ provider: 'native', scope: 'project', root: path.join(cwd, '.omp', 'skills'), requireDescription: true }] : []),
299
+ ...getPluginInstallPaths(cwd)
300
+ .filter(item => cwd || item.scope !== 'project')
301
+ .map(item => ({
302
+ provider: 'omp-plugins',
303
+ scope: item.scope || 'user',
304
+ root: path.join(item.path, 'skills'),
305
+ requireDescription: true
306
+ })),
307
+ { provider: 'claude', scope: 'user', root: NATIVE_PATHS.claude.skills, requireDescription: false },
308
+ ...(cwd ? [{ provider: 'claude', scope: 'project', root: path.join(cwd, '.claude', 'skills'), requireDescription: false }] : []),
309
+ ...getClaudePluginSkillRoots()
310
+ .filter(item => cwd || item.scope !== 'project')
311
+ .map(item => ({
312
+ provider: 'claude-plugins',
313
+ scope: item.scope,
314
+ root: item.path,
315
+ requireDescription: false
316
+ })),
317
+ { provider: 'agents', scope: 'user', root: path.join(userHome, '.agents', 'skills'), requireDescription: false },
318
+ { provider: 'agents', scope: 'user', root: path.join(userHome, '.agent', 'skills'), requireDescription: false },
319
+ ...(cwd ? [
320
+ { provider: 'agents', scope: 'project', root: path.join(cwd, '.agents', 'skills'), requireDescription: false },
321
+ { provider: 'agents', scope: 'project', root: path.join(cwd, '.agent', 'skills'), requireDescription: false }
322
+ ] : []),
323
+ { provider: 'codex', scope: 'user', root: path.join(userHome, '.codex', 'skills'), requireDescription: false },
324
+ ...(cwd ? [{ provider: 'codex', scope: 'project', root: path.join(cwd, '.codex', 'skills'), requireDescription: false }] : []),
325
+ { provider: 'opencode', scope: 'user', root: path.join(NATIVE_PATHS.opencode.config, 'skills'), requireDescription: false },
326
+ ...(cwd ? [{ provider: 'opencode', scope: 'project', root: path.join(cwd, '.opencode', 'skills'), requireDescription: false }] : [])
327
+ ];
328
+
329
+ const discovered = [];
330
+ for (const descriptor of descriptors) {
331
+ if (!providerEnabled(settings, descriptor.provider, descriptor.scope)) continue;
332
+ discovered.push(...scanOneLevel(service, descriptor.root, descriptor, settings));
333
+ }
334
+
335
+ const customDirectories = Array.isArray(settings.customDirectories)
336
+ ? settings.customDirectories
337
+ : [];
338
+ for (const customDirectory of customDirectories) {
339
+ const expanded = expandHome(customDirectory);
340
+ const root = path.isAbsolute(expanded)
341
+ ? expanded
342
+ : path.resolve(cwd || process.cwd(), expanded);
343
+ discovered.push(...scanOneLevel(service, root, {
344
+ provider: 'custom',
345
+ scope: cwd && root.startsWith(`${cwd}${path.sep}`) ? 'project' : 'user',
346
+ requireDescription: true
347
+ }, settings));
348
+ }
349
+
350
+ return deduplicateDiscoveredSkills(discovered);
351
+ }
352
+
353
+ module.exports = {
354
+ DEFAULT_SKILL_SETTINGS,
355
+ discoverOmpSkills,
356
+ deduplicateDiscoveredSkills,
357
+ readEffectiveSettings
358
+ };
@@ -0,0 +1,97 @@
1
+ const {
2
+ readOmpSettingsStrict,
3
+ writeOmpSettingsAtomic
4
+ } = require('./omp-config');
5
+
6
+ const OMP_SKILL_SETTING_DEFAULTS = Object.freeze({
7
+ enableCodexUser: true,
8
+ enableClaudeUser: true,
9
+ enablePiUser: true,
10
+ enablePiProject: true
11
+ });
12
+ const OMP_SKILL_SETTING_KEYS = Object.freeze(Object.keys(OMP_SKILL_SETTING_DEFAULTS));
13
+ const OMP_SKILL_SETTING_KEY_SET = new Set(OMP_SKILL_SETTING_KEYS);
14
+
15
+ function isPlainObject(value) {
16
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
17
+ return false;
18
+ }
19
+ const prototype = Object.getPrototypeOf(value);
20
+ return prototype === Object.prototype || prototype === null;
21
+ }
22
+
23
+ function selectOmpSkillSettings(skills) {
24
+ const source = skills || {};
25
+ const selected = {};
26
+ for (const key of OMP_SKILL_SETTING_KEYS) {
27
+ selected[key] = Object.prototype.hasOwnProperty.call(source, key)
28
+ ? source[key]
29
+ : OMP_SKILL_SETTING_DEFAULTS[key];
30
+ }
31
+ return selected;
32
+ }
33
+
34
+ function validatePersistedOmpSkillSettings(config) {
35
+ if (!Object.prototype.hasOwnProperty.call(config, 'skills')) {
36
+ return {};
37
+ }
38
+ if (!isPlainObject(config.skills)) {
39
+ throw new Error('Invalid OMP config skills');
40
+ }
41
+
42
+ for (const key of OMP_SKILL_SETTING_KEYS) {
43
+ if (
44
+ Object.prototype.hasOwnProperty.call(config.skills, key) &&
45
+ typeof config.skills[key] !== 'boolean'
46
+ ) {
47
+ throw new Error(`Invalid OMP skill setting value for ${key}: expected boolean`);
48
+ }
49
+ }
50
+ return config.skills;
51
+ }
52
+
53
+ function validateOmpSkillSettingsPatch(patch) {
54
+ if (!isPlainObject(patch)) {
55
+ throw new Error('Invalid OMP skill settings: expected an object');
56
+ }
57
+
58
+ for (const [key, value] of Object.entries(patch)) {
59
+ if (!OMP_SKILL_SETTING_KEY_SET.has(key)) {
60
+ throw new Error(`Invalid OMP skill setting: ${key}`);
61
+ }
62
+ if (typeof value !== 'boolean') {
63
+ throw new Error(`Invalid OMP skill setting value for ${key}: expected boolean`);
64
+ }
65
+ }
66
+ }
67
+
68
+ function readOmpSkillSettings() {
69
+ const config = readOmpSettingsStrict();
70
+ return selectOmpSkillSettings(validatePersistedOmpSkillSettings(config));
71
+ }
72
+
73
+ function updateOmpSkillSettings(patch) {
74
+ validateOmpSkillSettingsPatch(patch);
75
+
76
+ const config = readOmpSettingsStrict();
77
+ const existingSkills = validatePersistedOmpSkillSettings(config);
78
+ if (Object.keys(patch).length === 0) {
79
+ return selectOmpSkillSettings(existingSkills);
80
+ }
81
+
82
+ const nextConfig = {
83
+ ...config,
84
+ skills: {
85
+ ...existingSkills,
86
+ ...patch
87
+ }
88
+ };
89
+
90
+ writeOmpSettingsAtomic(nextConfig);
91
+ return selectOmpSkillSettings(nextConfig.skills);
92
+ }
93
+
94
+ module.exports = {
95
+ readOmpSkillSettings,
96
+ updateOmpSkillSettings
97
+ };