@lh8ppl/claude-memory-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/bin/cmk-compress-lazy.mjs +59 -0
  2. package/bin/cmk-daily-distill.mjs +67 -0
  3. package/bin/cmk-weekly-curate.mjs +56 -0
  4. package/bin/cmk.mjs +12 -0
  5. package/package.json +50 -0
  6. package/src/audit-log.mjs +103 -0
  7. package/src/auto-extract.mjs +742 -0
  8. package/src/capture-prompt.mjs +61 -0
  9. package/src/capture-turn.mjs +273 -0
  10. package/src/claude-md.mjs +212 -0
  11. package/src/compress-session.mjs +349 -0
  12. package/src/compressor.mjs +376 -0
  13. package/src/conflict-queue.mjs +796 -0
  14. package/src/cooldown.mjs +61 -0
  15. package/src/daily-distill.mjs +252 -0
  16. package/src/doctor.mjs +528 -0
  17. package/src/forget.mjs +335 -0
  18. package/src/frontmatter.mjs +73 -0
  19. package/src/import-anthropic-memory.mjs +266 -0
  20. package/src/index-db.mjs +154 -0
  21. package/src/index-rebuild.mjs +597 -0
  22. package/src/index.mjs +90 -0
  23. package/src/inject-context.mjs +484 -0
  24. package/src/install.mjs +327 -0
  25. package/src/lazy-compress.mjs +326 -0
  26. package/src/lock-discipline.mjs +166 -0
  27. package/src/mcp-server.mjs +498 -0
  28. package/src/memory-write.mjs +565 -0
  29. package/src/merge-facts.mjs +213 -0
  30. package/src/observe-edit.mjs +87 -0
  31. package/src/platform-commands.mjs +138 -0
  32. package/src/poison-guard.mjs +245 -0
  33. package/src/privacy.mjs +21 -0
  34. package/src/provenance.mjs +217 -0
  35. package/src/register-crons.mjs +354 -0
  36. package/src/reindex.mjs +134 -0
  37. package/src/repair.mjs +316 -0
  38. package/src/result-shapes.mjs +155 -0
  39. package/src/review-queue.mjs +345 -0
  40. package/src/roll.mjs +115 -0
  41. package/src/scratchpad.mjs +335 -0
  42. package/src/search.mjs +311 -0
  43. package/src/subcommands.mjs +1252 -0
  44. package/src/tier-paths.mjs +74 -0
  45. package/src/transcripts.mjs +234 -0
  46. package/src/trust.mjs +226 -0
  47. package/src/weekly-curate.mjs +454 -0
  48. package/src/write-fact.mjs +205 -0
  49. package/template/.claude/hooks/pre-tool-memory.js +78 -0
  50. package/template/.claude/hooks/transcript-capture.js +69 -0
  51. package/template/.claude/settings.json +27 -0
  52. package/template/.claude/skills/memory-write/SKILL.md +117 -0
  53. package/template/.gitignore.fragment +12 -0
  54. package/template/CLAUDE.md.template +49 -0
  55. package/template/docs/journey/journey-log.md.template +292 -0
  56. package/template/local/machine-paths.md.template +37 -0
  57. package/template/local/overrides.md.template +36 -0
  58. package/template/project/.index/.gitkeep +0 -0
  59. package/template/project/MEMORY.md.template +47 -0
  60. package/template/project/SOUL.md.template +35 -0
  61. package/template/project/memory/INDEX.md.template +47 -0
  62. package/template/project/memory/archive/superseded/.gitkeep +0 -0
  63. package/template/project/memory/archive/tombstones/.gitkeep +0 -0
  64. package/template/project/queues/.gitkeep +0 -0
  65. package/template/project/sessions/.gitkeep +0 -0
  66. package/template/project/transcripts/.gitkeep +0 -0
  67. package/template/support/cron-jobs/daily-memory-distill.md +15 -0
  68. package/template/support/cron-jobs/nightly-memsearch-index.md +17 -0
  69. package/template/support/cron-jobs/weekly-memory-curator.md +15 -0
  70. package/template/support/milvus-deploy/README.md +57 -0
  71. package/template/support/milvus-deploy/docker-compose.yml +66 -0
  72. package/template/support/scripts/auto-extract-memory.sh +102 -0
  73. package/template/support/scripts/memsearch-index-with-flush.sh +59 -0
  74. package/template/support/scripts/refresh-distill-timestamp.py +35 -0
  75. package/template/support/scripts/register-crons.py +242 -0
  76. package/template/support/scripts/run-daily-distill.sh +67 -0
  77. package/template/support/scripts/run-weekly-curate.sh +58 -0
  78. package/template/user/HABITS.md.template +18 -0
  79. package/template/user/LESSONS.md.template +18 -0
  80. package/template/user/USER.md.template +18 -0
  81. package/template/user/fragments/INDEX.md.template +23 -0
