@lh8ppl/claude-memory-kit 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/bin/cmk-auto-extract.mjs +13 -0
- package/bin/cmk-capture-prompt.mjs +0 -0
- package/bin/cmk-capture-turn.mjs +0 -0
- package/bin/cmk-compress-session.mjs +31 -17
- package/bin/cmk-inject-context.mjs +12 -2
- package/bin/cmk-observe-edit.mjs +0 -0
- package/bin/cmk-weekly-curate.mjs +14 -2
- package/package.json +3 -2
- package/src/audit-log.mjs +6 -0
- package/src/auto-drain.mjs +59 -0
- package/src/auto-extract.mjs +117 -6
- package/src/auto-persona.mjs +544 -0
- package/src/bullet-lookup.mjs +59 -0
- package/src/capture-turn.mjs +54 -0
- package/src/compress-session.mjs +6 -8
- package/src/compressor.mjs +37 -22
- package/src/conflict-queue.mjs +8 -1
- package/src/daily-distill.mjs +19 -11
- package/src/doctor.mjs +79 -26
- package/src/forget.mjs +14 -0
- package/src/graduate-session.mjs +65 -0
- package/src/graduation.mjs +179 -0
- package/src/index-rebuild.mjs +26 -4
- package/src/inject-context.mjs +352 -65
- package/src/install.mjs +52 -7
- package/src/lessons-promote.mjs +137 -0
- package/src/mcp-server.mjs +17 -0
- package/src/memory-write.mjs +20 -7
- package/src/native-memory.mjs +98 -0
- package/src/persona-portability.mjs +253 -0
- package/src/provenance.mjs +23 -5
- package/src/read-hook-stdin.mjs +47 -0
- package/src/register-crons.mjs +17 -8
- package/src/sanitize.mjs +39 -0
- package/src/scratchpad.mjs +247 -19
- package/src/session-end-tasks.mjs +127 -0
- package/src/settings-hooks.mjs +33 -3
- package/src/spawn-bin.mjs +83 -0
- package/src/subcommands.mjs +472 -26
- package/src/weekly-curate.mjs +53 -6
- package/src/write-fact.mjs +60 -3
- package/template/.claude/skills/memory-write/SKILL.md +47 -88
- package/template/.gitignore.fragment +6 -0
- package/template/CLAUDE.md.template +17 -7
- package/template/local/machine-paths.md.template +1 -12
- package/template/local/overrides.md.template +1 -11
- package/template/project/MEMORY.md.template +5 -26
- package/template/project/SOUL.md.template +1 -10
- package/template/user/fragments/INDEX.md.template +1 -1
- package/template/.claude/hooks/pre-tool-memory.js +0 -78
- package/template/.claude/hooks/transcript-capture.js +0 -69
- package/template/.claude/settings.json +0 -27
- package/template/support/scripts/auto-extract-memory.sh +0 -102
- package/template/support/scripts/refresh-distill-timestamp.py +0 -35
- package/template/support/scripts/register-crons.py +0 -242
- package/template/support/scripts/run-daily-distill.sh +0 -67
- package/template/support/scripts/run-weekly-curate.sh +0 -58
package/src/subcommands.mjs
CHANGED
|
@@ -19,15 +19,22 @@ 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';
|
|
26
|
+
import { autoPersona } from './auto-persona.mjs';
|
|
27
|
+
import { exportPersona, importPersona } from './persona-portability.mjs';
|
|
28
|
+
import { setNativeAutoMemory, nativeMemoryInstallNote } from './native-memory.mjs';
|
|
29
|
+
import { writeFact } from './write-fact.mjs';
|
|
30
|
+
import { createHash } from 'node:crypto';
|
|
25
31
|
import { runLazyCompress } from './lazy-compress.mjs';
|
|
26
32
|
import { runDoctor } from './doctor.mjs';
|
|
27
33
|
import { importAnthropicMemory } from './import-anthropic-memory.mjs';
|
|
28
34
|
import { extractTranscript, discoverSessions } from './transcripts.mjs';
|
|
29
35
|
import { runRepair } from './repair.mjs';
|
|
30
36
|
import { runRoll, ROLL_SCOPES } from './roll.mjs';
|
|
37
|
+
import { lessonsPromote } from './lessons-promote.mjs';
|
|
31
38
|
import {
|
|
32
39
|
markCronRegistered,
|
|
33
40
|
unmarkCronRegistered,
|
|
@@ -41,6 +48,7 @@ import {
|
|
|
41
48
|
} from './register-crons.mjs';
|
|
42
49
|
import { fileURLToPath } from 'node:url';
|
|
43
50
|
import { dirname } from 'node:path';
|
|
51
|
+
import { readFileSync } from 'node:fs';
|
|
44
52
|
|
|
45
53
|
const __filename_subcommands = fileURLToPath(import.meta.url);
|
|
46
54
|
const __dirname_subcommands = dirname(__filename_subcommands);
|
|
@@ -50,7 +58,7 @@ import { overrideTrust as overrideTrustAction } from './trust.mjs';
|
|
|
50
58
|
import { resolveConflictQueue, mergeScratchpadBullets } from './conflict-queue.mjs';
|
|
51
59
|
import { resolveReviewQueue } from './review-queue.mjs';
|
|
52
60
|
import { createInterface } from 'node:readline';
|
|
53
|
-
import { resolve as resolvePath, join } from 'node:path';
|
|
61
|
+
import { resolve as resolvePath, join, basename } from 'node:path';
|
|
54
62
|
|
|
55
63
|
const NOTICE_PREFIX = 'not yet implemented in v0.1.0';
|
|
56
64
|
|
|
@@ -64,20 +72,54 @@ const NOTICE_PREFIX = 'not yet implemented in v0.1.0';
|
|
|
64
72
|
async function runInstall(options /* , command */) {
|
|
65
73
|
// commander maps `--no-hooks` to options.hooks === false.
|
|
66
74
|
const noHooks = !!(options && options.hooks === false);
|
|
75
|
+
const verbose = !!(options && options.verbose);
|
|
67
76
|
const result = await installAction({ force: !!(options && options.force), noHooks });
|
|
68
|
-
const parts = [
|
|
69
|
-
`scaffolded ${result.created.length} file(s)`,
|
|
70
|
-
result.skipped.length ? `skipped ${result.skipped.length} existing` : null,
|
|
71
|
-
`.gitignore=${result.gitignore.action}`,
|
|
72
|
-
`CLAUDE.md=${result.claudeMd.action}`,
|
|
73
|
-
`hooks=${result.hooks.action}`,
|
|
74
|
-
].filter(Boolean);
|
|
75
|
-
console.log('cmk install: ' + parts.join(', '));
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
// Outcome over inventory (self-test UX finding): state the resulting state +
|
|
79
|
+
// next action, not a file tally. The old "scaffolded 5, skipped 4 existing"
|
|
80
|
+
// read like a problem on a FRESH folder — the "skipped" are the cross-project
|
|
81
|
+
// user tier at ~/.claude-memory-kit/ (OUTSIDE this folder), already on disk.
|
|
82
|
+
// The full per-tier breakdown is --verbose only.
|
|
83
|
+
const projectName = basename(resolvePath(process.cwd()));
|
|
84
|
+
const wired =
|
|
85
|
+
result.hooks.action === 'wired' || result.hooks.action === 'unchanged';
|
|
86
|
+
const broughtSomethingNew =
|
|
87
|
+
result.created.length > 0 ||
|
|
88
|
+
result.gitignore.action === 'created' ||
|
|
89
|
+
result.claudeMd.action === 'created';
|
|
90
|
+
|
|
91
|
+
if (broughtSomethingNew) {
|
|
92
|
+
console.log(
|
|
93
|
+
`cmk install: ${projectName} ready — context/ scaffolded${
|
|
94
|
+
wired ? ', hooks wired' : ''
|
|
95
|
+
}.`,
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
console.log(
|
|
99
|
+
`cmk install: ${projectName} already set up (your edits preserved)${
|
|
100
|
+
wired ? ', hooks refreshed' : ''
|
|
101
|
+
}.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (wired) {
|
|
78
105
|
console.log(
|
|
79
|
-
'
|
|
80
|
-
|
|
106
|
+
' Restart Claude Code to activate. Complete install — no separate /plugin step needed.',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
// Task 60 / ADR-0011 heads-up: the kit coexists with Claude Code's native
|
|
110
|
+
// Auto Memory by default; surface the one-command opt-out (null when already
|
|
111
|
+
// opted out, so we don't nag).
|
|
112
|
+
const nativeNote = nativeMemoryInstallNote(result.projectRoot);
|
|
113
|
+
if (nativeNote) console.log(nativeNote);
|
|
114
|
+
if (verbose) {
|
|
115
|
+
console.log(
|
|
116
|
+
` files: ${result.created.length} created, ${result.skipped.length} already present` +
|
|
117
|
+
(result.skipped.length
|
|
118
|
+
? ' (incl. the cross-project user tier at ~/.claude-memory-kit/, outside this folder)'
|
|
119
|
+
: ''),
|
|
120
|
+
);
|
|
121
|
+
console.log(
|
|
122
|
+
` .gitignore=${result.gitignore.action} · CLAUDE.md=${result.claudeMd.action} · hooks=${result.hooks.action}`,
|
|
81
123
|
);
|
|
82
124
|
}
|
|
83
125
|
|
|
@@ -160,6 +202,49 @@ function runTrust(id, level /* , options, command */) {
|
|
|
160
202
|
}
|
|
161
203
|
}
|
|
162
204
|
|
|
205
|
+
/**
|
|
206
|
+
* `cmk lessons promote <id> [--to <file>] [--section <title>]` — Task 76.
|
|
207
|
+
*
|
|
208
|
+
* The explicit half of the wedge: carry a project observation across ALL your
|
|
209
|
+
* projects. Routes through the safe promote path (home-path sanitization,
|
|
210
|
+
* Poison_Guard, dedup, audit trail) — never hand-edits the user-tier files.
|
|
211
|
+
*/
|
|
212
|
+
function runLessonsPromote(id, options = {}) {
|
|
213
|
+
const projectRoot = resolvePath(process.cwd());
|
|
214
|
+
const userDir =
|
|
215
|
+
process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
|
|
216
|
+
const result = lessonsPromote({
|
|
217
|
+
id,
|
|
218
|
+
projectRoot,
|
|
219
|
+
userDir,
|
|
220
|
+
to: options.to,
|
|
221
|
+
section: options.section,
|
|
222
|
+
});
|
|
223
|
+
if (result.action === 'promoted') {
|
|
224
|
+
console.log(`cmk lessons promote: ${result.id} → ${result.target} § ${result.section}`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (result.action === 'queued') {
|
|
228
|
+
// Exit 3 (not 2): the fact is durably SAVED to the review queue — it didn't
|
|
229
|
+
// fail, it just needs a `cmk queue review` to land in the persona. Distinct
|
|
230
|
+
// from the genuine-error exits (2) so scripts can tell them apart.
|
|
231
|
+
console.error(
|
|
232
|
+
`cmk lessons promote: saved to the user-tier review queue (${result.reason}) — run \`cmk queue review\` to land it`,
|
|
233
|
+
);
|
|
234
|
+
process.exitCode = 3;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (result.action === 'not-found') {
|
|
238
|
+
console.error(`cmk lessons promote: ${result.errors[0]}`);
|
|
239
|
+
process.exitCode = 2;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (result.action === 'error') {
|
|
243
|
+
for (const e of result.errors) console.error(`cmk lessons promote: ${e}`);
|
|
244
|
+
process.exitCode = 2;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
163
248
|
/**
|
|
164
249
|
* `cmk search` — Task 30. Hybrid keyword + optional semantic.
|
|
165
250
|
*
|
|
@@ -178,9 +263,28 @@ function runTrust(id, level /* , options, command */) {
|
|
|
178
263
|
*/
|
|
179
264
|
function runSearch(queryParts, options) {
|
|
180
265
|
const projectRoot = resolvePath(process.cwd());
|
|
266
|
+
const userDir =
|
|
267
|
+
process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
|
|
181
268
|
const query = Array.isArray(queryParts) ? queryParts.join(' ') : queryParts;
|
|
182
269
|
const db = openIndexDb({ projectRoot });
|
|
183
270
|
try {
|
|
271
|
+
// Refresh the index before querying. On a fresh install the FTS5 index
|
|
272
|
+
// is empty (auto-extract writes facts to MEMORY.md but doesn't reindex,
|
|
273
|
+
// and the runtime chokidar watcher isn't running for a one-shot CLI
|
|
274
|
+
// call), so without this `cmk search` returns "no results" for facts
|
|
275
|
+
// that are sitting right there in the scratchpads (self-test finding
|
|
276
|
+
// #0). reindexBoot is incremental — mtime/sha1 diff, only changed files
|
|
277
|
+
// — so it's cheap to run on every search. Degrade gracefully: a reindex
|
|
278
|
+
// failure falls back to whatever's already indexed rather than crashing
|
|
279
|
+
// the query.
|
|
280
|
+
try {
|
|
281
|
+
reindexBoot({ projectRoot, userDir, db });
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.error(
|
|
284
|
+
`cmk search: index refresh failed (${err?.message ?? err}); ` +
|
|
285
|
+
'searching the existing index. Run `cmk reindex --full` if results look stale.',
|
|
286
|
+
);
|
|
287
|
+
}
|
|
184
288
|
const r = searchAction({
|
|
185
289
|
db,
|
|
186
290
|
query,
|
|
@@ -235,6 +339,171 @@ function runSearch(queryParts, options) {
|
|
|
235
339
|
* (b) keeping it always-current avoids users having to think about which
|
|
236
340
|
* index to rebuild when.
|
|
237
341
|
*/
|
|
342
|
+
/**
|
|
343
|
+
* `cmk remember <text...>` — explicit durable capture (write-path fix #0b).
|
|
344
|
+
*
|
|
345
|
+
* Writes a provenance-tracked bullet to MEMORY.md (the session-start-recalled
|
|
346
|
+
* layer) through the SAME hardened path as auto-extract: Poison_Guard +
|
|
347
|
+
* home-path abstraction (#1) + conflict detection + dedup. This is the entry
|
|
348
|
+
* the scaffolded CLAUDE.md points the agent at INSTEAD of freehand-writing
|
|
349
|
+
* fact files — which produced wrong-schema, unindexable, username-leaking
|
|
350
|
+
* files in the self-test. Guaranteed-correct because it never touches raw
|
|
351
|
+
* frontmatter.
|
|
352
|
+
*
|
|
353
|
+
* Tier: v0.1.0 writes tier P (project MEMORY.md). U/L need per-tier scratchpad
|
|
354
|
+
* routing (same deferral as mk_remember, design §16) — the always-on home-path
|
|
355
|
+
* abstraction is the privacy net regardless of tier.
|
|
356
|
+
*/
|
|
357
|
+
// Task 63 (F1): a slug derived from the title — lowercased, non-alphanumerics
|
|
358
|
+
// collapsed to '-', trimmed, capped. Always passes writeFact's SLUG_PATTERN.
|
|
359
|
+
function slugifyFact(s) {
|
|
360
|
+
// Collapse every run of non-alphanumerics to a single '-' (so dashes are
|
|
361
|
+
// never doubled), cap, then trim a leading/trailing dash without a regex
|
|
362
|
+
// quantifier (static analysis flags trailing `-+$` as ReDoS-prone; a single
|
|
363
|
+
// dash is all that can remain after the collapse, so string ops suffice).
|
|
364
|
+
let base = String(s).toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 60);
|
|
365
|
+
if (base.startsWith('-')) base = base.slice(1);
|
|
366
|
+
if (base.endsWith('-')) base = base.slice(0, -1);
|
|
367
|
+
return base || 'fact';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Assemble the rich fact body in the v0.1.1 shape: headline + Why + How.
|
|
371
|
+
function buildRichFactBody({ text, why, how }) {
|
|
372
|
+
const parts = [String(text).trim()];
|
|
373
|
+
if (why && String(why).trim()) parts.push(`**Why:** ${String(why).trim()}`);
|
|
374
|
+
if (how && String(how).trim()) parts.push(`**How to apply:** ${String(how).trim()}`);
|
|
375
|
+
return parts.join('\n\n');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* `cmk remember --why … --how … --type … --title …` (Task 63 / F1) — RICH
|
|
380
|
+
* capture. Writes a real granular fact file (frontmatter + Why/How/links) via
|
|
381
|
+
* writeFact(), which ALREADY runs home-path sanitization + Poison_Guard + the
|
|
382
|
+
* correct schema. Restores v0.1.1 richness through v0.1.2's safe path. `deps`
|
|
383
|
+
* carries injection seams for testing.
|
|
384
|
+
*/
|
|
385
|
+
export function runRememberRich(text, options = {}, deps = {}) {
|
|
386
|
+
const projectRoot = deps.projectRoot ?? resolvePath(process.cwd());
|
|
387
|
+
const log = deps.log ?? console.log;
|
|
388
|
+
const logError = deps.logError ?? console.error;
|
|
389
|
+
const write = deps.writeFact ?? writeFact;
|
|
390
|
+
|
|
391
|
+
// M2: rich capture writes the project tier (P) in v0.1.x — same deferral as
|
|
392
|
+
// the terse path + mk_remember (U/L need per-tier scratchpad routing, design
|
|
393
|
+
// §16). Terse mode ERRORS on a non-P --tier; rich mode notes it and proceeds
|
|
394
|
+
// (a no-write surprise is worse than a captured-to-P note). Surface it so the
|
|
395
|
+
// divergence isn't silent.
|
|
396
|
+
if (options.tier && options.tier !== 'P') {
|
|
397
|
+
log(
|
|
398
|
+
`cmk remember: --tier '${options.tier}' is v0.1.x — rich capture writes the project tier (P) for now.`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const headline = String(text).trim();
|
|
403
|
+
const title = (options.title && String(options.title).trim()) || headline.split('\n')[0].slice(0, 80);
|
|
404
|
+
const body = buildRichFactBody({ text: headline, why: options.why, how: options.how });
|
|
405
|
+
const related = options.links
|
|
406
|
+
? String(options.links).split(',').map((s) => s.trim()).filter(Boolean)
|
|
407
|
+
: undefined;
|
|
408
|
+
|
|
409
|
+
const r = write({
|
|
410
|
+
tier: 'P',
|
|
411
|
+
type: options.type ?? 'feedback',
|
|
412
|
+
slug: slugifyFact(title),
|
|
413
|
+
title,
|
|
414
|
+
body,
|
|
415
|
+
writeSource: 'user-explicit',
|
|
416
|
+
trust: options.trust ?? 'high',
|
|
417
|
+
sourceFile: 'user-explicit',
|
|
418
|
+
sourceLine: 1,
|
|
419
|
+
// Content fingerprint for provenance/dedup — NOT a security context.
|
|
420
|
+
// Matches the kit's sha1-of-content convention (memory-write.mjs,
|
|
421
|
+
// index-rebuild.mjs); writeFact dedups by content-addressed id, this is
|
|
422
|
+
// just the source_sha1 provenance field. // NOSONAR
|
|
423
|
+
sourceSha1: createHash('sha1').update(body).digest('hex'), // NOSONAR
|
|
424
|
+
|
|
425
|
+
related,
|
|
426
|
+
projectRoot,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if (r.action === 'error') {
|
|
430
|
+
// M1: a collision means a fact file with this title (→ slug) already exists
|
|
431
|
+
// but with different content (different id). Give an actionable hint rather
|
|
432
|
+
// than the raw "refusing overwrite" — the user almost certainly wants to
|
|
433
|
+
// edit the existing fact or pick a new --title.
|
|
434
|
+
if (r.errorCategory === 'collision') {
|
|
435
|
+
logError(
|
|
436
|
+
`cmk remember: a fact titled "${title}" already exists with different content. ` +
|
|
437
|
+
`Edit it directly, or capture under a new --title.`,
|
|
438
|
+
);
|
|
439
|
+
return r;
|
|
440
|
+
}
|
|
441
|
+
logError(`cmk remember: ${(r.errors ?? [r.errorCategory ?? 'error']).join('; ')}`);
|
|
442
|
+
return r;
|
|
443
|
+
}
|
|
444
|
+
if (r.action === 'skipped') {
|
|
445
|
+
log(`cmk remember: already captured (${r.skipReason})${r.id ? ` [${r.id}]` : ''}`);
|
|
446
|
+
return r;
|
|
447
|
+
}
|
|
448
|
+
log(`cmk remember: saved rich fact → ${r.path}${r.id ? ` [${r.id}]` : ''}`);
|
|
449
|
+
return r;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function runRemember(textParts, options) {
|
|
453
|
+
const projectRoot = resolvePath(process.cwd());
|
|
454
|
+
const userDir =
|
|
455
|
+
process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
|
|
456
|
+
const text = Array.isArray(textParts) ? textParts.join(' ') : textParts;
|
|
457
|
+
// Rich mode: any of --why/--how/--type/--title/--links → write a real fact
|
|
458
|
+
// file (the F1 fix) instead of a terse MEMORY.md bullet. M3: --trust and
|
|
459
|
+
// --section are intentionally NOT triggers — --trust is shared by both forms
|
|
460
|
+
// (rich reads it too), and --section is terse-only (a MEMORY.md heading, no
|
|
461
|
+
// meaning for a granular fact file). So `--trust high` alone stays terse.
|
|
462
|
+
if (options?.why || options?.how || options?.type || options?.title || options?.links) {
|
|
463
|
+
runRememberRich(text, options, { projectRoot });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const tier = options?.tier ?? 'P';
|
|
467
|
+
if (tier !== 'P') {
|
|
468
|
+
console.error(
|
|
469
|
+
`cmk remember: tier '${tier}' not yet supported — v0.1.0 writes the project tier (P). ` +
|
|
470
|
+
'For machine-only config, edit context.local/machine-paths.md directly (v0.1.x will add --tier routing).',
|
|
471
|
+
);
|
|
472
|
+
process.exitCode = 2;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const trust = options?.trust ?? 'high';
|
|
476
|
+
const section = options?.section ?? 'Active Threads';
|
|
477
|
+
const r = memoryWrite({
|
|
478
|
+
action: 'add',
|
|
479
|
+
text,
|
|
480
|
+
tier,
|
|
481
|
+
scratchpad: 'MEMORY.md',
|
|
482
|
+
section,
|
|
483
|
+
trust,
|
|
484
|
+
source: 'user-explicit',
|
|
485
|
+
projectRoot,
|
|
486
|
+
userDir,
|
|
487
|
+
});
|
|
488
|
+
if (r.action === 'error') {
|
|
489
|
+
for (const e of r.errors ?? [`error (${r.errorCategory})`]) {
|
|
490
|
+
console.error(`cmk remember: ${e}`);
|
|
491
|
+
}
|
|
492
|
+
process.exitCode = 2;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (r.action === 'queued') {
|
|
496
|
+
console.log(
|
|
497
|
+
`cmk remember: queued for review — a higher-trust fact already covers this. ` +
|
|
498
|
+
`Resolve with \`cmk queue conflicts\` (${r.path}).`,
|
|
499
|
+
);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
console.log(
|
|
503
|
+
`cmk remember: saved to P/MEMORY.md (${section})${r.id ? ` [${r.id}]` : ''}`,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
238
507
|
function runReindex(options /* , command */) {
|
|
239
508
|
const projectRoot = resolvePath(process.cwd());
|
|
240
509
|
const userDir = join(homedir(), '.claude-memory-kit');
|
|
@@ -394,6 +663,125 @@ async function runWeeklyCurate(/* options */) {
|
|
|
394
663
|
}
|
|
395
664
|
}
|
|
396
665
|
|
|
666
|
+
/**
|
|
667
|
+
* `cmk persona generate` (Task 45 follow-up) — run cross-project persona
|
|
668
|
+
* synthesis ON DEMAND. Classifies this project's captured facts, auto-promotes
|
|
669
|
+
* the high-confidence cross-project doctrine into the user tier (trust:medium),
|
|
670
|
+
* and saves the low/medium-confidence candidates to
|
|
671
|
+
* <userDir>/queues/persona-review.md. Same pipeline weekly-curate runs
|
|
672
|
+
* automatically — this is the manual trigger (a deterministic hook for the
|
|
673
|
+
* fresh-session live test, and for users who want to fill the user tier now).
|
|
674
|
+
*/
|
|
675
|
+
// `opts` is the Commander options object in production (no relevant keys →
|
|
676
|
+
// every field falls back to its default). The injection seams (projectRoot,
|
|
677
|
+
// userDir, backend, log, logError) keep the wrapper unit-testable without a
|
|
678
|
+
// live `claude --print` spawn — see cli-auto-persona.test.js.
|
|
679
|
+
export async function runPersonaGenerate(opts = {}) {
|
|
680
|
+
const projectRoot = opts.projectRoot ?? resolvePath(process.cwd());
|
|
681
|
+
const userDir =
|
|
682
|
+
opts.userDir ?? process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
|
|
683
|
+
const log = opts.log ?? console.log;
|
|
684
|
+
const logError = opts.logError ?? console.error;
|
|
685
|
+
try {
|
|
686
|
+
const backend =
|
|
687
|
+
opts.backend ?? new (await import('./compressor.mjs')).HaikuViaAnthropicApi();
|
|
688
|
+
const r = await autoPersona({ projectRoot, userDir, backend });
|
|
689
|
+
if (r.action === 'error') {
|
|
690
|
+
logError(
|
|
691
|
+
`cmk persona generate: error (${r.errorCategory ?? 'unknown'})${(r.errors && r.errors.length) ? `: ${r.errors.join('; ')}` : ''}`,
|
|
692
|
+
);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const promoted = r.promoted?.length ?? 0;
|
|
696
|
+
const superseded = r.superseded?.length ?? 0;
|
|
697
|
+
const queued = r.queued?.length ?? 0;
|
|
698
|
+
const conflicts = r.conflicts?.length ?? 0;
|
|
699
|
+
log(
|
|
700
|
+
`cmk persona generate: ${r.action}${r.reason ? ` (${r.reason})` : ''} — promoted: ${promoted}, superseded: ${superseded}, queued: ${queued}, conflicts: ${conflicts}`,
|
|
701
|
+
);
|
|
702
|
+
if (queued > 0 && r.reviewQueuePath) {
|
|
703
|
+
log(` ${queued} lower-confidence candidate(s) saved for review → ${r.reviewQueuePath}`);
|
|
704
|
+
}
|
|
705
|
+
} catch (err) {
|
|
706
|
+
logError(`cmk persona generate: unexpected error: ${err?.message ?? err}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function resolveUserDir() {
|
|
711
|
+
return process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* `cmk persona export <file>` (Task 72) — bundle the user-tier persona into one
|
|
716
|
+
* portable file to carry to another of YOUR machines.
|
|
717
|
+
*/
|
|
718
|
+
export function runPersonaExport(file, options = {}) {
|
|
719
|
+
const outFile = file ?? options.out;
|
|
720
|
+
if (!outFile) {
|
|
721
|
+
console.error('cmk persona export: give an output file, e.g. `cmk persona export persona-bundle.json`');
|
|
722
|
+
process.exitCode = 2;
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const r = exportPersona({ userDir: resolveUserDir(), outFile });
|
|
726
|
+
if (r.action === 'error') {
|
|
727
|
+
console.error(`cmk persona export: ${r.errors?.join('; ') || r.errorCategory}`);
|
|
728
|
+
process.exitCode = 2;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
console.log(`cmk persona export: ${r.fileCount} files → ${r.path} (${r.bytes} B)`);
|
|
732
|
+
console.log(' Carry it via your own private channel (USB / private git repo / Dropbox), then `cmk persona import` on the other machine.');
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* `cmk persona import <file>` (Task 72) — apply a persona bundle to this
|
|
737
|
+
* machine's user tier. Overwrites; any replaced file is backed up first.
|
|
738
|
+
*/
|
|
739
|
+
export function runPersonaImport(file, options = {}) {
|
|
740
|
+
const inFile = file ?? options.from;
|
|
741
|
+
if (!inFile) {
|
|
742
|
+
console.error('cmk persona import: give a bundle file, e.g. `cmk persona import persona-bundle.json`');
|
|
743
|
+
process.exitCode = 2;
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const r = importPersona({ userDir: resolveUserDir(), inFile });
|
|
747
|
+
if (r.action === 'error') {
|
|
748
|
+
console.error(`cmk persona import: ${r.errors?.join('; ') || r.errorCategory}`);
|
|
749
|
+
process.exitCode = 2;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const bkp = r.backedUp > 0 ? ` (backed up ${r.backedUp} existing file(s) → ${r.backupPath})` : '';
|
|
753
|
+
console.log(`cmk persona import: ${r.fileCount} files → ${resolveUserDir()}${bkp}${r.reindexed ? ' · search reindexed' : ''}`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* `cmk disable-native-memory` / `cmk enable-native-memory` (Task 60, ADR-0011)
|
|
758
|
+
* — write `autoMemoryEnabled` into the project's committable
|
|
759
|
+
* `.claude/settings.json`. Default install does NOT touch it (coexist); this
|
|
760
|
+
* is the one-command opt-in/out. `opts` carries injection seams for testing.
|
|
761
|
+
*/
|
|
762
|
+
export function runSetNativeMemory(enabled, opts = {}) {
|
|
763
|
+
const projectRoot = opts.projectRoot ?? resolvePath(process.cwd());
|
|
764
|
+
const log = opts.log ?? console.log;
|
|
765
|
+
const logError = opts.logError ?? console.error;
|
|
766
|
+
const cmd = enabled ? 'enable-native-memory' : 'disable-native-memory';
|
|
767
|
+
const r = setNativeAutoMemory({ projectRoot, enabled });
|
|
768
|
+
if (r.action === 'error') {
|
|
769
|
+
logError(`cmk ${cmd}: ${(r.errors && r.errors.join('; ')) || 'could not update settings.json'}`);
|
|
770
|
+
return r;
|
|
771
|
+
}
|
|
772
|
+
const verb = enabled ? 'enabled' : 'disabled';
|
|
773
|
+
if (r.action === 'unchanged') {
|
|
774
|
+
log(`cmk ${cmd}: Anthropic native Auto Memory already ${verb} for this project (no change).`);
|
|
775
|
+
} else if (enabled) {
|
|
776
|
+
log('Anthropic native Auto Memory re-enabled for this project — native + kit memory will coexist again.');
|
|
777
|
+
log(` → ${r.settingsPath} (reverse with \`cmk disable-native-memory\`)`);
|
|
778
|
+
} else {
|
|
779
|
+
log('Anthropic native Auto Memory disabled for this project — the kit is now the sole memory layer (one lean snapshot at session start).');
|
|
780
|
+
log(` → ${r.settingsPath} (committable: travels with \`git clone\`; reverse with \`cmk enable-native-memory\`)`);
|
|
781
|
+
}
|
|
782
|
+
return r;
|
|
783
|
+
}
|
|
784
|
+
|
|
397
785
|
/**
|
|
398
786
|
* `cmk register-crons [--dry-run] [--unregister]` (Task 33) — register
|
|
399
787
|
* the daily-distill cron entry on the current platform.
|
|
@@ -1022,6 +1410,7 @@ export const subcommands = [
|
|
|
1022
1410
|
optionSpec: [
|
|
1023
1411
|
{ flags: '--force', description: 'allow downgrade of an existing newer-version CLAUDE.md block' },
|
|
1024
1412
|
{ flags: '--no-hooks', description: 'scaffold only; do NOT wire hooks into .claude/settings.json' },
|
|
1413
|
+
{ flags: '--verbose', description: 'show the per-tier created/skipped file breakdown' },
|
|
1025
1414
|
],
|
|
1026
1415
|
action: runInstall,
|
|
1027
1416
|
},
|
|
@@ -1037,6 +1426,24 @@ export const subcommands = [
|
|
|
1037
1426
|
milestone: 14,
|
|
1038
1427
|
action: runInitUserTier,
|
|
1039
1428
|
},
|
|
1429
|
+
{
|
|
1430
|
+
name: 'remember',
|
|
1431
|
+
description: 'capture a durable fact (Poison_Guard + home-path abstraction). Terse → a MEMORY.md bullet; RICH (--why/--how/--type) → a granular fact file with rationale (the safe way to capture richly).',
|
|
1432
|
+
milestone: 24,
|
|
1433
|
+
argSpec: [{ flags: '<text...>', description: 'the fact to remember' }],
|
|
1434
|
+
optionSpec: [
|
|
1435
|
+
{ flags: '--tier <tier>', description: 'P (default; U/L are v0.1.x)' },
|
|
1436
|
+
{ flags: '--trust <level>', description: 'high | medium | low (default: high)' },
|
|
1437
|
+
{ flags: '--section <name>', description: 'MEMORY.md section for the terse form (default: Active Threads)' },
|
|
1438
|
+
// Rich mode (Task 63 / F1): any of these → write a granular fact file.
|
|
1439
|
+
{ flags: '--why <text>', description: 'rich: the rationale (becomes the **Why:** block)' },
|
|
1440
|
+
{ flags: '--how <text>', description: 'rich: how to apply it (becomes the **How to apply:** block)' },
|
|
1441
|
+
{ flags: '--type <type>', description: 'rich: feedback | project | reference | user (default: feedback)' },
|
|
1442
|
+
{ flags: '--title <text>', description: 'rich: a short title (also the fact-file slug)' },
|
|
1443
|
+
{ flags: '--links <a,b>', description: 'rich: related fact names for [[cross-links]]' },
|
|
1444
|
+
],
|
|
1445
|
+
action: runRemember,
|
|
1446
|
+
},
|
|
1040
1447
|
{
|
|
1041
1448
|
name: 'search',
|
|
1042
1449
|
description: 'search memory — hybrid keyword + optional semantic',
|
|
@@ -1140,16 +1547,20 @@ export const subcommands = [
|
|
|
1140
1547
|
},
|
|
1141
1548
|
{
|
|
1142
1549
|
name: 'lessons',
|
|
1143
|
-
description: 'promote project-tier
|
|
1144
|
-
milestone:
|
|
1550
|
+
description: 'promote a project-tier observation to the user tier (carry it across all your projects)',
|
|
1551
|
+
milestone: 76,
|
|
1145
1552
|
children: [
|
|
1146
1553
|
{
|
|
1147
1554
|
name: 'promote',
|
|
1148
|
-
description: 'move a project observation to
|
|
1149
|
-
argSpec: [{ flags: '<id>', description: 'citation ID' }],
|
|
1555
|
+
description: 'move a project observation to the user tier through the safe promote path',
|
|
1556
|
+
argSpec: [{ flags: '<id>', description: 'citation ID (e.g. P-S79MJHFN)' }],
|
|
1557
|
+
optionSpec: [
|
|
1558
|
+
{ flags: '--to <file>', description: 'target user-tier file: LESSONS.md (default) | HABITS.md | USER.md' },
|
|
1559
|
+
{ flags: '--section <title>', description: 'landing section (default per-target)' },
|
|
1560
|
+
],
|
|
1561
|
+
action: (id, options) => runLessonsPromote(id, options),
|
|
1150
1562
|
},
|
|
1151
1563
|
],
|
|
1152
|
-
action: stub('lessons', 'v0.1.x'),
|
|
1153
1564
|
},
|
|
1154
1565
|
{
|
|
1155
1566
|
name: 'queue',
|
|
@@ -1212,6 +1623,44 @@ export const subcommands = [
|
|
|
1212
1623
|
milestone: 34,
|
|
1213
1624
|
action: runWeeklyCurate,
|
|
1214
1625
|
},
|
|
1626
|
+
{
|
|
1627
|
+
name: 'persona',
|
|
1628
|
+
description: 'cross-project persona — synthesize "how you work everywhere" into the user tier, and carry it across YOUR machines',
|
|
1629
|
+
milestone: 45,
|
|
1630
|
+
children: [
|
|
1631
|
+
{
|
|
1632
|
+
name: 'generate',
|
|
1633
|
+
description: 'synthesize cross-project doctrine from this project\'s captured facts now: auto-promote high-confidence to the user tier, save the rest to queues/persona-review.md',
|
|
1634
|
+
action: runPersonaGenerate,
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
name: 'export',
|
|
1638
|
+
description: 'export your cross-project persona (the user tier) to one portable bundle file, to carry to another of YOUR machines (the persona stays private — never committed to a project)',
|
|
1639
|
+
milestone: 72,
|
|
1640
|
+
argSpec: [{ flags: '<file>', description: 'output bundle path, e.g. persona-bundle.json' }],
|
|
1641
|
+
action: (file, options) => runPersonaExport(file, options),
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
name: 'import',
|
|
1645
|
+
description: 'import a persona bundle (from `cmk persona export`) into this machine\'s user tier; any file it replaces is backed up first',
|
|
1646
|
+
milestone: 72,
|
|
1647
|
+
argSpec: [{ flags: '<file>', description: 'bundle path produced by `cmk persona export`' }],
|
|
1648
|
+
action: (file, options) => runPersonaImport(file, options),
|
|
1649
|
+
},
|
|
1650
|
+
],
|
|
1651
|
+
},
|
|
1652
|
+
{
|
|
1653
|
+
name: 'disable-native-memory',
|
|
1654
|
+
description: 'opt out of Anthropic\'s native Auto Memory for THIS project (writes autoMemoryEnabled:false to the committable .claude/settings.json) so only the kit\'s memory runs — avoids the both-layers context bloat (ADR-0011)',
|
|
1655
|
+
milestone: 60,
|
|
1656
|
+
action: () => runSetNativeMemory(false),
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
name: 'enable-native-memory',
|
|
1660
|
+
description: 're-enable Anthropic\'s native Auto Memory for this project (reverses cmk disable-native-memory)',
|
|
1661
|
+
milestone: 60,
|
|
1662
|
+
action: () => runSetNativeMemory(true),
|
|
1663
|
+
},
|
|
1215
1664
|
{
|
|
1216
1665
|
name: 'compress',
|
|
1217
1666
|
description: 'lazy-on-read compression fallback for no-cron environments. Use `--lazy` to delegate to daily-distill or weekly-curate based on staleness (typically invoked detached from the SessionStart hook).',
|
|
@@ -1243,15 +1692,12 @@ export const subcommands = [
|
|
|
1243
1692
|
description: 'print the cmk version (alias for --version)',
|
|
1244
1693
|
milestone: 'always',
|
|
1245
1694
|
action: function action() {
|
|
1246
|
-
//
|
|
1247
|
-
//
|
|
1248
|
-
//
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
// For v0.1.0 stub-only milestone, defer the actual print to the
|
|
1253
|
-
// top-level --version handler.
|
|
1254
|
-
console.log(`cmk version: see \`cmk --version\``);
|
|
1695
|
+
// Print the same bare version string as `cmk --version`, resolved from
|
|
1696
|
+
// package.json (the single source). Was a v0.1.0 stub that punted to
|
|
1697
|
+
// `cmk --version`; the live test surfaced that as unhelpful friction.
|
|
1698
|
+
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
1699
|
+
const { version } = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8'));
|
|
1700
|
+
console.log(version);
|
|
1255
1701
|
},
|
|
1256
1702
|
},
|
|
1257
1703
|
];
|