@lh8ppl/claude-memory-kit 0.1.0 → 0.1.2

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 (46) hide show
  1. package/README.md +77 -0
  2. package/bin/cmk-auto-extract.mjs +62 -0
  3. package/bin/cmk-capture-prompt.mjs +65 -0
  4. package/bin/cmk-capture-turn.mjs +76 -0
  5. package/bin/cmk-compress-lazy.mjs +0 -0
  6. package/bin/cmk-compress-session.mjs +64 -0
  7. package/bin/cmk-daily-distill.mjs +0 -0
  8. package/bin/cmk-inject-context.mjs +69 -0
  9. package/bin/cmk-observe-edit.mjs +57 -0
  10. package/bin/cmk-weekly-curate.mjs +0 -0
  11. package/bin/cmk.mjs +11 -11
  12. package/package.json +10 -2
  13. package/src/audit-log.mjs +1 -0
  14. package/src/claude-md.mjs +212 -212
  15. package/src/compressor.mjs +18 -18
  16. package/src/doctor.mjs +21 -8
  17. package/src/frontmatter.mjs +73 -73
  18. package/src/index-rebuild.mjs +26 -4
  19. package/src/inject-context.mjs +150 -10
  20. package/src/install.mjs +49 -1
  21. package/src/mcp-server.mjs +17 -0
  22. package/src/memory-write.mjs +18 -5
  23. package/src/merge-facts.mjs +213 -213
  24. package/src/provenance.mjs +217 -217
  25. package/src/reindex.mjs +134 -134
  26. package/src/repair.mjs +26 -96
  27. package/src/sanitize.mjs +39 -0
  28. package/src/settings-hooks.mjs +186 -0
  29. package/src/spawn-bin.mjs +83 -0
  30. package/src/subcommands.mjs +144 -10
  31. package/src/write-fact.mjs +46 -3
  32. package/template/.gitignore.fragment +12 -12
  33. package/template/CLAUDE.md.template +53 -49
  34. package/template/docs/journey/journey-log.md.template +292 -292
  35. package/template/project/memory/INDEX.md.template +47 -47
  36. package/template/support/cron-jobs/daily-memory-distill.md +15 -15
  37. package/template/support/cron-jobs/nightly-memsearch-index.md +17 -17
  38. package/template/support/cron-jobs/weekly-memory-curator.md +15 -15
  39. package/template/support/milvus-deploy/README.md +57 -57
  40. package/template/support/milvus-deploy/docker-compose.yml +66 -66
  41. package/template/support/scripts/auto-extract-memory.sh +102 -102
  42. package/template/support/scripts/memsearch-index-with-flush.sh +59 -59
  43. package/template/support/scripts/refresh-distill-timestamp.py +35 -35
  44. package/template/support/scripts/register-crons.py +242 -242
  45. package/template/support/scripts/run-daily-distill.sh +67 -67
  46. package/template/support/scripts/run-weekly-curate.sh +58 -58
