claudeck 1.3.1 → 1.4.1
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/README.md +13 -9
- package/db/sqlite.js +1697 -0
- package/db.js +3 -1645
- package/package.json +2 -1
- package/plugins/claude-editor/manifest.json +10 -0
- package/plugins/linear/manifest.json +10 -0
- package/plugins/repos/manifest.json +10 -0
- package/public/css/ui/messages.css +25 -0
- package/public/css/ui/right-panel.css +207 -0
- package/public/css/ui/settings.css +75 -0
- package/public/index.html +7 -0
- package/public/js/components/settings-modal.js +65 -0
- package/public/js/core/api.js +23 -6
- package/public/js/core/events.js +11 -0
- package/public/js/core/plugin-loader.js +96 -11
- package/public/js/core/store.js +11 -0
- package/public/js/core/ws.js +12 -0
- package/public/js/features/chat.js +4 -0
- package/public/js/features/sessions.js +102 -10
- package/public/js/main.js +1 -0
- package/public/js/panels/assistant-bot.js +16 -0
- package/public/js/panels/dev-docs.js +2 -2
- package/public/js/panels/memory.js +1 -0
- package/public/js/ui/context-gauge.js +10 -1
- package/public/js/ui/header-dropdowns.js +30 -0
- package/public/js/ui/input-meta.js +13 -6
- package/public/js/ui/max-turns.js +6 -3
- package/public/js/ui/messages.js +42 -0
- package/public/js/ui/model-selector.js +1 -0
- package/public/js/ui/parallel.js +2 -4
- package/public/js/ui/permissions.js +1 -0
- package/public/js/ui/tab-sdk.js +395 -176
- package/public/style.css +1 -0
- package/server/agent-loop.js +26 -26
- package/server/memory-extractor.js +4 -4
- package/server/memory-injector.js +11 -11
- package/server/memory-optimizer.js +19 -15
- package/server/notification-logger.js +5 -5
- package/server/orchestrator.js +15 -15
- package/server/push-sender.js +2 -2
- package/server/routes/agents.js +2 -2
- package/server/routes/marketplace.js +316 -0
- package/server/routes/memory.js +20 -20
- package/server/routes/messages.js +41 -10
- package/server/routes/notifications.js +20 -20
- package/server/routes/sessions.js +17 -17
- package/server/routes/stats.js +37 -37
- package/server/routes/worktrees.js +9 -9
- package/server/summarizer.js +3 -3
- package/server/ws-handler.js +163 -58
- package/server.js +20 -2
- package/plugins/event-stream/client.css +0 -207
- package/plugins/event-stream/client.js +0 -271
- package/plugins/sudoku/client.css +0 -196
- package/plugins/sudoku/client.js +0 -329
- package/plugins/tasks/client.css +0 -414
- package/plugins/tasks/client.js +0 -394
- package/plugins/tasks/server.js +0 -116
- package/plugins/tic-tac-toe/client.css +0 -167
- package/plugins/tic-tac-toe/client.js +0 -241
package/server/ws-handler.js
CHANGED
|
@@ -42,6 +42,37 @@ import { runAgent } from "./agent-loop.js";
|
|
|
42
42
|
import { runOrchestrator } from "./orchestrator.js";
|
|
43
43
|
import { runDag } from "./dag-executor.js";
|
|
44
44
|
|
|
45
|
+
// ── Session broadcast rooms ───────────────────────────────────────────────
|
|
46
|
+
// Maps sessionId → Set of WebSocket clients watching that session
|
|
47
|
+
const sessionRooms = new Map();
|
|
48
|
+
|
|
49
|
+
function joinRoom(sessionId, ws) {
|
|
50
|
+
if (!sessionRooms.has(sessionId)) sessionRooms.set(sessionId, new Set());
|
|
51
|
+
sessionRooms.get(sessionId).add(ws);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function leaveRoom(ws) {
|
|
55
|
+
for (const [sessionId, clients] of sessionRooms) {
|
|
56
|
+
clients.delete(ws);
|
|
57
|
+
if (clients.size === 0) sessionRooms.delete(sessionId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function broadcastToSession(sessionId, message, excludeWs = null) {
|
|
62
|
+
const clients = sessionRooms.get(sessionId);
|
|
63
|
+
if (!clients) return;
|
|
64
|
+
const payload = JSON.stringify({ ...message, _broadcast: true });
|
|
65
|
+
for (const client of clients) {
|
|
66
|
+
if (client !== excludeWs && client.readyState === 1) {
|
|
67
|
+
client.send(payload);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Global pending approvals — enables cross-connection approval (any client can approve)
|
|
73
|
+
// Key: approval ID, Value: { resolve, timer, toolInput, ws, localMap, sessionId }
|
|
74
|
+
const globalPendingApprovals = new Map();
|
|
75
|
+
|
|
45
76
|
// Tools that are read-only and safe to auto-approve in "confirmDangerous" mode
|
|
46
77
|
export const READ_ONLY_TOOLS = new Set([
|
|
47
78
|
"Read", "Glob", "Grep", "WebSearch", "WebFetch", "Agent",
|
|
@@ -87,7 +118,7 @@ export function getActiveSessionIds() {
|
|
|
87
118
|
* Creates a canUseTool callback that sends permission requests over WebSocket
|
|
88
119
|
* AND Telegram (for AFK approval). Whichever channel responds first wins.
|
|
89
120
|
*/
|
|
90
|
-
export function makeCanUseTool(ws, pendingApprovals, permissionMode, chatId, sessionTitle) {
|
|
121
|
+
export function makeCanUseTool(ws, pendingApprovals, permissionMode, chatId, sessionTitle, getSessionId = null) {
|
|
91
122
|
return async (toolName, toolInput, options) => {
|
|
92
123
|
// Bypass mode — auto-approve everything
|
|
93
124
|
if (permissionMode === "bypass") {
|
|
@@ -109,6 +140,8 @@ export function makeCanUseTool(ws, pendingApprovals, permissionMode, chatId, ses
|
|
|
109
140
|
}
|
|
110
141
|
|
|
111
142
|
ws.send(JSON.stringify(payload));
|
|
143
|
+
const permSid = getSessionId?.();
|
|
144
|
+
if (permSid) broadcastToSession(permSid, payload, ws);
|
|
112
145
|
|
|
113
146
|
// Also send to Telegram for AFK approval
|
|
114
147
|
if (telegramEnabled()) {
|
|
@@ -122,8 +155,11 @@ export function makeCanUseTool(ws, pendingApprovals, permissionMode, chatId, ses
|
|
|
122
155
|
const timeoutMs = getApprovalTimeoutMs();
|
|
123
156
|
|
|
124
157
|
return new Promise((resolve) => {
|
|
158
|
+
const permSidForApproval = getSessionId?.();
|
|
159
|
+
|
|
125
160
|
const timer = setTimeout(() => {
|
|
126
161
|
pendingApprovals.delete(id);
|
|
162
|
+
globalPendingApprovals.delete(id);
|
|
127
163
|
markTelegramMessageResolved(id, "timeout").catch(() => {});
|
|
128
164
|
resolve({ behavior: "deny", message: `Approval timed out (${Math.round(timeoutMs / 60000)}min)` });
|
|
129
165
|
}, timeoutMs);
|
|
@@ -133,12 +169,14 @@ export function makeCanUseTool(ws, pendingApprovals, permissionMode, chatId, ses
|
|
|
133
169
|
options.signal.addEventListener("abort", () => {
|
|
134
170
|
clearTimeout(timer);
|
|
135
171
|
pendingApprovals.delete(id);
|
|
172
|
+
globalPendingApprovals.delete(id);
|
|
136
173
|
markTelegramMessageResolved(id, "abort").catch(() => {});
|
|
137
174
|
resolve({ behavior: "deny", message: "Aborted by user" });
|
|
138
175
|
}, { once: true });
|
|
139
176
|
}
|
|
140
177
|
|
|
141
178
|
pendingApprovals.set(id, { resolve, timer, toolInput, ws });
|
|
179
|
+
globalPendingApprovals.set(id, { resolve, timer, toolInput, ws, localMap: pendingApprovals, sessionId: permSidForApproval });
|
|
142
180
|
});
|
|
143
181
|
};
|
|
144
182
|
}
|
|
@@ -164,17 +202,17 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
|
|
|
164
202
|
const sKey = chatId ? `${ourSid}::${chatId}` : ourSid;
|
|
165
203
|
sessionIds.set(sKey, claudeSessionId);
|
|
166
204
|
|
|
167
|
-
if (!getSession(ourSid)) {
|
|
168
|
-
createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
|
|
205
|
+
if (!await getSession(ourSid)) {
|
|
206
|
+
await createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
|
|
169
207
|
if (isWorkflow) {
|
|
170
|
-
updateSessionTitle(ourSid, `Workflow: ${stepLabel}`);
|
|
208
|
+
await updateSessionTitle(ourSid, `Workflow: ${stepLabel}`);
|
|
171
209
|
}
|
|
172
210
|
} else {
|
|
173
|
-
updateClaudeSessionId(ourSid, claudeSessionId);
|
|
211
|
+
await updateClaudeSessionId(ourSid, claudeSessionId);
|
|
174
212
|
}
|
|
175
213
|
|
|
176
214
|
if (chatId) {
|
|
177
|
-
setClaudeSession(ourSid, chatId, claudeSessionId);
|
|
215
|
+
await setClaudeSession(ourSid, chatId, claudeSessionId);
|
|
178
216
|
}
|
|
179
217
|
|
|
180
218
|
wsSend({ type: "session", sessionId: ourSid });
|
|
@@ -185,12 +223,12 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
|
|
|
185
223
|
// user message saved by caller for chat; for workflow, save with step label
|
|
186
224
|
}
|
|
187
225
|
if (isWorkflow) {
|
|
188
|
-
addMessage(resolvedSid, "user", JSON.stringify({ text: msgText }), null, wfMeta);
|
|
226
|
+
await addMessage(resolvedSid, "user", JSON.stringify({ text: msgText }), null, wfMeta);
|
|
189
227
|
}
|
|
190
228
|
|
|
191
229
|
if (!isWorkflow) {
|
|
192
230
|
// Auto-set session title from first user message
|
|
193
|
-
const existingSession = getSession(ourSid);
|
|
231
|
+
const existingSession = await getSession(ourSid);
|
|
194
232
|
if (existingSession && !existingSession.title) {
|
|
195
233
|
// Title is set by caller
|
|
196
234
|
}
|
|
@@ -204,12 +242,12 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
|
|
|
204
242
|
if (block.type === "text" && block.text) {
|
|
205
243
|
wsSend({ type: "text", text: block.text });
|
|
206
244
|
if (resolvedSid) {
|
|
207
|
-
addMessage(resolvedSid, "assistant", JSON.stringify({ text: block.text }), chatId || null, wfMeta);
|
|
245
|
+
await addMessage(resolvedSid, "assistant", JSON.stringify({ text: block.text }), chatId || null, wfMeta);
|
|
208
246
|
}
|
|
209
247
|
} else if (block.type === "tool_use") {
|
|
210
248
|
wsSend({ type: "tool", id: block.id, name: block.name, input: block.input });
|
|
211
249
|
if (resolvedSid) {
|
|
212
|
-
addMessage(resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), chatId || null, wfMeta);
|
|
250
|
+
await addMessage(resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), chatId || null, wfMeta);
|
|
213
251
|
}
|
|
214
252
|
}
|
|
215
253
|
}
|
|
@@ -231,7 +269,7 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
|
|
|
231
269
|
([, v]) => v === claudeSessionId
|
|
232
270
|
)?.[0];
|
|
233
271
|
if (sid) {
|
|
234
|
-
addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: "success", isError: 0, cacheReadTokens, cacheCreationTokens });
|
|
272
|
+
await addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: "success", isError: 0, cacheReadTokens, cacheCreationTokens });
|
|
235
273
|
}
|
|
236
274
|
|
|
237
275
|
wsSend({
|
|
@@ -239,7 +277,7 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
|
|
|
239
277
|
duration_ms: sdkMsg.duration_ms,
|
|
240
278
|
num_turns: sdkMsg.num_turns,
|
|
241
279
|
cost_usd: sdkMsg.total_cost_usd,
|
|
242
|
-
totalCost: getTotalCost(),
|
|
280
|
+
totalCost: await getTotalCost(),
|
|
243
281
|
input_tokens: inputTokens,
|
|
244
282
|
output_tokens: outputTokens,
|
|
245
283
|
cache_read_tokens: cacheReadTokens,
|
|
@@ -251,7 +289,7 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
|
|
|
251
289
|
lastMetrics = { durationMs, costUsd, inputTokens, outputTokens, model, turns: numTurns, isError: false };
|
|
252
290
|
|
|
253
291
|
if (resolvedSid) {
|
|
254
|
-
addMessage(resolvedSid, "result", JSON.stringify({
|
|
292
|
+
await addMessage(resolvedSid, "result", JSON.stringify({
|
|
255
293
|
duration_ms: sdkMsg.duration_ms,
|
|
256
294
|
num_turns: sdkMsg.num_turns,
|
|
257
295
|
cost_usd: sdkMsg.total_cost_usd,
|
|
@@ -273,8 +311,8 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
|
|
|
273
311
|
([, v]) => v === claudeSessionId
|
|
274
312
|
)?.[0];
|
|
275
313
|
if (sid) {
|
|
276
|
-
addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: sdkMsg.subtype, isError: 1, cacheReadTokens, cacheCreationTokens });
|
|
277
|
-
addMessage(sid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype, duration_ms: durationMs, cost_usd: costUsd, model }), chatId || null, wfMeta);
|
|
314
|
+
await addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: sdkMsg.subtype, isError: 1, cacheReadTokens, cacheCreationTokens });
|
|
315
|
+
await addMessage(sid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype, duration_ms: durationMs, cost_usd: costUsd, model }), chatId || null, wfMeta);
|
|
278
316
|
}
|
|
279
317
|
lastMetrics = { durationMs, costUsd, inputTokens, outputTokens, model, turns: numTurns, isError: true, error: errMsg };
|
|
280
318
|
wsSend({ type: "error", error: errMsg });
|
|
@@ -303,7 +341,7 @@ export async function processSdkStream(q, { ws, wsSend, sessionIds, clientSid, c
|
|
|
303
341
|
content: text.slice(0, 10000),
|
|
304
342
|
isError: block.is_error || false,
|
|
305
343
|
};
|
|
306
|
-
addMessage(resolvedSid, "tool_result", JSON.stringify(dbPayload), chatId || null, wfMeta);
|
|
344
|
+
await addMessage(resolvedSid, "tool_result", JSON.stringify(dbPayload), chatId || null, wfMeta);
|
|
307
345
|
}
|
|
308
346
|
}
|
|
309
347
|
}
|
|
@@ -344,6 +382,7 @@ export function handleClose({ activeQueries, pendingApprovals }) {
|
|
|
344
382
|
for (const [id, { resolve, timer }] of pendingApprovals) {
|
|
345
383
|
clearTimeout(timer);
|
|
346
384
|
resolve({ behavior: "deny", message: "Client disconnected" });
|
|
385
|
+
globalPendingApprovals.delete(id);
|
|
347
386
|
}
|
|
348
387
|
pendingApprovals.clear();
|
|
349
388
|
}
|
|
@@ -361,21 +400,48 @@ export function handleAbort(msg, { activeQueries, pendingApprovals }) {
|
|
|
361
400
|
for (const [id, { resolve, timer }] of pendingApprovals) {
|
|
362
401
|
clearTimeout(timer);
|
|
363
402
|
resolve({ behavior: "deny", message: "Aborted by user" });
|
|
403
|
+
globalPendingApprovals.delete(id);
|
|
364
404
|
}
|
|
365
405
|
pendingApprovals.clear();
|
|
366
406
|
}
|
|
367
407
|
|
|
368
408
|
// ── Extracted handler: permission response ────────────────────────────────
|
|
369
|
-
export function handlePermissionResponse(msg, { pendingApprovals }) {
|
|
370
|
-
|
|
409
|
+
export function handlePermissionResponse(msg, { pendingApprovals, ws: responderWs }) {
|
|
410
|
+
// Check local first (same connection that initiated the request)
|
|
411
|
+
let pending = pendingApprovals.get(msg.id);
|
|
412
|
+
let isLocal = !!pending;
|
|
413
|
+
|
|
414
|
+
// If not found locally, check global (cross-connection approval from another client)
|
|
415
|
+
if (!pending) {
|
|
416
|
+
pending = globalPendingApprovals.get(msg.id);
|
|
417
|
+
}
|
|
418
|
+
|
|
371
419
|
if (pending) {
|
|
420
|
+
// Get sessionId before deleting (local pending doesn't have it, global does)
|
|
421
|
+
const sessionId = pending.sessionId || globalPendingApprovals.get(msg.id)?.sessionId;
|
|
422
|
+
|
|
372
423
|
clearTimeout(pending.timer);
|
|
373
424
|
pendingApprovals.delete(msg.id);
|
|
425
|
+
globalPendingApprovals.delete(msg.id);
|
|
426
|
+
// Also clean up from the originating connection's local map
|
|
427
|
+
if (!isLocal && pending.localMap) pending.localMap.delete(msg.id);
|
|
428
|
+
|
|
374
429
|
if (msg.behavior === "allow") {
|
|
375
430
|
pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
|
|
376
431
|
} else {
|
|
377
432
|
pending.resolve({ behavior: "deny", message: "Denied by user" });
|
|
378
433
|
}
|
|
434
|
+
|
|
435
|
+
// Broadcast permission_response_external to dismiss modals on all other clients
|
|
436
|
+
if (sessionId) {
|
|
437
|
+
broadcastToSession(sessionId, {
|
|
438
|
+
type: "permission_response_external",
|
|
439
|
+
id: msg.id,
|
|
440
|
+
behavior: msg.behavior,
|
|
441
|
+
source: "broadcast",
|
|
442
|
+
}, responderWs);
|
|
443
|
+
}
|
|
444
|
+
|
|
379
445
|
// Update Telegram message to show it was resolved via web
|
|
380
446
|
markTelegramMessageResolved(msg.id, msg.behavior === "allow" ? "allow" : "deny").catch(() => {});
|
|
381
447
|
}
|
|
@@ -389,6 +455,7 @@ export async function handleWorkflow(msg, { ws, sessionIds, activeQueries, pendi
|
|
|
389
455
|
function wfSend(payload) {
|
|
390
456
|
if (ws.readyState !== 1) return;
|
|
391
457
|
ws.send(JSON.stringify(payload));
|
|
458
|
+
if (clientSid) broadcastToSession(clientSid, payload, ws);
|
|
392
459
|
}
|
|
393
460
|
|
|
394
461
|
wfSend({ type: "workflow_started", workflow: { id: workflow.id, title: workflow.title, steps: workflow.steps.map((s) => s.label) } });
|
|
@@ -425,7 +492,7 @@ export async function handleWorkflow(msg, { ws, sessionIds, activeQueries, pendi
|
|
|
425
492
|
};
|
|
426
493
|
|
|
427
494
|
if (!useBypass && !usePlan) {
|
|
428
|
-
stepOpts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, null, `Workflow: ${workflow.title}
|
|
495
|
+
stepOpts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, null, `Workflow: ${workflow.title}`, () => clientSid);
|
|
429
496
|
}
|
|
430
497
|
if (wfModel) stepOpts.model = resolveModel(wfModel);
|
|
431
498
|
|
|
@@ -512,6 +579,7 @@ export async function handleAgentChain(msg, { ws, sessionIds, activeQueries, pen
|
|
|
512
579
|
function chainSend(payload) {
|
|
513
580
|
if (ws.readyState !== 1) return;
|
|
514
581
|
ws.send(JSON.stringify(payload));
|
|
582
|
+
if (clientSid) broadcastToSession(clientSid, payload, ws);
|
|
515
583
|
}
|
|
516
584
|
|
|
517
585
|
chainSend({
|
|
@@ -628,7 +696,9 @@ export async function handleOrchestrate(msg, { ws, sessionIds, activeQueries, pe
|
|
|
628
696
|
try {
|
|
629
697
|
agents = JSON.parse(await readFile(configPath("agents.json"), "utf-8"));
|
|
630
698
|
} catch {
|
|
631
|
-
|
|
699
|
+
const errPayload = { type: "error", error: "Failed to load agents" };
|
|
700
|
+
ws.send(JSON.stringify(errPayload));
|
|
701
|
+
if (clientSid) broadcastToSession(clientSid, errPayload, ws);
|
|
632
702
|
return;
|
|
633
703
|
}
|
|
634
704
|
|
|
@@ -654,11 +724,13 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
654
724
|
|
|
655
725
|
// Handle /remember command — save memory and respond without calling Claude
|
|
656
726
|
if (message && message.trim().toLowerCase().startsWith('/remember ') && cwd) {
|
|
657
|
-
const result = parseRememberCommand(message, cwd, clientSid);
|
|
727
|
+
const result = await parseRememberCommand(message, cwd, clientSid);
|
|
658
728
|
function remSend(payload) {
|
|
659
729
|
if (ws.readyState !== 1) return;
|
|
660
730
|
if (chatId) payload.chatId = chatId;
|
|
731
|
+
if (clientSid) payload.sessionId = clientSid;
|
|
661
732
|
ws.send(JSON.stringify(payload));
|
|
733
|
+
if (clientSid) broadcastToSession(clientSid, payload, ws);
|
|
662
734
|
}
|
|
663
735
|
if (result) {
|
|
664
736
|
remSend({ type: "text", text: result.saved
|
|
@@ -677,8 +749,8 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
677
749
|
const sessionKey = chatId ? `${clientSid}::${chatId}` : clientSid;
|
|
678
750
|
const resumeId = clientSid ? sessionIds.get(sessionKey) : undefined;
|
|
679
751
|
|
|
680
|
-
if (clientSid && getSession(clientSid)) {
|
|
681
|
-
touchSession(clientSid);
|
|
752
|
+
if (clientSid && await getSession(clientSid)) {
|
|
753
|
+
await touchSession(clientSid);
|
|
682
754
|
}
|
|
683
755
|
|
|
684
756
|
const abortController = new AbortController();
|
|
@@ -701,7 +773,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
701
773
|
const wtResult = await createWorktree(cwd, branchName);
|
|
702
774
|
const wtId = crypto.randomUUID();
|
|
703
775
|
|
|
704
|
-
createWorktreeRecord(wtId, clientSid || null, cwd, wtResult.worktreePath, branchName, baseBranch, (message || "").slice(0, 200));
|
|
776
|
+
await createWorktreeRecord(wtId, clientSid || null, cwd, wtResult.worktreePath, branchName, baseBranch, (message || "").slice(0, 200));
|
|
705
777
|
worktreeRecord = { id: wtId, worktreePath: wtResult.worktreePath, branchName, baseBranch };
|
|
706
778
|
effectiveCwd = wtResult.worktreePath;
|
|
707
779
|
|
|
@@ -710,6 +782,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
710
782
|
const wtPayload = { type: "worktree_created", worktreeId: wtId, branchName, baseBranch, worktreePath: wtResult.worktreePath };
|
|
711
783
|
if (chatId) wtPayload.chatId = chatId;
|
|
712
784
|
ws.send(JSON.stringify(wtPayload));
|
|
785
|
+
if (clientSid) broadcastToSession(clientSid, wtPayload, ws);
|
|
713
786
|
}
|
|
714
787
|
} catch (err) {
|
|
715
788
|
console.error("Worktree creation failed:", err.message, err.stack);
|
|
@@ -736,7 +809,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
736
809
|
if (effectiveMaxTurns) opts.maxTurns = effectiveMaxTurns;
|
|
737
810
|
|
|
738
811
|
if (!useBypass && !usePlan) {
|
|
739
|
-
opts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, chatId, projectName || "Chat");
|
|
812
|
+
opts.canUseTool = makeCanUseTool(ws, pendingApprovals, effectivePermMode, chatId, projectName || "Chat", () => state.resolvedSid);
|
|
740
813
|
}
|
|
741
814
|
if (chatModel) opts.model = resolveModel(chatModel);
|
|
742
815
|
if (Array.isArray(disabledTools) && disabledTools.length > 0) {
|
|
@@ -754,10 +827,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
754
827
|
(opts.appendSystemPrompt ? '\n\n' : '') + systemPrompt;
|
|
755
828
|
}
|
|
756
829
|
// Run memory maintenance (decay stale, clean expired) on each session
|
|
757
|
-
if (cwd) runMaintenance(cwd);
|
|
830
|
+
if (cwd) await runMaintenance(cwd);
|
|
758
831
|
// Inject persistent memories for this project (smart: uses user message for relevance)
|
|
759
832
|
if (cwd) {
|
|
760
|
-
const { prompt: memPrompt, count: memCount, memories: memList } = buildMemoryPrompt(cwd, 10, message);
|
|
833
|
+
const { prompt: memPrompt, count: memCount, memories: memList } = await buildMemoryPrompt(cwd, 10, message);
|
|
761
834
|
if (memPrompt) {
|
|
762
835
|
opts.appendSystemPrompt = (opts.appendSystemPrompt || '') +
|
|
763
836
|
(opts.appendSystemPrompt ? '\n\n' : '') + memPrompt;
|
|
@@ -778,6 +851,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
778
851
|
const payload = { type: "memories_injected", count: memCount, memories: memList };
|
|
779
852
|
if (chatId) payload.chatId = chatId;
|
|
780
853
|
ws.send(JSON.stringify(payload));
|
|
854
|
+
if (clientSid) broadcastToSession(clientSid, payload, ws);
|
|
781
855
|
}
|
|
782
856
|
} else {
|
|
783
857
|
console.log(`\n══════ MEMORY INJECTION ══════`);
|
|
@@ -795,6 +869,8 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
795
869
|
if (chatId) payload.chatId = chatId;
|
|
796
870
|
if (state.resolvedSid) payload.sessionId = state.resolvedSid;
|
|
797
871
|
ws.send(JSON.stringify(payload));
|
|
872
|
+
// Broadcast to other clients watching this session
|
|
873
|
+
if (state.resolvedSid) broadcastToSession(state.resolvedSid, payload, ws);
|
|
798
874
|
}
|
|
799
875
|
|
|
800
876
|
// Register for global tracking if we already know the session
|
|
@@ -819,30 +895,43 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
819
895
|
const sKey = chatId ? `${ourSid}::${chatId}` : ourSid;
|
|
820
896
|
sessionIds.set(sKey, claudeSessionId);
|
|
821
897
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
898
|
+
const isBotChat = chatId === 'assistant-bot';
|
|
899
|
+
|
|
900
|
+
if (!isBotChat) {
|
|
901
|
+
if (!await getSession(ourSid)) {
|
|
902
|
+
await createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
|
|
903
|
+
} else {
|
|
904
|
+
await updateClaudeSessionId(ourSid, claudeSessionId);
|
|
905
|
+
}
|
|
826
906
|
}
|
|
827
907
|
|
|
828
|
-
if (chatId) {
|
|
829
|
-
setClaudeSession(ourSid, chatId, claudeSessionId);
|
|
908
|
+
if (chatId && !isBotChat) {
|
|
909
|
+
await setClaudeSession(ourSid, chatId, claudeSessionId);
|
|
830
910
|
}
|
|
831
911
|
|
|
832
912
|
wsSend({ type: "session", sessionId: ourSid });
|
|
833
|
-
|
|
834
|
-
if (
|
|
835
|
-
userMsgData
|
|
913
|
+
|
|
914
|
+
if (!isBotChat) {
|
|
915
|
+
const userMsgData = { text: message };
|
|
916
|
+
if (images?.length) {
|
|
917
|
+
userMsgData.images = images.map(i => ({ name: i.name, data: i.data, mimeType: i.mimeType }));
|
|
918
|
+
}
|
|
919
|
+
await addMessage(state.resolvedSid, "user", JSON.stringify(userMsgData), chatId || null);
|
|
920
|
+
|
|
921
|
+
// Broadcast user message to observers (sender already rendered it locally)
|
|
922
|
+
const userBroadcast = { type: "user_message", text: message, sessionId: state.resolvedSid };
|
|
923
|
+
if (chatId) userBroadcast.chatId = chatId;
|
|
924
|
+
if (images?.length) userBroadcast.images = images.map(i => ({ name: i.name, mimeType: i.mimeType }));
|
|
925
|
+
broadcastToSession(state.resolvedSid, userBroadcast, ws);
|
|
836
926
|
}
|
|
837
|
-
addMessage(state.resolvedSid, "user", JSON.stringify(userMsgData), chatId || null);
|
|
838
927
|
|
|
839
928
|
// Register global query tracking now that we know the session
|
|
840
929
|
if (!clientSid) registerGlobalQuery(state.resolvedSid, queryKey);
|
|
841
930
|
|
|
842
|
-
const existingSession = getSession(ourSid);
|
|
931
|
+
const existingSession = await getSession(ourSid);
|
|
843
932
|
if (existingSession && !existingSession.title) {
|
|
844
933
|
const title = message.slice(0, 100).split("\n")[0];
|
|
845
|
-
updateSessionTitle(ourSid, title);
|
|
934
|
+
await updateSessionTitle(ourSid, title);
|
|
846
935
|
}
|
|
847
936
|
continue;
|
|
848
937
|
}
|
|
@@ -852,10 +941,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
852
941
|
if (block.type === "text" && block.text) {
|
|
853
942
|
state.lastAssistantText += (state.lastAssistantText ? "\n\n" : "") + block.text;
|
|
854
943
|
wsSend({ type: "text", text: block.text });
|
|
855
|
-
if (state.resolvedSid) addMessage(state.resolvedSid, "assistant", JSON.stringify({ text: block.text }), chatId || null);
|
|
944
|
+
if (state.resolvedSid) await addMessage(state.resolvedSid, "assistant", JSON.stringify({ text: block.text }), chatId || null);
|
|
856
945
|
} else if (block.type === "tool_use") {
|
|
857
946
|
wsSend({ type: "tool", id: block.id, name: block.name, input: block.input });
|
|
858
|
-
if (state.resolvedSid) addMessage(state.resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), chatId || null);
|
|
947
|
+
if (state.resolvedSid) await addMessage(state.resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), chatId || null);
|
|
859
948
|
}
|
|
860
949
|
}
|
|
861
950
|
continue;
|
|
@@ -869,10 +958,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
869
958
|
const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
|
|
870
959
|
const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
|
|
871
960
|
const model = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
|
|
872
|
-
if (sid) addCost(sid, sdkMsg.total_cost_usd || 0, sdkMsg.duration_ms || 0, sdkMsg.num_turns || 0, inputTokens, outputTokens, { model, stopReason: "success", isError: 0, cacheReadTokens, cacheCreationTokens });
|
|
873
|
-
wsSend({ type: "result", duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, totalCost: getTotalCost(), input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "success" });
|
|
961
|
+
if (sid) await addCost(sid, sdkMsg.total_cost_usd || 0, sdkMsg.duration_ms || 0, sdkMsg.num_turns || 0, inputTokens, outputTokens, { model, stopReason: "success", isError: 0, cacheReadTokens, cacheCreationTokens });
|
|
962
|
+
wsSend({ type: "result", duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, totalCost: await getTotalCost(), input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "success" });
|
|
874
963
|
state.lastChatMetrics = { durationMs: sdkMsg.duration_ms, costUsd: sdkMsg.total_cost_usd, inputTokens, outputTokens, model, turns: sdkMsg.num_turns, isError: false };
|
|
875
|
-
if (state.resolvedSid) addMessage(state.resolvedSid, "result", JSON.stringify({ duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "success" }), chatId || null);
|
|
964
|
+
if (state.resolvedSid) await addMessage(state.resolvedSid, "result", JSON.stringify({ duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "success" }), chatId || null);
|
|
876
965
|
} else if (sdkMsg.subtype === "error_max_turns") {
|
|
877
966
|
// Max turns reached — treat as a normal completion with a notice
|
|
878
967
|
const sid = state.resolvedSid || [...sessionIds.entries()].find(([, v]) => v === claudeSessionId)?.[0];
|
|
@@ -881,8 +970,8 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
881
970
|
const cacheReadTokens = sdkMsg.usage?.cache_read_input_tokens || 0;
|
|
882
971
|
const cacheCreationTokens = sdkMsg.usage?.cache_creation_input_tokens || 0;
|
|
883
972
|
const model = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
|
|
884
|
-
if (sid) addCost(sid, sdkMsg.total_cost_usd || 0, sdkMsg.duration_ms || 0, sdkMsg.num_turns || 0, inputTokens, outputTokens, { model, stopReason: "error_max_turns", isError: 0, cacheReadTokens, cacheCreationTokens });
|
|
885
|
-
wsSend({ type: "result", duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, totalCost: getTotalCost(), input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "error_max_turns" });
|
|
973
|
+
if (sid) await addCost(sid, sdkMsg.total_cost_usd || 0, sdkMsg.duration_ms || 0, sdkMsg.num_turns || 0, inputTokens, outputTokens, { model, stopReason: "error_max_turns", isError: 0, cacheReadTokens, cacheCreationTokens });
|
|
974
|
+
wsSend({ type: "result", duration_ms: sdkMsg.duration_ms, num_turns: sdkMsg.num_turns, cost_usd: sdkMsg.total_cost_usd, totalCost: await getTotalCost(), input_tokens: inputTokens, output_tokens: outputTokens, cache_read_tokens: cacheReadTokens, cache_creation_tokens: cacheCreationTokens, model, stop_reason: "error_max_turns" });
|
|
886
975
|
wsSend({ type: "error", error: `Reached max turns limit (${sdkMsg.num_turns}). Send another message to continue.` });
|
|
887
976
|
} else if (sdkMsg.subtype?.startsWith("error")) {
|
|
888
977
|
const errMsg = sdkMsg.errors?.join(", ") || sdkMsg.error || sdkMsg.message || "Unknown error";
|
|
@@ -898,8 +987,8 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
898
987
|
const sid = state.resolvedSid || [...sessionIds.entries()].find(([, v]) => v === claudeSessionId)?.[0];
|
|
899
988
|
state.lastChatMetrics = { durationMs, costUsd, inputTokens, outputTokens, model, turns: numTurns, isError: true, error: errMsg };
|
|
900
989
|
if (sid) {
|
|
901
|
-
addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: sdkMsg.subtype, isError: 1, cacheReadTokens, cacheCreationTokens });
|
|
902
|
-
addMessage(sid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype, duration_ms: durationMs, cost_usd: costUsd, model }), chatId || null);
|
|
990
|
+
await addCost(sid, costUsd, durationMs, numTurns, inputTokens, outputTokens, { model, stopReason: sdkMsg.subtype, isError: 1, cacheReadTokens, cacheCreationTokens });
|
|
991
|
+
await addMessage(sid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype, duration_ms: durationMs, cost_usd: costUsd, model }), chatId || null);
|
|
903
992
|
}
|
|
904
993
|
wsSend({ type: "error", error: errMsg });
|
|
905
994
|
}
|
|
@@ -915,7 +1004,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
915
1004
|
wsSend({ type: "tool_result", ...wirePayload });
|
|
916
1005
|
if (state.resolvedSid) {
|
|
917
1006
|
const dbPayload = { toolUseId: block.tool_use_id, content: text.slice(0, 10000), isError: block.is_error || false };
|
|
918
|
-
addMessage(state.resolvedSid, "tool_result", JSON.stringify(dbPayload), chatId || null);
|
|
1007
|
+
await addMessage(state.resolvedSid, "tool_result", JSON.stringify(dbPayload), chatId || null);
|
|
919
1008
|
}
|
|
920
1009
|
}
|
|
921
1010
|
}
|
|
@@ -929,7 +1018,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
929
1018
|
wsSend({ type: "done" });
|
|
930
1019
|
} catch (err) {
|
|
931
1020
|
if (err.name === "AbortError") {
|
|
932
|
-
if (state.resolvedSid) addMessage(state.resolvedSid, "aborted", JSON.stringify({ timestamp: Date.now() }), chatId || null);
|
|
1021
|
+
if (state.resolvedSid) await addMessage(state.resolvedSid, "aborted", JSON.stringify({ timestamp: Date.now() }), chatId || null);
|
|
933
1022
|
wsSend({ type: "aborted" });
|
|
934
1023
|
} else {
|
|
935
1024
|
const stderrOutput = stderrChunks.join("");
|
|
@@ -944,7 +1033,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
944
1033
|
wsSend({ type: "done" });
|
|
945
1034
|
} catch (retryErr) {
|
|
946
1035
|
if (retryErr.name === "AbortError") {
|
|
947
|
-
if (state.resolvedSid) addMessage(state.resolvedSid, "aborted", JSON.stringify({ timestamp: Date.now() }), chatId || null);
|
|
1036
|
+
if (state.resolvedSid) await addMessage(state.resolvedSid, "aborted", JSON.stringify({ timestamp: Date.now() }), chatId || null);
|
|
948
1037
|
wsSend({ type: "aborted" });
|
|
949
1038
|
} else {
|
|
950
1039
|
console.error("Query retry error:", retryErr.message);
|
|
@@ -960,7 +1049,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
960
1049
|
activeQueries.delete(queryKey);
|
|
961
1050
|
unregisterGlobalQuery(state.resolvedSid, queryKey);
|
|
962
1051
|
// Send push notification when query completes
|
|
963
|
-
const session = state.resolvedSid ? getSession(state.resolvedSid) : null;
|
|
1052
|
+
const session = state.resolvedSid ? await getSession(state.resolvedSid) : null;
|
|
964
1053
|
const pushTitle = session?.title || "Session complete";
|
|
965
1054
|
sendPushNotification("Claudeck", pushTitle, `chat-${state.resolvedSid}`);
|
|
966
1055
|
|
|
@@ -1008,10 +1097,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
1008
1097
|
if (cwd && state.lastAssistantText) {
|
|
1009
1098
|
try {
|
|
1010
1099
|
// 1. Parse explicit ```memory blocks (Claude-requested saves)
|
|
1011
|
-
const explicitCount = saveExplicitMemories(cwd, state.lastAssistantText, state.resolvedSid);
|
|
1100
|
+
const explicitCount = await saveExplicitMemories(cwd, state.lastAssistantText, state.resolvedSid);
|
|
1012
1101
|
|
|
1013
1102
|
// 2. Heuristic extraction from assistant text
|
|
1014
|
-
const autoCount = captureMemories(cwd, state.lastAssistantText, state.resolvedSid, null);
|
|
1103
|
+
const autoCount = await captureMemories(cwd, state.lastAssistantText, state.resolvedSid, null);
|
|
1015
1104
|
|
|
1016
1105
|
const totalCaptured = explicitCount + autoCount;
|
|
1017
1106
|
if (totalCaptured > 0) {
|
|
@@ -1021,6 +1110,7 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
1021
1110
|
const payload = { type: "memories_captured", count: totalCaptured, explicit: explicitCount, auto: autoCount };
|
|
1022
1111
|
if (chatId) payload.chatId = chatId;
|
|
1023
1112
|
ws.send(JSON.stringify(payload));
|
|
1113
|
+
if (state.resolvedSid) broadcastToSession(state.resolvedSid, payload, ws);
|
|
1024
1114
|
}
|
|
1025
1115
|
}
|
|
1026
1116
|
} catch (e) { console.error("Memory capture error:", e.message); }
|
|
@@ -1029,13 +1119,13 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
1029
1119
|
// Worktree post-completion: auto-commit, diff stats, notify
|
|
1030
1120
|
if (worktreeRecord) {
|
|
1031
1121
|
try {
|
|
1032
|
-
if (state.resolvedSid) updateWorktreeSession(worktreeRecord.id, state.resolvedSid);
|
|
1122
|
+
if (state.resolvedSid) await updateWorktreeSession(worktreeRecord.id, state.resolvedSid);
|
|
1033
1123
|
|
|
1034
1124
|
const commitMsg = `claudeck: ${(message || "worktree changes").slice(0, 72)}`;
|
|
1035
1125
|
await autoCommitWorktree(worktreeRecord.worktreePath, commitMsg);
|
|
1036
1126
|
|
|
1037
1127
|
const stats = await getWorktreeDiffStats(worktreeRecord.worktreePath, worktreeRecord.baseBranch);
|
|
1038
|
-
updateWorktreeStatus(worktreeRecord.id, "completed");
|
|
1128
|
+
await updateWorktreeStatus(worktreeRecord.id, "completed");
|
|
1039
1129
|
|
|
1040
1130
|
if (ws.readyState === 1) {
|
|
1041
1131
|
const wtPayload = {
|
|
@@ -1046,9 +1136,10 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
|
|
|
1046
1136
|
};
|
|
1047
1137
|
if (chatId) wtPayload.chatId = chatId;
|
|
1048
1138
|
ws.send(JSON.stringify(wtPayload));
|
|
1139
|
+
if (state.resolvedSid) broadcastToSession(state.resolvedSid, wtPayload, ws);
|
|
1049
1140
|
}
|
|
1050
1141
|
|
|
1051
|
-
logNotification(
|
|
1142
|
+
await logNotification(
|
|
1052
1143
|
"worktree",
|
|
1053
1144
|
`Worktree "${worktreeRecord.branchName}" ready`,
|
|
1054
1145
|
`+${stats.insertions} -${stats.deletions} lines in ${stats.files} file(s)`,
|
|
@@ -1068,12 +1159,26 @@ export function setupWebSocket(wss, sessionIds) {
|
|
|
1068
1159
|
wss.on("connection", (ws) => {
|
|
1069
1160
|
const ctx = { ws, sessionIds, activeQueries: new Map(), pendingApprovals: new Map() };
|
|
1070
1161
|
|
|
1071
|
-
ws.on("close", () =>
|
|
1162
|
+
ws.on("close", () => {
|
|
1163
|
+
leaveRoom(ws);
|
|
1164
|
+
handleClose(ctx);
|
|
1165
|
+
});
|
|
1072
1166
|
|
|
1073
1167
|
ws.on("message", async (raw) => {
|
|
1074
1168
|
let msg;
|
|
1075
1169
|
try { msg = JSON.parse(raw); } catch { return; }
|
|
1076
1170
|
|
|
1171
|
+
// Session broadcast: subscribe/unsubscribe
|
|
1172
|
+
if (msg.type === "subscribe") {
|
|
1173
|
+
leaveRoom(ws); // leave any previous room first
|
|
1174
|
+
if (msg.sessionId) joinRoom(msg.sessionId, ws);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
if (msg.type === "unsubscribe") {
|
|
1178
|
+
leaveRoom(ws);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1077
1182
|
if (msg.type === "abort") return handleAbort(msg, ctx);
|
|
1078
1183
|
if (msg.type === "permission_response") return handlePermissionResponse(msg, ctx);
|
|
1079
1184
|
if (msg.type === "workflow") return handleWorkflow(msg, ctx);
|
package/server.js
CHANGED
|
@@ -32,6 +32,7 @@ import notificationsRouter, { setVapidPublicKey } from "./server/routes/notifica
|
|
|
32
32
|
import memoryRouter from "./server/routes/memory.js";
|
|
33
33
|
import worktreesRouter from "./server/routes/worktrees.js";
|
|
34
34
|
import skillsRouter from "./server/routes/skills.js";
|
|
35
|
+
import marketplaceRouter, { setApp as setMarketplaceApp } from "./server/routes/marketplace.js";
|
|
35
36
|
import { setupWebSocket } from "./server/ws-handler.js";
|
|
36
37
|
import { setWss } from "./server/notification-logger.js";
|
|
37
38
|
import { authMiddleware, verifyWsClient, isAuthEnabled, getToken, loginHandler, statusHandler } from "./server/auth.js";
|
|
@@ -88,7 +89,7 @@ const sessionIds = new Map();
|
|
|
88
89
|
for (const row of rows) {
|
|
89
90
|
sessionIds.set(row.id, row.claude_session_id);
|
|
90
91
|
}
|
|
91
|
-
const csRows = allClaudeSessions();
|
|
92
|
+
const csRows = await allClaudeSessions();
|
|
92
93
|
for (const row of csRows) {
|
|
93
94
|
const key = row.chat_id ? `${row.session_id}::${row.chat_id}` : row.session_id;
|
|
94
95
|
sessionIds.set(key, row.claude_session_id);
|
|
@@ -129,6 +130,8 @@ app.use("/api/telegram", telegramRouter);
|
|
|
129
130
|
app.use("/api/memory", memoryRouter);
|
|
130
131
|
app.use("/api/worktrees", worktreesRouter);
|
|
131
132
|
app.use("/api/skills", skillsRouter);
|
|
133
|
+
app.use("/api/marketplace", marketplaceRouter);
|
|
134
|
+
setMarketplaceApp(app);
|
|
132
135
|
|
|
133
136
|
// Version endpoint
|
|
134
137
|
import { readFileSync } from "fs";
|
|
@@ -154,12 +157,19 @@ app.get("/api/plugins", (req, res) => {
|
|
|
154
157
|
if (!existsSync(join(dir, "client.js"))) continue;
|
|
155
158
|
const hasCss = existsSync(join(dir, "client.css"));
|
|
156
159
|
const hasServer = existsSync(join(dir, "server.js"));
|
|
160
|
+
// Read manifest.json if it exists
|
|
161
|
+
let manifest = null;
|
|
162
|
+
const manifestPath = join(dir, "manifest.json");
|
|
163
|
+
if (existsSync(manifestPath)) {
|
|
164
|
+
try { manifest = JSON.parse(readFileSync(manifestPath, "utf8")); } catch {}
|
|
165
|
+
}
|
|
157
166
|
plugins.push({
|
|
158
167
|
name,
|
|
159
168
|
js: `plugins/${name}/client.js`,
|
|
160
169
|
css: hasCss ? `plugins/${name}/client.css` : null,
|
|
161
170
|
source: "builtin",
|
|
162
171
|
apiBase: hasServer ? `/api/plugins/${name}` : null,
|
|
172
|
+
manifest,
|
|
163
173
|
});
|
|
164
174
|
}
|
|
165
175
|
}
|
|
@@ -173,12 +183,20 @@ app.get("/api/plugins", (req, res) => {
|
|
|
173
183
|
const hasCss = existsSync(join(dir, "client.css"));
|
|
174
184
|
const allowUserServer = process.env.CLAUDECK_USER_SERVER_PLUGINS === "true";
|
|
175
185
|
const hasServer = allowUserServer && existsSync(join(dir, "server.js"));
|
|
186
|
+
const fromMarketplace = existsSync(join(dir, ".marketplace"));
|
|
187
|
+
let manifest = null;
|
|
188
|
+
const manifestPath = join(dir, "manifest.json");
|
|
189
|
+
if (existsSync(manifestPath)) {
|
|
190
|
+
try { manifest = JSON.parse(readFileSync(manifestPath, "utf8")); } catch {}
|
|
191
|
+
}
|
|
176
192
|
plugins.push({
|
|
177
193
|
name: entry,
|
|
178
194
|
js: `user-plugins/${entry}/client.js`,
|
|
179
195
|
css: hasCss ? `user-plugins/${entry}/client.css` : null,
|
|
180
196
|
source: "user",
|
|
197
|
+
fromMarketplace,
|
|
181
198
|
apiBase: hasServer ? `/api/plugins/${entry}` : null,
|
|
199
|
+
manifest,
|
|
182
200
|
});
|
|
183
201
|
}
|
|
184
202
|
}
|
|
@@ -214,7 +232,7 @@ ${isAuthEnabled() ? ` \x1b[2m➜ Auth:\x1b[0m \x1b[33menabled\x1b[0m\n \x1
|
|
|
214
232
|
});
|
|
215
233
|
|
|
216
234
|
// Purge old notifications once per day
|
|
217
|
-
setInterval(() => purgeOldNotifications(90), 24 * 60 * 60 * 1000);
|
|
235
|
+
setInterval(async () => { try { await purgeOldNotifications(90); } catch {} }, 24 * 60 * 60 * 1000);
|
|
218
236
|
|
|
219
237
|
// Graceful shutdown
|
|
220
238
|
process.on("SIGINT", () => {
|