claude-mem-lite 2.30.1 → 2.31.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/.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 +17 -0
- package/install.mjs +21 -1
- 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
|
@@ -716,6 +716,23 @@ async function handleSessionStart() {
|
|
|
716
716
|
} catch (e) { debugCatch(e, 'session-start-exit-fast-summary'); }
|
|
717
717
|
}
|
|
718
718
|
|
|
719
|
+
// T10c: Startup dashboard — aggregate git/tasks/plans/handoff/events into a
|
|
720
|
+
// structured JSON hookSpecificOutput block. Emitted BEFORE the plain-text
|
|
721
|
+
// <claude-mem-context> so both surfaces coexist. Empty string → skip.
|
|
722
|
+
try {
|
|
723
|
+
const { buildDashboard } = await import('./lib/startup-dashboard.mjs');
|
|
724
|
+
const dashboardText = buildDashboard({ db, project, projectPath: process.cwd() });
|
|
725
|
+
if (dashboardText) {
|
|
726
|
+
process.stdout.write(JSON.stringify({
|
|
727
|
+
suppressOutput: true,
|
|
728
|
+
hookSpecificOutput: {
|
|
729
|
+
hookEventName: 'SessionStart',
|
|
730
|
+
additionalContext: dashboardText,
|
|
731
|
+
},
|
|
732
|
+
}) + '\n');
|
|
733
|
+
}
|
|
734
|
+
} catch (e) { debugCatch(e, 'session-start-dashboard'); }
|
|
735
|
+
|
|
719
736
|
// Build the full context body via shared helper (also used by `mem-cli context`).
|
|
720
737
|
// Queries session_summaries, key observations, clear handoff, and the
|
|
721
738
|
// token-budgeted observation pool directly from the DB.
|
package/install.mjs
CHANGED
|
@@ -211,6 +211,17 @@ async function install() {
|
|
|
211
211
|
'install-metadata.mjs', 'mem-cli.mjs', 'tier.mjs', 'tfidf.mjs',
|
|
212
212
|
'nlp.mjs', 'synonyms.mjs', 'scoring-sql.mjs', 'stop-words.mjs', 'project-utils.mjs',
|
|
213
213
|
'secret-scrub.mjs', 'format-utils.mjs', 'hash-utils.mjs', 'bash-utils.mjs',
|
|
214
|
+
// v2.31 T9: hook-llm now statically imports lib/activity.mjs (events routing).
|
|
215
|
+
// SOURCE_FILES entries with a subdir prefix require install.mjs to mkdir the
|
|
216
|
+
// parent before symlink/copy — handled in the IS_DEV and else-branch loops.
|
|
217
|
+
'lib/activity.mjs',
|
|
218
|
+
// v2.31 T10: startup dashboard aggregator + handoff task/git anchoring.
|
|
219
|
+
// task-reader + plan-reader + git-state are read by lib/startup-dashboard.mjs
|
|
220
|
+
// on SessionStart (hook.mjs) and by hook-handoff.mjs on /exit + /clear.
|
|
221
|
+
'lib/task-reader.mjs',
|
|
222
|
+
'lib/plan-reader.mjs',
|
|
223
|
+
'lib/git-state.mjs',
|
|
224
|
+
'lib/startup-dashboard.mjs',
|
|
214
225
|
];
|
|
215
226
|
|
|
216
227
|
if (IS_DEV) {
|
|
@@ -220,6 +231,9 @@ async function install() {
|
|
|
220
231
|
const target = join(PROJECT_DIR, f);
|
|
221
232
|
const link = join(DATA_DIR, f);
|
|
222
233
|
if (existsSync(target)) {
|
|
234
|
+
// Ensure parent dir exists for subdir entries (e.g. 'lib/activity.mjs')
|
|
235
|
+
const linkParent = dirname(link);
|
|
236
|
+
if (!existsSync(linkParent)) mkdirSync(linkParent, { recursive: true });
|
|
223
237
|
// Remove existing file/symlink before creating
|
|
224
238
|
if (existsSync(link)) try { unlinkSync(link); } catch {}
|
|
225
239
|
symlinkSync(target, link);
|
|
@@ -252,7 +266,13 @@ async function install() {
|
|
|
252
266
|
if (!existsSync(scriptsDir)) mkdirSync(scriptsDir, { recursive: true });
|
|
253
267
|
for (const f of SOURCE_FILES) {
|
|
254
268
|
const src = join(PROJECT_DIR, f);
|
|
255
|
-
|
|
269
|
+
const dst = join(DATA_DIR, f);
|
|
270
|
+
if (existsSync(src)) {
|
|
271
|
+
// Ensure parent dir exists for subdir entries (e.g. 'lib/activity.mjs')
|
|
272
|
+
const dstParent = dirname(dst);
|
|
273
|
+
if (!existsSync(dstParent)) mkdirSync(dstParent, { recursive: true });
|
|
274
|
+
copyFileSync(src, dst);
|
|
275
|
+
}
|
|
256
276
|
}
|
|
257
277
|
// Copy scripts
|
|
258
278
|
const postToolSrc = join(PROJECT_DIR, 'scripts', 'post-tool-use.sh');
|
package/mem-cli.mjs
CHANGED
|
@@ -2048,6 +2048,16 @@ Commands:
|
|
|
2048
2048
|
remove Remove resource --name N --resource-type T
|
|
2049
2049
|
reindex Rebuild FTS5 index
|
|
2050
2050
|
|
|
2051
|
+
activity <action> Non-memdir event log (v2.31) — bugfix/lesson/bug/discovery/etc.
|
|
2052
|
+
save --type T "<title>" [--body "<text>"] [--files f1,f2] [--file path] [--importance 1-3] [--project P]
|
|
2053
|
+
search "<query>" Search events [--type T] [--limit N] [--project P]
|
|
2054
|
+
recent [N] Most recent events [--type T] [--project P]
|
|
2055
|
+
show <id> Show full event row by id
|
|
2056
|
+
|
|
2057
|
+
Valid types: bugfix, lesson, bug, discovery, refactor, feature, observation, decision
|
|
2058
|
+
--files (plural, comma-split) preferred; --file (singular) kept for back-compat.
|
|
2059
|
+
Use /lesson or /bug slash commands for faster capture (T8).
|
|
2060
|
+
|
|
2051
2061
|
DB: ${DB_PATH}`);
|
|
2052
2062
|
}
|
|
2053
2063
|
|
|
@@ -2193,6 +2203,122 @@ async function cmdOptimize(db, args) {
|
|
|
2193
2203
|
if (results.smartCompress) out(` Smart-compress: ${results.smartCompress.compressed || 0} compressed of ${results.smartCompress.processed || 0} clusters`);
|
|
2194
2204
|
}
|
|
2195
2205
|
|
|
2206
|
+
async function cmdDoctor(db, args) {
|
|
2207
|
+
if (args.includes('--benchmark')) {
|
|
2208
|
+
const { runBenchmark } = await import('./lib/doctor-benchmark.mjs');
|
|
2209
|
+
const project = inferProject();
|
|
2210
|
+
const result = runBenchmark(db, { project });
|
|
2211
|
+
out(JSON.stringify(result, null, 2));
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
out('[mem] doctor: supported flags: --benchmark');
|
|
2215
|
+
process.exitCode = 1;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// ─── Activity (T7 v2.31) ─────────────────────────────────────────────────────
|
|
2219
|
+
// Separate namespace from observations. Handlers are thin wrappers over
|
|
2220
|
+
// lib/activity.mjs pure functions; imported lazily to match the doctor pattern.
|
|
2221
|
+
|
|
2222
|
+
function formatActivityResults(rows) {
|
|
2223
|
+
if (!rows || rows.length === 0) return '(no events)';
|
|
2224
|
+
return rows.map(r => `#${r.id} [${r.event_type}] ${r.title}`).join('\n');
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
async function cmdActivity(db, args) {
|
|
2228
|
+
const sub = args[0];
|
|
2229
|
+
if (!sub) {
|
|
2230
|
+
fail('[mem] Usage: claude-mem-lite activity <save|search|recent|show> ...');
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
const { positional, flags } = parseArgs(args.slice(1));
|
|
2235
|
+
const { saveEvent, searchEvents, recentEvents, getEvent, EVENT_TYPES } = await import('./lib/activity.mjs');
|
|
2236
|
+
const VALID_EVENT_TYPES = new Set(EVENT_TYPES);
|
|
2237
|
+
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
2238
|
+
|
|
2239
|
+
if (sub === 'save') {
|
|
2240
|
+
const type = flags.type || 'observation';
|
|
2241
|
+
if (!VALID_EVENT_TYPES.has(type)) {
|
|
2242
|
+
fail(`[mem] activity save: invalid --type "${type}". Valid: ${[...VALID_EVENT_TYPES].join(', ')}`);
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
const title = flags.title || positional.join(' ').trim();
|
|
2246
|
+
if (!title) {
|
|
2247
|
+
fail('[mem] activity save: --title or positional text required');
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
const body = flags.body || null;
|
|
2251
|
+
// Accept both --file (singular, backward compat) and --files (plural,
|
|
2252
|
+
// comma-split, preferred — matches cmdSave). Merge both sources.
|
|
2253
|
+
const filesFromPlural = flags.files && typeof flags.files === 'string'
|
|
2254
|
+
? flags.files.split(',').map(s => s.trim()).filter(Boolean)
|
|
2255
|
+
: [];
|
|
2256
|
+
const filesFromSingular = flags.file && typeof flags.file === 'string' ? [flags.file] : [];
|
|
2257
|
+
const file_paths_merged = [...filesFromSingular, ...filesFromPlural];
|
|
2258
|
+
const file_paths = file_paths_merged.length > 0 ? file_paths_merged : null;
|
|
2259
|
+
const rawImp = flags.importance !== undefined ? parseInt(flags.importance, 10) : 2;
|
|
2260
|
+
if (flags.importance !== undefined && (isNaN(rawImp) || rawImp < 1 || rawImp > 3)) {
|
|
2261
|
+
fail(`[mem] Invalid importance "${flags.importance}". Must be 1, 2, or 3.`);
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
const id = saveEvent(db, {
|
|
2265
|
+
project,
|
|
2266
|
+
event_type: type,
|
|
2267
|
+
title,
|
|
2268
|
+
body,
|
|
2269
|
+
importance: rawImp,
|
|
2270
|
+
file_paths,
|
|
2271
|
+
});
|
|
2272
|
+
out(JSON.stringify({ ok: true, id }));
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
if (sub === 'search') {
|
|
2277
|
+
const q = positional.join(' ');
|
|
2278
|
+
if (!q) {
|
|
2279
|
+
fail('[mem] activity search: query required');
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
const type = flags.type || null;
|
|
2283
|
+
if (type !== null && !VALID_EVENT_TYPES.has(type)) {
|
|
2284
|
+
fail(`[mem] activity search: invalid --type "${type}". Valid: ${[...VALID_EVENT_TYPES].join(', ')}`);
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
const limit = flags.limit !== undefined ? parseInt(flags.limit, 10) : 10;
|
|
2288
|
+
const rows = searchEvents(db, q, { project, type, limit });
|
|
2289
|
+
out(formatActivityResults(rows));
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
if (sub === 'recent') {
|
|
2294
|
+
// Accept either `activity recent 5` or `activity recent --limit 5`.
|
|
2295
|
+
const posLimit = positional.length > 0 ? parseInt(positional[0], 10) : NaN;
|
|
2296
|
+
const flagLimit = flags.limit !== undefined ? parseInt(flags.limit, 10) : NaN;
|
|
2297
|
+
const limit = Number.isFinite(posLimit) ? posLimit : (Number.isFinite(flagLimit) ? flagLimit : 20);
|
|
2298
|
+
const type = flags.type || null;
|
|
2299
|
+
if (type !== null && !VALID_EVENT_TYPES.has(type)) {
|
|
2300
|
+
fail(`[mem] activity recent: invalid --type "${type}". Valid: ${[...VALID_EVENT_TYPES].join(', ')}`);
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
const rows = recentEvents(db, { project, type, limit });
|
|
2304
|
+
out(formatActivityResults(rows));
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
if (sub === 'show') {
|
|
2309
|
+
const id = positional.length > 0 ? parseInt(positional[0], 10) : NaN;
|
|
2310
|
+
if (!Number.isFinite(id)) {
|
|
2311
|
+
fail('[mem] activity show: numeric id required');
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
const row = getEvent(db, id);
|
|
2315
|
+
out(row ? JSON.stringify(row, null, 2) : 'Not found');
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
fail(`[mem] Unknown activity subcommand: ${sub}`);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2196
2322
|
// ─── Main Entry Point ────────────────────────────────────────────────────────
|
|
2197
2323
|
|
|
2198
2324
|
export async function run(argv) {
|
|
@@ -2241,6 +2367,8 @@ export async function run(argv) {
|
|
|
2241
2367
|
case 'registry': cmdRegistry(db, cmdArgs); break;
|
|
2242
2368
|
case 'import': await cmdImport(cmdArgs); break;
|
|
2243
2369
|
case 'enrich': await cmdEnrich(cmdArgs); break;
|
|
2370
|
+
case 'doctor': await cmdDoctor(db, cmdArgs); break;
|
|
2371
|
+
case 'activity': await cmdActivity(db, cmdArgs); break;
|
|
2244
2372
|
default:
|
|
2245
2373
|
out(`[mem] Unknown command: ${cmd}`);
|
|
2246
2374
|
out('[mem] Run "claude-mem-lite help" for usage');
|