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
|
@@ -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;
|
package/server/routes/memory.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|