claude-mem-lite 2.30.1 → 2.31.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 +6 -1
- package/hook-handoff.mjs +56 -4
- package/hook-llm.mjs +155 -34
- package/hook.mjs +33 -0
- package/install.mjs +65 -6
- package/mem-cli.mjs +128 -0
- package/package.json +1 -1
- package/schema.mjs +64 -1
- package/scripts/pre-tool-recall.js +46 -7
- package/scripts/user-prompt-search.js +48 -52
- package/server.mjs +27 -21
- package/tool-schemas.mjs +323 -0
package/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'browse', 'delete', 'update', 'export', 'compress', 'maintain', 'optimize', 'fts-check', 'registry', 'import', 'enrich', 'help']);
|
|
2
|
+
const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'browse', 'delete', 'update', 'export', 'compress', 'maintain', 'optimize', 'fts-check', 'registry', 'import', 'enrich', 'activity', 'help']);
|
|
3
3
|
const INSTALL_COMMANDS = new Set(['install', 'uninstall', 'status', 'doctor', 'cleanup', 'cleanup-hooks', 'self-update', 'release']);
|
|
4
4
|
|
|
5
5
|
const cmd = process.argv[2];
|
|
@@ -13,6 +13,11 @@ if (cmd === '--version' || cmd === '-v') {
|
|
|
13
13
|
} else if (cmd === '--help' || cmd === '-h') {
|
|
14
14
|
const { run } = await import('./mem-cli.mjs');
|
|
15
15
|
await run(['help']);
|
|
16
|
+
} else if (cmd === 'doctor' && process.argv.slice(3).includes('--benchmark')) {
|
|
17
|
+
// doctor --benchmark is a baseline-capture tool, routed through mem-cli (DB layer);
|
|
18
|
+
// plain `doctor` continues to run the install health-check below.
|
|
19
|
+
const { run } = await import('./mem-cli.mjs');
|
|
20
|
+
await run(process.argv.slice(2));
|
|
16
21
|
} else if (CLI_COMMANDS.has(cmd)) {
|
|
17
22
|
const { run } = await import('./mem-cli.mjs');
|
|
18
23
|
await run(process.argv.slice(2));
|
package/hook-handoff.mjs
CHANGED
|
@@ -6,6 +6,11 @@ import { truncate, extractMatchKeywords, tokenizeHandoff, isSpecificTerm, LOW_SI
|
|
|
6
6
|
import {
|
|
7
7
|
HANDOFF_EXPIRY_CLEAR, HANDOFF_EXPIRY_EXIT, HANDOFF_MATCH_THRESHOLD, CONTINUE_KEYWORDS,
|
|
8
8
|
} from './hook-shared.mjs';
|
|
9
|
+
// T10d: import the whole module (not a named export) so tests can spy on
|
|
10
|
+
// gitStateModule.readGitState via vi.spyOn. Named-import bindings are
|
|
11
|
+
// immutable in ESM and cannot be mocked after the fact.
|
|
12
|
+
import * as gitStateModule from './lib/git-state.mjs';
|
|
13
|
+
import * as taskReaderModule from './lib/task-reader.mjs';
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* Build and save a handoff snapshot to session_handoffs table.
|
|
@@ -51,6 +56,24 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
|
|
|
51
56
|
.filter(d => { if (seenDescs.has(d)) return false; seenDescs.add(d); return true; });
|
|
52
57
|
if (pendingDescs.length > 0) unfinished = pendingDescs.join('; ');
|
|
53
58
|
}
|
|
59
|
+
|
|
60
|
+
// T10d: TaskList-sourced Unfinished. When no episode pending entries exist,
|
|
61
|
+
// prefer the structured signal from ~/.claude/tasks/<list>/*.json over the
|
|
62
|
+
// narrative-only fallback — a user-maintained task list is a stronger signal
|
|
63
|
+
// than a session with no recent tool activity. When the episode already has
|
|
64
|
+
// pending entries, those stay (they're fresher than the task file).
|
|
65
|
+
if (!unfinished) {
|
|
66
|
+
try {
|
|
67
|
+
const tasks = taskReaderModule.readProjectTasks({ projectPath: process.cwd() });
|
|
68
|
+
if (tasks.length > 0) {
|
|
69
|
+
unfinished = tasks
|
|
70
|
+
.slice(0, 5)
|
|
71
|
+
.map(t => `[${t.status}] ${t.title}`)
|
|
72
|
+
.join('\n');
|
|
73
|
+
}
|
|
74
|
+
} catch { /* task reader is best-effort; never block handoff */ }
|
|
75
|
+
}
|
|
76
|
+
|
|
54
77
|
// Enrich unfinished with full session edit history from observation narratives.
|
|
55
78
|
// Since handoff is UPSERT (max 2 rows per project), storing more data is free.
|
|
56
79
|
// Always use \n---\n separator so extractUnfinishedSummary can distinguish
|
|
@@ -89,11 +112,18 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
|
|
|
89
112
|
const allText = [workingOn, ...completed.map(c => c.title).filter(Boolean), unfinished].join(' ');
|
|
90
113
|
const keywords = extractMatchKeywords(allText, [...fileSet]);
|
|
91
114
|
|
|
115
|
+
// T10d: capture HEAD sha so detectContinuationIntent can anchor on it later.
|
|
116
|
+
// Best-effort — failures (non-git dir, missing binary, timeout) yield null.
|
|
117
|
+
let gitShaAtHandoff = null;
|
|
118
|
+
try {
|
|
119
|
+
gitShaAtHandoff = gitStateModule.readGitState({ cwd: process.cwd() }).headSha || null;
|
|
120
|
+
} catch { /* swallow — handoff must still persist */ }
|
|
121
|
+
|
|
92
122
|
// UPSERT keyed on (project, type, session_id) — parallel sessions coexist.
|
|
93
123
|
// Same session re-writing its own handoff (e.g. repeated /clear) updates in place.
|
|
94
124
|
db.prepare(`
|
|
95
|
-
INSERT INTO session_handoffs (project, type, session_id, working_on, completed, unfinished, key_files, key_decisions, match_keywords, created_at_epoch)
|
|
96
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
125
|
+
INSERT INTO session_handoffs (project, type, session_id, working_on, completed, unfinished, key_files, key_decisions, match_keywords, created_at_epoch, git_sha_at_handoff)
|
|
126
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
97
127
|
ON CONFLICT(project, type, session_id) DO UPDATE SET
|
|
98
128
|
working_on = excluded.working_on,
|
|
99
129
|
completed = excluded.completed,
|
|
@@ -101,7 +131,8 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
|
|
|
101
131
|
key_files = excluded.key_files,
|
|
102
132
|
key_decisions = excluded.key_decisions,
|
|
103
133
|
match_keywords = excluded.match_keywords,
|
|
104
|
-
created_at_epoch = excluded.created_at_epoch
|
|
134
|
+
created_at_epoch = excluded.created_at_epoch,
|
|
135
|
+
git_sha_at_handoff = excluded.git_sha_at_handoff
|
|
105
136
|
`).run(
|
|
106
137
|
project, type, sessionId,
|
|
107
138
|
truncate(workingOn, 1000),
|
|
@@ -110,7 +141,8 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
|
|
|
110
141
|
JSON.stringify([...fileSet].slice(0, 20)),
|
|
111
142
|
decisions.map(d => d.title).join('\n'),
|
|
112
143
|
keywords,
|
|
113
|
-
Date.now()
|
|
144
|
+
Date.now(),
|
|
145
|
+
gitShaAtHandoff,
|
|
114
146
|
);
|
|
115
147
|
}
|
|
116
148
|
|
|
@@ -137,6 +169,26 @@ export function detectContinuationIntent(db, promptText, project, currentCcSessi
|
|
|
137
169
|
if (!promptText || typeof promptText !== 'string') return false;
|
|
138
170
|
if (promptText.trim().length < 2) return false;
|
|
139
171
|
|
|
172
|
+
// T10d Stage -1: Git-commit anchor — if ANY handoff (any age) has a
|
|
173
|
+
// git_sha_at_handoff matching current HEAD, the working tree hasn't moved
|
|
174
|
+
// since that handoff, so assume continuation regardless of time / prompt.
|
|
175
|
+
//
|
|
176
|
+
// Trade-off: after weeks of no commits this fires aggressively. Users can
|
|
177
|
+
// reset by making a commit or by typing a long unrelated prompt (this
|
|
178
|
+
// anchor runs BEFORE the Stage 0 long-prompt guard, so that escape hatch
|
|
179
|
+
// does not apply here). This is an MVP choice — see plan 10d concern.
|
|
180
|
+
try {
|
|
181
|
+
const currentSha = gitStateModule.readGitState({ cwd: process.cwd() }).headSha;
|
|
182
|
+
if (currentSha) {
|
|
183
|
+
const anchor = db.prepare(`
|
|
184
|
+
SELECT 1 FROM session_handoffs
|
|
185
|
+
WHERE project = ? AND git_sha_at_handoff = ?
|
|
186
|
+
ORDER BY created_at_epoch DESC LIMIT 1
|
|
187
|
+
`).get(project, currentSha);
|
|
188
|
+
if (anchor) return true;
|
|
189
|
+
}
|
|
190
|
+
} catch { /* git/DB failure must not break the rest of the pipeline */ }
|
|
191
|
+
|
|
140
192
|
// Stage 0: Non-expired 'clear' handoff — assume continuation unless long unrelated prompt.
|
|
141
193
|
// Session scoping: with currentCcSessionId, only your OWN clear handoff qualifies.
|
|
142
194
|
const clearHandoff = currentCcSessionId
|
package/hook-llm.mjs
CHANGED
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
RUNTIME_DIR, DEDUP_WINDOW_MS, RELATED_OBS_WINDOW_MS,
|
|
16
16
|
sessionFile, getSessionId, openDb, callLLM, sleep,
|
|
17
17
|
} from './hook-shared.mjs';
|
|
18
|
+
import { EVENT_TYPES, saveEvent } from './lib/activity.mjs';
|
|
19
|
+
|
|
20
|
+
// T9: memdir-incompatible types live in the `events` table, not `observations`.
|
|
21
|
+
// Set lookup is O(1) — authoritative source is lib/activity.mjs::EVENT_TYPES.
|
|
22
|
+
const EVENT_TYPE_SET = new Set(EVENT_TYPES);
|
|
18
23
|
|
|
19
24
|
// ─── Save Observation to DB ─────────────────────────────────────────────────
|
|
20
25
|
|
|
@@ -172,6 +177,96 @@ export function saveObservation(obs, projectOverride, sessionIdOverride, externa
|
|
|
172
177
|
}
|
|
173
178
|
}
|
|
174
179
|
|
|
180
|
+
// ─── obs → summary shape mapping ────────────────────────────────────────────
|
|
181
|
+
// handleLLMEpisode's internal `obs` uses camelCase/legacy names (lessonLearned,
|
|
182
|
+
// files, filesRead). persistHaikuSummary takes the plan's stable snake_case
|
|
183
|
+
// shape. Keep the mapping here so the dispatcher's public signature stays
|
|
184
|
+
// clean for external callers that already use the plan shape.
|
|
185
|
+
function obsToSummary(obs) {
|
|
186
|
+
return {
|
|
187
|
+
type: obs.type,
|
|
188
|
+
title: obs.title,
|
|
189
|
+
subtitle: obs.subtitle,
|
|
190
|
+
narrative: obs.narrative,
|
|
191
|
+
concepts: obs.concepts,
|
|
192
|
+
facts: obs.facts,
|
|
193
|
+
files_modified: obs.files,
|
|
194
|
+
files_read: obs.filesRead,
|
|
195
|
+
importance: obs.importance,
|
|
196
|
+
lesson_learned: obs.lessonLearned,
|
|
197
|
+
search_aliases: obs.searchAliases,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── T9: Haiku Summary Dispatcher (events vs observations routing) ──────────
|
|
202
|
+
//
|
|
203
|
+
// Routes memdir-incompatible types (the 8 values in EVENT_TYPES) to the
|
|
204
|
+
// activity `events` table, and keeps legacy/memdir-aligned types (e.g.
|
|
205
|
+
// `change`, which is the only non-event type hook-llm currently emits) on
|
|
206
|
+
// the existing observations path.
|
|
207
|
+
//
|
|
208
|
+
// Input shape (matches the v2.31 MVP plan's stable interface):
|
|
209
|
+
// summary: { type, title, lesson_learned?, narrative?, importance?, files_modified? }
|
|
210
|
+
// ctx: { project, session_id, preSavedObsId? }
|
|
211
|
+
//
|
|
212
|
+
// Returns { table: 'events'|'observations', id: number|null }. Callers inspect
|
|
213
|
+
// `table` to decide whether follow-up observations-only logic (linking, vector
|
|
214
|
+
// refresh) applies.
|
|
215
|
+
//
|
|
216
|
+
// NOTE: The foreground pre-save in hook.mjs:110/336 intentionally still writes
|
|
217
|
+
// to `observations` for immediate visibility. When the background worker
|
|
218
|
+
// processes the episode (handleLLMEpisode), it passes the pre-saved id in
|
|
219
|
+
// `ctx.preSavedObsId`; this dispatcher then deletes the pre-saved observations
|
|
220
|
+
// row and inserts a fresh event for event-typed summaries (upgrade-delete
|
|
221
|
+
// semantics). Observations-typed summaries reuse the pre-saved row directly
|
|
222
|
+
// (caller handles the UPDATE, since it needs the enriched FTS fields).
|
|
223
|
+
export function persistHaikuSummary(db, summary, ctx) {
|
|
224
|
+
if (EVENT_TYPE_SET.has(summary.type)) {
|
|
225
|
+
// Upgrade-delete: the foreground pre-save landed in `observations` with a
|
|
226
|
+
// rule-inferred type; now that Haiku has classified it as an event type,
|
|
227
|
+
// we must remove the stale observations row before inserting the event.
|
|
228
|
+
// Atomic via better-sqlite3 transaction: either both succeed or neither.
|
|
229
|
+
const insertEvent = () => saveEvent(db, {
|
|
230
|
+
project: ctx.project,
|
|
231
|
+
event_type: summary.type,
|
|
232
|
+
title: summary.title,
|
|
233
|
+
body: summary.lesson_learned || summary.narrative || null,
|
|
234
|
+
file_paths: (Array.isArray(summary.files_modified) && summary.files_modified.length > 0)
|
|
235
|
+
? summary.files_modified
|
|
236
|
+
: null,
|
|
237
|
+
importance: summary.importance ?? 1,
|
|
238
|
+
created_at_epoch: Date.now(),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (ctx.preSavedObsId) {
|
|
242
|
+
const id = db.transaction(() => {
|
|
243
|
+
db.prepare(`DELETE FROM observations WHERE id = ?`).run(ctx.preSavedObsId);
|
|
244
|
+
return insertEvent();
|
|
245
|
+
})();
|
|
246
|
+
return { table: 'events', id };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { table: 'events', id: insertEvent() };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Fallthrough: memdir-compatible / legacy types use the observations path.
|
|
253
|
+
// Map the Haiku/plan field names to saveObservation's expected shape.
|
|
254
|
+
const id = saveObservation({
|
|
255
|
+
type: summary.type,
|
|
256
|
+
title: summary.title,
|
|
257
|
+
subtitle: summary.subtitle || '',
|
|
258
|
+
narrative: summary.narrative || '',
|
|
259
|
+
concepts: summary.concepts || [],
|
|
260
|
+
facts: summary.facts || [],
|
|
261
|
+
files: summary.files_modified || [],
|
|
262
|
+
filesRead: summary.files_read || [],
|
|
263
|
+
importance: summary.importance ?? 1,
|
|
264
|
+
lessonLearned: summary.lesson_learned || null,
|
|
265
|
+
searchAliases: summary.search_aliases || null,
|
|
266
|
+
}, ctx.project, ctx.session_id, db);
|
|
267
|
+
return { table: 'observations', id };
|
|
268
|
+
}
|
|
269
|
+
|
|
175
270
|
// ─── Related Observation Linking ─────────────────────────────────────────────
|
|
176
271
|
|
|
177
272
|
function linkRelatedObservations(db, savedId, obs, episode) {
|
|
@@ -504,46 +599,72 @@ search_aliases: 2-6 alternative search terms someone might use to find this memo
|
|
|
504
599
|
|
|
505
600
|
try {
|
|
506
601
|
let savedId;
|
|
602
|
+
let savedTable;
|
|
507
603
|
|
|
508
604
|
if (episode.savedId && obs) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
605
|
+
if (EVENT_TYPE_SET.has(obs.type)) {
|
|
606
|
+
// Upgrade-delete: pre-saved observation's rule-inferred type was later
|
|
607
|
+
// classified by Haiku as an event type. Delete the stale observations
|
|
608
|
+
// row and insert into events instead. Dispatcher handles the atomic
|
|
609
|
+
// swap via transaction.
|
|
610
|
+
const result = persistHaikuSummary(db, obsToSummary(obs), {
|
|
611
|
+
project: episode.project,
|
|
612
|
+
session_id: episode.sessionId,
|
|
613
|
+
preSavedObsId: episode.savedId,
|
|
614
|
+
});
|
|
615
|
+
savedId = result.id;
|
|
616
|
+
savedTable = result.table;
|
|
617
|
+
debugLog('DEBUG', 'llm-episode', `upgrade-delete: obs #${episode.savedId} → event #${savedId}`);
|
|
618
|
+
} else {
|
|
619
|
+
// Non-event type (e.g. `change`) — upgrade pre-saved observations row in place
|
|
620
|
+
// so the enriched FTS text field + minhash + vector are refreshed atomically.
|
|
621
|
+
const { conceptsText, factsText, textField } = buildFtsTextField(obs);
|
|
622
|
+
const minhashSig = computeMinHash((obs.title || '') + ' ' + (obs.narrative || ''));
|
|
623
|
+
db.prepare(`
|
|
624
|
+
UPDATE observations SET type=?, title=?, subtitle=?, narrative=?, concepts=?, facts=?,
|
|
625
|
+
text=?, importance=?, files_read=?, minhash_sig=?, lesson_learned=?, search_aliases=?
|
|
626
|
+
WHERE id = ?
|
|
627
|
+
`).run(
|
|
628
|
+
obs.type, truncate(obs.title, 120), obs.subtitle || '',
|
|
629
|
+
truncate(obs.narrative || '', 500),
|
|
630
|
+
conceptsText, factsText, textField,
|
|
631
|
+
obs.importance,
|
|
632
|
+
JSON.stringify(obs.filesRead || []),
|
|
633
|
+
minhashSig,
|
|
634
|
+
obs.lessonLearned || null,
|
|
635
|
+
obs.searchAliases || null,
|
|
636
|
+
episode.savedId
|
|
637
|
+
);
|
|
638
|
+
savedId = episode.savedId;
|
|
639
|
+
savedTable = 'observations';
|
|
640
|
+
debugLog('DEBUG', 'llm-episode', `upgraded pre-saved obs #${savedId}`);
|
|
641
|
+
|
|
642
|
+
// Update TF-IDF vector with enriched content
|
|
643
|
+
try {
|
|
644
|
+
const vocab = getVocabulary(db);
|
|
645
|
+
if (vocab) {
|
|
646
|
+
const vecText = [obs.title || '', obs.narrative || '', conceptsText].filter(Boolean).join(' ');
|
|
647
|
+
const vec = computeVector(vecText, vocab);
|
|
648
|
+
if (vec) {
|
|
649
|
+
db.prepare('INSERT OR REPLACE INTO observation_vectors (observation_id, vector, vocab_version, created_at_epoch) VALUES (?, ?, ?, ?)')
|
|
650
|
+
.run(savedId, Buffer.from(vec.buffer), vocab.version, Date.now());
|
|
651
|
+
}
|
|
539
652
|
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
653
|
+
} catch (e) { debugCatch(e, 'handleLLMEpisode-vector'); }
|
|
654
|
+
}
|
|
542
655
|
} else {
|
|
543
|
-
|
|
656
|
+
// Clean insert (no pre-save) — dispatcher routes by type.
|
|
657
|
+
const result = persistHaikuSummary(db, obsToSummary(obs), {
|
|
658
|
+
project: episode.project,
|
|
659
|
+
session_id: episode.sessionId,
|
|
660
|
+
});
|
|
661
|
+
savedId = result.id;
|
|
662
|
+
savedTable = result.table;
|
|
544
663
|
}
|
|
545
664
|
|
|
546
|
-
|
|
665
|
+
// Related-observation linking only applies to rows in `observations` —
|
|
666
|
+
// the `events` table has its own lifecycle (supersede/accessed_count).
|
|
667
|
+
if (savedId && savedTable === 'observations') {
|
|
547
668
|
try {
|
|
548
669
|
linkRelatedObservations(db, savedId, obs, episode);
|
|
549
670
|
} catch (e) { debugCatch(e, 'relatedObsLinking'); }
|
package/hook.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import { searchRelevantMemories } from './hook-memory.mjs';
|
|
|
32
32
|
import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection, extractUnfinishedSummary } from './hook-handoff.mjs';
|
|
33
33
|
import { checkForUpdate } from './hook-update.mjs';
|
|
34
34
|
import { handleLLMOptimize } from './hook-optimize.mjs';
|
|
35
|
+
import { clearPluginCacheHooks, hasInstallManagedHooks } from './plugin-cache-guard.mjs';
|
|
35
36
|
import { SKIP_TOOLS, SKIP_PREFIXES } from './skip-tools.mjs';
|
|
36
37
|
import { getVocabulary } from './tfidf.mjs';
|
|
37
38
|
|
|
@@ -397,6 +398,21 @@ async function handleStop() {
|
|
|
397
398
|
// ─── SessionStart Handler + CLAUDE.md Persistence (Tier 1 A, E) ─────────────
|
|
398
399
|
|
|
399
400
|
async function handleSessionStart() {
|
|
401
|
+
// Plugin cache self-heal: Claude Code auto-updates the marketplace plugin can
|
|
402
|
+
// re-populate cache/<ver>/hooks/hooks.json, reintroducing duplicate hook
|
|
403
|
+
// registration alongside install.mjs-managed settings.json entries. Silently
|
|
404
|
+
// clear — gated by hasInstallManagedHooks to avoid breaking plugin-only users.
|
|
405
|
+
try {
|
|
406
|
+
if (hasInstallManagedHooks()) {
|
|
407
|
+
const cleared = clearPluginCacheHooks({
|
|
408
|
+
reason: 'Auto-healed by hook.mjs session-start — install.mjs-managed hooks active in settings.json',
|
|
409
|
+
});
|
|
410
|
+
if (cleared.length > 0) {
|
|
411
|
+
debugLog('DEBUG', 'session-start', `auto-healed stale plugin cache hooks.json in version(s): ${cleared.join(', ')}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
} catch (e) { debugCatch(e, 'session-start-cache-heal'); }
|
|
415
|
+
|
|
400
416
|
// Read CC real session_id from hook stdin — used to scope handoff rows so parallel
|
|
401
417
|
// sessions for the same project don't clobber each other (see docs/bug.txt).
|
|
402
418
|
let ccSessionId = null;
|
|
@@ -716,6 +732,23 @@ async function handleSessionStart() {
|
|
|
716
732
|
} catch (e) { debugCatch(e, 'session-start-exit-fast-summary'); }
|
|
717
733
|
}
|
|
718
734
|
|
|
735
|
+
// T10c: Startup dashboard — aggregate git/tasks/plans/handoff/events into a
|
|
736
|
+
// structured JSON hookSpecificOutput block. Emitted BEFORE the plain-text
|
|
737
|
+
// <claude-mem-context> so both surfaces coexist. Empty string → skip.
|
|
738
|
+
try {
|
|
739
|
+
const { buildDashboard } = await import('./lib/startup-dashboard.mjs');
|
|
740
|
+
const dashboardText = buildDashboard({ db, project, projectPath: process.cwd() });
|
|
741
|
+
if (dashboardText) {
|
|
742
|
+
process.stdout.write(JSON.stringify({
|
|
743
|
+
suppressOutput: true,
|
|
744
|
+
hookSpecificOutput: {
|
|
745
|
+
hookEventName: 'SessionStart',
|
|
746
|
+
additionalContext: dashboardText,
|
|
747
|
+
},
|
|
748
|
+
}) + '\n');
|
|
749
|
+
}
|
|
750
|
+
} catch (e) { debugCatch(e, 'session-start-dashboard'); }
|
|
751
|
+
|
|
719
752
|
// Build the full context body via shared helper (also used by `mem-cli context`).
|
|
720
753
|
// Queries session_summaries, key observations, clear handoff, and the
|
|
721
754
|
// token-budgeted observation pool directly from the DB.
|
package/install.mjs
CHANGED
|
@@ -26,6 +26,7 @@ const PLUGIN_KEY = `claude-mem-lite@${MARKETPLACE_KEY}`;
|
|
|
26
26
|
const NPM_INSTALL_CMD = 'npm install --omit=dev --no-audit --no-fund';
|
|
27
27
|
|
|
28
28
|
import { RESOURCE_METADATA } from './install-metadata.mjs';
|
|
29
|
+
import { scanPluginCacheHookPollution } from './plugin-cache-guard.mjs';
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Derive invocation_name from resource name when metadata doesn't provide one.
|
|
@@ -205,12 +206,24 @@ async function install() {
|
|
|
205
206
|
'cli.mjs', 'server.mjs', 'server-internals.mjs', 'tool-schemas.mjs',
|
|
206
207
|
'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs', 'skip-tools.mjs',
|
|
207
208
|
'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs', 'hook-update.mjs', 'hook-optimize.mjs',
|
|
209
|
+
'plugin-cache-guard.mjs',
|
|
208
210
|
'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'package-lock.json', 'skill.md',
|
|
209
211
|
'registry.mjs', 'registry-scanner.mjs', 'registry-indexer.mjs',
|
|
210
212
|
'registry-retriever.mjs', 'resource-discovery.mjs',
|
|
211
213
|
'install-metadata.mjs', 'mem-cli.mjs', 'tier.mjs', 'tfidf.mjs',
|
|
212
214
|
'nlp.mjs', 'synonyms.mjs', 'scoring-sql.mjs', 'stop-words.mjs', 'project-utils.mjs',
|
|
213
215
|
'secret-scrub.mjs', 'format-utils.mjs', 'hash-utils.mjs', 'bash-utils.mjs',
|
|
216
|
+
// v2.31 T9: hook-llm now statically imports lib/activity.mjs (events routing).
|
|
217
|
+
// SOURCE_FILES entries with a subdir prefix require install.mjs to mkdir the
|
|
218
|
+
// parent before symlink/copy — handled in the IS_DEV and else-branch loops.
|
|
219
|
+
'lib/activity.mjs',
|
|
220
|
+
// v2.31 T10: startup dashboard aggregator + handoff task/git anchoring.
|
|
221
|
+
// task-reader + plan-reader + git-state are read by lib/startup-dashboard.mjs
|
|
222
|
+
// on SessionStart (hook.mjs) and by hook-handoff.mjs on /exit + /clear.
|
|
223
|
+
'lib/task-reader.mjs',
|
|
224
|
+
'lib/plan-reader.mjs',
|
|
225
|
+
'lib/git-state.mjs',
|
|
226
|
+
'lib/startup-dashboard.mjs',
|
|
214
227
|
];
|
|
215
228
|
|
|
216
229
|
if (IS_DEV) {
|
|
@@ -220,6 +233,9 @@ async function install() {
|
|
|
220
233
|
const target = join(PROJECT_DIR, f);
|
|
221
234
|
const link = join(DATA_DIR, f);
|
|
222
235
|
if (existsSync(target)) {
|
|
236
|
+
// Ensure parent dir exists for subdir entries (e.g. 'lib/activity.mjs')
|
|
237
|
+
const linkParent = dirname(link);
|
|
238
|
+
if (!existsSync(linkParent)) mkdirSync(linkParent, { recursive: true });
|
|
223
239
|
// Remove existing file/symlink before creating
|
|
224
240
|
if (existsSync(link)) try { unlinkSync(link); } catch {}
|
|
225
241
|
symlinkSync(target, link);
|
|
@@ -252,7 +268,13 @@ async function install() {
|
|
|
252
268
|
if (!existsSync(scriptsDir)) mkdirSync(scriptsDir, { recursive: true });
|
|
253
269
|
for (const f of SOURCE_FILES) {
|
|
254
270
|
const src = join(PROJECT_DIR, f);
|
|
255
|
-
|
|
271
|
+
const dst = join(DATA_DIR, f);
|
|
272
|
+
if (existsSync(src)) {
|
|
273
|
+
// Ensure parent dir exists for subdir entries (e.g. 'lib/activity.mjs')
|
|
274
|
+
const dstParent = dirname(dst);
|
|
275
|
+
if (!existsSync(dstParent)) mkdirSync(dstParent, { recursive: true });
|
|
276
|
+
copyFileSync(src, dst);
|
|
277
|
+
}
|
|
256
278
|
}
|
|
257
279
|
// Copy scripts
|
|
258
280
|
const postToolSrc = join(PROJECT_DIR, 'scripts', 'post-tool-use.sh');
|
|
@@ -381,18 +403,43 @@ async function install() {
|
|
|
381
403
|
}
|
|
382
404
|
} catch (e) { warn(`Marketplace hooks dedup: ${e.message}`); }
|
|
383
405
|
|
|
384
|
-
// Sync launch.mjs to plugin cache — ensures MCP server loads dev code via symlink detection
|
|
406
|
+
// Sync launch.mjs to plugin cache — ensures MCP server loads dev code via symlink detection.
|
|
407
|
+
// ALSO clear cached hooks.json in every version dir — Claude Code runtime reads hooks from
|
|
408
|
+
// ~/.claude/plugins/cache/<mp>/<plugin>/<ver>/hooks/hooks.json, NOT from the marketplace source.
|
|
409
|
+
// Clearing only the marketplace source (above) leaves stale cache copies that double-register
|
|
410
|
+
// hooks alongside install.mjs-written settings.json entries.
|
|
385
411
|
try {
|
|
386
412
|
const cacheBase = join(homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_KEY, 'claude-mem-lite');
|
|
387
413
|
if (existsSync(cacheBase)) {
|
|
388
414
|
const srcLaunch = join(PROJECT_DIR, 'scripts', 'launch.mjs');
|
|
415
|
+
let clearedHooks = 0;
|
|
389
416
|
for (const ver of readdirSync(cacheBase)) {
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
417
|
+
const verDir = join(cacheBase, ver);
|
|
418
|
+
|
|
419
|
+
// Sync launch.mjs
|
|
420
|
+
if (existsSync(join(verDir, 'scripts'))) {
|
|
421
|
+
try { copyFileSync(srcLaunch, join(verDir, 'scripts', 'launch.mjs')); } catch {}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Clear cached hooks.json (runtime reads here, not marketplace source)
|
|
425
|
+
const cachedHooksPath = join(verDir, 'hooks', 'hooks.json');
|
|
426
|
+
if (existsSync(cachedHooksPath)) {
|
|
427
|
+
try {
|
|
428
|
+
const h = JSON.parse(readFileSync(cachedHooksPath, 'utf8'));
|
|
429
|
+
if (h.hooks && Object.keys(h.hooks).length > 0) {
|
|
430
|
+
writeFileSync(cachedHooksPath, JSON.stringify({
|
|
431
|
+
description: h.description || 'claude-mem-lite hooks',
|
|
432
|
+
_note: `Hooks managed by install.mjs in settings.json — cache hooks.json cleared to prevent duplicate registration (cache ver: ${ver})`,
|
|
433
|
+
hooks: {}
|
|
434
|
+
}, null, 2) + '\n');
|
|
435
|
+
clearedHooks++;
|
|
436
|
+
}
|
|
437
|
+
} catch { /* silent — never block install on one bad cache entry */ }
|
|
393
438
|
}
|
|
394
439
|
}
|
|
395
|
-
|
|
440
|
+
const parts = ['launch.mjs synced (dev mode MCP routing)'];
|
|
441
|
+
if (clearedHooks > 0) parts.push(`${clearedHooks} stale hooks.json cleared`);
|
|
442
|
+
ok(`Plugin cache: ${parts.join('; ')}`);
|
|
396
443
|
}
|
|
397
444
|
} catch (e) { warn(`Plugin cache sync: ${e.message}`); }
|
|
398
445
|
}
|
|
@@ -942,6 +989,18 @@ async function status() {
|
|
|
942
989
|
fail('Hooks: not configured');
|
|
943
990
|
}
|
|
944
991
|
|
|
992
|
+
// Plugin cache pollution: populated hooks.json in cache AND install.mjs-managed
|
|
993
|
+
// settings.json hooks → runtime registers both → duplicate firing.
|
|
994
|
+
const polluted = scanPluginCacheHookPollution();
|
|
995
|
+
if (polluted.length > 0 && hasHooks) {
|
|
996
|
+
fail(`Plugin cache: stale hooks.json in version(s) ${polluted.join(', ')} — duplicate firing alongside settings.json (run 'install' to auto-clear)`);
|
|
997
|
+
} else if (polluted.length > 0) {
|
|
998
|
+
// plugin-only mode (no settings.json hooks) — cache hooks.json is the sole source, expected
|
|
999
|
+
ok(`Plugin cache: ${polluted.length} version(s) with hooks.json (plugin-only mode)`);
|
|
1000
|
+
} else if (pluginEnabled || hasHooks) {
|
|
1001
|
+
ok('Plugin cache: no stale hooks.json (no duplicate firing)');
|
|
1002
|
+
}
|
|
1003
|
+
|
|
945
1004
|
// Database
|
|
946
1005
|
if (existsSync(DB_PATH)) {
|
|
947
1006
|
try {
|