@lh8ppl/claude-memory-kit 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -6,10 +6,10 @@
6
6
 
7
7
  - **Cross-project persona — the wedge (v0.2)** — when you state how you work *everywhere* ("always use uv, never pip", "from now on run the linter before committing"), the per-turn auto-extract promotes it into your **user tier** (`~/.claude-memory-kit/`) **that turn**. So a brand-new project **cold-opens already knowing your style** — layered structure, your tooling, your testing discipline — with no hand-curation and no waiting. Carry it between your own machines with `cmk persona export`/`import`, or pin a single fact across projects with `cmk lessons promote`.
8
8
  - **Frozen snapshot at session start** — MEMORY.md + USER.md + SOUL.md + INDEX.md + today's session log inject once at the first tool call, so Claude sees your context every session without you re-telling it.
9
- - **Auto-extract on every assistant turn** — a background `claude --print` subagent reads each turn and saves durable facts (decisions, preferences, environment) to memory. No manual writes needed.
10
- - **Explicit capture when you want it** — say "remember this" / "from now on" / "we decided" / "forget X" (the `memory-write` skill), or run `cmk remember "<fact>"`. Both dedup, screen for secrets, abstract machine paths to `~`, and write silently.
11
- - **Search + MCP** — `cmk search "<term>"` (keyword/hybrid over facts + scratchpads); `cmk mcp` exposes the same to Claude Code as tools.
12
- - **Bounded by compression** — session → daily → weekly Haiku rollups (cron or lazy-on-read) keep the snapshot small as history grows.
9
+ - **Auto-extract on every assistant turn** — a background `claude --print` subagent reads each turn and saves durable facts to memory. Durable project knowledge (setup/config, conventions, workflows, tool quirks) becomes a **rich Why/How fact file** (structured + searchable); lighter signals stay terse `MEMORY.md` bullets. Runs automatically, so the rich tier survives even when the model uses Claude Code's built-in memory instead. No manual writes needed.
10
+ - **Explicit capture when you want it** — say "remember this" / "from now on" / "we decided" / "forget X" (the `memory-write` skill), or run `cmk remember "<fact>"`. Both dedup, screen for secrets, abstract machine paths to `~`, and write silently. For backtick/quote-heavy rich facts, capture them shell-safe as JSON: `cmk remember --from-file fact.json` (or `--json` from stdin) — content never touches the shell.
11
+ - **Search + MCP — Claude runs every memory op for you, in conversation** — `cmk search "<term>"` (keyword/hybrid over facts + scratchpads). `cmk install` registers the kit's **MCP server**, so Claude can do the whole memory surface as tools without you ever typing `cmk`: capture (`mk_remember`, rich Why/How too), recall (`mk_search` / `mk_get` / `mk_timeline` / `mk_cite`), adjust trust (`mk_trust`), promote a fact across projects (`mk_lessons_promote`), forget (`mk_forget` — previews first, then deletes on confirm), and clear the review/conflict queues (`mk_queue_list` / `mk_queue_resolve`). The tools are allow-listed on install, so they run prompt-free.
12
+ - **Bounded by compression** — session → daily → weekly Haiku rollups (cron or lazy-on-read) keep the snapshot small as history grows. The session-buffer rollup self-heals at session start too, so memory stays bounded even if you never cleanly close the window.
13
13
  - **Per-project, in-repo** — `context/` lives inside your project and travels with `git clone`. Each project keeps its own memory.
14
14
  - **9 health checks** — `cmk doctor` validates install, hook wiring, distill freshness, INDEX consistency, cron registration, and stale locks.
15
15
 
@@ -26,7 +26,7 @@ cmk install # scaffolds context/ + the memory-write skill AND wires the l
26
26
  cmk doctor # verify, then restart Claude Code
