@luckydraw/cumulus 0.27.17 → 0.27.19
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/dist/cli/cumulus.js +0 -0
- package/dist/gateway/adapters/webchat.d.ts +1 -3
- package/dist/gateway/adapters/webchat.d.ts.map +1 -1
- package/dist/gateway/adapters/webchat.js +7 -150
- package/dist/gateway/adapters/webchat.js.map +1 -1
- package/dist/gateway/config.d.ts +7 -0
- package/dist/gateway/config.d.ts.map +1 -1
- package/dist/gateway/config.js +13 -0
- package/dist/gateway/config.js.map +1 -1
- package/dist/gateway/daemon.js +26 -8
- package/dist/gateway/daemon.js.map +1 -1
- package/dist/gateway/gateway-agents-mcp.js +49 -0
- package/dist/gateway/gateway-agents-mcp.js.map +1 -1
- package/dist/gateway/push.d.ts +66 -0
- package/dist/gateway/push.d.ts.map +1 -0
- package/dist/gateway/push.js +131 -0
- package/dist/gateway/push.js.map +1 -0
- package/dist/gateway/server.d.ts +3 -9
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +112 -243
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/static/sw.js +73 -0
- package/dist/gateway/static/widget.js +125 -759
- package/dist/index.d.ts +3 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -6
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +0 -8
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +1 -61
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/content-detector.d.ts.map +1 -1
- package/dist/lib/content-detector.js +2 -9
- package/dist/lib/content-detector.js.map +1 -1
- package/dist/lib/gateway.d.ts +2 -14
- package/dist/lib/gateway.d.ts.map +1 -1
- package/dist/lib/gateway.js +103 -234
- package/dist/lib/gateway.js.map +1 -1
- package/dist/lib/summaries.d.ts.map +1 -1
- package/dist/lib/summaries.js +2 -9
- package/dist/lib/summaries.js.map +1 -1
- package/dist/mcp/index.js +0 -0
- package/dist/mcp/tool-handler.d.ts.map +1 -1
- package/dist/mcp/tool-handler.js +3 -10
- package/dist/mcp/tool-handler.js.map +1 -1
- package/dist/tui/hooks/useClaudeProcess.d.ts.map +1 -1
- package/dist/tui/hooks/useClaudeProcess.js +23 -18
- package/dist/tui/hooks/useClaudeProcess.js.map +1 -1
- package/package.json +3 -1
- package/dist/gateway/device-mcp.d.ts +0 -21
- package/dist/gateway/device-mcp.d.ts.map +0 -1
- package/dist/gateway/device-mcp.js +0 -165
- package/dist/gateway/device-mcp.js.map +0 -1
- package/dist/lib/executor.d.ts +0 -168
- package/dist/lib/executor.d.ts.map +0 -1
- package/dist/lib/executor.js +0 -284
- package/dist/lib/executor.js.map +0 -1
- package/dist/lib/projects.d.ts.map +0 -1
package/dist/gateway/server.js
CHANGED
|
@@ -17,15 +17,14 @@ import * as fs from 'fs';
|
|
|
17
17
|
import * as http from 'http';
|
|
18
18
|
import * as os from 'os';
|
|
19
19
|
import * as path from 'path';
|
|
20
|
-
import { WebSocketServer } from 'ws';
|
|
21
20
|
import { loadThreadConfig, saveThreadConfig } from '../lib/config.js';
|
|
22
21
|
import { loadEmbeddings, generateMissingEmbeddings } from '../lib/embeddings.js';
|
|
23
|
-
import { ExecutorRegistry } from '../lib/executor.js';
|
|
24
22
|
import { sendMessage, getOrCreateThread, clearThreadCache, } from '../lib/gateway.js';
|
|
25
23
|
import { catchUpSegmentation, loadSegmentBoundaries, saveSegmentBoundaries, } from '../lib/segments.js';
|
|
26
24
|
import { listTemplates, scaffoldFromTemplate } from '../lib/templates.js';
|
|
27
25
|
import { serveStaticFile } from './adapters/webchat.js';
|
|
28
26
|
import { authenticate } from './auth.js';
|
|
27
|
+
import { configureVapid, sendNotification, subscribe as pushSubscribe, unsubscribe as pushUnsubscribe, } from './push.js';
|
|
29
28
|
const threadQueues = new Map();
|
|
30
29
|
const threadBusy = new Map();
|
|
31
30
|
function enqueue(threadName, item) {
|
|
@@ -86,7 +85,6 @@ async function processMessage(threadName, item, opts) {
|
|
|
86
85
|
projectRoot: opts.projectRoot,
|
|
87
86
|
extraMcpServers: opts.extraMcpServers,
|
|
88
87
|
gatewayAgentsConfig: opts.gatewayAgentsConfig,
|
|
89
|
-
executorRegistry: opts.executorRegistry,
|
|
90
88
|
signal: ac.signal,
|
|
91
89
|
onToken: text => sendSSE('token', { text }),
|
|
92
90
|
onSegment: segment => sendSSE('segment', segment),
|
|
@@ -204,7 +202,6 @@ async function handleAgentInject(req, res, pipelineOpts) {
|
|
|
204
202
|
await getOrCreateThread(targetName);
|
|
205
203
|
// Send as a message through the pipeline (fire-and-forget)
|
|
206
204
|
threadBusy.set(targetName, true);
|
|
207
|
-
const broadcast = pipelineOpts.broadcastToThread;
|
|
208
205
|
sendMessage({
|
|
209
206
|
threadName: targetName,
|
|
210
207
|
message: formatted,
|
|
@@ -214,35 +211,6 @@ async function handleAgentInject(req, res, pipelineOpts) {
|
|
|
214
211
|
projectRoot: pipelineOpts.projectRoot,
|
|
215
212
|
extraMcpServers: pipelineOpts.extraMcpServers,
|
|
216
213
|
gatewayAgentsConfig: pipelineOpts.gatewayAgentsConfig,
|
|
217
|
-
executorRegistry: pipelineOpts.executorRegistry,
|
|
218
|
-
})
|
|
219
|
-
.then(result => {
|
|
220
|
-
// Notify connected clients that new messages arrived on this thread
|
|
221
|
-
console.log(`[Gateway] Agent inject completed for "${targetName}", broadcast=${!!broadcast}, result=${!!result}`);
|
|
222
|
-
if (broadcast && result) {
|
|
223
|
-
const newMessages = [];
|
|
224
|
-
if (result.userMessage) {
|
|
225
|
-
newMessages.push({ id: result.userMessage.id, role: 'user', content: formatted });
|
|
226
|
-
}
|
|
227
|
-
if (result.assistantMessage) {
|
|
228
|
-
newMessages.push({
|
|
229
|
-
id: result.assistantMessage.id,
|
|
230
|
-
role: 'assistant',
|
|
231
|
-
content: result.response,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
if (newMessages.length > 0) {
|
|
235
|
-
console.log(`[Gateway] Broadcasting new_messages to "${targetName}": ${newMessages.length} messages`);
|
|
236
|
-
broadcast(targetName, {
|
|
237
|
-
type: 'new_messages',
|
|
238
|
-
threadName: targetName,
|
|
239
|
-
messages: newMessages,
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
else if (!broadcast) {
|
|
244
|
-
console.warn(`[Gateway] No broadcast function — agent inject for "${targetName}" won't notify clients`);
|
|
245
|
-
}
|
|
246
214
|
})
|
|
247
215
|
.catch(err => console.error(`[Gateway] agent inject error for ${targetName}:`, err))
|
|
248
216
|
.finally(() => {
|
|
@@ -354,7 +322,6 @@ function drainAgentQueue(threadName, pipelineOpts) {
|
|
|
354
322
|
projectRoot: pipelineOpts.projectRoot,
|
|
355
323
|
extraMcpServers: pipelineOpts.extraMcpServers,
|
|
356
324
|
gatewayAgentsConfig: pipelineOpts.gatewayAgentsConfig,
|
|
357
|
-
executorRegistry: pipelineOpts.executorRegistry,
|
|
358
325
|
})
|
|
359
326
|
.catch(err => console.error(`[Gateway] agent queue drain error for ${threadName}:`, err))
|
|
360
327
|
.finally(() => {
|
|
@@ -382,15 +349,6 @@ function parseRoute(url) {
|
|
|
382
349
|
query,
|
|
383
350
|
};
|
|
384
351
|
}
|
|
385
|
-
// Match /api/device/:executorId/tool
|
|
386
|
-
const deviceMatch = pathPart.match(/^\/api\/device\/([^/]+)\/tool$/);
|
|
387
|
-
if (deviceMatch) {
|
|
388
|
-
return {
|
|
389
|
-
path: '/api/device/:executorId/tool',
|
|
390
|
-
params: { executorId: decodeURIComponent(deviceMatch[1]) },
|
|
391
|
-
query,
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
352
|
return { path: pathPart, params: {}, query };
|
|
395
353
|
}
|
|
396
354
|
function readBody(req) {
|
|
@@ -426,27 +384,16 @@ async function handlePostMessage(threadName, req, res, opts) {
|
|
|
426
384
|
drainQueue(threadName, opts).catch(err => console.error(`[Gateway] drain error:`, err));
|
|
427
385
|
}
|
|
428
386
|
async function handleGetHistory(threadName, query, res) {
|
|
429
|
-
const limit = Math.min(parseInt(query.limit || '
|
|
430
|
-
const hasOffset = query.offset !== undefined;
|
|
387
|
+
const limit = Math.min(parseInt(query.limit || '20', 10) || 20, 200);
|
|
431
388
|
const offset = parseInt(query.offset || '0', 10) || 0;
|
|
432
389
|
try {
|
|
433
390
|
const thread = await getOrCreateThread(threadName);
|
|
434
391
|
const all = await thread.history.getAll();
|
|
435
392
|
const total = all.length;
|
|
436
|
-
|
|
437
|
-
let slice;
|
|
438
|
-
let startIndex;
|
|
439
|
-
if (!hasOffset) {
|
|
440
|
-
startIndex = Math.max(0, total - limit);
|
|
441
|
-
slice = all.slice(startIndex);
|
|
442
|
-
}
|
|
443
|
-
else {
|
|
444
|
-
startIndex = offset;
|
|
445
|
-
slice = all.slice(offset, offset + limit);
|
|
446
|
-
}
|
|
393
|
+
const slice = all.slice(offset, offset + limit);
|
|
447
394
|
const messages = slice.map((m, i) => ({
|
|
448
395
|
id: m.id,
|
|
449
|
-
index:
|
|
396
|
+
index: offset + i,
|
|
450
397
|
role: m.role,
|
|
451
398
|
content: m.content,
|
|
452
399
|
timestamp: m.timestamp,
|
|
@@ -477,7 +424,7 @@ async function handleGetStatus(threadName, res) {
|
|
|
477
424
|
jsonResponse(res, 500, { error: String(err) });
|
|
478
425
|
}
|
|
479
426
|
}
|
|
480
|
-
async function handleListThreads(res
|
|
427
|
+
async function handleListThreads(res) {
|
|
481
428
|
try {
|
|
482
429
|
if (!fs.existsSync(THREADS_DIR)) {
|
|
483
430
|
jsonResponse(res, 200, { threads: [] });
|
|
@@ -498,7 +445,7 @@ async function handleListThreads(res, registry) {
|
|
|
498
445
|
catch {
|
|
499
446
|
// skip
|
|
500
447
|
}
|
|
501
|
-
// Read thread config to include projectDir
|
|
448
|
+
// Read thread config to include projectDir if set
|
|
502
449
|
const tc = await loadThreadConfig(name);
|
|
503
450
|
const entry = {
|
|
504
451
|
name,
|
|
@@ -508,12 +455,6 @@ async function handleListThreads(res, registry) {
|
|
|
508
455
|
if (tc.projectDir) {
|
|
509
456
|
entry.projectDir = tc.projectDir;
|
|
510
457
|
}
|
|
511
|
-
// Include executor info
|
|
512
|
-
const executor = tc.executor || 'thundercat';
|
|
513
|
-
entry.executor = executor;
|
|
514
|
-
if (registry) {
|
|
515
|
-
entry.executorOnline = registry.isOnline(executor);
|
|
516
|
-
}
|
|
517
458
|
threads.push(entry);
|
|
518
459
|
}
|
|
519
460
|
// Sort by last activity descending
|
|
@@ -530,46 +471,6 @@ async function handleListThreads(res, registry) {
|
|
|
530
471
|
jsonResponse(res, 500, { error: String(err) });
|
|
531
472
|
}
|
|
532
473
|
}
|
|
533
|
-
async function handleListExecutors(res, registry) {
|
|
534
|
-
try {
|
|
535
|
-
const executors = registry.listExecutors();
|
|
536
|
-
jsonResponse(res, 200, { executors });
|
|
537
|
-
}
|
|
538
|
-
catch (err) {
|
|
539
|
-
jsonResponse(res, 500, { error: String(err) });
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
/**
|
|
543
|
-
* Route a device tool call to a remote executor.
|
|
544
|
-
* The executor executes the tool locally and returns the result.
|
|
545
|
-
*/
|
|
546
|
-
async function handleDeviceToolCall(executorId, req, res, registry) {
|
|
547
|
-
let body;
|
|
548
|
-
try {
|
|
549
|
-
const raw = await readBody(req);
|
|
550
|
-
body = JSON.parse(raw);
|
|
551
|
-
}
|
|
552
|
-
catch {
|
|
553
|
-
jsonResponse(res, 400, { error: 'Invalid JSON body' });
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
if (!body.tool) {
|
|
557
|
-
jsonResponse(res, 400, { error: 'Missing tool name' });
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
if (!registry.isOnline(executorId)) {
|
|
561
|
-
jsonResponse(res, 503, { error: `Device "${executorId}" is offline` });
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
try {
|
|
565
|
-
const result = await registry.deviceToolCall(executorId, body.tool, body.args || {});
|
|
566
|
-
jsonResponse(res, 200, result);
|
|
567
|
-
}
|
|
568
|
-
catch (err) {
|
|
569
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
570
|
-
jsonResponse(res, 500, { error: errorMsg, isError: true });
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
474
|
async function handleDeleteThread(threadName, req, res) {
|
|
574
475
|
const confirm = req.headers['x-confirm'];
|
|
575
476
|
if (confirm !== 'delete') {
|
|
@@ -618,60 +519,6 @@ async function handleDeleteThread(threadName, req, res) {
|
|
|
618
519
|
jsonResponse(res, 500, { error: String(err) });
|
|
619
520
|
}
|
|
620
521
|
}
|
|
621
|
-
async function handleDeleteMessages(threadName, req, res) {
|
|
622
|
-
const confirm = req.headers['x-confirm'];
|
|
623
|
-
if (confirm !== 'delete') {
|
|
624
|
-
jsonResponse(res, 400, { error: 'Missing X-Confirm: delete header' });
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
let body;
|
|
628
|
-
try {
|
|
629
|
-
const raw = await readBody(req);
|
|
630
|
-
body = JSON.parse(raw);
|
|
631
|
-
}
|
|
632
|
-
catch {
|
|
633
|
-
jsonResponse(res, 400, { error: 'Invalid JSON body' });
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
if (!Array.isArray(body.messageIds) || body.messageIds.length === 0) {
|
|
637
|
-
jsonResponse(res, 400, { error: 'Missing or empty "messageIds" array' });
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
const idsToDelete = new Set(body.messageIds);
|
|
641
|
-
try {
|
|
642
|
-
const filePath = path.join(THREADS_DIR, threadName + '.jsonl');
|
|
643
|
-
if (!fs.existsSync(filePath)) {
|
|
644
|
-
jsonResponse(res, 404, { error: 'Thread not found' });
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
648
|
-
const lines = raw.split('\n').filter(l => l.trim());
|
|
649
|
-
const kept = [];
|
|
650
|
-
let deleted = 0;
|
|
651
|
-
for (const line of lines) {
|
|
652
|
-
try {
|
|
653
|
-
const msg = JSON.parse(line);
|
|
654
|
-
if (msg.id && idsToDelete.has(msg.id)) {
|
|
655
|
-
deleted++;
|
|
656
|
-
}
|
|
657
|
-
else {
|
|
658
|
-
kept.push(line);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
catch {
|
|
662
|
-
// Malformed line — keep it
|
|
663
|
-
kept.push(line);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
fs.writeFileSync(filePath, kept.join('\n') + (kept.length > 0 ? '\n' : ''), 'utf-8');
|
|
667
|
-
// Clear in-memory cache so next request reads fresh data
|
|
668
|
-
clearThreadCache(threadName);
|
|
669
|
-
jsonResponse(res, 200, { deleted, remaining: kept.length });
|
|
670
|
-
}
|
|
671
|
-
catch (err) {
|
|
672
|
-
jsonResponse(res, 500, { error: String(err) });
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
522
|
async function handleSyncMessages(threadName, req, res) {
|
|
676
523
|
let body;
|
|
677
524
|
try {
|
|
@@ -759,14 +606,6 @@ async function handleUpdateThreadConfig(threadName, req, res) {
|
|
|
759
606
|
updates.projectDir = body.projectDir;
|
|
760
607
|
if (body.template !== undefined)
|
|
761
608
|
updates.template = body.template;
|
|
762
|
-
if (body.executor !== undefined)
|
|
763
|
-
updates.executor = body.executor;
|
|
764
|
-
if (body.executorPaths !== undefined)
|
|
765
|
-
updates.executorPaths = body.executorPaths;
|
|
766
|
-
if (body.devices !== undefined)
|
|
767
|
-
updates.devices = body.devices;
|
|
768
|
-
if (body.gitRemoteUrl !== undefined)
|
|
769
|
-
updates.gitRemoteUrl = body.gitRemoteUrl;
|
|
770
609
|
await saveThreadConfig(threadName, updates);
|
|
771
610
|
const updated = await loadThreadConfig(threadName);
|
|
772
611
|
jsonResponse(res, 200, { config: updated });
|
|
@@ -917,15 +756,95 @@ async function handleMigrate(req, res, opts) {
|
|
|
917
756
|
jsonResponse(res, 500, { error: String(err) });
|
|
918
757
|
}
|
|
919
758
|
}
|
|
759
|
+
// ─── Push notification handlers ─────────────────────────────────────────────
|
|
760
|
+
function handleGetVapidKey(res, vapid) {
|
|
761
|
+
if (!vapid) {
|
|
762
|
+
jsonResponse(res, 404, { error: 'Push notifications not configured' });
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
jsonResponse(res, 200, { publicKey: vapid.publicKey });
|
|
766
|
+
}
|
|
767
|
+
async function handlePushSubscribe(req, res) {
|
|
768
|
+
let body;
|
|
769
|
+
try {
|
|
770
|
+
const raw = await readBody(req);
|
|
771
|
+
body = JSON.parse(raw);
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
jsonResponse(res, 400, { error: 'Invalid JSON body' });
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (!body.endpoint || !body.keys?.p256dh || !body.keys?.auth) {
|
|
778
|
+
jsonResponse(res, 400, {
|
|
779
|
+
error: 'Missing required fields: endpoint, keys.p256dh, keys.auth',
|
|
780
|
+
});
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const sub = {
|
|
784
|
+
endpoint: body.endpoint,
|
|
785
|
+
keys: { p256dh: body.keys.p256dh, auth: body.keys.auth },
|
|
786
|
+
createdAt: new Date().toISOString(),
|
|
787
|
+
label: body.label,
|
|
788
|
+
};
|
|
789
|
+
pushSubscribe(sub);
|
|
790
|
+
jsonResponse(res, 200, { subscribed: true });
|
|
791
|
+
}
|
|
792
|
+
async function handlePushUnsubscribe(req, res) {
|
|
793
|
+
let body;
|
|
794
|
+
try {
|
|
795
|
+
const raw = await readBody(req);
|
|
796
|
+
body = JSON.parse(raw);
|
|
797
|
+
}
|
|
798
|
+
catch {
|
|
799
|
+
jsonResponse(res, 400, { error: 'Invalid JSON body' });
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (!body.endpoint) {
|
|
803
|
+
jsonResponse(res, 400, { error: 'Missing "endpoint"' });
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const removed = pushUnsubscribe(body.endpoint);
|
|
807
|
+
jsonResponse(res, 200, { removed });
|
|
808
|
+
}
|
|
809
|
+
async function handleNotify(req, res) {
|
|
810
|
+
let body;
|
|
811
|
+
try {
|
|
812
|
+
const raw = await readBody(req);
|
|
813
|
+
body = JSON.parse(raw);
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
jsonResponse(res, 400, { error: 'Invalid JSON body' });
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const title = body.title || 'Cumulus';
|
|
820
|
+
const notifBody = body.body || body.message || '';
|
|
821
|
+
if (!notifBody) {
|
|
822
|
+
jsonResponse(res, 400, { error: 'Missing "body" or "message"' });
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
try {
|
|
826
|
+
const delivered = await sendNotification({
|
|
827
|
+
title,
|
|
828
|
+
body: notifBody,
|
|
829
|
+
threadName: body.threadName,
|
|
830
|
+
priority: body.priority,
|
|
831
|
+
});
|
|
832
|
+
jsonResponse(res, 200, { delivered });
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
920
838
|
// ─── Server ──────────────────────────────────────────────────────────────────
|
|
921
839
|
export async function startGatewayServer(options) {
|
|
922
840
|
const { port, ...pipelineOpts } = options;
|
|
923
841
|
let currentApiKeys = options.apiKeys;
|
|
924
|
-
//
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
842
|
+
// Initialize VAPID for push notifications (if configured)
|
|
843
|
+
const vapidConfig = options.vapid;
|
|
844
|
+
if (vapidConfig) {
|
|
845
|
+
configureVapid(vapidConfig);
|
|
846
|
+
console.log('[Gateway] Push notifications enabled (VAPID configured)');
|
|
847
|
+
}
|
|
929
848
|
const server = http.createServer(async (req, res) => {
|
|
930
849
|
// CORS headers for web clients
|
|
931
850
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -946,6 +865,11 @@ export async function startGatewayServer(options) {
|
|
|
946
865
|
if (req.method === 'GET' && serveStaticFile(req, res, currentApiKeys)) {
|
|
947
866
|
return;
|
|
948
867
|
}
|
|
868
|
+
// GET /api/push/vapid-key — public (no auth), returns VAPID public key
|
|
869
|
+
if (routePath === '/api/push/vapid-key' && req.method === 'GET') {
|
|
870
|
+
handleGetVapidKey(res, vapidConfig);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
949
873
|
// All other routes require auth
|
|
950
874
|
if (!authenticate(req, res, currentApiKeys))
|
|
951
875
|
return;
|
|
@@ -975,22 +899,6 @@ export async function startGatewayServer(options) {
|
|
|
975
899
|
await handleDeleteThread(params.name, req, res);
|
|
976
900
|
return;
|
|
977
901
|
}
|
|
978
|
-
// DELETE /api/thread/:name/messages
|
|
979
|
-
if (routePath === '/api/thread/:name/messages' && req.method === 'DELETE') {
|
|
980
|
-
await handleDeleteMessages(params.name, req, res);
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
// GET /api/thread/:name/config — read thread config
|
|
984
|
-
if (routePath === '/api/thread/:name/config' && req.method === 'GET') {
|
|
985
|
-
try {
|
|
986
|
-
const tc = await loadThreadConfig(params.name);
|
|
987
|
-
jsonResponse(res, 200, tc);
|
|
988
|
-
}
|
|
989
|
-
catch (err) {
|
|
990
|
-
jsonResponse(res, 500, { error: String(err) });
|
|
991
|
-
}
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
902
|
// PUT /api/thread/:name/config — update thread config (projectDir, template)
|
|
995
903
|
if (routePath === '/api/thread/:name/config' && req.method === 'PUT') {
|
|
996
904
|
await handleUpdateThreadConfig(params.name, req, res);
|
|
@@ -998,12 +906,7 @@ export async function startGatewayServer(options) {
|
|
|
998
906
|
}
|
|
999
907
|
// GET /api/threads
|
|
1000
908
|
if (routePath === '/api/threads' && req.method === 'GET') {
|
|
1001
|
-
await handleListThreads(res
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
// GET /api/executors
|
|
1005
|
-
if (routePath === '/api/executors' && req.method === 'GET') {
|
|
1006
|
-
await handleListExecutors(res, executorRegistry);
|
|
909
|
+
await handleListThreads(res);
|
|
1007
910
|
return;
|
|
1008
911
|
}
|
|
1009
912
|
// GET /api/templates — list templates
|
|
@@ -1021,16 +924,26 @@ export async function startGatewayServer(options) {
|
|
|
1021
924
|
await handleAgentInject(req, res, pipelineOpts);
|
|
1022
925
|
return;
|
|
1023
926
|
}
|
|
1024
|
-
// POST /api/device/:executorId/tool — route device tool call to executor
|
|
1025
|
-
if (routePath === '/api/device/:executorId/tool' && req.method === 'POST') {
|
|
1026
|
-
await handleDeviceToolCall(params.executorId, req, res, executorRegistry);
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
927
|
// POST /api/migrate — sync thread + clone repo + set projectDir
|
|
1030
928
|
if (routePath === '/api/migrate' && req.method === 'POST') {
|
|
1031
929
|
await handleMigrate(req, res, pipelineOpts);
|
|
1032
930
|
return;
|
|
1033
931
|
}
|
|
932
|
+
// POST /api/push/subscribe — register push subscription
|
|
933
|
+
if (routePath === '/api/push/subscribe' && req.method === 'POST') {
|
|
934
|
+
await handlePushSubscribe(req, res);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
// DELETE /api/push/subscribe — remove push subscription
|
|
938
|
+
if (routePath === '/api/push/subscribe' && req.method === 'DELETE') {
|
|
939
|
+
await handlePushUnsubscribe(req, res);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
// POST /api/notify — send push notification
|
|
943
|
+
if (routePath === '/api/notify' && req.method === 'POST') {
|
|
944
|
+
await handleNotify(req, res);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
1034
947
|
// 404
|
|
1035
948
|
jsonResponse(res, 404, { error: 'Not found' });
|
|
1036
949
|
}
|
|
@@ -1041,40 +954,6 @@ export async function startGatewayServer(options) {
|
|
|
1041
954
|
}
|
|
1042
955
|
}
|
|
1043
956
|
});
|
|
1044
|
-
// Handle executor WebSocket upgrades on /ws/executor
|
|
1045
|
-
server.on('upgrade', (req, socket, head) => {
|
|
1046
|
-
const url = req.url?.split('?')[0] || '/';
|
|
1047
|
-
if (url !== '/ws/executor')
|
|
1048
|
-
return; // let other handlers (webchat) handle non-executor paths
|
|
1049
|
-
// Authenticate via query param
|
|
1050
|
-
const queryApiKey = new URL(req.url || '/', `http://${req.headers.host}`).searchParams.get('apiKey');
|
|
1051
|
-
if (!queryApiKey || !currentApiKeys.includes(queryApiKey)) {
|
|
1052
|
-
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
1053
|
-
socket.destroy();
|
|
1054
|
-
return;
|
|
1055
|
-
}
|
|
1056
|
-
executorWss.handleUpgrade(req, socket, head, (ws) => {
|
|
1057
|
-
// Wait for registration message
|
|
1058
|
-
const timeout = setTimeout(() => {
|
|
1059
|
-
ws.close(1008, 'Registration timeout');
|
|
1060
|
-
}, 10_000);
|
|
1061
|
-
ws.once('message', (raw) => {
|
|
1062
|
-
clearTimeout(timeout);
|
|
1063
|
-
try {
|
|
1064
|
-
const data = JSON.parse(raw.toString());
|
|
1065
|
-
if (data.type === 'executor_register') {
|
|
1066
|
-
executorRegistry.register(data, ws);
|
|
1067
|
-
}
|
|
1068
|
-
else {
|
|
1069
|
-
ws.close(1008, 'Expected executor_register message');
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
catch {
|
|
1073
|
-
ws.close(1008, 'Invalid registration message');
|
|
1074
|
-
}
|
|
1075
|
-
});
|
|
1076
|
-
});
|
|
1077
|
-
});
|
|
1078
957
|
return new Promise((resolve, reject) => {
|
|
1079
958
|
server.on('error', reject);
|
|
1080
959
|
server.listen(port, () => {
|
|
@@ -1086,23 +965,13 @@ export async function startGatewayServer(options) {
|
|
|
1086
965
|
port: actualPort,
|
|
1087
966
|
url,
|
|
1088
967
|
server,
|
|
1089
|
-
executorRegistry,
|
|
1090
968
|
updateApiKeys: (newKeys) => {
|
|
1091
969
|
currentApiKeys = newKeys;
|
|
1092
970
|
},
|
|
1093
|
-
setBroadcastToThread: (fn) => {
|
|
1094
|
-
pipelineOpts.broadcastToThread = fn;
|
|
1095
|
-
},
|
|
1096
971
|
close: async () => {
|
|
1097
972
|
// Abort any in-flight message processing
|
|
1098
973
|
threadQueues.clear();
|
|
1099
974
|
threadBusy.clear();
|
|
1100
|
-
executorRegistry.shutdown();
|
|
1101
|
-
// Close executor WebSocket server
|
|
1102
|
-
for (const client of executorWss.clients) {
|
|
1103
|
-
client.close();
|
|
1104
|
-
}
|
|
1105
|
-
executorWss.close();
|
|
1106
975
|
return new Promise(resolveClose => {
|
|
1107
976
|
server.close(() => {
|
|
1108
977
|
clearThreadCache();
|