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
@@ -0,0 +1,316 @@
1
+ // Marketplace routes — fetch community plugin registry, install/uninstall plugins
2
+ import { Router } from "express";
3
+ import { join } from "path";
4
+ import { existsSync, mkdirSync, rmSync, readdirSync, readFileSync, writeFileSync, statSync } from "fs";
5
+ import { pathToFileURL } from "url";
6
+ import { userPluginsDir, userConfigDir, packageRoot } from "../paths.js";
7
+
8
+ const router = Router();
9
+
10
+ // Reference to Express app (set during mount for hot-mounting plugin server routes)
11
+ let _app = null;
12
+ export function setApp(app) { _app = app; }
13
+
14
+ // Track hot-mounted plugin routers so we can swap/remove them on uninstall
15
+ const mountedPluginRouters = new Map();
16
+
17
+ // ── Config ──────────────────────────────────────────────────
18
+ const MARKETPLACE_REPO = "hamedafarag/claudeck-marketplace";
19
+ const MARKETPLACE_BRANCH = "main";
20
+ const MARKETPLACE_FILE = "marketplace.json";
21
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
22
+
23
+ let registryCache = null;
24
+ let registryCacheTime = 0;
25
+
26
+ // ── GET /api/marketplace — fetch registry ───────────────────
27
+ router.get("/", async (req, res) => {
28
+ try {
29
+ const now = Date.now();
30
+ const force = req.query.refresh === "true";
31
+
32
+ if (!force && registryCache && now - registryCacheTime < CACHE_TTL) {
33
+ return res.json(enrichRegistry(registryCache));
34
+ }
35
+
36
+ const url = `https://raw.githubusercontent.com/${MARKETPLACE_REPO}/${MARKETPLACE_BRANCH}/${MARKETPLACE_FILE}`;
37
+ const resp = await fetch(url);
38
+ if (!resp.ok) {
39
+ // Return cached data if available, empty otherwise
40
+ if (registryCache) return res.json(enrichRegistry(registryCache));
41
+ return res.json({ name: "claudeck-marketplace", version: "0.0.0", plugins: [] });
42
+ }
43
+
44
+ registryCache = await resp.json();
45
+ registryCacheTime = now;
46
+ res.json(enrichRegistry(registryCache));
47
+ } catch (err) {
48
+ console.error("Marketplace fetch error:", err.message);
49
+ if (registryCache) return res.json(enrichRegistry(registryCache));
50
+ res.json({ name: "claudeck-marketplace", version: "0.0.0", plugins: [] });
51
+ }
52
+ });
53
+
54
+ // ── GET /api/marketplace/installed — list installed community plugins ──
55
+ router.get("/installed", (req, res) => {
56
+ const installed = [];
57
+ if (!existsSync(userPluginsDir)) return res.json(installed);
58
+
59
+ for (const name of readdirSync(userPluginsDir)) {
60
+ const dir = join(userPluginsDir, name);
61
+ if (!statSync(dir).isDirectory()) continue;
62
+ const manifestPath = join(dir, "manifest.json");
63
+ if (!existsSync(manifestPath)) continue;
64
+ try {
65
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
66
+ // Only list community plugins (those with _marketplace marker)
67
+ const metaPath = join(dir, ".marketplace");
68
+ if (existsSync(metaPath)) {
69
+ const meta = JSON.parse(readFileSync(metaPath, "utf8"));
70
+ installed.push({ ...manifest, installedFrom: meta.source, installedAt: meta.installedAt });
71
+ }
72
+ } catch {}
73
+ }
74
+ res.json(installed);
75
+ });
76
+
77
+ // Validate plugin ID to prevent path traversal
78
+ const SAFE_ID = /^[a-z0-9][a-z0-9-]*$/;
79
+
80
+ // ── POST /api/marketplace/install — install a community plugin ──
81
+ router.post("/install", async (req, res) => {
82
+ const { id, repo, source } = req.body;
83
+ if (!id) return res.status(400).json({ error: "Plugin id required" });
84
+ if (!SAFE_ID.test(id)) return res.status(400).json({ error: "Invalid plugin id" });
85
+
86
+ const pluginDir = join(userPluginsDir, id);
87
+
88
+ try {
89
+ // Determine download source
90
+ let downloadUrl;
91
+ let isMonorepo = false;
92
+
93
+ if (source && source.startsWith("./")) {
94
+ // Monorepo plugin — download from marketplace repo subdirectory
95
+ isMonorepo = true;
96
+ downloadUrl = `https://api.github.com/repos/${MARKETPLACE_REPO}/tarball/${MARKETPLACE_BRANCH}`;
97
+ } else if (repo) {
98
+ // External repo — download tarball
99
+ downloadUrl = `https://api.github.com/repos/${repo}/tarball`;
100
+ } else {
101
+ return res.status(400).json({ error: "Plugin must have 'repo' or 'source'" });
102
+ }
103
+
104
+ // Download tarball
105
+ const resp = await fetch(downloadUrl, {
106
+ headers: { "Accept": "application/vnd.github+json", "User-Agent": "claudeck" },
107
+ redirect: "follow",
108
+ });
109
+ if (!resp.ok) {
110
+ return res.status(502).json({ error: `GitHub download failed: ${resp.status} ${resp.statusText}` });
111
+ }
112
+
113
+ // Write tarball to temp file
114
+ const tmpTar = join(userPluginsDir, `_tmp_${id}.tar.gz`);
115
+ const tmpDir = join(userPluginsDir, `_tmp_${id}`);
116
+ const buffer = Buffer.from(await resp.arrayBuffer());
117
+ writeFileSync(tmpTar, buffer);
118
+
119
+ // Extract tarball
120
+ mkdirSync(tmpDir, { recursive: true });
121
+ const { execSync } = await import("child_process");
122
+ execSync(`tar -xzf "${tmpTar}" -C "${tmpDir}" --no-same-owner --no-same-permissions`, { stdio: "pipe" });
123
+
124
+ // Find extracted content (tarball has a root dir like owner-repo-sha/)
125
+ const extracted = readdirSync(tmpDir);
126
+ const rootDir = extracted.length === 1 ? join(tmpDir, extracted[0]) : tmpDir;
127
+
128
+ // Determine source directory within extracted content
129
+ let sourceDir;
130
+ if (isMonorepo) {
131
+ // For monorepo plugins, navigate to the plugin subdirectory
132
+ const subPath = source.replace("./", "");
133
+ sourceDir = join(rootDir, subPath);
134
+ if (!existsSync(sourceDir) || !existsSync(join(sourceDir, "client.js"))) {
135
+ // Cleanup
136
+ rmSync(tmpTar, { force: true });
137
+ rmSync(tmpDir, { recursive: true, force: true });
138
+ return res.status(404).json({ error: `Plugin directory not found in marketplace repo: ${subPath}` });
139
+ }
140
+ } else {
141
+ // External repo — root is the plugin
142
+ sourceDir = rootDir;
143
+ if (!existsSync(join(sourceDir, "client.js"))) {
144
+ rmSync(tmpTar, { force: true });
145
+ rmSync(tmpDir, { recursive: true, force: true });
146
+ return res.status(400).json({ error: "Plugin repo must contain client.js at root" });
147
+ }
148
+ }
149
+
150
+ // Remove old installation if exists
151
+ if (existsSync(pluginDir)) {
152
+ rmSync(pluginDir, { recursive: true, force: true });
153
+ }
154
+
155
+ // Move plugin to final location
156
+ const { cpSync } = await import("fs");
157
+ cpSync(sourceDir, pluginDir, { recursive: true });
158
+
159
+ // Patch server.js imports: rewrite relative db.js imports to absolute path
160
+ const serverFilePath = join(pluginDir, "server.js");
161
+ if (existsSync(serverFilePath)) {
162
+ try {
163
+ let code = readFileSync(serverFilePath, "utf8");
164
+ const dbAbsolute = pathToFileURL(join(packageRoot, "db.js")).href;
165
+ // Replace relative imports like ../../db.js, ../db.js, etc.
166
+ code = code.replace(
167
+ /from\s+["'](?:\.\.\/)+db\.js["']/g,
168
+ `from "${dbAbsolute}"`
169
+ );
170
+ writeFileSync(serverFilePath, code);
171
+ } catch (err) {
172
+ console.warn(`Could not patch server.js imports for ${id}:`, err.message);
173
+ }
174
+ }
175
+
176
+ // Write marketplace metadata
177
+ writeFileSync(join(pluginDir, ".marketplace"), JSON.stringify({
178
+ source: repo || source,
179
+ installedAt: new Date().toISOString(),
180
+ marketplace: MARKETPLACE_REPO,
181
+ }));
182
+
183
+ // Copy default config if plugin ships one
184
+ const configSrc = join(pluginDir, "config.json");
185
+ const configDst = join(userConfigDir, `${id}-config.json`);
186
+ if (existsSync(configSrc) && !existsSync(configDst)) {
187
+ const { copyFileSync } = await import("fs");
188
+ copyFileSync(configSrc, configDst);
189
+ }
190
+
191
+ // Cleanup temp files
192
+ rmSync(tmpTar, { force: true });
193
+ rmSync(tmpDir, { recursive: true, force: true });
194
+
195
+ // Read installed manifest
196
+ const manifestPath = join(pluginDir, "manifest.json");
197
+ let manifest = null;
198
+ if (existsSync(manifestPath)) {
199
+ try { manifest = JSON.parse(readFileSync(manifestPath, "utf8")); } catch {}
200
+ }
201
+
202
+ // Hot-mount server routes if plugin has server.js and app is available
203
+ let serverMounted = false;
204
+ const serverFile = join(pluginDir, "server.js");
205
+ if (_app && existsSync(serverFile)) {
206
+ const allowUserServer = process.env.CLAUDECK_USER_SERVER_PLUGINS === "true";
207
+ if (allowUserServer) {
208
+ try {
209
+ // Use cache-busting query to force re-import on reinstall
210
+ const mod = await import(pathToFileURL(serverFile).href + `?t=${Date.now()}`);
211
+
212
+ // If already mounted, swap the inner handler; otherwise mount a wrapper
213
+ if (mountedPluginRouters.has(id)) {
214
+ mountedPluginRouters.get(id).inner = mod.default;
215
+ } else {
216
+ const wrapper = { inner: mod.default };
217
+ const wrapperRouter = Router();
218
+ wrapperRouter.use((req, res, next) => {
219
+ if (wrapper.inner) wrapper.inner(req, res, next);
220
+ else next();
221
+ });
222
+ _app.use(`/api/plugins/${id}`, wrapperRouter);
223
+ mountedPluginRouters.set(id, wrapper);
224
+ }
225
+ serverMounted = true;
226
+ console.log(`Hot-mounted plugin routes: /api/plugins/${id}`);
227
+ } catch (err) {
228
+ console.error(`Failed to hot-mount plugin server: ${id}`, err.message);
229
+ }
230
+ }
231
+ }
232
+
233
+ res.json({ ok: true, id, manifest, serverMounted });
234
+ } catch (err) {
235
+ // Cleanup on error
236
+ const tmpTar = join(userPluginsDir, `_tmp_${id}.tar.gz`);
237
+ const tmpDir = join(userPluginsDir, `_tmp_${id}`);
238
+ rmSync(tmpTar, { force: true });
239
+ rmSync(tmpDir, { recursive: true, force: true });
240
+ console.error(`Marketplace install error (${id}):`, err.message);
241
+ res.status(500).json({ error: err.message });
242
+ }
243
+ });
244
+
245
+ // ── POST /api/marketplace/uninstall — remove a community plugin ──
246
+ router.post("/uninstall", (req, res) => {
247
+ const { id } = req.body;
248
+ if (!id) return res.status(400).json({ error: "Plugin id required" });
249
+ if (!SAFE_ID.test(id)) return res.status(400).json({ error: "Invalid plugin id" });
250
+
251
+ const pluginDir = join(userPluginsDir, id);
252
+ const marketplaceMeta = join(pluginDir, ".marketplace");
253
+
254
+ // Only allow uninstalling community plugins (has .marketplace marker)
255
+ if (!existsSync(marketplaceMeta)) {
256
+ return res.status(400).json({ error: "Plugin is not a community plugin or not installed" });
257
+ }
258
+
259
+ try {
260
+ rmSync(pluginDir, { recursive: true, force: true });
261
+
262
+ // Disable hot-mounted server routes (wrapper stays but inner is nulled)
263
+ if (mountedPluginRouters.has(id)) {
264
+ mountedPluginRouters.get(id).inner = null;
265
+ console.log(`Disabled plugin routes: /api/plugins/${id}`);
266
+ }
267
+
268
+ res.json({ ok: true, id });
269
+ } catch (err) {
270
+ console.error(`Marketplace uninstall error (${id}):`, err.message);
271
+ res.status(500).json({ error: err.message });
272
+ }
273
+ });
274
+
275
+ // ── Helpers ─────────────────────────────────────────────────
276
+
277
+ /** Enrich registry with installation status and built-in detection */
278
+ function enrichRegistry(registry) {
279
+ const builtinDir = join(packageRoot, "plugins");
280
+ const enriched = { ...registry, plugins: [] };
281
+ for (const plugin of registry.plugins || []) {
282
+ // Check if this plugin ships as a built-in
283
+ const isBuiltin = existsSync(join(builtinDir, plugin.id, "client.js"));
284
+
285
+ const dir = join(userPluginsDir, plugin.id);
286
+ const installed = existsSync(join(dir, ".marketplace"));
287
+ let installedVersion = null;
288
+ if (installed) {
289
+ const mPath = join(dir, "manifest.json");
290
+ if (existsSync(mPath)) {
291
+ try { installedVersion = JSON.parse(readFileSync(mPath, "utf8")).version; } catch {}
292
+ }
293
+ }
294
+ enriched.plugins.push({
295
+ ...plugin,
296
+ isBuiltin,
297
+ installed,
298
+ installedVersion,
299
+ updateAvailable: installed && installedVersion && semverNewer(plugin.version, installedVersion),
300
+ });
301
+ }
302
+ return enriched;
303
+ }
304
+
305
+ /** Return true if `a` is strictly newer than `b` (simple semver comparison) */
306
+ function semverNewer(a, b) {
307
+ const pa = (a || "0.0.0").split(".").map(Number);
308
+ const pb = (b || "0.0.0").split(".").map(Number);
309
+ for (let i = 0; i < 3; i++) {
310
+ if ((pa[i] || 0) > (pb[i] || 0)) return true;
311
+ if ((pa[i] || 0) < (pb[i] || 0)) return false;
312
+ }
313
+ return false;
314
+ }
315
+
316
+ export default router;
@@ -11,11 +11,11 @@ const router = Router();
11
11
  const CATEGORIES = ["convention", "decision", "discovery", "warning"];
12
12
 
13
13
  // List memories for a project
14
- router.get("/", (req, res) => {
14
+ router.get("/", async (req, res) => {
15
15
  try {
16
16
  const { project, category } = req.query;
17
17
  if (!project) return res.status(400).json({ error: "project query param required" });
18
- const memories = listMemories(project, category || null);
18
+ const memories = await listMemories(project, category || null);
19
19
  res.json(memories);
20
20
  } catch (err) {
21
21
  res.status(500).json({ error: err.message });
@@ -23,11 +23,11 @@ router.get("/", (req, res) => {
23
23
  });
24
24
 
25
25
  // Search memories
26
- router.get("/search", (req, res) => {
26
+ router.get("/search", async (req, res) => {
27
27
  try {
28
28
  const { project, q, limit } = req.query;
29
29
  if (!project || !q) return res.status(400).json({ error: "project and q required" });
30
- const results = searchMemories(project, q, Number(limit) || 20);
30
+ const results = await searchMemories(project, q, Number(limit) || 20);
31
31
  res.json(results);
32
32
  } catch (err) {
33
33
  res.status(500).json({ error: err.message });
@@ -35,13 +35,13 @@ router.get("/search", (req, res) => {
35
35
  });
36
36
 
37
37
  // Get top relevant memories (used for prompt injection)
38
- router.get("/top", (req, res) => {
38
+ router.get("/top", async (req, res) => {
39
39
  try {
40
40
  const { project, limit } = req.query;
41
41
  if (!project) return res.status(400).json({ error: "project required" });
42
- const memories = getTopMemories(project, Number(limit) || 10);
42
+ const memories = await getTopMemories(project, Number(limit) || 10);
43
43
  // Touch each memory to boost relevance
44
- for (const m of memories) touchMemory(m.id);
44
+ for (const m of memories) await touchMemory(m.id);
45
45
  res.json(memories);
46
46
  } catch (err) {
47
47
  res.status(500).json({ error: err.message });
@@ -49,12 +49,12 @@ router.get("/top", (req, res) => {
49
49
  });
50
50
 
51
51
  // Get stats
52
- router.get("/stats", (req, res) => {
52
+ router.get("/stats", async (req, res) => {
53
53
  try {
54
54
  const { project } = req.query;
55
55
  if (!project) return res.status(400).json({ error: "project required" });
56
- const stats = getMemoryStats(project);
57
- const counts = getMemoryCounts(project);
56
+ const stats = await getMemoryStats(project);
57
+ const counts = await getMemoryCounts(project);
58
58
  res.json({ ...stats, categories: counts });
59
59
  } catch (err) {
60
60
  res.status(500).json({ error: err.message });
@@ -62,14 +62,14 @@ router.get("/stats", (req, res) => {
62
62
  });
63
63
 
64
64
  // Create a memory
65
- router.post("/", (req, res) => {
65
+ router.post("/", async (req, res) => {
66
66
  try {
67
67
  const { project, category, content, sessionId, agentId } = req.body;
68
68
  if (!project || !content) {
69
69
  return res.status(400).json({ error: "project and content required" });
70
70
  }
71
71
  const cat = CATEGORIES.includes(category) ? category : "discovery";
72
- const info = createMemory(project, cat, content.trim(), sessionId || null, agentId || null);
72
+ const info = await createMemory(project, cat, content.trim(), sessionId || null, agentId || null);
73
73
  res.json({ id: info.lastInsertRowid });
74
74
  } catch (err) {
75
75
  res.status(500).json({ error: err.message });
@@ -77,13 +77,13 @@ router.post("/", (req, res) => {
77
77
  });
78
78
 
79
79
  // Update a memory
80
- router.put("/:id", (req, res) => {
80
+ router.put("/:id", async (req, res) => {
81
81
  try {
82
82
  const id = Number(req.params.id);
83
83
  const { content, category } = req.body;
84
84
  if (!content) return res.status(400).json({ error: "content required" });
85
85
  const cat = CATEGORIES.includes(category) ? category : "discovery";
86
- updateMemory(id, content.trim(), cat);
86
+ await updateMemory(id, content.trim(), cat);
87
87
  res.json({ ok: true });
88
88
  } catch (err) {
89
89
  res.status(500).json({ error: err.message });
@@ -91,10 +91,10 @@ router.put("/:id", (req, res) => {
91
91
  });
92
92
 
93
93
  // Delete a memory
94
- router.delete("/:id", (req, res) => {
94
+ router.delete("/:id", async (req, res) => {
95
95
  try {
96
96
  const id = Number(req.params.id);
97
- deleteMemory(id);
97
+ await deleteMemory(id);
98
98
  res.json({ ok: true });
99
99
  } catch (err) {
100
100
  res.status(500).json({ error: err.message });
@@ -102,11 +102,11 @@ router.delete("/:id", (req, res) => {
102
102
  });
103
103
 
104
104
  // Decay old memories and clean expired
105
- router.post("/maintain", (req, res) => {
105
+ router.post("/maintain", async (req, res) => {
106
106
  try {
107
107
  const { project } = req.body;
108
108
  if (!project) return res.status(400).json({ error: "project required" });
109
- maintainMemories(project);
109
+ await maintainMemories(project);
110
110
  res.json({ ok: true });
111
111
  } catch (err) {
112
112
  res.status(500).json({ error: err.message });
@@ -129,13 +129,13 @@ router.post("/optimize", async (req, res) => {
129
129
  });
130
130
 
131
131
  // Apply optimization — replace memories with optimized set
132
- router.post("/optimize/apply", (req, res) => {
132
+ router.post("/optimize/apply", async (req, res) => {
133
133
  try {
134
134
  const { project, optimized } = req.body;
135
135
  if (!project || !Array.isArray(optimized)) {
136
136
  return res.status(400).json({ error: "project and optimized array required" });
137
137
  }
138
- const result = applyOptimization(project, optimized);
138
+ const result = await applyOptimization(project, optimized);
139
139
  res.json(result);
140
140
  } catch (err) {
141
141
  console.error("Apply optimization error:", err);
@@ -1,32 +1,63 @@
1
1
  import { Router } from "express";
2
- import { getMessages, getMessagesByChatId, getMessagesNoChatId } from "../../db.js";
2
+ import {
3
+ getMessages, getMessagesByChatId, getMessagesNoChatId,
4
+ getRecentMessages, getRecentMessagesByChatId, getRecentMessagesNoChatId,
5
+ getOlderMessages, getOlderMessagesByChatId, getOlderMessagesNoChatId,
6
+ } from "../../db.js";
3
7
 
4
8
  const router = Router();
5
9
 
6
- // Get all messages for a session
7
- router.get("/:id/messages", (req, res) => {
10
+ // Get all messages for a session (supports ?limit=N&before=ID)
11
+ router.get("/:id/messages", async (req, res) => {
8
12
  try {
9
- const messages = getMessages(req.params.id);
13
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : 0;
14
+ const before = req.query.before ? parseInt(req.query.before, 10) : 0;
15
+ let messages;
16
+ if (limit > 0 && before > 0) {
17
+ messages = await getOlderMessages(req.params.id, before, limit);
18
+ } else if (limit > 0) {
19
+ messages = await getRecentMessages(req.params.id, limit);
20
+ } else {
21
+ messages = await getMessages(req.params.id);
22
+ }
10
23
  res.json(messages);
11
24
  } catch (err) {
12
25
  res.status(500).json({ error: err.message });
13
26
  }
14
27
  });
15
28
 
16
- // Get messages filtered by chatId
17
- router.get("/:id/messages/:chatId", (req, res) => {
29
+ // Get messages filtered by chatId (supports ?limit=N&before=ID)
30
+ router.get("/:id/messages/:chatId", async (req, res) => {
18
31
  try {
19
- const messages = getMessagesByChatId(req.params.id, req.params.chatId);
32
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : 0;
33
+ const before = req.query.before ? parseInt(req.query.before, 10) : 0;
34
+ let messages;
35
+ if (limit > 0 && before > 0) {
36
+ messages = await getOlderMessagesByChatId(req.params.id, req.params.chatId, before, limit);
37
+ } else if (limit > 0) {
38
+ messages = await getRecentMessagesByChatId(req.params.id, req.params.chatId, limit);
39
+ } else {
40
+ messages = await getMessagesByChatId(req.params.id, req.params.chatId);
41
+ }
20
42
  res.json(messages);
21
43
  } catch (err) {
22
44
  res.status(500).json({ error: err.message });
23
45
  }
24
46
  });
25
47
 
26
- // Get messages where chat_id IS NULL (single-mode)
27
- router.get("/:id/messages-single", (req, res) => {
48
+ // Get messages where chat_id IS NULL (supports ?limit=N&before=ID)
49
+ router.get("/:id/messages-single", async (req, res) => {
28
50
  try {
29
- const messages = getMessagesNoChatId(req.params.id);
51
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : 0;
52
+ const before = req.query.before ? parseInt(req.query.before, 10) : 0;
53
+ let messages;
54
+ if (limit > 0 && before > 0) {
55
+ messages = await getOlderMessagesNoChatId(req.params.id, before, limit);
56
+ } else if (limit > 0) {
57
+ messages = await getRecentMessagesNoChatId(req.params.id, limit);
58
+ } else {
59
+ messages = await getMessagesNoChatId(req.params.id);
60
+ }
30
61
  res.json(messages);
31
62
  } catch (err) {
32
63
  res.status(500).json({ error: err.message });
@@ -26,67 +26,67 @@ router.get("/vapid-public-key", (req, res) => {
26
26
  res.json({ key: vapidPublicKey });
27
27
  });
28
28
 
29
- router.post("/subscribe", (req, res) => {
29
+ router.post("/subscribe", async (req, res) => {
30
30
  const { endpoint, keys } = req.body;
31
31
  if (!endpoint || !keys?.p256dh || !keys?.auth) {
32
32
  return res.status(400).json({ error: "Invalid subscription" });
33
33
  }
34
- upsertPushSubscription(endpoint, keys.p256dh, keys.auth);
34
+ await upsertPushSubscription(endpoint, keys.p256dh, keys.auth);
35
35
  res.json({ ok: true });
36
36
  });
37
37
 
38
- router.post("/unsubscribe", (req, res) => {
38
+ router.post("/unsubscribe", async (req, res) => {
39
39
  const { endpoint } = req.body;
40
40
  if (!endpoint) {
41
41
  return res.status(400).json({ error: "Missing endpoint" });
42
42
  }
43
- deletePushSubscription(endpoint);
43
+ await deletePushSubscription(endpoint);
44
44
  res.json({ ok: true });
45
45
  });
46
46
 
47
47
  // ── Create notification (from frontend) ───────────────────
48
- router.post("/create", (req, res) => {
48
+ router.post("/create", async (req, res) => {
49
49
  const { type, title, body, metadata, sourceSessionId, sourceAgentId } = req.body;
50
50
  if (!type || !title) {
51
51
  return res.status(400).json({ error: "type and title are required" });
52
52
  }
53
- const notification = logNotification(type, title, body || null, metadata || null, sourceSessionId || null, sourceAgentId || null);
53
+ const notification = await logNotification(type, title, body || null, metadata || null, sourceSessionId || null, sourceAgentId || null);
54
54
  res.json(notification);
55
55
  });
56
56
 
57
57
  // ── Notification history & management ─────────────────────
58
- router.get("/history", (req, res) => {
58
+ router.get("/history", async (req, res) => {
59
59
  const limit = Math.min(parseInt(req.query.limit) || 20, 100);
60
60
  const offset = parseInt(req.query.offset) || 0;
61
61
  const unreadOnly = req.query.unread_only === "true";
62
62
  const type = req.query.type || null;
63
- const items = getNotificationHistory(limit, offset, unreadOnly, type);
63
+ const items = await getNotificationHistory(limit, offset, unreadOnly, type);
64
64
  res.json(items);
65
65
  });
66
66
 
67
- router.get("/unread-count", (_req, res) => {
68
- res.json({ count: getUnreadNotificationCount() });
67
+ router.get("/unread-count", async (_req, res) => {
68
+ res.json({ count: await getUnreadNotificationCount() });
69
69
  });
70
70
 
71
- router.post("/read", (req, res) => {
71
+ router.post("/read", async (req, res) => {
72
72
  const { ids, all, before } = req.body;
73
73
  if (all) {
74
- markAllNotificationsRead();
75
- broadcastReadUpdate([]);
74
+ await markAllNotificationsRead();
75
+ await broadcastReadUpdate([]);
76
76
  } else if (before) {
77
- markNotificationsReadBefore(before);
78
- broadcastReadUpdate([]);
77
+ await markNotificationsReadBefore(before);
78
+ await broadcastReadUpdate([]);
79
79
  } else if (Array.isArray(ids) && ids.length > 0) {
80
- markNotificationsRead(ids);
81
- broadcastReadUpdate(ids);
80
+ await markNotificationsRead(ids);
81
+ await broadcastReadUpdate(ids);
82
82
  } else {
83
83
  return res.status(400).json({ error: "Provide ids, all, or before" });
84
84
  }
85
- res.json({ ok: true, unreadCount: getUnreadNotificationCount() });
85
+ res.json({ ok: true, unreadCount: await getUnreadNotificationCount() });
86
86
  });
87
87
 
88
- router.delete("/old", (_req, res) => {
89
- purgeOldNotifications(90);
88
+ router.delete("/old", async (_req, res) => {
89
+ await purgeOldNotifications(90);
90
90
  res.json({ ok: true });
91
91
  });
92
92