27
27
  ```
28
28
 
29
- `cmk install` is a complete entry point: it scaffolds `context/`, drops the `memory-write` skill into `.claude/skills/` (committed — travels with `git clone`), and writes the 5 lifecycle hooks (PATH-resolved, cross-OS) into the project's `.claude/settings.json`. No separate `/plugin` step needed. Use `cmk install --no-hooks` for a scaffold-only install.
29
+ `cmk install` is a complete entry point: it scaffolds `context/`, drops the `memory-write` skill into `.claude/skills/` (committed — travels with `git clone`), and writes the 5 lifecycle hooks (PATH-resolved, cross-OS) into the project's `.claude/settings.json`. It also **registers the kit's MCP server** in `.mcp.json` and allow-lists its tools (`mcp__cmk__*`) in `.claude/settings.json`, so Claude can drive memory as tools with no per-call prompt. No separate `/plugin` step needed. Use `cmk install --no-hooks` to skip the hooks + MCP wiring (scaffold-only).
30
30
 
31
31
  > Installing the package globally adds the `cmk` CLI **and** the installer. It's the `cmk install` *subcommand* that wires the hooks — not the bare `npm install`.
32
32
 
@@ -51,9 +51,10 @@ Most-used commands (full list via `cmk --help`):
51
51
  | `cmk doctor` | Run HC-1..HC-9 health checks, surface repair commands |
52
52
  | `cmk repair --hooks` / `--locks` / `--index` / `--all` | Idempotent self-repair |
53
53
  | `cmk search "<query>" [--mode keyword\|semantic\|hybrid]` | Search accumulated memory (keyword default) |
54
+ | `cmk get <id…>` / `cmk timeline <id>` / `cmk cite <id>` / `cmk recent-activity` | Read the index back — full fact bodies + provenance, sequential context around an observation, a canonical citation link, recent changes (the CLI side of the `mk_*` MCP read tools) |
54
55
  | `cmk roll --scope now\|today\|recent` | Manually trigger a compression pipeline |
55
56
  | `cmk register-crons [--dry-run] [--unregister]` | Register daily + weekly jobs with cron / launchd / Task Scheduler |
56
- | `cmk forget <id>` | Tombstone a fact (preserves audit trail) |
57
+ | `cmk forget <id>` | Tombstone a fact — disappears from `cmk search` immediately, no manual reindex (audit trail preserved) |
57
58
  | `cmk lessons promote <id> [--to USER.md\|HABITS.md]` | Promote one captured fact to your cross-project **user tier** (the safe path — sanitized, secret-screened, audited) so it applies in **every** project |
58
59
  | `cmk disable-native-memory` / `enable-native-memory` | Opt out of Claude Code's built-in Auto Memory so the kit is your single, lean memory layer (committable — travels with `git clone`) |
59
60
  | `cmk persona generate` | Run cross-project persona synthesis on demand (instead of waiting for the weekly pass) |
@@ -11,7 +11,6 @@
11
11
  // transcript, emit {"continue": true}. Always exit 0 — a hook that errors
12
12
  // would interrupt the user mid-prompt.
13
13
 
14
- import { readFileSync } from 'node:fs';
15
14
  import { dirname, join } from 'node:path';
16
15
  import { fileURLToPath, pathToFileURL } from 'node:url';
17
16
 
@@ -19,35 +18,36 @@ function emitContinue() {
19
18
  process.stdout.write('{"continue": true}');
20
19
  }
21
20
 
22
- let rawInput = '';
23
- try {
24
- rawInput = readFileSync(0, 'utf8');
25
- } catch {
26
- emitContinue();
27
- process.exit(0);
28
- }
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+ const readHookStdinPath = join(__dirname, '..', 'src', 'read-hook-stdin.mjs');
24
+ const modulePath = join(__dirname, '..', 'src', 'capture-prompt.mjs');
29
25
 
30
- let payload;
26
+ let readHookStdin;
27
+ let capturePrompt;
31
28
  try {
32
- payload = rawInput.trim() === '' ? {} : JSON.parse(rawInput);
29
+ ({ readHookStdin } = await import(pathToFileURL(readHookStdinPath).href));
30
+ ({ capturePrompt } = await import(pathToFileURL(modulePath).href));
33
31
  } catch (err) {
34
32
  process.stderr.write(
35
- `cmk-capture-prompt: failed to parse stdin JSON: ${err?.message ?? err}\n`,
33
+ `cmk-capture-prompt: failed to load modules: ${err?.message ?? err}\n`,
36
34
  );
37
35
  emitContinue();
38
36
  process.exit(0);
39
37
  }
40
38
 
41
- const __filename = fileURLToPath(import.meta.url);
42
- const __dirname = dirname(__filename);
43
- const modulePath = join(__dirname, '..', 'src', 'capture-prompt.mjs');
39
+ // Drain the hook payload — but NOT on an interactive TTY (a manual run):
40
+ // a blocking stdin read would hang forever on a console that never sends EOF, before
41
+ // any body runs (Task 101; DECISION-LOG 2026-06-06). readHookStdin returns ''
42
+ // for a TTY so a manual invocation finishes instead of hanging.
43
+ const rawInput = readHookStdin({ isTTY: process.stdin.isTTY });
44
44
 
45
- let capturePrompt;
45
+ let payload;
46
46
  try {
47
- ({ capturePrompt } = await import(pathToFileURL(modulePath).href));
47
+ payload = rawInput.trim() === '' ? {} : JSON.parse(rawInput);
48
48
  } catch (err) {
49
49
  process.stderr.write(
50
- `cmk-capture-prompt: failed to load module: ${err?.message ?? err}\n`,
50
+ `cmk-capture-prompt: failed to parse stdin JSON: ${err?.message ?? err}\n`,
51
51
  );
52
52
  emitContinue();
53
53
  process.exit(0);
@@ -13,7 +13,7 @@
13
13
  // append to transcripts, spawn detached auto-extract, emit
14
14
  // {"continue": true}, exit 0 within ~50ms (NFR-1). Always exit 0.
15
15
 
16
- import { readFileSync, existsSync } from 'node:fs';
16
+ import { existsSync } from 'node:fs';
17
17
  import { dirname, join } from 'node:path';
18
18
  import { fileURLToPath, pathToFileURL } from 'node:url';
19
19
 
@@ -21,27 +21,9 @@ function emitContinue() {
21
21
  process.stdout.write('{"continue": true}');
22
22
  }
23
23
 
24
- let raw = '';
25
- try {
26
- raw = readFileSync(0, 'utf8');
27
- } catch {
28
- emitContinue();
29
- process.exit(0);
30
- }
31
-
32
- let payload;
33
- try {
34
- payload = raw.trim() === '' ? {} : JSON.parse(raw);
35
- } catch (err) {
36
- process.stderr.write(
37
- `cmk-capture-turn: failed to parse stdin JSON: ${err?.message ?? err}\n`,
38
- );
39
- emitContinue();
40
- process.exit(0);
41
- }
42
-
43
24
  const __filename = fileURLToPath(import.meta.url);
44
25
  const __dirname = dirname(__filename);
26
+ const readHookStdinPath = join(__dirname, '..', 'src', 'read-hook-stdin.mjs');
45
27
  const modulePath = join(__dirname, '..', 'src', 'capture-turn.mjs');
46
28
 
47
29
  // Auto-extract path: env override → sibling cmk-auto-extract.mjs (ships
@@ -53,12 +35,31 @@ const autoExtractPath =
53
35
  ? join(__dirname, 'cmk-auto-extract.mjs')
54
36
  : null);
55
37
 
38
+ let readHookStdin;
56
39
  let captureTurn;
57
40
  try {
41
+ ({ readHookStdin } = await import(pathToFileURL(readHookStdinPath).href));
58
42
  ({ captureTurn } = await import(pathToFileURL(modulePath).href));
59
43
  } catch (err) {
60
44
  process.stderr.write(
61
- `cmk-capture-turn: failed to load module: ${err?.message ?? err}\n`,
45
+ `cmk-capture-turn: failed to load modules: ${err?.message ?? err}\n`,
46
+ );
47
+ emitContinue();
48
+ process.exit(0);
49
+ }
50
+
51
+ // Drain the hook payload — but NOT on an interactive TTY (a manual run):
52
+ // a blocking stdin read would hang forever on a console that never sends EOF, before
53
+ // any body runs (Task 101; DECISION-LOG 2026-06-06). readHookStdin returns ''
54
+ // for a TTY so a manual invocation finishes instead of hanging.
55
+ const raw = readHookStdin({ isTTY: process.stdin.isTTY });
56
+
57
+ let payload;
58
+ try {
59
+ payload = raw.trim() === '' ? {} : JSON.parse(raw);
60
+ } catch (err) {
61
+ process.stderr.write(
62
+ `cmk-capture-turn: failed to parse stdin JSON: ${err?.message ?? err}\n`,
62
63
  );
63
64
  emitContinue();
64
65
  process.exit(0);
@@ -43,8 +43,8 @@ try {
43
43
  }
44
44
 
45
45
  // Drain the hook payload so Claude Code's pipe closes cleanly — but NOT when
46
- // stdin is an interactive TTY (a manual run): readFileSync(0) would block
47
- // forever on a console that never sends EOF, hanging before any of the body
46
+ // stdin is an interactive TTY (a manual run): a blocking stdin read would hang
47
+ // forever on a console that never sends EOF, before any of the body
48
48
  // runs (DECISION-LOG 2026-06-06). The payload is discarded; we read state from
49
49
  // disk. readHookStdin returns '' for a TTY so a manual invocation finishes.
50
50
  readHookStdin({ isTTY: process.stdin.isTTY });
@@ -14,19 +14,13 @@
14
14
  // unconditionally — a throwing SessionStart hook would interrupt
15
15
  // session start, worse than an empty additionalContext.
16
16
 
17
- import { readFileSync, existsSync } from 'node:fs';
17
+ import { existsSync } from 'node:fs';
18
18
  import { dirname, join } from 'node:path';
19
19
  import { fileURLToPath, pathToFileURL } from 'node:url';
20
20
 
21
- // Drain stdin so callers blocking on EPIPE don't hang.
22
- try {
23
- readFileSync(0, 'utf8');
24
- } catch {
25
- // stdin not connected; fine.
26
- }
27
-
28
21
  const __filename = fileURLToPath(import.meta.url);
29
22
  const __dirname = dirname(__filename);
23
+ const readHookStdinPath = join(__dirname, '..', 'src', 'read-hook-stdin.mjs');
30
24
  const modulePath = join(__dirname, '..', 'src', 'inject-context.mjs');
31
25
 
32
26
  // Resolve the sibling lazy-compress bin (ships in this same bin/ dir) so
@@ -39,14 +33,14 @@ const compressLazyPath =
39
33
  ? join(__dirname, 'cmk-compress-lazy.mjs')
40
34
  : null);
41
35
 
36
+ let readHookStdin;
42
37
  let injectContext;
43
38
  try {
39
+ ({ readHookStdin } = await import(pathToFileURL(readHookStdinPath).href));
44
40
  ({ injectContext } = await import(pathToFileURL(modulePath).href));
45
41
  } catch (err) {
46
42
  process.stderr.write(
47
- `cmk-inject-context: failed to load module at ${modulePath}: ${
48
- err?.message ?? String(err)
49
- }\n`,
43
+ `cmk-inject-context: failed to load modules: ${err?.message ?? String(err)}\n`,
50
44
  );
51
45
  process.stdout.write(
52
46
  JSON.stringify({
@@ -59,6 +53,12 @@ try {
59
53
  process.exit(0);
60
54
  }
61
55
 
56
+ // Drain stdin so callers blocking on EPIPE don't hang — but NOT on an
57
+ // interactive TTY (a manual run): a blocking stdin read would hang forever on a
58
+ // console that never sends EOF (Task 101; DECISION-LOG 2026-06-06). The payload
59
+ // is discarded; readHookStdin returns '' for a TTY so a manual run finishes.
60
+ readHookStdin({ isTTY: process.stdin.isTTY });
61
+
62
62
  try {
63
63
  const r = injectContext({ cwd: process.cwd(), compressLazyPath });
64
64
  process.stdout.write(JSON.stringify(r.hookOutput));
@@ -11,37 +11,38 @@
11
11
  // must never surface in the user's session. The append is
12
12
  // fire-and-forget by design.
13
13
 
14
- import { readFileSync } from 'node:fs';
15
14
  import { dirname, join } from 'node:path';
16
15
  import { fileURLToPath, pathToFileURL } from 'node:url';
17
16
 
18
- let raw = '';
19
- try {
20
- raw = readFileSync(0, 'utf8');
21
- } catch {
22
- process.exit(0);
23
- }
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+ const readHookStdinPath = join(__dirname, '..', 'src', 'read-hook-stdin.mjs');
20
+ const modulePath = join(__dirname, '..', 'src', 'observe-edit.mjs');
24
21
 
25
- let payload;
22
+ let readHookStdin;
23
+ let observeEdit;
26
24
  try {
27
- payload = raw.trim() === '' ? {} : JSON.parse(raw);
25
+ ({ readHookStdin } = await import(pathToFileURL(readHookStdinPath).href));
26
+ ({ observeEdit } = await import(pathToFileURL(modulePath).href));
28
27
  } catch (err) {
29
28
  process.stderr.write(
30
- `cmk-observe-edit: failed to parse stdin JSON: ${err?.message ?? err}\n`,
29
+ `cmk-observe-edit: failed to load modules: ${err?.message ?? err}\n`,
31
30
  );
32
31
  process.exit(0);
33
32
  }
34
33
 
35
- const __filename = fileURLToPath(import.meta.url);
36
- const __dirname = dirname(__filename);
37
- const modulePath = join(__dirname, '..', 'src', 'observe-edit.mjs');
34
+ // Drain the hook payload — but NOT on an interactive TTY (a manual run):
35
+ // a blocking stdin read would hang forever on a console that never sends EOF, before
36
+ // any body runs (Task 101; DECISION-LOG 2026-06-06). readHookStdin returns ''
37
+ // for a TTY so a manual invocation finishes instead of hanging.
38
+ const raw = readHookStdin({ isTTY: process.stdin.isTTY });
38
39
 
39
- let observeEdit;
40
+ let payload;
40
41
  try {
41
- ({ observeEdit } = await import(pathToFileURL(modulePath).href));
42
+ payload = raw.trim() === '' ? {} : JSON.parse(raw);
42
43
  } catch (err) {
43
44
  process.stderr.write(
44
- `cmk-observe-edit: failed to load module: ${err?.message ?? err}\n`,
45
+ `cmk-observe-edit: failed to parse stdin JSON: ${err?.message ?? err}\n`,
45
46
  );
46
47
  process.exit(0);
47
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lh8ppl/claude-memory-kit",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "cmk — the CLI for claude-memory-kit. Per-project, in-repo memory system for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/audit-log.mjs CHANGED
@@ -30,6 +30,7 @@ export const AUDIT_LOG_SCHEMA_VERSION = 1;
30
30
  // they come online; the rule is one canonical machine-parseable token per
31
31
  // kind of audit event (not a free-text reason field).
32
32
  export const REASON_CODES = Object.freeze({
33
+ FACT_CREATED: 'fact-created', // writeFact: a new fact file was written (Task 123.A — the default create audit; callers emitting a richer code opt out via audit:false)
33
34
  DUPLICATE: 'duplicate', // writeFact: same path + same id
34
35
  DUPLICATE_ELSEWHERE: 'duplicate-elsewhere', // writeFact: different path + same id
35
36
  USER_REQUESTED: 'user-requested', // forget: user-initiated tombstone