@lh8ppl/claude-memory-kit 0.3.0 → 0.3.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.
@@ -27,12 +27,14 @@ import { weeklyCurate } from './weekly-curate.mjs';
27
27
  import { autoPersona } from './auto-persona.mjs';
28
28
  import { exportPersona, importPersona } from './persona-portability.mjs';
29
29
  import { setNativeAutoMemory, nativeMemoryInstallNote } from './native-memory.mjs';
30
- import { rememberRich, richFactTitle, nonProjectTierNote } from './remember-core.mjs';
30
+ import { rememberRich, richFactTitle, nonProjectTierNote, prepareNearDupGuard } from './remember-core.mjs';
31
31
  import { getObservations, citeLink, buildTimeline, recentActivity } from './read-core.mjs';
32
32
  import { readHookStdin } from './read-hook-stdin.mjs';
33
33
  import { runLazyCompress } from './lazy-compress.mjs';
34
34
  import { runDoctor } from './doctor.mjs';
35
35
  import { importAnthropicMemory } from './import-anthropic-memory.mjs';
36
+ import { configGet, configSet, configShowOrigin } from './config-core.mjs';
37
+ import { importClaudeMd } from './import-claude-md.mjs';
36
38
  import { extractTranscript, discoverSessions } from './transcripts.mjs';
37
39
  import { runRepair } from './repair.mjs';
38
40
  import { runRoll, ROLL_SCOPES } from './roll.mjs';
@@ -60,6 +62,8 @@ import { overrideTrust as overrideTrustAction } from './trust.mjs';
60
62
  import { resolveConflictQueue, mergeScratchpadBullets } from './conflict-queue.mjs';
61
63
  import { resolveReviewQueue } from './review-queue.mjs';
62
64
  import { createInterface } from 'node:readline';
65
+ import { spawnSync } from 'node:child_process';
66
+ import { checkKitBinding } from './native-binding.mjs';
63
67
  import { resolve as resolvePath, join, basename } from 'node:path';
64
68
 
65
69
  const NOTICE_PREFIX = 'not yet implemented';
