@lh8ppl/claude-memory-kit 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/bin/cmk-auto-extract.mjs +62 -0
- package/bin/cmk-capture-prompt.mjs +65 -0
- package/bin/cmk-capture-turn.mjs +76 -0
- package/bin/cmk-compress-lazy.mjs +0 -0
- package/bin/cmk-compress-session.mjs +64 -0
- package/bin/cmk-daily-distill.mjs +0 -0
- package/bin/cmk-inject-context.mjs +69 -0
- package/bin/cmk-observe-edit.mjs +57 -0
- package/bin/cmk-weekly-curate.mjs +0 -0
- package/bin/cmk.mjs +11 -11
- package/package.json +10 -2
- package/src/audit-log.mjs +1 -0
- package/src/claude-md.mjs +212 -212
- package/src/compressor.mjs +18 -18
- package/src/doctor.mjs +21 -8
- package/src/frontmatter.mjs +73 -73
- package/src/index-rebuild.mjs +26 -4
- package/src/inject-context.mjs +150 -10
- package/src/install.mjs +49 -1
- package/src/mcp-server.mjs +17 -0
- package/src/memory-write.mjs +18 -5
- package/src/merge-facts.mjs +213 -213
- package/src/provenance.mjs +217 -217
- package/src/reindex.mjs +134 -134
- package/src/repair.mjs +26 -96
- package/src/sanitize.mjs +39 -0
- package/src/settings-hooks.mjs +186 -0
- package/src/spawn-bin.mjs +83 -0
- package/src/subcommands.mjs +144 -10
- package/src/write-fact.mjs +46 -3
- package/template/.gitignore.fragment +12 -12
- package/template/CLAUDE.md.template +53 -49
- package/template/docs/journey/journey-log.md.template +292 -292
- package/template/project/memory/INDEX.md.template +47 -47
- package/template/support/cron-jobs/daily-memory-distill.md +15 -15
- package/template/support/cron-jobs/nightly-memsearch-index.md +17 -17
- package/template/support/cron-jobs/weekly-memory-curator.md +15 -15
- package/template/support/milvus-deploy/README.md +57 -57
- package/template/support/milvus-deploy/docker-compose.yml +66 -66
- package/template/support/scripts/auto-extract-memory.sh +102 -102
- package/template/support/scripts/memsearch-index-with-flush.sh +59 -59
- package/template/support/scripts/refresh-distill-timestamp.py +35 -35
- package/template/support/scripts/register-crons.py +242 -242
- package/template/support/scripts/run-daily-distill.sh +67 -67
- package/template/support/scripts/run-weekly-curate.sh +58 -58
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// spawn-bin.mjs — cross-platform subprocess spawning that AVOIDS the
|
|
2
|
+
// `shell:true` + args-array combination (self-test finding #4).
|
|
3
|
+
//
|
|
4
|
+
// Why this exists
|
|
5
|
+
// ---------------
|
|
6
|
+
// Spawning an npm-global bin (claude, memsearch, cmk-*) needs `shell:true` on
|
|
7
|
+
// Windows so the `.cmd` shim resolves through cmd.exe — Node won't auto-resolve
|
|
8
|
+
// `.cmd`/`.bat` without a shell (CVE-2024-27980 hardening). But `shell:true`
|
|
9
|
+
// WITH an args array is doubly bad:
|
|
10
|
+
// 1. Node emits DEP0190 ("passing args to a child process with shell:true …
|
|
11
|
+
// arguments are not escaped, only concatenated").
|
|
12
|
+
// 2. The args ARE concatenated unescaped, so a path containing a space
|
|
13
|
+
// (e.g. `--mcp-config C:\Users\First Last\…\empty-mcp.json` under tmpdir)
|
|
14
|
+
// is re-tokenized by cmd.exe and breaks parsing — silently failing
|
|
15
|
+
// auto-extract/compression for any Windows user whose profile has a space.
|
|
16
|
+
//
|
|
17
|
+
// The fix: never pass an args array together with shell:true.
|
|
18
|
+
// - POSIX: spawn(bin, args, {shell:false}) — Node resolves PATH directly and
|
|
19
|
+
// passes argv safely. No shell, no concatenation.
|
|
20
|
+
// - Windows: shell:true with a SINGLE pre-quoted command string (no args
|
|
21
|
+
// array) — clears DEP0190 and lets us quote each arg so spaces survive.
|
|
22
|
+
//
|
|
23
|
+
// The kit's spawn args are controlled (flags + filesystem paths, never
|
|
24
|
+
// user-supplied shell text), so cmd.exe double-quoting is sufficient: inside
|
|
25
|
+
// double-quotes cmd.exe treats &|<>^ as literal; `%` is the only residual
|
|
26
|
+
// special char and kit paths/flags never contain it.
|
|
27
|
+
|
|
28
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Quote one argument for a cmd.exe command line. Quotes args that contain a
|
|
32
|
+
* space or a double-quote, AND empty-string args (an unquoted empty arg would
|
|
33
|
+
* vanish and shift the next token into its value slot — the compressor passes
|
|
34
|
+
* `--allowed-tools ''`). Embedded double-quotes are doubled.
|
|
35
|
+
*/
|
|
36
|
+
function quoteWinArg(s) {
|
|
37
|
+
const str = String(s);
|
|
38
|
+
if (str === '' || /[\s"]/.test(str)) {
|
|
39
|
+
// Double embedded double-quotes, AND double any trailing backslash run so
|
|
40
|
+
// a quoted value ending in `\` (e.g. a directory path with a space) does
|
|
41
|
+
// NOT escape the closing quote — the classic Windows CommandLineToArgv /
|
|
42
|
+
// cmd.exe footgun (`"C:\dir\"` parses as `C:\dir"`).
|
|
43
|
+
const escaped = str.replace(/"/g, '""').replace(/(\\+)$/, '$1$1');
|
|
44
|
+
return `"${escaped}"`;
|
|
45
|
+
}
|
|
46
|
+
return str;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the single cmd.exe command string for a Windows `shell:true` spawn.
|
|
51
|
+
* Exported for direct unit testing of the quoting (platform-independent).
|
|
52
|
+
*/
|
|
53
|
+
export function winCommandLine(bin, args = []) {
|
|
54
|
+
return [bin, ...args].map(quoteWinArg).join(' ');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Spawn a bin cross-platform without ever pairing `shell:true` with an args
|
|
59
|
+
* array. `deps` allows tests to inject a recording spawn + force a platform:
|
|
60
|
+
* deps.spawnImpl — defaults to node:child_process spawn (compressor injects
|
|
61
|
+
* its own for the kill-chain / testability).
|
|
62
|
+
* deps.platform — defaults to process.platform.
|
|
63
|
+
* Returns whatever the spawn impl returns (a ChildProcess in production).
|
|
64
|
+
*/
|
|
65
|
+
export function spawnBin(bin, args = [], opts = {}, deps = {}) {
|
|
66
|
+
const { spawnImpl = spawn, platform = process.platform } = deps;
|
|
67
|
+
// spawn-discipline: ignore pass-through helper — timeout/kill is the
|
|
68
|
+
// caller's contract (compressor terminateSubprocess + setTimeout; doctor
|
|
69
|
+
// timeout:3500), not this wrapper's.
|
|
70
|
+
if (platform === 'win32') {
|
|
71
|
+
return spawnImpl(winCommandLine(bin, args), { ...opts, shell: true });
|
|
72
|
+
}
|
|
73
|
+
return spawnImpl(bin, args, { ...opts, shell: false });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Synchronous twin of spawnBin (for one-shot checks like `cmk doctor`'s memsearch probe). */
|
|
77
|
+
export function spawnBinSync(bin, args = [], opts = {}, deps = {}) {
|
|
78
|
+
const { spawnImpl = spawnSync, platform = process.platform } = deps;
|
|
79
|
+
if (platform === 'win32') {
|
|
80
|
+
return spawnImpl(winCommandLine(bin, args), { ...opts, shell: true });
|
|
81
|
+
}
|
|
82
|
+
return spawnImpl(bin, args, { ...opts, shell: false });
|
|
83
|
+
}
|
package/src/subcommands.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { reindex as reindexAction } from './reindex.mjs';
|
|
|
19
19
|
import { openIndexDb } from './index-db.mjs';
|
|
20
20
|
import { reindexBoot, reindexFull } from './index-rebuild.mjs';
|
|
21
21
|
import { search as searchAction, SEARCH_MODES } from './search.mjs';
|
|
22
|
+
import { memoryWrite } from './memory-write.mjs';
|
|
22
23
|
import { runMcpServer } from './mcp-server.mjs';
|
|
23
24
|
import { dailyDistill } from './daily-distill.mjs';
|
|
24
25
|
import { weeklyCurate } from './weekly-curate.mjs';
|
|
@@ -50,7 +51,7 @@ import { overrideTrust as overrideTrustAction } from './trust.mjs';
|
|
|
50
51
|
import { resolveConflictQueue, mergeScratchpadBullets } from './conflict-queue.mjs';
|
|
51
52
|
import { resolveReviewQueue } from './review-queue.mjs';
|
|
52
53
|
import { createInterface } from 'node:readline';
|
|
53
|
-
import { resolve as resolvePath, join } from 'node:path';
|
|
54
|
+
import { resolve as resolvePath, join, basename } from 'node:path';
|
|
54
55
|
|
|
55
56
|
const NOTICE_PREFIX = 'not yet implemented in v0.1.0';
|
|
56
57
|
|
|
@@ -62,14 +63,53 @@ const NOTICE_PREFIX = 'not yet implemented in v0.1.0';
|
|
|
62
63
|
* replaced / upgraded / downgrade-blocked / forced-downgrade / unchanged).
|
|
63
64
|
*/
|
|
64
65
|
async function runInstall(options /* , command */) {
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
// commander maps `--no-hooks` to options.hooks === false.
|
|
67
|
+
const noHooks = !!(options && options.hooks === false);
|
|
68
|
+
const verbose = !!(options && options.verbose);
|
|
69
|
+
const result = await installAction({ force: !!(options && options.force), noHooks });
|
|
70
|
+
|
|
71
|
+
// Outcome over inventory (self-test UX finding): state the resulting state +
|
|
72
|
+
// next action, not a file tally. The old "scaffolded 5, skipped 4 existing"
|
|
73
|
+
// read like a problem on a FRESH folder — the "skipped" are the cross-project
|
|
74
|
+
// user tier at ~/.claude-memory-kit/ (OUTSIDE this folder), already on disk.
|
|
75
|
+
// The full per-tier breakdown is --verbose only.
|
|
76
|
+
const projectName = basename(resolvePath(process.cwd()));
|
|
77
|
+
const wired =
|
|
78
|
+
result.hooks.action === 'wired' || result.hooks.action === 'unchanged';
|
|
79
|
+
const broughtSomethingNew =
|
|
80
|
+
result.created.length > 0 ||
|
|
81
|
+
result.gitignore.action === 'created' ||
|
|
82
|
+
result.claudeMd.action === 'created';
|
|
83
|
+
|
|
84
|
+
if (broughtSomethingNew) {
|
|
85
|
+
console.log(
|
|
86
|
+
`cmk install: ${projectName} ready — context/ scaffolded${
|
|
87
|
+
wired ? ', hooks wired' : ''
|
|
88
|
+
}.`,
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
console.log(
|
|
92
|
+
`cmk install: ${projectName} already set up (your edits preserved)${
|
|
93
|
+
wired ? ', hooks refreshed' : ''
|
|
94
|
+
}.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (wired) {
|
|
98
|
+
console.log(
|
|
99
|
+
' Restart Claude Code to activate. Complete install — no separate /plugin step needed.',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (verbose) {
|
|
103
|
+
console.log(
|
|
104
|
+
` files: ${result.created.length} created, ${result.skipped.length} already present` +
|
|
105
|
+
(result.skipped.length
|
|
106
|
+
? ' (incl. the cross-project user tier at ~/.claude-memory-kit/, outside this folder)'
|
|
107
|
+
: ''),
|
|
108
|
+
);
|
|
109
|
+
console.log(
|
|
110
|
+
` .gitignore=${result.gitignore.action} · CLAUDE.md=${result.claudeMd.action} · hooks=${result.hooks.action}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
73
113
|
|
|
74
114
|
if (result.claudeMd.action === 'downgrade-blocked') {
|
|
75
115
|
console.error(
|
|
@@ -168,9 +208,28 @@ function runTrust(id, level /* , options, command */) {
|
|
|
168
208
|
*/
|
|
169
209
|
function runSearch(queryParts, options) {
|
|
170
210
|
const projectRoot = resolvePath(process.cwd());
|
|
211
|
+
const userDir =
|
|
212
|
+
process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
|
|
171
213
|
const query = Array.isArray(queryParts) ? queryParts.join(' ') : queryParts;
|
|
172
214
|
const db = openIndexDb({ projectRoot });
|
|
173
215
|
try {
|
|
216
|
+
// Refresh the index before querying. On a fresh install the FTS5 index
|
|
217
|
+
// is empty (auto-extract writes facts to MEMORY.md but doesn't reindex,
|
|
218
|
+
// and the runtime chokidar watcher isn't running for a one-shot CLI
|
|
219
|
+
// call), so without this `cmk search` returns "no results" for facts
|
|
220
|
+
// that are sitting right there in the scratchpads (self-test finding
|
|
221
|
+
// #0). reindexBoot is incremental — mtime/sha1 diff, only changed files
|
|
222
|
+
// — so it's cheap to run on every search. Degrade gracefully: a reindex
|
|
223
|
+
// failure falls back to whatever's already indexed rather than crashing
|
|
224
|
+
// the query.
|
|
225
|
+
try {
|
|
226
|
+
reindexBoot({ projectRoot, userDir, db });
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error(
|
|
229
|
+
`cmk search: index refresh failed (${err?.message ?? err}); ` +
|
|
230
|
+
'searching the existing index. Run `cmk reindex --full` if results look stale.',
|
|
231
|
+
);
|
|
232
|
+
}
|
|
174
233
|
const r = searchAction({
|
|
175
234
|
db,
|
|
176
235
|
query,
|
|
@@ -225,6 +284,67 @@ function runSearch(queryParts, options) {
|
|
|
225
284
|
* (b) keeping it always-current avoids users having to think about which
|
|
226
285
|
* index to rebuild when.
|
|
227
286
|
*/
|
|
287
|
+
/**
|
|
288
|
+
* `cmk remember <text...>` — explicit durable capture (write-path fix #0b).
|
|
289
|
+
*
|
|
290
|
+
* Writes a provenance-tracked bullet to MEMORY.md (the session-start-recalled
|
|
291
|
+
* layer) through the SAME hardened path as auto-extract: Poison_Guard +
|
|
292
|
+
* home-path abstraction (#1) + conflict detection + dedup. This is the entry
|
|
293
|
+
* the scaffolded CLAUDE.md points the agent at INSTEAD of freehand-writing
|
|
294
|
+
* fact files — which produced wrong-schema, unindexable, username-leaking
|
|
295
|
+
* files in the self-test. Guaranteed-correct because it never touches raw
|
|
296
|
+
* frontmatter.
|
|
297
|
+
*
|
|
298
|
+
* Tier: v0.1.0 writes tier P (project MEMORY.md). U/L need per-tier scratchpad
|
|
299
|
+
* routing (same deferral as mk_remember, design §16) — the always-on home-path
|
|
300
|
+
* abstraction is the privacy net regardless of tier.
|
|
301
|
+
*/
|
|
302
|
+
function runRemember(textParts, options) {
|
|
303
|
+
const projectRoot = resolvePath(process.cwd());
|
|
304
|
+
const userDir =
|
|
305
|
+
process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
|
|
306
|
+
const text = Array.isArray(textParts) ? textParts.join(' ') : textParts;
|
|
307
|
+
const tier = options?.tier ?? 'P';
|
|
308
|
+
if (tier !== 'P') {
|
|
309
|
+
console.error(
|
|
310
|
+
`cmk remember: tier '${tier}' not yet supported — v0.1.0 writes the project tier (P). ` +
|
|
311
|
+
'For machine-only config, edit context.local/machine-paths.md directly (v0.1.x will add --tier routing).',
|
|
312
|
+
);
|
|
313
|
+
process.exitCode = 2;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const trust = options?.trust ?? 'high';
|
|
317
|
+
const section = options?.section ?? 'Active Threads';
|
|
318
|
+
const r = memoryWrite({
|
|
319
|
+
action: 'add',
|
|
320
|
+
text,
|
|
321
|
+
tier,
|
|
322
|
+
scratchpad: 'MEMORY.md',
|
|
323
|
+
section,
|
|
324
|
+
trust,
|
|
325
|
+
source: 'user-explicit',
|
|
326
|
+
projectRoot,
|
|
327
|
+
userDir,
|
|
328
|
+
});
|
|
329
|
+
if (r.action === 'error') {
|
|
330
|
+
for (const e of r.errors ?? [`error (${r.errorCategory})`]) {
|
|
331
|
+
console.error(`cmk remember: ${e}`);
|
|
332
|
+
}
|
|
333
|
+
process.exitCode = 2;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (r.action === 'queued') {
|
|
337
|
+
console.log(
|
|
338
|
+
`cmk remember: queued for review — a higher-trust fact already covers this. ` +
|
|
339
|
+
`Resolve with \`cmk queue conflicts\` (${r.path}).`,
|
|
340
|
+
);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
console.log(
|
|
344
|
+
`cmk remember: saved to P/MEMORY.md (${section})${r.id ? ` [${r.id}]` : ''}`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
228
348
|
function runReindex(options /* , command */) {
|
|
229
349
|
const projectRoot = resolvePath(process.cwd());
|
|
230
350
|
const userDir = join(homedir(), '.claude-memory-kit');
|
|
@@ -1007,10 +1127,12 @@ function stub(name, milestone, extra) {
|
|
|
1007
1127
|
export const subcommands = [
|
|
1008
1128
|
{
|
|
1009
1129
|
name: 'install',
|
|
1010
|
-
description: 'cross-OS one-shot install — scaffold 3-tier dirs + inject .gitignore + drop kit CLAUDE.md block',
|
|
1130
|
+
description: 'cross-OS one-shot install — scaffold 3-tier dirs + inject .gitignore + drop kit CLAUDE.md block + wire Claude Code hooks',
|
|
1011
1131
|
milestone: 3,
|
|
1012
1132
|
optionSpec: [
|
|
1013
1133
|
{ flags: '--force', description: 'allow downgrade of an existing newer-version CLAUDE.md block' },
|
|
1134
|
+
{ flags: '--no-hooks', description: 'scaffold only; do NOT wire hooks into .claude/settings.json' },
|
|
1135
|
+
{ flags: '--verbose', description: 'show the per-tier created/skipped file breakdown' },
|
|
1014
1136
|
],
|
|
1015
1137
|
action: runInstall,
|
|
1016
1138
|
},
|
|
@@ -1026,6 +1148,18 @@ export const subcommands = [
|
|
|
1026
1148
|
milestone: 14,
|
|
1027
1149
|
action: runInitUserTier,
|
|
1028
1150
|
},
|
|
1151
|
+
{
|
|
1152
|
+
name: 'remember',
|
|
1153
|
+
description: 'explicitly capture a durable fact to MEMORY.md (Poison_Guard + home-path abstraction + dedup; the safe alternative to hand-writing fact files)',
|
|
1154
|
+
milestone: 24,
|
|
1155
|
+
argSpec: [{ flags: '<text...>', description: 'the fact to remember' }],
|
|
1156
|
+
optionSpec: [
|
|
1157
|
+
{ flags: '--tier <tier>', description: 'P (default; U/L are v0.1.x)' },
|
|
1158
|
+
{ flags: '--trust <level>', description: 'high | medium | low (default: high)' },
|
|
1159
|
+
{ flags: '--section <name>', description: 'MEMORY.md section (default: Active Threads)' },
|
|
1160
|
+
],
|
|
1161
|
+
action: runRemember,
|
|
1162
|
+
},
|
|
1029
1163
|
{
|
|
1030
1164
|
name: 'search',
|
|
1031
1165
|
description: 'search memory — hybrid keyword + optional semantic',
|
package/src/write-fact.mjs
CHANGED
|
@@ -19,6 +19,8 @@ import { VALID_TIERS, resolveTierRoot, resolveFactDir } from './tier-paths.mjs';
|
|
|
19
19
|
import { parse, format } from './frontmatter.mjs';
|
|
20
20
|
import { appendAuditEntry, nowIso, REASON_CODES } from './audit-log.mjs';
|
|
21
21
|
import { ERROR_CATEGORIES, errorResult } from './result-shapes.mjs';
|
|
22
|
+
import { sanitizeHomePaths } from './sanitize.mjs';
|
|
23
|
+
import { checkPoisonGuard, logPoisonGuardRejection } from './poison-guard.mjs';
|
|
22
24
|
|
|
23
25
|
const VALID_TYPES = new Set(['user', 'feedback', 'project', 'reference']);
|
|
24
26
|
const VALID_WRITE_SOURCES = new Set([
|
|
@@ -148,7 +150,48 @@ export function writeFact(opts = {}) {
|
|
|
148
150
|
});
|
|
149
151
|
}
|
|
150
152
|
|
|
151
|
-
|
|
153
|
+
// Privacy (write-path fix #1): abstract absolute home-dir paths to `~` in
|
|
154
|
+
// committed/shared tiers (P/U) so a fact never ships the local username
|
|
155
|
+
// and stays portable. Local tier (L) keeps machine-specific paths verbatim
|
|
156
|
+
// — that's its purpose. The id hashes the SANITIZED body, so dedup keys on
|
|
157
|
+
// what actually lands on disk.
|
|
158
|
+
let { body, title } = opts;
|
|
159
|
+
if (opts.tier === 'P' || opts.tier === 'U') {
|
|
160
|
+
body = sanitizeHomePaths(body);
|
|
161
|
+
title = sanitizeHomePaths(title);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Poison_Guard (write-path fix #1): fact files previously bypassed the
|
|
165
|
+
// secret/poison screen that scratchpad writes get via memoryWrite. Screen
|
|
166
|
+
// the (sanitized) body before any disk write; a rejection logs the redacted
|
|
167
|
+
// excerpt to .locks/poison-guard.log and returns a poison_guard error.
|
|
168
|
+
const guard = checkPoisonGuard(body);
|
|
169
|
+
if (guard.rejected) {
|
|
170
|
+
// Best-effort log; guard on projectRoot so a U-tier write with no
|
|
171
|
+
// project context can't turn a clean rejection into a crash.
|
|
172
|
+
if (guard.pattern_id !== 'schema' && opts.projectRoot) {
|
|
173
|
+
logPoisonGuardRejection({
|
|
174
|
+
projectRoot: opts.projectRoot,
|
|
175
|
+
ts: opts.createdAt ?? nowIso(),
|
|
176
|
+
pattern_id: guard.pattern_id,
|
|
177
|
+
source_file: `write-fact:${opts.type}_${opts.slug}`,
|
|
178
|
+
source_line: 1,
|
|
179
|
+
redacted_excerpt: guard.redacted_excerpt,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return errorResult({
|
|
183
|
+
category: ERROR_CATEGORIES.POISON_GUARD,
|
|
184
|
+
errors: [`Poison_Guard rejected write: pattern_id=${guard.pattern_id}`],
|
|
185
|
+
pattern_id: guard.pattern_id,
|
|
186
|
+
redacted_excerpt: guard.redacted_excerpt,
|
|
187
|
+
id: null,
|
|
188
|
+
path: null,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Use the sanitized body/title for id, frontmatter, and the file body.
|
|
193
|
+
const factOpts = { ...opts, body, title };
|
|
194
|
+
const id = opts.id ?? generateId(opts.tier, body);
|
|
152
195
|
const createdAt = opts.createdAt ?? nowIso();
|
|
153
196
|
const tierRoot = resolveTierRoot(opts);
|
|
154
197
|
const factDir = resolveFactDir(opts.tier, tierRoot);
|
|
@@ -198,8 +241,8 @@ export function writeFact(opts = {}) {
|
|
|
198
241
|
}
|
|
199
242
|
|
|
200
243
|
mkdirSync(factDir, { recursive: true });
|
|
201
|
-
const frontmatter = buildFrontmatterObject(
|
|
202
|
-
writeFileSync(path, format({ frontmatter, body: `\n${
|
|
244
|
+
const frontmatter = buildFrontmatterObject(factOpts, { id, createdAt });
|
|
245
|
+
writeFileSync(path, format({ frontmatter, body: `\n${factOpts.body}\n` }), 'utf8');
|
|
203
246
|
|
|
204
247
|
return { action: 'created', id, path };
|
|
205
248
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
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
|
-
# Local-tier (per-machine, never committed)
|
|
6
|
-
context.local/
|
|
7
|
-
|
|
8
|
-
# SQLite + FTS5 read cache (regenerable from markdown)
|
|
9
|
-
context/.index/
|
|
10
|
-
|
|
11
|
-
# Run-time locks + transient state
|
|
12
|
-
context/.locks/
|
|
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
|
+
# Local-tier (per-machine, never committed)
|
|
6
|
+
context.local/
|
|
7
|
+
|
|
8
|
+
# SQLite + FTS5 read cache (regenerable from markdown)
|
|
9
|
+
context/.index/
|
|
10
|
+
|
|
11
|
+
# Run-time locks + transient state
|
|
12
|
+
context/.locks/
|
|
@@ -1,49 +1,53 @@
|
|
|
1
|
-
## Memory System — claude-memory-kit
|
|
2
|
-
|
|
3
|
-
This project uses **claude-memory-kit** for per-project, in-repo memory that survives session boundaries. Memory lives in `context/` (committed) and `context.local/` (gitignored). Cross-project memory lives at `~/.claude-memory-kit/` (or `$MEMORY_KIT_USER_DIR`).
|
|
4
|
-
|
|
5
|
-
> v0.1.0 is under active development. This block is the runtime contract. Specific mechanisms (auto-extract, memory-write skill, MCP search) come online incrementally — `cmk doctor` will tell you which layers are active in the current install. Full architecture: <https://github.com/LH8PPL/claude-memory-kit/blob/main/docs/journey/v0.1.0-build-log.md>
|
|
6
|
-
|
|
7
|
-
### Where memory lives
|
|
8
|
-
|
|
9
|
-
| Tier | Path | Travels with `git clone`? |
|
|
10
|
-
| --- | --- | --- |
|
|
11
|
-
| **User** | `~/.claude-memory-kit/` | No — machine-local, cross-project |
|
|
12
|
-
| **Project** | `<repo>/context/` | **Yes** — committed |
|
|
13
|
-
| **Local** | `<repo>/context.local/` | No — gitignored, per-machine |
|
|
14
|
-
|
|
15
|
-
Precedence at session start: local > project > user (most-specific wins, others are logged as shadowed).
|
|
16
|
-
|
|
17
|
-
### How memory works
|
|
18
|
-
|
|
19
|
-
- **Session start** — the kit injects a frozen snapshot (≤10 KB) from the three tiers into Claude's context. Loaded once, never mutated mid-session — preserves the prefix cache.
|
|
20
|
-
- **During the session** — durable facts get captured automatically by the Stop hook + `memory-write` skill (once Layer 4 is live). The user can also explicitly say "remember this", "from now on", "we decided X".
|
|
21
|
-
- **End of session** — the rolling-window pipeline compresses `sessions/now.md` into a daily summary. Cron jobs distill into `recent.md` and `archive.md` over time (Layer 6, optional).
|
|
22
|
-
|
|
23
|
-
### Health checks (when `cmk doctor` is live)
|
|
24
|
-
|
|
25
|
-
Health checks (HC-1..HC-8) verify each layer is wired correctly: install integrity, hook registration, transcript capture freshness, INDEX accuracy, cron registration, semantic search backend, native Auto Memory coexistence. See [`docs/adr/`](docs/adr/) and [`specs/v0.1.0/design.md`](specs/v0.1.0/design.md) for the full contract.
|
|
26
|
-
|
|
27
|
-
### Memory write rules (for Claude)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
1. **
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
## Memory System — claude-memory-kit
|
|
2
|
+
|
|
3
|
+
This project uses **claude-memory-kit** for per-project, in-repo memory that survives session boundaries. Memory lives in `context/` (committed) and `context.local/` (gitignored). Cross-project memory lives at `~/.claude-memory-kit/` (or `$MEMORY_KIT_USER_DIR`).
|
|
4
|
+
|
|
5
|
+
> v0.1.0 is under active development. This block is the runtime contract. Specific mechanisms (auto-extract, memory-write skill, MCP search) come online incrementally — `cmk doctor` will tell you which layers are active in the current install. Full architecture: <https://github.com/LH8PPL/claude-memory-kit/blob/main/docs/journey/v0.1.0-build-log.md>
|
|
6
|
+
|
|
7
|
+
### Where memory lives
|
|
8
|
+
|
|
9
|
+
| Tier | Path | Travels with `git clone`? |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| **User** | `~/.claude-memory-kit/` | No — machine-local, cross-project |
|
|
12
|
+
| **Project** | `<repo>/context/` | **Yes** — committed |
|
|
13
|
+
| **Local** | `<repo>/context.local/` | No — gitignored, per-machine |
|
|
14
|
+
|
|
15
|
+
Precedence at session start: local > project > user (most-specific wins, others are logged as shadowed).
|
|
16
|
+
|
|
17
|
+
### How memory works
|
|
18
|
+
|
|
19
|
+
- **Session start** — the kit injects a frozen snapshot (≤10 KB) from the three tiers into Claude's context. Loaded once, never mutated mid-session — preserves the prefix cache.
|
|
20
|
+
- **During the session** — durable facts get captured automatically by the Stop hook + `memory-write` skill (once Layer 4 is live). The user can also explicitly say "remember this", "from now on", "we decided X".
|
|
21
|
+
- **End of session** — the rolling-window pipeline compresses `sessions/now.md` into a daily summary. Cron jobs distill into `recent.md` and `archive.md` over time (Layer 6, optional).
|
|
22
|
+
|
|
23
|
+
### Health checks (when `cmk doctor` is live)
|
|
24
|
+
|
|
25
|
+
Health checks (HC-1..HC-8) verify each layer is wired correctly: install integrity, hook registration, transcript capture freshness, INDEX accuracy, cron registration, semantic search backend, native Auto Memory coexistence. See [`docs/adr/`](docs/adr/) and [`specs/v0.1.0/design.md`](specs/v0.1.0/design.md) for the full contract.
|
|
26
|
+
|
|
27
|
+
### Memory write rules (for Claude)
|
|
28
|
+
|
|
29
|
+
Most capture is automatic — the Stop hook extracts durable facts each turn, no action needed. When you want to capture something **explicitly**:
|
|
30
|
+
|
|
31
|
+
1. **Use `cmk remember "<the fact>"`** — do NOT hand-write files under `context/memory/`. The command routes through the kit's safe write path: it screens for secrets (Poison_Guard), abstracts machine-specific home paths to `~` (so a committed fact never leaks the local username), dedups, and writes the correct schema. Hand-writing fact files bypasses all of that and produces files the index can't read.
|
|
32
|
+
```bash
|
|
33
|
+
cmk remember "We deploy with Kamal to Hetzner; never to Vercel."
|
|
34
|
+
cmk remember "Lior prefers terse responses, no preamble." --trust high
|
|
35
|
+
```
|
|
36
|
+
2. **Machine-specific config** (absolute tool paths that only make sense on this machine) → put it in `context.local/machine-paths.md` (gitignored, never committed). `cmk remember` writes the committed project tier; for machine-only paths, edit the local tier directly.
|
|
37
|
+
3. **Cross-project lesson** (true on every project, not just this one) → `cmk lessons promote <id>` moves a project fact to the user tier (`~/.claude-memory-kit/`).
|
|
38
|
+
4. **Confirm silently.** Don't announce captures. Frozen-snapshot semantics mean a write takes effect next session.
|
|
39
|
+
|
|
40
|
+
### Privacy
|
|
41
|
+
|
|
42
|
+
- Anything inside `<private>...</private>` tags in a user prompt is stripped before any disk write — never persisted in any form.
|
|
43
|
+
- `cmk remember` (and auto-extract) abstract absolute home-dir paths (`C:\Users\you\…`, `/home/you/…`, `/Users/you/…`) to `~` before writing to a committed/shared tier, so a fact never ships your username and stays portable across machines. Genuinely machine-specific paths belong in `context.local/` (gitignored).
|
|
44
|
+
|
|
45
|
+
### Uninstall / remove this block
|
|
46
|
+
|
|
47
|
+
This block is managed by `cmk install`. To remove it cleanly:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cmk uninstall
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Everything outside the `claude-memory-kit:start` / `:end` markers is byte-preserved.
|