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.
Files changed (60) hide show
  1. package/README.md +13 -9
  2. package/db/sqlite.js +1697 -0
  3. package/db.js +3 -1645
  4. package/package.json +2 -1
  5. package/plugins/claude-editor/manifest.json +10 -0
  6. package/plugins/linear/manifest.json +10 -0
  7. package/plugins/repos/manifest.json +10 -0
  8. package/public/css/ui/messages.css +25 -0
  9. package/public/css/ui/right-panel.css +207 -0
  10. package/public/css/ui/settings.css +75 -0
  11. package/public/index.html +7 -0
  12. package/public/js/components/settings-modal.js +65 -0
  13. package/public/js/core/api.js +23 -6
  14. package/public/js/core/events.js +11 -0
  15. package/public/js/core/plugin-loader.js +96 -11
  16. package/public/js/core/store.js +11 -0
  17. package/public/js/core/ws.js +12 -0
  18. package/public/js/features/chat.js +4 -0
  19. package/public/js/features/sessions.js +102 -10
  20. package/public/js/main.js +1 -0
  21. package/public/js/panels/assistant-bot.js +16 -0
  22. package/public/js/panels/dev-docs.js +2 -2
  23. package/public/js/panels/memory.js +1 -0
  24. package/public/js/ui/context-gauge.js +10 -1
  25. package/public/js/ui/header-dropdowns.js +30 -0
  26. package/public/js/ui/input-meta.js +13 -6
  27. package/public/js/ui/max-turns.js +6 -3
  28. package/public/js/ui/messages.js +42 -0
  29. package/public/js/ui/model-selector.js +1 -0
  30. package/public/js/ui/parallel.js +2 -4
  31. package/public/js/ui/permissions.js +1 -0
  32. package/public/js/ui/tab-sdk.js +395 -176
  33. package/public/style.css +1 -0
  34. package/server/agent-loop.js +26 -26
  35. package/server/memory-extractor.js +4 -4
  36. package/server/memory-injector.js +11 -11
  37. package/server/memory-optimizer.js +19 -15
  38. package/server/notification-logger.js +5 -5
  39. package/server/orchestrator.js +15 -15
  40. package/server/push-sender.js +2 -2
  41. package/server/routes/agents.js +2 -2
  42. package/server/routes/marketplace.js +316 -0
  43. package/server/routes/memory.js +20 -20
  44. package/server/routes/messages.js +41 -10
  45. package/server/routes/notifications.js +20 -20
  46. package/server/routes/sessions.js +17 -17
  47. package/server/routes/stats.js +37 -37
  48. package/server/routes/worktrees.js +9 -9
  49. package/server/summarizer.js +3 -3
  50. package/server/ws-handler.js +163 -58
  51. package/server.js +20 -2
  52. package/plugins/event-stream/client.css +0 -207
  53. package/plugins/event-stream/client.js +0 -271
  54. package/plugins/sudoku/client.css +0 -196
  55. package/plugins/sudoku/client.js +0 -329
  56. package/plugins/tasks/client.css +0 -414
  57. package/plugins/tasks/client.js +0 -394
  58. package/plugins/tasks/server.js +0 -116
  59. package/plugins/tic-tac-toe/client.css +0 -167
  60. package/plugins/tic-tac-toe/client.js +0 -241
@@ -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
- const pending = pendingApprovals.get(msg.id);
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
- ws.send(JSON.stringify({ type: "error", error: "Failed to load agents" }));
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
- if (!getSession(ourSid)) {
823
- createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
824
- } else {
825
- updateClaudeSessionId(ourSid, claudeSessionId);
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
- const userMsgData = { text: message };
834
- if (images?.length) {
835
- userMsgData.images = images.map(i => ({ name: i.name, data: i.data, mimeType: i.mimeType }));
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", () => handleClose(ctx));
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", () => {