vg-coder-cli 2.0.45 → 2.0.47
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/INTEGRATION.md +418 -0
- package/debug.log +42 -0
- package/dist/vg-coder-bundle.js +50 -44
- package/package.json +2 -1
- package/src/server/api-server.js +355 -2
- package/src/server/task-queue.js +705 -0
- package/src/server/task-store.js +112 -0
- package/src/server/task-webhook.js +48 -0
- package/src/server/views/css/agent-panel.css +259 -3
- package/src/server/views/css/code-viewer.css +158 -3
- package/src/server/views/js/features/agent-panel.js +248 -12
- package/src/server/views/js/features/code-viewer.js +18 -3
- package/src/server/views/js/features/git-view.js +1 -1
- package/src/server/views/js/features/mermaid-viewer.js +494 -0
- package/src/server/views/js/features/resize.js +1 -1
- package/src/server/views/js/features/task-worker.js +448 -0
- package/src/server/views/js/main.js +4 -0
- package/src/server/views/vg-coder/background.js +17860 -11946
- package/src/server/views/vg-coder/controller.js +42 -10
- package/src/server/views/vg-coder/manifest.json +2 -1
- package/src/server/views/vg-coder/sidepanel.js +13 -7
- package/test-large/package.json +1 -0
- package/test-large/src/components/Button.tsx +1 -0
- package/test-large/src/index.ts +1 -0
- package/test-small/package.json +1 -0
- package/test-small/src/index.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vg-coder-cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.47",
|
|
4
4
|
"description": "🚀 CLI tool to analyze projects, concatenate source files, count tokens, and export HTML with syntax highlighting and copy functionality",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"ignore": "^5.3.0",
|
|
46
46
|
"markdown-it": "^14.0.0",
|
|
47
47
|
"mermaid": "^11.4.1",
|
|
48
|
+
"multer": "^1.4.5-lts.1",
|
|
48
49
|
"node-pty": "^1.1.0",
|
|
49
50
|
"ora": "^5.4.1",
|
|
50
51
|
"path": "^0.12.7",
|
package/src/server/api-server.js
CHANGED
|
@@ -17,6 +17,9 @@ const TokenManager = require('../tokenizer/token-manager');
|
|
|
17
17
|
const BashExecutor = require('../utils/bash-executor');
|
|
18
18
|
const terminalManager = require('./terminal-manager');
|
|
19
19
|
const projectManager = require('./project-manager');
|
|
20
|
+
const taskQueue = require('./task-queue');
|
|
21
|
+
const taskStore = require('./task-store');
|
|
22
|
+
const multer = require('multer');
|
|
20
23
|
|
|
21
24
|
class ApiServer {
|
|
22
25
|
constructor(port = 6868) {
|
|
@@ -32,6 +35,19 @@ class ApiServer {
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
setupMiddleware() {
|
|
38
|
+
// Private Network Access — Chrome 130+ requires this header for HTTPS
|
|
39
|
+
// origins (e.g. aistudio.google.com) calling localhost.
|
|
40
|
+
this.app.use((req, res, next) => {
|
|
41
|
+
res.setHeader('Access-Control-Allow-Private-Network', 'true');
|
|
42
|
+
if (req.method === 'OPTIONS') {
|
|
43
|
+
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
|
|
44
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
45
|
+
res.setHeader('Access-Control-Allow-Headers', req.headers['access-control-request-headers'] || 'Content-Type, Authorization');
|
|
46
|
+
res.setHeader('Access-Control-Max-Age', '600');
|
|
47
|
+
return res.sendStatus(204);
|
|
48
|
+
}
|
|
49
|
+
next();
|
|
50
|
+
});
|
|
35
51
|
this.app.use(cors());
|
|
36
52
|
this.app.use(bodyParser.json({ limit: '50mb' }));
|
|
37
53
|
this.app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }));
|
|
@@ -47,6 +63,7 @@ class ApiServer {
|
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
setupSocketIO() {
|
|
66
|
+
taskQueue.attachIO(this.io);
|
|
50
67
|
this.io.on('connection', (socket) => {
|
|
51
68
|
socket.on('terminal:init', (data) => {
|
|
52
69
|
if (!data || !data.termId) return;
|
|
@@ -65,7 +82,26 @@ class ApiServer {
|
|
|
65
82
|
socket.on('terminal:input', (data) => { if (data && data.termId) terminalManager.write(data.termId, data.data); });
|
|
66
83
|
socket.on('terminal:resize', (data) => { if (data && data.termId) terminalManager.resize(data.termId, data.cols, data.rows); });
|
|
67
84
|
socket.on('terminal:kill', (data) => { if (data && data.termId) terminalManager.kill(data.termId); });
|
|
68
|
-
|
|
85
|
+
|
|
86
|
+
// Task worker registration & lifecycle
|
|
87
|
+
socket.on('worker:register', (meta) => { taskQueue.setWorker(socket, meta || {}); });
|
|
88
|
+
socket.on('worker:heartbeat', () => { /* keep-alive only */ });
|
|
89
|
+
socket.on('task:complete', (payload) => {
|
|
90
|
+
if (payload?.taskId) taskQueue.onWorkerComplete(payload.taskId, payload, socket.id);
|
|
91
|
+
});
|
|
92
|
+
socket.on('task:failed', (payload) => {
|
|
93
|
+
if (payload?.taskId) taskQueue.onWorkerFailed(payload.taskId, { code: payload.code, message: payload.message }, socket.id);
|
|
94
|
+
});
|
|
95
|
+
socket.on('task:progress', () => { /* ignored for now */ });
|
|
96
|
+
|
|
97
|
+
// Profile launcher (extension background SW). One per Chrome profile.
|
|
98
|
+
socket.on('launcher:register', (meta) => { taskQueue.setLauncher(socket, meta || {}); });
|
|
99
|
+
|
|
100
|
+
socket.on('disconnect', () => {
|
|
101
|
+
terminalManager.cleanupSocket(socket.id);
|
|
102
|
+
taskQueue.clearWorker(socket);
|
|
103
|
+
taskQueue.clearLauncher(socket);
|
|
104
|
+
});
|
|
69
105
|
});
|
|
70
106
|
}
|
|
71
107
|
|
|
@@ -408,6 +444,306 @@ class ApiServer {
|
|
|
408
444
|
} catch (error) { res.status(500).json({ error: error.message }); }
|
|
409
445
|
});
|
|
410
446
|
|
|
447
|
+
// Chat History (per-project, stored in .vg/chats/<id>.json)
|
|
448
|
+
const chatsDir = (workingDir) => path.join(workingDir, '.vg', 'chats');
|
|
449
|
+
const sanitizeChatId = (id) => String(id || '').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 200);
|
|
450
|
+
|
|
451
|
+
this.app.get('/api/chats', async (req, res) => {
|
|
452
|
+
try {
|
|
453
|
+
const dir = chatsDir(req.workingDir);
|
|
454
|
+
if (!await fs.pathExists(dir)) return res.json({ chats: [] });
|
|
455
|
+
const files = (await fs.readdir(dir)).filter(f => f.endsWith('.json'));
|
|
456
|
+
const chats = [];
|
|
457
|
+
for (const f of files) {
|
|
458
|
+
try {
|
|
459
|
+
const data = await fs.readJson(path.join(dir, f));
|
|
460
|
+
chats.push({
|
|
461
|
+
id: data.id,
|
|
462
|
+
title: data.title || '(untitled)',
|
|
463
|
+
source: data.source || 'unknown',
|
|
464
|
+
createdAt: data.createdAt,
|
|
465
|
+
updatedAt: data.updatedAt,
|
|
466
|
+
count: Array.isArray(data.messages) ? data.messages.length : 0
|
|
467
|
+
});
|
|
468
|
+
} catch (_) { /* skip corrupt file */ }
|
|
469
|
+
}
|
|
470
|
+
chats.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
471
|
+
res.json({ chats });
|
|
472
|
+
} catch (error) { res.status(500).json({ error: error.message }); }
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
this.app.get('/api/chats/:id', async (req, res) => {
|
|
476
|
+
try {
|
|
477
|
+
const id = sanitizeChatId(req.params.id);
|
|
478
|
+
const file = path.join(chatsDir(req.workingDir), `${id}.json`);
|
|
479
|
+
if (!await fs.pathExists(file)) return res.status(404).json({ error: 'Chat not found' });
|
|
480
|
+
res.json(await fs.readJson(file));
|
|
481
|
+
} catch (error) { res.status(500).json({ error: error.message }); }
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
this.app.post('/api/chats/:id', async (req, res) => {
|
|
485
|
+
try {
|
|
486
|
+
const id = sanitizeChatId(req.params.id);
|
|
487
|
+
if (!id) return res.status(400).json({ error: 'Invalid chat id' });
|
|
488
|
+
const dir = chatsDir(req.workingDir);
|
|
489
|
+
await fs.ensureDir(dir);
|
|
490
|
+
const file = path.join(dir, `${id}.json`);
|
|
491
|
+
const now = Date.now();
|
|
492
|
+
const incoming = req.body || {};
|
|
493
|
+
const existing = await fs.pathExists(file) ? await fs.readJson(file) : null;
|
|
494
|
+
const data = {
|
|
495
|
+
id,
|
|
496
|
+
source: incoming.source || existing?.source || 'unknown',
|
|
497
|
+
title: incoming.title || existing?.title || '(untitled)',
|
|
498
|
+
createdAt: existing?.createdAt || now,
|
|
499
|
+
updatedAt: now,
|
|
500
|
+
messages: Array.isArray(incoming.messages) ? incoming.messages : (existing?.messages || [])
|
|
501
|
+
};
|
|
502
|
+
await fs.writeJson(file, data, { spaces: 2 });
|
|
503
|
+
res.json({ success: true, id, count: data.messages.length });
|
|
504
|
+
} catch (error) { res.status(500).json({ error: error.message }); }
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
this.app.delete('/api/chats/:id', async (req, res) => {
|
|
508
|
+
try {
|
|
509
|
+
const id = sanitizeChatId(req.params.id);
|
|
510
|
+
const file = path.join(chatsDir(req.workingDir), `${id}.json`);
|
|
511
|
+
if (await fs.pathExists(file)) await fs.remove(file);
|
|
512
|
+
res.json({ success: true });
|
|
513
|
+
} catch (error) { res.status(500).json({ error: error.message }); }
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// ---------------- Remote Task API ----------------
|
|
517
|
+
const taskUpload = multer({
|
|
518
|
+
storage: multer.diskStorage({
|
|
519
|
+
destination: (req, file, cb) => {
|
|
520
|
+
// Stage uploads in a per-request temp dir; we move into final task dir after id assigned
|
|
521
|
+
const tmpDir = path.join(req.workingDir, '.vg', 'tasks', '_uploads', String(Date.now()) + '_' + Math.random().toString(36).slice(2, 8));
|
|
522
|
+
fs.ensureDir(tmpDir).then(() => cb(null, tmpDir)).catch(cb);
|
|
523
|
+
req._taskTmpDir = req._taskTmpDir || tmpDir;
|
|
524
|
+
},
|
|
525
|
+
filename: (req, file, cb) => cb(null, `${Date.now()}__${file.originalname}`)
|
|
526
|
+
}),
|
|
527
|
+
limits: { fileSize: 50 * 1024 * 1024, files: 10 }
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
this.app.post('/api/tasks', taskUpload.array('files', 10), async (req, res) => {
|
|
531
|
+
try {
|
|
532
|
+
const prompt = (req.body?.prompt || '').toString();
|
|
533
|
+
if (!prompt && (!req.files || req.files.length === 0)) {
|
|
534
|
+
return res.status(400).json({ error: 'prompt or files required' });
|
|
535
|
+
}
|
|
536
|
+
let meta = null;
|
|
537
|
+
if (req.body?.meta) {
|
|
538
|
+
try { meta = JSON.parse(req.body.meta); } catch (_) { meta = req.body.meta; }
|
|
539
|
+
}
|
|
540
|
+
const webhookUrl = req.body?.webhookUrl ? String(req.body.webhookUrl) : null;
|
|
541
|
+
const workerLabel = req.body?.workerLabel
|
|
542
|
+
? String(req.body.workerLabel).toLowerCase().trim()
|
|
543
|
+
: null;
|
|
544
|
+
|
|
545
|
+
const id = taskStore.newTaskId();
|
|
546
|
+
const finalDir = taskStore.taskDir(req.workingDir, id);
|
|
547
|
+
const finalFiles = path.join(finalDir, 'files');
|
|
548
|
+
await fs.ensureDir(finalFiles);
|
|
549
|
+
|
|
550
|
+
const fileEntries = [];
|
|
551
|
+
for (let i = 0; i < (req.files || []).length; i++) {
|
|
552
|
+
const f = req.files[i];
|
|
553
|
+
const safeName = path.basename(f.originalname).replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120) || `file_${i}`;
|
|
554
|
+
const dest = path.join(finalFiles, `${i}__${safeName}`);
|
|
555
|
+
await fs.move(f.path, dest, { overwrite: true });
|
|
556
|
+
fileEntries.push({ idx: i, name: f.originalname, mime: f.mimetype, size: f.size, path: dest });
|
|
557
|
+
}
|
|
558
|
+
// Cleanup tmp dir
|
|
559
|
+
if (req._taskTmpDir) {
|
|
560
|
+
try { await fs.remove(req._taskTmpDir); } catch (_) {}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const task = {
|
|
564
|
+
id,
|
|
565
|
+
status: 'queued',
|
|
566
|
+
prompt,
|
|
567
|
+
meta,
|
|
568
|
+
files: fileEntries,
|
|
569
|
+
webhookUrl,
|
|
570
|
+
webhook: { attempts: [], deliveredAt: null },
|
|
571
|
+
worker: null,
|
|
572
|
+
workerLabel,
|
|
573
|
+
attempts: [],
|
|
574
|
+
result: null,
|
|
575
|
+
error: null,
|
|
576
|
+
timing: { createdAt: Date.now() },
|
|
577
|
+
cancelRequestedAt: null,
|
|
578
|
+
workingDir: req.workingDir
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const result = await taskQueue.enqueue(task);
|
|
582
|
+
res.status(202).json({
|
|
583
|
+
taskId: id,
|
|
584
|
+
status: result.status,
|
|
585
|
+
position: result.position,
|
|
586
|
+
pollUrl: `/api/tasks/${id}`,
|
|
587
|
+
createdAt: task.timing.createdAt
|
|
588
|
+
});
|
|
589
|
+
} catch (error) {
|
|
590
|
+
console.error('[Tasks] create failed:', error);
|
|
591
|
+
res.status(500).json({ error: error.message });
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
this.app.get('/api/tasks', async (req, res) => {
|
|
596
|
+
try {
|
|
597
|
+
const filter = {};
|
|
598
|
+
if (req.query.status) filter.status = String(req.query.status);
|
|
599
|
+
if (req.query.limit) filter.limit = Math.min(parseInt(req.query.limit, 10) || 50, 500);
|
|
600
|
+
res.json({ tasks: await taskQueue.list(req.workingDir, filter) });
|
|
601
|
+
} catch (error) { res.status(500).json({ error: error.message }); }
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
this.app.get('/api/tasks/:id', async (req, res) => {
|
|
605
|
+
try {
|
|
606
|
+
const id = taskStore.sanitizeTaskId(req.params.id);
|
|
607
|
+
const task = await taskQueue.get(id, req.workingDir);
|
|
608
|
+
if (!task) return res.status(404).json({ error: 'Task not found' });
|
|
609
|
+
const result = { ...task };
|
|
610
|
+
if (task.status === 'done') {
|
|
611
|
+
result.result = { ...(task.result || {}), markdown: await taskStore.readResult(task.workingDir, task.id) };
|
|
612
|
+
}
|
|
613
|
+
res.json(result);
|
|
614
|
+
} catch (error) { res.status(500).json({ error: error.message }); }
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
this.app.delete('/api/tasks/:id', async (req, res) => {
|
|
618
|
+
try {
|
|
619
|
+
const id = taskStore.sanitizeTaskId(req.params.id);
|
|
620
|
+
const r = await taskQueue.cancel(id, req.workingDir);
|
|
621
|
+
if (!r.ok) return res.status(r.code || 400).json({ error: 'cannot_cancel', status: r.status });
|
|
622
|
+
res.json({ canceled: true, status: r.status });
|
|
623
|
+
} catch (error) { res.status(500).json({ error: error.message }); }
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
this.app.get('/api/tasks/:id/files/:idx', async (req, res) => {
|
|
627
|
+
try {
|
|
628
|
+
const id = taskStore.sanitizeTaskId(req.params.id);
|
|
629
|
+
const idx = parseInt(req.params.idx, 10);
|
|
630
|
+
if (!Number.isInteger(idx) || idx < 0) return res.status(400).json({ error: 'invalid idx' });
|
|
631
|
+
const task = await taskQueue.get(id, req.workingDir);
|
|
632
|
+
if (!task) return res.status(404).json({ error: 'task not found' });
|
|
633
|
+
const file = (task.files || []).find(f => f.idx === idx);
|
|
634
|
+
if (!file || !file.path) return res.status(404).json({ error: 'file not found' });
|
|
635
|
+
if (!await fs.pathExists(file.path)) return res.status(404).json({ error: 'file missing on disk' });
|
|
636
|
+
res.setHeader('Content-Type', file.mime || 'application/octet-stream');
|
|
637
|
+
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(file.name)}"`);
|
|
638
|
+
fs.createReadStream(file.path).pipe(res);
|
|
639
|
+
} catch (error) { res.status(500).json({ error: error.message }); }
|
|
640
|
+
});
|
|
641
|
+
// ---------------- /Remote Task API ----------------
|
|
642
|
+
|
|
643
|
+
// ---------------- Remote Debug API ----------------
|
|
644
|
+
// Localhost-only debug surface for connected AI Studio workers.
|
|
645
|
+
// Pass `workerLabel` (email) in query/body to target a specific worker.
|
|
646
|
+
|
|
647
|
+
// Helper: extract worker target from query/body
|
|
648
|
+
const workerOpts = (req) => {
|
|
649
|
+
const label = (req.query?.label || req.query?.workerLabel || req.body?.workerLabel || '').toString().toLowerCase().trim();
|
|
650
|
+
return label ? { workerLabel: label } : {};
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
this.app.get('/api/workers', (req, res) => {
|
|
654
|
+
res.json({ workers: taskQueue.listWorkers() });
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
this.app.get('/api/launchers', (req, res) => {
|
|
658
|
+
res.json({ launchers: taskQueue.listLaunchers() });
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// List AI Studio tabs across all profiles (or 1 if workerLabel given).
|
|
662
|
+
this.app.get('/api/launcher/tabs', async (req, res) => {
|
|
663
|
+
try {
|
|
664
|
+
const label = (req.query?.label || req.query?.workerLabel || '').toString().trim();
|
|
665
|
+
const opts = label ? { workerLabel: label } : { all: true };
|
|
666
|
+
const result = await taskQueue.requestLauncher('launcher:list_tabs', {}, opts, 5_000);
|
|
667
|
+
res.json(result);
|
|
668
|
+
} catch (e) { res.status(503).json({ error: e.message }); }
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// Close tab(s). Body: { workerLabel?, tabId? } — omit tabId to close all.
|
|
672
|
+
this.app.post('/api/launcher/close-tab', async (req, res) => {
|
|
673
|
+
try {
|
|
674
|
+
const body = req.body || {};
|
|
675
|
+
const opts = body.workerLabel ? { workerLabel: body.workerLabel } : (body.all ? { all: true } : {});
|
|
676
|
+
const payload = body.tabId != null ? { tabId: body.tabId } : {};
|
|
677
|
+
const result = await taskQueue.requestLauncher('launcher:close_tab', payload, opts, 5_000);
|
|
678
|
+
res.json(result);
|
|
679
|
+
} catch (e) { res.status(503).json({ error: e.message }); }
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// Open new AI Studio tab. Body: { workerLabel?, model?, url?, active? }
|
|
683
|
+
this.app.post('/api/launcher/open-tab', async (req, res) => {
|
|
684
|
+
try {
|
|
685
|
+
const body = req.body || {};
|
|
686
|
+
const opts = body.workerLabel ? { workerLabel: body.workerLabel } : {};
|
|
687
|
+
const payload = { url: body.url, model: body.model, active: body.active };
|
|
688
|
+
const result = await taskQueue.requestLauncher('launcher:open_tab', payload, opts, 8_000);
|
|
689
|
+
res.json(result);
|
|
690
|
+
} catch (e) { res.status(503).json({ error: e.message }); }
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
this.app.get('/api/worker/status', (req, res) => {
|
|
694
|
+
const label = (req.query?.label || req.query?.workerLabel || '').toString().toLowerCase().trim();
|
|
695
|
+
res.json(taskQueue.workerStatus(label || undefined));
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
this.app.get('/api/worker/url', async (req, res) => {
|
|
699
|
+
try { res.json(await taskQueue.requestWorker('debug:url', {}, workerOpts(req))); }
|
|
700
|
+
catch (e) { res.status(503).json({ error: e.message }); }
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
this.app.post('/api/worker/probe', async (req, res) => {
|
|
704
|
+
try {
|
|
705
|
+
const { selector, kind } = req.body || {};
|
|
706
|
+
if (!selector) return res.status(400).json({ error: 'selector required' });
|
|
707
|
+
res.json(await taskQueue.requestWorker('debug:probe', { selector, kind: kind || 'text' }, workerOpts(req)));
|
|
708
|
+
} catch (e) { res.status(503).json({ error: e.message }); }
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
this.app.post('/api/worker/dom', async (req, res) => {
|
|
712
|
+
try {
|
|
713
|
+
const { selector, maxBytes } = req.body || {};
|
|
714
|
+
res.json(await taskQueue.requestWorker('debug:dom', { selector, maxBytes: maxBytes || 200_000 }, workerOpts(req)));
|
|
715
|
+
} catch (e) { res.status(503).json({ error: e.message }); }
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
this.app.post('/api/worker/eval', async (req, res) => {
|
|
719
|
+
try {
|
|
720
|
+
const { code, timeoutMs } = req.body || {};
|
|
721
|
+
if (!code) return res.status(400).json({ error: 'code required' });
|
|
722
|
+
res.json(await taskQueue.requestWorker('debug:eval', { code }, workerOpts(req), timeoutMs || 15_000));
|
|
723
|
+
} catch (e) { res.status(503).json({ error: e.message }); }
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
this.app.post('/api/worker/logs', async (req, res) => {
|
|
727
|
+
try {
|
|
728
|
+
const { since, level, limit, clear } = req.body || {};
|
|
729
|
+
res.json(await taskQueue.requestWorker('debug:logs', { since, level, limit, clear }, workerOpts(req)));
|
|
730
|
+
} catch (e) { res.status(503).json({ error: e.message }); }
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
this.app.get('/api/worker/screenshot', async (req, res) => {
|
|
734
|
+
try {
|
|
735
|
+
const format = (req.query?.format === 'jpeg') ? 'jpeg' : 'png';
|
|
736
|
+
const quality = req.query?.quality ? Number(req.query.quality) : undefined;
|
|
737
|
+
const data = await taskQueue.requestWorker('debug:screenshot', { format, quality }, workerOpts(req), 15_000);
|
|
738
|
+
if (!data?.ok) return res.status(500).json({ error: data?.error || 'screenshot_failed' });
|
|
739
|
+
const buf = Buffer.from(data.dataUrl.split(',')[1], 'base64');
|
|
740
|
+
res.setHeader('Content-Type', `image/${format}`);
|
|
741
|
+
res.setHeader('Content-Disposition', `inline; filename="worker-screenshot.${format}"`);
|
|
742
|
+
res.send(buf);
|
|
743
|
+
} catch (e) { res.status(503).json({ error: e.message }); }
|
|
744
|
+
});
|
|
745
|
+
// ---------------- /Remote Debug API ----------------
|
|
746
|
+
|
|
411
747
|
this.app.get('/api/extension-path', (req, res) => {
|
|
412
748
|
try {
|
|
413
749
|
const extensionPath = path.join(__dirname, 'views', 'vg-coder');
|
|
@@ -427,6 +763,20 @@ class ApiServer {
|
|
|
427
763
|
res.status(500).json({ success: false, error: error.message });
|
|
428
764
|
}
|
|
429
765
|
});
|
|
766
|
+
|
|
767
|
+
// Clipboard API - Write system clipboard from server-side (avoids the
|
|
768
|
+
// browser's clipboard-write permission prompt on the page).
|
|
769
|
+
this.app.post('/api/clipboard', async (req, res) => {
|
|
770
|
+
try {
|
|
771
|
+
const text = (req.body?.text ?? '').toString();
|
|
772
|
+
const clipboardy = await import('clipboardy');
|
|
773
|
+
await clipboardy.default.write(text);
|
|
774
|
+
res.json({ success: true });
|
|
775
|
+
} catch (error) {
|
|
776
|
+
console.error('Clipboard write error:', error);
|
|
777
|
+
res.status(500).json({ success: false, error: error.message });
|
|
778
|
+
}
|
|
779
|
+
});
|
|
430
780
|
|
|
431
781
|
this.app.post('/api/shutdown', async (req, res) => {
|
|
432
782
|
res.json({ success: true });
|
|
@@ -447,11 +797,14 @@ class ApiServer {
|
|
|
447
797
|
}
|
|
448
798
|
};
|
|
449
799
|
this.httpServer.once('error', onError);
|
|
450
|
-
this.server = this.httpServer.listen(port, () => {
|
|
800
|
+
this.server = this.httpServer.listen(port, '127.0.0.1', () => {
|
|
451
801
|
this.httpServer.removeListener('error', onError);
|
|
452
802
|
this.port = this.server.address().port;
|
|
453
803
|
console.log(chalk.green(`🚀 Server Online: http://localhost:${this.port}`));
|
|
454
804
|
console.log(chalk.blue(`📦 Dist served at: http://localhost:${this.port}/dist`));
|
|
805
|
+
taskQueue.rehydrateFromProjects(this.projectManager).catch(err => {
|
|
806
|
+
console.log(chalk.yellow(`[TaskQueue] Rehydrate failed: ${err.message}`));
|
|
807
|
+
});
|
|
455
808
|
resolve();
|
|
456
809
|
});
|
|
457
810
|
};
|