@@ -97,7 +101,71 @@ export function formatSemanticSummary(semantic, { noHooks = false } = {}) {
97
101
  * summary, and reports the CLAUDE.md action (created / appended /
98
102
  * replaced / upgraded / downgrade-blocked / forced-downgrade / unchanged).
99
103
  */
100
- async function runInstall(options /* , command */) {
104
+ // Task 141a (D-129): the install-time binding ask. npm 12 blocks
105
+ // better-sqlite3's binding build on a fresh `npm install -g` — the user's
106
+ // 2026-06-12 steer: ask AT INSTALL, never leave it to a secondary command.
107
+ // Interactive consent is required because the fix is itself an
108
+ // `npm install -g` (the design §14 ask-before-install rule); non-interactive
109
+ // runs print the command instead. All deps injectable for tests.
110
+ async function offerBindingFix(nativeBinding, options, { log, logError }) {
111
+ if (!nativeBinding || nativeBinding.ok) return;
112
+ const remedy = nativeBinding.remedy;
113
+ logError(
114
+ ` warning: better-sqlite3's native binding is unavailable (${nativeBinding.reason}).`,
115
+ );
116
+ logError(
117
+ ' Most common cause: npm 12 blocks dependency install scripts by default (a Node major upgrade is the other). Search/reindex cannot work until the binding is rebuilt.',
118
+ );
119
+ // An explicit askImpl implies a consent channel exists (the test seam /
120
+ // programmatic caller); only the readline default needs a real TTY.
121
+ const interactive =
122
+ options?.interactive ?? (options?.askImpl ? true : process.stdin.isTTY === true);
123
+ const askFn =
124
+ options?.askImpl ??
125
+ (interactive
126
+ ? (question) =>
127
+ new Promise((resolveAnswer) => {
128
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
129
+ rl.question(question, (answer) => {
130
+ rl.close();
131
+ resolveAnswer(answer);
132
+ });
133
+ })
134
+ : null);
135
+ if (!interactive || !askFn) {
136
+ logError(` Fix it any time with: ${remedy}`);
137
+ return;
138
+ }
139
+ const answer = String(await askFn(` Fix it now by running \`${remedy}\`? [Y/n] `))
140
+ .trim()
141
+ .toLowerCase();
142
+ const yes = answer === '' || answer === 'y' || answer === 'yes';
143
+ if (!yes) {
144
+ log(` Skipped. Fix it any time with: ${remedy}`);
145
+ return;
146
+ }
147
+ const fixRunner =
148
+ options?.fixRunner ??
149
+ ((cmd) =>
150
+ // Constant command under shell:true (npm is npm.cmd on Windows); the
151
+ // 10-min ceiling mirrors buildDefaultNpmRunner's spawn discipline.
152
+ spawnSync(cmd, { stdio: 'inherit', shell: true, timeout: 600_000 }));
153
+ const r = fixRunner(remedy);
154
+ const reProbe = options?.reProbe ?? checkKitBinding;
155
+ const after = r.status === 0 ? reProbe() : { ok: false };
156
+ if (after.ok) {
157
+ log(' Binding rebuilt — search is ready.');
158
+ } else {
159
+ logError(` The binding is still unavailable — run it manually later: ${remedy}`);
160
+ }
161
+ }
162
+
163
+ // Exported for tests (Task 141a) — dep-injectable (cwd / userTier / log /
164
+ // logError / bindingProbe / askImpl / fixRunner / reProbe / interactive) on
165
+ // the runImportClaudeMd pattern. Defaults unchanged for production.
166
+ export async function runInstall(options /* , command */) {
167
+ const log = options?.log ?? console.log;
168
+ const logError = options?.logError ?? console.error;
101
169
  // commander maps `--no-hooks` to options.hooks === false.
102
170
  const noHooks = !!(options && options.hooks === false);
103
171
  const verbose = !!(options && options.verbose);
@@ -110,6 +178,9 @@ async function runInstall(options /* , command */) {
110
178
  // to options.withSemantic.
111
179
  withSemantic: !!(options && options.withSemantic),
112
180
  noSemantic: !!(options && options.semantic === false),
181
+ projectRoot: options?.cwd,
182
+ userTier: options?.userTier,
183
+ bindingProbe: options?.bindingProbe,
113
184
  });
114
185
 
115
186
  // Outcome over inventory (self-test UX finding): state the resulting state +
@@ -117,7 +188,7 @@ async function runInstall(options /* , command */) {
117
188
  // read like a problem on a FRESH folder — the "skipped" are the cross-project
118
189
  // user tier at ~/.claude-memory-kit/ (OUTSIDE this folder), already on disk.
119
190
  // The full per-tier breakdown is --verbose only.
120
- const projectName = basename(resolvePath(process.cwd()));
191
+ const projectName = basename(result.projectRoot);
121
192
  const wired =
122
193
  result.hooks.action === 'wired' || result.hooks.action === 'unchanged';
123
194
  const broughtSomethingNew =
@@ -126,20 +197,20 @@ async function runInstall(options /* , command */) {
126
197
  result.claudeMd.action === 'created';
127
198
 
128
199
  if (broughtSomethingNew) {
129
- console.log(
200
+ log(
130
201
  `cmk install: ${projectName} ready — context/ scaffolded${
131
202
  wired ? ', hooks wired' : ''
132
203
  }.`,
133
204
  );
134
205
  } else {
135
- console.log(
206
+ log(
136
207
  `cmk install: ${projectName} already set up (your edits preserved)${
137
208
  wired ? ', hooks refreshed' : ''
138
209
  }.`,
139
210
  );
140
211
  }
141
212
  if (wired) {
142
- console.log(
213
+ log(
143
214
  ' Restart Claude Code to activate. Complete install — no separate /plugin step needed.',
144
215
  );
145
216
  }
@@ -147,35 +218,39 @@ async function runInstall(options /* , command */) {
147
218
  // Auto Memory by default; surface the one-command opt-out (null when already
148
219
  // opted out, so we don't nag).
149
220
  const nativeNote = nativeMemoryInstallNote(result.projectRoot);
150
- if (nativeNote) console.log(nativeNote);
221
+ if (nativeNote) log(nativeNote);
151
222
  // Task 46: semantic-recall outcome (pure formatter, Task 125.4 — testable
152
223
  // without spawning install; the error case returns null because enableSemantic
153
224
  // errors already land in result.errors and print through the error path).
154
225
  const semanticLine = formatSemanticSummary(result.semantic, { noHooks });
155
- if (semanticLine) console.log(semanticLine);
226
+ if (semanticLine) log(semanticLine);
156
227
  if (verbose) {
157
- console.log(
228
+ log(
158
229
  ` files: ${result.created.length} created, ${result.skipped.length} already present` +
159
230
  (result.skipped.length
160
231
  ? ' (incl. the cross-project user tier at ~/.claude-memory-kit/, outside this folder)'
161
232
  : ''),
162
233
  );
163
- console.log(
234
+ log(
164
235
  ` .gitignore=${result.gitignore.action} · CLAUDE.md=${result.claudeMd.action} · hooks=${result.hooks.action}`,
165
236
  );
166
237
  }
167
238
 
168
239
  if (result.claudeMd.action === 'downgrade-blocked') {
169
- console.error(
240
+ logError(
170
241
  ` warning: CLAUDE.md already has a newer kit block (v${result.claudeMd.oldVersion}). ` +
171
242
  `Re-run with --force to downgrade.`
172
243
  );
173
244
  }
174
245
 
175
246
  if (result.errors.length > 0) {
176
- for (const e of result.errors) console.error(` error: ${e.path}: ${e.error}`);
247
+ for (const e of result.errors) logError(` error: ${e.path}: ${e.error}`);
177
248
  process.exitCode = 1;
178
249
  }
250
+
251
+ // Task 141a: the binding ask comes LAST — it's the one thing the user may
252
+ // still need to act on, and the tail of install output is what gets read.
253
+ await offerBindingFix(result.nativeBinding, options, { log, logError });
179
254
  }
180
255
 
181
256
  /**
@@ -656,7 +731,10 @@ export function parseFactInput(options, { readFile, readStdin } = {}) {
656
731
  };
657
732
  }
658
733
 
659
- export function runRemember(textParts, options, deps = {}) {
734
+ // Task 143: async since the near-dup guard may embed the incoming text
735
+ // (one model call, explicit path only). Commander awaits actions; the
736
+ // terse-path tests were updated to await (contract change, intent preserved).
737
+ export async function runRemember(textParts, options, deps = {}) {
660
738
  const projectRoot = deps.projectRoot ?? resolvePath(process.cwd());
661
739
  const userDir =
662
740
  deps.userDir ?? process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
@@ -717,6 +795,10 @@ export function runRemember(textParts, options, deps = {}) {
717
795
  const tier = 'P';
718
796
  const trust = options?.trust ?? 'high';
719
797
  const section = options?.section ?? 'Active Threads';
798
+ // Task 143 (D-130): semantic near-dup guard — extra opts only when this
799
+ // project is semantic-configured and the embedder is available; {} keeps
800
+ // the literal pipeline (graceful degradation, never blocks capture).
801
+ const nearDup = await prepareNearDupGuard({ projectRoot, text, ...(deps.nearDupGuard ? { prepareImpl: deps.nearDupGuard.prepareImpl, resolveModeImpl: deps.nearDupGuard.resolveModeImpl } : {}) });
720
802
  const r = memoryWrite({
721
803
  action: 'add',
722
804
  text,
@@ -727,22 +809,23 @@ export function runRemember(textParts, options, deps = {}) {
727
809
  source: 'user-explicit',
728
810
  projectRoot,
729
811
  userDir,
812
+ ...nearDup,
730
813
  });
731
814
  if (r.action === 'error') {
732
815
  for (const e of r.errors ?? [`error (${r.errorCategory})`]) {
733
- console.error(`cmk remember: ${e}`);
816
+ logError(`cmk remember: ${e}`);
734
817
  }
735
818
  process.exitCode = 2;
736
819
  return;
737
820
  }
738
821
  if (r.action === 'queued') {
739
- console.log(
740
- `cmk remember: queued for review — a higher-trust fact already covers this. ` +
822
+ log(
823
+ `cmk remember: queued for review — a similar or higher-trust fact already covers this. ` +
741
824
  `Resolve with \`cmk queue conflicts\` (${r.path}).`,
742
825
  );
743
826
  return;
744
827
  }
745
- console.log(
828
+ log(
746
829
  `cmk remember: saved to P/MEMORY.md (${section})${r.id ? ` [${r.id}]` : ''}`,
747
830
  );
748
831
  }
@@ -786,6 +869,32 @@ function runReindex(options /* , command */) {
786
869
  }
787
870
  }
788
871
 
872
+ /**
873
+ * `cmk digest` (Task 147) — print a regenerated, readable render of everything
874
+ * the kit currently knows, AND sync the append-only context/DECISIONS.md
875
+ * journal (the permanent decision ledger; D-161). The digest goes to stdout;
876
+ * the journal is a committed file the sync maintains in place.
877
+ */
878
+ async function runDigestCli(options) {
879
+ const projectRoot = resolvePath(process.cwd());
880
+ const { digest } = await import('./digest.mjs');
881
+ const { syncDecisionsJournal } = await import('./decisions-journal.mjs');
882
+
883
+ // Keep the permanent decision journal current (append-only; best-effort —
884
+ // a journal hiccup must not break the digest render).
885
+ const sync = syncDecisionsJournal({ projectRoot });
886
+
887
+ console.log(digest({ projectRoot }));
888
+
889
+ if (sync.written) {
890
+ console.log(`\ncontext/DECISIONS.md updated (+${sync.appended} bytes) — the append-only decision journal.`);
891
+ } else if (sync.error) {
892
+ console.error(`\n(decision journal not updated: ${sync.error})`);
893
+ } else {
894
+ console.log('\ncontext/DECISIONS.md is up to date.');
895
+ }
896
+ }
897
+
789
898
  /**
790
899
  * `cmk forget <id-or-query>` — wired in Task 9. Tombstones the matching
791
900
  * fact (moves it to <tier>/<memory|fragments>/archive/tombstones/<id>.md
@@ -1183,12 +1292,93 @@ async function runDoctorCli(/* options */) {
1183
1292
  `Summary: ${counts.pass} pass · ${counts.fail} fail · ${counts.skip} skip (${r.duration_ms}ms)`,
1184
1293
  );
1185
1294
  if (counts.fail > 0) process.exitCode = 1;
1295
+
1296
+ // Task 144 (D-130): the memory-HEALTH section — content quality, not
1297
+ // plumbing. Informational only: read-only, never changes the exit code,
1298
+ // best-effort (a content-stat hiccup must not fail a healthy doctor).
1299
+ try {
1300
+ const { analyzeMemoryHealth, formatMemoryHealth } = await import('./memory-health.mjs');
1301
+ console.log('');
1302
+ console.log(formatMemoryHealth(analyzeMemoryHealth({ projectRoot })));
1303
+ } catch {
1304
+ // informational section only — stay silent on failure
1305
+ }
1186
1306
  } catch (err) {
1187
1307
  console.error(`cmk doctor: unexpected error: ${err?.message ?? err}`);
1188
1308
  process.exitCode = 2;
1189
1309
  }
1190
1310
  }
1191
1311
 
1312
+ // Task 129 (D-121): `cmk config` — real, replacing the v0.1.0 stub. Dotted-key
1313
+ // get/set/--show-origin over the per-tier settings.json files. Dep-injectable
1314
+ // (cwd/userDir/log/logError) on the runImportClaudeMd pattern for testing the
1315
+ // CLI surface. The pure resolution/mutation lives in config-core.mjs.
1316
+ const TIER_FLAG_TO_NAME = { local: 'local', project: 'project', user: 'user' };
1317
+
1318
+ export function runConfigGet(key, options = {}) {
1319
+ const projectRoot = options?.cwd ?? resolvePath(process.cwd());
1320
+ const userDir = options?.userDir ?? join(homedir(), '.claude-memory-kit');
1321
+ const log = options?.log ?? console.log;
1322
+ const logError = options?.logError ?? console.error;
1323
+ const r = configGet(key, { projectRoot, userDir });
1324
+ if (!r.found) {
1325
+ logError(`cmk config get: '${key}' is not set in any tier`);
1326
+ process.exitCode = 2;
1327
+ return r;
1328
+ }
1329
+ log(typeof r.value === 'string' ? r.value : JSON.stringify(r.value));
1330
+ return r;
1331
+ }
1332
+
1333
+ export function runConfigSet(key, value, options = {}) {
1334
+ const projectRoot = options?.cwd ?? resolvePath(process.cwd());
1335
+ const userDir = options?.userDir ?? join(homedir(), '.claude-memory-kit');
1336
+ const log = options?.log ?? console.log;
1337
+ const logError = options?.logError ?? console.error;
1338
+ const tier = TIER_FLAG_TO_NAME[options?.tier ?? 'project'] ?? 'project';
1339
+ const r = configSet(key, value, { projectRoot, userDir, tier });
1340
+ if (!r.ok) {
1341
+ logError(`cmk config set: ${r.error}`);
1342
+ process.exitCode = 2;
1343
+ return r;
1344
+ }
1345
+ log(`cmk config set: ${key} = ${value} (${r.tier} tier)`);
1346
+ return r;
1347
+ }
1348
+
1349
+ export function runConfigShowOrigin(key, options = {}) {
1350
+ const projectRoot = options?.cwd ?? resolvePath(process.cwd());
1351
+ const userDir = options?.userDir ?? join(homedir(), '.claude-memory-kit');
1352
+ const log = options?.log ?? console.log;
1353
+ const logError = options?.logError ?? console.error;
1354
+ const r = configShowOrigin(key, { projectRoot, userDir });
1355
+ if (!r.found) {
1356
+ logError(`cmk config --show-origin: '${key}' is not set in any tier`);
1357
+ process.exitCode = 2;
1358
+ return r;
1359
+ }
1360
+ for (const e of r.entries) {
1361
+ const val = typeof e.value === 'string' ? `"${e.value}"` : JSON.stringify(e.value);
1362
+ const note = e.winner ? '' : ` (shadowed by ${e.shadowedBy})`;
1363
+ log(`${e.tier.padEnd(8)} ${e.path} ${val}${note}`);
1364
+ }
1365
+ return r;
1366
+ }
1367
+
1368
+ // The parent `cmk config` action: handle the --show-origin flag here; the
1369
+ // get/set children carry their own actions (wired in the registry below).
1370
+ // Exported for the branch test (the no-subcommand path).
1371
+ export function runConfigCli(options /* , command */) {
1372
+ if (options?.showOrigin) {
1373
+ return runConfigShowOrigin(options.showOrigin, options);
1374
+ }
1375
+ const logError = options?.logError ?? console.error;
1376
+ logError(
1377
+ 'cmk config: specify a subcommand — `get <key>`, `set <key> <value>`, or `--show-origin <key>`.',
1378
+ );
1379
+ process.exitCode = 2;
1380
+ }
1381
+
1192
1382
  async function runRepairCli(options /* , command */) {
1193
1383
  const projectRoot = resolvePath(process.cwd());
1194
1384
  const userDir = join(homedir(), '.claude-memory-kit');
@@ -1304,6 +1494,59 @@ export async function runImportAnthropicMemory(options = {}) {
1304
1494
  }
1305
1495
  }
1306
1496
 
1497
+ // Task 142 (D-130): onboard from an existing rules file. Dep-injectable
1498
+ // (projectRoot / log / logError / importFn) on the runImportAnthropicMemory
1499
+ // pattern so the real CLI path is verifiable in a sandbox. `file` is the
1500
+ // optional positional (commander passes it first), defaulting to CLAUDE.md.
1501
+ export async function runImportClaudeMd(file, options = {}) {
1502
+ const projectRoot = options?.projectRoot ?? resolvePath(process.cwd());
1503
+ const log = options?.log ?? console.log;
1504
+ const logError = options?.logError ?? console.error;
1505
+ const dryRun = options?.dryRun === true;
1506
+ const acceptAll = options?.yes === true;
1507
+ const importFn = options?.importFn ?? importClaudeMd;
1508
+ try {
1509
+ const r = await importFn({ projectRoot, file, dryRun, acceptAll });
1510
+ if (r.action === 'error') {
1511
+ logError(`cmk import-claude-md: error — ${(r.errors ?? []).join('; ')}`);
1512
+ process.exitCode = 2;
1513
+ return r;
1514
+ }
1515
+ if (r.reason === 'no-source') {
1516
+ log(`cmk import-claude-md: no rules file found at ${r.sourcePath}`);
1517
+ return r;
1518
+ }
1519
+ if (r.reason) {
1520
+ // e.g. read-source-failed — completed-with-failure must not print the
1521
+ // success-shaped "applied 0" line (skill-review 2026-06-12 finding).
1522
+ logError(`cmk import-claude-md: ${r.reason} (${r.sourcePath})`);
1523
+ process.exitCode = 2;
1524
+ return r;
1525
+ }
1526
+ const listProposals = () => {
1527
+ for (const p of r.proposals) log(` + [${p.type}] L${p.line}: ${p.text}`);
1528
+ };
1529
+ if (r.mode === 'dry-run') {
1530
+ log(`cmk import-claude-md: dry-run — ${r.proposals.length} proposal(s), ${r.skipped} duplicate(s) skipped`);
1531
+ listProposals();
1532
+ return r;
1533
+ }
1534
+ if (r.mode === 'requires-confirmation') {
1535
+ log(`cmk import-claude-md: ${r.proposals.length} proposal(s) ready to apply.`);
1536
+ log(' Re-run with --yes to apply, or --dry-run to inspect.');
1537
+ listProposals();
1538
+ return r;
1539
+ }
1540
+ const rejectedNote = r.rejected > 0 ? `, ${r.rejected} rejected by Poison_Guard` : '';
1541
+ const errorNote = r.errors > 0 ? `, ${r.errors} error(s)` : '';
1542
+ log(`cmk import-claude-md: applied ${r.accepted} fact(s), skipped ${r.skipped} duplicate(s)${rejectedNote}${errorNote}`);
1543
+ return r;
1544
+ } catch (err) {
1545
+ logError(`cmk import-claude-md: unexpected error: ${err?.message ?? err}`);
1546
+ process.exitCode = 2;
1547
+ }
1548
+ }
1549
+
1307
1550
  async function runTranscriptsDispatch(childName, options) {
1308
1551
  if (childName === 'extract') {
1309
1552
  return runTranscriptsExtract(options);
@@ -1426,7 +1669,10 @@ async function runMcpDispatch(childName) {
1426
1669
  }
1427
1670
  return;
1428
1671
  }
1429
- console.error(`cmk mcp: ${NOTICE_PREFIX} (unknown sub-verb '${childName}')`);
1672
+ // A bare `cmk mcp` (no sub-verb) reaches here post-Task-129 (the parent
1673
+ // action is now wired) — commander passes an options object, not a string.
1674
+ const verb = typeof childName === 'string' ? childName : '(none)';
1675
+ console.error(`cmk mcp: ${NOTICE_PREFIX} (run \`cmk mcp serve\`; got sub-verb '${verb}')`);
1430
1676
  process.exitCode = 2;
1431
1677
  }
1432
1678
 
@@ -1437,7 +1683,10 @@ async function runQueueDispatch(childName) {
1437
1683
  if (childName === 'review') {
1438
1684
  return runQueueReview();
1439
1685
  }
1440
- console.log(`cmk queue: ${NOTICE_PREFIX} (unknown sub-verb '${childName}')`);
1686
+ // A bare `cmk queue` reaches here post-Task-129 (parent action wired);
1687
+ // commander passes an options object, not a string sub-verb.
1688
+ const verb = typeof childName === 'string' ? childName : '(none)';
1689
+ console.log(`cmk queue: ${NOTICE_PREFIX} (run \`cmk queue review\` or \`cmk queue conflicts\`; got '${verb}')`);
1441
1690
  process.exitCode = 2;
1442
1691
  }
1443
1692
 
@@ -1796,29 +2045,40 @@ export const subcommands = [
1796
2045
  milestone: 37,
1797
2046
  action: runDoctorCli,
1798
2047
  },
2048
+ {
2049
+ name: 'digest',
2050
+ description: 'print a readable digest of everything in memory + sync the append-only DECISIONS.md decision journal',
2051
+ milestone: 147,
2052
+ action: runDigestCli,
2053
+ },
1799
2054
  {
1800
2055
  name: 'config',
1801
- description: 'settings access (per design §7.2)',
1802
- milestone: 'v0.1.x',
2056
+ description: 'read/write kit settings (context/settings.json) without hand-editing JSON',
2057
+ milestone: 129,
1803
2058
  optionSpec: [
1804
- { flags: '--show-origin <key>', description: 'print where each value comes from (project / user / local tier)' },
2059
+ { flags: '--show-origin <key>', description: 'print every tier that defines a setting (winner + shadowed) the "where did this come from?" debug surface' },
1805
2060
  ],
1806
2061
  children: [
1807
2062
  {
1808
2063
  name: 'get',
1809
- description: 'print the resolved value of a setting',
1810
- argSpec: [{ flags: '<key>', description: 'setting key (dotted path)' }],
2064
+ description: 'print the resolved value of a setting (dotted key; local > project > user)',
2065
+ argSpec: [{ flags: '<key>', description: 'setting key (dotted path, e.g. search.default_mode)' }],
2066
+ action: (key, options) => runConfigGet(key, options),
1811
2067
  },
1812
2068
  {
1813
2069
  name: 'set',
1814
- description: 'set a setting in the current tier',
2070
+ description: 'set a setting in the project tier (or --local)',
1815
2071
  argSpec: [
1816
2072
  { flags: '<key>', description: 'setting key (dotted path)' },
1817
- { flags: '<value>', description: 'new value' },
2073
+ { flags: '<value>', description: 'new value (true/false/number coerced; else string)' },
2074
+ ],
2075
+ optionSpec: [
2076
+ { flags: '--local', description: 'write to the local tier (context.local/, gitignored) instead of project' },
1818
2077
  ],
2078
+ action: (key, value, options) => runConfigSet(key, value, { tier: options?.local ? 'local' : 'project' }),
1819
2079
  },
1820
2080
  ],
1821
- action: stub('config', 'v0.1.x'),
2081
+ action: runConfigCli,
1822
2082
  },
1823
2083
  {
1824
2084
  name: 'import-anthropic-memory',
@@ -1830,6 +2090,19 @@ export const subcommands = [
1830
2090
  ],
1831
2091
  action: runImportAnthropicMemory,
1832
2092
  },
2093
+ {
2094
+ name: 'import-claude-md',
2095
+ description: 'onboard from an existing rules file (CLAUDE.md / .cursorrules / AGENTS.md) — parse it into typed facts through the safe write path',
2096
+ milestone: 142,
2097
+ argSpec: [
2098
+ { flags: '[file]', description: 'rules file to import, relative to the project root (default: CLAUDE.md)' },
2099
+ ],
2100
+ optionSpec: [
2101
+ { flags: '--dry-run', description: 'preview the typed proposals without modifying files' },
2102
+ { flags: '--yes', description: 'apply every proposal without prompting (apply requires explicit --yes)' },
2103
+ ],
2104
+ action: runImportClaudeMd,
2105
+ },
1833
2106
  {
1834
2107
  name: 'transcripts',
1835
2108
  description: "extract clean markdown transcripts from Claude Code session jsonls under ~/.claude/projects/",
@@ -15,7 +15,7 @@
15
15
  // chunkTranscript(text) → [{heading, body, sourceLine, chunkIdx}] (pure)
16
16
  // syncTranscriptChunks({db, projectRoot, now?}) → {files, chunks}
17
17
 
18
- import { createHash } from 'node:crypto';
18
+ import { hashContent } from './content-hash.mjs';
19
19
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
21
 
@@ -57,8 +57,11 @@ export function chunkTranscript(text) {
57
57
  return chunks;
58
58
  }
59
59
 
60
+ // Transcript-chunk fingerprint for the `files`-table diff key (column name
61
+ // `sha1` kept for checkpoint back-compat; algorithm is SHA-256 via hashContent,
62
+ // D-149). Self-heals on the first post-upgrade boot like the observation index.
60
63
  function sha1(text) {
61
- return createHash('sha1').update(text, 'utf8').digest('hex');
64
+ return hashContent(text);
62
65
  }
63
66
 
64
67
  // Task 126 (D-119) — the raw-tier scope covers BOTH halves of the session
@@ -21,6 +21,7 @@ import { reindex } from './reindex.mjs';
21
21
  import { appendAuditEntry, nowIso, REASON_CODES } from './audit-log.mjs';
22
22
  import { ERROR_CATEGORIES, errorResult } from './result-shapes.mjs';
23
23
  import { sanitizeHomePaths } from './sanitize.mjs';
24
+ import { sanitizePrivacyTags } from './privacy.mjs';
24
25
  import { checkPoisonGuard, logPoisonGuardRejection } from './poison-guard.mjs';
25
26
 
26
27
  const VALID_TYPES = new Set(['user', 'feedback', 'project', 'reference']);
@@ -157,6 +158,14 @@ export function writeFact(opts = {}) {
157
158
  // — that's its purpose. The id hashes the SANITIZED body, so dedup keys on
158
159
  // what actually lands on disk.
159
160
  let { body, title } = opts;
161
+ // Privacy: strip <private>…</private> FIRST, on EVERY tier (cut-gate
162
+ // v0.3.1 finding — the tag was honored only by the UserPromptSubmit hook,
163
+ // so a fact written via cmk remember/mk_remember/import kept the secret).
164
+ // Runs before home-path sanitization, Poison_Guard, and id-generation, so
165
+ // the redacted body is what gets screened, hashed (dedup keys on what
166
+ // lands), and written.
167
+ body = sanitizePrivacyTags(body);
168
+ title = sanitizePrivacyTags(title);
160
169
  if (opts.tier === 'P' || opts.tier === 'U') {
161
170
  body = sanitizeHomePaths(body);
162
171
  title = sanitizeHomePaths(title);
@@ -252,10 +261,32 @@ export function writeFact(opts = {}) {
252
261
  // 2026-06-03 — "users should get it working from the start"). Best-effort: the
253
262
  // fact is already durably on disk, so an index-rebuild hiccup must not turn a
254
263
  // successful capture into an error — the next reindex/search self-heals.
264
+ //
265
+ // D-152: the failure is OBSERVABLE, not silently swallowed. A detached
266
+ // auto-extract child whose reindex was killed mid-rebuild (hook ceiling) used
267
+ // to leave INDEX.md lagging with ZERO trace — so a stale committed INDEX was
268
+ // undiagnosable (the user caught a 5-fact lag in the cut-gate). On throw we
269
+ // now record an INDEX_REBUILD_FAILED audit entry; HC-4 still detects the drift
270
+ // and `cmk reindex` corrects it. The `_reindexFn` seam is test-only.
271
+ const doReindex = opts._reindexFn ?? reindex;
255
272
  try {
256
- reindex({ tier: opts.tier, projectRoot: opts.projectRoot, userDir: opts.userDir, warn: () => {} });
257
- } catch {
258
- // index rebuild is best-effort; capture already succeeded
273
+ doReindex({ tier: opts.tier, projectRoot: opts.projectRoot, userDir: opts.userDir, warn: () => {} });
274
+ } catch (reindexErr) {
275
+ // index rebuild is best-effort; capture already succeeded — but leave a
276
+ // trace so a lagging committed INDEX is diagnosable, never silent.
277
+ try {
278
+ appendAuditEntry(tierRoot, {
279
+ ts: createdAt,
280
+ action: 'index-rebuild-failed',
281
+ tier: opts.tier,
282
+ id,
283
+ reasonCode: REASON_CODES.INDEX_REBUILD_FAILED,
284
+ paths: { after: path },
285
+ extra: { error: String(reindexErr?.message ?? reindexErr) },
286
+ });
287
+ } catch {
288
+ // even the audit append is best-effort; the fact is already on disk
289
+ }
259
290
  }
260
291
 
261
292
  // Default create-audit (Task 123.A / D-103). writeFact is the single boundary
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: memory-search
3
- description: Searches the project's deep memory archive (claude-memory-kit) and returns a curated summary of relevant facts, decisions, and history. Use when the answer may already be recorded from a past session "what did we decide about X", "why did we do Y", "have we seen this error before", "how did we solve this last time", "what's our convention for Z" or before re-deriving any project knowledge, setup, or prior decision from the code. The session-start memory snapshot is a bounded hot index, not everything; this skill reaches the rest. Skip when the question is purely about current code state (use Read/Grep), about this conversation only, or the user asked to ignore memory.
3
+ description: Searches the project's recorded memory (claude-memory-kit) decisions, conventions, architecture, the reasoning behind choices, and where things live — and returns a curated, cited summary. Fire whenever the answer might be something the project already established in past work, HOWEVER the question is phrased any prior decision, convention, rationale, or "how/where/why is it this way" question, including oblique or roundabout asks ("why is everything so spread out?", "remind me what we settled on for X", "how come these files are tiny?"). Also fire when a "[claude-memory-kit] Memory available" hint appears on the prompt. The examples are illustrative, not a checklist — prefer recalling over re-deriving an answer from the code. The session-start snapshot is a bounded index; this skill reaches the rest. Skip only when the question is purely about uncommitted or just-edited live code that memory cannot know, concerns this conversation only, or the user asked to ignore memory.
4
4
  context: fork
5
5
  allowed-tools: mcp__cmk__mk_search mcp__cmk__mk_get mcp__cmk__mk_timeline mcp__cmk__mk_recent_activity Bash(cmk search *) Bash(cmk get *) Bash(cmk timeline *) Bash(cmk recent-activity *)
6
6
  ---
@@ -0,0 +1,16 @@
1
+ # claude-memory-kit — added by `cmk install`. Do not edit by hand;
2
+ # `cmk install` refreshes these lines idempotently. Remove via
3
+ # `cmk uninstall`.
4
+ #
5
+ # Force LF on the COMMITTED memory files. The kit's frontmatter parser uses
6
+ # a strict-LF boundary and split('\n') readers; default Windows git
7
+ # (autocrlf=true) rewrites line endings at clone, which made every fact
8
+ # invisible to search on a Windows checkout (D-126). The read side now
9
+ # self-heals (tolerant CRLF reads), but pinning LF here PREVENTS the
10
+ # mangling so it never has to.
11
+ #
12
+ # Scoped to the text extensions the kit actually commits (.md + .json) under
13
+ # context/ — NOT a blanket `context/** text`, so a future committed binary
14
+ # under context/ is never force-normalized + corrupted.
15
+ context/**/*.md text eol=lf
16
+ context/**/*.json text eol=lf
@@ -26,7 +26,7 @@ The `cmk doctor` health checks verify each layer is wired correctly: install int
26
26
 
27
27
  ### Recalling memory (for Claude)
28
28
 
29
- The snapshot injected at session start is a **bounded hot index, not everything** — there is a deeper, queryable archive. When a question is "what did we decide / what's our X / how does the user work / what's the setup," **query your memory instead of re-deriving the answer from scratch**:
29
+ The snapshot injected at session start is a **bounded hot index, not everything** — there is a deeper, queryable archive. When a question is "what did we decide / what's our X / how does the user work / what's the setup / **how is this project structured or built / where does X live / what's the architecture**," **query your memory instead of re-deriving the answer from scratch** — the structure is a recorded decision, recall it before re-reading the files to reconstruct it:
30
30
 
31
31
  - **`cmk search "<topic>"`** — find any captured fact (decisions, preferences, config, lessons) across the project + user tiers.
32
32
  - **`context/memory/<type>_<slug>.md`** — the granular fact archive with full **Why / How** rationale (`context/memory/INDEX.md` lists them).