coding-tool-x 3.6.13 → 3.7.0

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 (86) hide show
  1. package/README.md +46 -33
  2. package/dist/web/assets/{Analytics-CnA75GxO.js → Analytics-Dma_j2Rq.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-D8XEscQY.js → ConfigTemplates-C49Y1PLH.js} +1 -1
  4. package/dist/web/assets/Home-DGGC8H4g.js +1 -0
  5. package/dist/web/assets/Home-ewgeIhF7.css +1 -0
  6. package/dist/web/assets/{PluginManager-BzKZ8TYu.js → PluginManager-B1ECTHyY.js} +1 -1
  7. package/dist/web/assets/{ProjectList-DL4JK6ci.css → ProjectList-B_L55-l9.css} +1 -1
  8. package/dist/web/assets/ProjectList-DyiGyT-H.js +1 -0
  9. package/dist/web/assets/SessionList-CH5GwwiJ.js +1 -0
  10. package/dist/web/assets/{SkillManager-fxOvFTYD.js → SkillManager-CRyVhjYQ.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-Ch2mM8jm.js → WorkspaceManager-CO9tclVt.js} +1 -1
  12. package/dist/web/assets/{icons-QFXzEgqM.js → icons-Dop5j7Bf.js} +1 -1
  13. package/dist/web/assets/index-CM26UrnJ.css +1 -0
  14. package/dist/web/assets/index-EQq56pRa.js +2 -0
  15. package/dist/web/assets/{naive-ui-kcWDeJwl.js → naive-ui-CVyvdmg1.js} +1 -1
  16. package/dist/web/index.html +4 -4
  17. package/package.json +2 -1
  18. package/src/commands/channels.js +85 -24
  19. package/src/commands/daemon.js +7 -4
  20. package/src/commands/doctor.js +17 -8
  21. package/src/commands/logs.js +48 -6
  22. package/src/commands/port-config.js +83 -75
  23. package/src/commands/proxy-control.js +52 -19
  24. package/src/commands/stats.js +37 -19
  25. package/src/commands/switch.js +5 -1
  26. package/src/commands/toggle-proxy.js +131 -53
  27. package/src/commands/ui.js +3 -4
  28. package/src/commands/workspace.js +3 -35
  29. package/src/config/default.js +19 -2
  30. package/src/config/loader.js +5 -76
  31. package/src/config/paths.js +99 -11
  32. package/src/index.js +11 -16
  33. package/src/server/api/commands.js +1 -1
  34. package/src/server/api/config-export.js +4 -2
  35. package/src/server/api/config-registry.js +40 -34
  36. package/src/server/api/config.js +5 -1
  37. package/src/server/api/dashboard.js +22 -6
  38. package/src/server/api/mcp.js +5 -3
  39. package/src/server/api/pi-channels.js +349 -0
  40. package/src/server/api/pi-config.js +164 -0
  41. package/src/server/api/pi-projects.js +76 -0
  42. package/src/server/api/pi-proxy.js +154 -0
  43. package/src/server/api/pi-sessions.js +334 -0
  44. package/src/server/api/pi-statistics.js +40 -0
  45. package/src/server/api/plugins.js +1 -1
  46. package/src/server/api/prompts.js +3 -2
  47. package/src/server/api/settings.js +2 -0
  48. package/src/server/api/skills.js +1 -1
  49. package/src/server/api/workspaces.js +3 -41
  50. package/src/server/index.js +41 -0
  51. package/src/server/pi-proxy-server.js +66 -0
  52. package/src/server/services/agents-service.js +2 -1
  53. package/src/server/services/base/proxy-utils.js +1 -0
  54. package/src/server/services/channel-balance.js +151 -1
  55. package/src/server/services/channel-scheduler.js +7 -1
  56. package/src/server/services/commands-service.js +11 -2
  57. package/src/server/services/config-export-service.js +275 -18
  58. package/src/server/services/config-registry-service.js +20 -18
  59. package/src/server/services/config-sync-manager.js +115 -6
  60. package/src/server/services/config-sync-service.js +4 -2
  61. package/src/server/services/config-templates-service.js +183 -45
  62. package/src/server/services/mcp-service.js +217 -33
  63. package/src/server/services/pi-auth-providers.js +294 -0
  64. package/src/server/services/pi-channels.js +94 -0
  65. package/src/server/services/pi-config.js +287 -0
  66. package/src/server/services/pi-sessions.js +490 -0
  67. package/src/server/services/pi-settings-manager.js +571 -0
  68. package/src/server/services/pi-statistics-service.js +317 -0
  69. package/src/server/services/plugins-service.js +673 -11
  70. package/src/server/services/prompts-service.js +112 -7
  71. package/src/server/services/proxy-log-helper.js +1 -0
  72. package/src/server/services/proxy-runtime.js +5 -1
  73. package/src/server/services/sessions.js +19 -35
  74. package/src/server/services/skill-service.js +13 -3
  75. package/src/server/services/speed-test.js +9 -3
  76. package/src/server/services/ui-config.js +50 -12
  77. package/src/server/services/workspace-service.js +117 -225
  78. package/src/server/websocket-server.js +7 -1
  79. package/src/utils/session.js +4 -10
  80. package/dist/web/assets/Home-BiaKZMah.js +0 -1
  81. package/dist/web/assets/Home-CIua-b-o.css +0 -1
  82. package/dist/web/assets/ProjectList-BPfG_92X.js +0 -1
  83. package/dist/web/assets/SessionList-BIVtw00v.js +0 -1
  84. package/dist/web/assets/index-BpBFF3b2.js +0 -2
  85. package/dist/web/assets/index-Dsh4Q5Cg.css +0 -1
  86. package/src/utils/cli-flags.js +0 -7
