@shaykec/bridge 0.4.19 → 0.4.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/canvas-dist/assets/{_basePickBy-BOTBlJNd.js → _basePickBy-CWoeT3J7.js} +1 -1
  2. package/canvas-dist/assets/{_baseUniq-EF6Y2_Wm.js → _baseUniq-Dtuvtwtn.js} +1 -1
  3. package/canvas-dist/assets/{arc-C_vIirh2.js → arc-YYWnrNJU.js} +1 -1
  4. package/canvas-dist/assets/{architectureDiagram-VXUJARFQ-EvM6tQ7I.js → architectureDiagram-VXUJARFQ-CegbV-RR.js} +1 -1
  5. package/canvas-dist/assets/{blockDiagram-VD42YOAC-B_rbZyqc.js → blockDiagram-VD42YOAC-C2e_j6ry.js} +1 -1
  6. package/canvas-dist/assets/{c4Diagram-YG6GDRKO-J9PHecY3.js → c4Diagram-YG6GDRKO-rIpnAud9.js} +1 -1
  7. package/canvas-dist/assets/channel-BzJVlie3.js +1 -0
  8. package/canvas-dist/assets/{chunk-4BX2VUAB-DjcN96Mk.js → chunk-4BX2VUAB-CpZGetnU.js} +1 -1
  9. package/canvas-dist/assets/{chunk-55IACEB6-CTdcUQSV.js → chunk-55IACEB6-L0OhcFdd.js} +1 -1
  10. package/canvas-dist/assets/{chunk-B4BG7PRW-Dcov7eRi.js → chunk-B4BG7PRW-Cv9vsAzg.js} +1 -1
  11. package/canvas-dist/assets/{chunk-DI55MBZ5-DUJCBZzM.js → chunk-DI55MBZ5-B3p1mU43.js} +1 -1
  12. package/canvas-dist/assets/{chunk-FMBD7UC4-EfGA9ufe.js → chunk-FMBD7UC4-JCLAHw5x.js} +1 -1
  13. package/canvas-dist/assets/{chunk-QN33PNHL-Cu6V1xBU.js → chunk-QN33PNHL-C9arKEVq.js} +1 -1
  14. package/canvas-dist/assets/{chunk-QZHKN3VN-avF3sH_r.js → chunk-QZHKN3VN-Bs1r3d9U.js} +1 -1
  15. package/canvas-dist/assets/{chunk-TZMSLE5B-CkWW-qpk.js → chunk-TZMSLE5B-_Ye6r84Y.js} +1 -1
  16. package/canvas-dist/assets/classDiagram-2ON5EDUG-BTs-zEmB.js +1 -0
  17. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-BTs-zEmB.js +1 -0
  18. package/canvas-dist/assets/clone-CXEfuXmc.js +1 -0
  19. package/canvas-dist/assets/{cose-bilkent-S5V4N54A-DDE4zf7X.js → cose-bilkent-S5V4N54A-2O2oovOj.js} +1 -1
  20. package/canvas-dist/assets/{dagre-6UL2VRFP-BD6MGb7B.js → dagre-6UL2VRFP-gRmGLrEW.js} +1 -1
  21. package/canvas-dist/assets/{diagram-PSM6KHXK-yyu-ytzf.js → diagram-PSM6KHXK-B7Li-xxw.js} +1 -1
  22. package/canvas-dist/assets/{diagram-QEK2KX5R-B_H957Uf.js → diagram-QEK2KX5R-B_NNUAm3.js} +1 -1
  23. package/canvas-dist/assets/{diagram-S2PKOQOG-DuebuBVv.js → diagram-S2PKOQOG-NcK-KHaA.js} +1 -1
  24. package/canvas-dist/assets/{erDiagram-Q2GNP2WA-AxqPt6IZ.js → erDiagram-Q2GNP2WA-CG7dqzk3.js} +1 -1
  25. package/canvas-dist/assets/{flowDiagram-NV44I4VS-mDhW3D3Q.js → flowDiagram-NV44I4VS-CBzCj5D6.js} +1 -1
  26. package/canvas-dist/assets/{ganttDiagram-JELNMOA3-sA8pHJPp.js → ganttDiagram-JELNMOA3-CHw-4qJC.js} +1 -1
  27. package/canvas-dist/assets/{gitGraphDiagram-V2S2FVAM-CvLzvhKr.js → gitGraphDiagram-V2S2FVAM-Dqrc4wUs.js} +1 -1
  28. package/canvas-dist/assets/{graph-BVZqMrwW.js → graph-X9Kzu-pf.js} +1 -1
  29. package/canvas-dist/assets/{index-CF3qc2Xb.js → index-BQFKo-II.js} +1 -1
  30. package/canvas-dist/assets/index-DJ49c6u-.js +426 -0
  31. package/canvas-dist/assets/{infoDiagram-HS3SLOUP-D1Kg3Q9d.js → infoDiagram-HS3SLOUP-CflnZPsm.js} +1 -1
  32. package/canvas-dist/assets/{journeyDiagram-XKPGCS4Q-D7ogbx9z.js → journeyDiagram-XKPGCS4Q-D2gkCipQ.js} +1 -1
  33. package/canvas-dist/assets/{kanban-definition-3W4ZIXB7-CDcnICM9.js → kanban-definition-3W4ZIXB7-CtLLz4o8.js} +1 -1
  34. package/canvas-dist/assets/{layout-CuaK7i3M.js → layout-CjvV_Dms.js} +1 -1
  35. package/canvas-dist/assets/{linear-CLSTOJ0g.js → linear-D3cIYHoS.js} +1 -1
  36. package/canvas-dist/assets/{mindmap-definition-VGOIOE7T-TrK7CIKt.js → mindmap-definition-VGOIOE7T-DSgjVg-P.js} +1 -1
  37. package/canvas-dist/assets/{pieDiagram-ADFJNKIX-BcIKTRbi.js → pieDiagram-ADFJNKIX-B_lYaGFj.js} +1 -1
  38. package/canvas-dist/assets/{quadrantDiagram-AYHSOK5B-EOHXFGoQ.js → quadrantDiagram-AYHSOK5B-DLZLTJe3.js} +1 -1
  39. package/canvas-dist/assets/{requirementDiagram-UZGBJVZJ-CJ8lImGs.js → requirementDiagram-UZGBJVZJ-CZE26rhL.js} +1 -1
  40. package/canvas-dist/assets/{sankeyDiagram-TZEHDZUN-4cANY87E.js → sankeyDiagram-TZEHDZUN-DQMRJAPV.js} +1 -1
  41. package/canvas-dist/assets/{sequenceDiagram-WL72ISMW-D9HrEsci.js → sequenceDiagram-WL72ISMW-BY723FEn.js} +1 -1
  42. package/canvas-dist/assets/{stateDiagram-FKZM4ZOC-qVbMjauZ.js → stateDiagram-FKZM4ZOC-C_UdOFhy.js} +1 -1
  43. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-DXIiFh0L.js +1 -0
  44. package/canvas-dist/assets/{timeline-definition-IT6M3QCI-DDBlkydm.js → timeline-definition-IT6M3QCI-DkrJqww0.js} +1 -1
  45. package/canvas-dist/assets/{treemap-GDKQZRPO-D4a8udjO.js → treemap-GDKQZRPO-B-6bMZqD.js} +1 -1
  46. package/canvas-dist/assets/{xychartDiagram-PRI3JC2R-DteXAAAu.js → xychartDiagram-PRI3JC2R-DkBhUy_D.js} +1 -1
  47. package/canvas-dist/index.html +1 -1
  48. package/package.json +3 -2
  49. package/src/server.e2e.test.js +10 -41
  50. package/src/server.js +302 -186
  51. package/canvas-dist/assets/channel-saCUO1KA.js +0 -1
  52. package/canvas-dist/assets/classDiagram-2ON5EDUG-CBLbQwHx.js +0 -1
  53. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CBLbQwHx.js +0 -1
  54. package/canvas-dist/assets/clone-DXnda9BY.js +0 -1
  55. package/canvas-dist/assets/index-DYNtb52W.js +0 -426
  56. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-MT16RLO4.js +0 -1
  57. package/src/claude-session.js +0 -414
  58. package/src/claude-session.test.js +0 -326
