@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,217 +1,217 @@
1
- // Provenance frontmatter writer + reader (Task 13, T-011).
2
- // Pure-functional formatting/parsing — no I/O. Two cooperating boundaries
3
- // share the same on-disk canonical shape so write → read → write is
4
- // byte-identical.
5
- //
6
- // Public surface:
7
- // writeBullet({id, text, provenance}) → result
8
- // - formats a 2-line bullet (bullet text on line 1, HTML-comment
9
- // provenance on line 2) with all 7 required fields
10
- // readBullet({bulletLine, commentLine}) → {id, text, provenance} | null
11
- // - parses the pair; returns null on any non-match (graceful skip
12
- // so callers iterating freeform markdown don't crash)
13
- // parseBulletProvenance(commentLine) → provenance | null
14
- // - just the comment parser; used by scratchpad.mjs's consolidator
15
- // and (post-extraction) anywhere else that needs to read
16
- // provenance from a freestanding comment line
17
- //
18
- // The 7 required fields per Task 13.2 / design §4:
19
- // - id (in bullet line as `(P-XXX)`, not duplicated in comment)
20
- // - text (the bullet body)
21
- // - source (file path, no inline line number)
22
- // - source_line (positive integer; separate from `source`)
23
- // - sha1
24
- // - write (enum)
25
- // - trust (enum)
26
- // - at (ISO 8601 UTC timestamp)
27
- //
28
- // Spec deviation: design §2.1's example uses `source: file.md:142` inline.
29
- // This module uses `source: file.md, source_line: 142` (separate fields)
30
- // per Task 13.2's explicit "7 required" enumeration. design.md §2.1 is
31
- // updated in this PR to match.
32
- //
33
- // Uses shared modules per CLAUDE.md "Shared modules" rule:
34
- // tier-paths.mjs — ID_PATTERN (validates the id format in the bullet line)
35
- // result-shapes.mjs — ERROR_CATEGORIES, errorResult
36
-
37
- import { ID_PATTERN } from './tier-paths.mjs';
38
- import { ERROR_CATEGORIES, errorResult } from './result-shapes.mjs';
39
-
40
- const VALID_TRUST = new Set(['high', 'medium', 'low']);
41
- const VALID_WRITE_SOURCES = new Set([
42
- 'user-explicit',
43
- 'auto-extract',
44
- 'compressor',
45
- 'manual-edit',
46
- 'imported',
47
- ]);
48
- const REQUIRED_PROVENANCE_FIELDS = [
49
- 'source',
50
- 'source_line',
51
- 'sha1',
52
- 'write',
53
- 'trust',
54
- 'at',
55
- ];
56
-
57
- // PR-1 finding B2 was about newlines/colons in YAML-frontmatter scalar values.
58
- // Layer-3 review finding B3 is the same shape with `,` as the separator: the
59
- // HTML-comment provenance is `key: value, key: value, ...`, so a value
60
- // containing `,` would silently inject a fake field. A `source` of
61
- // `"Innocent, sha1: fake"` would round-trip as if it had an `sha1: fake`
62
- // field of its own. Defensive boundary check: reject these chars in scalar
63
- // provenance fields + the bullet text.
64
- //
65
- // `write` and `trust` are enums (already rejected if not in the allow-list);
66
- // `source_line` is a number (no string-injection possible).
67
- const UNSAFE_FOR_COMMENT = /[,\n\r]/;
68
- const UNSAFE_FOR_BULLET_TEXT = /[\n\r]/; // commas are fine in bullet text (line 1, not the comment)
69
- const FIELDS_TO_SANITIZE = ['source', 'sha1', 'at'];
70
-
71
- // Match the bullet line: `- (<id>) <text>`. The id pattern is the kit's
72
- // custom base32 alphabet from tier-paths.mjs; non-conforming ids are
73
- // treated as "not a kit bullet" by readBullet.
74
- const BULLET_RE = new RegExp(
75
- `^- \\((${ID_PATTERN.source.replace(/^\^/, '').replace(/\$$/, '')})\\)\\s+(.+)$`,
76
- );
77
-
78
- // Match a provenance comment, tolerant of leading indentation.
79
- const COMMENT_RE = /^\s*<!--.*-->\s*$/;
80
-
81
- function validateBulletInput({ id, text, provenance }) {
82
- const errors = [];
83
-
84
- if (!id || typeof id !== 'string') {
85
- errors.push('id: required, non-empty string');
86
- } else if (!ID_PATTERN.test(id)) {
87
- errors.push(
88
- `id: must match the kit's citation-ID format (got ${JSON.stringify(id)})`,
89
- );
90
- }
91
-
92
- if (!text || typeof text !== 'string' || !text.trim()) {
93
- errors.push('text: required, non-empty string');
94
- } else if (UNSAFE_FOR_BULLET_TEXT.test(text)) {
95
- errors.push(
96
- 'text: must not contain newlines (would break the 2-line bullet+comment shape; see review finding B3)',
97
- );
98
- }
99
-
100
- if (!provenance || typeof provenance !== 'object') {
101
- errors.push(
102
- 'provenance: required object with source/source_line/sha1/write/trust/at',
103
- );
104
- return errors;
105
- }
106
-
107
- for (const f of REQUIRED_PROVENANCE_FIELDS) {
108
- const v = provenance[f];
109
- if (v === undefined || v === null || v === '') {
110
- errors.push(`provenance.${f}: required, non-empty`);
111
- }
112
- }
113
-
114
- if (
115
- provenance.source_line !== undefined &&
116
- provenance.source_line !== null &&
117
- provenance.source_line !== ''
118
- ) {
119
- if (
120
- typeof provenance.source_line !== 'number' ||
121
- !Number.isInteger(provenance.source_line) ||
122
- provenance.source_line < 1
123
- ) {
124
- errors.push(
125
- 'provenance.source_line: must be a positive integer (number type)',
126
- );
127
- }
128
- }
129
-
130
- if (provenance.trust && !VALID_TRUST.has(provenance.trust)) {
131
- errors.push(
132
- `provenance.trust: must be one of high/medium/low (got ${JSON.stringify(provenance.trust)})`,
133
- );
134
- }
135
-
136
- if (provenance.write && !VALID_WRITE_SOURCES.has(provenance.write)) {
137
- errors.push(
138
- `provenance.write: must be one of user-explicit/auto-extract/compressor/manual-edit/imported (got ${JSON.stringify(provenance.write)})`,
139
- );
140
- }
141
-
142
- // B3 defense: scalar string fields that land in the comment must not contain
143
- // `,` / `\n` / `\r`. A `,` would silently spawn a fake field on read; a
144
- // newline would break the single-line comment shape.
145
- for (const f of FIELDS_TO_SANITIZE) {
146
- const v = provenance[f];
147
- if (typeof v === 'string' && UNSAFE_FOR_COMMENT.test(v)) {
148
- errors.push(
149
- `provenance.${f}: must not contain commas, newlines, or carriage returns ` +
150
- '(comment-format injection risk; see review finding B3)',
151
- );
152
- }
153
- }
154
-
155
- return errors;
156
- }
157
-
158
- export function writeBullet(opts = {}) {
159
- const errors = validateBulletInput(opts);
160
- if (errors.length > 0) {
161
- return errorResult({
162
- category: ERROR_CATEGORIES.SCHEMA,
163
- errors,
164
- });
165
- }
166
-
167
- const { id, text, provenance: p } = opts;
168
- const bullet = `- (${id}) ${text}`;
169
- // Canonical field order (matches Task 13.2 enumeration):
170
- // source, source_line, sha1, write, trust, at
171
- const comment =
172
- ` <!-- source: ${p.source}, source_line: ${p.source_line},` +
173
- ` sha1: ${p.sha1}, write: ${p.write}, trust: ${p.trust},` +
174
- ` at: ${p.at} -->`;
175
- return {
176
- action: 'formatted',
177
- id,
178
- text,
179
- bullet,
180
- comment,
181
- lines: `${bullet}\n${comment}`,
182
- };
183
- }
184
-
185
- export function parseBulletProvenance(line) {
186
- if (typeof line !== 'string') return null;
187
- if (!COMMENT_RE.test(line)) return null;
188
-
189
- const inner = line.replace(/^\s*<!--/, '').replace(/-->\s*$/, '');
190
- const fields = {};
191
- for (const part of inner.split(',')) {
192
- const idx = part.indexOf(':');
193
- if (idx === -1) continue;
194
- const k = part.slice(0, idx).trim();
195
- const v = part.slice(idx + 1).trim();
196
- if (!k) continue;
197
- fields[k] = v;
198
- }
199
- if (Object.keys(fields).length === 0) return null;
200
-
201
- // Coerce numeric fields back to numbers for symmetric round-trip.
202
- if (fields.source_line && /^\d+$/.test(fields.source_line)) {
203
- fields.source_line = parseInt(fields.source_line, 10);
204
- }
205
- return fields;
206
- }
207
-
208
- export function readBullet(opts = {}) {
209
- const { bulletLine, commentLine } = opts;
210
- if (typeof bulletLine !== 'string') return null;
211
- const m = bulletLine.match(BULLET_RE);
212
- if (!m) return null;
213
- const [, id, text] = m;
214
- const provenance = parseBulletProvenance(commentLine);
215
- if (!provenance) return null;
216
- return { id, text, provenance };
217
- }
1
+ // Provenance frontmatter writer + reader (Task 13, T-011).
2
+ // Pure-functional formatting/parsing — no I/O. Two cooperating boundaries
3
+ // share the same on-disk canonical shape so write → read → write is
4
+ // byte-identical.
5
+ //
6
+ // Public surface:
7
+ // writeBullet({id, text, provenance}) → result
8
+ // - formats a 2-line bullet (bullet text on line 1, HTML-comment
9
+ // provenance on line 2) with all 7 required fields
10
+ // readBullet({bulletLine, commentLine}) → {id, text, provenance} | null
11
+ // - parses the pair; returns null on any non-match (graceful skip
12
+ // so callers iterating freeform markdown don't crash)
13
+ // parseBulletProvenance(commentLine) → provenance | null
14
+ // - just the comment parser; used by scratchpad.mjs's consolidator
15
+ // and (post-extraction) anywhere else that needs to read
16
+ // provenance from a freestanding comment line
17
+ //
18
+ // The 7 required fields per Task 13.2 / design §4:
19
+ // - id (in bullet line as `(P-XXX)`, not duplicated in comment)
20
+ // - text (the bullet body)
21
+ // - source (file path, no inline line number)
22
+ // - source_line (positive integer; separate from `source`)
23
+ // - sha1
24
+ // - write (enum)
25
+ // - trust (enum)
26
+ // - at (ISO 8601 UTC timestamp)
27
+ //
28
+ // Spec deviation: design §2.1's example uses `source: file.md:142` inline.
29
+ // This module uses `source: file.md, source_line: 142` (separate fields)
30
+ // per Task 13.2's explicit "7 required" enumeration. design.md §2.1 is
31
+ // updated in this PR to match.
32
+ //
33
+ // Uses shared modules per CLAUDE.md "Shared modules" rule:
34
+ // tier-paths.mjs — ID_PATTERN (validates the id format in the bullet line)
35
+ // result-shapes.mjs — ERROR_CATEGORIES, errorResult
36
+
37
+ import { ID_PATTERN } from './tier-paths.mjs';
38
+ import { ERROR_CATEGORIES, errorResult } from './result-shapes.mjs';
39
+
40
+ const VALID_TRUST = new Set(['high', 'medium', 'low']);
41
+ const VALID_WRITE_SOURCES = new Set([
42
+ 'user-explicit',
43
+ 'auto-extract',
44
+ 'compressor',
45
+ 'manual-edit',
46
+ 'imported',
47
+ ]);
48
+ const REQUIRED_PROVENANCE_FIELDS = [
49
+ 'source',
50
+ 'source_line',
51
+ 'sha1',
52
+ 'write',
53
+ 'trust',
54
+ 'at',
55
+ ];
56
+
57
+ // PR-1 finding B2 was about newlines/colons in YAML-frontmatter scalar values.
58
+ // Layer-3 review finding B3 is the same shape with `,` as the separator: the
59
+ // HTML-comment provenance is `key: value, key: value, ...`, so a value
60
+ // containing `,` would silently inject a fake field. A `source` of
61
+ // `"Innocent, sha1: fake"` would round-trip as if it had an `sha1: fake`
62
+ // field of its own. Defensive boundary check: reject these chars in scalar
63
+ // provenance fields + the bullet text.
64
+ //
65
+ // `write` and `trust` are enums (already rejected if not in the allow-list);
66
+ // `source_line` is a number (no string-injection possible).
67
+ const UNSAFE_FOR_COMMENT = /[,\n\r]/;
68
+ const UNSAFE_FOR_BULLET_TEXT = /[\n\r]/; // commas are fine in bullet text (line 1, not the comment)
69
+ const FIELDS_TO_SANITIZE = ['source', 'sha1', 'at'];
70
+
71
+ // Match the bullet line: `- (<id>) <text>`. The id pattern is the kit's
72
+ // custom base32 alphabet from tier-paths.mjs; non-conforming ids are
73
+ // treated as "not a kit bullet" by readBullet.
74
+ const BULLET_RE = new RegExp(
75
+ `^- \\((${ID_PATTERN.source.replace(/^\^/, '').replace(/\$$/, '')})\\)\\s+(.+)$`,
76
+ );
77
+
78
+ // Match a provenance comment, tolerant of leading indentation.
79
+ const COMMENT_RE = /^\s*<!--.*-->\s*$/;
80
+
81
+ function validateBulletInput({ id, text, provenance }) {
82
+ const errors = [];
83
+
84
+ if (!id || typeof id !== 'string') {
85
+ errors.push('id: required, non-empty string');
86
+ } else if (!ID_PATTERN.test(id)) {
87
+ errors.push(
88
+ `id: must match the kit's citation-ID format (got ${JSON.stringify(id)})`,
89
+ );
90
+ }
91
+
92
+ if (!text || typeof text !== 'string' || !text.trim()) {
93
+ errors.push('text: required, non-empty string');
94
+ } else if (UNSAFE_FOR_BULLET_TEXT.test(text)) {
95
+ errors.push(
96
+ 'text: must not contain newlines (would break the 2-line bullet+comment shape; see review finding B3)',
97
+ );
98
+ }
99
+
100
+ if (!provenance || typeof provenance !== 'object') {
101
+ errors.push(
102
+ 'provenance: required object with source/source_line/sha1/write/trust/at',
103
+ );
104
+ return errors;
105
+ }
106
+
107
+ for (const f of REQUIRED_PROVENANCE_FIELDS) {
108
+ const v = provenance[f];
109
+ if (v === undefined || v === null || v === '') {
110
+ errors.push(`provenance.${f}: required, non-empty`);
111
+ }
112
+ }
113
+
114
+ if (
115
+ provenance.source_line !== undefined &&
116
+ provenance.source_line !== null &&
117
+ provenance.source_line !== ''
118
+ ) {
119
+ if (
120
+ typeof provenance.source_line !== 'number' ||
121
+ !Number.isInteger(provenance.source_line) ||
122
+ provenance.source_line < 1
123
+ ) {
124
+ errors.push(
125
+ 'provenance.source_line: must be a positive integer (number type)',
126
+ );
127
+ }
128
+ }
129
+
130
+ if (provenance.trust && !VALID_TRUST.has(provenance.trust)) {
131
+ errors.push(
132
+ `provenance.trust: must be one of high/medium/low (got ${JSON.stringify(provenance.trust)})`,
133
+ );
134
+ }
135
+
136
+ if (provenance.write && !VALID_WRITE_SOURCES.has(provenance.write)) {
137
+ errors.push(
138
+ `provenance.write: must be one of user-explicit/auto-extract/compressor/manual-edit/imported (got ${JSON.stringify(provenance.write)})`,
139
+ );
140
+ }
141
+
142
+ // B3 defense: scalar string fields that land in the comment must not contain
143
+ // `,` / `\n` / `\r`. A `,` would silently spawn a fake field on read; a
144
+ // newline would break the single-line comment shape.
145
+ for (const f of FIELDS_TO_SANITIZE) {
146
+ const v = provenance[f];
147
+ if (typeof v === 'string' && UNSAFE_FOR_COMMENT.test(v)) {
148
+ errors.push(
149
+ `provenance.${f}: must not contain commas, newlines, or carriage returns ` +
150
+ '(comment-format injection risk; see review finding B3)',
151
+ );
152
+ }
153
+ }
154
+
155
+ return errors;
156
+ }
157
+
158
+ export function writeBullet(opts = {}) {
159
+ const errors = validateBulletInput(opts);
160
+ if (errors.length > 0) {
161
+ return errorResult({
162
+ category: ERROR_CATEGORIES.SCHEMA,
163
+ errors,
164
+ });
165
+ }
166
+
167
+ const { id, text, provenance: p } = opts;
168
+ const bullet = `- (${id}) ${text}`;
169
+ // Canonical field order (matches Task 13.2 enumeration):
170
+ // source, source_line, sha1, write, trust, at
171
+ const comment =
172
+ ` <!-- source: ${p.source}, source_line: ${p.source_line},` +
173
+ ` sha1: ${p.sha1}, write: ${p.write}, trust: ${p.trust},` +
174
+ ` at: ${p.at} -->`;
175
+ return {
176
+ action: 'formatted',
177
+ id,
178
+ text,
179
+ bullet,
180
+ comment,
181
+ lines: `${bullet}\n${comment}`,
182
+ };
183
+ }
184
+
185
+ export function parseBulletProvenance(line) {
186
+ if (typeof line !== 'string') return null;
187
+ if (!COMMENT_RE.test(line)) return null;
188
+
189
+ const inner = line.replace(/^\s*<!--/, '').replace(/-->\s*$/, '');
190
+ const fields = {};
191
+ for (const part of inner.split(',')) {
192
+ const idx = part.indexOf(':');
193
+ if (idx === -1) continue;
194
+ const k = part.slice(0, idx).trim();
195
+ const v = part.slice(idx + 1).trim();
196
+ if (!k) continue;
197
+ fields[k] = v;
198
+ }
199
+ if (Object.keys(fields).length === 0) return null;
200
+
201
+ // Coerce numeric fields back to numbers for symmetric round-trip.
202
+ if (fields.source_line && /^\d+$/.test(fields.source_line)) {
203
+ fields.source_line = parseInt(fields.source_line, 10);
204
+ }
205
+ return fields;
206
+ }
207
+
208
+ export function readBullet(opts = {}) {
209
+ const { bulletLine, commentLine } = opts;
210
+ if (typeof bulletLine !== 'string') return null;
211
+ const m = bulletLine.match(BULLET_RE);
212
+ if (!m) return null;
213
+ const [, id, text] = m;
214
+ const provenance = parseBulletProvenance(commentLine);
215
+ if (!provenance) return null;
216
+ return { id, text, provenance };
217
+ }