@@ -1,213 +1,213 @@
1
- // Fact consolidation (Task 10, refactored in cleanup-layer-2-cross-module-drift).
2
- // Single public boundary: mergeFacts(opts) → result. See design §3.4.
3
- //
4
- // Uses shared modules: tier-paths, frontmatter, audit-log, result-shapes.
5
- // Composes writeFact() to create the new merged fact, then moves A + B into
6
- // archive/superseded/ with superseded_by injected. See CLAUDE.md "Shared
7
- // modules" rule.
8
-
9
- import {
10
- existsSync,
11
- mkdirSync,
12
- readdirSync,
13
- readFileSync,
14
- statSync,
15
- unlinkSync,
16
- writeFileSync,
17
- } from 'node:fs';
18
- import { join } from 'node:path';
19
- import {
20
- VALID_TIERS,
21
- ID_PATTERN,
22
- resolveTierRoot,
23
- resolveFactDir,
24
- } from './tier-paths.mjs';
25
- import { parse, format } from './frontmatter.mjs';
26
- import { appendAuditEntry, nowIso, REASON_CODES } from './audit-log.mjs';
27
- import { ERROR_CATEGORIES, errorResult, notFoundResult } from './result-shapes.mjs';
28
- import { writeFact } from './write-fact.mjs';
29
-
30
- function listLiveFactFiles(factDir) {
31
- if (!existsSync(factDir)) return [];
32
- const out = [];
33
- for (const entry of readdirSync(factDir, { withFileTypes: true })) {
34
- if (!entry.isFile()) continue;
35
- if (!entry.name.endsWith('.md')) continue;
36
- if (entry.name === 'INDEX.md') continue;
37
- out.push(entry.name);
38
- }
39
- return out;
40
- }
41
-
42
- function findLiveFactById(factDir, id) {
43
- if (!existsSync(factDir)) return null;
44
- for (const filename of listLiveFactFiles(factDir)) {
45
- const p = join(factDir, filename);
46
- if (!statSync(p).isFile()) continue;
47
- const { frontmatter, body } = parse(readFileSync(p, 'utf8'));
48
- if (frontmatter?.id === id && !frontmatter.deleted_at) {
49
- return { id, path: p, frontmatter, body };
50
- }
51
- }
52
- return null;
53
- }
54
-
55
- function moveToSuperseded(match, supersededBy) {
56
- const supersededDir = join(match.factDir, 'archive', 'superseded');
57
- mkdirSync(supersededDir, { recursive: true });
58
- const newPath = join(supersededDir, `${match.id}.md`);
59
- const { frontmatter, body } = parse(readFileSync(match.path, 'utf8'));
60
- const updated = {
61
- superseded_by: supersededBy,
62
- ...(frontmatter ?? {}),
63
- };
64
- writeFileSync(newPath, format({ frontmatter: updated, body }), 'utf8');
65
- unlinkSync(match.path);
66
- return newPath;
67
- }
68
-
69
- export function mergeFacts(opts = {}) {
70
- const {
71
- idA,
72
- idB,
73
- mergedBody,
74
- mergedTitle,
75
- mergedSlug,
76
- mergedType,
77
- writeSource,
78
- trust,
79
- sourceFile,
80
- sourceLine,
81
- sourceSha1,
82
- mergedTags,
83
- projectRoot,
84
- userDir,
85
- now,
86
- } = opts;
87
-
88
- const errors = [];
89
- if (!idA || !ID_PATTERN.test(idA)) errors.push('idA: must be a valid citation ID');
90
- if (!idB || !ID_PATTERN.test(idB)) errors.push('idB: must be a valid citation ID');
91
- if (idA && idB && idA === idB) {
92
- return errorResult({
93
- category: ERROR_CATEGORIES.SCHEMA,
94
- errors: [`idA and idB are the same (${idA}); cannot merge a fact with itself`],
95
- });
96
- }
97
- if (!mergedBody || typeof mergedBody !== 'string' || !mergedBody.length) {
98
- errors.push('mergedBody: required, non-empty string');
99
- }
100
- if (!mergedTitle || typeof mergedTitle !== 'string') {
101
- errors.push('mergedTitle: required, non-empty string');
102
- }
103
- // Layer-2 review S4: removed the redundant `mergedSlug` truthy check. The
104
- // downstream writeFact owns all slug validation (pattern + presence).
105
- // Inconsistent layering disappears; bad slugs surface from writeFact with
106
- // a clear schema error.
107
- //
108
- // Layer-2 review S3: writeSource is now REQUIRED (no compressor default).
109
- // Compressor was the most-suspicious default — Task 23 auto-extract and
110
- // Task 24 memory-write are NOT compressor-driven. Forcing the caller to
111
- // pick avoids accidentally tagging human-curated merges as 'compressor'.
112
- if (!writeSource || typeof writeSource !== 'string') {
113
- errors.push('writeSource: required (no default). Pick one of user-explicit/auto-extract/compressor/manual-edit/imported.');
114
- }
115
- if (errors.length > 0) {
116
- return errorResult({ category: ERROR_CATEGORIES.SCHEMA, errors });
117
- }
118
-
119
- const tierA = idA[0];
120
- const tierB = idB[0];
121
- if (tierA !== tierB) {
122
- return errorResult({
123
- category: ERROR_CATEGORIES.SCHEMA,
124
- errors: [
125
- `cross-tier merge not supported: idA tier (${tierA}) ≠ idB tier (${tierB}). Promote one side to the same tier first.`,
126
- ],
127
- });
128
- }
129
- const tier = tierA;
130
- if (!VALID_TIERS.has(tier)) {
131
- return errorResult({
132
- category: ERROR_CATEGORIES.SCHEMA,
133
- errors: [`invalid tier prefix on ids: ${tier}`],
134
- });
135
- }
136
-
137
- const tierRoot = resolveTierRoot({ tier, projectRoot, userDir });
138
- const factDir = resolveFactDir(tier, tierRoot);
139
-
140
- const matchA = findLiveFactById(factDir, idA);
141
- const matchB = findLiveFactById(factDir, idB);
142
- if (!matchA || !matchB) {
143
- const missing = [];
144
- if (!matchA) missing.push(idA);
145
- if (!matchB) missing.push(idB);
146
- return notFoundResult({
147
- errors: [`no live fact found for ${missing.join(', ')}`],
148
- });
149
- }
150
- matchA.factDir = factDir;
151
- matchB.factDir = factDir;
152
-
153
- const typeC =
154
- mergedType ?? matchA.frontmatter.type ?? matchB.frontmatter.type;
155
-
156
- const writeResult = writeFact({
157
- tier,
158
- type: typeC,
159
- slug: mergedSlug,
160
- title: mergedTitle,
161
- body: mergedBody,
162
- writeSource,
163
- trust: trust ?? 'high',
164
- sourceFile: sourceFile ?? matchA.frontmatter.source_file ?? 'merge',
165
- sourceLine: sourceLine ?? 1,
166
- sourceSha1: sourceSha1 ?? matchA.frontmatter.source_sha1 ?? 'merged',
167
- mergedFrom: [idA, idB],
168
- tags: mergedTags,
169
- projectRoot,
170
- userDir,
171
- });
172
- if (writeResult.action === 'error') {
173
- return errorResult({
174
- category: writeResult.errorCategory,
175
- errors: writeResult.errors,
176
- });
177
- }
178
- // PR-1 blocker B1 fix preserved: writeFact dedup'd to an existing unrelated
179
- // fact → return collision error rather than silently retargeting A and B.
180
- if (writeResult.action !== 'created') {
181
- return errorResult({
182
- category: ERROR_CATEGORIES.COLLISION,
183
- errors: [
184
- `merged body collides with existing fact ${writeResult.id} (writeFact returned ${writeResult.action}${writeResult.skipReason ? ': ' + writeResult.skipReason : ''}); choose a different mergedBody`,
185
- ],
186
- });
187
- }
188
-
189
- const supersededA = moveToSuperseded(matchA, writeResult.id);
190
- const supersededB = moveToSuperseded(matchB, writeResult.id);
191
-
192
- const ts = now ?? nowIso();
193
- appendAuditEntry(tierRoot, {
194
- ts,
195
- action: 'merged',
196
- tier,
197
- id: writeResult.id,
198
- reasonCode: REASON_CODES.CURATED_MERGE,
199
- paths: {
200
- after: writeResult.path,
201
- archive: [supersededA, supersededB],
202
- },
203
- extra: { mergedFrom: [idA, idB] },
204
- });
205
-
206
- return {
207
- action: 'merged',
208
- id: writeResult.id,
209
- tier,
210
- path: writeResult.path,
211
- supersededPaths: [supersededA, supersededB],
212
- };
213
- }
1
+ // Fact consolidation (Task 10, refactored in cleanup-layer-2-cross-module-drift).
2
+ // Single public boundary: mergeFacts(opts) → result. See design §3.4.
3
+ //
4
+ // Uses shared modules: tier-paths, frontmatter, audit-log, result-shapes.
5
+ // Composes writeFact() to create the new merged fact, then moves A + B into
6
+ // archive/superseded/ with superseded_by injected. See CLAUDE.md "Shared
7
+ // modules" rule.
8
+
9
+ import {
10
+ existsSync,
11
+ mkdirSync,
12
+ readdirSync,
13
+ readFileSync,
14
+ statSync,
15
+ unlinkSync,
16
+ writeFileSync,
17
+ } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import {
20
+ VALID_TIERS,
21
+ ID_PATTERN,
22
+ resolveTierRoot,
23
+ resolveFactDir,
24
+ } from './tier-paths.mjs';
25
+ import { parse, format } from './frontmatter.mjs';
26
+ import { appendAuditEntry, nowIso, REASON_CODES } from './audit-log.mjs';
27
+ import { ERROR_CATEGORIES, errorResult, notFoundResult } from './result-shapes.mjs';
28
+ import { writeFact } from './write-fact.mjs';
29
+
30
+ function listLiveFactFiles(factDir) {
31
+ if (!existsSync(factDir)) return [];
32
+ const out = [];
33
+ for (const entry of readdirSync(factDir, { withFileTypes: true })) {
34
+ if (!entry.isFile()) continue;
35
+ if (!entry.name.endsWith('.md')) continue;
36
+ if (entry.name === 'INDEX.md') continue;
37
+ out.push(entry.name);
38
+ }
39
+ return out;
40
+ }
41
+
42
+ function findLiveFactById(factDir, id) {
43
+ if (!existsSync(factDir)) return null;
44
+ for (const filename of listLiveFactFiles(factDir)) {
45
+ const p = join(factDir, filename);
46
+ if (!statSync(p).isFile()) continue;
47
+ const { frontmatter, body } = parse(readFileSync(p, 'utf8'));
48
+ if (frontmatter?.id === id && !frontmatter.deleted_at) {
49
+ return { id, path: p, frontmatter, body };
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+
55
+ function moveToSuperseded(match, supersededBy) {
56
+ const supersededDir = join(match.factDir, 'archive', 'superseded');
57
+ mkdirSync(supersededDir, { recursive: true });
58
+ const newPath = join(supersededDir, `${match.id}.md`);
59
+ const { frontmatter, body } = parse(readFileSync(match.path, 'utf8'));
60
+ const updated = {
61
+ superseded_by: supersededBy,
62
+ ...(frontmatter ?? {}),
63
+ };
64
+ writeFileSync(newPath, format({ frontmatter: updated, body }), 'utf8');
65
+ unlinkSync(match.path);
66
+ return newPath;
67
+ }
68
+
69
+ export function mergeFacts(opts = {}) {
70
+ const {
71
+ idA,
72
+ idB,
73
+ mergedBody,
74
+ mergedTitle,
75
+ mergedSlug,
76
+ mergedType,
77
+ writeSource,
78
+ trust,
79
+ sourceFile,
80
+ sourceLine,
81
+ sourceSha1,
82
+ mergedTags,
83
+ projectRoot,
84
+ userDir,
85
+ now,
86
+ } = opts;
87
+
88
+ const errors = [];
89
+ if (!idA || !ID_PATTERN.test(idA)) errors.push('idA: must be a valid citation ID');
90
+ if (!idB || !ID_PATTERN.test(idB)) errors.push('idB: must be a valid citation ID');
91
+ if (idA && idB && idA === idB) {
92
+ return errorResult({
93
+ category: ERROR_CATEGORIES.SCHEMA,
94
+ errors: [`idA and idB are the same (${idA}); cannot merge a fact with itself`],
95
+ });
96
+ }
97
+ if (!mergedBody || typeof mergedBody !== 'string' || !mergedBody.length) {
98
+ errors.push('mergedBody: required, non-empty string');
99
+ }
100
+ if (!mergedTitle || typeof mergedTitle !== 'string') {
101
+ errors.push('mergedTitle: required, non-empty string');
102
+ }
103
+ // Layer-2 review S4: removed the redundant `mergedSlug` truthy check. The
104
+ // downstream writeFact owns all slug validation (pattern + presence).
105
+ // Inconsistent layering disappears; bad slugs surface from writeFact with
106
+ // a clear schema error.
107
+ //
108
+ // Layer-2 review S3: writeSource is now REQUIRED (no compressor default).
109
+ // Compressor was the most-suspicious default — Task 23 auto-extract and
110
+ // Task 24 memory-write are NOT compressor-driven. Forcing the caller to
111
+ // pick avoids accidentally tagging human-curated merges as 'compressor'.
112
+ if (!writeSource || typeof writeSource !== 'string') {
113
+ errors.push('writeSource: required (no default). Pick one of user-explicit/auto-extract/compressor/manual-edit/imported.');
114
+ }
115
+ if (errors.length > 0) {
116
+ return errorResult({ category: ERROR_CATEGORIES.SCHEMA, errors });
117
+ }
118
+
119
+ const tierA = idA[0];
120
+ const tierB = idB[0];
121
+ if (tierA !== tierB) {
122
+ return errorResult({
123
+ category: ERROR_CATEGORIES.SCHEMA,
124
+ errors: [
125
+ `cross-tier merge not supported: idA tier (${tierA}) ≠ idB tier (${tierB}). Promote one side to the same tier first.`,
126
+ ],
127
+ });
128
+ }
129
+ const tier = tierA;
130
+ if (!VALID_TIERS.has(tier)) {
131
+ return errorResult({
132
+ category: ERROR_CATEGORIES.SCHEMA,
133
+ errors: [`invalid tier prefix on ids: ${tier}`],
134
+ });
135
+ }
136
+
137
+ const tierRoot = resolveTierRoot({ tier, projectRoot, userDir });
138
+ const factDir = resolveFactDir(tier, tierRoot);
139
+
140
+ const matchA = findLiveFactById(factDir, idA);
141
+ const matchB = findLiveFactById(factDir, idB);
142
+ if (!matchA || !matchB) {
143
+ const missing = [];
144
+ if (!matchA) missing.push(idA);
145
+ if (!matchB) missing.push(idB);
146
+ return notFoundResult({
147
+ errors: [`no live fact found for ${missing.join(', ')}`],
148
+ });
149
+ }
150
+ matchA.factDir = factDir;
151
+ matchB.factDir = factDir;
152
+
153
+ const typeC =
154
+ mergedType ?? matchA.frontmatter.type ?? matchB.frontmatter.type;
155
+
156
+ const writeResult = writeFact({
157
+ tier,
158
+ type: typeC,
159
+ slug: mergedSlug,
160
+ title: mergedTitle,
161
+ body: mergedBody,
162
+ writeSource,
163
+ trust: trust ?? 'high',
164
+ sourceFile: sourceFile ?? matchA.frontmatter.source_file ?? 'merge',
165
+ sourceLine: sourceLine ?? 1,
166
+ sourceSha1: sourceSha1 ?? matchA.frontmatter.source_sha1 ?? 'merged',
167
+ mergedFrom: [idA, idB],
168
+ tags: mergedTags,
169
+ projectRoot,
170
+ userDir,
171
+ });
172
+ if (writeResult.action === 'error') {
173
+ return errorResult({
174
+ category: writeResult.errorCategory,
175
+ errors: writeResult.errors,
176
+ });
177
+ }
178
+ // PR-1 blocker B1 fix preserved: writeFact dedup'd to an existing unrelated
179
+ // fact → return collision error rather than silently retargeting A and B.
180
+ if (writeResult.action !== 'created') {
181
+ return errorResult({
182
+ category: ERROR_CATEGORIES.COLLISION,
183
+ errors: [
184
+ `merged body collides with existing fact ${writeResult.id} (writeFact returned ${writeResult.action}${writeResult.skipReason ? ': ' + writeResult.skipReason : ''}); choose a different mergedBody`,
185
+ ],
186
+ });
187
+ }
188
+
189
+ const supersededA = moveToSuperseded(matchA, writeResult.id);
190
+ const supersededB = moveToSuperseded(matchB, writeResult.id);
191
+
192
+ const ts = now ?? nowIso();
193
+ appendAuditEntry(tierRoot, {
194
+ ts,
195
+ action: 'merged',
196
+ tier,
197
+ id: writeResult.id,
198
+ reasonCode: REASON_CODES.CURATED_MERGE,
199
+ paths: {
200
+ after: writeResult.path,
201
+ archive: [supersededA, supersededB],
202
+ },
203
+ extra: { mergedFrom: [idA, idB] },
204
+ });
205
+
206
+ return {
207
+ action: 'merged',
208
+ id: writeResult.id,
209
+ tier,
210
+ path: writeResult.path,
211
+ supersededPaths: [supersededA, supersededB],
212
+ };
213
+ }