@@ -0,0 +1,349 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ getChannels,
5
+ createChannel,
6
+ updateChannel,
7
+ deleteChannel,
8
+ saveChannelOrder
9
+ } = require('../services/pi-channels');
10
+ const { isPiInstalled } = require('../services/pi-sessions');
11
+ const { getSchedulerState } = require('../services/channel-scheduler');
12
+ const { getChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
13
+ const { broadcastSchedulerState } = require('../websocket-server');
14
+ const {
15
+ testChannelSpeed,
16
+ sanitizeBatchConcurrency,
17
+ runWithConcurrencyLimit
18
+ } = require('../services/speed-test');
19
+ const {
20
+ fetchModelsFromProvider,
21
+ probeModelAvailability
22
+ } = require('../services/model-detector');
23
+ const {
24
+ findAuthProviderForKey,
25
+ getOmpAuthProviderSnapshot
26
+ } = require('../services/pi-auth-providers');
27
+
28
+ function uniqueModels(models = []) {
29
+ const seen = new Set();
30
+ const result = [];
31
+ models.forEach((model) => {
32
+ if (typeof model !== 'string') return;
33
+ const trimmed = model.trim();
34
+ if (!trimmed) return;
35
+ const key = trimmed.toLowerCase();
36
+ if (seen.has(key)) return;
37
+ seen.add(key);
38
+ result.push(trimmed);
39
+ });
40
+ return result;
41
+ }
42
+
43
+ function resolveGatewaySourceType(channel) {
44
+ const value = String(channel?.gatewaySourceType || '').trim().toLowerCase();
45
+ if (value === 'claude') return 'claude';
46
+ if (value === 'codex') return 'codex';
47
+ if (value === 'gemini') return 'gemini';
48
+ if (value === 'opencode') return 'opencode';
49
+ if (value === 'openai_compatible') return 'openai_compatible';
50
+ return 'openai_compatible';
51
+ }
52
+
53
+ function collectPreferredModels(channel = {}) {
54
+ return uniqueModels([
55
+ channel.model,
56
+ channel.speedTestModel,
57
+ ...(Array.isArray(channel.allowedModels) ? channel.allowedModels : []),
58
+ ...(Array.isArray(channel.modelRedirects)
59
+ ? channel.modelRedirects.flatMap(rule => [rule?.from, rule?.to])
60
+ : [])
61
+ ]);
62
+ }
63
+
64
+ function attachAuthProvider(channel, snapshot) {
65
+ const authProvider = findAuthProviderForKey(channel.providerKey || channel.provider || channel.name, snapshot);
66
+ if (!authProvider) return channel;
67
+ return {
68
+ ...channel,
69
+ ompAuthProvider: {
70
+ id: authProvider.id,
71
+ name: authProvider.name,
72
+ loggedIn: authProvider.loggedIn,
73
+ accountCount: authProvider.accountCount,
74
+ accounts: authProvider.accounts || [],
75
+ checked: authProvider.checked,
76
+ error: authProvider.error || null
77
+ }
78
+ };
79
+ }
80
+
81
+ module.exports = () => {
82
+ router.get('/', (req, res) => {
83
+ try {
84
+ if (!isPiInstalled()) {
85
+ return res.json({ channels: [], installed: false, error: 'OMP CLI not installed' });
86
+ }
87
+ const data = getChannels();
88
+ const authSnapshot = getOmpAuthProviderSnapshot({ accountCheck: false, includeStatus: false });
89
+ const channels = (data.channels || []).map(ch => ({
90
+ ...attachAuthProvider(ch, authSnapshot),
91
+ health: getChannelHealthStatus(ch.id, 'pi')
92
+ }));
93
+ res.json({ channels, installed: true });
94
+ } catch (err) {
95
+ console.error('[OMP Channels API] Failed to get channels:', err);
96
+ res.status(500).json({ error: err.message });
97
+ }
98
+ });
99
+
100
+ router.get('/enabled', (req, res) => {
101
+ try {
102
+ if (!isPiInstalled()) {
103
+ return res.json({ channels: [], installed: false, error: 'OMP CLI not installed' });
104
+ }
105
+ const data = getChannels();
106
+ const authSnapshot = getOmpAuthProviderSnapshot({ accountCheck: false, includeStatus: false });
107
+ const channels = (data.channels || [])
108
+ .filter(ch => ch.enabled !== false)
109
+ .map(ch => ({ ...attachAuthProvider(ch, authSnapshot), health: getChannelHealthStatus(ch.id, 'pi') }));
110
+ res.json({ channels, installed: true });
111
+ } catch (err) {
112
+ console.error('[OMP Channels API] Failed to get enabled channels:', err);
113
+ res.status(500).json({ error: err.message });
114
+ }
115
+ });
116
+
117
+ router.post('/probe-models', async (req, res) => {
118
+ try {
119
+ const { baseUrl, apiKey, gatewaySourceType } = req.body || {};
120
+ if (!baseUrl) {
121
+ return res.status(400).json({ error: 'baseUrl is required' });
122
+ }
123
+ const tempChannel = { baseUrl, apiKey: apiKey || '', gatewaySourceType: gatewaySourceType || 'openai_compatible' };
124
+ const resolvedGatewaySourceType = resolveGatewaySourceType(tempChannel);
125
+ const modelListSourceType = resolvedGatewaySourceType === 'opencode' ? 'openai_compatible' : resolvedGatewaySourceType;
126
+ const listResult = await fetchModelsFromProvider(tempChannel, modelListSourceType, {
127
+ useV1ModelsEndpoint: true,
128
+ forceRefresh: true
129
+ });
130
+ const models = Array.isArray(listResult.models) ? uniqueModels(listResult.models) : [];
131
+ res.json({
132
+ models,
133
+ supported: models.length > 0,
134
+ error: models.length > 0 ? null : (listResult.error || '未返回可用模型列表'),
135
+ errorHint: models.length > 0 ? null : (listResult.errorHint || '请手动填写模型名称')
136
+ });
137
+ } catch (error) {
138
+ console.error('[OMP Channels API] Error probing models:', error);
139
+ res.status(500).json({ error: 'Failed to probe models' });
140
+ }
141
+ });
142
+
143
+ router.get('/:channelId/models', async (req, res) => {
144
+ try {
145
+ const { channelId } = req.params;
146
+ const channels = getChannels().channels || [];
147
+ const channel = channels.find(ch => ch.id === channelId);
148
+ if (!channel) {
149
+ return res.status(404).json({ error: 'Channel not found' });
150
+ }
151
+ const forceRefresh = req.query.forceRefresh === 'true';
152
+ const gatewaySourceType = resolveGatewaySourceType(channel);
153
+ const modelListSourceType = gatewaySourceType === 'opencode' ? 'openai_compatible' : gatewaySourceType;
154
+ const listResult = await fetchModelsFromProvider(channel, modelListSourceType, {
155
+ useV1ModelsEndpoint: true,
156
+ forceRefresh
157
+ });
158
+ let models = Array.isArray(listResult.models) ? uniqueModels(listResult.models) : [];
159
+ let error = models.length > 0 ? null : listResult.error;
160
+ let errorHint = models.length > 0 ? null : listResult.errorHint;
161
+
162
+ if (models.length === 0) {
163
+ const probe = await probeModelAvailability(channel, gatewaySourceType, {
164
+ stopOnFirstAvailable: false,
165
+ preferredModels: collectPreferredModels(channel)
166
+ });
167
+ models = Array.isArray(probe.availableModels) ? uniqueModels(probe.availableModels) : [];
168
+ error = models.length > 0 ? null : (error || '无法获取可用模型');
169
+ errorHint = models.length > 0 ? '模型列表接口不可用,已自动切换为模型探测结果' : (errorHint || '请手动填写模型名称');
170
+ }
171
+
172
+ res.json({
173
+ channelId,
174
+ gatewaySourceType,
175
+ models,
176
+ supported: models.length > 0,
177
+ cached: !!listResult.cached,
178
+ fallbackUsed: false,
179
+ fetchedAt: listResult.lastChecked || new Date().toISOString(),
180
+ error,
181
+ errorHint
182
+ });
183
+ } catch (error) {
184
+ console.error('[OMP Channels API] Error fetching models:', error);
185
+ res.status(500).json({ error: 'Failed to fetch model list', channelId: req.params.channelId });
186
+ }
187
+ });
188
+
189
+ router.post('/', (req, res) => {
190
+ try {
191
+ const {
192
+ name,
193
+ baseUrl,
194
+ apiKey,
195
+ wireApi,
196
+ enabled,
197
+ weight,
198
+ maxConcurrency,
199
+ model,
200
+ gatewaySourceType,
201
+ modelRedirects,
202
+ allowedModels,
203
+ speedTestModel,
204
+ providerKey,
205
+ providerApi,
206
+ presetId,
207
+ websiteUrl,
208
+ balanceToken,
209
+ balanceUserId
210
+ } = req.body || {};
211
+
212
+ if (!name || !baseUrl) {
213
+ return res.status(400).json({ error: 'Missing required fields: name and baseUrl' });
214
+ }
215
+ if (!apiKey) {
216
+ return res.status(400).json({ error: 'API Key is required' });
217
+ }
218
+
219
+ const channel = createChannel(name, baseUrl, apiKey, {
220
+ wireApi: wireApi || 'openai',
221
+ providerApi: providerApi || wireApi || 'openai-completions',
222
+ providerKey,
223
+ enabled,
224
+ weight,
225
+ maxConcurrency,
226
+ model,
227
+ gatewaySourceType: gatewaySourceType || 'openai_compatible',
228
+ modelRedirects: modelRedirects || [],
229
+ allowedModels: Array.isArray(allowedModels) ? allowedModels : [],
230
+ speedTestModel: speedTestModel || null,
231
+ presetId,
232
+ websiteUrl: websiteUrl || '',
233
+ balanceToken: balanceToken || '',
234
+ balanceUserId: balanceUserId || null
235
+ });
236
+ res.json(channel);
237
+ broadcastSchedulerState('pi', getSchedulerState('pi'));
238
+ } catch (err) {
239
+ console.error('[OMP Channels API] Failed to create channel:', err);
240
+ res.status(500).json({ error: err.message });
241
+ }
242
+ });
243
+
244
+ router.put('/:channelId', (req, res) => {
245
+ try {
246
+ const channel = updateChannel(req.params.channelId, req.body || {});
247
+ res.json(channel);
248
+ broadcastSchedulerState('pi', getSchedulerState('pi'));
249
+ } catch (err) {
250
+ console.error('[OMP Channels API] Failed to update channel:', err);
251
+ res.status(500).json({ error: err.message });
252
+ }
253
+ });
254
+
255
+ router.delete('/:channelId', async (req, res) => {
256
+ try {
257
+ const result = await deleteChannel(req.params.channelId);
258
+ res.json(result);
259
+ broadcastSchedulerState('pi', getSchedulerState('pi'));
260
+ } catch (err) {
261
+ console.error('[OMP Channels API] Failed to delete channel:', err);
262
+ res.status(500).json({ error: err.message });
263
+ }
264
+ });
265
+
266
+ router.post('/order', (req, res) => {
267
+ try {
268
+ const { order } = req.body || {};
269
+ if (!Array.isArray(order)) {
270
+ return res.status(400).json({ error: 'Order must be an array' });
271
+ }
272
+ saveChannelOrder(order);
273
+ res.json({ success: true });
274
+ broadcastSchedulerState('pi', getSchedulerState('pi'));
275
+ } catch (err) {
276
+ console.error('[OMP Channels API] Failed to save order:', err);
277
+ res.status(500).json({ error: err.message });
278
+ }
279
+ });
280
+
281
+ router.post('/:channelId/reset-health', (req, res) => {
282
+ try {
283
+ resetChannelHealth(req.params.channelId, 'pi');
284
+ res.json({ success: true });
285
+ broadcastSchedulerState('pi', getSchedulerState('pi'));
286
+ } catch (err) {
287
+ console.error('[OMP Channels API] Failed to reset health:', err);
288
+ res.status(500).json({ error: err.message });
289
+ }
290
+ });
291
+
292
+ router.post('/:channelId/speed-test', async (req, res) => {
293
+ try {
294
+ const channel = (getChannels().channels || []).find(ch => ch.id === req.params.channelId);
295
+ if (!channel) {
296
+ return res.status(404).json({ error: 'Channel not found' });
297
+ }
298
+ const result = await testChannelSpeed(channel, req.body?.timeout || 20000, resolveGatewaySourceType(channel), {
299
+ authSourceType: 'pi'
300
+ });
301
+ res.json(result);
302
+ } catch (error) {
303
+ console.error('[OMP Channels API] Speed test failed:', error);
304
+ res.status(500).json({ error: error.message });
305
+ }
306
+ });
307
+
308
+ router.post('/speed-test-all', async (req, res) => {
309
+ try {
310
+ const { timeout = 20000, concurrency } = req.body || {};
311
+ const channels = getChannels().channels || [];
312
+ const safeConcurrency = sanitizeBatchConcurrency(concurrency);
313
+ const results = await runWithConcurrencyLimit(
314
+ channels,
315
+ safeConcurrency,
316
+ channel => testChannelSpeed(channel, timeout, resolveGatewaySourceType(channel), {
317
+ authSourceType: 'pi'
318
+ })
319
+ );
320
+ results.sort((a, b) => {
321
+ if (a.success && !b.success) return -1;
322
+ if (!a.success && b.success) return 1;
323
+ if (a.success && b.success) {
324
+ return ((a.latency ?? Infinity) - (b.latency ?? Infinity));
325
+ }
326
+ return 0;
327
+ });
328
+ const successResults = results.filter(r => r.success);
329
+ const successWithLatency = successResults.filter(r => r.latency !== null && r.latency !== undefined);
330
+ res.json({
331
+ results,
332
+ summary: {
333
+ total: results.length,
334
+ success: successResults.length,
335
+ failed: results.length - successResults.length,
336
+ avgLatency: successWithLatency.length > 0
337
+ ? Math.round(successWithLatency.reduce((sum, r) => sum + r.latency, 0) / successWithLatency.length)
338
+ : null,
339
+ concurrency: safeConcurrency
340
+ }
341
+ });
342
+ } catch (error) {
343
+ console.error('[OMP Channels API] Speed test all failed:', error);
344
+ res.status(500).json({ error: error.message });
345
+ }
346
+ });
347
+
348
+ return router;
349
+ };
@@ -0,0 +1,164 @@
1
+ const express = require('express');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const router = express.Router();
5
+ const {
6
+ getPiPaths,
7
+ getPiStatus,
8
+ readJsonFile,
9
+ readYamlFile,
10
+ readPiSettings
11
+ } = require('../services/pi-config');
12
+ const {
13
+ getOmpAuthProviderSnapshot
14
+ } = require('../services/pi-auth-providers');
15
+
16
+ function listDirEntries(dirPath, options = {}) {
17
+ const { extensions = null, includeDirectories = true } = options;
18
+ if (!fs.existsSync(dirPath)) return [];
19
+ try {
20
+ return fs.readdirSync(dirPath, { withFileTypes: true })
21
+ .filter(entry => !entry.name.startsWith('.'))
22
+ .filter((entry) => {
23
+ if (entry.isDirectory()) return includeDirectories;
24
+ if (!Array.isArray(extensions) || extensions.length === 0) return true;
25
+ return extensions.includes(path.extname(entry.name).toLowerCase());
26
+ })
27
+ .map((entry) => {
28
+ const fullPath = path.join(dirPath, entry.name);
29
+ const stat = fs.statSync(fullPath);
30
+ return {
31
+ name: entry.name,
32
+ type: entry.isDirectory() ? 'directory' : 'file',
33
+ path: fullPath,
34
+ size: stat.size,
35
+ updatedAt: new Date(stat.mtimeMs).toISOString()
36
+ };
37
+ });
38
+ } catch {
39
+ return [];
40
+ }
41
+ }
42
+
43
+ function normalizeArray(value) {
44
+ return Array.isArray(value) ? value : [];
45
+ }
46
+
47
+ function buildCapabilities() {
48
+ return {
49
+ platform: 'pi',
50
+ runtime: 'omp',
51
+ native: {
52
+ config: true,
53
+ settings: true,
54
+ models: true,
55
+ commands: true,
56
+ notes: true,
57
+ extensions: true,
58
+ skills: true,
59
+ promptTemplates: true,
60
+ packages: true,
61
+ rpc: true,
62
+ sessions: true
63
+ },
64
+ mapped: {
65
+ command: 'commands/prompts',
66
+ plugin: 'packages/extensions',
67
+ mcp: 'mcp.json',
68
+ agent: 'OMP discovery capability'
69
+ },
70
+ writable: {
71
+ config: true,
72
+ models: true,
73
+ commands: true,
74
+ notes: true,
75
+ settings: true,
76
+ skills: true,
77
+ promptTemplates: true,
78
+ packages: true,
79
+ extensions: true,
80
+ mcp: true,
81
+ agent: false
82
+ },
83
+ notes: [
84
+ 'The pi channel is backed by OMP. coding-tool-x keeps the pi route/key for compatibility.',
85
+ 'Provider channels are enabled by merging managed ctx-* providers into OMP models.yml.'
86
+ ]
87
+ };
88
+ }
89
+
90
+ router.get('/', (req, res) => {
91
+ try {
92
+ const paths = getPiPaths();
93
+ const settings = readPiSettings();
94
+ const resources = {
95
+ config: readYamlFile(paths.config, {}),
96
+ settings: readYamlFile(paths.settings, {}),
97
+ auth: fs.existsSync(paths.auth) ? { exists: true, path: paths.auth } : { exists: false, path: paths.auth },
98
+ authProviders: getOmpAuthProviderSnapshot({ accountCheck: false }),
99
+ models: readYamlFile(paths.modelsYml, {}),
100
+ legacyModels: readJsonFile(paths.modelsJsonLegacy, {}),
101
+ packages: normalizeArray(settings.packages),
102
+ disabledPackages: normalizeArray(settings.disabledPackages),
103
+ skills: listDirEntries(paths.skills),
104
+ prompts: listDirEntries(paths.prompts, { extensions: ['.md'], includeDirectories: true }),
105
+ commands: listDirEntries(paths.commands, { extensions: ['.md'], includeDirectories: true }),
106
+ notes: listDirEntries(paths.notes, { extensions: ['.md'], includeDirectories: true }),
107
+ extensions: listDirEntries(paths.extensions, { extensions: ['.js', '.mjs', '.cjs', '.ts'], includeDirectories: true }),
108
+ sessionsDir: paths.sessions
109
+ };
110
+
111
+ res.json({
112
+ success: true,
113
+ status: getPiStatus(),
114
+ capabilities: buildCapabilities(),
115
+ resources
116
+ });
117
+ } catch (error) {
118
+ console.error('[OMP Config API] Failed to read config:', error);
119
+ res.status(500).json({ error: error.message });
120
+ }
121
+ });
122
+
123
+ router.get('/capabilities', (req, res) => {
124
+ res.json({
125
+ success: true,
126
+ platform: 'pi',
127
+ capabilities: buildCapabilities()
128
+ });
129
+ });
130
+
131
+ router.get('/auth-providers', (req, res) => {
132
+ try {
133
+ const forceRefresh = req.query.forceRefresh === 'true';
134
+ res.json({
135
+ success: true,
136
+ ...getOmpAuthProviderSnapshot({ forceRefresh })
137
+ });
138
+ } catch (error) {
139
+ console.error('[OMP Config API] Failed to list auth providers:', error);
140
+ res.status(500).json({ error: error.message });
141
+ }
142
+ });
143
+
144
+ router.get('/resources', (req, res) => {
145
+ try {
146
+ const paths = getPiPaths();
147
+ const settings = readPiSettings();
148
+ res.json({
149
+ success: true,
150
+ paths,
151
+ packages: normalizeArray(settings.packages),
152
+ skills: listDirEntries(paths.skills),
153
+ prompts: listDirEntries(paths.prompts, { extensions: ['.md'], includeDirectories: true }),
154
+ commands: listDirEntries(paths.commands, { extensions: ['.md'], includeDirectories: true }),
155
+ notes: listDirEntries(paths.notes, { extensions: ['.md'], includeDirectories: true }),
156
+ extensions: listDirEntries(paths.extensions, { extensions: ['.js', '.mjs', '.cjs', '.ts'], includeDirectories: true })
157
+ });
158
+ } catch (error) {
159
+ console.error('[OMP Config API] Failed to list resources:', error);
160
+ res.status(500).json({ error: error.message });
161
+ }
162
+ });
163
+
164
+ module.exports = router;
@@ -0,0 +1,76 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ getProjects,
5
+ saveProjectOrder,
6
+ deleteProject,
7
+ isPiInstalled
8
+ } = require('../services/pi-sessions');
9
+
10
+ function isNotFoundError(error) {
11
+ return !!(error && error.message === 'Project not found');
12
+ }
13
+
14
+ module.exports = () => {
15
+ router.get('/', (req, res) => {
16
+ try {
17
+ if (!isPiInstalled()) {
18
+ return res.json({
19
+ projects: [],
20
+ currentProject: null,
21
+ error: 'OMP CLI not installed or not found'
22
+ });
23
+ }
24
+
25
+ const projects = getProjects();
26
+ res.json({
27
+ projects,
28
+ currentProject: projects[0] ? projects[0].name : null
29
+ });
30
+ } catch (err) {
31
+ console.error('[OMP API] Failed to get projects:', err);
32
+ if (err.code === 'ENOENT') {
33
+ return res.status(404).json({
34
+ error: 'OMP data directory not found',
35
+ projects: []
36
+ });
37
+ }
38
+ res.status(500).json({ error: err.message, projects: [] });
39
+ }
40
+ });
41
+
42
+ router.post('/order', (req, res) => {
43
+ try {
44
+ if (!isPiInstalled()) {
45
+ return res.status(404).json({ error: 'OMP CLI not installed' });
46
+ }
47
+ const { order } = req.body || {};
48
+ if (!Array.isArray(order)) {
49
+ return res.status(400).json({ error: 'order must be an array' });
50
+ }
51
+ saveProjectOrder(order);
52
+ res.json({ success: true });
53
+ } catch (err) {
54
+ console.error('[OMP API] Failed to save project order:', err);
55
+ res.status(500).json({ error: err.message });
56
+ }
57
+ });
58
+
59
+ router.delete('/:projectName', (req, res) => {
60
+ try {
61
+ if (!isPiInstalled()) {
62
+ return res.status(404).json({ error: 'OMP CLI not installed' });
63
+ }
64
+ const result = deleteProject(req.params.projectName);
65
+ res.json(result);
66
+ } catch (err) {
67
+ if (isNotFoundError(err)) {
68
+ return res.status(404).json({ error: err.message });
69
+ }
70
+ console.error('[OMP API] Failed to delete project:', err);
71
+ res.status(500).json({ error: err.message });
72
+ }
73
+ });
74
+
75
+ return router;
76
+ };