@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,59 @@
1
+ #!/usr/bin/env node
2
+ // Lazy compress bin wrapper (Task 35, T-030). Invoked detached from
3
+ // inject-context.mjs (SessionStart hook) when staleness is detected
4
+ // and cron is NOT active. Humans normally don't invoke this directly;
5
+ // the documented user-facing entry point is `cmk compress --lazy`.
6
+ //
7
+ // Protocol: one-shot process; exits when done. No stdin payload.
8
+ // Project root from CMK_PROJECT_DIR env or cwd.
9
+ //
10
+ // Composes on runLazyCompress() per design §8.2.2. Always exits 0 —
11
+ // a crashed lazy-compress should never propagate back to the
12
+ // SessionStart hook that spawned it (detached posture); the kit's
13
+ // lazy-compress.log NDJSON + cooldown marker are the load-bearing
14
+ // observability surfaces.
15
+
16
+ import { dirname, join } from 'node:path';
17
+ import { fileURLToPath, pathToFileURL } from 'node:url';
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+
22
+ const lazyCompressModulePath = join(__dirname, '..', 'src', 'lazy-compress.mjs');
23
+ const compressorModulePath = join(__dirname, '..', 'src', 'compressor.mjs');
24
+
25
+ let runLazyCompress;
26
+ let HaikuViaAnthropicApi;
27
+ try {
28
+ ({ runLazyCompress } = await import(pathToFileURL(lazyCompressModulePath).href));
29
+ ({ HaikuViaAnthropicApi } = await import(pathToFileURL(compressorModulePath).href));
30
+ } catch (err) {
31
+ process.stderr.write(
32
+ `cmk-compress-lazy: failed to load modules: ${err?.message ?? err}\n`,
33
+ );
34
+ process.exit(0);
35
+ }
36
+
37
+ // Task 36 B1 fix: accept projectRoot via argv[2]. Inject-context.mjs's
38
+ // detached spawn passes CMK_PROJECT_DIR via env; accepting argv[2]
39
+ // keeps the bin uniform with cmk-daily-distill / cmk-weekly-curate
40
+ // and makes manual debugging (`node cmk-compress-lazy.mjs /path`) work.
41
+ const argvRoot = process.argv[2] && process.argv[2].length > 0 ? process.argv[2] : null;
42
+ const envRoot = process.env.CMK_PROJECT_DIR && process.env.CMK_PROJECT_DIR.length > 0
43
+ ? process.env.CMK_PROJECT_DIR
44
+ : null;
45
+ const projectRoot = argvRoot ?? envRoot ?? process.cwd();
46
+
47
+ try {
48
+ const backend = new HaikuViaAnthropicApi();
49
+ const r = await runLazyCompress({ projectRoot, backend });
50
+ process.stderr.write(
51
+ `cmk-compress-lazy: ${r.action}${r.reason ? ` (${r.reason})` : ''}${r.delegatedTo ? ` → ${r.delegatedTo}` : ''} ms: ${r.duration_ms ?? 0}\n`,
52
+ );
53
+ } catch (err) {
54
+ process.stderr.write(
55
+ `cmk-compress-lazy: unexpected error: ${err?.message ?? err}\n`,
56
+ );
57
+ }
58
+
59
+ process.exit(0);
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ // Daily distill bin wrapper (Task 33, T-028). Invoked by the
3
+ // host scheduler (cron / launchd / Task Scheduler) registered via
4
+ // `cmk register-crons` per design §8.6.2.
5
+ //
6
+ // Protocol: the wrapper is a one-shot process that exits when done.
7
+ // No stdin payload expected (unlike Stop/SessionEnd hooks). The kit's
8
+ // project root is read from CMK_PROJECT_DIR env (set by the cron
9
+ // command line) or falls back to cwd.
10
+ //
11
+ // Composes on dailyDistill() per design §8.6.1. Always exits 0 — a
12
+ // crashed cron job rotates noise into the scheduler logs without any
13
+ // recovery affordance for the user; the kit's distill.log NDJSON
14
+ // + cooldown marker are the load-bearing observability surfaces.
15
+
16
+ import { dirname, join } from 'node:path';
17
+ import { fileURLToPath, pathToFileURL } from 'node:url';
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+
22
+ // This bin lives under packages/cli/bin/ (Task 33 B1 fix — was originally
23
+ // under plugin/bin/ but that tree isn't in the published @lh8ppl/claude-memory-kit
24
+ // npm package, so `cmk register-crons` emitted cron commands pointing at
25
+ // paths that don't exist in `npm install -g` installs). Paths resolved
26
+ // relative to bin/ → ../src/ for both modules.
27
+ const dailyDistillModulePath = join(__dirname, '..', 'src', 'daily-distill.mjs');
28
+ const compressorModulePath = join(__dirname, '..', 'src', 'compressor.mjs');
29
+
30
+ let dailyDistill;
31
+ let HaikuViaAnthropicApi;
32
+ try {
33
+ ({ dailyDistill } = await import(pathToFileURL(dailyDistillModulePath).href));
34
+ ({ HaikuViaAnthropicApi } = await import(pathToFileURL(compressorModulePath).href));
35
+ } catch (err) {
36
+ process.stderr.write(
37
+ `cmk-daily-distill: failed to load modules: ${err?.message ?? err}\n`,
38
+ );
39
+ process.exit(0);
40
+ }
41
+
42
+ // Task 36 B1 fix: accept projectRoot via argv[2] (the registered cron
43
+ // emits an absolute path here). Env var + cwd remain as fallbacks for
44
+ // users invoking the bin manually. Host schedulers (cron / launchd /
45
+ // schtasks) all have a non-kit default cwd, so argv-or-env is the
46
+ // load-bearing source for cron-emitted invocations.
47
+ // Treat empty-string env var as "missing" (?? falls through only on
48
+ // null/undefined; empty strings would otherwise become invalid paths).
49
+ const argvRoot = process.argv[2] && process.argv[2].length > 0 ? process.argv[2] : null;
50
+ const envRoot = process.env.CMK_PROJECT_DIR && process.env.CMK_PROJECT_DIR.length > 0
51
+ ? process.env.CMK_PROJECT_DIR
52
+ : null;
53
+ const projectRoot = argvRoot ?? envRoot ?? process.cwd();
54
+
55
+ try {
56
+ const backend = new HaikuViaAnthropicApi();
57
+ const r = await dailyDistill({ projectRoot, backend });
58
+ process.stderr.write(
59
+ `cmk-daily-distill: ${r.action}${r.reason ? ` (${r.reason})` : ''}${r.bytesIn ? ` (in: ${r.bytesIn}b, out: ${r.bytesOut}b, days: ${r.sourceDays})` : ''} ms: ${r.duration_ms ?? 0}\n`,
60
+ );
61
+ } catch (err) {
62
+ process.stderr.write(
63
+ `cmk-daily-distill: unexpected error: ${err?.message ?? err}\n`,
64
+ );
65
+ }
66
+
67
+ process.exit(0);
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ // Weekly curate bin wrapper (Task 34, T-029). Invoked by the host
3
+ // scheduler (cron / launchd / Task Scheduler) registered via
4
+ // `cmk register-crons --weekly` per design §8.7.
5
+ //
6
+ // Protocol: one-shot process; exits when done. No stdin payload
7
+ // expected. Project root from CMK_PROJECT_DIR env or cwd.
8
+ //
9
+ // Composes on weeklyCurate() per design §8.7.1. Always exits 0 — a
10
+ // crashed cron job rotates noise into the scheduler logs without any
11
+ // recovery affordance for the user; the kit's curate.log NDJSON +
12
+ // cooldown marker are the load-bearing observability surfaces.
13
+
14
+ import { dirname, join } from 'node:path';
15
+ import { fileURLToPath, pathToFileURL } from 'node:url';
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+
20
+ const weeklyCurateModulePath = join(__dirname, '..', 'src', 'weekly-curate.mjs');
21
+ const compressorModulePath = join(__dirname, '..', 'src', 'compressor.mjs');
22
+
23
+ let weeklyCurate;
24
+ let HaikuViaAnthropicApi;
25
+ try {
26
+ ({ weeklyCurate } = await import(pathToFileURL(weeklyCurateModulePath).href));
27
+ ({ HaikuViaAnthropicApi } = await import(pathToFileURL(compressorModulePath).href));
28
+ } catch (err) {
29
+ process.stderr.write(
30
+ `cmk-weekly-curate: failed to load modules: ${err?.message ?? err}\n`,
31
+ );
32
+ process.exit(0);
33
+ }
34
+
35
+ // Task 36 B1 fix: accept projectRoot via argv[2] (the registered cron
36
+ // emits an absolute path here). Env var + cwd remain as fallbacks.
37
+ // See cmk-daily-distill.mjs for rationale.
38
+ const argvRoot = process.argv[2] && process.argv[2].length > 0 ? process.argv[2] : null;
39
+ const envRoot = process.env.CMK_PROJECT_DIR && process.env.CMK_PROJECT_DIR.length > 0
40
+ ? process.env.CMK_PROJECT_DIR
41
+ : null;
42
+ const projectRoot = argvRoot ?? envRoot ?? process.cwd();
43
+
44
+ try {
45
+ const backend = new HaikuViaAnthropicApi();
46
+ const r = await weeklyCurate({ projectRoot, backend });
47
+ process.stderr.write(
48
+ `cmk-weekly-curate: ${r.action}${r.reason ? ` (${r.reason})` : ''}${r.archivedDays ? ` (archived: ${r.archivedDays}d, current: ${r.currentDays}d, in: ${r.bytesIn}b, out: ${r.bytesOut}b)` : ''} ms: ${r.duration_ms ?? 0}\n`,
49
+ );
50
+ } catch (err) {
51
+ process.stderr.write(
52
+ `cmk-weekly-curate: unexpected error: ${err?.message ?? err}\n`,
53
+ );
54
+ }
55
+
56
+ process.exit(0);
package/bin/cmk.mjs ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ // cmk — claude-memory-kit CLI entry point.
3
+ // Thin shim: defers all argv parsing + dispatch to src/index.mjs.
4
+ // Kept thin so the bin file rarely needs to change once installed.
5
+
6
+ import { run } from '../src/index.mjs';
7
+
8
+ run(process.argv).catch((err) => {
9
+ console.error('cmk: unexpected error');
10
+ console.error(err && err.stack ? err.stack : err);
11
+ process.exit(1);
12
+ });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@lh8ppl/claude-memory-kit",
3
+ "version": "0.1.0",
4
+ "description": "cmk — the CLI for claude-memory-kit. Per-project, in-repo memory system for Claude Code.",
5
+ "type": "module",
6
+ "bin": {
7
+ "cmk": "./bin/cmk.mjs",
8
+ "cmk-daily-distill": "./bin/cmk-daily-distill.mjs",
9
+ "cmk-weekly-curate": "./bin/cmk-weekly-curate.mjs",
10
+ "cmk-compress-lazy": "./bin/cmk-compress-lazy.mjs"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "src/",
15
+ "template/",
16
+ "README.md"
17
+ ],
18
+ "exports": {
19
+ ".": "./src/index.mjs"
20
+ },
21
+ "scripts": {
22
+ "test": "vitest run",
23
+ "prepublishOnly": "node ../../scripts/prepublish-copy-template.mjs"
24
+ },
25
+ "dependencies": {
26
+ "@lh8ppl/cmk-canonicalize": "0.1.0",
27
+ "@modelcontextprotocol/sdk": "^1.29.0",
28
+ "better-sqlite3": "^12.10.0",
29
+ "chokidar": "^5.0.0",
30
+ "commander": "^12.1.0",
31
+ "js-yaml": "^4.1.0",
32
+ "zod": "^4.4.3"
33
+ },
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/LH8PPL/claude-memory-kit.git",
41
+ "directory": "packages/cli"
42
+ },
43
+ "keywords": [
44
+ "claude",
45
+ "claude-code",
46
+ "memory",
47
+ "anthropic",
48
+ "cli"
49
+ ]
50
+ }
@@ -0,0 +1,103 @@
1
+ // Canonical audit-log writer for every kit module that mutates state.
2
+ // Per the Layer-2 review's I4 finding, the three writers (writeFact-skipped,
3
+ // forget, merge-facts) had been writing to the same <tierRoot>/.locks/audit.log
4
+ // in three different schemas — `path` vs `originalPath` vs `newPath`; `reason`
5
+ // overloaded as enum vs free-text; `tier` missing from writeFact entries.
6
+ //
7
+ // This module is the single sanctioned audit-log writer. New writers (Task 24
8
+ // memory-write, Task 15 trust override, etc.) MUST import appendAuditEntry
9
+ // from here. See CLAUDE.md "Shared modules" rule.
10
+ //
11
+ // Schema v1 (canonical):
12
+ // {
13
+ // ts: ISO 8601 UTC,
14
+ // schema: 1,
15
+ // action: enum string,
16
+ // tier: 'P' | 'L' | 'U',
17
+ // id: citation ID of the affected fact,
18
+ // reasonCode: enum string (machine-parseable),
19
+ // reasonText?: free-form string (human-readable, optional),
20
+ // paths?: { before?, after?, archive? } where each value is string|string[],
21
+ // extra?: object (action-specific extension fields)
22
+ // }
23
+
24
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+
27
+ export const AUDIT_LOG_SCHEMA_VERSION = 1;
28
+
29
+ // Enum of reasonCode values seen in v0.1. New writers may add codes here as
30
+ // they come online; the rule is one canonical machine-parseable token per
31
+ // kind of audit event (not a free-text reason field).
32
+ export const REASON_CODES = Object.freeze({
33
+ DUPLICATE: 'duplicate', // writeFact: same path + same id
34
+ DUPLICATE_ELSEWHERE: 'duplicate-elsewhere', // writeFact: different path + same id
35
+ USER_REQUESTED: 'user-requested', // forget: user-initiated tombstone
36
+ CURATED_MERGE: 'curated-merge', // mergeFacts: explicit merge of A + B → C
37
+ SCRATCHPAD_APPEND: 'scratchpad-append', // scratchpad: appendScratchpadBullet (Task 12)
38
+ TRUST_CHANGE: 'trust-change', // trust: overrideTrust (Task 15)
39
+ CONFLICT_QUEUED: 'conflict-queued', // conflict-queue: new write contradicts existing higher-trust fact, routed to queues/conflicts.md (Task 25, design §6.8)
40
+ CONFLICT_RESOLVED: 'conflict-resolved', // conflict-queue: user resolved a pending conflict via cmk queue conflicts (keep-old / keep-new / merge-both)
41
+ REVIEW_PROMOTED: 'review-promoted', // review-queue: user promoted a medium-trust auto-extract to MEMORY.md (Task 26, design §6.2)
42
+ REVIEW_DISCARDED: 'review-discarded', // review-queue: user discarded a medium-trust auto-extract via cmk queue review
43
+ IMPORT_APPLIED: 'import-applied', // import-anthropic-memory: bullet applied to project MEMORY.md with write_source:imported (Task 38)
44
+ IMPORT_SKIPPED_DUPLICATE: 'import-skipped-duplicate', // import-anthropic-memory: candidate canonicalize-matched existing fact, skipped (Task 38)
45
+ REPAIR_HOOKS_APPLIED: 'repair-hooks-applied', // cmk repair --hooks: settings.json updated with canonical kit hooks (Task 39)
46
+ REPAIR_HOOKS_NOOP: 'repair-hooks-noop', // cmk repair --hooks: settings.json already canonical, no-op (Task 39)
47
+ REPAIR_LOCK_REMOVED: 'repair-lock-removed', // cmk repair --locks: stale lock unlinked (Task 39)
48
+ });
49
+
50
+ export function nowIso() {
51
+ return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
52
+ }
53
+
54
+ function auditLogPath(tierRoot) {
55
+ return join(tierRoot, '.locks', 'audit.log');
56
+ }
57
+
58
+ export function appendAuditEntry(tierRoot, entry) {
59
+ // Required-field validation. Throw immediately on caller error; this catches
60
+ // schema drift at write time rather than at log-parse time later.
61
+ for (const field of ['action', 'tier', 'id', 'reasonCode']) {
62
+ if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
63
+ throw new Error(
64
+ `audit-log: missing required field "${field}" in entry ${JSON.stringify(entry)}`,
65
+ );
66
+ }
67
+ }
68
+
69
+ const canonical = {
70
+ ts: entry.ts ?? nowIso(),
71
+ schema: AUDIT_LOG_SCHEMA_VERSION,
72
+ action: entry.action,
73
+ tier: entry.tier,
74
+ id: entry.id,
75
+ reasonCode: entry.reasonCode,
76
+ };
77
+ if (entry.reasonText !== undefined) canonical.reasonText = entry.reasonText;
78
+ if (entry.paths !== undefined) canonical.paths = entry.paths;
79
+ // Field name is `extra` (singular). Caller convention across the kit
80
+ // (memory-write, conflict-queue, etc.); plural `extras` is a common
81
+ // typo — caught in tests but worth naming here so future modules
82
+ // pick the right key.
83
+ if (entry.extra !== undefined) canonical.extra = entry.extra;
84
+
85
+ const locksDir = join(tierRoot, '.locks');
86
+ mkdirSync(locksDir, { recursive: true });
87
+ appendFileSync(
88
+ auditLogPath(tierRoot),
89
+ JSON.stringify(canonical) + '\n',
90
+ 'utf8',
91
+ );
92
+ }
93
+
94
+ // Convenience reader, useful for tests + future cmk doctor + cmk queue
95
+ // commands. Returns [] if the log doesn't exist yet.
96
+ export function readAuditLog(tierRoot) {
97
+ const path = auditLogPath(tierRoot);
98
+ if (!existsSync(path)) return [];
99
+ return readFileSync(path, 'utf8')
100
+ .split('\n')
101
+ .filter((l) => l.trim())
102
+ .map((l) => JSON.parse(l));
103
+ }