@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.
- package/bin/cmk-compress-lazy.mjs +59 -0
- package/bin/cmk-daily-distill.mjs +67 -0
- package/bin/cmk-weekly-curate.mjs +56 -0
- package/bin/cmk.mjs +12 -0
- package/package.json +50 -0
- package/src/audit-log.mjs +103 -0
- package/src/auto-extract.mjs +742 -0
- package/src/capture-prompt.mjs +61 -0
- package/src/capture-turn.mjs +273 -0
- package/src/claude-md.mjs +212 -0
- package/src/compress-session.mjs +349 -0
- package/src/compressor.mjs +376 -0
- package/src/conflict-queue.mjs +796 -0
- package/src/cooldown.mjs +61 -0
- package/src/daily-distill.mjs +252 -0
- package/src/doctor.mjs +528 -0
- package/src/forget.mjs +335 -0
- package/src/frontmatter.mjs +73 -0
- package/src/import-anthropic-memory.mjs +266 -0
- package/src/index-db.mjs +154 -0
- package/src/index-rebuild.mjs +597 -0
- package/src/index.mjs +90 -0
- package/src/inject-context.mjs +484 -0
- package/src/install.mjs +327 -0
- package/src/lazy-compress.mjs +326 -0
- package/src/lock-discipline.mjs +166 -0
- package/src/mcp-server.mjs +498 -0
- package/src/memory-write.mjs +565 -0
- package/src/merge-facts.mjs +213 -0
- package/src/observe-edit.mjs +87 -0
- package/src/platform-commands.mjs +138 -0
- package/src/poison-guard.mjs +245 -0
- package/src/privacy.mjs +21 -0
- package/src/provenance.mjs +217 -0
- package/src/register-crons.mjs +354 -0
- package/src/reindex.mjs +134 -0
- package/src/repair.mjs +316 -0
- package/src/result-shapes.mjs +155 -0
- package/src/review-queue.mjs +345 -0
- package/src/roll.mjs +115 -0
- package/src/scratchpad.mjs +335 -0
- package/src/search.mjs +311 -0
- package/src/subcommands.mjs +1252 -0
- package/src/tier-paths.mjs +74 -0
- package/src/transcripts.mjs +234 -0
- package/src/trust.mjs +226 -0
- package/src/weekly-curate.mjs +454 -0
- package/src/write-fact.mjs +205 -0
- package/template/.claude/hooks/pre-tool-memory.js +78 -0
- package/template/.claude/hooks/transcript-capture.js +69 -0
- package/template/.claude/settings.json +27 -0
- package/template/.claude/skills/memory-write/SKILL.md +117 -0
- package/template/.gitignore.fragment +12 -0
- package/template/CLAUDE.md.template +49 -0
- package/template/docs/journey/journey-log.md.template +292 -0
- package/template/local/machine-paths.md.template +37 -0
- package/template/local/overrides.md.template +36 -0
- package/template/project/.index/.gitkeep +0 -0
- package/template/project/MEMORY.md.template +47 -0
- package/template/project/SOUL.md.template +35 -0
- package/template/project/memory/INDEX.md.template +47 -0
- package/template/project/memory/archive/superseded/.gitkeep +0 -0
- package/template/project/memory/archive/tombstones/.gitkeep +0 -0
- package/template/project/queues/.gitkeep +0 -0
- package/template/project/sessions/.gitkeep +0 -0
- package/template/project/transcripts/.gitkeep +0 -0
- package/template/support/cron-jobs/daily-memory-distill.md +15 -0
- package/template/support/cron-jobs/nightly-memsearch-index.md +17 -0
- package/template/support/cron-jobs/weekly-memory-curator.md +15 -0
- package/template/support/milvus-deploy/README.md +57 -0
- package/template/support/milvus-deploy/docker-compose.yml +66 -0
- package/template/support/scripts/auto-extract-memory.sh +102 -0
- package/template/support/scripts/memsearch-index-with-flush.sh +59 -0
- package/template/support/scripts/refresh-distill-timestamp.py +35 -0
- package/template/support/scripts/register-crons.py +242 -0
- package/template/support/scripts/run-daily-distill.sh +67 -0
- package/template/support/scripts/run-weekly-curate.sh +58 -0
- package/template/user/HABITS.md.template +18 -0
- package/template/user/LESSONS.md.template +18 -0
- package/template/user/USER.md.template +18 -0
- package/template/user/fragments/INDEX.md.template +23 -0
package/src/cooldown.mjs
ADDED
|
@@ -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
|
+
}
|