claude-mem-lite 2.12.2 → 2.13.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/cli.mjs +10 -2
- package/commands/mem.md +15 -18
- package/commands/recall.md +9 -0
- package/commands/recent.md +7 -0
- package/commands/search.md +9 -0
- package/commands/timeline.md +7 -0
- package/hook-shared.mjs +2 -87
- package/hook-update.mjs +2 -3
- package/hook.mjs +24 -210
- package/hooks/hooks.json +5 -12
- package/install-metadata.mjs +2072 -0
- package/install.mjs +82 -25
- package/mem-cli.mjs +595 -0
- package/package.json +9 -6
- package/scripts/user-prompt-search.js +271 -0
- package/server.mjs +12 -25
- package/skip-tools.mjs +21 -0
- package/dispatch-feedback.mjs +0 -382
- package/dispatch-inject.mjs +0 -213
- package/dispatch-patterns.mjs +0 -173
- package/dispatch-workflow.mjs +0 -170
- package/dispatch.mjs +0 -1239
package/cli.mjs
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'help']);
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const cmd = process.argv[2];
|
|
5
|
+
|
|
6
|
+
if (CLI_COMMANDS.has(cmd)) {
|
|
7
|
+
const { run } = await import('./mem-cli.mjs');
|
|
8
|
+
await run(process.argv.slice(2));
|
|
9
|
+
} else {
|
|
10
|
+
const { main } = await import('./install.mjs');
|
|
11
|
+
await main(process.argv.slice(2));
|
|
12
|
+
}
|
package/commands/mem.md
CHANGED
|
@@ -2,39 +2,36 @@
|
|
|
2
2
|
description: Search and manage project memory (observations, sessions, prompts)
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
# Memory
|
|
5
|
+
# Memory
|
|
6
6
|
|
|
7
7
|
Search and browse your project memory efficiently.
|
|
8
8
|
|
|
9
|
-
## Commands
|
|
9
|
+
## Quick Commands
|
|
10
10
|
|
|
11
|
-
- `/mem search <query>` — FTS5 full-text search
|
|
12
|
-
- `/mem recent [n]` — Show recent N observations (default
|
|
11
|
+
- `/mem search <query>` — Search all memories (FTS5 full-text search)
|
|
12
|
+
- `/mem recent [n]` — Show recent N observations (default 5)
|
|
13
|
+
- `/mem recall <file>` — History for a file before editing
|
|
14
|
+
- `/mem timeline <id>` — Browse timeline around an observation
|
|
13
15
|
- `/mem save <text>` — Save a manual memory/note
|
|
14
16
|
- `/mem stats` — Show memory statistics
|
|
15
|
-
- `/mem timeline <query>` — Browse timeline around a matching observation
|
|
16
17
|
- `/mem cleanup` — Scan and interactively purge stale data
|
|
17
18
|
- `/mem cleanup [N]d` — Purge stale data older than N days (e.g. `cleanup 60d`)
|
|
18
19
|
- `/mem cleanup keep [N]d` — Purge stale data but retain last N days (e.g. `cleanup keep 14d`)
|
|
19
20
|
|
|
20
|
-
## Efficient Search Workflow (3 steps, saves 10x tokens)
|
|
21
|
-
|
|
22
|
-
1. **Search** → `mem_search(query="...")` → get compact ID index
|
|
23
|
-
2. **Browse** → `mem_timeline(anchor=ID)` → see surrounding context
|
|
24
|
-
3. **Detail** → `mem_get(ids=[...])` → get full content for specific IDs
|
|
25
|
-
|
|
26
21
|
## Instructions
|
|
27
22
|
|
|
28
23
|
When the user invokes `/mem`, parse their intent:
|
|
29
24
|
|
|
30
|
-
- `/mem search <query>` →
|
|
31
|
-
- `/mem recent` or `/mem recent 20` →
|
|
32
|
-
- `/mem
|
|
33
|
-
- `/mem
|
|
34
|
-
- `/mem
|
|
25
|
+
- `/mem search <query>` → run `claude-mem-lite search <query>` via Bash
|
|
26
|
+
- `/mem recent` or `/mem recent 20` → run `claude-mem-lite recent [N]` via Bash
|
|
27
|
+
- `/mem recall <file>` → run `claude-mem-lite recall <file>` via Bash
|
|
28
|
+
- `/mem timeline <id>` → run `claude-mem-lite timeline --anchor <id>` via Bash
|
|
29
|
+
- `/mem save <text>` → call `mem_save` MCP tool with the text as content
|
|
30
|
+
- `/mem stats` → run `claude-mem-lite stats` via Bash
|
|
31
|
+
- `/mem get <ids>` → run `claude-mem-lite get <ids>` via Bash
|
|
35
32
|
- `/mem cleanup` → run `mem_maintain(action="scan")`, report pending purge count and stale items to user, ask for confirmation, then run `mem_maintain(action="execute", operations=["purge_stale"])` if confirmed
|
|
36
33
|
- `/mem cleanup Nd` (e.g. `60d`) → same as above but use `retain_days=N` to only purge items older than N days
|
|
37
34
|
- `/mem cleanup keep Nd` (e.g. `keep 14d`) → same as above with `retain_days=N`
|
|
38
|
-
- `/mem <query>` (no subcommand) → treat as search,
|
|
35
|
+
- `/mem <query>` (no subcommand) → treat as search, run `claude-mem-lite search <query>` via Bash
|
|
39
36
|
|
|
40
|
-
|
|
37
|
+
Use Bash commands first. For detailed data, use `claude-mem-lite get <id>` via Bash.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Search memory for past bugfixes, decisions, discoveries
|
|
3
|
+
argument-hint: <query>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Memory Search
|
|
7
|
+
!`claude-mem-lite search $ARGUMENTS 2>/dev/null || echo "No results found"`
|
|
8
|
+
|
|
9
|
+
Use `claude-mem-lite get <id>` via Bash for full details on any result.
|
package/hook-shared.mjs
CHANGED
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
import { execFileSync, spawn } from 'child_process';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import { join } from 'path';
|
|
7
|
-
import { existsSync, readFileSync, writeFileSync,
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs';
|
|
8
8
|
import { inferProject, debugCatch } from './utils.mjs';
|
|
9
|
-
import { ensureDb, DB_DIR
|
|
10
|
-
import { ensureRegistryDb } from './registry.mjs';
|
|
9
|
+
import { ensureDb, DB_DIR } from './schema.mjs';
|
|
11
10
|
import { getClaudePath as getClaudePathShared, resolveModel as resolveModelShared } from './haiku-client.mjs';
|
|
12
11
|
|
|
13
12
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
@@ -24,7 +23,6 @@ export const STALE_LOCK_MS = 30000; // 30s
|
|
|
24
23
|
export const DEDUP_WINDOW_MS = 5 * 60 * 1000; // 5 min (title dedup)
|
|
25
24
|
export const RELATED_OBS_WINDOW_MS = 7 * 86400000; // 7 days
|
|
26
25
|
export const FALLBACK_OBS_WINDOW_MS = RELATED_OBS_WINDOW_MS; // same window
|
|
27
|
-
export const RESOURCE_RESCAN_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
28
26
|
|
|
29
27
|
// Handoff system constants
|
|
30
28
|
export const HANDOFF_EXPIRY_CLEAR = 6 * 3600000; // 6 hours (covers lunch/meeting breaks)
|
|
@@ -69,20 +67,6 @@ export function openDb() {
|
|
|
69
67
|
}
|
|
70
68
|
}
|
|
71
69
|
|
|
72
|
-
// ─── Registry Database (dispatch system) ─────────────────────────────────────
|
|
73
|
-
let _registryDb = null;
|
|
74
|
-
|
|
75
|
-
export function getRegistryDb() {
|
|
76
|
-
if (_registryDb) return _registryDb;
|
|
77
|
-
try { _registryDb = ensureRegistryDb(REGISTRY_DB_PATH); } catch (e) { debugCatch(e, 'getRegistryDb'); }
|
|
78
|
-
return _registryDb;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function closeRegistryDb() {
|
|
82
|
-
if (_registryDb) try { _registryDb.close(); } catch {}
|
|
83
|
-
_registryDb = null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
70
|
// ─── LLM via claude CLI ─────────────────────────────────────────────────────
|
|
87
71
|
|
|
88
72
|
export function callLLM(prompt, timeoutMs = 15000) {
|
|
@@ -127,75 +111,6 @@ export function spawnBackground(bgEvent, ...extraArgs) {
|
|
|
127
111
|
|
|
128
112
|
export function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
129
113
|
|
|
130
|
-
// ─── Injection Budget (per hook invocation, in-memory) ───────────────────────
|
|
131
|
-
// Limits context injections within a single hook process to prevent context bloat.
|
|
132
|
-
// Note: each hook event runs in a separate process, so this is per-invocation,
|
|
133
|
-
// not per-session. Session-level dedup is handled by cooldown/sessionId checks.
|
|
134
|
-
|
|
135
|
-
export const MAX_INJECTIONS_PER_SESSION = 3;
|
|
136
|
-
let _injectionCount = 0;
|
|
137
|
-
|
|
138
|
-
export function getInjectionCount() { return _injectionCount; }
|
|
139
|
-
export function incrementInjection() { _injectionCount++; }
|
|
140
|
-
export function resetInjectionBudget() { _injectionCount = 0; }
|
|
141
|
-
export function hasInjectionBudget() { return _injectionCount < MAX_INJECTIONS_PER_SESSION; }
|
|
142
|
-
|
|
143
|
-
// ─── Previous Session Context (for user-prompt dispatch enrichment) ──────────
|
|
144
|
-
// Session-start caches next_steps; first user-prompt reads+clears for richer dispatch.
|
|
145
|
-
|
|
146
|
-
// ─── Tool Event Tracking (for dispatch feedback) ────────────────────────────
|
|
147
|
-
// PostToolUse appends feedback-relevant tool events (Skill, Task, Edit, Write, Bash errors).
|
|
148
|
-
// Stop handler reads them and passes to collectFeedback for adoption/outcome detection.
|
|
149
|
-
|
|
150
|
-
export function toolEventsFile() {
|
|
151
|
-
return join(RUNTIME_DIR, `tool-events-${inferProject()}.jsonl`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Append a tool event for feedback tracking.
|
|
156
|
-
* Only call for feedback-relevant events (Skill, Task, Edit, Write, Bash).
|
|
157
|
-
* @param {object} event { tool_name, tool_input, tool_response }
|
|
158
|
-
*/
|
|
159
|
-
export function appendToolEvent(event) {
|
|
160
|
-
try {
|
|
161
|
-
appendFileSync(toolEventsFile(), JSON.stringify(event) + '\n');
|
|
162
|
-
} catch {}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Read all tracked tool events and remove the file.
|
|
167
|
-
* Uses rename→read→delete for atomicity.
|
|
168
|
-
* @returns {object[]} Array of tool event objects
|
|
169
|
-
*/
|
|
170
|
-
export function readAndClearToolEvents() {
|
|
171
|
-
const file = toolEventsFile();
|
|
172
|
-
const claimFile = file + `.claim-${process.pid}-${Date.now()}`;
|
|
173
|
-
try {
|
|
174
|
-
renameSync(file, claimFile);
|
|
175
|
-
const raw = readFileSync(claimFile, 'utf8');
|
|
176
|
-
try { unlinkSync(claimFile); } catch {}
|
|
177
|
-
return raw.trim().split('\n').filter(Boolean).map(line => {
|
|
178
|
-
try { return JSON.parse(line); } catch { return null; }
|
|
179
|
-
}).filter(Boolean);
|
|
180
|
-
} catch {
|
|
181
|
-
return [];
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Read tracked tool events WITHOUT clearing the file.
|
|
187
|
-
* Used by dispatch to detect active suites mid-session.
|
|
188
|
-
* @returns {object[]} Array of tool event objects
|
|
189
|
-
*/
|
|
190
|
-
export function peekToolEvents() {
|
|
191
|
-
try {
|
|
192
|
-
const raw = readFileSync(toolEventsFile(), 'utf8');
|
|
193
|
-
return raw.trim().split('\n').filter(Boolean).map(line => {
|
|
194
|
-
try { return JSON.parse(line); } catch { return null; }
|
|
195
|
-
}).filter(Boolean);
|
|
196
|
-
} catch { return []; }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
114
|
/**
|
|
200
115
|
* Extract partial response from CLI error output (timeout/error recovery).
|
|
201
116
|
* @param {Error} error The caught error from execFileSync
|
package/hook-update.mjs
CHANGED
|
@@ -194,13 +194,12 @@ export function getCurrentVersion() {
|
|
|
194
194
|
// ── Source files to copy (must match install.mjs SOURCE_FILES) ──
|
|
195
195
|
const SOURCE_FILES = [
|
|
196
196
|
'server.mjs', 'server-internals.mjs', 'tool-schemas.mjs',
|
|
197
|
-
'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs',
|
|
197
|
+
'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs', 'skip-tools.mjs',
|
|
198
198
|
'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs', 'hook-update.mjs',
|
|
199
199
|
'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'package-lock.json', 'skill.md',
|
|
200
200
|
'registry.mjs', 'registry-scanner.mjs', 'registry-indexer.mjs',
|
|
201
201
|
'registry-retriever.mjs', 'resource-discovery.mjs',
|
|
202
|
-
'
|
|
203
|
-
'install.mjs', 'install-metadata.mjs',
|
|
202
|
+
'install.mjs', 'install-metadata.mjs', 'mem-cli.mjs',
|
|
204
203
|
];
|
|
205
204
|
const SWITCHABLE_PATHS = [...SOURCE_FILES, 'scripts', 'registry', 'node_modules'];
|
|
206
205
|
|
package/hook.mjs
CHANGED
|
@@ -21,15 +21,11 @@ import {
|
|
|
21
21
|
writePendingEntry, mergePendingEntries, episodeHasSignificantContent,
|
|
22
22
|
} from './hook-episode.mjs';
|
|
23
23
|
import { selectWithTokenBudget, updateClaudeMd, buildSummaryLines } from './hook-context.mjs';
|
|
24
|
-
import { dispatchOnPreToolUse, dispatchOnUserPrompt } from './dispatch.mjs';
|
|
25
|
-
import { collectFeedback } from './dispatch-feedback.mjs';
|
|
26
24
|
import {
|
|
27
25
|
RUNTIME_DIR, EPISODE_BUFFER_SIZE, EPISODE_TIME_GAP_MS,
|
|
28
26
|
SESSION_EXPIRY_MS, STALE_SESSION_MS, STALE_LOCK_MS, FALLBACK_OBS_WINDOW_MS,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
closeRegistryDb, spawnBackground, appendToolEvent, readAndClearToolEvents,
|
|
32
|
-
resetInjectionBudget, hasInjectionBudget, incrementInjection,
|
|
27
|
+
sessionFile, getSessionId, createSessionId, openDb,
|
|
28
|
+
spawnBackground,
|
|
33
29
|
} from './hook-shared.mjs';
|
|
34
30
|
import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObservation } from './hook-llm.mjs';
|
|
35
31
|
import { searchRelevantMemories, recallForFile } from './hook-memory.mjs';
|
|
@@ -38,9 +34,9 @@ import { checkForUpdate } from './hook-update.mjs';
|
|
|
38
34
|
import { SKIP_TOOLS, SKIP_PREFIXES } from './skip-tools.mjs';
|
|
39
35
|
|
|
40
36
|
// Prevent recursive hooks from background claude -p calls
|
|
41
|
-
// Background workers (llm-episode, llm-summary
|
|
37
|
+
// Background workers (llm-episode, llm-summary) are exempt — they're ours
|
|
42
38
|
const event = process.argv[2];
|
|
43
|
-
const BG_EVENTS = new Set(['llm-episode', 'llm-summary'
|
|
39
|
+
const BG_EVENTS = new Set(['llm-episode', 'llm-summary']);
|
|
44
40
|
|
|
45
41
|
// Respect Claude Code plugin disable state even when legacy settings.json hooks remain.
|
|
46
42
|
// install.mjs writes direct hooks into ~/.claude/settings.json, so disabling the plugin
|
|
@@ -253,21 +249,6 @@ async function handlePostToolUse() {
|
|
|
253
249
|
|
|
254
250
|
writeEpisode(episode);
|
|
255
251
|
|
|
256
|
-
// Track feedback-relevant tool events for dispatch adoption detection.
|
|
257
|
-
// Skill/Agent: adoption detection checks these tool names.
|
|
258
|
-
// Edit/Write/NotebookEdit: outcome detection checks for edits.
|
|
259
|
-
// Grep: investigation signal for debugging pattern detection.
|
|
260
|
-
// Bash errors: outcome detection checks for error signals.
|
|
261
|
-
// Bash test/build success: TDD and verification pattern detection.
|
|
262
|
-
const isTrackableBash = tool_name === 'Bash' && (bashSig?.isError || bashSig?.isTest || bashSig?.isBuild);
|
|
263
|
-
if (['Skill', 'Agent', 'Edit', 'Write', 'NotebookEdit', 'Grep'].includes(tool_name) || isTrackableBash) {
|
|
264
|
-
appendToolEvent({
|
|
265
|
-
tool_name,
|
|
266
|
-
tool_input: toolInput,
|
|
267
|
-
tool_response: (tool_name === 'Bash' && bashSig?.isSignificant) ? scrubSecrets(resp.slice(0, 500)) : '',
|
|
268
|
-
timestamp: Date.now(),
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
252
|
} finally {
|
|
272
253
|
releaseLock();
|
|
273
254
|
if (db) try { db.close(); } catch {}
|
|
@@ -396,18 +377,6 @@ async function handleStop() {
|
|
|
396
377
|
}
|
|
397
378
|
}
|
|
398
379
|
|
|
399
|
-
// Dispatch: collect feedback on recommendations using actual tool events
|
|
400
|
-
// PostToolUse tracks Skill/Task/Edit/Write/Bash events in a JSONL file.
|
|
401
|
-
// These events drive adoption detection (Skill/Task) and outcome detection (Edit/Bash errors).
|
|
402
|
-
// Always clear event file to prevent stale events accumulating if registry DB is unavailable.
|
|
403
|
-
try {
|
|
404
|
-
const sessionEvents = readAndClearToolEvents();
|
|
405
|
-
const rdb = getRegistryDb();
|
|
406
|
-
if (rdb) {
|
|
407
|
-
await collectFeedback(rdb, sessionId, sessionEvents);
|
|
408
|
-
}
|
|
409
|
-
} catch (e) { debugCatch(e, 'handleStop-feedback'); }
|
|
410
|
-
|
|
411
380
|
// Spawn background for session summary (pass sessionId and project)
|
|
412
381
|
spawnBackground('llm-summary', sessionId, project);
|
|
413
382
|
|
|
@@ -418,8 +387,6 @@ async function handleStop() {
|
|
|
418
387
|
// ─── SessionStart Handler + CLAUDE.md Persistence (Tier 1 A, E) ─────────────
|
|
419
388
|
|
|
420
389
|
async function handleSessionStart() {
|
|
421
|
-
resetInjectionBudget();
|
|
422
|
-
|
|
423
390
|
// Snapshot episode BEFORE flush for handoff extraction
|
|
424
391
|
const episodeSnapshot = readEpisodeRaw();
|
|
425
392
|
|
|
@@ -538,15 +505,6 @@ async function handleSessionStart() {
|
|
|
538
505
|
).get(prevProject || project, 'clear');
|
|
539
506
|
} catch {}
|
|
540
507
|
|
|
541
|
-
// Collect dispatch feedback for previous session
|
|
542
|
-
try {
|
|
543
|
-
const rdb = getRegistryDb();
|
|
544
|
-
if (rdb) {
|
|
545
|
-
const sessionEvents = readAndClearToolEvents();
|
|
546
|
-
await collectFeedback(rdb, prevSessionId, sessionEvents);
|
|
547
|
-
}
|
|
548
|
-
} catch (e) { debugCatch(e, 'session-start-prev-feedback'); }
|
|
549
|
-
|
|
550
508
|
// Generate session summary for previous session (background Haiku — richer version)
|
|
551
509
|
spawnBackground('llm-summary', prevSessionId, prevProject || project);
|
|
552
510
|
|
|
@@ -759,13 +717,6 @@ async function handleSessionStart() {
|
|
|
759
717
|
// CLAUDE.md: slim (summary + handoff state — observations already in stdout)
|
|
760
718
|
updateClaudeMd([...summaryLines, ...handoffLines].join('\n'));
|
|
761
719
|
|
|
762
|
-
// Background rescan: detect changed/new managed resources since last scan.
|
|
763
|
-
// TTL-based (1h) — avoids redundant filesystem scans on every session.
|
|
764
|
-
// Non-blocking: spawns detached worker, results available before first user prompt.
|
|
765
|
-
if (needsResourceRescan()) {
|
|
766
|
-
spawnBackground('resource-scan');
|
|
767
|
-
}
|
|
768
|
-
|
|
769
720
|
// Auto-update check (24h throttle, 3s timeout, silent on failure)
|
|
770
721
|
// Fire-and-forget: don't block SessionStart for up to 3s network timeout
|
|
771
722
|
checkForUpdate().then(updateResult => {
|
|
@@ -784,51 +735,6 @@ async function handleSessionStart() {
|
|
|
784
735
|
}
|
|
785
736
|
}
|
|
786
737
|
|
|
787
|
-
// ─── PreToolUse Handler (Dispatch) ──────────────────────────────────────────
|
|
788
|
-
|
|
789
|
-
async function handlePreToolUse() {
|
|
790
|
-
let raw;
|
|
791
|
-
try { raw = await readStdin(); } catch { return; }
|
|
792
|
-
|
|
793
|
-
let hookData;
|
|
794
|
-
try { hookData = JSON.parse(raw.text); } catch { return; }
|
|
795
|
-
|
|
796
|
-
const rdb = getRegistryDb();
|
|
797
|
-
if (!rdb) return;
|
|
798
|
-
|
|
799
|
-
// Quick session context from user prompts DB + episode buffer
|
|
800
|
-
const sessionId = getSessionId();
|
|
801
|
-
const sessionCtx = { sessionId };
|
|
802
|
-
const db = openDb();
|
|
803
|
-
if (db) {
|
|
804
|
-
try {
|
|
805
|
-
const latest = db.prepare(
|
|
806
|
-
'SELECT prompt_text FROM user_prompts WHERE content_session_id = ? ORDER BY created_at_epoch DESC LIMIT 1'
|
|
807
|
-
).get(sessionId);
|
|
808
|
-
if (latest) sessionCtx.userPrompt = latest.prompt_text;
|
|
809
|
-
} catch (e) { debugCatch(e, 'handlePreToolUse-queryPrompt'); } finally { db.close(); }
|
|
810
|
-
}
|
|
811
|
-
// Collect recent file paths from episode buffer for tech stack inference
|
|
812
|
-
try {
|
|
813
|
-
const episode = readEpisodeRaw();
|
|
814
|
-
if (episode?.entries) {
|
|
815
|
-
const files = new Set();
|
|
816
|
-
for (const e of episode.entries) {
|
|
817
|
-
if (e.files) for (const f of e.files) files.add(f);
|
|
818
|
-
}
|
|
819
|
-
if (files.size > 0) sessionCtx.recentFiles = [...files].slice(-10);
|
|
820
|
-
}
|
|
821
|
-
} catch {}
|
|
822
|
-
|
|
823
|
-
if (hasInjectionBudget()) {
|
|
824
|
-
const injection = await dispatchOnPreToolUse(rdb, hookData, sessionCtx);
|
|
825
|
-
if (injection) {
|
|
826
|
-
process.stdout.write(injection + '\n');
|
|
827
|
-
incrementInjection();
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
738
|
// ─── UserPromptSubmit Handler ────────────────────────────────────────────────
|
|
833
739
|
|
|
834
740
|
async function handleUserPrompt() {
|
|
@@ -874,13 +780,12 @@ async function handleUserPrompt() {
|
|
|
874
780
|
);
|
|
875
781
|
|
|
876
782
|
// Cross-session handoff injection (first 3 prompts window, before semantic memory)
|
|
877
|
-
if (counter?.prompt_counter <= 3
|
|
783
|
+
if (counter?.prompt_counter <= 3) {
|
|
878
784
|
try {
|
|
879
785
|
if (detectContinuationIntent(db, promptText, project)) {
|
|
880
786
|
const injection = renderHandoffInjection(db, project);
|
|
881
787
|
if (injection) {
|
|
882
788
|
process.stdout.write(injection + '\n');
|
|
883
|
-
incrementInjection();
|
|
884
789
|
// Consume clear handoff after injection to prevent duplicate injection on prompts 2-3.
|
|
885
790
|
// Exit handoffs are kept (7d TTL, content-dependent keyword/FTS matching won't re-trigger).
|
|
886
791
|
try { db.prepare("DELETE FROM session_handoffs WHERE project = ? AND type = 'clear'").run(project); } catch {}
|
|
@@ -890,44 +795,29 @@ async function handleUserPrompt() {
|
|
|
890
795
|
}
|
|
891
796
|
|
|
892
797
|
// Semantic memory injection: search past observations for the user's prompt
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
lines.push(`- [${m.type}] ${truncate(m.title, 80)}${lessonTag} (#${m.id})`);
|
|
909
|
-
}
|
|
910
|
-
lines.push('</memory-context>');
|
|
911
|
-
process.stdout.write(lines.join('\n') + '\n');
|
|
912
|
-
incrementInjection();
|
|
798
|
+
try {
|
|
799
|
+
const keyObs = db.prepare(`
|
|
800
|
+
SELECT id FROM observations
|
|
801
|
+
WHERE project = ? AND COALESCE(compressed_into, 0) = 0
|
|
802
|
+
AND COALESCE(importance, 1) >= 2
|
|
803
|
+
ORDER BY created_at_epoch DESC LIMIT 5
|
|
804
|
+
`).all(project);
|
|
805
|
+
const keyContextIds = keyObs.map(o => o.id);
|
|
806
|
+
|
|
807
|
+
const memories = searchRelevantMemories(db, promptText, project, keyContextIds);
|
|
808
|
+
if (memories.length > 0) {
|
|
809
|
+
const lines = ['<memory-context relevance="high">'];
|
|
810
|
+
for (const m of memories) {
|
|
811
|
+
const lessonTag = m.lesson_learned ? ` | Lesson: ${m.lesson_learned}` : '';
|
|
812
|
+
lines.push(`- [${m.type}] ${truncate(m.title, 80)}${lessonTag} (#${m.id})`);
|
|
913
813
|
}
|
|
914
|
-
|
|
915
|
-
|
|
814
|
+
lines.push('</memory-context>');
|
|
815
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
816
|
+
}
|
|
817
|
+
} catch (e) { debugCatch(e, 'handleUserPrompt-memory'); }
|
|
916
818
|
} finally {
|
|
917
819
|
db.close();
|
|
918
820
|
}
|
|
919
|
-
|
|
920
|
-
// Dispatch: only fires for explicit user requests ("I need X skill", "find me a tool for Y")
|
|
921
|
-
try {
|
|
922
|
-
const rdb = getRegistryDb();
|
|
923
|
-
if (rdb && hasInjectionBudget()) {
|
|
924
|
-
const result = await dispatchOnUserPrompt(rdb, promptText, sessionId);
|
|
925
|
-
if (result) {
|
|
926
|
-
process.stdout.write(result + '\n');
|
|
927
|
-
incrementInjection();
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
} catch (e) { debugCatch(e, 'handleUserPrompt-dispatch'); }
|
|
931
821
|
}
|
|
932
822
|
|
|
933
823
|
// ─── Auto-Compress (Background Worker) ───────────────────────────────────────
|
|
@@ -1001,77 +891,6 @@ function handleAutoCompress() {
|
|
|
1001
891
|
}
|
|
1002
892
|
}
|
|
1003
893
|
|
|
1004
|
-
// ─── Resource Rescan (Background Worker) ─────────────────────────────────────
|
|
1005
|
-
|
|
1006
|
-
const RESCAN_MARKER = join(RUNTIME_DIR, 'last-resource-scan');
|
|
1007
|
-
|
|
1008
|
-
/**
|
|
1009
|
-
* Check if resource rescan is needed (marker older than RESOURCE_RESCAN_INTERVAL_MS).
|
|
1010
|
-
* @returns {boolean}
|
|
1011
|
-
*/
|
|
1012
|
-
function needsResourceRescan() {
|
|
1013
|
-
try {
|
|
1014
|
-
const marker = statSync(RESCAN_MARKER);
|
|
1015
|
-
return (Date.now() - marker.mtimeMs) > RESOURCE_RESCAN_INTERVAL_MS;
|
|
1016
|
-
} catch {
|
|
1017
|
-
return true; // No marker = never scanned
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
/**
|
|
1022
|
-
* Background worker: scan filesystem for changed resources, upsert any diffs.
|
|
1023
|
-
* Spawned by SessionStart when marker is stale. Non-blocking to the hook caller.
|
|
1024
|
-
*/
|
|
1025
|
-
async function handleResourceScan() {
|
|
1026
|
-
const rdb = getRegistryDb();
|
|
1027
|
-
if (!rdb) return;
|
|
1028
|
-
|
|
1029
|
-
try {
|
|
1030
|
-
const { scanAllResources, diffResources } = await import('./registry-scanner.mjs');
|
|
1031
|
-
const { upsertResource } = await import('./registry.mjs');
|
|
1032
|
-
|
|
1033
|
-
const scanned = scanAllResources();
|
|
1034
|
-
const { toIndex, toDisable } = diffResources(rdb, scanned);
|
|
1035
|
-
|
|
1036
|
-
if (toIndex.length === 0 && toDisable.length === 0) {
|
|
1037
|
-
// Touch marker even if nothing changed — avoids rescanning every session
|
|
1038
|
-
writeFileSync(RESCAN_MARKER, String(Date.now()));
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
// Upsert changed resources with fallback metadata (no Haiku)
|
|
1043
|
-
let upsertErrors = 0;
|
|
1044
|
-
for (const res of toIndex) {
|
|
1045
|
-
try {
|
|
1046
|
-
upsertResource(rdb, {
|
|
1047
|
-
name: res.name,
|
|
1048
|
-
type: res.type,
|
|
1049
|
-
status: 'active',
|
|
1050
|
-
source: res.source,
|
|
1051
|
-
repo_url: res.repoUrl || null,
|
|
1052
|
-
local_path: res.localPath,
|
|
1053
|
-
file_hash: res.fileHash,
|
|
1054
|
-
intent_tags: res.name.replace(/-/g, ' ').replace(/\//g, ' '),
|
|
1055
|
-
trigger_patterns: `when user needs ${res.name.replace(/-/g, ' ').replace(/\//g, ' ')}`,
|
|
1056
|
-
capability_summary: `${res.type}: ${res.name.replace(/-/g, ' ')}`,
|
|
1057
|
-
});
|
|
1058
|
-
} catch (e) { upsertErrors++; if (upsertErrors <= 3) debugCatch(e, `handleResourceScan-upsert[${upsertErrors}]`); }
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// Disable resources no longer on filesystem
|
|
1062
|
-
for (const row of toDisable) {
|
|
1063
|
-
try {
|
|
1064
|
-
rdb.prepare("UPDATE resources SET status = 'disabled', updated_at = datetime('now') WHERE id = ?").run(row.id);
|
|
1065
|
-
} catch {}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
debugLog('DEBUG', 'resource-scan', `indexed ${toIndex.length}, disabled ${toDisable.length}`);
|
|
1069
|
-
writeFileSync(RESCAN_MARKER, String(Date.now()));
|
|
1070
|
-
} catch (e) {
|
|
1071
|
-
debugCatch(e, 'handleResourceScan');
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
894
|
// ─── Utilities ──────────────────────────────────────────────────────────────
|
|
1076
895
|
|
|
1077
896
|
function readStdin() {
|
|
@@ -1137,14 +956,12 @@ function normalizeToolResponse(toolResponse) {
|
|
|
1137
956
|
|
|
1138
957
|
try {
|
|
1139
958
|
switch (event) {
|
|
1140
|
-
case 'pre-tool-use': await handlePreToolUse(); break;
|
|
1141
959
|
case 'post-tool-use': await handlePostToolUse(); break;
|
|
1142
960
|
case 'session-start': await handleSessionStart(); break;
|
|
1143
961
|
case 'stop': await handleStop(); break;
|
|
1144
962
|
case 'user-prompt': await handleUserPrompt(); break;
|
|
1145
963
|
case 'llm-episode': await handleLLMEpisode(); break;
|
|
1146
964
|
case 'llm-summary': await handleLLMSummary(); break;
|
|
1147
|
-
case 'resource-scan': await handleResourceScan(); break;
|
|
1148
965
|
case 'auto-compress': handleAutoCompress(); break;
|
|
1149
966
|
}
|
|
1150
967
|
} catch (err) {
|
|
@@ -1153,7 +970,4 @@ try {
|
|
|
1153
970
|
console.error(`[claude-mem-lite] [${ts}] [ERROR] ${event}: ${err.message}`);
|
|
1154
971
|
}
|
|
1155
972
|
|
|
1156
|
-
// Close singleton registry DB to prevent WAL residue
|
|
1157
|
-
closeRegistryDb();
|
|
1158
|
-
|
|
1159
973
|
process.exit(0);
|
package/hooks/hooks.json
CHANGED
|
@@ -18,18 +18,6 @@
|
|
|
18
18
|
]
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
|
-
"PreToolUse": [
|
|
22
|
-
{
|
|
23
|
-
"matcher": "*",
|
|
24
|
-
"hooks": [
|
|
25
|
-
{
|
|
26
|
-
"type": "command",
|
|
27
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hook.mjs\" pre-tool-use",
|
|
28
|
-
"timeout": 2
|
|
29
|
-
}
|
|
30
|
-
]
|
|
31
|
-
}
|
|
32
|
-
],
|
|
33
21
|
"PostToolUse": [
|
|
34
22
|
{
|
|
35
23
|
"matcher": "*",
|
|
@@ -58,6 +46,11 @@
|
|
|
58
46
|
{
|
|
59
47
|
"matcher": "*",
|
|
60
48
|
"hooks": [
|
|
49
|
+
{
|
|
50
|
+
"type": "command",
|
|
51
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/user-prompt-search.js\"",
|
|
52
|
+
"timeout": 2
|
|
53
|
+
},
|
|
61
54
|
{
|
|
62
55
|
"type": "command",
|
|
63
56
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hook.mjs\" user-prompt",
|