claudeck 1.0.7 → 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 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.7",
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
- // Listen for project changes (projectSelect is a DOM <select>, not in store)
162
- const projectSelect = document.getElementById('project-select');
163
- if (projectSelect) {
164
- projectSelect.addEventListener('change', () => {
165
- if (isDirty && !confirm('You have unsaved CLAUDE.md changes. Discard them?')) return;
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', () => {