claudeck 1.0.7 → 1.1.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 CHANGED
@@ -44,6 +44,7 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
44
44
 
45
45
  - **Zero-framework** — Vanilla JS, 6 npm dependencies, no build step
46
46
  - **Full agent orchestration** — Chains, DAGs, orchestrator, and monitoring dashboard
47
+ - **Persistent memory** — Cross-session project knowledge with FTS5 search and AI optimization
47
48
  - **Cost visibility** — Per-session tracking, daily charts, token breakdowns
48
49
  - **Works everywhere** — PWA, mobile responsive, Telegram AFK approval
49
50
  - **Extensible** — Full-stack plugin system with auto-discovery
@@ -87,6 +88,15 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
87
88
  - Input/output token breakdown, streaming token counter
88
89
  - Error pattern analysis (9 categories), tool usage stats
89
90
 
91
+ ### Persistent Memory
92
+
93
+ - Cross-session project knowledge that survives restarts
94
+ - Auto-capture from assistant responses using pattern-based heuristic extraction
95
+ - `/remember` command for manual memory creation
96
+ - FTS5 full-text search with relevance scoring and time-decay
97
+ - AI-powered optimization (consolidation via Claude Haiku)
98
+ - Memory panel in right sidebar with search, filtering, and inline editing
99
+
90
100
  ### Integrations
91
101
 
92
102
  - **MCP Manager** — Add/edit/remove MCP servers (global + per-project)
@@ -127,8 +137,9 @@ browser ──── WebSocket ──── server.js ──── Claude Code S
127
137
  server/routes/ ~/.claudeck/
128
138
  server/agent-loop.js ├── config/ (JSON configs)
129
139
  server/orchestrator.js ├── plugins/ (user plugins)
130
- server/dag-executor.js ├── data.db (SQLite)
131
- plugins/ └── .env (VAPID keys)
140
+ server/dag-executor.js ├── data.db (SQLite + memories)
141
+ server/memory-optimizer.js └── .env (VAPID keys)
142
+ plugins/
132
143
  ```
133
144
 
134
145
  | Layer | Technology |
@@ -148,6 +159,7 @@ browser ──── WebSocket ──── server.js ──── Claude Code S
148
159
  /clear /new /parallel /export /theme /shortcuts App
149
160
  /costs /analytics Dashboards
150
161
  /files /git /repos /events /mcp /tips Panels
162
+ /remember Memory
151
163
  /review-pr /onboard-repo /migration-plan /code-health Workflows
152
164
  /agent-pr-reviewer /agent-bug-hunter /agent-test-writer Agents
153
165
  /orchestrate /monitor /chain-* /dag-* Multi-Agent
@@ -199,7 +211,7 @@ See [CONFIGURATION.md](docs/CONFIGURATION.md) for the full guide.
199
211
 
200
212
  ## Plugins
201
213
 
202
- Claudeck includes 6 built-in plugins and supports user plugins via `~/.claudeck/plugins/`:
214
+ Claudeck includes 7 built-in plugins and supports user plugins via `~/.claudeck/plugins/`:
203
215
 
204
216
  | Plugin | Description |
205
217
  |--------|-------------|
@@ -228,6 +240,7 @@ npx skills add https://github.com/hamedafarag/claudeck-skills
228
240
  |----------|-------------|
229
241
  | [DOCUMENTATION.md](docs/DOCUMENTATION.md) | Full feature docs, API reference, database schema |
230
242
  | [CONFIGURATION.md](docs/CONFIGURATION.md) | User data directory, config files, plugin system |
243
+ | [AGENT-ARCHITECTURE.md](docs/AGENT-ARCHITECTURE.md) | How agents, chains, DAGs, and orchestrator work |
231
244
  | [CROSS-PLATFORM-AUDIT.md](docs/CROSS-PLATFORM-AUDIT.md) | Windows/Linux compatibility |
232
245
  | [COMPETITIVE-ANALYSIS.md](docs/COMPETITIVE-ANALYSIS.md) | Feature comparison with similar tools |
233
246
 
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.1",
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', () => {