coding-tool-x 3.6.12 → 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 (80) hide show
  1. package/README.md +46 -33
  2. package/dist/web/assets/{Analytics-BuBlsbcI.js → Analytics-Dma_j2Rq.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-DQczxuOX.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-C8gAlaps.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-BDbNbNkK.js → SkillManager-CRyVhjYQ.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-DGGaGW2c.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 +81 -75
  23. package/src/commands/proxy-control.js +52 -19
  24. package/src/commands/stats.js +37 -19
  25. package/src/commands/toggle-proxy.js +131 -53
  26. package/src/commands/ui.js +3 -4
  27. package/src/config/default.js +14 -3
  28. package/src/config/paths.js +99 -11
  29. package/src/index.js +11 -16
  30. package/src/server/api/commands.js +1 -1
  31. package/src/server/api/config-export.js +4 -2
  32. package/src/server/api/config-registry.js +40 -34
  33. package/src/server/api/config.js +2 -1
  34. package/src/server/api/dashboard.js +22 -6
  35. package/src/server/api/mcp.js +5 -3
  36. package/src/server/api/pi-channels.js +349 -0
  37. package/src/server/api/pi-config.js +164 -0
  38. package/src/server/api/pi-projects.js +76 -0
  39. package/src/server/api/pi-proxy.js +154 -0
  40. package/src/server/api/pi-sessions.js +334 -0
  41. package/src/server/api/pi-statistics.js +40 -0
  42. package/src/server/api/plugins.js +1 -1
  43. package/src/server/api/prompts.js +3 -2
  44. package/src/server/api/skills.js +1 -1
  45. package/src/server/api/workspaces.js +2 -2
  46. package/src/server/index.js +41 -0
  47. package/src/server/pi-proxy-server.js +66 -0
  48. package/src/server/services/agents-service.js +2 -1
  49. package/src/server/services/base/proxy-utils.js +1 -0
  50. package/src/server/services/channel-balance.js +151 -1
  51. package/src/server/services/channel-scheduler.js +7 -1
  52. package/src/server/services/commands-service.js +11 -2
  53. package/src/server/services/config-export-service.js +275 -18
  54. package/src/server/services/config-registry-service.js +20 -18
  55. package/src/server/services/config-sync-manager.js +115 -6
  56. package/src/server/services/config-sync-service.js +4 -2
  57. package/src/server/services/config-templates-service.js +183 -45
  58. package/src/server/services/mcp-service.js +217 -33
  59. package/src/server/services/pi-auth-providers.js +294 -0
  60. package/src/server/services/pi-channels.js +94 -0
  61. package/src/server/services/pi-config.js +287 -0
  62. package/src/server/services/pi-sessions.js +490 -0
  63. package/src/server/services/pi-settings-manager.js +571 -0
  64. package/src/server/services/pi-statistics-service.js +317 -0
  65. package/src/server/services/plugins-service.js +673 -11
  66. package/src/server/services/prompts-service.js +112 -7
  67. package/src/server/services/proxy-log-helper.js +1 -0
  68. package/src/server/services/proxy-runtime.js +5 -1
  69. package/src/server/services/skill-service.js +13 -3
  70. package/src/server/services/speed-test.js +9 -3
  71. package/src/server/services/ui-config.js +50 -12
  72. package/src/server/services/workspace-service.js +19 -5
  73. package/src/server/websocket-server.js +7 -1
  74. package/dist/web/assets/Home-BvAzzWnB.js +0 -1
  75. package/dist/web/assets/Home-CIua-b-o.css +0 -1
  76. package/dist/web/assets/ProjectList-nqgi6_cR.js +0 -1
  77. package/dist/web/assets/SessionList-D8AxMYy6.js +0 -1
  78. package/dist/web/assets/index-Dsh4Q5Cg.css +0 -1
  79. package/dist/web/assets/index-y-eEayxR.js +0 -2
  80. package/src/utils/cli-flags.js +0 -7
@@ -5,6 +5,8 @@
5
5
  const express = require('express');
6
6
  const router = express.Router();
7
7
  const mcpService = require('../services/mcp-service');
8
+ const MCP_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode', 'pi'];
9
+ const MCP_EXPORT_FORMATS = ['json', 'claude', 'codex', 'gemini', 'opencode', 'pi'];
8
10
 
9
11
  /**
10
12
  * GET /api/mcp/servers
@@ -177,7 +179,7 @@ router.post('/import/:platform', async (req, res) => {
177
179
  try {
178
180
  const { platform } = req.params;
179
181
 
180
- if (!['claude', 'codex', 'gemini', 'opencode'].includes(platform)) {
182
+ if (!MCP_PLATFORMS.includes(platform)) {
181
183
  return res.status(400).json({
182
184
  success: false,
183
185
  error: `无效的平台: ${platform}`
@@ -285,7 +287,7 @@ router.get('/export', (req, res) => {
285
287
  try {
286
288
  const format = req.query.format || 'json';
287
289
 
288
- if (!['json', 'claude', 'codex', 'opencode'].includes(format)) {
290
+ if (!MCP_EXPORT_FORMATS.includes(format)) {
289
291
  return res.status(400).json({
290
292
  success: false,
291
293
  error: `无效的导出格式: ${format}`
@@ -314,7 +316,7 @@ router.get('/export/download', (req, res) => {
314
316
  try {
315
317
  const format = req.query.format || 'json';
316
318
 
317
- if (!['json', 'claude', 'codex', 'opencode'].includes(format)) {
319
+ if (!MCP_EXPORT_FORMATS.includes(format)) {
318
320
  return res.status(400).json({
319
321
  success: false,
320
322
  error: `无效的导出格式: ${format}`
@@ -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
+ };