@lh8ppl/claude-memory-kit 0.2.4 → 0.3.1
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 +16 -10
- package/bin/cmk-capture-prompt.mjs +21 -1
- package/package.json +2 -1
- package/src/audit-log.mjs +1 -0
- package/src/auto-drain.mjs +17 -1
- package/src/auto-extract.mjs +72 -16
- package/src/auto-persona.mjs +86 -1
- package/src/capture-prompt.mjs +34 -1
- package/src/capture-turn.mjs +64 -6
- package/src/config-core.mjs +161 -0
- package/src/conflict-queue.mjs +20 -3
- package/src/content-hash.mjs +30 -0
- package/src/doctor.mjs +62 -3
- package/src/forget.mjs +13 -0
- package/src/frontmatter.mjs +4 -1
- package/src/import-anthropic-memory.mjs +25 -1
- package/src/import-claude-md.mjs +333 -0
- package/src/index-db.mjs +39 -0
- package/src/index-rebuild.mjs +48 -4
- package/src/index.mjs +10 -0
- package/src/inject-context.mjs +179 -7
- package/src/install.mjs +180 -1
- package/src/mcp-server.mjs +63 -8
- package/src/memory-health.mjs +229 -0
- package/src/memory-write.mjs +32 -10
- package/src/merge-facts.mjs +12 -0
- package/src/native-binding.mjs +142 -0
- package/src/poison-guard.mjs +55 -0
- package/src/provenance.mjs +4 -0
- package/src/remember-core.mjs +53 -8
- package/src/repair.mjs +20 -3
- package/src/result-shapes.mjs +1 -1
- package/src/scratchpad.mjs +5 -3
- package/src/search.mjs +96 -9
- package/src/semantic-backend.mjs +599 -0
- package/src/settings-hooks.mjs +4 -1
- package/src/subcommands.mjs +359 -42
- package/src/transcript-index.mjs +165 -0
- package/src/turn-tools.mjs +179 -0
- package/src/write-fact.mjs +34 -3
- package/template/.claude/skills/memory-search/SKILL.md +86 -0
- package/template/.gitattributes.fragment +16 -0
- package/template/CLAUDE.md.template +3 -1
package/src/subcommands.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { install as installAction, initUserTier as initUserTierAction } from './
|
|
|
17
17
|
import { removeClaudeMdBlock } from './claude-md.mjs';
|
|
18
18
|
import { reindex as reindexAction } from './reindex.mjs';
|
|
19
19
|
import { openIndexDb } from './index-db.mjs';
|
|
20
|
+
import { resolveDefaultSearchMode } from './semantic-backend.mjs';
|
|
20
21
|
import { reindexBoot, reindexFull } from './index-rebuild.mjs';
|
|
21
22
|
import { search as searchAction, SEARCH_MODES } from './search.mjs';
|
|
22
23
|
import { memoryWrite } from './memory-write.mjs';
|
|
@@ -26,12 +27,14 @@ import { weeklyCurate } from './weekly-curate.mjs';
|
|
|
26
27
|
import { autoPersona } from './auto-persona.mjs';
|
|
27
28
|
import { exportPersona, importPersona } from './persona-portability.mjs';
|
|
28
29
|
import { setNativeAutoMemory, nativeMemoryInstallNote } from './native-memory.mjs';
|
|
29
|
-
import { rememberRich, richFactTitle, nonProjectTierNote } from './remember-core.mjs';
|
|
30
|
+
import { rememberRich, richFactTitle, nonProjectTierNote, prepareNearDupGuard } from './remember-core.mjs';
|
|
30
31
|
import { getObservations, citeLink, buildTimeline, recentActivity } from './read-core.mjs';
|
|
31
32
|
import { readHookStdin } from './read-hook-stdin.mjs';
|
|
32
33
|
import { runLazyCompress } from './lazy-compress.mjs';
|
|
33
34
|
import { runDoctor } from './doctor.mjs';
|
|
34
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';
|
|
35
38
|
import { extractTranscript, discoverSessions } from './transcripts.mjs';
|
|
36
39
|
import { runRepair } from './repair.mjs';
|
|
37
40
|
import { runRoll, ROLL_SCOPES } from './roll.mjs';
|
|
@@ -59,10 +62,38 @@ import { overrideTrust as overrideTrustAction } from './trust.mjs';
|
|
|
59
62
|
import { resolveConflictQueue, mergeScratchpadBullets } from './conflict-queue.mjs';
|
|
60
63
|
import { resolveReviewQueue } from './review-queue.mjs';
|
|
61
64
|
import { createInterface } from 'node:readline';
|
|
65
|
+
import { spawnSync } from 'node:child_process';
|
|
66
|
+
import { checkKitBinding } from './native-binding.mjs';
|
|
62
67
|
import { resolve as resolvePath, join, basename } from 'node:path';
|
|
63
68
|
|
|
64
69
|
const NOTICE_PREFIX = 'not yet implemented';
|
|
65
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The install summary line for the Task-46 semantic outcome (Task 125.4:
|
|
73
|
+
* pure + exported so the branches are testable without running install).
|
|
74
|
+
* Returns null when there is nothing to print: an `error` action already
|
|
75
|
+
* surfaces through result.errors, and the opt-in tip is suppressed under
|
|
76
|
+
* --no-hooks (scaffold-only installs).
|
|
77
|
+
*/
|
|
78
|
+
export function formatSemanticSummary(semantic, { noHooks = false } = {}) {
|
|
79
|
+
if (semantic?.action === 'enabled') {
|
|
80
|
+
const w = semantic.warmed;
|
|
81
|
+
return (
|
|
82
|
+
' Semantic recall ENABLED — `cmk search` now defaults to hybrid here.' +
|
|
83
|
+
(w?.ok
|
|
84
|
+
? ` Model cached (${Math.round(w.ms / 1000)}s).`
|
|
85
|
+
: ' Model downloads on first search.')
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (semantic?.action === 'disabled') {
|
|
89
|
+
return ' Semantic recall pinned OFF for this project (search.default_mode=keyword).';
|
|
90
|
+
}
|
|
91
|
+
if (semantic?.action === 'skipped' && !noHooks) {
|
|
92
|
+
return ' Tip: `cmk install --with-semantic` adds local semantic recall (ask in your own words; one-time ~260 MB, no API calls).';
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
/**
|
|
67
98
|
* Real `cmk install` action — wired in Task 3, extended in Task 4 with
|
|
68
99
|
* --force passed through to the CLAUDE.md downgrade guard. Reads CLI
|
|
@@ -70,18 +101,94 @@ const NOTICE_PREFIX = 'not yet implemented';
|
|
|
70
101
|
* summary, and reports the CLAUDE.md action (created / appended /
|
|
71
102
|
* replaced / upgraded / downgrade-blocked / forced-downgrade / unchanged).
|
|
72
103
|
*/
|
|
73
|
-
|
|
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;
|
|
74
169
|
// commander maps `--no-hooks` to options.hooks === false.
|
|
75
170
|
const noHooks = !!(options && options.hooks === false);
|
|
76
171
|
const verbose = !!(options && options.verbose);
|
|
77
|
-
const result = await installAction({
|
|
172
|
+
const result = await installAction({
|
|
173
|
+
force: !!(options && options.force),
|
|
174
|
+
noHooks,
|
|
175
|
+
// Task 46: two flags, 3-state semantics (enable / pin-off / untouched).
|
|
176
|
+
// commander maps `--no-semantic` to options.semantic === false (the
|
|
177
|
+
// same negation pattern as --no-hooks above); `--with-semantic` maps
|
|
178
|
+
// to options.withSemantic.
|
|
179
|
+
withSemantic: !!(options && options.withSemantic),
|
|
180
|
+
noSemantic: !!(options && options.semantic === false),
|
|
181
|
+
projectRoot: options?.cwd,
|
|
182
|
+
userTier: options?.userTier,
|
|
183
|
+
bindingProbe: options?.bindingProbe,
|
|
184
|
+
});
|
|
78
185
|
|
|
79
186
|
// Outcome over inventory (self-test UX finding): state the resulting state +
|
|
80
187
|
// next action, not a file tally. The old "scaffolded 5, skipped 4 existing"
|
|
81
188
|
// read like a problem on a FRESH folder — the "skipped" are the cross-project
|
|
82
189
|
// user tier at ~/.claude-memory-kit/ (OUTSIDE this folder), already on disk.
|
|
83
190
|
// The full per-tier breakdown is --verbose only.
|
|
84
|
-
const projectName = basename(
|
|
191
|
+
const projectName = basename(result.projectRoot);
|
|
85
192
|
const wired =
|
|
86
193
|
result.hooks.action === 'wired' || result.hooks.action === 'unchanged';
|
|
87
194
|
const broughtSomethingNew =
|
|
@@ -90,20 +197,20 @@ async function runInstall(options /* , command */) {
|
|
|
90
197
|
result.claudeMd.action === 'created';
|
|
91
198
|
|
|
92
199
|
if (broughtSomethingNew) {
|
|
93
|
-
|
|
200
|
+
log(
|
|
94
201
|
`cmk install: ${projectName} ready — context/ scaffolded${
|
|
95
202
|
wired ? ', hooks wired' : ''
|
|
96
203
|
}.`,
|
|
97
204
|
);
|
|
98
205
|
} else {
|
|
99
|
-
|
|
206
|
+
log(
|
|
100
207
|
`cmk install: ${projectName} already set up (your edits preserved)${
|
|
101
208
|
wired ? ', hooks refreshed' : ''
|
|
102
209
|
}.`,
|
|
103
210
|
);
|
|
104
211
|
}
|
|
105
212
|
if (wired) {
|
|
106
|
-
|
|
213
|
+
log(
|
|
107
214
|
' Restart Claude Code to activate. Complete install — no separate /plugin step needed.',
|
|
108
215
|
);
|
|
109
216
|
}
|
|
@@ -111,30 +218,39 @@ async function runInstall(options /* , command */) {
|
|
|
111
218
|
// Auto Memory by default; surface the one-command opt-out (null when already
|
|
112
219
|
// opted out, so we don't nag).
|
|
113
220
|
const nativeNote = nativeMemoryInstallNote(result.projectRoot);
|
|
114
|
-
if (nativeNote)
|
|
221
|
+
if (nativeNote) log(nativeNote);
|
|
222
|
+
// Task 46: semantic-recall outcome (pure formatter, Task 125.4 — testable
|
|
223
|
+
// without spawning install; the error case returns null because enableSemantic
|
|
224
|
+
// errors already land in result.errors and print through the error path).
|
|
225
|
+
const semanticLine = formatSemanticSummary(result.semantic, { noHooks });
|
|
226
|
+
if (semanticLine) log(semanticLine);
|
|
115
227
|
if (verbose) {
|
|
116
|
-
|
|
228
|
+
log(
|
|
117
229
|
` files: ${result.created.length} created, ${result.skipped.length} already present` +
|
|
118
230
|
(result.skipped.length
|
|
119
231
|
? ' (incl. the cross-project user tier at ~/.claude-memory-kit/, outside this folder)'
|
|
120
232
|
: ''),
|
|
121
233
|
);
|
|
122
|
-
|
|
234
|
+
log(
|
|
123
235
|
` .gitignore=${result.gitignore.action} · CLAUDE.md=${result.claudeMd.action} · hooks=${result.hooks.action}`,
|
|
124
236
|
);
|
|
125
237
|
}
|
|
126
238
|
|
|
127
239
|
if (result.claudeMd.action === 'downgrade-blocked') {
|
|
128
|
-
|
|
240
|
+
logError(
|
|
129
241
|
` warning: CLAUDE.md already has a newer kit block (v${result.claudeMd.oldVersion}). ` +
|
|
130
242
|
`Re-run with --force to downgrade.`
|
|
131
243
|
);
|
|
132
244
|
}
|
|
133
245
|
|
|
134
246
|
if (result.errors.length > 0) {
|
|
135
|
-
for (const e of result.errors)
|
|
247
|
+
for (const e of result.errors) logError(` error: ${e.path}: ${e.error}`);
|
|
136
248
|
process.exitCode = 1;
|
|
137
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 });
|
|
138
254
|
}
|
|
139
255
|
|
|
140
256
|
/**
|
|
@@ -251,8 +367,9 @@ function runLessonsPromote(id, options = {}) {
|
|
|
251
367
|
*
|
|
252
368
|
* The keyword backend (FTS5 BM25 over the observations index) always
|
|
253
369
|
* ships. Semantic + hybrid modes require the Layer-5b semantic backend,
|
|
254
|
-
*
|
|
255
|
-
*
|
|
370
|
+
* (Task 65: prepared automatically when the optional embedder is installed;
|
|
371
|
+
* absent embedder errors with exit code 2 + an install hint, per the 30.2
|
|
372
|
+
* contract). The `semanticBackend` DI seam
|
|
256
373
|
* is the drop-in point for the future backend.
|
|
257
374
|
*
|
|
258
375
|
* Filter flags (per tasks.md 30.4):
|
|
@@ -263,7 +380,7 @@ function runLessonsPromote(id, options = {}) {
|
|
|
263
380
|
* --limit <N> (default 20)
|
|
264
381
|
* --include-tombstoned (default false)
|
|
265
382
|
*/
|
|
266
|
-
function runSearch(queryParts, options) {
|
|
383
|
+
async function runSearch(queryParts, options) {
|
|
267
384
|
const projectRoot = resolvePath(process.cwd());
|
|
268
385
|
const userDir =
|
|
269
386
|
process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
|
|
@@ -287,15 +404,51 @@ function runSearch(queryParts, options) {
|
|
|
287
404
|
'searching the existing index. Run `cmk reindex --full` if results look stale.',
|
|
288
405
|
);
|
|
289
406
|
}
|
|
407
|
+
// Task 65: semantic/hybrid prepare the REAL embedded backend (async —
|
|
408
|
+
// search() itself stays sync; the seam gets a sync closure over the
|
|
409
|
+
// pre-embedded query vector). Task 46: an explicit --mode wins;
|
|
410
|
+
// otherwise the project's configured default (context/settings.json
|
|
411
|
+
// search.default_mode, set by `cmk install --with-semantic`), falling
|
|
412
|
+
// back to keyword. Explicit-but-unavailable → exit 2 + hint (the 30.2
|
|
413
|
+
// contract); configured-but-unavailable → graceful keyword fallback
|
|
414
|
+
// (the default must never break every search).
|
|
415
|
+
const explicitMode = options?.mode;
|
|
416
|
+
let mode = explicitMode ?? resolveDefaultSearchMode({ projectRoot });
|
|
417
|
+
// Task 104.2 — the L3 raw tier: `--scope transcripts` searches the
|
|
418
|
+
// separate transcript-chunk index (synthetic T: ids; no tier/trust).
|
|
419
|
+
const scope = options?.scope ?? 'facts';
|
|
420
|
+
let semanticBackend;
|
|
421
|
+
if (mode === SEARCH_MODES.SEMANTIC || mode === SEARCH_MODES.HYBRID) {
|
|
422
|
+
const { prepareSemanticBackend } = await import('./semantic-backend.mjs');
|
|
423
|
+
const prep = await prepareSemanticBackend({ db, query, scope });
|
|
424
|
+
if (!prep.ok && explicitMode) {
|
|
425
|
+
console.error(
|
|
426
|
+
`cmk search: semantic backend unavailable (${prep.reason}).` +
|
|
427
|
+
(prep.hint ? `\n ${prep.hint}` : ' Use --mode=keyword.'),
|
|
428
|
+
);
|
|
429
|
+
process.exitCode = 2;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!prep.ok) {
|
|
433
|
+
console.error(
|
|
434
|
+
`cmk search: semantic default unavailable (${prep.reason}) — falling back to keyword.`,
|
|
435
|
+
);
|
|
436
|
+
mode = SEARCH_MODES.KEYWORD;
|
|
437
|
+
} else {
|
|
438
|
+
semanticBackend = prep.backend;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
290
441
|
const r = searchAction({
|
|
291
442
|
db,
|
|
292
443
|
query,
|
|
293
|
-
mode
|
|
444
|
+
mode,
|
|
445
|
+
scope,
|
|
294
446
|
minTrust: options?.minTrust,
|
|
295
447
|
tier: options?.tier,
|
|
296
448
|
since: options?.since,
|
|
297
449
|
limit: options?.limit !== undefined ? Number(options.limit) : undefined,
|
|
298
450
|
includeTombstoned: options?.includeTombstoned === true,
|
|
451
|
+
semanticBackend,
|
|
299
452
|
});
|
|
300
453
|
if (r.action === 'error') {
|
|
301
454
|
for (const e of r.errors) console.error(`cmk search: ${e}`);
|
|
@@ -311,13 +464,15 @@ function runSearch(queryParts, options) {
|
|
|
311
464
|
for (const hit of r.results) {
|
|
312
465
|
// Plain-text output suitable for terminal piping. Snippet uses
|
|
313
466
|
// FTS5's <b>...</b> markers; preserved as-is so callers can pipe
|
|
314
|
-
// to a TUI that renders them OR strip via sed.
|
|
467
|
+
// to a TUI that renders them OR strip via sed. Transcript hits carry
|
|
468
|
+
// no tier/trust (raw chunks) — the column shows the scope instead.
|
|
469
|
+
const provenance = hit.tier ? `${hit.tier}/${hit.trust}` : 'transcript';
|
|
315
470
|
console.log(
|
|
316
|
-
`${hit.id}\t${
|
|
471
|
+
`${hit.id}\t${provenance}\t${hit.source_file}:${hit.source_line}\t${hit.snippet}`,
|
|
317
472
|
);
|
|
318
473
|
}
|
|
319
474
|
console.log(
|
|
320
|
-
`\ncmk search: ${r.results.length} result(s) (mode=${r.mode})`,
|
|
475
|
+
`\ncmk search: ${r.results.length} result(s) (mode=${r.mode}${r.scope && r.scope !== 'facts' ? `, scope=${r.scope}` : ''})`,
|
|
321
476
|
);
|
|
322
477
|
} finally {
|
|
323
478
|
db.close();
|
|
@@ -576,7 +731,10 @@ export function parseFactInput(options, { readFile, readStdin } = {}) {
|
|
|
576
731
|
};
|
|
577
732
|
}
|
|
578
733
|
|
|
579
|
-
|
|
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 = {}) {
|
|
580
738
|
const projectRoot = deps.projectRoot ?? resolvePath(process.cwd());
|
|
581
739
|
const userDir =
|
|
582
740
|
deps.userDir ?? process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
|
|
@@ -637,6 +795,10 @@ export function runRemember(textParts, options, deps = {}) {
|
|
|
637
795
|
const tier = 'P';
|
|
638
796
|
const trust = options?.trust ?? 'high';
|
|
639
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 } : {}) });
|
|
640
802
|
const r = memoryWrite({
|
|
641
803
|
action: 'add',
|
|
642
804
|
text,
|
|
@@ -647,22 +809,23 @@ export function runRemember(textParts, options, deps = {}) {
|
|
|
647
809
|
source: 'user-explicit',
|
|
648
810
|
projectRoot,
|
|
649
811
|
userDir,
|
|
812
|
+
...nearDup,
|
|
650
813
|
});
|
|
651
814
|
if (r.action === 'error') {
|
|
652
815
|
for (const e of r.errors ?? [`error (${r.errorCategory})`]) {
|
|
653
|
-
|
|
816
|
+
logError(`cmk remember: ${e}`);
|
|
654
817
|
}
|
|
655
818
|
process.exitCode = 2;
|
|
656
819
|
return;
|
|
657
820
|
}
|
|
658
821
|
if (r.action === 'queued') {
|
|
659
|
-
|
|
660
|
-
`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. ` +
|
|
661
824
|
`Resolve with \`cmk queue conflicts\` (${r.path}).`,
|
|
662
825
|
);
|
|
663
826
|
return;
|
|
664
827
|
}
|
|
665
|
-
|
|
828
|
+
log(
|
|
666
829
|
`cmk remember: saved to P/MEMORY.md (${section})${r.id ? ` [${r.id}]` : ''}`,
|
|
667
830
|
);
|
|
668
831
|
}
|
|
@@ -1103,12 +1266,93 @@ async function runDoctorCli(/* options */) {
|
|
|
1103
1266
|
`Summary: ${counts.pass} pass · ${counts.fail} fail · ${counts.skip} skip (${r.duration_ms}ms)`,
|
|
1104
1267
|
);
|
|
1105
1268
|
if (counts.fail > 0) process.exitCode = 1;
|
|
1269
|
+
|
|
1270
|
+
// Task 144 (D-130): the memory-HEALTH section — content quality, not
|
|
1271
|
+
// plumbing. Informational only: read-only, never changes the exit code,
|
|
1272
|
+
// best-effort (a content-stat hiccup must not fail a healthy doctor).
|
|
1273
|
+
try {
|
|
1274
|
+
const { analyzeMemoryHealth, formatMemoryHealth } = await import('./memory-health.mjs');
|
|
1275
|
+
console.log('');
|
|
1276
|
+
console.log(formatMemoryHealth(analyzeMemoryHealth({ projectRoot })));
|
|
1277
|
+
} catch {
|
|
1278
|
+
// informational section only — stay silent on failure
|
|
1279
|
+
}
|
|
1106
1280
|
} catch (err) {
|
|
1107
1281
|
console.error(`cmk doctor: unexpected error: ${err?.message ?? err}`);
|
|
1108
1282
|
process.exitCode = 2;
|
|
1109
1283
|
}
|
|
1110
1284
|
}
|
|
1111
1285
|
|
|
1286
|
+
// Task 129 (D-121): `cmk config` — real, replacing the v0.1.0 stub. Dotted-key
|
|
1287
|
+
// get/set/--show-origin over the per-tier settings.json files. Dep-injectable
|
|
1288
|
+
// (cwd/userDir/log/logError) on the runImportClaudeMd pattern for testing the
|
|
1289
|
+
// CLI surface. The pure resolution/mutation lives in config-core.mjs.
|
|
1290
|
+
const TIER_FLAG_TO_NAME = { local: 'local', project: 'project', user: 'user' };
|
|
1291
|
+
|
|
1292
|
+
export function runConfigGet(key, options = {}) {
|
|
1293
|
+
const projectRoot = options?.cwd ?? resolvePath(process.cwd());
|
|
1294
|
+
const userDir = options?.userDir ?? join(homedir(), '.claude-memory-kit');
|
|
1295
|
+
const log = options?.log ?? console.log;
|
|
1296
|
+
const logError = options?.logError ?? console.error;
|
|
1297
|
+
const r = configGet(key, { projectRoot, userDir });
|
|
1298
|
+
if (!r.found) {
|
|
1299
|
+
logError(`cmk config get: '${key}' is not set in any tier`);
|
|
1300
|
+
process.exitCode = 2;
|
|
1301
|
+
return r;
|
|
1302
|
+
}
|
|
1303
|
+
log(typeof r.value === 'string' ? r.value : JSON.stringify(r.value));
|
|
1304
|
+
return r;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
export function runConfigSet(key, value, options = {}) {
|
|
1308
|
+
const projectRoot = options?.cwd ?? resolvePath(process.cwd());
|
|
1309
|
+
const userDir = options?.userDir ?? join(homedir(), '.claude-memory-kit');
|
|
1310
|
+
const log = options?.log ?? console.log;
|
|
1311
|
+
const logError = options?.logError ?? console.error;
|
|
1312
|
+
const tier = TIER_FLAG_TO_NAME[options?.tier ?? 'project'] ?? 'project';
|
|
1313
|
+
const r = configSet(key, value, { projectRoot, userDir, tier });
|
|
1314
|
+
if (!r.ok) {
|
|
1315
|
+
logError(`cmk config set: ${r.error}`);
|
|
1316
|
+
process.exitCode = 2;
|
|
1317
|
+
return r;
|
|
1318
|
+
}
|
|
1319
|
+
log(`cmk config set: ${key} = ${value} (${r.tier} tier)`);
|
|
1320
|
+
return r;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
export function runConfigShowOrigin(key, options = {}) {
|
|
1324
|
+
const projectRoot = options?.cwd ?? resolvePath(process.cwd());
|
|
1325
|
+
const userDir = options?.userDir ?? join(homedir(), '.claude-memory-kit');
|
|
1326
|
+
const log = options?.log ?? console.log;
|
|
1327
|
+
const logError = options?.logError ?? console.error;
|
|
1328
|
+
const r = configShowOrigin(key, { projectRoot, userDir });
|
|
1329
|
+
if (!r.found) {
|
|
1330
|
+
logError(`cmk config --show-origin: '${key}' is not set in any tier`);
|
|
1331
|
+
process.exitCode = 2;
|
|
1332
|
+
return r;
|
|
1333
|
+
}
|
|
1334
|
+
for (const e of r.entries) {
|
|
1335
|
+
const val = typeof e.value === 'string' ? `"${e.value}"` : JSON.stringify(e.value);
|
|
1336
|
+
const note = e.winner ? '' : ` (shadowed by ${e.shadowedBy})`;
|
|
1337
|
+
log(`${e.tier.padEnd(8)} ${e.path} ${val}${note}`);
|
|
1338
|
+
}
|
|
1339
|
+
return r;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// The parent `cmk config` action: handle the --show-origin flag here; the
|
|
1343
|
+
// get/set children carry their own actions (wired in the registry below).
|
|
1344
|
+
// Exported for the branch test (the no-subcommand path).
|
|
1345
|
+
export function runConfigCli(options /* , command */) {
|
|
1346
|
+
if (options?.showOrigin) {
|
|
1347
|
+
return runConfigShowOrigin(options.showOrigin, options);
|
|
1348
|
+
}
|
|
1349
|
+
const logError = options?.logError ?? console.error;
|
|
1350
|
+
logError(
|
|
1351
|
+
'cmk config: specify a subcommand — `get <key>`, `set <key> <value>`, or `--show-origin <key>`.',
|
|
1352
|
+
);
|
|
1353
|
+
process.exitCode = 2;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1112
1356
|
async function runRepairCli(options /* , command */) {
|
|
1113
1357
|
const projectRoot = resolvePath(process.cwd());
|
|
1114
1358
|
const userDir = join(homedir(), '.claude-memory-kit');
|
|
@@ -1224,6 +1468,59 @@ export async function runImportAnthropicMemory(options = {}) {
|
|
|
1224
1468
|
}
|
|
1225
1469
|
}
|
|
1226
1470
|
|
|
1471
|
+
// Task 142 (D-130): onboard from an existing rules file. Dep-injectable
|
|
1472
|
+
// (projectRoot / log / logError / importFn) on the runImportAnthropicMemory
|
|
1473
|
+
// pattern so the real CLI path is verifiable in a sandbox. `file` is the
|
|
1474
|
+
// optional positional (commander passes it first), defaulting to CLAUDE.md.
|
|
1475
|
+
export async function runImportClaudeMd(file, options = {}) {
|
|
1476
|
+
const projectRoot = options?.projectRoot ?? resolvePath(process.cwd());
|
|
1477
|
+
const log = options?.log ?? console.log;
|
|
1478
|
+
const logError = options?.logError ?? console.error;
|
|
1479
|
+
const dryRun = options?.dryRun === true;
|
|
1480
|
+
const acceptAll = options?.yes === true;
|
|
1481
|
+
const importFn = options?.importFn ?? importClaudeMd;
|
|
1482
|
+
try {
|
|
1483
|
+
const r = await importFn({ projectRoot, file, dryRun, acceptAll });
|
|
1484
|
+
if (r.action === 'error') {
|
|
1485
|
+
logError(`cmk import-claude-md: error — ${(r.errors ?? []).join('; ')}`);
|
|
1486
|
+
process.exitCode = 2;
|
|
1487
|
+
return r;
|
|
1488
|
+
}
|
|
1489
|
+
if (r.reason === 'no-source') {
|
|
1490
|
+
log(`cmk import-claude-md: no rules file found at ${r.sourcePath}`);
|
|
1491
|
+
return r;
|
|
1492
|
+
}
|
|
1493
|
+
if (r.reason) {
|
|
1494
|
+
// e.g. read-source-failed — completed-with-failure must not print the
|
|
1495
|
+
// success-shaped "applied 0" line (skill-review 2026-06-12 finding).
|
|
1496
|
+
logError(`cmk import-claude-md: ${r.reason} (${r.sourcePath})`);
|
|
1497
|
+
process.exitCode = 2;
|
|
1498
|
+
return r;
|
|
1499
|
+
}
|
|
1500
|
+
const listProposals = () => {
|
|
1501
|
+
for (const p of r.proposals) log(` + [${p.type}] L${p.line}: ${p.text}`);
|
|
1502
|
+
};
|
|
1503
|
+
if (r.mode === 'dry-run') {
|
|
1504
|
+
log(`cmk import-claude-md: dry-run — ${r.proposals.length} proposal(s), ${r.skipped} duplicate(s) skipped`);
|
|
1505
|
+
listProposals();
|
|
1506
|
+
return r;
|
|
1507
|
+
}
|
|
1508
|
+
if (r.mode === 'requires-confirmation') {
|
|
1509
|
+
log(`cmk import-claude-md: ${r.proposals.length} proposal(s) ready to apply.`);
|
|
1510
|
+
log(' Re-run with --yes to apply, or --dry-run to inspect.');
|
|
1511
|
+
listProposals();
|
|
1512
|
+
return r;
|
|
1513
|
+
}
|
|
1514
|
+
const rejectedNote = r.rejected > 0 ? `, ${r.rejected} rejected by Poison_Guard` : '';
|
|
1515
|
+
const errorNote = r.errors > 0 ? `, ${r.errors} error(s)` : '';
|
|
1516
|
+
log(`cmk import-claude-md: applied ${r.accepted} fact(s), skipped ${r.skipped} duplicate(s)${rejectedNote}${errorNote}`);
|
|
1517
|
+
return r;
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
logError(`cmk import-claude-md: unexpected error: ${err?.message ?? err}`);
|
|
1520
|
+
process.exitCode = 2;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1227
1524
|
async function runTranscriptsDispatch(childName, options) {
|
|
1228
1525
|
if (childName === 'extract') {
|
|
1229
1526
|
return runTranscriptsExtract(options);
|
|
@@ -1346,7 +1643,10 @@ async function runMcpDispatch(childName) {
|
|
|
1346
1643
|
}
|
|
1347
1644
|
return;
|
|
1348
1645
|
}
|
|
1349
|
-
|
|
1646
|
+
// A bare `cmk mcp` (no sub-verb) reaches here post-Task-129 (the parent
|
|
1647
|
+
// action is now wired) — commander passes an options object, not a string.
|
|
1648
|
+
const verb = typeof childName === 'string' ? childName : '(none)';
|
|
1649
|
+
console.error(`cmk mcp: ${NOTICE_PREFIX} (run \`cmk mcp serve\`; got sub-verb '${verb}')`);
|
|
1350
1650
|
process.exitCode = 2;
|
|
1351
1651
|
}
|
|
1352
1652
|
|
|
@@ -1357,7 +1657,10 @@ async function runQueueDispatch(childName) {
|
|
|
1357
1657
|
if (childName === 'review') {
|
|
1358
1658
|
return runQueueReview();
|
|
1359
1659
|
}
|
|
1360
|
-
|
|
1660
|
+
// A bare `cmk queue` reaches here post-Task-129 (parent action wired);
|
|
1661
|
+
// commander passes an options object, not a string sub-verb.
|
|
1662
|
+
const verb = typeof childName === 'string' ? childName : '(none)';
|
|
1663
|
+
console.log(`cmk queue: ${NOTICE_PREFIX} (run \`cmk queue review\` or \`cmk queue conflicts\`; got '${verb}')`);
|
|
1361
1664
|
process.exitCode = 2;
|
|
1362
1665
|
}
|
|
1363
1666
|
|
|
@@ -1611,6 +1914,8 @@ export const subcommands = [
|
|
|
1611
1914
|
optionSpec: [
|
|
1612
1915
|
{ flags: '--force', description: 'allow downgrade of an existing newer-version CLAUDE.md block' },
|
|
1613
1916
|
{ flags: '--no-hooks', description: 'scaffold only; do NOT wire hooks into .claude/settings.json' },
|
|
1917
|
+
{ flags: '--with-semantic', description: 'enable semantic recall: install the local embedder (~260 MB once), default search to hybrid, pre-warm the model' },
|
|
1918
|
+
{ flags: '--no-semantic', description: 'pin keyword-only search for this project (writes search.default_mode=keyword)' },
|
|
1614
1919
|
{ flags: '--verbose', description: 'show the per-tier created/skipped file breakdown' },
|
|
1615
1920
|
],
|
|
1616
1921
|
action: runInstall,
|
|
@@ -1653,7 +1958,8 @@ export const subcommands = [
|
|
|
1653
1958
|
milestone: 30,
|
|
1654
1959
|
argSpec: [{ flags: '<query...>', description: 'query terms' }],
|
|
1655
1960
|
optionSpec: [
|
|
1656
|
-
{ flags: '--mode <mode>', description: 'keyword | semantic | hybrid (default: keyword; semantic + hybrid
|
|
1961
|
+
{ flags: '--mode <mode>', description: 'keyword | semantic | hybrid (default: keyword; semantic + hybrid use the embedded Layer-5b backend — needs the optional @huggingface/transformers embedder)' },
|
|
1962
|
+
{ flags: '--scope <scope>', description: 'facts | transcripts (default: facts — curated memory; transcripts = the raw session record, the last-resort recall tier)' },
|
|
1657
1963
|
{ flags: '--min-trust <level>', description: 'low | medium | high' },
|
|
1658
1964
|
{ flags: '--tier <tier>', description: 'U | P | L (filter to a single tier)' },
|
|
1659
1965
|
{ flags: '--since <date>', description: 'ISO date — exclude observations older than this' },
|
|
@@ -1715,34 +2021,32 @@ export const subcommands = [
|
|
|
1715
2021
|
},
|
|
1716
2022
|
{
|
|
1717
2023
|
name: 'config',
|
|
1718
|
-
description: 'settings
|
|
1719
|
-
milestone:
|
|
2024
|
+
description: 'read/write kit settings (context/settings.json) without hand-editing JSON',
|
|
2025
|
+
milestone: 129,
|
|
1720
2026
|
optionSpec: [
|
|
1721
|
-
{ flags: '--show-origin <key>', description: 'print
|
|
2027
|
+
{ flags: '--show-origin <key>', description: 'print every tier that defines a setting (winner + shadowed) — the "where did this come from?" debug surface' },
|
|
1722
2028
|
],
|
|
1723
2029
|
children: [
|
|
1724
2030
|
{
|
|
1725
2031
|
name: 'get',
|
|
1726
|
-
description: 'print the resolved value of a setting',
|
|
1727
|
-
argSpec: [{ flags: '<key>', description: 'setting key (dotted path)' }],
|
|
2032
|
+
description: 'print the resolved value of a setting (dotted key; local > project > user)',
|
|
2033
|
+
argSpec: [{ flags: '<key>', description: 'setting key (dotted path, e.g. search.default_mode)' }],
|
|
2034
|
+
action: (key, options) => runConfigGet(key, options),
|
|
1728
2035
|
},
|
|
1729
2036
|
{
|
|
1730
2037
|
name: 'set',
|
|
1731
|
-
description: 'set a setting in the
|
|
2038
|
+
description: 'set a setting in the project tier (or --local)',
|
|
1732
2039
|
argSpec: [
|
|
1733
2040
|
{ flags: '<key>', description: 'setting key (dotted path)' },
|
|
1734
|
-
{ flags: '<value>', description: 'new value' },
|
|
2041
|
+
{ flags: '<value>', description: 'new value (true/false/number coerced; else string)' },
|
|
1735
2042
|
],
|
|
2043
|
+
optionSpec: [
|
|
2044
|
+
{ flags: '--local', description: 'write to the local tier (context.local/, gitignored) instead of project' },
|
|
2045
|
+
],
|
|
2046
|
+
action: (key, value, options) => runConfigSet(key, value, { tier: options?.local ? 'local' : 'project' }),
|
|
1736
2047
|
},
|
|
1737
2048
|
],
|
|
1738
|
-
action:
|
|
1739
|
-
},
|
|
1740
|
-
{
|
|
1741
|
-
name: 'view',
|
|
1742
|
-
description: 'open a local markdown viewer at 127.0.0.1:37778',
|
|
1743
|
-
milestone: 'v0.1.x',
|
|
1744
|
-
optionSpec: [{ flags: '--port <n>', description: 'override default port 37778' }],
|
|
1745
|
-
action: stub('view', 'v0.1.x'),
|
|
2049
|
+
action: runConfigCli,
|
|
1746
2050
|
},
|
|
1747
2051
|
{
|
|
1748
2052
|
name: 'import-anthropic-memory',
|
|
@@ -1754,6 +2058,19 @@ export const subcommands = [
|
|
|
1754
2058
|
],
|
|
1755
2059
|
action: runImportAnthropicMemory,
|
|
1756
2060
|
},
|
|
2061
|
+
{
|
|
2062
|
+
name: 'import-claude-md',
|
|
2063
|
+
description: 'onboard from an existing rules file (CLAUDE.md / .cursorrules / AGENTS.md) — parse it into typed facts through the safe write path',
|
|
2064
|
+
milestone: 142,
|
|
2065
|
+
argSpec: [
|
|
2066
|
+
{ flags: '[file]', description: 'rules file to import, relative to the project root (default: CLAUDE.md)' },
|
|
2067
|
+
],
|
|
2068
|
+
optionSpec: [
|
|
2069
|
+
{ flags: '--dry-run', description: 'preview the typed proposals without modifying files' },
|
|
2070
|
+
{ flags: '--yes', description: 'apply every proposal without prompting (apply requires explicit --yes)' },
|
|
2071
|
+
],
|
|
2072
|
+
action: runImportClaudeMd,
|
|
2073
|
+
},
|
|
1757
2074
|
{
|
|
1758
2075
|
name: 'transcripts',
|
|
1759
2076
|
description: "extract clean markdown transcripts from Claude Code session jsonls under ~/.claude/projects/",
|