@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.
- package/canvas-dist/assets/{_basePickBy-BOTBlJNd.js → _basePickBy-CWoeT3J7.js} +1 -1
- package/canvas-dist/assets/{_baseUniq-EF6Y2_Wm.js → _baseUniq-Dtuvtwtn.js} +1 -1
- package/canvas-dist/assets/{arc-C_vIirh2.js → arc-YYWnrNJU.js} +1 -1
- package/canvas-dist/assets/{architectureDiagram-VXUJARFQ-EvM6tQ7I.js → architectureDiagram-VXUJARFQ-CegbV-RR.js} +1 -1
- package/canvas-dist/assets/{blockDiagram-VD42YOAC-B_rbZyqc.js → blockDiagram-VD42YOAC-C2e_j6ry.js} +1 -1
- package/canvas-dist/assets/{c4Diagram-YG6GDRKO-J9PHecY3.js → c4Diagram-YG6GDRKO-rIpnAud9.js} +1 -1
- package/canvas-dist/assets/channel-BzJVlie3.js +1 -0
- package/canvas-dist/assets/{chunk-4BX2VUAB-DjcN96Mk.js → chunk-4BX2VUAB-CpZGetnU.js} +1 -1
- package/canvas-dist/assets/{chunk-55IACEB6-CTdcUQSV.js → chunk-55IACEB6-L0OhcFdd.js} +1 -1
- package/canvas-dist/assets/{chunk-B4BG7PRW-Dcov7eRi.js → chunk-B4BG7PRW-Cv9vsAzg.js} +1 -1
- package/canvas-dist/assets/{chunk-DI55MBZ5-DUJCBZzM.js → chunk-DI55MBZ5-B3p1mU43.js} +1 -1
- package/canvas-dist/assets/{chunk-FMBD7UC4-EfGA9ufe.js → chunk-FMBD7UC4-JCLAHw5x.js} +1 -1
- package/canvas-dist/assets/{chunk-QN33PNHL-Cu6V1xBU.js → chunk-QN33PNHL-C9arKEVq.js} +1 -1
- package/canvas-dist/assets/{chunk-QZHKN3VN-avF3sH_r.js → chunk-QZHKN3VN-Bs1r3d9U.js} +1 -1
- package/canvas-dist/assets/{chunk-TZMSLE5B-CkWW-qpk.js → chunk-TZMSLE5B-_Ye6r84Y.js} +1 -1
- package/canvas-dist/assets/classDiagram-2ON5EDUG-BTs-zEmB.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-BTs-zEmB.js +1 -0
- package/canvas-dist/assets/clone-CXEfuXmc.js +1 -0
- package/canvas-dist/assets/{cose-bilkent-S5V4N54A-DDE4zf7X.js → cose-bilkent-S5V4N54A-2O2oovOj.js} +1 -1
- package/canvas-dist/assets/{dagre-6UL2VRFP-BD6MGb7B.js → dagre-6UL2VRFP-gRmGLrEW.js} +1 -1
- package/canvas-dist/assets/{diagram-PSM6KHXK-yyu-ytzf.js → diagram-PSM6KHXK-B7Li-xxw.js} +1 -1
- package/canvas-dist/assets/{diagram-QEK2KX5R-B_H957Uf.js → diagram-QEK2KX5R-B_NNUAm3.js} +1 -1
- package/canvas-dist/assets/{diagram-S2PKOQOG-DuebuBVv.js → diagram-S2PKOQOG-NcK-KHaA.js} +1 -1
- package/canvas-dist/assets/{erDiagram-Q2GNP2WA-AxqPt6IZ.js → erDiagram-Q2GNP2WA-CG7dqzk3.js} +1 -1
- package/canvas-dist/assets/{flowDiagram-NV44I4VS-mDhW3D3Q.js → flowDiagram-NV44I4VS-CBzCj5D6.js} +1 -1
- package/canvas-dist/assets/{ganttDiagram-JELNMOA3-sA8pHJPp.js → ganttDiagram-JELNMOA3-CHw-4qJC.js} +1 -1
- package/canvas-dist/assets/{gitGraphDiagram-V2S2FVAM-CvLzvhKr.js → gitGraphDiagram-V2S2FVAM-Dqrc4wUs.js} +1 -1
- package/canvas-dist/assets/{graph-BVZqMrwW.js → graph-X9Kzu-pf.js} +1 -1
- package/canvas-dist/assets/{index-CF3qc2Xb.js → index-BQFKo-II.js} +1 -1
- package/canvas-dist/assets/index-DJ49c6u-.js +426 -0
- package/canvas-dist/assets/{infoDiagram-HS3SLOUP-D1Kg3Q9d.js → infoDiagram-HS3SLOUP-CflnZPsm.js} +1 -1
- package/canvas-dist/assets/{journeyDiagram-XKPGCS4Q-D7ogbx9z.js → journeyDiagram-XKPGCS4Q-D2gkCipQ.js} +1 -1
- package/canvas-dist/assets/{kanban-definition-3W4ZIXB7-CDcnICM9.js → kanban-definition-3W4ZIXB7-CtLLz4o8.js} +1 -1
- package/canvas-dist/assets/{layout-CuaK7i3M.js → layout-CjvV_Dms.js} +1 -1
- package/canvas-dist/assets/{linear-CLSTOJ0g.js → linear-D3cIYHoS.js} +1 -1
- package/canvas-dist/assets/{mindmap-definition-VGOIOE7T-TrK7CIKt.js → mindmap-definition-VGOIOE7T-DSgjVg-P.js} +1 -1
- package/canvas-dist/assets/{pieDiagram-ADFJNKIX-BcIKTRbi.js → pieDiagram-ADFJNKIX-B_lYaGFj.js} +1 -1
- package/canvas-dist/assets/{quadrantDiagram-AYHSOK5B-EOHXFGoQ.js → quadrantDiagram-AYHSOK5B-DLZLTJe3.js} +1 -1
- package/canvas-dist/assets/{requirementDiagram-UZGBJVZJ-CJ8lImGs.js → requirementDiagram-UZGBJVZJ-CZE26rhL.js} +1 -1
- package/canvas-dist/assets/{sankeyDiagram-TZEHDZUN-4cANY87E.js → sankeyDiagram-TZEHDZUN-DQMRJAPV.js} +1 -1
- package/canvas-dist/assets/{sequenceDiagram-WL72ISMW-D9HrEsci.js → sequenceDiagram-WL72ISMW-BY723FEn.js} +1 -1
- package/canvas-dist/assets/{stateDiagram-FKZM4ZOC-qVbMjauZ.js → stateDiagram-FKZM4ZOC-C_UdOFhy.js} +1 -1
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-DXIiFh0L.js +1 -0
- package/canvas-dist/assets/{timeline-definition-IT6M3QCI-DDBlkydm.js → timeline-definition-IT6M3QCI-DkrJqww0.js} +1 -1
- package/canvas-dist/assets/{treemap-GDKQZRPO-D4a8udjO.js → treemap-GDKQZRPO-B-6bMZqD.js} +1 -1
- package/canvas-dist/assets/{xychartDiagram-PRI3JC2R-DteXAAAu.js → xychartDiagram-PRI3JC2R-DkBhUy_D.js} +1 -1
- package/canvas-dist/index.html +1 -1
- package/package.json +3 -2
- package/src/server.e2e.test.js +10 -41
- package/src/server.js +302 -186
- package/canvas-dist/assets/channel-saCUO1KA.js +0 -1
- package/canvas-dist/assets/classDiagram-2ON5EDUG-CBLbQwHx.js +0 -1
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CBLbQwHx.js +0 -1
- package/canvas-dist/assets/clone-DXnda9BY.js +0 -1
- package/canvas-dist/assets/index-DYNtb52W.js +0 -426
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-MT16RLO4.js +0 -1
- package/src/claude-session.js +0 -414
- 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 {
|
|
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
|
-
//
|
|
90
|
-
const
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
126
|
-
const sessionId = envelope.payload?.sessionId;
|
|
127
|
-
if (sessionId) {
|
|
128
|
-
chatSession.stop(sessionId).catch(() => {});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
};
|
|
138
|
+
});
|
|
132
139
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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(`
|
|
230
|
-
console.log(`
|
|
231
|
-
console.log(`
|
|
232
|
-
console.log(`
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
// ---
|
|
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
|
-
// ---
|
|
350
|
-
if (req.method === '
|
|
351
|
-
|
|
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
|
-
|
|
366
|
-
|
|
398
|
+
// --- Settings ---
|
|
399
|
+
if (req.method === 'GET' && pathname === '/api/settings') {
|
|
400
|
+
handleApiSettingsGet(res);
|
|
367
401
|
return;
|
|
368
402
|
}
|
|
369
403
|
|
|
370
|
-
if (req.method === '
|
|
371
|
-
|
|
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
|
-
|
|
489
|
-
sendJson(res, 200, progress);
|
|
523
|
+
progress = options.progressProvider.getProgress();
|
|
490
524
|
} else {
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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};
|