opencode-studio-server 1.1.0 → 1.1.3
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/index.js +1126 -986
- package/launcher.vbs +6 -6
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,986 +1,1126 @@
|
|
|
1
|
-
const express = require('express');
|
|
2
|
-
const cors = require('cors');
|
|
3
|
-
const bodyParser = require('body-parser');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const { exec, spawn } = require('child_process');
|
|
8
|
-
|
|
9
|
-
const app = express();
|
|
10
|
-
const PORT = 3001;
|
|
11
|
-
const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
12
|
-
|
|
13
|
-
let lastActivityTime = Date.now();
|
|
14
|
-
let idleTimer = null;
|
|
15
|
-
|
|
16
|
-
function resetIdleTimer() {
|
|
17
|
-
lastActivityTime = Date.now();
|
|
18
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
19
|
-
idleTimer = setTimeout(() => {
|
|
20
|
-
console.log('Server idle for 30 minutes, shutting down...');
|
|
21
|
-
process.exit(0);
|
|
22
|
-
}, IDLE_TIMEOUT_MS);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
resetIdleTimer();
|
|
26
|
-
|
|
27
|
-
app.use((req, res, next) => {
|
|
28
|
-
resetIdleTimer();
|
|
29
|
-
next();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const ALLOWED_ORIGINS = [
|
|
33
|
-
'http://localhost:3000',
|
|
34
|
-
'http://127.0.0.1:3000',
|
|
35
|
-
'https://opencode-studio.vercel.app',
|
|
36
|
-
'https://opencode.micr.dev',
|
|
37
|
-
'https://opencode-studio.micr.dev',
|
|
38
|
-
/\.vercel\.app$/,
|
|
39
|
-
/\.micr\.dev$/,
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
app.use(cors({
|
|
43
|
-
origin: (origin, callback) => {
|
|
44
|
-
if (!origin) return callback(null, true);
|
|
45
|
-
const allowed = ALLOWED_ORIGINS.some(o =>
|
|
46
|
-
o instanceof RegExp ? o.test(origin) : o === origin
|
|
47
|
-
);
|
|
48
|
-
callback(null, allowed);
|
|
49
|
-
},
|
|
50
|
-
credentials: true,
|
|
51
|
-
}));
|
|
52
|
-
app.use(bodyParser.json({ limit: '50mb' }));
|
|
53
|
-
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
|
54
|
-
|
|
55
|
-
const HOME_DIR = os.homedir();
|
|
56
|
-
const STUDIO_CONFIG_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'studio.json');
|
|
57
|
-
const PENDING_ACTION_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'pending-action.json');
|
|
58
|
-
|
|
59
|
-
let pendingActionMemory = null;
|
|
60
|
-
|
|
61
|
-
function loadStudioConfig() {
|
|
62
|
-
if (!fs.existsSync(STUDIO_CONFIG_PATH)) {
|
|
63
|
-
return {};
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
return JSON.parse(fs.readFileSync(STUDIO_CONFIG_PATH, 'utf8'));
|
|
67
|
-
} catch {
|
|
68
|
-
return {};
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function saveStudioConfig(config) {
|
|
73
|
-
try {
|
|
74
|
-
const dir = path.dirname(STUDIO_CONFIG_PATH);
|
|
75
|
-
if (!fs.existsSync(dir)) {
|
|
76
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
77
|
-
}
|
|
78
|
-
fs.writeFileSync(STUDIO_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
|
|
79
|
-
return true;
|
|
80
|
-
} catch (err) {
|
|
81
|
-
console.error('Failed to save studio config:', err);
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function loadPendingAction() {
|
|
87
|
-
if (pendingActionMemory) return pendingActionMemory;
|
|
88
|
-
|
|
89
|
-
if (fs.existsSync(PENDING_ACTION_PATH)) {
|
|
90
|
-
try {
|
|
91
|
-
const action = JSON.parse(fs.readFileSync(PENDING_ACTION_PATH, 'utf8'));
|
|
92
|
-
if (action.timestamp && Date.now() - action.timestamp < 60000) {
|
|
93
|
-
pendingActionMemory = action;
|
|
94
|
-
fs.unlinkSync(PENDING_ACTION_PATH);
|
|
95
|
-
return action;
|
|
96
|
-
}
|
|
97
|
-
fs.unlinkSync(PENDING_ACTION_PATH);
|
|
98
|
-
} catch {
|
|
99
|
-
try { fs.unlinkSync(PENDING_ACTION_PATH); } catch {}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
app.get('/api/pending-action', (req, res) => {
|
|
106
|
-
const action = loadPendingAction();
|
|
107
|
-
res.json({ action });
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
app.delete('/api/pending-action', (req, res) => {
|
|
111
|
-
pendingActionMemory = null;
|
|
112
|
-
if (fs.existsSync(PENDING_ACTION_PATH)) {
|
|
113
|
-
try { fs.unlinkSync(PENDING_ACTION_PATH); } catch {}
|
|
114
|
-
}
|
|
115
|
-
res.json({ success: true });
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const getPaths = () => {
|
|
119
|
-
const platform = process.platform;
|
|
120
|
-
const home = os.homedir();
|
|
121
|
-
|
|
122
|
-
let candidates = [];
|
|
123
|
-
if (platform === 'win32') {
|
|
124
|
-
candidates = [
|
|
125
|
-
path.join(process.env.APPDATA, 'opencode', 'opencode.json'),
|
|
126
|
-
path.join(home, '.config', 'opencode', 'opencode.json'),
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
path.join(home, '.opencode', 'opencode.json'),
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
res.
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if (!fs.existsSync(
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
ensureAuthProfilesDir();
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
})
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const cors = require('cors');
|
|
3
|
+
const bodyParser = require('body-parser');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { exec, spawn } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const app = express();
|
|
10
|
+
const PORT = 3001;
|
|
11
|
+
const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
let lastActivityTime = Date.now();
|
|
14
|
+
let idleTimer = null;
|
|
15
|
+
|
|
16
|
+
function resetIdleTimer() {
|
|
17
|
+
lastActivityTime = Date.now();
|
|
18
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
19
|
+
idleTimer = setTimeout(() => {
|
|
20
|
+
console.log('Server idle for 30 minutes, shutting down...');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}, IDLE_TIMEOUT_MS);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
resetIdleTimer();
|
|
26
|
+
|
|
27
|
+
app.use((req, res, next) => {
|
|
28
|
+
resetIdleTimer();
|
|
29
|
+
next();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const ALLOWED_ORIGINS = [
|
|
33
|
+
'http://localhost:3000',
|
|
34
|
+
'http://127.0.0.1:3000',
|
|
35
|
+
'https://opencode-studio.vercel.app',
|
|
36
|
+
'https://opencode.micr.dev',
|
|
37
|
+
'https://opencode-studio.micr.dev',
|
|
38
|
+
/\.vercel\.app$/,
|
|
39
|
+
/\.micr\.dev$/,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
app.use(cors({
|
|
43
|
+
origin: (origin, callback) => {
|
|
44
|
+
if (!origin) return callback(null, true);
|
|
45
|
+
const allowed = ALLOWED_ORIGINS.some(o =>
|
|
46
|
+
o instanceof RegExp ? o.test(origin) : o === origin
|
|
47
|
+
);
|
|
48
|
+
callback(null, allowed);
|
|
49
|
+
},
|
|
50
|
+
credentials: true,
|
|
51
|
+
}));
|
|
52
|
+
app.use(bodyParser.json({ limit: '50mb' }));
|
|
53
|
+
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
|
54
|
+
|
|
55
|
+
const HOME_DIR = os.homedir();
|
|
56
|
+
const STUDIO_CONFIG_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'studio.json');
|
|
57
|
+
const PENDING_ACTION_PATH = path.join(HOME_DIR, '.config', 'opencode-studio', 'pending-action.json');
|
|
58
|
+
|
|
59
|
+
let pendingActionMemory = null;
|
|
60
|
+
|
|
61
|
+
function loadStudioConfig() {
|
|
62
|
+
if (!fs.existsSync(STUDIO_CONFIG_PATH)) {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(fs.readFileSync(STUDIO_CONFIG_PATH, 'utf8'));
|
|
67
|
+
} catch {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function saveStudioConfig(config) {
|
|
73
|
+
try {
|
|
74
|
+
const dir = path.dirname(STUDIO_CONFIG_PATH);
|
|
75
|
+
if (!fs.existsSync(dir)) {
|
|
76
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
fs.writeFileSync(STUDIO_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
|
|
79
|
+
return true;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error('Failed to save studio config:', err);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function loadPendingAction() {
|
|
87
|
+
if (pendingActionMemory) return pendingActionMemory;
|
|
88
|
+
|
|
89
|
+
if (fs.existsSync(PENDING_ACTION_PATH)) {
|
|
90
|
+
try {
|
|
91
|
+
const action = JSON.parse(fs.readFileSync(PENDING_ACTION_PATH, 'utf8'));
|
|
92
|
+
if (action.timestamp && Date.now() - action.timestamp < 60000) {
|
|
93
|
+
pendingActionMemory = action;
|
|
94
|
+
fs.unlinkSync(PENDING_ACTION_PATH);
|
|
95
|
+
return action;
|
|
96
|
+
}
|
|
97
|
+
fs.unlinkSync(PENDING_ACTION_PATH);
|
|
98
|
+
} catch {
|
|
99
|
+
try { fs.unlinkSync(PENDING_ACTION_PATH); } catch {}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
app.get('/api/pending-action', (req, res) => {
|
|
106
|
+
const action = loadPendingAction();
|
|
107
|
+
res.json({ action });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
app.delete('/api/pending-action', (req, res) => {
|
|
111
|
+
pendingActionMemory = null;
|
|
112
|
+
if (fs.existsSync(PENDING_ACTION_PATH)) {
|
|
113
|
+
try { fs.unlinkSync(PENDING_ACTION_PATH); } catch {}
|
|
114
|
+
}
|
|
115
|
+
res.json({ success: true });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const getPaths = () => {
|
|
119
|
+
const platform = process.platform;
|
|
120
|
+
const home = os.homedir();
|
|
121
|
+
|
|
122
|
+
let candidates = [];
|
|
123
|
+
if (platform === 'win32') {
|
|
124
|
+
candidates = [
|
|
125
|
+
path.join(process.env.APPDATA, 'opencode', 'opencode.json'),
|
|
126
|
+
path.join(home, '.config', 'opencode', 'opencode.json'),
|
|
127
|
+
path.join(home, '.local', 'share', 'opencode', 'opencode.json'),
|
|
128
|
+
];
|
|
129
|
+
} else {
|
|
130
|
+
candidates = [
|
|
131
|
+
path.join(home, '.config', 'opencode', 'opencode.json'),
|
|
132
|
+
path.join(home, '.opencode', 'opencode.json'),
|
|
133
|
+
path.join(home, '.local', 'share', 'opencode', 'opencode.json'),
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const studioConfig = loadStudioConfig();
|
|
138
|
+
const manualPath = studioConfig.configPath;
|
|
139
|
+
|
|
140
|
+
let detected = null;
|
|
141
|
+
for (const p of candidates) {
|
|
142
|
+
if (fs.existsSync(p)) {
|
|
143
|
+
detected = p;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
detected,
|
|
150
|
+
manual: manualPath,
|
|
151
|
+
current: manualPath || detected,
|
|
152
|
+
candidates
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const getConfigPath = () => {
|
|
157
|
+
const paths = getPaths();
|
|
158
|
+
return paths.current;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const loadConfig = () => {
|
|
162
|
+
const configPath = getConfigPath();
|
|
163
|
+
if (!configPath || !fs.existsSync(configPath)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const saveConfig = (config) => {
|
|
174
|
+
const configPath = getConfigPath();
|
|
175
|
+
if (!configPath) {
|
|
176
|
+
throw new Error('No config path found');
|
|
177
|
+
}
|
|
178
|
+
const dir = path.dirname(configPath);
|
|
179
|
+
if (!fs.existsSync(dir)) {
|
|
180
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
181
|
+
}
|
|
182
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
app.get('/api/health', (req, res) => {
|
|
186
|
+
res.json({ status: 'ok' });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
app.post('/api/shutdown', (req, res) => {
|
|
190
|
+
res.json({ success: true });
|
|
191
|
+
setTimeout(() => process.exit(0), 100);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
app.get('/api/paths', (req, res) => {
|
|
195
|
+
res.json(getPaths());
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
app.post('/api/paths', (req, res) => {
|
|
199
|
+
const { configPath } = req.body;
|
|
200
|
+
const studioConfig = loadStudioConfig();
|
|
201
|
+
studioConfig.configPath = configPath;
|
|
202
|
+
saveStudioConfig(studioConfig);
|
|
203
|
+
res.json({ success: true, current: getConfigPath() });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
app.get('/api/config', (req, res) => {
|
|
207
|
+
const config = loadConfig();
|
|
208
|
+
if (!config) {
|
|
209
|
+
return res.status(404).json({ error: 'Config not found' });
|
|
210
|
+
}
|
|
211
|
+
res.json(config);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
app.post('/api/config', (req, res) => {
|
|
215
|
+
const config = req.body;
|
|
216
|
+
try {
|
|
217
|
+
saveConfig(config);
|
|
218
|
+
res.json({ success: true });
|
|
219
|
+
} catch (err) {
|
|
220
|
+
res.status(500).json({ error: err.message });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const getSkillDir = () => {
|
|
225
|
+
const configPath = getConfigPath();
|
|
226
|
+
if (!configPath) return null;
|
|
227
|
+
return path.join(path.dirname(configPath), 'skill');
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
app.get('/api/skills', (req, res) => {
|
|
231
|
+
const skillDir = getSkillDir();
|
|
232
|
+
if (!skillDir || !fs.existsSync(skillDir)) {
|
|
233
|
+
return res.json([]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const skills = [];
|
|
237
|
+
const entries = fs.readdirSync(skillDir, { withFileTypes: true });
|
|
238
|
+
|
|
239
|
+
for (const entry of entries) {
|
|
240
|
+
if (entry.isDirectory()) {
|
|
241
|
+
const skillPath = path.join(skillDir, entry.name, 'SKILL.md');
|
|
242
|
+
if (fs.existsSync(skillPath)) {
|
|
243
|
+
skills.push({
|
|
244
|
+
name: entry.name,
|
|
245
|
+
path: skillPath,
|
|
246
|
+
enabled: !entry.name.endsWith('.disabled')
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
res.json(skills);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
app.get('/api/skills/:name', (req, res) => {
|
|
255
|
+
const skillDir = getSkillDir();
|
|
256
|
+
if (!skillDir) return res.status(404).json({ error: 'No config' });
|
|
257
|
+
|
|
258
|
+
const name = req.params.name;
|
|
259
|
+
const skillPath = path.join(skillDir, name, 'SKILL.md');
|
|
260
|
+
|
|
261
|
+
if (!fs.existsSync(skillPath)) {
|
|
262
|
+
return res.status(404).json({ error: 'Skill not found' });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
266
|
+
res.json({ name, content });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
app.post('/api/skills/:name', (req, res) => {
|
|
270
|
+
const skillDir = getSkillDir();
|
|
271
|
+
if (!skillDir) return res.status(404).json({ error: 'No config' });
|
|
272
|
+
|
|
273
|
+
const name = req.params.name;
|
|
274
|
+
const { content } = req.body;
|
|
275
|
+
const dirPath = path.join(skillDir, name);
|
|
276
|
+
|
|
277
|
+
if (!fs.existsSync(dirPath)) {
|
|
278
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fs.writeFileSync(path.join(dirPath, 'SKILL.md'), content, 'utf8');
|
|
282
|
+
res.json({ success: true });
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
app.delete('/api/skills/:name', (req, res) => {
|
|
286
|
+
const skillDir = getSkillDir();
|
|
287
|
+
if (!skillDir) return res.status(404).json({ error: 'No config' });
|
|
288
|
+
|
|
289
|
+
const name = req.params.name;
|
|
290
|
+
const dirPath = path.join(skillDir, name);
|
|
291
|
+
|
|
292
|
+
if (fs.existsSync(dirPath)) {
|
|
293
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
294
|
+
}
|
|
295
|
+
res.json({ success: true });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
app.post('/api/skills/:name/toggle', (req, res) => {
|
|
299
|
+
res.json({ success: true, enabled: true });
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const getPluginDir = () => {
|
|
303
|
+
const configPath = getConfigPath();
|
|
304
|
+
if (!configPath) return null;
|
|
305
|
+
return path.join(path.dirname(configPath), 'plugin');
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
app.get('/api/plugins', (req, res) => {
|
|
309
|
+
const pluginDir = getPluginDir();
|
|
310
|
+
const configPath = getConfigPath();
|
|
311
|
+
const configRoot = configPath ? path.dirname(configPath) : null;
|
|
312
|
+
|
|
313
|
+
const plugins = [];
|
|
314
|
+
|
|
315
|
+
const addPlugin = (name, p, enabled = true) => {
|
|
316
|
+
if (!plugins.some(pl => pl.name === name)) {
|
|
317
|
+
plugins.push({ name, path: p, enabled });
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
if (pluginDir && fs.existsSync(pluginDir)) {
|
|
322
|
+
const entries = fs.readdirSync(pluginDir, { withFileTypes: true });
|
|
323
|
+
for (const entry of entries) {
|
|
324
|
+
const fullPath = path.join(pluginDir, entry.name);
|
|
325
|
+
const stats = fs.lstatSync(fullPath);
|
|
326
|
+
if (stats.isDirectory()) {
|
|
327
|
+
const jsPath = path.join(fullPath, 'index.js');
|
|
328
|
+
const tsPath = path.join(fullPath, 'index.ts');
|
|
329
|
+
if (fs.existsSync(jsPath) || fs.existsSync(tsPath)) {
|
|
330
|
+
addPlugin(entry.name, fs.existsSync(jsPath) ? jsPath : tsPath);
|
|
331
|
+
}
|
|
332
|
+
} else if ((stats.isFile() || stats.isSymbolicLink()) && (entry.name.endsWith('.js') || entry.name.endsWith('.ts'))) {
|
|
333
|
+
addPlugin(entry.name.replace(/\.(js|ts)$/, ''), fullPath);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (configRoot && fs.existsSync(configRoot)) {
|
|
339
|
+
const rootEntries = fs.readdirSync(configRoot, { withFileTypes: true });
|
|
340
|
+
const knownPlugins = ['oh-my-opencode', 'superpowers', 'opencode-gemini-auth'];
|
|
341
|
+
|
|
342
|
+
for (const entry of rootEntries) {
|
|
343
|
+
if (knownPlugins.includes(entry.name) && entry.isDirectory()) {
|
|
344
|
+
const fullPath = path.join(configRoot, entry.name);
|
|
345
|
+
addPlugin(entry.name, fullPath);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
res.json(plugins);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
app.get('/api/plugins/:name', (req, res) => {
|
|
354
|
+
const pluginDir = getPluginDir();
|
|
355
|
+
if (!pluginDir) return res.status(404).json({ error: 'No config' });
|
|
356
|
+
|
|
357
|
+
const name = req.params.name;
|
|
358
|
+
const dirPath = path.join(pluginDir, name);
|
|
359
|
+
|
|
360
|
+
let content = '';
|
|
361
|
+
let filename = '';
|
|
362
|
+
|
|
363
|
+
if (fs.existsSync(path.join(dirPath, 'index.js'))) {
|
|
364
|
+
content = fs.readFileSync(path.join(dirPath, 'index.js'), 'utf8');
|
|
365
|
+
filename = 'index.js';
|
|
366
|
+
} else if (fs.existsSync(path.join(dirPath, 'index.ts'))) {
|
|
367
|
+
content = fs.readFileSync(path.join(dirPath, 'index.ts'), 'utf8');
|
|
368
|
+
filename = 'index.ts';
|
|
369
|
+
} else if (fs.existsSync(path.join(pluginDir, name + '.js'))) {
|
|
370
|
+
content = fs.readFileSync(path.join(pluginDir, name + '.js'), 'utf8');
|
|
371
|
+
filename = name + '.js';
|
|
372
|
+
} else if (fs.existsSync(path.join(pluginDir, name + '.ts'))) {
|
|
373
|
+
content = fs.readFileSync(path.join(pluginDir, name + '.ts'), 'utf8');
|
|
374
|
+
filename = name + '.ts';
|
|
375
|
+
} else {
|
|
376
|
+
return res.status(404).json({ error: 'Plugin not found' });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
res.json({ name, content, filename });
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
app.post('/api/plugins/:name', (req, res) => {
|
|
383
|
+
const pluginDir = getPluginDir();
|
|
384
|
+
if (!pluginDir) return res.status(404).json({ error: 'No config' });
|
|
385
|
+
|
|
386
|
+
const name = req.params.name;
|
|
387
|
+
const { content } = req.body;
|
|
388
|
+
const dirPath = path.join(pluginDir, name);
|
|
389
|
+
|
|
390
|
+
if (!fs.existsSync(dirPath)) {
|
|
391
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const filePath = path.join(dirPath, 'index.js');
|
|
395
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
396
|
+
res.json({ success: true });
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
app.delete('/api/plugins/:name', (req, res) => {
|
|
400
|
+
const pluginDir = getPluginDir();
|
|
401
|
+
if (!pluginDir) return res.status(404).json({ error: 'No config' });
|
|
402
|
+
|
|
403
|
+
const name = req.params.name;
|
|
404
|
+
const dirPath = path.join(pluginDir, name);
|
|
405
|
+
|
|
406
|
+
if (fs.existsSync(dirPath)) {
|
|
407
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
408
|
+
} else if (fs.existsSync(path.join(pluginDir, name + '.js'))) {
|
|
409
|
+
fs.unlinkSync(path.join(pluginDir, name + '.js'));
|
|
410
|
+
} else if (fs.existsSync(path.join(pluginDir, name + '.ts'))) {
|
|
411
|
+
fs.unlinkSync(path.join(pluginDir, name + '.ts'));
|
|
412
|
+
}
|
|
413
|
+
res.json({ success: true });
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
app.post('/api/plugins/:name/toggle', (req, res) => {
|
|
417
|
+
res.json({ success: true, enabled: true });
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
app.post('/api/fetch-url', async (req, res) => {
|
|
421
|
+
const { url } = req.body;
|
|
422
|
+
if (!url) return res.status(400).json({ error: 'URL required' });
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const fetch = (await import('node-fetch')).default;
|
|
426
|
+
const response = await fetch(url);
|
|
427
|
+
if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
428
|
+
const content = await response.text();
|
|
429
|
+
const filename = path.basename(new URL(url).pathname) || 'file.txt';
|
|
430
|
+
|
|
431
|
+
res.json({ content, filename, url });
|
|
432
|
+
} catch (err) {
|
|
433
|
+
res.status(500).json({ error: err.message });
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
app.post('/api/bulk-fetch', async (req, res) => {
|
|
438
|
+
const { urls } = req.body;
|
|
439
|
+
if (!Array.isArray(urls)) return res.status(400).json({ error: 'URLs array required' });
|
|
440
|
+
|
|
441
|
+
const fetch = (await import('node-fetch')).default;
|
|
442
|
+
const results = [];
|
|
443
|
+
|
|
444
|
+
for (const url of urls) {
|
|
445
|
+
try {
|
|
446
|
+
const response = await fetch(url);
|
|
447
|
+
if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
448
|
+
const content = await response.text();
|
|
449
|
+
const filename = path.basename(new URL(url).pathname) || 'file.txt';
|
|
450
|
+
|
|
451
|
+
results.push({
|
|
452
|
+
url,
|
|
453
|
+
success: true,
|
|
454
|
+
content,
|
|
455
|
+
filename,
|
|
456
|
+
name: filename.replace(/\.(md|js|ts)$/, ''),
|
|
457
|
+
});
|
|
458
|
+
} catch (err) {
|
|
459
|
+
results.push({
|
|
460
|
+
url,
|
|
461
|
+
success: false,
|
|
462
|
+
error: err.message
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
res.json({ results });
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
app.get('/api/backup', (req, res) => {
|
|
471
|
+
const studioConfig = loadStudioConfig();
|
|
472
|
+
const opencodeConfig = loadConfig();
|
|
473
|
+
|
|
474
|
+
const skills = [];
|
|
475
|
+
const skillDir = getSkillDir();
|
|
476
|
+
if (skillDir && fs.existsSync(skillDir)) {
|
|
477
|
+
const entries = fs.readdirSync(skillDir, { withFileTypes: true });
|
|
478
|
+
for (const entry of entries) {
|
|
479
|
+
if (entry.isDirectory()) {
|
|
480
|
+
const p = path.join(skillDir, entry.name, 'SKILL.md');
|
|
481
|
+
if (fs.existsSync(p)) {
|
|
482
|
+
skills.push({ name: entry.name, content: fs.readFileSync(p, 'utf8') });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const plugins = [];
|
|
489
|
+
const pluginDir = getPluginDir();
|
|
490
|
+
if (pluginDir && fs.existsSync(pluginDir)) {
|
|
491
|
+
const entries = fs.readdirSync(pluginDir, { withFileTypes: true });
|
|
492
|
+
for (const entry of entries) {
|
|
493
|
+
if (entry.isDirectory()) {
|
|
494
|
+
const p = path.join(pluginDir, entry.name, 'index.js');
|
|
495
|
+
if (fs.existsSync(p)) {
|
|
496
|
+
plugins.push({ name: entry.name, content: fs.readFileSync(p, 'utf8') });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
res.json({
|
|
503
|
+
version: 1,
|
|
504
|
+
timestamp: new Date().toISOString(),
|
|
505
|
+
studioConfig,
|
|
506
|
+
opencodeConfig,
|
|
507
|
+
skills,
|
|
508
|
+
plugins
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
app.post('/api/restore', (req, res) => {
|
|
513
|
+
const backup = req.body;
|
|
514
|
+
|
|
515
|
+
if (backup.studioConfig) {
|
|
516
|
+
saveStudioConfig(backup.studioConfig);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (backup.opencodeConfig) {
|
|
520
|
+
saveConfig(backup.opencodeConfig);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (Array.isArray(backup.skills)) {
|
|
524
|
+
const skillDir = getSkillDir();
|
|
525
|
+
if (skillDir) {
|
|
526
|
+
if (!fs.existsSync(skillDir)) fs.mkdirSync(skillDir, { recursive: true });
|
|
527
|
+
for (const skill of backup.skills) {
|
|
528
|
+
const dir = path.join(skillDir, skill.name);
|
|
529
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
530
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), skill.content, 'utf8');
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (Array.isArray(backup.plugins)) {
|
|
536
|
+
const pluginDir = getPluginDir();
|
|
537
|
+
if (pluginDir) {
|
|
538
|
+
if (!fs.existsSync(pluginDir)) fs.mkdirSync(pluginDir, { recursive: true });
|
|
539
|
+
for (const plugin of backup.plugins) {
|
|
540
|
+
const dir = path.join(pluginDir, plugin.name);
|
|
541
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
542
|
+
fs.writeFileSync(path.join(dir, 'index.js'), plugin.content, 'utf8');
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
res.json({ success: true });
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
function loadAuthConfig() {
|
|
551
|
+
const configPath = getConfigPath();
|
|
552
|
+
if (!configPath) return null;
|
|
553
|
+
|
|
554
|
+
const authPath = path.join(path.dirname(configPath), 'auth.json');
|
|
555
|
+
if (!fs.existsSync(authPath)) return null;
|
|
556
|
+
try {
|
|
557
|
+
return JSON.parse(fs.readFileSync(authPath, 'utf8'));
|
|
558
|
+
} catch {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
app.get('/api/auth', (req, res) => {
|
|
564
|
+
const authConfig = loadAuthConfig() || {};
|
|
565
|
+
const activeProfiles = getActiveProfiles();
|
|
566
|
+
|
|
567
|
+
const credentials = [];
|
|
568
|
+
const providers = [
|
|
569
|
+
{ id: 'google', name: 'Google AI', type: 'oauth' },
|
|
570
|
+
{ id: 'anthropic', name: 'Anthropic', type: 'api' },
|
|
571
|
+
{ id: 'openai', name: 'OpenAI', type: 'api' },
|
|
572
|
+
{ id: 'xai', name: 'xAI', type: 'api' },
|
|
573
|
+
{ id: 'groq', name: 'Groq', type: 'api' },
|
|
574
|
+
{ id: 'together', name: 'Together AI', type: 'api' },
|
|
575
|
+
{ id: 'mistral', name: 'Mistral', type: 'api' },
|
|
576
|
+
{ id: 'deepseek', name: 'DeepSeek', type: 'api' },
|
|
577
|
+
{ id: 'openrouter', name: 'OpenRouter', type: 'api' },
|
|
578
|
+
{ id: 'amazon-bedrock', name: 'Amazon Bedrock', type: 'api' },
|
|
579
|
+
{ id: 'azure', name: 'Azure OpenAI', type: 'api' },
|
|
580
|
+
{ id: 'github-copilot', name: 'GitHub Copilot', type: 'api' }
|
|
581
|
+
];
|
|
582
|
+
|
|
583
|
+
providers.forEach(p => {
|
|
584
|
+
const profileInfo = listAuthProfiles(p.id);
|
|
585
|
+
const hasCurrent = !!authConfig[p.id];
|
|
586
|
+
const hasProfiles = profileInfo.length > 0;
|
|
587
|
+
|
|
588
|
+
if (hasCurrent || hasProfiles) {
|
|
589
|
+
credentials.push({
|
|
590
|
+
...p,
|
|
591
|
+
active: activeProfiles[p.id] || (hasCurrent ? 'current' : null)
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
res.json({
|
|
597
|
+
...authConfig,
|
|
598
|
+
credentials,
|
|
599
|
+
authFile: path.join(path.dirname(getConfigPath() || ''), 'auth.json'),
|
|
600
|
+
hasGeminiAuthPlugin: true
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
app.post('/api/auth/login', (req, res) => {
|
|
605
|
+
const { provider } = req.body;
|
|
606
|
+
const cmd = process.platform === 'win32' ? 'opencode.cmd' : 'opencode';
|
|
607
|
+
|
|
608
|
+
const child = spawn(cmd, ['auth', 'login', provider], {
|
|
609
|
+
stdio: 'inherit',
|
|
610
|
+
shell: true
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
res.json({ success: true, message: 'Launched auth flow', note: 'Please check the terminal window where the server is running' });
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
app.delete('/api/auth/:provider', (req, res) => {
|
|
617
|
+
const { provider } = req.params;
|
|
618
|
+
const cmd = process.platform === 'win32' ? 'opencode.cmd' : 'opencode';
|
|
619
|
+
|
|
620
|
+
spawn(cmd, ['auth', 'logout', provider], {
|
|
621
|
+
stdio: 'inherit',
|
|
622
|
+
shell: true
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
res.json({ success: true });
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
app.get('/api/auth/providers', (req, res) => {
|
|
629
|
+
const providers = [
|
|
630
|
+
{ id: 'google', name: 'Google AI', type: 'oauth', description: 'Use Google Gemini models' },
|
|
631
|
+
{ id: 'anthropic', name: 'Anthropic', type: 'api', description: 'Use Claude models' },
|
|
632
|
+
{ id: 'openai', name: 'OpenAI', type: 'api', description: 'Use GPT models' },
|
|
633
|
+
{ id: 'xai', name: 'xAI', type: 'api', description: 'Use Grok models' },
|
|
634
|
+
{ id: 'groq', name: 'Groq', type: 'api', description: 'Fast inference' },
|
|
635
|
+
{ id: 'together', name: 'Together AI', type: 'api', description: 'Open source models' },
|
|
636
|
+
{ id: 'mistral', name: 'Mistral', type: 'api', description: 'Mistral models' },
|
|
637
|
+
{ id: 'deepseek', name: 'DeepSeek', type: 'api', description: 'DeepSeek models' },
|
|
638
|
+
{ id: 'openrouter', name: 'OpenRouter', type: 'api', description: 'Multiple providers' },
|
|
639
|
+
{ id: 'amazon-bedrock', name: 'Amazon Bedrock', type: 'api', description: 'AWS models' },
|
|
640
|
+
{ id: 'azure', name: 'Azure OpenAI', type: 'api', description: 'Azure GPT models' },
|
|
641
|
+
];
|
|
642
|
+
res.json(providers);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const AUTH_PROFILES_DIR = path.join(HOME_DIR, '.config', 'opencode-studio', 'auth-profiles');
|
|
646
|
+
|
|
647
|
+
function ensureAuthProfilesDir() {
|
|
648
|
+
if (!fs.existsSync(AUTH_PROFILES_DIR)) {
|
|
649
|
+
fs.mkdirSync(AUTH_PROFILES_DIR, { recursive: true });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function getProviderProfilesDir(provider) {
|
|
654
|
+
return path.join(AUTH_PROFILES_DIR, provider);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function listAuthProfiles(provider) {
|
|
658
|
+
const dir = getProviderProfilesDir(provider);
|
|
659
|
+
if (!fs.existsSync(dir)) return [];
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
return fs.readdirSync(dir)
|
|
663
|
+
.filter(f => f.endsWith('.json'))
|
|
664
|
+
.map(f => f.replace('.json', ''));
|
|
665
|
+
} catch {
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function getNextProfileName(provider) {
|
|
671
|
+
const existing = listAuthProfiles(provider);
|
|
672
|
+
let num = 1;
|
|
673
|
+
while (existing.includes(`account-${num}`)) {
|
|
674
|
+
num++;
|
|
675
|
+
}
|
|
676
|
+
return `account-${num}`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function saveAuthProfile(provider, profileName, data) {
|
|
680
|
+
ensureAuthProfilesDir();
|
|
681
|
+
const dir = getProviderProfilesDir(provider);
|
|
682
|
+
if (!fs.existsSync(dir)) {
|
|
683
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
684
|
+
}
|
|
685
|
+
const filePath = path.join(dir, `${profileName}.json`);
|
|
686
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function loadAuthProfile(provider, profileName) {
|
|
691
|
+
const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
|
|
692
|
+
if (!fs.existsSync(filePath)) return null;
|
|
693
|
+
try {
|
|
694
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
695
|
+
} catch {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function deleteAuthProfile(provider, profileName) {
|
|
701
|
+
const filePath = path.join(getProviderProfilesDir(provider), `${profileName}.json`);
|
|
702
|
+
if (fs.existsSync(filePath)) {
|
|
703
|
+
fs.unlinkSync(filePath);
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function getActiveProfiles() {
|
|
710
|
+
const studioConfig = loadStudioConfig();
|
|
711
|
+
return studioConfig.activeProfiles || {};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function setActiveProfile(provider, profileName) {
|
|
715
|
+
const studioConfig = loadStudioConfig();
|
|
716
|
+
studioConfig.activeProfiles = studioConfig.activeProfiles || {};
|
|
717
|
+
studioConfig.activeProfiles[provider] = profileName;
|
|
718
|
+
saveStudioConfig(studioConfig);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function verifyActiveProfile(p, n, c) { console.log("Verifying:", p, n); if (!n || !c) return false; const d = loadAuthProfile(p, n); if (d && c) { console.log("Profile refresh:", d.refresh); console.log("Current refresh:", c.refresh); } if (!d) return false; return JSON.stringify(d) === JSON.stringify(c); }
|
|
722
|
+
|
|
723
|
+
app.get('/api/auth/profiles', (req, res) => {
|
|
724
|
+
ensureAuthProfilesDir();
|
|
725
|
+
const activeProfiles = getActiveProfiles();
|
|
726
|
+
const authConfig = loadAuthConfig() || {};
|
|
727
|
+
|
|
728
|
+
const profiles = {};
|
|
729
|
+
|
|
730
|
+
const savedProviders = fs.existsSync(AUTH_PROFILES_DIR) ? fs.readdirSync(AUTH_PROFILES_DIR) : [];
|
|
731
|
+
|
|
732
|
+
const standardProviders = [
|
|
733
|
+
'google', 'anthropic', 'openai', 'xai', 'groq',
|
|
734
|
+
'together', 'mistral', 'deepseek', 'openrouter',
|
|
735
|
+
'amazon-bedrock', 'azure', 'github-copilot'
|
|
736
|
+
];
|
|
737
|
+
|
|
738
|
+
const allProviders = [...new Set([...savedProviders, ...standardProviders])];
|
|
739
|
+
|
|
740
|
+
allProviders.forEach(p => {
|
|
741
|
+
const saved = listAuthProfiles(p);
|
|
742
|
+
const active = activeProfiles[p];
|
|
743
|
+
const current = authConfig[p];
|
|
744
|
+
|
|
745
|
+
if (saved.length > 0 || current) {
|
|
746
|
+
profiles[p] = {
|
|
747
|
+
active: active,
|
|
748
|
+
profiles: saved,
|
|
749
|
+
saved: saved,
|
|
750
|
+
hasCurrent: !!current,
|
|
751
|
+
hasCurrentAuth: !!current
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
res.json(profiles);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
app.get('/api/debug/paths', (req, res) => {
|
|
760
|
+
const home = os.homedir();
|
|
761
|
+
const candidatePaths = [
|
|
762
|
+
process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'opencode', 'storage', 'message') : null,
|
|
763
|
+
path.join(home, '.local', 'share', 'opencode', 'storage', 'message'),
|
|
764
|
+
path.join(home, '.opencode', 'storage', 'message')
|
|
765
|
+
].filter(p => p);
|
|
766
|
+
|
|
767
|
+
const results = candidatePaths.map(p => ({
|
|
768
|
+
path: p,
|
|
769
|
+
exists: fs.existsSync(p),
|
|
770
|
+
isDirectory: fs.existsSync(p) && fs.statSync(p).isDirectory(),
|
|
771
|
+
fileCount: (fs.existsSync(p) && fs.statSync(p).isDirectory()) ? fs.readdirSync(p).length : 0
|
|
772
|
+
}));
|
|
773
|
+
|
|
774
|
+
res.json({
|
|
775
|
+
home,
|
|
776
|
+
platform: process.platform,
|
|
777
|
+
candidates: results
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
app.get('/api/usage', async (req, res) => {
|
|
782
|
+
try {
|
|
783
|
+
const { projectId: filterProjectId, granularity = 'daily', range = '30d' } = req.query;
|
|
784
|
+
const home = os.homedir();
|
|
785
|
+
const candidatePaths = [
|
|
786
|
+
process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'opencode', 'storage', 'message') : null,
|
|
787
|
+
path.join(home, '.local', 'share', 'opencode', 'storage', 'message'),
|
|
788
|
+
path.join(home, '.opencode', 'storage', 'message')
|
|
789
|
+
].filter(p => p && fs.existsSync(p));
|
|
790
|
+
|
|
791
|
+
const getSessionDir = (messageDir) => {
|
|
792
|
+
return path.join(path.dirname(messageDir), 'session');
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
const sessionProjectMap = new Map();
|
|
796
|
+
|
|
797
|
+
const loadProjects = (messageDir) => {
|
|
798
|
+
const sessionDir = getSessionDir(messageDir);
|
|
799
|
+
if (!fs.existsSync(sessionDir)) return;
|
|
800
|
+
|
|
801
|
+
try {
|
|
802
|
+
const projectDirs = fs.readdirSync(sessionDir);
|
|
803
|
+
|
|
804
|
+
for (const projDir of projectDirs) {
|
|
805
|
+
const fullProjPath = path.join(sessionDir, projDir);
|
|
806
|
+
if (!fs.statSync(fullProjPath).isDirectory()) continue;
|
|
807
|
+
|
|
808
|
+
const files = fs.readdirSync(fullProjPath);
|
|
809
|
+
for (const file of files) {
|
|
810
|
+
if (file.startsWith('ses_') && file.endsWith('.json')) {
|
|
811
|
+
const sessionId = file.replace('.json', '');
|
|
812
|
+
try {
|
|
813
|
+
const meta = JSON.parse(fs.readFileSync(path.join(fullProjPath, file), 'utf8'));
|
|
814
|
+
let projectName = 'Unknown Project';
|
|
815
|
+
if (meta.directory) {
|
|
816
|
+
projectName = path.basename(meta.directory);
|
|
817
|
+
} else if (meta.projectID) {
|
|
818
|
+
projectName = meta.projectID.substring(0, 8);
|
|
819
|
+
}
|
|
820
|
+
sessionProjectMap.set(sessionId, { name: projectName, id: meta.projectID || projDir });
|
|
821
|
+
} catch (e) {}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
} catch (e) {
|
|
826
|
+
console.error('Error loading projects:', e);
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
for (const logDir of candidatePaths) {
|
|
831
|
+
loadProjects(logDir);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const stats = {
|
|
835
|
+
totalCost: 0,
|
|
836
|
+
totalTokens: 0,
|
|
837
|
+
byModel: {},
|
|
838
|
+
byTime: {},
|
|
839
|
+
byProject: {}
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const processedFiles = new Set();
|
|
843
|
+
const now = Date.now();
|
|
844
|
+
let minTimestamp = 0;
|
|
845
|
+
|
|
846
|
+
if (range === '24h') minTimestamp = now - 24 * 60 * 60 * 1000;
|
|
847
|
+
else if (range === '7d') minTimestamp = now - 7 * 24 * 60 * 60 * 1000;
|
|
848
|
+
else if (range === '30d') minTimestamp = now - 30 * 24 * 60 * 60 * 1000;
|
|
849
|
+
|
|
850
|
+
const processMessage = (filePath, sessionId) => {
|
|
851
|
+
if (processedFiles.has(filePath)) return;
|
|
852
|
+
processedFiles.add(filePath);
|
|
853
|
+
|
|
854
|
+
try {
|
|
855
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
856
|
+
const msg = JSON.parse(content);
|
|
857
|
+
|
|
858
|
+
const model = msg.modelID || (msg.model && (msg.model.modelID || msg.model.id)) || 'unknown';
|
|
859
|
+
const projectInfo = sessionProjectMap.get(sessionId) || { name: 'Unassigned', id: 'unknown' };
|
|
860
|
+
const projectId = projectInfo.id || 'unknown';
|
|
861
|
+
|
|
862
|
+
if (filterProjectId && filterProjectId !== 'all' && projectId !== filterProjectId) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (minTimestamp > 0 && msg.time.created < minTimestamp) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (msg.role === 'assistant' && msg.tokens) {
|
|
871
|
+
const cost = msg.cost || 0;
|
|
872
|
+
const inputTokens = msg.tokens.input || 0;
|
|
873
|
+
const outputTokens = msg.tokens.output || 0;
|
|
874
|
+
const tokens = inputTokens + outputTokens;
|
|
875
|
+
|
|
876
|
+
const timestamp = msg.time.created;
|
|
877
|
+
const dateObj = new Date(timestamp);
|
|
878
|
+
let timeKey;
|
|
879
|
+
|
|
880
|
+
if (granularity === 'hourly') {
|
|
881
|
+
timeKey = dateObj.toISOString().substring(0, 13) + ':00:00Z';
|
|
882
|
+
} else if (granularity === 'weekly') {
|
|
883
|
+
const day = dateObj.getDay();
|
|
884
|
+
const diff = dateObj.getDate() - day + (day === 0 ? -6 : 1);
|
|
885
|
+
const monday = new Date(dateObj.setDate(diff));
|
|
886
|
+
timeKey = monday.toISOString().split('T')[0];
|
|
887
|
+
} else if (granularity === 'monthly') {
|
|
888
|
+
timeKey = dateObj.toISOString().substring(0, 7) + '-01';
|
|
889
|
+
} else {
|
|
890
|
+
timeKey = dateObj.toISOString().split('T')[0];
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (tokens > 0) {
|
|
894
|
+
stats.totalCost += cost;
|
|
895
|
+
stats.totalTokens += tokens;
|
|
896
|
+
|
|
897
|
+
if (!stats.byModel[model]) {
|
|
898
|
+
stats.byModel[model] = { name: model, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
|
|
899
|
+
}
|
|
900
|
+
stats.byModel[model].cost += cost;
|
|
901
|
+
stats.byModel[model].tokens += tokens;
|
|
902
|
+
stats.byModel[model].inputTokens += inputTokens;
|
|
903
|
+
stats.byModel[model].outputTokens += outputTokens;
|
|
904
|
+
|
|
905
|
+
if (!stats.byTime[timeKey]) {
|
|
906
|
+
stats.byTime[timeKey] = { date: timeKey, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
|
|
907
|
+
}
|
|
908
|
+
stats.byTime[timeKey].cost += cost;
|
|
909
|
+
stats.byTime[timeKey].tokens += tokens;
|
|
910
|
+
stats.byTime[timeKey].inputTokens += inputTokens;
|
|
911
|
+
stats.byTime[timeKey].outputTokens += outputTokens;
|
|
912
|
+
|
|
913
|
+
const projectName = projectInfo.name;
|
|
914
|
+
if (!stats.byProject[projectId]) {
|
|
915
|
+
stats.byProject[projectId] = { id: projectId, name: projectName, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
|
|
916
|
+
}
|
|
917
|
+
stats.byProject[projectId].cost += cost;
|
|
918
|
+
stats.byProject[projectId].tokens += tokens;
|
|
919
|
+
stats.byProject[projectId].inputTokens += inputTokens;
|
|
920
|
+
stats.byProject[projectId].outputTokens += outputTokens;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
} catch (err) {
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
for (const logDir of candidatePaths) {
|
|
928
|
+
try {
|
|
929
|
+
const sessions = fs.readdirSync(logDir);
|
|
930
|
+
for (const session of sessions) {
|
|
931
|
+
if (!session.startsWith('ses_')) continue;
|
|
932
|
+
|
|
933
|
+
const sessionDir = path.join(logDir, session);
|
|
934
|
+
if (fs.statSync(sessionDir).isDirectory()) {
|
|
935
|
+
const messages = fs.readdirSync(sessionDir);
|
|
936
|
+
for (const msgFile of messages) {
|
|
937
|
+
if (msgFile.endsWith('.json')) {
|
|
938
|
+
processMessage(path.join(sessionDir, msgFile), session);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
} catch (err) {
|
|
944
|
+
console.error(`Error reading log dir ${logDir}:`, err);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const response = {
|
|
949
|
+
totalCost: stats.totalCost,
|
|
950
|
+
totalTokens: stats.totalTokens,
|
|
951
|
+
byModel: Object.values(stats.byModel).sort((a, b) => b.cost - a.cost),
|
|
952
|
+
byDay: Object.values(stats.byTime).sort((a, b) => a.date.localeCompare(b.date)),
|
|
953
|
+
byProject: Object.values(stats.byProject).sort((a, b) => b.cost - a.cost)
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
res.json(response);
|
|
957
|
+
} catch (error) {
|
|
958
|
+
console.error('Error fetching usage stats:', error);
|
|
959
|
+
res.status(500).json({ error: 'Failed to fetch usage stats' });
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
app.get('/api/auth/profiles/:provider', (req, res) => {
|
|
964
|
+
const { provider } = req.params;
|
|
965
|
+
const providerProfiles = listAuthProfiles(provider);
|
|
966
|
+
const activeProfiles = getActiveProfiles();
|
|
967
|
+
const authConfig = loadAuthConfig() || {};
|
|
968
|
+
|
|
969
|
+
res.json({
|
|
970
|
+
profiles: providerProfiles,
|
|
971
|
+
active: (activeProfiles[provider] && verifyActiveProfile(provider, activeProfiles[provider], authConfig[provider])) ? activeProfiles[provider] : null,
|
|
972
|
+
hasCurrentAuth: !!authConfig[provider],
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
app.post('/api/auth/profiles/:provider', (req, res) => {
|
|
977
|
+
const { provider } = req.params;
|
|
978
|
+
const { name } = req.body;
|
|
979
|
+
|
|
980
|
+
const authConfig = loadAuthConfig();
|
|
981
|
+
if (!authConfig || !authConfig[provider]) {
|
|
982
|
+
return res.status(400).json({ error: `No active auth for ${provider} to save` });
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const profileName = name || getNextProfileName(provider);
|
|
986
|
+
const data = authConfig[provider];
|
|
987
|
+
|
|
988
|
+
try {
|
|
989
|
+
saveAuthProfile(provider, profileName, data);
|
|
990
|
+
setActiveProfile(provider, profileName);
|
|
991
|
+
res.json({ success: true, name: profileName });
|
|
992
|
+
} catch (err) {
|
|
993
|
+
res.status(500).json({ error: 'Failed to save profile', details: err.message });
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
app.post('/api/auth/profiles/:provider/:name/activate', (req, res) => {
|
|
998
|
+
const { provider, name } = req.params;
|
|
999
|
+
|
|
1000
|
+
const profileData = loadAuthProfile(provider, name);
|
|
1001
|
+
if (!profileData) {
|
|
1002
|
+
return res.status(404).json({ error: 'Profile not found' });
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const authConfig = loadAuthConfig() || {};
|
|
1006
|
+
authConfig[provider] = profileData;
|
|
1007
|
+
|
|
1008
|
+
const configPath = getConfigPath();
|
|
1009
|
+
if (configPath) {
|
|
1010
|
+
const authPath = path.join(path.dirname(configPath), 'auth.json');
|
|
1011
|
+
try {
|
|
1012
|
+
fs.writeFileSync(authPath, JSON.stringify(authConfig, null, 2), 'utf8');
|
|
1013
|
+
setActiveProfile(provider, name);
|
|
1014
|
+
res.json({ success: true });
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
res.status(500).json({ error: 'Failed to write auth config', details: err.message });
|
|
1017
|
+
}
|
|
1018
|
+
} else {
|
|
1019
|
+
res.status(500).json({ error: 'Config path not found' });
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
app.delete('/api/auth/profiles/:provider/:name', (req, res) => {
|
|
1024
|
+
const { provider, name } = req.params;
|
|
1025
|
+
const success = deleteAuthProfile(provider, name);
|
|
1026
|
+
if (success) {
|
|
1027
|
+
const activeProfiles = getActiveProfiles();
|
|
1028
|
+
if (activeProfiles[provider] === name) {
|
|
1029
|
+
const studioConfig = loadStudioConfig();
|
|
1030
|
+
if (studioConfig.activeProfiles) {
|
|
1031
|
+
delete studioConfig.activeProfiles[provider];
|
|
1032
|
+
saveStudioConfig(studioConfig);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
res.json({ success: true });
|
|
1036
|
+
} else {
|
|
1037
|
+
res.status(404).json({ error: 'Profile not found' });
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
app.put('/api/auth/profiles/:provider/:name', (req, res) => {
|
|
1042
|
+
const { provider, name } = req.params;
|
|
1043
|
+
const { newName } = req.body;
|
|
1044
|
+
|
|
1045
|
+
if (!newName) return res.status(400).json({ error: 'New name required' });
|
|
1046
|
+
|
|
1047
|
+
const profileData = loadAuthProfile(provider, name);
|
|
1048
|
+
if (!profileData) return res.status(404).json({ error: 'Profile not found' });
|
|
1049
|
+
|
|
1050
|
+
const success = saveAuthProfile(provider, newName, profileData);
|
|
1051
|
+
if (success) {
|
|
1052
|
+
deleteAuthProfile(provider, name);
|
|
1053
|
+
|
|
1054
|
+
const activeProfiles = getActiveProfiles();
|
|
1055
|
+
if (activeProfiles[provider] === name) {
|
|
1056
|
+
setActiveProfile(provider, newName);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
res.json({ success: true, name: newName });
|
|
1060
|
+
} else {
|
|
1061
|
+
res.status(500).json({ error: 'Failed to rename profile' });
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
app.post('/api/plugins/config/add', (req, res) => {
|
|
1066
|
+
const { plugins } = req.body;
|
|
1067
|
+
if (!Array.isArray(plugins) || plugins.length === 0) {
|
|
1068
|
+
return res.status(400).json({ error: 'Plugins array required' });
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const config = loadConfig();
|
|
1072
|
+
if (!config) return res.status(404).json({ error: 'Config not found' });
|
|
1073
|
+
|
|
1074
|
+
if (!config.plugins) config.plugins = {};
|
|
1075
|
+
|
|
1076
|
+
const result = {
|
|
1077
|
+
added: [],
|
|
1078
|
+
skipped: []
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
for (const pluginName of plugins) {
|
|
1082
|
+
const pluginDir = getPluginDir();
|
|
1083
|
+
const dirPath = path.join(pluginDir, pluginName);
|
|
1084
|
+
const hasJs = fs.existsSync(path.join(dirPath, 'index.js'));
|
|
1085
|
+
const hasTs = fs.existsSync(path.join(dirPath, 'index.ts'));
|
|
1086
|
+
|
|
1087
|
+
if (!hasJs && !hasTs) {
|
|
1088
|
+
result.skipped.push(`${pluginName} (not found)`);
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (config.plugins[pluginName]) {
|
|
1093
|
+
result.skipped.push(`${pluginName} (already configured)`);
|
|
1094
|
+
} else {
|
|
1095
|
+
config.plugins[pluginName] = { enabled: true };
|
|
1096
|
+
result.added.push(pluginName);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (result.added.length > 0) {
|
|
1101
|
+
saveConfig(config);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
res.json(result);
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
app.delete('/api/plugins/config/:name', (req, res) => {
|
|
1108
|
+
const pluginName = decodeURIComponent(req.params.name);
|
|
1109
|
+
const config = loadConfig();
|
|
1110
|
+
|
|
1111
|
+
if (!config || !config.plugins) {
|
|
1112
|
+
return res.status(404).json({ error: 'Config not found or no plugins' });
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (config.plugins[pluginName]) {
|
|
1116
|
+
delete config.plugins[pluginName];
|
|
1117
|
+
saveConfig(config);
|
|
1118
|
+
res.json({ success: true });
|
|
1119
|
+
} else {
|
|
1120
|
+
res.status(404).json({ error: 'Plugin not in config' });
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
app.listen(PORT, () => {
|
|
1125
|
+
console.log(`Server running at http://localhost:${PORT}`);
|
|
1126
|
+
});
|