@@ -0,0 +1,61 @@
1
+ // Shared cooldown-marker helpers (Task 27 checkpoint extraction).
2
+ //
3
+ // Before this module, `touchCooldownMarker` + `isCooldownActive` lived
4
+ // inline in compress-session.mjs and were ONLY called from there.
5
+ // auto-extract.mjs never touched the marker — even though
6
+ // compress-session.mjs's design rationale explicitly documents that
7
+ // auto-extract participates in the cooldown ("the auto-extract
8
+ // subagent may have just spent the budget on a Stop-hook fire").
9
+ // That gap meant the cooldown only fired on SessionEnd→SessionEnd
10
+ // within 120s — which doesn't happen in practice — instead of the
11
+ // documented Stop→SessionEnd guarding. Each session paid ~2x the
12
+ // budgeted Haiku cost.
13
+ //
14
+ // This module is the single source of truth for cooldown state.
15
+ // Both compress-session and auto-extract import from here.
16
+
17
+ import {
18
+ existsSync,
19
+ mkdirSync,
20
+ statSync,
21
+ utimesSync,
22
+ writeFileSync,
23
+ } from 'node:fs';
24
+ import { dirname, join } from 'node:path';
25
+
26
+ const COOLDOWN_RELATIVE = ['context', '.locks', 'last-haiku-call.ts'];
27
+
28
+ export const DEFAULT_COOLDOWN_MS = 120_000;
29
+
30
+ export function cooldownMarkerPath(projectRoot) {
31
+ return join(projectRoot, ...COOLDOWN_RELATIVE);
32
+ }
33
+
34
+ export function isCooldownActive({ projectRoot, now, cooldownMs }) {
35
+ const marker = cooldownMarkerPath(projectRoot);
36
+ if (!existsSync(marker)) return false;
37
+ let mtime;
38
+ try {
39
+ mtime = statSync(marker).mtimeMs;
40
+ } catch {
41
+ return false;
42
+ }
43
+ const nowMs = new Date(now).getTime();
44
+ return nowMs - mtime < cooldownMs;
45
+ }
46
+
47
+ export function touchCooldownMarker({ projectRoot, now }) {
48
+ const marker = cooldownMarkerPath(projectRoot);
49
+ mkdirSync(dirname(marker), { recursive: true });
50
+ if (!existsSync(marker)) {
51
+ writeFileSync(marker, '', 'utf8');
52
+ }
53
+ const ts = new Date(now);
54
+ try {
55
+ utimesSync(marker, ts, ts);
56
+ } catch {
57
+ // utimes can fail on exotic filesystems; the existence of the
58
+ // marker is the load-bearing signal — mtime drift by a few
59
+ // seconds doesn't break cooldown logic.
60
+ }
61
+ }
@@ -0,0 +1,252 @@
1
+ // Daily distill (Task 33, T-028).
2
+ //
3
+ // Reads the last 7 days of `context/sessions/today-{date}.md`, sends
4
+ // them to Haiku as a single consolidation prompt, and writes the
5
+ // distilled summary to `context/sessions/recent.md`. Honors the kit's
6
+ // 120s Haiku cooldown via the shared `cooldown.mjs` module.
7
+ //
8
+ // Public boundary: `dailyDistill({projectRoot, backend, now, cooldownMs?, maxOutputBytes?})`.
9
+ //
10
+ // Composes on top of:
11
+ // - cooldown.mjs (Task 28 B2 + Task 22) — shared `isCooldownActive` /
12
+ // `touchCooldownMarker` to honor the kit's 120s Haiku budget
13
+ // - compressor.mjs (Task 22) — CompressorBackend interface; the bin
14
+ // wrapper passes HaikuViaAnthropicApi
15
+ // - result-shapes.mjs — errorResult + ERROR_CATEGORIES
16
+ //
17
+ // Per design §1.4 + §8.1 + tasks.md 33.
18
+
19
+ import {
20
+ appendFileSync,
21
+ existsSync,
22
+ mkdirSync,
23
+ readdirSync,
24
+ readFileSync,
25
+ writeFileSync,
26
+ } from 'node:fs';
27
+ import { join } from 'node:path';
28
+ import { nowIso } from './audit-log.mjs';
29
+ import { ERROR_CATEGORIES } from './result-shapes.mjs';
30
+ import { HaikuTimeoutError } from './compressor.mjs';
31
+ import {
32
+ DEFAULT_COOLDOWN_MS,
33
+ isCooldownActive,
34
+ touchCooldownMarker,
35
+ } from './cooldown.mjs';
36
+
37
+ const DEFAULT_MAX_OUTPUT_BYTES = 4096;
38
+ const SESSIONS_REL = ['context', 'sessions'];
39
+ const RECENT_MD_REL = ['context', 'sessions', 'recent.md'];
40
+
41
+ // Match `today-YYYY-MM-DD.md` exactly so other files in sessions/ don't
42
+ // get pulled into the distill (e.g., now.md, *.compress.log, *.extract.log).
43
+ const TODAY_RE = /^today-(\d{4}-\d{2}-\d{2})\.md$/;
44
+
45
+ function buildDistillInstructions(maxOutputBytes) {
46
+ return [
47
+ 'You are a memory consolidator for claude-memory-kit. Your task is to combine the daily session summaries below into a single weekly-or-shorter rolling summary.',
48
+ '',
49
+ 'Output ONLY the consolidated Markdown. Do not write preamble. Do not acknowledge the task. Begin your response with the first section heading.',
50
+ '',
51
+ 'REQUIRED FORMAT (emit headings exactly, in this order; omit any heading whose section would have no entries):',
52
+ '',
53
+ '## Decisions',
54
+ '- <one bullet per concrete decision across all days, ≤80 chars>',
55
+ '',
56
+ '## Open Questions',
57
+ '- <one bullet per unresolved question, ≤80 chars>',
58
+ '',
59
+ '## Files Touched',
60
+ '- path: <relative path> — <verb summary across days>',
61
+ '',
62
+ '## Active Threads',
63
+ '- <one bullet per active work-in-progress thread, ≤80 chars>',
64
+ '',
65
+ 'HARD RULES:',
66
+ ' 1. Preserve every citation ID matching /#[ULP]-[A-Z0-9]{6,8}/ verbatim. Never invent new IDs.',
67
+ ` 2. Total output ≤ ${maxOutputBytes} bytes.`,
68
+ ' 3. If a section has no entries, omit the heading entirely.',
69
+ ' 4. No prose around the headings — only the bulleted list per section.',
70
+ ' 5. Deduplicate aggressively: if the same decision appears across multiple days, list it ONCE.',
71
+ ' 6. Your output goes directly into the next session\'s memory. Do not address the user, do not refer to yourself.',
72
+ '',
73
+ '=== BEGIN DAILY SUMMARIES TO CONSOLIDATE ===',
74
+ ].join('\n');
75
+ }
76
+
77
+ function listTodayFiles(projectRoot, now) {
78
+ const sessionsDir = join(projectRoot, ...SESSIONS_REL);
79
+ if (!existsSync(sessionsDir)) return [];
80
+ const cutoffMs = new Date(now).getTime() - 7 * 24 * 60 * 60 * 1000;
81
+ const matches = [];
82
+ for (const name of readdirSync(sessionsDir)) {
83
+ const m = TODAY_RE.exec(name);
84
+ if (!m) continue;
85
+ const fileDate = m[1];
86
+ const fileMs = new Date(fileDate + 'T00:00:00Z').getTime();
87
+ if (Number.isFinite(fileMs) && fileMs >= cutoffMs) {
88
+ matches.push({ name, date: fileDate, path: join(sessionsDir, name) });
89
+ }
90
+ }
91
+ // Chronological order (oldest first) so Haiku sees days in sequence.
92
+ matches.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
93
+ return matches;
94
+ }
95
+
96
+ function readBuffer(files) {
97
+ return files
98
+ .map((f) => `## ${f.date}\n\n${readFileSync(f.path, 'utf8')}`)
99
+ .join('\n\n');
100
+ }
101
+
102
+ function distillLogPath(projectRoot, date) {
103
+ return join(projectRoot, ...SESSIONS_REL, `${date}.distill.log`);
104
+ }
105
+
106
+ function writeDistillLogEntry({ projectRoot, date, entry }) {
107
+ const path = distillLogPath(projectRoot, date);
108
+ mkdirSync(join(projectRoot, ...SESSIONS_REL), { recursive: true });
109
+ appendFileSync(path, JSON.stringify(entry) + '\n', 'utf8');
110
+ return path;
111
+ }
112
+
113
+ function recentMdPath(projectRoot) {
114
+ return join(projectRoot, ...RECENT_MD_REL);
115
+ }
116
+
117
+ /**
118
+ * Run the daily distill cycle.
119
+ *
120
+ * @returns {Promise<object>} action: 'distilled' | 'skipped' | 'error'
121
+ */
122
+ export async function dailyDistill({
123
+ projectRoot,
124
+ backend,
125
+ now,
126
+ cooldownMs = DEFAULT_COOLDOWN_MS,
127
+ maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES,
128
+ } = {}) {
129
+ const ts = now ?? nowIso();
130
+ const date = ts.slice(0, 10);
131
+ const t0 = Date.now();
132
+
133
+ if (!projectRoot) {
134
+ return { action: 'error', error_category: ERROR_CATEGORIES.MISSING_PROJECT_ROOT };
135
+ }
136
+ if (!backend || typeof backend.compress !== 'function') {
137
+ return { action: 'error', error_category: ERROR_CATEGORIES.MISSING_BACKEND };
138
+ }
139
+
140
+ // Project must be installed (sessions/ exists). If not, no-op silently.
141
+ const sessionsDir = join(projectRoot, ...SESSIONS_REL);
142
+ if (!existsSync(sessionsDir)) {
143
+ return { action: 'skipped', reason: 'no-context-dir', duration_ms: Date.now() - t0 };
144
+ }
145
+
146
+ // Cooldown gate per design §8.2.
147
+ if (isCooldownActive({ projectRoot, now: ts, cooldownMs })) {
148
+ const duration_ms = Date.now() - t0;
149
+ const entry = {
150
+ ts, scope: 'daily-distill',
151
+ input_bytes: 0, output_bytes: 0,
152
+ model_id: typeof backend.modelId === 'function' ? backend.modelId() : null,
153
+ cost_usd: 0, duration_ms, success: true, skipped_reason: 'cooldown',
154
+ };
155
+ writeDistillLogEntry({ projectRoot, date, entry });
156
+ return { action: 'skipped', reason: 'cooldown', duration_ms };
157
+ }
158
+
159
+ // Read last 7 days of today-*.md.
160
+ const files = listTodayFiles(projectRoot, ts);
161
+ if (files.length === 0) {
162
+ // Task 36 Door-4 fix (skill-review during checkpoint, 2026-05-28):
163
+ // emit an NDJSON entry on the no-input skip path so ops have
164
+ // observability + so the spawn-smoke chain test can prove
165
+ // projectRoot was correctly resolved from argv. Same posture as
166
+ // weekly-curate's no-old-files path.
167
+ const duration_ms = Date.now() - t0;
168
+ writeDistillLogEntry({
169
+ projectRoot,
170
+ date,
171
+ entry: {
172
+ ts,
173
+ scope: 'daily-distill',
174
+ input_bytes: 0,
175
+ output_bytes: 0,
176
+ model_id: null,
177
+ cost_usd: 0,
178
+ duration_ms,
179
+ success: true,
180
+ skipped_reason: 'no-input',
181
+ },
182
+ });
183
+ return { action: 'skipped', reason: 'no-input', duration_ms };
184
+ }
185
+
186
+ const buffer = readBuffer(files);
187
+ const input_bytes = Buffer.byteLength(buffer, 'utf8');
188
+ const instructions = buildDistillInstructions(maxOutputBytes);
189
+
190
+ let result;
191
+ try {
192
+ result = await backend.compress({
193
+ input: buffer,
194
+ instructions,
195
+ preserveCitationIds: true,
196
+ maxOutputBytes,
197
+ timeoutMs: 50_000,
198
+ });
199
+ touchCooldownMarker({ projectRoot, now: ts });
200
+ } catch (err) {
201
+ touchCooldownMarker({ projectRoot, now: ts });
202
+ const errorCategory =
203
+ err instanceof HaikuTimeoutError
204
+ ? ERROR_CATEGORIES.HAIKU_TIMEOUT
205
+ : ERROR_CATEGORIES.COMPRESS_FAILED;
206
+ const duration_ms = Date.now() - t0;
207
+ writeDistillLogEntry({
208
+ projectRoot, date,
209
+ entry: {
210
+ ts, scope: 'daily-distill', input_bytes, output_bytes: 0,
211
+ model_id: typeof backend.modelId === 'function' ? backend.modelId() : null,
212
+ cost_usd: 0, duration_ms, success: false, error_category: errorCategory,
213
+ },
214
+ });
215
+ return {
216
+ action: 'error', error_category: errorCategory, duration_ms,
217
+ errorMessage: err?.message ?? String(err),
218
+ };
219
+ }
220
+
221
+ const output = result?.outputText ?? '';
222
+ const output_bytes = Buffer.byteLength(output, 'utf8');
223
+
224
+ // Overwrite recent.md atomically: write to a temp file then rename.
225
+ // For v0.1.0 a direct overwrite is fine (single-writer assumption);
226
+ // atomic-rename would be a v0.1.x hardening if cron + manual roll
227
+ // ever overlap.
228
+ const path = recentMdPath(projectRoot);
229
+ mkdirSync(join(projectRoot, ...SESSIONS_REL), { recursive: true });
230
+ writeFileSync(path, output, 'utf8');
231
+
232
+ const duration_ms = Date.now() - t0;
233
+ writeDistillLogEntry({
234
+ projectRoot, date,
235
+ entry: {
236
+ ts, scope: 'daily-distill', input_bytes, output_bytes,
237
+ model_id:
238
+ result?.modelId ??
239
+ (typeof backend.modelId === 'function' ? backend.modelId() : null),
240
+ cost_usd: result?.costUSD ?? 0,
241
+ duration_ms, success: true, source_days: files.length,
242
+ },
243
+ });
244
+ return {
245
+ action: 'distilled',
246
+ outputPath: path,
247
+ bytesIn: input_bytes,
248
+ bytesOut: output_bytes,
249
+ sourceDays: files.length,
250
+ duration_ms,
251
+ };
252
+ }