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.
- package/CHANGELOG.md +3 -0
- package/README.md +1 -1
- package/dist/web/assets/{Analytics-CkUtnoz9.js → Analytics-8dRv1aYW.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BwPqhE-N.js → ConfigTemplates-BQRXSICO.js} +1 -1
- package/dist/web/assets/Home-BtzWhKIa.js +1 -0
- package/dist/web/assets/Home-DYVy4cGZ.css +1 -0
- package/dist/web/assets/{PluginManager-JMXWl37b.js → PluginManager-BTa-Ygf6.js} +1 -1
- package/dist/web/assets/{ProjectList-CZwDgVvt.js → ProjectList-F_cB8ldA.js} +1 -1
- package/dist/web/assets/{SessionList-2fZmN7Au.js → SessionList-LOrbPQvu.js} +1 -1
- package/dist/web/assets/SkillManager-BsX8Qpj0.css +1 -0
- package/dist/web/assets/SkillManager-BzI_t-yV.js +1 -0
- package/dist/web/assets/{WorkspaceManager-B3lhhBD0.js → WorkspaceManager-Db9RGiz7.js} +1 -1
- package/dist/web/assets/index-DIWJmyL6.js +2 -0
- package/dist/web/assets/{index-C5nLTbOk.css → index-bJRG3RXH.css} +1 -1
- package/dist/web/index.html +2 -2
- package/docs/superpowers/plans/2026-08-01-omp-skill-scan-settings.md +759 -0
- package/docs/superpowers/specs/2026-08-01-omp-skill-scan-settings-design.md +224 -0
- package/package.json +1 -1
- package/src/commands/proxy-control.js +12 -12
- package/src/commands/toggle-proxy.js +16 -17
- package/src/commands/ui.js +11 -1
- package/src/config/default.js +1 -1
- package/src/config/paths.js +52 -29
- package/src/index.js +42 -16
- package/src/server/api/omp-channels.js +6 -4
- package/src/server/api/omp-config.js +1 -1
- package/src/server/api/omp-proxy.js +13 -42
- package/src/server/api/plugins.js +119 -47
- package/src/server/api/skills.js +63 -10
- package/src/server/index.js +3 -3
- package/src/server/omp-proxy-server.js +142 -10
- package/src/server/services/channel-scheduler.js +59 -9
- package/src/server/services/model-definition-schema.js +2 -1
- package/src/server/services/omp-auth-gateway-client.js +62 -0
- package/src/server/services/omp-channels.js +168 -12
- package/src/server/services/omp-config.js +71 -6
- package/src/server/services/omp-gateway-routing.js +295 -0
- package/src/server/services/omp-gateway.js +919 -0
- package/src/server/services/omp-native-plugin-adapter.js +456 -0
- package/src/server/services/omp-session-log-observer.js +138 -0
- package/src/server/services/omp-sessions.js +80 -0
- package/src/server/services/omp-settings-manager.js +338 -87
- package/src/server/services/omp-skill-discovery.js +358 -0
- package/src/server/services/omp-skill-settings-service.js +97 -0
- package/src/server/services/pi-omp-migration.js +273 -0
- package/src/server/services/platform-resolution.js +44 -0
- package/src/server/services/plugins-service.js +85 -561
- package/src/server/services/project-path-validation.js +65 -0
- package/src/server/services/skill-service.js +169 -77
- package/dist/web/assets/Home-BANl91zG.js +0 -1
- package/dist/web/assets/Home-CqnBXFxj.css +0 -1
- package/dist/web/assets/SkillManager-Bhzhkeis.js +0 -1
- package/dist/web/assets/SkillManager-D7pd-d_P.css +0 -1
- package/dist/web/assets/index-LYvv7wtx.js +0 -2
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
const { getOmpCommand, getOmpPaths } = require('./omp-config');
|
|
5
|
+
|
|
6
|
+
const VALID_SCOPES = new Set(['user', 'project']);
|
|
7
|
+
const GENERIC_SOURCE_KINDS = new Set(['npm', 'link', 'linked', 'marketplace']);
|
|
8
|
+
|
|
9
|
+
function normalizeScope(scope, fallback = 'user') {
|
|
10
|
+
return VALID_SCOPES.has(scope) ? scope : fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseJsonOutput(output, commandLabel, emptyMessages = []) {
|
|
14
|
+
const text = stripAnsi(output).trim();
|
|
15
|
+
if (!text || emptyMessages.some(message => text.includes(message))) return null;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(text);
|
|
18
|
+
} catch {
|
|
19
|
+
throw new Error(`OMP returned invalid JSON for "${commandLabel}"`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stripAnsi(output) {
|
|
24
|
+
return String(output || '').replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function splitMarketplaceId(pluginId = '') {
|
|
28
|
+
const value = String(pluginId || '');
|
|
29
|
+
const index = value.lastIndexOf('@');
|
|
30
|
+
if (index <= 0) return { name: value, marketplace: '' };
|
|
31
|
+
return {
|
|
32
|
+
name: value.slice(0, index),
|
|
33
|
+
marketplace: value.slice(index + 1)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function collectEntries(value, inherited = {}, result = []) {
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
value.forEach(item => collectEntries(item, inherited, result));
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
if (!value || typeof value !== 'object') return result;
|
|
43
|
+
|
|
44
|
+
const looksLikePlugin = Boolean(
|
|
45
|
+
value.name || value.id || value.pluginId || value.package || value.installPath
|
|
46
|
+
);
|
|
47
|
+
if (looksLikePlugin) {
|
|
48
|
+
result.push({ ...inherited, ...value });
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
53
|
+
const next = { ...inherited };
|
|
54
|
+
if (key === 'user' || key === 'project') next.scope = key;
|
|
55
|
+
if (key === 'npm') next.pluginKind = 'npm';
|
|
56
|
+
if (key === 'linked' || key === 'link') next.pluginKind = 'link';
|
|
57
|
+
if (key === 'marketplace') next.pluginKind = 'marketplace';
|
|
58
|
+
collectEntries(nested, next, result);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeInstalledPlugin(entry, kindHint = '') {
|
|
64
|
+
const firstEntry = Array.isArray(entry.entries) ? entry.entries[0] || {} : {};
|
|
65
|
+
const pluginKind = entry.pluginKind
|
|
66
|
+
|| entry.kind
|
|
67
|
+
|| entry.sourceKind
|
|
68
|
+
|| (entry.marketplace ? 'marketplace' : kindHint)
|
|
69
|
+
|| (entry.linked ? 'link' : 'npm');
|
|
70
|
+
const isMarketplace = pluginKind === 'marketplace';
|
|
71
|
+
const sourceIdentity = String(entry.source || '').trim();
|
|
72
|
+
const rawIdentity = String(
|
|
73
|
+
entry.pluginId
|
|
74
|
+
|| entry.id
|
|
75
|
+
|| entry.package
|
|
76
|
+
|| entry.installTarget
|
|
77
|
+
|| (!GENERIC_SOURCE_KINDS.has(sourceIdentity.toLowerCase()) ? sourceIdentity : '')
|
|
78
|
+
|| entry.name
|
|
79
|
+
|| ''
|
|
80
|
+
).trim();
|
|
81
|
+
const marketplace = String(entry.marketplace || '').trim();
|
|
82
|
+
const pluginId = isMarketplace && marketplace && !rawIdentity.endsWith(`@${marketplace}`)
|
|
83
|
+
? `${rawIdentity}@${marketplace}`
|
|
84
|
+
: rawIdentity;
|
|
85
|
+
const parsed = isMarketplace ? splitMarketplaceId(pluginId) : { name: entry.name || pluginId, marketplace };
|
|
86
|
+
const scope = normalizeScope(entry.scope, 'user');
|
|
87
|
+
|
|
88
|
+
if (!pluginId) {
|
|
89
|
+
throw new Error('OMP plugin list returned an entry without a native identity');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
...entry,
|
|
94
|
+
key: `omp:${pluginKind}:${pluginId}:${scope}`,
|
|
95
|
+
pluginId,
|
|
96
|
+
id: pluginId,
|
|
97
|
+
name: String(entry.displayName || entry.name || parsed.name || pluginId),
|
|
98
|
+
marketplace: marketplace || parsed.marketplace,
|
|
99
|
+
installTarget: pluginId,
|
|
100
|
+
directory: entry.directory || entry.installPath || pluginId,
|
|
101
|
+
installPath: entry.installPath || entry.path || firstEntry.installPath || firstEntry.path || '',
|
|
102
|
+
installSource: entry.installSource || entry.source || pluginId,
|
|
103
|
+
source: `omp-${pluginKind}`,
|
|
104
|
+
scope,
|
|
105
|
+
pluginKind,
|
|
106
|
+
pluginType: pluginKind,
|
|
107
|
+
version: entry.version || firstEntry.version || 'latest',
|
|
108
|
+
description: entry.description || entry.manifest?.description || firstEntry.description || '',
|
|
109
|
+
installed: true,
|
|
110
|
+
enabled: entry.enabled !== false && firstEntry.enabled !== false,
|
|
111
|
+
readonly: false
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeMarketplacePlugin(entry, inheritedMarketplace = '') {
|
|
116
|
+
const marketplace = String(entry.marketplace || inheritedMarketplace || '').trim();
|
|
117
|
+
const rawName = String(entry.name || entry.id || entry.pluginId || '').trim();
|
|
118
|
+
if (!rawName) return null;
|
|
119
|
+
const parsed = splitMarketplaceId(rawName);
|
|
120
|
+
const name = parsed.name;
|
|
121
|
+
const marketplaceName = marketplace || parsed.marketplace;
|
|
122
|
+
const pluginId = marketplaceName ? `${name}@${marketplaceName}` : rawName;
|
|
123
|
+
return {
|
|
124
|
+
...entry,
|
|
125
|
+
key: `omp-market:${pluginId}`,
|
|
126
|
+
pluginId,
|
|
127
|
+
id: pluginId,
|
|
128
|
+
name,
|
|
129
|
+
marketplace: marketplaceName,
|
|
130
|
+
installTarget: pluginId,
|
|
131
|
+
installSource: pluginId,
|
|
132
|
+
source: entry.source || pluginId,
|
|
133
|
+
directory: entry.directory || name,
|
|
134
|
+
version: entry.version || 'latest',
|
|
135
|
+
description: entry.description || '',
|
|
136
|
+
pluginKind: 'marketplace',
|
|
137
|
+
pluginType: 'marketplace',
|
|
138
|
+
readonly: false,
|
|
139
|
+
isInstalled: entry.installed === true
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
class OmpNativePluginAdapter {
|
|
144
|
+
constructor(options = {}) {
|
|
145
|
+
this.commandRunner = options.commandRunner || execFileSync;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
run(args, options = {}) {
|
|
149
|
+
const command = getOmpCommand();
|
|
150
|
+
try {
|
|
151
|
+
return this.commandRunner(command, args, {
|
|
152
|
+
cwd: options.cwd || process.cwd(),
|
|
153
|
+
encoding: 'utf8',
|
|
154
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
155
|
+
timeout: options.timeout || 120000,
|
|
156
|
+
windowsHide: true
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const detail = String(error?.stderr || error?.message || error).trim();
|
|
160
|
+
throw new Error(
|
|
161
|
+
`OMP 17.1+ plugin CLI command failed (${command} ${args.join(' ')}): ${detail}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
runJson(args, options = {}, emptyMessages = []) {
|
|
167
|
+
const output = this.run(args, options);
|
|
168
|
+
return parseJsonOutput(output, args.join(' '), emptyMessages);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
listPlugins(options = {}) {
|
|
172
|
+
const payload = this.runJson(['plugin', 'list', '--json'], options);
|
|
173
|
+
if (!payload || typeof payload !== 'object') {
|
|
174
|
+
throw new Error('OMP plugin list returned an invalid JSON payload');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const plugins = [];
|
|
178
|
+
for (const entry of collectEntries(payload.npm || [])) {
|
|
179
|
+
plugins.push(normalizeInstalledPlugin(entry, 'npm'));
|
|
180
|
+
}
|
|
181
|
+
for (const entry of collectEntries(payload.linked || payload.link || [])) {
|
|
182
|
+
plugins.push(normalizeInstalledPlugin(entry, 'link'));
|
|
183
|
+
}
|
|
184
|
+
for (const entry of collectEntries(payload.marketplace || [])) {
|
|
185
|
+
plugins.push(normalizeInstalledPlugin(entry, 'marketplace'));
|
|
186
|
+
}
|
|
187
|
+
plugins.push(...this.listLooseExtensions());
|
|
188
|
+
return {
|
|
189
|
+
plugins: options.cwd
|
|
190
|
+
? plugins
|
|
191
|
+
: plugins.filter(plugin => plugin.scope !== 'project')
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
listLooseExtensions() {
|
|
196
|
+
const extensionsDir = getOmpPaths().extensions;
|
|
197
|
+
if (!fs.existsSync(extensionsDir)) return [];
|
|
198
|
+
const plugins = [];
|
|
199
|
+
|
|
200
|
+
for (const entry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
|
|
201
|
+
if (entry.name.startsWith('.')) continue;
|
|
202
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
203
|
+
if (!entry.isDirectory() && !['.js', '.mjs', '.cjs', '.ts'].includes(extension)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const installPath = path.join(extensionsDir, entry.name);
|
|
207
|
+
let manifest = {};
|
|
208
|
+
if (entry.isDirectory()) {
|
|
209
|
+
for (const manifestName of ['omp.json', 'extension.json', 'plugin.json', 'package.json']) {
|
|
210
|
+
const manifestPath = path.join(installPath, manifestName);
|
|
211
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
212
|
+
try {
|
|
213
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
214
|
+
} catch {
|
|
215
|
+
manifest = {};
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
let realPath = installPath;
|
|
221
|
+
try {
|
|
222
|
+
realPath = fs.realpathSync(installPath);
|
|
223
|
+
} catch {
|
|
224
|
+
// The absolute install path remains a stable local identity.
|
|
225
|
+
}
|
|
226
|
+
const name = manifest.name || (entry.isDirectory()
|
|
227
|
+
? entry.name
|
|
228
|
+
: entry.name.slice(0, -extension.length));
|
|
229
|
+
plugins.push({
|
|
230
|
+
key: `omp:extension:${realPath}`,
|
|
231
|
+
pluginId: `extension:${realPath}`,
|
|
232
|
+
id: `extension:${realPath}`,
|
|
233
|
+
name,
|
|
234
|
+
directory: entry.name,
|
|
235
|
+
installPath,
|
|
236
|
+
installTarget: '',
|
|
237
|
+
installSource: installPath,
|
|
238
|
+
source: 'omp-extension',
|
|
239
|
+
scope: 'user',
|
|
240
|
+
version: manifest.version || 'local',
|
|
241
|
+
description: manifest.description || '',
|
|
242
|
+
installed: true,
|
|
243
|
+
enabled: true,
|
|
244
|
+
pluginType: 'extension',
|
|
245
|
+
pluginKind: 'extension',
|
|
246
|
+
readonly: true
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return plugins;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
installPlugin(target, metadata = {}, options = {}) {
|
|
253
|
+
const pluginId = String(target || metadata.pluginId || metadata.installSource || '').trim();
|
|
254
|
+
if (!pluginId) throw new Error('Missing OMP plugin target');
|
|
255
|
+
const args = ['plugin', 'install', pluginId];
|
|
256
|
+
if (options.force) args.push('--force');
|
|
257
|
+
if (options.scope) args.push('--scope', normalizeScope(options.scope));
|
|
258
|
+
this.run(args, options);
|
|
259
|
+
const parsed = splitMarketplaceId(pluginId);
|
|
260
|
+
return {
|
|
261
|
+
success: true,
|
|
262
|
+
plugin: normalizeInstalledPlugin({
|
|
263
|
+
...metadata,
|
|
264
|
+
pluginId,
|
|
265
|
+
name: metadata.name || parsed.name || pluginId,
|
|
266
|
+
marketplace: metadata.marketplace || parsed.marketplace,
|
|
267
|
+
scope: options.scope || metadata.scope,
|
|
268
|
+
pluginKind: metadata.pluginKind || (parsed.marketplace ? 'marketplace' : 'npm')
|
|
269
|
+
})
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
uninstallPlugin(pluginId, options = {}) {
|
|
274
|
+
this.runMutation('uninstall', pluginId, options);
|
|
275
|
+
return { success: true, message: 'Plugin uninstalled successfully' };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
togglePlugin(pluginId, enabled, options = {}) {
|
|
279
|
+
this.runMutation(enabled ? 'enable' : 'disable', pluginId, options);
|
|
280
|
+
return {
|
|
281
|
+
pluginId,
|
|
282
|
+
name: splitMarketplaceId(pluginId).name,
|
|
283
|
+
scope: options.scope || 'user',
|
|
284
|
+
enabled,
|
|
285
|
+
success: true
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
updatePluginConfig(pluginId, config, options = {}) {
|
|
290
|
+
const target = String(pluginId || '').trim();
|
|
291
|
+
if (!target) throw new Error('Missing OMP pluginId');
|
|
292
|
+
for (const [key, value] of Object.entries(config || {})) {
|
|
293
|
+
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
|
294
|
+
const args = ['plugin', 'config', 'set', target, key, serialized];
|
|
295
|
+
if (options.scope) args.push('--scope', normalizeScope(options.scope));
|
|
296
|
+
this.run(args, options);
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
success: true,
|
|
300
|
+
message: `Configuration updated for plugin "${target}"`
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
runMutation(action, pluginId, options = {}) {
|
|
305
|
+
const target = String(pluginId || '').trim();
|
|
306
|
+
if (!target) throw new Error('Missing OMP pluginId');
|
|
307
|
+
if (target.startsWith('extension:')) {
|
|
308
|
+
throw new Error('OMP loose extensions are readonly and cannot be managed by the plugin CLI');
|
|
309
|
+
}
|
|
310
|
+
const args = ['plugin', action, target];
|
|
311
|
+
if (options.scope) args.push('--scope', normalizeScope(options.scope));
|
|
312
|
+
this.run(args, options);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
discover(options = {}) {
|
|
316
|
+
const output = this.run(['plugin', 'discover', '--json'], options);
|
|
317
|
+
const text = stripAnsi(output).trim();
|
|
318
|
+
if (!text || text.includes('No plugins available')) return [];
|
|
319
|
+
let payload;
|
|
320
|
+
try {
|
|
321
|
+
payload = JSON.parse(text);
|
|
322
|
+
} catch {
|
|
323
|
+
return this.discoverFromText(text, options);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const entries = [];
|
|
327
|
+
if (Array.isArray(payload)) {
|
|
328
|
+
entries.push(...payload);
|
|
329
|
+
} else if (Array.isArray(payload.plugins)) {
|
|
330
|
+
entries.push(...payload.plugins);
|
|
331
|
+
} else if (Array.isArray(payload.marketplaces)) {
|
|
332
|
+
for (const marketplace of payload.marketplaces) {
|
|
333
|
+
for (const plugin of marketplace.plugins || []) {
|
|
334
|
+
entries.push({ ...plugin, marketplace: plugin.marketplace || marketplace.name });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
entries.push(...collectEntries(payload));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const seen = new Set();
|
|
342
|
+
return entries
|
|
343
|
+
.map(entry => normalizeMarketplacePlugin(entry, entry.marketplace))
|
|
344
|
+
.filter(plugin => {
|
|
345
|
+
if (!plugin || seen.has(plugin.pluginId)) return false;
|
|
346
|
+
seen.add(plugin.pluginId);
|
|
347
|
+
return true;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
discoverFromText(initialOutput, options = {}) {
|
|
352
|
+
const marketplaces = this.listMarketplaces(options);
|
|
353
|
+
if (marketplaces.length === 0) return [];
|
|
354
|
+
const results = [];
|
|
355
|
+
for (const marketplace of marketplaces) {
|
|
356
|
+
const output = marketplaces.length === 1
|
|
357
|
+
? initialOutput
|
|
358
|
+
: this.run(['plugin', 'discover', marketplace.id, '--json'], options);
|
|
359
|
+
const lines = stripAnsi(output).split(/\r?\n/);
|
|
360
|
+
let current = null;
|
|
361
|
+
for (const rawLine of lines) {
|
|
362
|
+
const line = rawLine.trim();
|
|
363
|
+
if (!line || line.startsWith('Available Plugins')) continue;
|
|
364
|
+
if (/^No plugins (available|found)/i.test(line)) continue;
|
|
365
|
+
if (/^\S/.test(rawLine)) continue;
|
|
366
|
+
if (/^\s{4,}\S/.test(rawLine) && current) {
|
|
367
|
+
current.description = line;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const match = line.match(/^(.+?)(?:@([^@\s]+))?$/);
|
|
371
|
+
if (!match) continue;
|
|
372
|
+
current = normalizeMarketplacePlugin({
|
|
373
|
+
name: match[1],
|
|
374
|
+
version: match[2] || 'latest',
|
|
375
|
+
marketplace: marketplace.id
|
|
376
|
+
}, marketplace.id);
|
|
377
|
+
if (current) results.push(current);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return results;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
listMarketplaces(options = {}) {
|
|
384
|
+
const output = this.run(['plugin', 'marketplace', 'list', '--json'], options);
|
|
385
|
+
const text = stripAnsi(output).trim();
|
|
386
|
+
if (!text || text.includes('No marketplaces configured')) return [];
|
|
387
|
+
let payload;
|
|
388
|
+
try {
|
|
389
|
+
payload = JSON.parse(text);
|
|
390
|
+
} catch {
|
|
391
|
+
const entries = text.split(/\r?\n/)
|
|
392
|
+
.filter(line => /^\s{2,}\S/.test(line))
|
|
393
|
+
.map(line => {
|
|
394
|
+
const match = line.trim().match(/^(\S+)\s+(.+)$/);
|
|
395
|
+
return match ? { name: match[1], sourceUri: match[2], source: match[2] } : null;
|
|
396
|
+
})
|
|
397
|
+
.filter(Boolean);
|
|
398
|
+
if (entries.length === 0) {
|
|
399
|
+
throw new Error('OMP returned invalid marketplace list output');
|
|
400
|
+
}
|
|
401
|
+
payload = entries;
|
|
402
|
+
}
|
|
403
|
+
const entries = Array.isArray(payload)
|
|
404
|
+
? payload
|
|
405
|
+
: (payload.marketplaces || collectEntries(payload));
|
|
406
|
+
return entries.map(entry => {
|
|
407
|
+
const name = String(entry.name || entry.id || entry.marketplace || '').trim();
|
|
408
|
+
const source = entry.source || entry.url || entry.repo || entry.repository || '';
|
|
409
|
+
return {
|
|
410
|
+
...entry,
|
|
411
|
+
id: name,
|
|
412
|
+
name,
|
|
413
|
+
label: name,
|
|
414
|
+
source,
|
|
415
|
+
sourceUri: typeof source === 'string' ? source : JSON.stringify(source),
|
|
416
|
+
provider: 'omp-marketplace',
|
|
417
|
+
enabled: true,
|
|
418
|
+
mutable: {
|
|
419
|
+
toggle: false,
|
|
420
|
+
auth: false,
|
|
421
|
+
remove: true,
|
|
422
|
+
update: true
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
}).filter(entry => entry.id);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
addMarketplace(source, options = {}) {
|
|
429
|
+
const target = String(source || '').trim();
|
|
430
|
+
if (!target) throw new Error('Missing marketplace source');
|
|
431
|
+
this.run(['plugin', 'marketplace', 'add', target], options);
|
|
432
|
+
return this.listMarketplaces(options);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
removeMarketplace(name, options = {}) {
|
|
436
|
+
const target = String(name || '').trim();
|
|
437
|
+
if (!target) throw new Error('Missing marketplace name');
|
|
438
|
+
this.run(['plugin', 'marketplace', 'remove', target], options);
|
|
439
|
+
return this.listMarketplaces(options);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
updateMarketplaces(name = '', options = {}) {
|
|
443
|
+
const args = ['plugin', 'marketplace', 'update'];
|
|
444
|
+
if (name) args.push(name);
|
|
445
|
+
this.run(args, options);
|
|
446
|
+
return this.listMarketplaces(options);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
module.exports = {
|
|
451
|
+
OmpNativePluginAdapter,
|
|
452
|
+
collectEntries,
|
|
453
|
+
normalizeInstalledPlugin,
|
|
454
|
+
normalizeMarketplacePlugin,
|
|
455
|
+
parseJsonOutput
|
|
456
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const { createOmpUsageEventCursor } = require('./omp-sessions');
|
|
2
|
+
const { getEnabledChannels } = require('./omp-channels');
|
|
3
|
+
const {
|
|
4
|
+
getManagedProviderId,
|
|
5
|
+
isManagedProviderId,
|
|
6
|
+
normalizeProviderId
|
|
7
|
+
} = require('./omp-settings-manager');
|
|
8
|
+
const { buildSuccessLogPayload, hasMeaningfulUsage } = require('./proxy-log-helper');
|
|
9
|
+
const { broadcastLog } = require('../websocket-server');
|
|
10
|
+
|
|
11
|
+
const DEFAULT_INTERVAL_MS = 5000;
|
|
12
|
+
const DEFAULT_MAX_SEEN_EVENTS = 10000;
|
|
13
|
+
let pollTimer = null;
|
|
14
|
+
let seenEventKeys = new Set();
|
|
15
|
+
let maxSeenEvents = DEFAULT_MAX_SEEN_EVENTS;
|
|
16
|
+
let usageEventCursor = null;
|
|
17
|
+
|
|
18
|
+
function getEventKey(event = {}) {
|
|
19
|
+
return String(event.key || event.id || '').trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function rememberEventKey(key) {
|
|
23
|
+
if (!key) return;
|
|
24
|
+
if (seenEventKeys.has(key)) {
|
|
25
|
+
seenEventKeys.delete(key);
|
|
26
|
+
}
|
|
27
|
+
seenEventKeys.add(key);
|
|
28
|
+
while (seenEventKeys.size > maxSeenEvents) {
|
|
29
|
+
seenEventKeys.delete(seenEventKeys.values().next().value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveChannel(event = {}, channels = getEnabledChannels()) {
|
|
34
|
+
const providerId = normalizeProviderId(event.provider || '');
|
|
35
|
+
const matched = (channels || []).find((channel) => {
|
|
36
|
+
const candidates = [
|
|
37
|
+
getManagedProviderId(channel),
|
|
38
|
+
channel.providerKey,
|
|
39
|
+
channel.provider,
|
|
40
|
+
channel.name,
|
|
41
|
+
channel.id
|
|
42
|
+
].map(normalizeProviderId);
|
|
43
|
+
return candidates.includes(providerId);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
id: matched?.id || null,
|
|
48
|
+
name: matched?.name || event.provider || 'OMP'
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeTimestamp(value) {
|
|
53
|
+
const timestamp = value ? new Date(value).getTime() : Date.now();
|
|
54
|
+
return Number.isFinite(timestamp) ? timestamp : Date.now();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function publishEvent(event, channels) {
|
|
58
|
+
if (isManagedProviderId(normalizeProviderId(event.provider || ''))) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const channel = resolveChannel(event, channels);
|
|
62
|
+
const usage = event.usage || {};
|
|
63
|
+
broadcastLog(buildSuccessLogPayload({
|
|
64
|
+
source: 'omp',
|
|
65
|
+
requestId: event.id,
|
|
66
|
+
channel: channel.name,
|
|
67
|
+
model: event.model || '',
|
|
68
|
+
tokens: usage,
|
|
69
|
+
cost: Number(usage.cost) || 0,
|
|
70
|
+
timestamp: normalizeTimestamp(event.timestamp),
|
|
71
|
+
usageMissing: !hasMeaningfulUsage('omp', usage)
|
|
72
|
+
}));
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function pollOmpSessionLogs() {
|
|
77
|
+
if (!pollTimer) return getOmpSessionLogObserverStatus();
|
|
78
|
+
|
|
79
|
+
const events = usageEventCursor.read();
|
|
80
|
+
const channels = getEnabledChannels();
|
|
81
|
+
events.forEach((event) => {
|
|
82
|
+
const key = getEventKey(event);
|
|
83
|
+
if (!key || seenEventKeys.has(key)) return;
|
|
84
|
+
rememberEventKey(key);
|
|
85
|
+
publishEvent(event, channels);
|
|
86
|
+
});
|
|
87
|
+
return getOmpSessionLogObserverStatus();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function startOmpSessionLogObserver(options = {}) {
|
|
91
|
+
if (pollTimer) return getOmpSessionLogObserverStatus();
|
|
92
|
+
|
|
93
|
+
usageEventCursor = createOmpUsageEventCursor();
|
|
94
|
+
maxSeenEvents = Math.max(1, Number(options.maxSeenEvents) || DEFAULT_MAX_SEEN_EVENTS);
|
|
95
|
+
seenEventKeys = new Set();
|
|
96
|
+
usageEventCursor.read().map(getEventKey).filter(Boolean).forEach(rememberEventKey);
|
|
97
|
+
const intervalMs = Math.max(100, Number(options.intervalMs) || DEFAULT_INTERVAL_MS);
|
|
98
|
+
pollTimer = setInterval(() => {
|
|
99
|
+
try {
|
|
100
|
+
pollOmpSessionLogs();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.warn('[OMP Sessions] Failed to observe usage logs:', error.message);
|
|
103
|
+
}
|
|
104
|
+
}, intervalMs);
|
|
105
|
+
if (typeof pollTimer.unref === 'function') {
|
|
106
|
+
pollTimer.unref();
|
|
107
|
+
}
|
|
108
|
+
return getOmpSessionLogObserverStatus();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function stopOmpSessionLogObserver() {
|
|
112
|
+
if (pollTimer) {
|
|
113
|
+
clearInterval(pollTimer);
|
|
114
|
+
}
|
|
115
|
+
pollTimer = null;
|
|
116
|
+
seenEventKeys = new Set();
|
|
117
|
+
maxSeenEvents = DEFAULT_MAX_SEEN_EVENTS;
|
|
118
|
+
usageEventCursor?.reset?.();
|
|
119
|
+
usageEventCursor = null;
|
|
120
|
+
return getOmpSessionLogObserverStatus();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getOmpSessionLogObserverStatus() {
|
|
124
|
+
return {
|
|
125
|
+
running: Boolean(pollTimer),
|
|
126
|
+
seenEvents: seenEventKeys.size
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
startOmpSessionLogObserver,
|
|
132
|
+
stopOmpSessionLogObserver,
|
|
133
|
+
pollOmpSessionLogs,
|
|
134
|
+
getOmpSessionLogObserverStatus,
|
|
135
|
+
_test: {
|
|
136
|
+
resolveChannel
|
|
137
|
+
}
|
|
138
|
+
};
|
|
@@ -212,6 +212,42 @@ function parseSessionFile(filePath) {
|
|
|
212
212
|
};
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
function parseSessionUsageEvents(filePath) {
|
|
216
|
+
const entries = readJsonLines(filePath);
|
|
217
|
+
const header = entries.find(entry => entry?.type === 'session') || {};
|
|
218
|
+
const sessionId = header.id || parseOmpSessionId(filePath);
|
|
219
|
+
let activeProvider = '';
|
|
220
|
+
let activeModel = '';
|
|
221
|
+
const events = [];
|
|
222
|
+
|
|
223
|
+
entries.forEach((entry, index) => {
|
|
224
|
+
if (entry?.type === 'model_change') {
|
|
225
|
+
activeProvider = entry.provider || activeProvider;
|
|
226
|
+
activeModel = entry.modelId || entry.model || activeModel;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (entry?.type !== 'message') return;
|
|
230
|
+
|
|
231
|
+
const message = entry.message || {};
|
|
232
|
+
const role = message.role || entry.role;
|
|
233
|
+
if (role !== 'assistant') return;
|
|
234
|
+
|
|
235
|
+
const eventId = entry.id || message.id || `assistant-${index}`;
|
|
236
|
+
events.push({
|
|
237
|
+
key: `${filePath}:${eventId}`,
|
|
238
|
+
id: `${sessionId}:${eventId}`,
|
|
239
|
+
sessionId,
|
|
240
|
+
filePath,
|
|
241
|
+
provider: entry.provider || message.provider || activeProvider || '',
|
|
242
|
+
model: entry.model || message.model || activeModel || '',
|
|
243
|
+
timestamp: entry.timestamp || message.timestamp || header.timestamp || null,
|
|
244
|
+
usage: parseUsage(entry.usage || message.usage || {})
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return events;
|
|
249
|
+
}
|
|
250
|
+
|
|
215
251
|
function getOmpSessionPaths() {
|
|
216
252
|
// Reading historical sessions normally only needs OMP's native directory
|
|
217
253
|
// convention. Avoid starting the CLI here: project/count snapshot workers
|
|
@@ -264,6 +300,47 @@ function getAllSessions() {
|
|
|
264
300
|
.sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0));
|
|
265
301
|
}
|
|
266
302
|
|
|
303
|
+
function getOmpUsageEvents(rootDir = getOmpSessionPaths().sessions) {
|
|
304
|
+
return scanSessionFiles(rootDir).flatMap((filePath) => {
|
|
305
|
+
try {
|
|
306
|
+
return parseSessionUsageEvents(filePath);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.warn('[OMP Sessions] Failed to parse usage events:', filePath, error.message);
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function createOmpUsageEventCursor(rootDir = null) {
|
|
315
|
+
let signatures = new Map();
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
read() {
|
|
319
|
+
const files = scanSessionFiles(rootDir || getOmpSessionPaths().sessions);
|
|
320
|
+
const nextSignatures = new Map();
|
|
321
|
+
const events = [];
|
|
322
|
+
|
|
323
|
+
files.forEach((filePath) => {
|
|
324
|
+
try {
|
|
325
|
+
const stat = fs.statSync(filePath);
|
|
326
|
+
const signature = `${stat.size}:${stat.mtimeMs}`;
|
|
327
|
+
nextSignatures.set(filePath, signature);
|
|
328
|
+
if (signatures.get(filePath) === signature) return;
|
|
329
|
+
events.push(...parseSessionUsageEvents(filePath));
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.warn('[OMP Sessions] Failed to read changed usage events:', filePath, error.message);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
signatures = nextSignatures;
|
|
336
|
+
return events;
|
|
337
|
+
},
|
|
338
|
+
reset() {
|
|
339
|
+
signatures = new Map();
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
267
344
|
function loadProjectOrder() {
|
|
268
345
|
const data = safeReadJson(PROJECT_ORDER_FILE, { order: [] });
|
|
269
346
|
return Array.isArray(data.order) ? data.order : [];
|
|
@@ -491,12 +568,14 @@ function isOmpCliInstalled() {
|
|
|
491
568
|
|
|
492
569
|
module.exports = {
|
|
493
570
|
buildLaunchCommand,
|
|
571
|
+
createOmpUsageEventCursor,
|
|
494
572
|
decodeProjectName,
|
|
495
573
|
deleteProject,
|
|
496
574
|
deleteSession,
|
|
497
575
|
encodeProjectName,
|
|
498
576
|
forkSession,
|
|
499
577
|
getAllSessions,
|
|
578
|
+
getOmpUsageEvents,
|
|
500
579
|
getProjectAndSessionCounts,
|
|
501
580
|
getProjects,
|
|
502
581
|
getRecentSessions,
|
|
@@ -506,6 +585,7 @@ module.exports = {
|
|
|
506
585
|
isOmpInstalled: isOmpCliInstalled,
|
|
507
586
|
normalizeSession,
|
|
508
587
|
parseSessionFile,
|
|
588
|
+
parseSessionUsageEvents,
|
|
509
589
|
scanSessionFiles,
|
|
510
590
|
searchSessions,
|
|
511
591
|
saveProjectOrder,
|