claudeck 1.0.6 → 1.1.0
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/db.js +199 -0
- package/package.json +1 -1
- package/plugins/claude-editor/client.js +5 -8
- package/public/css/panels/memory.css +550 -0
- package/public/css/ui/messages.css +69 -0
- package/public/css/ui/status-bar.css +3 -0
- package/public/index.html +23 -0
- package/public/js/core/dom.js +10 -0
- package/public/js/features/chat.js +57 -0
- package/public/js/main.js +1 -0
- package/public/js/panels/memory.js +439 -0
- package/public/js/ui/status-bar.js +12 -2
- package/public/js/ui/tab-sdk.js +41 -2
- package/public/style.css +1 -0
- package/server/agent-loop.js +31 -3
- package/server/memory-extractor.js +139 -0
- package/server/memory-injector.js +221 -0
- package/server/memory-optimizer.js +279 -0
- package/server/orchestrator.js +13 -1
- package/server/routes/memory.js +146 -0
- package/server/ws-handler.js +81 -1
- package/server.js +7 -0
package/db.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
|
+
import { createHash } from "crypto";
|
|
2
3
|
import { dbPath } from "./server/paths.js";
|
|
3
4
|
|
|
4
5
|
const db = new Database(dbPath);
|
|
@@ -119,6 +120,75 @@ db.exec(`
|
|
|
119
120
|
CREATE INDEX IF NOT EXISTS idx_agent_runs_run_id ON agent_runs(run_id);
|
|
120
121
|
`);
|
|
121
122
|
|
|
123
|
+
// Persistent memories table (cross-session context)
|
|
124
|
+
db.exec(`
|
|
125
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
126
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
127
|
+
project_path TEXT NOT NULL,
|
|
128
|
+
category TEXT NOT NULL DEFAULT 'discovery',
|
|
129
|
+
content TEXT NOT NULL,
|
|
130
|
+
content_hash TEXT,
|
|
131
|
+
source_session_id TEXT,
|
|
132
|
+
source_agent_id TEXT,
|
|
133
|
+
relevance_score REAL DEFAULT 1.0,
|
|
134
|
+
created_at INTEGER DEFAULT (unixepoch()),
|
|
135
|
+
accessed_at INTEGER DEFAULT (unixepoch()),
|
|
136
|
+
expires_at INTEGER
|
|
137
|
+
);
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_path);
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
|
|
140
|
+
CREATE INDEX IF NOT EXISTS idx_memories_relevance ON memories(relevance_score DESC);
|
|
141
|
+
`);
|
|
142
|
+
|
|
143
|
+
// Migration: add content_hash column if missing (existing DBs)
|
|
144
|
+
try { db.exec(`ALTER TABLE memories ADD COLUMN content_hash TEXT`); } catch { /* already exists */ }
|
|
145
|
+
try { db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_hash ON memories(project_path, content_hash)`); } catch { /* already exists */ }
|
|
146
|
+
|
|
147
|
+
// FTS5 full-text search for memories
|
|
148
|
+
db.exec(`
|
|
149
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
150
|
+
content,
|
|
151
|
+
content='memories',
|
|
152
|
+
content_rowid='id'
|
|
153
|
+
);
|
|
154
|
+
`);
|
|
155
|
+
|
|
156
|
+
// Triggers to keep FTS in sync
|
|
157
|
+
db.exec(`
|
|
158
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
159
|
+
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
|
160
|
+
END;
|
|
161
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
162
|
+
INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
163
|
+
END;
|
|
164
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE OF content ON memories BEGIN
|
|
165
|
+
INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
166
|
+
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
|
167
|
+
END;
|
|
168
|
+
`);
|
|
169
|
+
|
|
170
|
+
// Backfill content_hash for existing rows
|
|
171
|
+
const unhashed = db.prepare(`SELECT id, project_path, content FROM memories WHERE content_hash IS NULL`).all();
|
|
172
|
+
if (unhashed.length > 0) {
|
|
173
|
+
const backfill = db.prepare(`UPDATE memories SET content_hash = ? WHERE id = ?`);
|
|
174
|
+
const backfillTx = db.transaction((rows) => {
|
|
175
|
+
for (const row of rows) {
|
|
176
|
+
const hash = createHash("sha256").update(`${row.project_path}:${row.content}`).digest("hex");
|
|
177
|
+
backfill.run(hash, row.id);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
backfillTx(unhashed);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Backfill FTS index for existing memories not yet indexed
|
|
184
|
+
try {
|
|
185
|
+
const ftsCount = db.prepare(`SELECT COUNT(*) as c FROM memories_fts`).get();
|
|
186
|
+
const memCount = db.prepare(`SELECT COUNT(*) as c FROM memories`).get();
|
|
187
|
+
if (ftsCount.c < memCount.c) {
|
|
188
|
+
db.exec(`INSERT INTO memories_fts(memories_fts) VALUES ('rebuild')`);
|
|
189
|
+
}
|
|
190
|
+
} catch { /* ignore */ }
|
|
191
|
+
|
|
122
192
|
// Brags table
|
|
123
193
|
db.exec(`
|
|
124
194
|
CREATE TABLE IF NOT EXISTS brags (
|
|
@@ -1193,6 +1263,135 @@ export function getAgentRunsDaily() {
|
|
|
1193
1263
|
return runStmts.dailyRuns.all();
|
|
1194
1264
|
}
|
|
1195
1265
|
|
|
1266
|
+
// ── Memories (persistent cross-session context) ──────────
|
|
1267
|
+
function hashContent(projectPath, content) {
|
|
1268
|
+
return createHash("sha256").update(`${projectPath}:${content}`).digest("hex");
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const memStmts = {
|
|
1272
|
+
insert: db.prepare(
|
|
1273
|
+
`INSERT OR IGNORE INTO memories (project_path, category, content, content_hash, source_session_id, source_agent_id)
|
|
1274
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1275
|
+
),
|
|
1276
|
+
findByHash: db.prepare(
|
|
1277
|
+
`SELECT id FROM memories WHERE project_path = ? AND content_hash = ?`
|
|
1278
|
+
),
|
|
1279
|
+
list: db.prepare(
|
|
1280
|
+
`SELECT * FROM memories WHERE project_path = ?
|
|
1281
|
+
ORDER BY relevance_score DESC, accessed_at DESC`
|
|
1282
|
+
),
|
|
1283
|
+
listByCategory: db.prepare(
|
|
1284
|
+
`SELECT * FROM memories WHERE project_path = ? AND category = ?
|
|
1285
|
+
ORDER BY relevance_score DESC, accessed_at DESC`
|
|
1286
|
+
),
|
|
1287
|
+
searchFts: db.prepare(
|
|
1288
|
+
`SELECT m.* FROM memories m
|
|
1289
|
+
JOIN memories_fts fts ON fts.rowid = m.id
|
|
1290
|
+
WHERE m.project_path = ? AND memories_fts MATCH ?
|
|
1291
|
+
ORDER BY rank, m.relevance_score DESC LIMIT ?`
|
|
1292
|
+
),
|
|
1293
|
+
searchLike: db.prepare(
|
|
1294
|
+
`SELECT * FROM memories WHERE project_path = ? AND content LIKE ?
|
|
1295
|
+
ORDER BY relevance_score DESC LIMIT ?`
|
|
1296
|
+
),
|
|
1297
|
+
topRelevant: db.prepare(
|
|
1298
|
+
`SELECT * FROM memories WHERE project_path = ?
|
|
1299
|
+
ORDER BY relevance_score DESC, accessed_at DESC LIMIT ?`
|
|
1300
|
+
),
|
|
1301
|
+
update: db.prepare(
|
|
1302
|
+
`UPDATE memories SET content = ?, category = ? WHERE id = ?`
|
|
1303
|
+
),
|
|
1304
|
+
touch: db.prepare(
|
|
1305
|
+
`UPDATE memories SET accessed_at = unixepoch(),
|
|
1306
|
+
relevance_score = MIN(relevance_score + 0.1, 2.0) WHERE id = ?`
|
|
1307
|
+
),
|
|
1308
|
+
decay: db.prepare(
|
|
1309
|
+
`UPDATE memories SET relevance_score = MAX(relevance_score * 0.95, 0.1)
|
|
1310
|
+
WHERE project_path = ? AND accessed_at < unixepoch() - ?`
|
|
1311
|
+
),
|
|
1312
|
+
delete: db.prepare(`DELETE FROM memories WHERE id = ?`),
|
|
1313
|
+
deleteExpired: db.prepare(
|
|
1314
|
+
`DELETE FROM memories WHERE expires_at IS NOT NULL AND expires_at < unixepoch()`
|
|
1315
|
+
),
|
|
1316
|
+
count: db.prepare(
|
|
1317
|
+
`SELECT category, COUNT(*) as count FROM memories
|
|
1318
|
+
WHERE project_path = ? GROUP BY category`
|
|
1319
|
+
),
|
|
1320
|
+
stats: db.prepare(
|
|
1321
|
+
`SELECT COUNT(*) as total,
|
|
1322
|
+
SUM(CASE WHEN accessed_at > unixepoch() - 86400 THEN 1 ELSE 0 END) as accessed_today,
|
|
1323
|
+
AVG(relevance_score) as avg_relevance
|
|
1324
|
+
FROM memories WHERE project_path = ?`
|
|
1325
|
+
),
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
export function createMemory(projectPath, category, content, sourceSessionId = null, sourceAgentId = null) {
|
|
1329
|
+
const hash = hashContent(projectPath, content);
|
|
1330
|
+
// Dedup: if identical content already exists, just touch it
|
|
1331
|
+
const existing = memStmts.findByHash.get(projectPath, hash);
|
|
1332
|
+
if (existing) {
|
|
1333
|
+
memStmts.touch.run(existing.id);
|
|
1334
|
+
return { lastInsertRowid: existing.id, changes: 0, isDuplicate: true };
|
|
1335
|
+
}
|
|
1336
|
+
return memStmts.insert.run(projectPath, category, content, hash, sourceSessionId, sourceAgentId);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
export function listMemories(projectPath, category = null) {
|
|
1340
|
+
if (category) return memStmts.listByCategory.all(projectPath, category);
|
|
1341
|
+
return memStmts.list.all(projectPath);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
export function searchMemories(projectPath, queryText, limit = 20) {
|
|
1345
|
+
// Try FTS5 first, fall back to LIKE for non-FTS-compatible queries
|
|
1346
|
+
try {
|
|
1347
|
+
const ftsQuery = queryText.split(/\s+/).filter(Boolean).map(w => `"${w}"`).join(" OR ");
|
|
1348
|
+
if (ftsQuery) {
|
|
1349
|
+
return memStmts.searchFts.all(projectPath, ftsQuery, limit);
|
|
1350
|
+
}
|
|
1351
|
+
} catch {
|
|
1352
|
+
// FTS parse error — fall back
|
|
1353
|
+
}
|
|
1354
|
+
return memStmts.searchLike.all(projectPath, `%${queryText}%`, limit);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
export function getTopMemories(projectPath, limit = 10) {
|
|
1358
|
+
return memStmts.topRelevant.all(projectPath, limit);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
export function updateMemory(id, content, category) {
|
|
1362
|
+
return memStmts.update.run(content, category, id);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
export function touchMemory(id) {
|
|
1366
|
+
return memStmts.touch.run(id);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
export function decayMemories(projectPath, olderThanSecs = 604800) {
|
|
1370
|
+
return memStmts.decay.run(projectPath, olderThanSecs);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
export function deleteMemory(id) {
|
|
1374
|
+
return memStmts.delete.run(id);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
export function deleteExpiredMemories() {
|
|
1378
|
+
return memStmts.deleteExpired.run();
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
export function getMemoryCounts(projectPath) {
|
|
1382
|
+
return memStmts.count.all(projectPath);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
export function getMemoryStats(projectPath) {
|
|
1386
|
+
return memStmts.stats.get(projectPath);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Run decay + cleanup for a project (call on session start)
|
|
1390
|
+
export function maintainMemories(projectPath) {
|
|
1391
|
+
decayMemories(projectPath, 604800); // 7 days
|
|
1392
|
+
deleteExpiredMemories();
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1196
1395
|
export function getDb() {
|
|
1197
1396
|
return db;
|
|
1198
1397
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudeck",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A browser-based UI for Claude Code — chat, run workflows, manage MCP servers, track costs, and orchestrate autonomous agents from a local web interface. Installable as a PWA.",
|
|
6
6
|
"main": "server.js",
|
|
@@ -158,14 +158,11 @@ registerTab({
|
|
|
158
158
|
}
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
loadFile();
|
|
167
|
-
});
|
|
168
|
-
}
|
|
161
|
+
// Reload when project changes
|
|
162
|
+
ctx.on('projectChanged', () => {
|
|
163
|
+
if (isDirty && !confirm('You have unsaved CLAUDE.md changes. Discard them?')) return;
|
|
164
|
+
loadFile();
|
|
165
|
+
});
|
|
169
166
|
|
|
170
167
|
// Also reload when projects data arrives (covers initial page load)
|
|
171
168
|
ctx.onState('projectsData', () => {
|