package/src/server.js CHANGED
@@ -37,13 +37,14 @@ import {
37
37
  DEFAULT_PORT,
38
38
  INBOX_DIR,
39
39
  MAX_LONG_POLL_TIMEOUT_MS,
40
+ ACHIEVEMENTS,
40
41
  } from '@shaykec/shared';
41
42
 
42
43
  import { TierManager, generateClientId, validateHandshake } from './protocol.js';
43
44
  import { EventRouter } from './router.js';
44
45
  import { renderTemplate, listTemplates } from './templates.js';
45
46
  import { createSession, execCommand, getSession, destroySession, cleanupIdleSessions } from './terminal.js';
46
- import { ClaudeSessionManager } from './claude-session.js';
47
+ import { createAgentServer } from '@shaykec/agent-web/server';
47
48
 
48
49
  const __filename = fileURLToPath(import.meta.url);
49
50
  const __dirname = dirname(__filename);
@@ -84,10 +85,25 @@ export function startServer(options = {}) {
84
85
  const port = options.port || DEFAULT_PORT;
85
86
  const tierManager = new TierManager();
86
87
  const router = new EventRouter(tierManager);
87
- const chatSession = new ClaudeSessionManager();
88
88
 
89
- // Maps sessionId -> clientId for targeted chat message delivery
90
- const sessionClientMap = new Map();
89
+ // Chat is delegated entirely to @shaykec/agent-web
90
+ const agentConfig = { cwd: process.cwd() };
91
+ if (options.pluginDir) {
92
+ agentConfig.plugins = [{ type: 'local', path: options.pluginDir }];
93
+ }
94
+ const agentServer = createAgentServer({
95
+ basePath: '/api',
96
+ config: agentConfig,
97
+ hooks: {
98
+ // Relay chat envelopes to visual WS clients so the canvas receives responses
99
+ onMessage: (envelope) => {
100
+ const data = JSON.stringify(envelope);
101
+ tierManager.broadcastWs(data);
102
+ tierManager.broadcastSse(data);
103
+ },
104
+ },
105
+ });
106
+ const chatMiddleware = agentServer.middleware();
91
107
 
92
108
  if (options.onTierChange) {
93
109
  tierManager.onTierChange(options.onTierChange);
@@ -96,47 +112,57 @@ export function startServer(options = {}) {
96
112
  // Ensure inbox directory exists
97
113
  ensureDir(INBOX_DIR);
98
114
 
99
- /**
100
- * Create an onMessage callback that sends chat envelopes to a specific client.
101
- * Falls back to broadcast if the client disconnected.
102
- * @param {string} clientId
103
- * @returns {function}
104
- */
105
- function createTargetedSend(clientId) {
106
- return (envelope) => {
107
- const data = JSON.stringify(envelope);
108
- if (!tierManager.sendToClient(clientId, data)) {
109
- // Client disconnected — broadcast as fallback
110
- tierManager.broadcastWs(data);
111
- tierManager.broadcastSse(data);
112
- }
113
- };
114
- }
115
+ // Create HTTP server
116
+ const server = createServer((req, res) => {
117
+ handleRequest(req, res, tierManager, router, options, chatMiddleware);
118
+ });
119
+
120
+ // Both WSSes use noServer to avoid ws library destroying sockets
121
+ // on path mismatch (ws docs: use noServer for multiple WSSes)
122
+ const wss = new WebSocketServer({ noServer: true });
123
+ const chatWss = new WebSocketServer({ noServer: true });
115
124
 
116
- // Wire up chat messages from browser -> session manager
117
- router.onChatMessage = (envelope) => {
118
- if (envelope.type === 'chat:send' && envelope.payload?.text) {
119
- const sessionId = envelope.payload.sessionId;
120
- if (!sessionId || !chatSession.isActive(sessionId)) return;
121
- chatSession.sendMessage(sessionId, envelope.payload.text).catch((err) => {
122
- console.error('[chat] sendMessage error:', err.message);
125
+ server.on('upgrade', (req, socket, head) => {
126
+ const { pathname } = new URL(req.url, `http://${req.headers.host}`);
127
+ if (pathname === '/ws') {
128
+ wss.handleUpgrade(req, socket, head, (ws) => {
129
+ wss.emit('connection', ws, req);
123
130
  });
131
+ } else if (pathname === '/api/ws') {
132
+ chatWss.handleUpgrade(req, socket, head, (ws) => {
133
+ chatWss.emit('connection', ws, req);
134
+ });
135
+ } else {
136
+ socket.destroy();
124
137
  }
125
- if (envelope.type === 'chat:stop') {
126
- const sessionId = envelope.payload?.sessionId;
127
- if (sessionId) {
128
- chatSession.stop(sessionId).catch(() => {});
129
- }
130
- }
131
- };
138
+ });
132
139
 
133
- // Create HTTP server
134
- const server = createServer((req, res) => {
135
- handleRequest(req, res, tierManager, router, options, chatSession, sessionClientMap, createTargetedSend);
140
+ // Wire agent-web transport to the chat WSS
141
+ chatWss.on('connection', (ws) => {
142
+ agentServer.transport.handleWsConnection(
143
+ ws,
144
+ () => {},
145
+ (clientId, envelope) => {
146
+ if (envelope.type === 'chat:send') {
147
+ const sessionId = envelope.sessionId || envelope.payload?.sessionId;
148
+ const text = envelope.payload?.text;
149
+ if (sessionId && text) {
150
+ agentServer.sessions.sendMessage(sessionId, text).catch(err => {
151
+ console.error('[agent-web] sendMessage error:', err.message);
152
+ });
153
+ }
154
+ }
155
+ if (envelope.type === 'chat:stop') {
156
+ const sessionId = envelope.sessionId || envelope.payload?.sessionId;
157
+ if (sessionId) {
158
+ agentServer.sessions.stopSession(sessionId).catch(() => {});
159
+ }
160
+ }
161
+ }
162
+ );
136
163
  });
137
164
 
138
- // Create WebSocket server on the same HTTP server
139
- const wss = new WebSocketServer({ server, path: '/ws' });
165
+ agentServer.transport.startHeartbeat();
140
166
 
141
167
  wss.on('connection', (ws, req) => {
142
168
  const clientId = generateClientId();
@@ -185,7 +211,27 @@ export function startServer(options = {}) {
185
211
  return;
186
212
  }
187
213
 
188
- // After handshake, route messages through the router
214
+ // Intercept chat messages and forward to agent-web session manager
215
+ const { valid: msgValid, envelope: msgEnv } = parseEnvelope(data);
216
+ if (msgValid && msgEnv.type === 'chat:send') {
217
+ const sessionId = msgEnv.sessionId || msgEnv.payload?.sessionId;
218
+ const text = msgEnv.payload?.text;
219
+ if (sessionId && text) {
220
+ agentServer.sessions.sendMessage(sessionId, text).catch(err => {
221
+ console.error('[bridge] chat:send error:', err.message);
222
+ });
223
+ }
224
+ return;
225
+ }
226
+ if (msgValid && msgEnv.type === 'chat:stop') {
227
+ const sessionId = msgEnv.sessionId || msgEnv.payload?.sessionId;
228
+ if (sessionId) {
229
+ agentServer.sessions.stopSession(sessionId).catch(() => {});
230
+ }
231
+ return;
232
+ }
233
+
234
+ // After handshake, route visual/event messages through the router
189
235
  const result = router.handleWsMessage(data, clientId);
190
236
  if (!result.handled && result.error) {
191
237
  ws.send(JSON.stringify({ error: result.error }));
@@ -226,13 +272,10 @@ export function startServer(options = {}) {
226
272
  console.log(` POST /api/terminal/exec — execute terminal commands`);
227
273
  console.log(` POST /api/quiz — send quiz + wait for answer`);
228
274
  console.log(` POST /api/game — send game + wait for result`);
229
- console.log(` POST /api/terminal/exec execute terminal commands`);
230
- console.log(` POST /api/chat/start start chat session`);
231
- console.log(` POST /api/chat/message send chat message`);
232
- console.log(` POST /api/chat/stop stop generation`);
233
- console.log(` POST /api/chat/resume — resume chat session`);
234
- console.log(` GET /api/chat/sessions — list sessions`);
235
- console.log(` GET /health — health check`);
275
+ console.log(` REST /api/chat/* chat (via @shaykec/agent-web)`);
276
+ console.log(` WebSocket /api/ws — chat WebSocket`);
277
+ console.log(` SSE /api/sse — chat SSE`);
278
+ console.log(` GET /health health check`);
236
279
  if (existsSync(CANVAS_DIST)) {
237
280
  console.log(` Static / — canvas app from ${CANVAS_DIST}`);
238
281
  }
@@ -245,21 +288,20 @@ export function startServer(options = {}) {
245
288
 
246
289
  const close = () => {
247
290
  clearInterval(terminalCleanupInterval);
248
- chatSession.closeAll().catch(() => {});
249
- sessionClientMap.clear();
291
+ agentServer.close().catch(() => {});
250
292
  router.cancelWaiters();
251
293
  tierManager.stopHeartbeat();
252
294
  wss.close();
253
295
  server.close();
254
296
  };
255
297
 
256
- return { server, wss, tierManager, router, chatSession, close };
298
+ return { server, wss, tierManager, router, agentServer, close };
257
299
  }
258
300
 
259
301
  /**
260
302
  * Handle HTTP requests.
261
303
  */
262
- function handleRequest(req, res, tierManager, router, options, chatSession, sessionClientMap, createTargetedSend) {
304
+ function handleRequest(req, res, tierManager, router, options, chatMiddleware) {
263
305
  // CORS headers
264
306
  res.setHeader('Access-Control-Allow-Origin', '*');
265
307
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
@@ -274,7 +316,13 @@ function handleRequest(req, res, tierManager, router, options, chatSession, sess
274
316
  const url = new URL(req.url, `http://${req.headers.host}`);
275
317
  const pathname = url.pathname;
276
318
 
277
- // --- SSE endpoint ---
319
+ // --- Chat endpoints delegated to @shaykec/agent-web ---
320
+ if (pathname.startsWith('/api/chat') || pathname === '/api/sse' || pathname === '/api/health') {
321
+ chatMiddleware(req, res);
322
+ return;
323
+ }
324
+
325
+ // --- SSE endpoint (visual/events) ---
278
326
  if (req.method === 'GET' && pathname === '/sse') {
279
327
  handleSse(req, res, tierManager);
280
328
  return;
@@ -336,39 +384,25 @@ function handleRequest(req, res, tierManager, router, options, chatSession, sess
336
384
  return;
337
385
  }
338
386
 
339
- if (req.method === 'POST' && pathname === '/api/terminal/exec') {
340
- handleApiTerminalExec(req, res, router);
341
- return;
342
- }
343
-
344
387
  if (req.method === 'POST' && pathname === '/api/open-extension') {
345
388
  handleApiOpenExtension(res);
346
389
  return;
347
390
  }
348
391
 
349
- // --- Chat API endpoints ---
350
- if (req.method === 'POST' && pathname === '/api/chat/start') {
351
- handleApiChatStart(req, res, chatSession, sessionClientMap, createTargetedSend, options);
352
- return;
353
- }
354
-
355
- if (req.method === 'POST' && pathname === '/api/chat/message') {
356
- handleApiChatMessage(req, res, chatSession);
357
- return;
358
- }
359
-
360
- if (req.method === 'POST' && pathname === '/api/chat/stop') {
361
- handleApiChatStop(req, res, chatSession);
392
+ // --- Modules catalog ---
393
+ if (req.method === 'GET' && pathname === '/api/modules') {
394
+ handleApiModules(res);
362
395
  return;
363
396
  }
364
397
 
365
- if (req.method === 'POST' && pathname === '/api/chat/resume') {
366
- handleApiChatResume(req, res, chatSession, sessionClientMap, createTargetedSend, options);
398
+ // --- Settings ---
399
+ if (req.method === 'GET' && pathname === '/api/settings') {
400
+ handleApiSettingsGet(res);
367
401
  return;
368
402
  }
369
403
 
370
- if (req.method === 'GET' && pathname === '/api/chat/sessions') {
371
- handleApiChatSessions(res, chatSession);
404
+ if (req.method === 'POST' && pathname === '/api/settings') {
405
+ handleApiSettingsPost(req, res);
372
406
  return;
373
407
  }
374
408
 
@@ -481,17 +515,196 @@ function handleApiCapture(req, res) {
481
515
  }
482
516
 
483
517
  /**
484
- * GET /api/progress — return progress data.
518
+ * GET /api/progress — return progress data with achievements.
485
519
  */
486
520
  function handleApiProgress(res, options) {
521
+ let progress;
487
522
  if (options.progressProvider && typeof options.progressProvider.getProgress === 'function') {
488
- const progress = options.progressProvider.getProgress();
489
- sendJson(res, 200, progress);
523
+ progress = options.progressProvider.getProgress();
490
524
  } else {
491
- sendJson(res, 200, { user: { xp: 0 }, modules: {} });
525
+ progress = { user: { xp: 0 }, modules: {} };
526
+ }
527
+
528
+ // Include achievements data from progress file
529
+ const achievements = progress.achievements || { unlocked: [], progress: {} };
530
+ const unlocked = (achievements.unlocked || []).map(a => ({
531
+ id: a.id,
532
+ date: a.date,
533
+ trigger: a.trigger,
534
+ ...a,
535
+ }));
536
+
537
+ // Build near-locked list from progress counters + ACHIEVEMENTS criteria
538
+ const near = [];
539
+ const progressCounters = achievements.progress || {};
540
+ const unlockedIds = new Set(unlocked.map(a => a.id));
541
+ for (const def of ACHIEVEMENTS) {
542
+ if (unlockedIds.has(def.id)) continue;
543
+ const counter = progressCounters[def.id];
544
+ if (counter && counter.current > 0) {
545
+ near.push({
546
+ id: def.id,
547
+ icon: def.icon,
548
+ name: def.name,
549
+ description: def.description,
550
+ category: def.category,
551
+ current: counter.current,
552
+ target: counter.target || def.criteria?.target || 1,
553
+ });
554
+ }
555
+ }
556
+ near.sort((a, b) => (b.current / b.target) - (a.current / a.target));
557
+
558
+ sendJson(res, 200, {
559
+ ...progress,
560
+ achievements: { unlocked, near },
561
+ });
562
+ }
563
+
564
+ /**
565
+ * GET /api/modules — return module catalog from built-in + installed modules.
566
+ */
567
+ function handleApiModules(res) {
568
+ const modulesDir = resolve(__dirname, '..', '..', 'modules');
569
+ const modules = [];
570
+
571
+ try {
572
+ if (existsSync(modulesDir)) {
573
+ const entries = readdirSync(modulesDir);
574
+ for (const entry of entries) {
575
+ const yamlPath = join(modulesDir, entry, 'module.yaml');
576
+ if (!existsSync(yamlPath)) continue;
577
+ try {
578
+ const raw = readFileSync(yamlPath, 'utf-8');
579
+ // Simple YAML parsing for module metadata (no dependency needed)
580
+ const meta = {};
581
+ for (const line of raw.split('\n')) {
582
+ const match = line.match(/^(\w[\w-]*):\s*(.+)$/);
583
+ if (match) {
584
+ let val = match[2].trim();
585
+ if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
586
+ if (val === 'true') val = true;
587
+ else if (val === 'false') val = false;
588
+ else if (/^\d+$/.test(val)) val = parseInt(val, 10);
589
+ meta[match[1]] = val;
590
+ }
591
+ }
592
+ if (meta.slug) {
593
+ modules.push({
594
+ slug: meta.slug,
595
+ title: meta.title || meta.slug,
596
+ description: meta.description || '',
597
+ category: meta.category || 'general',
598
+ difficulty: meta.difficulty || 'beginner',
599
+ icon: meta.icon || null,
600
+ });
601
+ }
602
+ } catch {
603
+ // Skip unparseable modules
604
+ }
605
+ }
606
+ }
607
+ } catch {
608
+ // Directory read failed
609
+ }
610
+
611
+ sendJson(res, 200, { modules });
612
+ }
613
+
614
+ /**
615
+ * Settings file path: ~/.claude-teach/config.yaml
616
+ */
617
+ const SETTINGS_DIR = join(
618
+ process.env.HOME || process.env.USERPROFILE || '/tmp',
619
+ '.claude-teach'
620
+ );
621
+ const SETTINGS_FILE = join(SETTINGS_DIR, 'config.yaml');
622
+
623
+ const DEFAULT_SETTINGS = {
624
+ theme: 'dark',
625
+ accent: 'blue',
626
+ chatPosition: 'left',
627
+ fontSize: 14,
628
+ animations: { confetti: true, transitions: true, diagrams: true },
629
+ };
630
+
631
+ function readSettings() {
632
+ try {
633
+ if (!existsSync(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
634
+ const raw = readFileSync(SETTINGS_FILE, 'utf-8');
635
+ const settings = { ...DEFAULT_SETTINGS };
636
+ for (const line of raw.split('\n')) {
637
+ const match = line.match(/^(\w[\w-]*):\s*(.+)$/);
638
+ if (match) {
639
+ let val = match[2].trim();
640
+ if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
641
+ if (val === 'true') val = true;
642
+ else if (val === 'false') val = false;
643
+ else if (/^\d+$/.test(val)) val = parseInt(val, 10);
644
+ settings[match[1]] = val;
645
+ }
646
+ }
647
+ return settings;
648
+ } catch {
649
+ return { ...DEFAULT_SETTINGS };
650
+ }
651
+ }
652
+
653
+ function writeSettings(settings) {
654
+ try {
655
+ if (!existsSync(SETTINGS_DIR)) mkdirSync(SETTINGS_DIR, { recursive: true });
656
+ const lines = [];
657
+ for (const [key, val] of Object.entries(settings)) {
658
+ if (typeof val === 'object') {
659
+ // Flatten nested objects (animations)
660
+ for (const [k, v] of Object.entries(val)) {
661
+ lines.push(`${key}_${k}: ${v}`);
662
+ }
663
+ } else {
664
+ lines.push(`${key}: ${val}`);
665
+ }
666
+ }
667
+ writeFileSync(SETTINGS_FILE, lines.join('\n') + '\n');
668
+ } catch {
669
+ // Ignore write errors
492
670
  }
493
671
  }
494
672
 
673
+ /**
674
+ * GET /api/settings — return canvas settings.
675
+ */
676
+ function handleApiSettingsGet(res) {
677
+ const settings = readSettings();
678
+ sendJson(res, 200, settings);
679
+ }
680
+
681
+ /**
682
+ * POST /api/settings — save canvas settings (partial update).
683
+ */
684
+ function handleApiSettingsPost(req, res) {
685
+ let body = '';
686
+ req.on('data', (chunk) => { body += chunk; });
687
+ req.on('end', () => {
688
+ try {
689
+ const patch = JSON.parse(body);
690
+ const current = readSettings();
691
+ // Merge patch into current settings
692
+ const updated = { ...current };
693
+ for (const [key, val] of Object.entries(patch)) {
694
+ if (key === 'animations' && typeof val === 'object') {
695
+ updated.animations = { ...current.animations, ...val };
696
+ } else if (DEFAULT_SETTINGS[key] !== undefined) {
697
+ updated[key] = val;
698
+ }
699
+ }
700
+ writeSettings(updated);
701
+ sendJson(res, 200, updated);
702
+ } catch {
703
+ sendJson(res, 400, { error: 'Invalid JSON' });
704
+ }
705
+ });
706
+ }
707
+
495
708
  /**
496
709
  * GET /api/events — poll queued user events.
497
710
  */
@@ -767,18 +980,7 @@ function handleApiOpenExtension(res) {
767
980
  return;
768
981
  }
769
982
 
770
- const platform = process.platform;
771
- if (platform === 'darwin') {
772
- exec(`open "${extensionDir}"`);
773
- exec('open -a "Google Chrome" "chrome://extensions"');
774
- } else if (platform === 'linux') {
775
- exec(`xdg-open "${extensionDir}"`);
776
- exec('google-chrome "chrome://extensions" 2>/dev/null || chromium-browser "chrome://extensions" 2>/dev/null');
777
- } else if (platform === 'win32') {
778
- exec(`explorer "${extensionDir}"`);
779
- exec('start chrome "chrome://extensions"');
780
- }
781
-
983
+ // Return the path only — no longer auto-opens Finder/Chrome.
782
984
  sendJson(res, 200, { ok: true, path: extensionDir });
783
985
  }
784
986
 
@@ -829,101 +1031,6 @@ function handleApiTerminalExec(req, res, router) {
829
1031
  });
830
1032
  }
831
1033
 
832
- // --- Chat API handlers ---
833
-
834
- function handleApiChatStart(req, res, chatSession, sessionClientMap, createTargetedSend, options) {
835
- readBody(req, async (err, body) => {
836
- if (err) body = {};
837
-
838
- // clientId is sent by the browser so we can target responses back
839
- const clientId = body?.clientId;
840
-
841
- try {
842
- const sessionId = await chatSession.createSession({
843
- cwd: body?.cwd || process.cwd(),
844
- pluginDir: body?.pluginDir || options.pluginDir,
845
- onMessage: clientId ? createTargetedSend(clientId) : createTargetedSend('broadcast'),
846
- });
847
- if (clientId) {
848
- sessionClientMap.set(sessionId, clientId);
849
- }
850
- sendJson(res, 200, { ok: true, sessionId });
851
- } catch (startErr) {
852
- sendJson(res, 500, { error: startErr.message });
853
- }
854
- });
855
- }
856
-
857
- function handleApiChatMessage(req, res, chatSession) {
858
- readBody(req, (err, body) => {
859
- if (err || !body?.text) {
860
- sendJson(res, 400, { error: 'Missing "text" in request body' });
861
- return;
862
- }
863
-
864
- const sessionId = body.sessionId;
865
- if (!sessionId || !chatSession.isActive(sessionId)) {
866
- sendJson(res, 400, { error: 'No active chat session. Provide a valid "sessionId".' });
867
- return;
868
- }
869
-
870
- sendJson(res, 200, { ok: true });
871
-
872
- chatSession.sendMessage(sessionId, body.text).catch((sendErr) => {
873
- console.error('[chat] sendMessage error:', sendErr.message);
874
- });
875
- });
876
- }
877
-
878
- function handleApiChatStop(req, res, chatSession) {
879
- readBody(req, (err, body) => {
880
- const sessionId = body?.sessionId;
881
- if (!sessionId) {
882
- sendJson(res, 400, { error: 'Missing "sessionId" in request body' });
883
- return;
884
- }
885
- chatSession.stop(sessionId).then(() => {
886
- sendJson(res, 200, { ok: true });
887
- }).catch((stopErr) => {
888
- sendJson(res, 500, { error: stopErr.message });
889
- });
890
- });
891
- }
892
-
893
- function handleApiChatResume(req, res, chatSession, sessionClientMap, createTargetedSend, options) {
894
- readBody(req, async (err, body) => {
895
- if (err || !body?.sessionId) {
896
- sendJson(res, 400, { error: 'Missing "sessionId" in request body' });
897
- return;
898
- }
899
-
900
- const clientId = body?.clientId;
901
-
902
- try {
903
- await chatSession.resumeSession(body.sessionId, {
904
- cwd: body?.cwd || process.cwd(),
905
- pluginDir: body?.pluginDir || options.pluginDir,
906
- onMessage: clientId ? createTargetedSend(clientId) : createTargetedSend('broadcast'),
907
- });
908
- if (clientId) {
909
- sessionClientMap.set(body.sessionId, clientId);
910
- }
911
- sendJson(res, 200, { ok: true, sessionId: body.sessionId });
912
- } catch (resumeErr) {
913
- sendJson(res, 500, { error: resumeErr.message });
914
- }
915
- });
916
- }
917
-
918
- async function handleApiChatSessions(res, chatSession) {
919
- try {
920
- const sessions = await chatSession.listSessions(process.cwd());
921
- sendJson(res, 200, { sessions });
922
- } catch (listErr) {
923
- sendJson(res, 500, { error: listErr.message });
924
- }
925
- }
926
-
927
1034
  // --- Helpers ---
928
1035
 
929
1036
  function readBody(req, callback) {
@@ -958,6 +1065,15 @@ function ensureDir(dirPath) {
958
1065
 
959
1066
  // --- Standalone execution ---
960
1067
  if (process.argv[1] && process.argv[1].includes('bridge/src/server.js')) {
1068
+ // Prevent unhandled errors from crashing the server
1069
+ process.on('uncaughtException', (err) => {
1070
+ console.error('[bridge] Uncaught exception (server kept alive):', err.message);
1071
+ });
1072
+ process.on('unhandledRejection', (err) => {
1073
+ console.error('[bridge] Unhandled rejection (server kept alive):', err?.message || err);
1074
+ });
1075
+
961
1076
  const port = parseInt(process.env.PORT || process.argv[2] || DEFAULT_PORT, 10);
962
- startServer({ port });
1077
+ const pluginDir = resolve(__dirname, '..', '..', 'plugin');
1078
+ startServer({ port, pluginDir });
963
1079
  }
@@ -1 +0,0 @@
1
- import{ap as o,aq as n}from"./index-DYNtb52W.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-Dcov7eRi.js";import{_ as i}from"./index-DYNtb52W.js";import"./chunk-FMBD7UC4-EfGA9ufe.js";import"./chunk-55IACEB6-CTdcUQSV.js";import"./chunk-QN33PNHL-Cu6V1xBU.js";var p={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{p as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-Dcov7eRi.js";import{_ as i}from"./index-DYNtb52W.js";import"./chunk-FMBD7UC4-EfGA9ufe.js";import"./chunk-55IACEB6-CTdcUQSV.js";import"./chunk-QN33PNHL-Cu6V1xBU.js";var p={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{p as diagram};
@@ -1 +0,0 @@
1
- import{b as r}from"./_baseUniq-EF6Y2_Wm.js";var e=4;function a(o){return r(o,e)}export{a as c};