@lh8ppl/claude-memory-kit 0.1.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/bin/cmk-compress-lazy.mjs +59 -0
- package/bin/cmk-daily-distill.mjs +67 -0
- package/bin/cmk-weekly-curate.mjs +56 -0
- package/bin/cmk.mjs +12 -0
- package/package.json +50 -0
- package/src/audit-log.mjs +103 -0
- package/src/auto-extract.mjs +742 -0
- package/src/capture-prompt.mjs +61 -0
- package/src/capture-turn.mjs +273 -0
- package/src/claude-md.mjs +212 -0
- package/src/compress-session.mjs +349 -0
- package/src/compressor.mjs +376 -0
- package/src/conflict-queue.mjs +796 -0
- package/src/cooldown.mjs +61 -0
- package/src/daily-distill.mjs +252 -0
- package/src/doctor.mjs +528 -0
- package/src/forget.mjs +335 -0
- package/src/frontmatter.mjs +73 -0
- package/src/import-anthropic-memory.mjs +266 -0
- package/src/index-db.mjs +154 -0
- package/src/index-rebuild.mjs +597 -0
- package/src/index.mjs +90 -0
- package/src/inject-context.mjs +484 -0
- package/src/install.mjs +327 -0
- package/src/lazy-compress.mjs +326 -0
- package/src/lock-discipline.mjs +166 -0
- package/src/mcp-server.mjs +498 -0
- package/src/memory-write.mjs +565 -0
- package/src/merge-facts.mjs +213 -0
- package/src/observe-edit.mjs +87 -0
- package/src/platform-commands.mjs +138 -0
- package/src/poison-guard.mjs +245 -0
- package/src/privacy.mjs +21 -0
- package/src/provenance.mjs +217 -0
- package/src/register-crons.mjs +354 -0
- package/src/reindex.mjs +134 -0
- package/src/repair.mjs +316 -0
- package/src/result-shapes.mjs +155 -0
- package/src/review-queue.mjs +345 -0
- package/src/roll.mjs +115 -0
- package/src/scratchpad.mjs +335 -0
- package/src/search.mjs +311 -0
- package/src/subcommands.mjs +1252 -0
- package/src/tier-paths.mjs +74 -0
- package/src/transcripts.mjs +234 -0
- package/src/trust.mjs +226 -0
- package/src/weekly-curate.mjs +454 -0
- package/src/write-fact.mjs +205 -0
- package/template/.claude/hooks/pre-tool-memory.js +78 -0
- package/template/.claude/hooks/transcript-capture.js +69 -0
- package/template/.claude/settings.json +27 -0
- package/template/.claude/skills/memory-write/SKILL.md +117 -0
- package/template/.gitignore.fragment +12 -0
- package/template/CLAUDE.md.template +49 -0
- package/template/docs/journey/journey-log.md.template +292 -0
- package/template/local/machine-paths.md.template +37 -0
- package/template/local/overrides.md.template +36 -0
- package/template/project/.index/.gitkeep +0 -0
- package/template/project/MEMORY.md.template +47 -0
- package/template/project/SOUL.md.template +35 -0
- package/template/project/memory/INDEX.md.template +47 -0
- package/template/project/memory/archive/superseded/.gitkeep +0 -0
- package/template/project/memory/archive/tombstones/.gitkeep +0 -0
- package/template/project/queues/.gitkeep +0 -0
- package/template/project/sessions/.gitkeep +0 -0
- package/template/project/transcripts/.gitkeep +0 -0
- package/template/support/cron-jobs/daily-memory-distill.md +15 -0
- package/template/support/cron-jobs/nightly-memsearch-index.md +17 -0
- package/template/support/cron-jobs/weekly-memory-curator.md +15 -0
- package/template/support/milvus-deploy/README.md +57 -0
- package/template/support/milvus-deploy/docker-compose.yml +66 -0
- package/template/support/scripts/auto-extract-memory.sh +102 -0
- package/template/support/scripts/memsearch-index-with-flush.sh +59 -0
- package/template/support/scripts/refresh-distill-timestamp.py +35 -0
- package/template/support/scripts/register-crons.py +242 -0
- package/template/support/scripts/run-daily-distill.sh +67 -0
- package/template/support/scripts/run-weekly-curate.sh +58 -0
- package/template/user/HABITS.md.template +18 -0
- package/template/user/LESSONS.md.template +18 -0
- package/template/user/USER.md.template +18 -0
- package/template/user/fragments/INDEX.md.template +23 -0
|
@@ -0,0 +1,1252 @@
|
|
|
1
|
+
// Subcommand registry for cmk.
|
|
2
|
+
//
|
|
3
|
+
// Single source of truth for every verb the CLI accepts. Each entry
|
|
4
|
+
// describes one verb + (optionally) its sub-verbs. v0.1.0 implements
|
|
5
|
+
// verbs incrementally — each task in tasks.md replaces one stub with
|
|
6
|
+
// a real action. Verbs still on stub print a "not yet implemented in
|
|
7
|
+
// v0.1.0 milestone N" notice (N = the tasks.md task that lights it up).
|
|
8
|
+
//
|
|
9
|
+
// The `milestone` field references the tasks.md parent task that will
|
|
10
|
+
// implement the subcommand. Use "v0.1.x" for verbs deferred past v0.1
|
|
11
|
+
// (per design §12 but not in the v0.1.0 critical path).
|
|
12
|
+
//
|
|
13
|
+
// Adding a new verb? Append to `subcommands` below — the test suite
|
|
14
|
+
// asserts exactly what's exported here, so coverage stays automatic.
|
|
15
|
+
|
|
16
|
+
import { install as installAction, initUserTier as initUserTierAction } from './install.mjs';
|
|
17
|
+
import { removeClaudeMdBlock } from './claude-md.mjs';
|
|
18
|
+
import { reindex as reindexAction } from './reindex.mjs';
|
|
19
|
+
import { openIndexDb } from './index-db.mjs';
|
|
20
|
+
import { reindexBoot, reindexFull } from './index-rebuild.mjs';
|
|
21
|
+
import { search as searchAction, SEARCH_MODES } from './search.mjs';
|
|
22
|
+
import { runMcpServer } from './mcp-server.mjs';
|
|
23
|
+
import { dailyDistill } from './daily-distill.mjs';
|
|
24
|
+
import { weeklyCurate } from './weekly-curate.mjs';
|
|
25
|
+
import { runLazyCompress } from './lazy-compress.mjs';
|
|
26
|
+
import { runDoctor } from './doctor.mjs';
|
|
27
|
+
import { importAnthropicMemory } from './import-anthropic-memory.mjs';
|
|
28
|
+
import { extractTranscript, discoverSessions } from './transcripts.mjs';
|
|
29
|
+
import { runRepair } from './repair.mjs';
|
|
30
|
+
import { runRoll, ROLL_SCOPES } from './roll.mjs';
|
|
31
|
+
import {
|
|
32
|
+
markCronRegistered,
|
|
33
|
+
unmarkCronRegistered,
|
|
34
|
+
} from './lazy-compress.mjs';
|
|
35
|
+
import {
|
|
36
|
+
registerCron,
|
|
37
|
+
unregisterCron,
|
|
38
|
+
CRON_ENTRY_NAME,
|
|
39
|
+
WEEKLY_ENTRY_NAME,
|
|
40
|
+
DEFAULT_WEEKLY_SCHEDULE,
|
|
41
|
+
} from './register-crons.mjs';
|
|
42
|
+
import { fileURLToPath } from 'node:url';
|
|
43
|
+
import { dirname } from 'node:path';
|
|
44
|
+
|
|
45
|
+
const __filename_subcommands = fileURLToPath(import.meta.url);
|
|
46
|
+
const __dirname_subcommands = dirname(__filename_subcommands);
|
|
47
|
+
import { homedir } from 'node:os';
|
|
48
|
+
import { forget as forgetAction } from './forget.mjs';
|
|
49
|
+
import { overrideTrust as overrideTrustAction } from './trust.mjs';
|
|
50
|
+
import { resolveConflictQueue, mergeScratchpadBullets } from './conflict-queue.mjs';
|
|
51
|
+
import { resolveReviewQueue } from './review-queue.mjs';
|
|
52
|
+
import { createInterface } from 'node:readline';
|
|
53
|
+
import { resolve as resolvePath, join } from 'node:path';
|
|
54
|
+
|
|
55
|
+
const NOTICE_PREFIX = 'not yet implemented in v0.1.0';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Real `cmk install` action — wired in Task 3, extended in Task 4 with
|
|
59
|
+
* --force passed through to the CLAUDE.md downgrade guard. Reads CLI
|
|
60
|
+
* options/flags, dispatches to the install module, prints a one-line
|
|
61
|
+
* summary, and reports the CLAUDE.md action (created / appended /
|
|
62
|
+
* replaced / upgraded / downgrade-blocked / forced-downgrade / unchanged).
|
|
63
|
+
*/
|
|
64
|
+
async function runInstall(options /* , command */) {
|
|
65
|
+
const result = await installAction({ force: !!(options && options.force) });
|
|
66
|
+
const parts = [
|
|
67
|
+
`scaffolded ${result.created.length} file(s)`,
|
|
68
|
+
result.skipped.length ? `skipped ${result.skipped.length} existing` : null,
|
|
69
|
+
`.gitignore=${result.gitignore.action}`,
|
|
70
|
+
`CLAUDE.md=${result.claudeMd.action}`,
|
|
71
|
+
].filter(Boolean);
|
|
72
|
+
console.log('cmk install: ' + parts.join(', '));
|
|
73
|
+
|
|
74
|
+
if (result.claudeMd.action === 'downgrade-blocked') {
|
|
75
|
+
console.error(
|
|
76
|
+
` warning: CLAUDE.md already has a newer kit block (v${result.claudeMd.oldVersion}). ` +
|
|
77
|
+
`Re-run with --force to downgrade.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (result.errors.length > 0) {
|
|
82
|
+
for (const e of result.errors) console.error(` error: ${e.path}: ${e.error}`);
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `cmk uninstall` — wired in Task 4. Strips the kit-managed block from
|
|
89
|
+
* the project's CLAUDE.md (if present). Everything outside the markers
|
|
90
|
+
* is byte-preserved. Does NOT touch context/, context.local/, the user
|
|
91
|
+
* tier, or .gitignore — `cmk uninstall` is conservative; users delete
|
|
92
|
+
* those by hand if they really want to.
|
|
93
|
+
*/
|
|
94
|
+
function runUninstall(/* options, command */) {
|
|
95
|
+
const projectRoot = resolvePath(process.cwd());
|
|
96
|
+
const result = removeClaudeMdBlock({ projectRoot });
|
|
97
|
+
console.log(`cmk uninstall: CLAUDE.md=${result.action} (${result.path})`);
|
|
98
|
+
if (result.action === 'not-found') {
|
|
99
|
+
console.log(' (no kit-managed block found; CLAUDE.md left unchanged)');
|
|
100
|
+
} else if (result.action === 'no-file') {
|
|
101
|
+
console.log(' (no CLAUDE.md to uninstall from)');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* `cmk init-user-tier` — wired in Task 14. User-tier-only install.
|
|
107
|
+
* Scaffolds USER.md, HABITS.md, LESSONS.md, fragments/ at the
|
|
108
|
+
* resolved user-tier path. Does NOT touch project/local tier files
|
|
109
|
+
* or .gitignore or CLAUDE.md (call `cmk install` for that).
|
|
110
|
+
*/
|
|
111
|
+
function runInitUserTier(/* options, command */) {
|
|
112
|
+
const result = initUserTierAction({});
|
|
113
|
+
console.log(
|
|
114
|
+
`cmk init-user-tier: scaffolded ${result.created.length} file(s)` +
|
|
115
|
+
(result.skipped.length ? `, skipped ${result.skipped.length} existing` : '') +
|
|
116
|
+
` at ${result.userTier}`,
|
|
117
|
+
);
|
|
118
|
+
if (result.errors.length > 0) {
|
|
119
|
+
for (const e of result.errors) console.error(` error: ${e.path}: ${e.error}`);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* `cmk trust <id> <level>` — wired in Task 15. Updates the `trust:`
|
|
126
|
+
* field in BOTH the matched fact file (YAML frontmatter) AND any
|
|
127
|
+
* scratchpad bullet with the matching id (HTML-comment provenance).
|
|
128
|
+
* Writes a canonical audit-log entry per design §6.1 + spec 15.3.
|
|
129
|
+
*/
|
|
130
|
+
function runTrust(id, level /* , options, command */) {
|
|
131
|
+
const projectRoot = resolvePath(process.cwd());
|
|
132
|
+
const result = overrideTrustAction({ id, level, projectRoot });
|
|
133
|
+
if (result.action === 'trust-updated') {
|
|
134
|
+
console.log(
|
|
135
|
+
`cmk trust: ${result.id} (${result.tier}) → ${result.level} — updated ${result.updatedLocations.length} location(s)`,
|
|
136
|
+
);
|
|
137
|
+
for (const loc of result.updatedLocations) {
|
|
138
|
+
console.log(` ${loc.type}: ${loc.path} (was ${loc.priorTrust})`);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (result.action === 'not-found') {
|
|
143
|
+
console.error(`cmk trust: ${result.errors[0]}`);
|
|
144
|
+
process.exitCode = 2;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (result.action === 'error') {
|
|
148
|
+
for (const e of result.errors) console.error(`cmk trust: ${e}`);
|
|
149
|
+
process.exitCode = 2;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* `cmk search` — Task 30. Hybrid keyword + optional semantic.
|
|
155
|
+
*
|
|
156
|
+
* v0.1.0 ships the keyword backend (FTS5 BM25 over the observations
|
|
157
|
+
* index). Semantic + hybrid modes require the Layer 5b memsearch+Milvus
|
|
158
|
+
* install which isn't bundled in v0.1.0; both error with exit code 2
|
|
159
|
+
* and a clear "memsearch not installed" hint per tasks.md 30.2.
|
|
160
|
+
*
|
|
161
|
+
* Filter flags (per tasks.md 30.4):
|
|
162
|
+
* --mode <keyword|semantic|hybrid> (default keyword)
|
|
163
|
+
* --min-trust <low|medium|high>
|
|
164
|
+
* --tier <U|P|L>
|
|
165
|
+
* --since <ISO date>
|
|
166
|
+
* --limit <N> (default 20)
|
|
167
|
+
* --include-tombstoned (default false)
|
|
168
|
+
*/
|
|
169
|
+
function runSearch(queryParts, options) {
|
|
170
|
+
const projectRoot = resolvePath(process.cwd());
|
|
171
|
+
const query = Array.isArray(queryParts) ? queryParts.join(' ') : queryParts;
|
|
172
|
+
const db = openIndexDb({ projectRoot });
|
|
173
|
+
try {
|
|
174
|
+
const r = searchAction({
|
|
175
|
+
db,
|
|
176
|
+
query,
|
|
177
|
+
mode: options?.mode ?? SEARCH_MODES.KEYWORD,
|
|
178
|
+
minTrust: options?.minTrust,
|
|
179
|
+
tier: options?.tier,
|
|
180
|
+
since: options?.since,
|
|
181
|
+
limit: options?.limit !== undefined ? Number(options.limit) : undefined,
|
|
182
|
+
includeTombstoned: options?.includeTombstoned === true,
|
|
183
|
+
});
|
|
184
|
+
if (r.action === 'error') {
|
|
185
|
+
for (const e of r.errors) console.error(`cmk search: ${e}`);
|
|
186
|
+
// Exit 2 per tasks.md 30.2 contract for semantic-unavailable; schema
|
|
187
|
+
// errors are exit 2 by general kit convention too.
|
|
188
|
+
process.exitCode = 2;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (r.results.length === 0) {
|
|
192
|
+
console.log('cmk search: no results');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
for (const hit of r.results) {
|
|
196
|
+
// Plain-text output suitable for terminal piping. Snippet uses
|
|
197
|
+
// FTS5's <b>...</b> markers; preserved as-is so callers can pipe
|
|
198
|
+
// to a TUI that renders them OR strip via sed.
|
|
199
|
+
console.log(
|
|
200
|
+
`${hit.id}\t${hit.tier}/${hit.trust}\t${hit.source_file}:${hit.source_line}\t${hit.snippet}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
console.log(
|
|
204
|
+
`\ncmk search: ${r.results.length} result(s) (mode=${r.mode})`,
|
|
205
|
+
);
|
|
206
|
+
} finally {
|
|
207
|
+
db.close();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* `cmk reindex` — three modes.
|
|
213
|
+
*
|
|
214
|
+
* no flag Markdown INDEX.md rebuild only (Task 8 behavior). Backward-
|
|
215
|
+
* compat for callers that haven't adopted the SQLite layer.
|
|
216
|
+
* --boot Same as no-flag PLUS SQLite boot diff (Task 29). Reindexes
|
|
217
|
+
* only the source files whose sha1 differs from the `files`
|
|
218
|
+
* checkpoint table. Fast on a warm cache.
|
|
219
|
+
* --full Same as no-flag PLUS SQLite full rebuild (Task 29). DROPs
|
|
220
|
+
* observations / observations_fts / files; walks every source
|
|
221
|
+
* and rebuilds. Recovery path for a corrupted index.
|
|
222
|
+
*
|
|
223
|
+
* Flag semantics per tasks.md 29.1 + 29.3. Markdown INDEX runs in every
|
|
224
|
+
* mode because (a) it's cheap (milliseconds to milliseconds-low-thousands),
|
|
225
|
+
* (b) keeping it always-current avoids users having to think about which
|
|
226
|
+
* index to rebuild when.
|
|
227
|
+
*/
|
|
228
|
+
function runReindex(options /* , command */) {
|
|
229
|
+
const projectRoot = resolvePath(process.cwd());
|
|
230
|
+
const userDir = join(homedir(), '.claude-memory-kit');
|
|
231
|
+
const result = reindexAction({ tier: 'P', projectRoot });
|
|
232
|
+
console.log(
|
|
233
|
+
`cmk reindex: tier=${result.tier} facts=${result.factCount} bytes=${result.bytes} (${result.indexPath})`,
|
|
234
|
+
);
|
|
235
|
+
const useBoot = options?.boot === true;
|
|
236
|
+
const useFull = options?.full === true;
|
|
237
|
+
if (!useBoot && !useFull) return;
|
|
238
|
+
if (useBoot && useFull) {
|
|
239
|
+
console.error('cmk reindex: --boot and --full are mutually exclusive');
|
|
240
|
+
process.exitCode = 2;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const db = openIndexDb({ projectRoot });
|
|
244
|
+
try {
|
|
245
|
+
const r = useFull
|
|
246
|
+
? reindexFull({ projectRoot, userDir, db })
|
|
247
|
+
: reindexBoot({ projectRoot, userDir, db });
|
|
248
|
+
if (useFull) {
|
|
249
|
+
console.log(
|
|
250
|
+
`cmk reindex --full: scanned=${r.filesScanned} observations=${r.observationsAffected} duration=${r.durationMs}ms`,
|
|
251
|
+
);
|
|
252
|
+
} else {
|
|
253
|
+
console.log(
|
|
254
|
+
`cmk reindex --boot: scanned=${r.filesScanned} reindexed=${r.filesReindexed} observations=${r.observationsAffected} duration=${r.durationMs}ms`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (r.skipped && r.skipped.length > 0) {
|
|
258
|
+
for (const s of r.skipped) {
|
|
259
|
+
console.error(` skipped ${s.path}: ${s.reason}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} finally {
|
|
263
|
+
db.close();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* `cmk forget <id-or-query>` — wired in Task 9. Tombstones the matching
|
|
269
|
+
* fact (moves it to <tier>/<memory|fragments>/archive/tombstones/<id>.md
|
|
270
|
+
* with deleted_at/deleted_reason/deleted_by frontmatter) and strips any
|
|
271
|
+
* citing bullets from same-tier scratchpads.
|
|
272
|
+
*
|
|
273
|
+
* v0.1 requires --yes — the interactive confirmation prompt is a v0.1.x
|
|
274
|
+
* follow-up (the boundary's `confirm()` callback path is still tested by
|
|
275
|
+
* cli-forget.test.js; the CLI just doesn't wire stdin readline yet).
|
|
276
|
+
*/
|
|
277
|
+
function runForget(idOrQuery, options /* , command */) {
|
|
278
|
+
if (!options.yes) {
|
|
279
|
+
console.error(
|
|
280
|
+
'cmk forget: --yes is required in v0.1.0 (interactive confirmation prompt is a v0.1.x follow-up). Re-run with --yes to confirm tombstoning.',
|
|
281
|
+
);
|
|
282
|
+
process.exitCode = 2;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const projectRoot = resolvePath(process.cwd());
|
|
286
|
+
const result = forgetAction({
|
|
287
|
+
idOrQuery,
|
|
288
|
+
projectRoot,
|
|
289
|
+
reason: options.reason,
|
|
290
|
+
deletedBy: options.deletedBy,
|
|
291
|
+
yes: true,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (result.action === 'tombstoned') {
|
|
295
|
+
console.log(
|
|
296
|
+
`cmk forget: tombstoned ${result.id} (${result.tier}) → ${result.tombstonePath}`,
|
|
297
|
+
);
|
|
298
|
+
if (result.scratchpadEdits.length > 0) {
|
|
299
|
+
const total = result.scratchpadEdits.reduce((n, e) => n + e.removed, 0);
|
|
300
|
+
console.log(
|
|
301
|
+
` scrubbed ${total} bullet(s) across ${result.scratchpadEdits.length} scratchpad(s)`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (result.action === 'not-found') {
|
|
307
|
+
console.error(`cmk forget: ${result.errors[0]}`);
|
|
308
|
+
process.exitCode = 2;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (result.action === 'error') {
|
|
312
|
+
for (const e of result.errors) console.error(`cmk forget: ${e}`);
|
|
313
|
+
process.exitCode = 2;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// cancelled (won't fire here since we pass yes:true above, but defensive)
|
|
317
|
+
console.log('cmk forget: cancelled');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Real `cmk queue` dispatcher — Task 25. Routes by sub-verb:
|
|
322
|
+
* - 'conflicts' → wire to resolveConflictQueue with a readline-based
|
|
323
|
+
* interactive prompter. merge-both decisions dispatch to mergeFacts.
|
|
324
|
+
* - 'review' → still stubbed (Task 26 / v0.1.x); print the standard
|
|
325
|
+
* notice.
|
|
326
|
+
*/
|
|
327
|
+
/**
|
|
328
|
+
* `cmk mcp <child>` dispatcher (Task 31). Currently one child:
|
|
329
|
+
* - 'serve' → start the stdio MCP server. Invoked by Claude Code as
|
|
330
|
+
* a subprocess; runs until stdin closes.
|
|
331
|
+
*/
|
|
332
|
+
/**
|
|
333
|
+
* `cmk daily-distill` (Task 33) — runs the daily-distill pipeline once.
|
|
334
|
+
* Designed to be invoked by the host scheduler (cron / launchd /
|
|
335
|
+
* schtasks) registered via `cmk register-crons`. Humans normally don't
|
|
336
|
+
* call this directly; they run register-crons once at install time.
|
|
337
|
+
*
|
|
338
|
+
* Always exits 0 — same posture as cmk-compress-session per design §8.6.1.
|
|
339
|
+
*/
|
|
340
|
+
async function runDailyDistill(/* options */) {
|
|
341
|
+
const projectRoot = resolvePath(process.cwd());
|
|
342
|
+
// Lazy-load HaikuViaAnthropicApi (avoids the dep when running unit tests).
|
|
343
|
+
const { HaikuViaAnthropicApi } = await import('./compressor.mjs');
|
|
344
|
+
try {
|
|
345
|
+
const backend = new HaikuViaAnthropicApi();
|
|
346
|
+
const r = await dailyDistill({ projectRoot, backend });
|
|
347
|
+
if (r.action === 'error') {
|
|
348
|
+
console.error(
|
|
349
|
+
`cmk daily-distill: error (${r.error_category ?? 'unknown'})${r.errorMessage ? `: ${r.errorMessage}` : ''}`,
|
|
350
|
+
);
|
|
351
|
+
} else {
|
|
352
|
+
console.log(
|
|
353
|
+
`cmk daily-distill: ${r.action}${r.reason ? ` (${r.reason})` : ''}${r.bytesIn ? ` (in: ${r.bytesIn}b, out: ${r.bytesOut}b, days: ${r.sourceDays})` : ''}`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.error(`cmk daily-distill: unexpected error: ${err?.message ?? err}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* `cmk weekly-curate` (Task 34) — runs the weekly-curate pipeline once.
|
|
363
|
+
* Designed to be invoked by the host scheduler registered via
|
|
364
|
+
* `cmk register-crons` (which registers both daily + weekly entries
|
|
365
|
+
* by default). Humans normally don't invoke this directly.
|
|
366
|
+
*/
|
|
367
|
+
async function runWeeklyCurate(/* options */) {
|
|
368
|
+
const projectRoot = resolvePath(process.cwd());
|
|
369
|
+
const { HaikuViaAnthropicApi } = await import('./compressor.mjs');
|
|
370
|
+
try {
|
|
371
|
+
const backend = new HaikuViaAnthropicApi();
|
|
372
|
+
const r = await weeklyCurate({ projectRoot, backend });
|
|
373
|
+
if (r.action === 'error') {
|
|
374
|
+
console.error(
|
|
375
|
+
`cmk weekly-curate: error (${r.errorCategory ?? 'unknown'})${(r.errors && r.errors.length) ? `: ${r.errors.join('; ')}` : ''}`,
|
|
376
|
+
);
|
|
377
|
+
} else {
|
|
378
|
+
console.log(
|
|
379
|
+
`cmk weekly-curate: ${r.action}${r.reason ? ` (${r.reason})` : ''}${r.archivedDays ? ` (archived: ${r.archivedDays}d, current: ${r.currentDays}d, in: ${r.bytesIn}b, out: ${r.bytesOut}b)` : ''}`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
} catch (err) {
|
|
383
|
+
console.error(`cmk weekly-curate: unexpected error: ${err?.message ?? err}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* `cmk register-crons [--dry-run] [--unregister]` (Task 33) — register
|
|
389
|
+
* the daily-distill cron entry on the current platform.
|
|
390
|
+
*
|
|
391
|
+
* Per design §8.6.2 cross-platform mapping. `--dry-run` prints the
|
|
392
|
+
* command without executing — recommended first run so the user
|
|
393
|
+
* sees what host-config will change before granting permissions.
|
|
394
|
+
*/
|
|
395
|
+
function runRegisterCrons(options /* , command */) {
|
|
396
|
+
const dryRun = options?.dryRun === true;
|
|
397
|
+
const unregister = options?.unregister === true;
|
|
398
|
+
// Task 36 B1+B2 fix: emit the FULL cron command as
|
|
399
|
+
// "<absolute-node-path>" "<absolute-bin-script-path>" "<absolute-project-root>"
|
|
400
|
+
// Rationale (from the layer-wide review):
|
|
401
|
+
// B1 — Cron / launchd / schtasks have non-kit default cwd ($HOME, /,
|
|
402
|
+
// C:\Windows\System32). The bin needs projectRoot resolved AT
|
|
403
|
+
// registration time, not via cwd at fire time.
|
|
404
|
+
// B2 — Bare bin names ('cmk-daily-distill') don't PATH-resolve under
|
|
405
|
+
// the scheduler's restricted PATH (/usr/bin:/bin for launchd; varies
|
|
406
|
+
// for cron). Emitting absolute paths sidesteps PATH entirely.
|
|
407
|
+
// This also bypasses the npm-installed bin shim (.cmd on Windows;
|
|
408
|
+
// symlink on POSIX) — `node <abs-script>` works directly on every
|
|
409
|
+
// platform regardless of how the kit was installed (npm global,
|
|
410
|
+
// npm link, vendored).
|
|
411
|
+
const nodePath = process.execPath;
|
|
412
|
+
const binDir = join(fileURLToPath(new URL('.', import.meta.url)), '..', 'bin');
|
|
413
|
+
const projectRoot = resolvePath(process.cwd());
|
|
414
|
+
|
|
415
|
+
// Helper: quote a path for the platform's cron-line shell.
|
|
416
|
+
// Linux + macOS: double-quote (the cron line is single-quoted around the
|
|
417
|
+
// whole `echo '...'`; double-quotes inside are safe).
|
|
418
|
+
// Windows: the schtasks /TR value is already double-quoted by registerCron,
|
|
419
|
+
// with `\"` escaping for inner quotes — registerCron's existing
|
|
420
|
+
// escapedCommand handles this.
|
|
421
|
+
const quote = (s) => `"${s}"`;
|
|
422
|
+
|
|
423
|
+
const jobs = [
|
|
424
|
+
{
|
|
425
|
+
label: 'daily-distill',
|
|
426
|
+
command: `${quote(nodePath)} ${quote(join(binDir, 'cmk-daily-distill.mjs'))} ${quote(projectRoot)}`,
|
|
427
|
+
entryName: CRON_ENTRY_NAME,
|
|
428
|
+
schedule: undefined, // registerCron default = daily 23:00
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
label: 'weekly-curate',
|
|
432
|
+
command: `${quote(nodePath)} ${quote(join(binDir, 'cmk-weekly-curate.mjs'))} ${quote(projectRoot)}`,
|
|
433
|
+
entryName: WEEKLY_ENTRY_NAME,
|
|
434
|
+
schedule: DEFAULT_WEEKLY_SCHEDULE,
|
|
435
|
+
},
|
|
436
|
+
];
|
|
437
|
+
let anyError = false;
|
|
438
|
+
let anySuccess = false;
|
|
439
|
+
for (const job of jobs) {
|
|
440
|
+
const r = unregister
|
|
441
|
+
? unregisterCron({ entryName: job.entryName, dryRun })
|
|
442
|
+
: registerCron({
|
|
443
|
+
command: job.command,
|
|
444
|
+
entryName: job.entryName,
|
|
445
|
+
schedule: job.schedule,
|
|
446
|
+
dryRun,
|
|
447
|
+
});
|
|
448
|
+
if (r.action === 'error') {
|
|
449
|
+
anyError = true;
|
|
450
|
+
console.error(
|
|
451
|
+
`cmk register-crons (${job.label}): error — ${(r.errors ?? []).join('; ')}`,
|
|
452
|
+
);
|
|
453
|
+
if (r.error) console.error(` ${r.error}`);
|
|
454
|
+
if (r.output) console.error(r.output);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
anySuccess = true;
|
|
458
|
+
console.log(`cmk register-crons (${job.label}): ${r.action} on ${r.platform}`);
|
|
459
|
+
console.log(` command: ${r.command}`);
|
|
460
|
+
if (r.output) console.log(` output: ${r.output.trim()}`);
|
|
461
|
+
}
|
|
462
|
+
// Task 35.3: maintain the cron-registered sentinel so lazy-compress
|
|
463
|
+
// can short-circuit when cron is active. Skip on --dry-run (no
|
|
464
|
+
// host-scheduler state changed, so kit state shouldn't either).
|
|
465
|
+
//
|
|
466
|
+
// M3 fix (skill-review 2026-05-28): anySuccess gates the sentinel
|
|
467
|
+
// write even on PARTIAL failure (one job registered, the other
|
|
468
|
+
// errored). Correct: at least one cron entry is now active, so
|
|
469
|
+
// detectStaleness SHOULD short-circuit to 'cron-active'. The
|
|
470
|
+
// partial failure surfaces to the user via process.exitCode=2 below
|
|
471
|
+
// — kit state (sentinel) and host-scheduler state (the registered
|
|
472
|
+
// job) stay coherent.
|
|
473
|
+
if (!dryRun) {
|
|
474
|
+
const projectRoot = resolvePath(process.cwd());
|
|
475
|
+
if (unregister) {
|
|
476
|
+
unmarkCronRegistered({ projectRoot });
|
|
477
|
+
} else if (anySuccess) {
|
|
478
|
+
markCronRegistered({ projectRoot });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (anyError) process.exitCode = 2;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* `cmk compress --lazy` (Task 35) — runs the lazy-compress pipeline once.
|
|
486
|
+
* Designed to be invoked as a detached subprocess from inject-context.mjs
|
|
487
|
+
* (SessionStart hook) when staleness is detected and cron is NOT active.
|
|
488
|
+
* Humans normally don't invoke this directly.
|
|
489
|
+
*/
|
|
490
|
+
/**
|
|
491
|
+
* `cmk doctor` (Task 37) — runs the 9 health checks and prints a
|
|
492
|
+
* structured report with repair commands. Per design §14 + tasks.md 37.3.
|
|
493
|
+
*
|
|
494
|
+
* Per NFR-9 + tasks.md 37.5: any recoveryCommand whose underlying
|
|
495
|
+
* action requires a system-level install (pip install / npm install /
|
|
496
|
+
* docker compose up etc.) must NOT be auto-invoked. v0.1.0 surfaces
|
|
497
|
+
* the command to stdout — the user runs it themselves. Auto-repair
|
|
498
|
+
* with --yes is a v0.1.x candidate (design §16).
|
|
499
|
+
*/
|
|
500
|
+
async function runDoctorCli(/* options */) {
|
|
501
|
+
const projectRoot = resolvePath(process.cwd());
|
|
502
|
+
const userDir = join(homedir(), '.claude-memory-kit');
|
|
503
|
+
try {
|
|
504
|
+
const r = await runDoctor({ projectRoot, userDir });
|
|
505
|
+
if (r.action === 'error') {
|
|
506
|
+
console.error(`cmk doctor: error — ${(r.errors ?? []).join('; ')}`);
|
|
507
|
+
process.exitCode = 2;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Structured report: one line per check
|
|
511
|
+
const counts = { pass: 0, fail: 0, skip: 0 };
|
|
512
|
+
for (const c of r.checks) {
|
|
513
|
+
counts[c.status] += 1;
|
|
514
|
+
const statusLabel = c.status.toUpperCase().padEnd(4);
|
|
515
|
+
console.log(`[${statusLabel}] ${c.id}: ${c.name}`);
|
|
516
|
+
console.log(` ${c.message}`);
|
|
517
|
+
if (c.status === 'fail' && c.recoveryCommand) {
|
|
518
|
+
// Repair-command surfaced for the user. Per 37.5 + NFR-9, we
|
|
519
|
+
// do NOT auto-invoke install-requiring repairs — the user
|
|
520
|
+
// copies the command.
|
|
521
|
+
const installNote = c.requiresInstall
|
|
522
|
+
? ' (REQUIRES INSTALL — review before running)'
|
|
523
|
+
: '';
|
|
524
|
+
console.log(` → repair: ${c.recoveryCommand}${installNote}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
console.log('');
|
|
528
|
+
console.log(
|
|
529
|
+
`Summary: ${counts.pass} pass · ${counts.fail} fail · ${counts.skip} skip (${r.duration_ms}ms)`,
|
|
530
|
+
);
|
|
531
|
+
if (counts.fail > 0) process.exitCode = 1;
|
|
532
|
+
} catch (err) {
|
|
533
|
+
console.error(`cmk doctor: unexpected error: ${err?.message ?? err}`);
|
|
534
|
+
process.exitCode = 2;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function runRepairCli(options /* , command */) {
|
|
539
|
+
const projectRoot = resolvePath(process.cwd());
|
|
540
|
+
const userDir = join(homedir(), '.claude-memory-kit');
|
|
541
|
+
// Scope flags: --hooks / --locks / --index → run that one only.
|
|
542
|
+
// --all OR no flag → run all three.
|
|
543
|
+
let scope;
|
|
544
|
+
if (options?.hooks && !options?.locks && !options?.index) scope = 'hooks';
|
|
545
|
+
else if (options?.locks && !options?.hooks && !options?.index) scope = 'locks';
|
|
546
|
+
else if (options?.index && !options?.hooks && !options?.locks) scope = 'index';
|
|
547
|
+
else scope = 'all';
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
const r = await runRepair({ projectRoot, userDir, scope });
|
|
551
|
+
if (r.action === 'error') {
|
|
552
|
+
console.error(`cmk repair: error — ${(r.errors ?? []).join('; ')}`);
|
|
553
|
+
process.exitCode = 2;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
for (const repair of r.repairs) {
|
|
557
|
+
if (repair.error) {
|
|
558
|
+
console.error(`cmk repair (${repair.kind}): error — ${repair.error}`);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
const status = repair.changed ? 'fixed' : 'no-op';
|
|
562
|
+
console.log(`cmk repair (${repair.kind}): ${status}`);
|
|
563
|
+
if (repair.kind === 'hooks' && repair.changed) {
|
|
564
|
+
console.log(` → updated ${repair.settingsPath}`);
|
|
565
|
+
console.log(` events: ${repair.events.join(', ')}`);
|
|
566
|
+
}
|
|
567
|
+
if (repair.kind === 'locks') {
|
|
568
|
+
if (repair.removed && repair.removed.length > 0) {
|
|
569
|
+
for (const l of repair.removed) console.log(` removed: ${l.path} (${l.reason})`);
|
|
570
|
+
}
|
|
571
|
+
if (repair.preserved && repair.preserved.length > 0) {
|
|
572
|
+
for (const l of repair.preserved) console.log(` preserved: ${l.path} (${l.reason})`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (repair.kind === 'index' && repair.changed) {
|
|
576
|
+
console.log(` → reindex completed`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (r.errors > 0) process.exitCode = 1;
|
|
580
|
+
} catch (err) {
|
|
581
|
+
console.error(`cmk repair: unexpected error: ${err?.message ?? err}`);
|
|
582
|
+
process.exitCode = 2;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function runRollCli(options /* , command */) {
|
|
587
|
+
const projectRoot = resolvePath(process.cwd());
|
|
588
|
+
const scope = options?.scope ?? ROLL_SCOPES.NOW;
|
|
589
|
+
// I2 fix (Task 39 skill-review 2026-05-28): dropped unused userDir
|
|
590
|
+
// computation. runRoll's underlying pipelines (compress-session,
|
|
591
|
+
// daily-distill, weekly-curate) all operate purely on projectRoot —
|
|
592
|
+
// none take userDir. Same forward-compat-rot anti-pattern Task 37 M3
|
|
593
|
+
// + Task 38 I1 already removed.
|
|
594
|
+
const { HaikuViaAnthropicApi } = await import('./compressor.mjs');
|
|
595
|
+
try {
|
|
596
|
+
const backend = new HaikuViaAnthropicApi();
|
|
597
|
+
const r = await runRoll({ projectRoot, scope, backend });
|
|
598
|
+
if (r.action === 'error') {
|
|
599
|
+
console.error(`cmk roll: error — ${(r.errors ?? []).join('; ')}`);
|
|
600
|
+
process.exitCode = 2;
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const inner = r.result;
|
|
604
|
+
console.log(`cmk roll --scope ${scope} → ${r.delegatedTo}: ${inner?.action ?? 'unknown'}${inner?.reason ? ` (${inner.reason})` : ''}`);
|
|
605
|
+
} catch (err) {
|
|
606
|
+
console.error(`cmk roll: unexpected error: ${err?.message ?? err}`);
|
|
607
|
+
process.exitCode = 2;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async function runImportAnthropicMemory(options /* , command */) {
|
|
612
|
+
const projectRoot = resolvePath(process.cwd());
|
|
613
|
+
const dryRun = options?.dryRun === true;
|
|
614
|
+
const acceptAll = options?.yes === true;
|
|
615
|
+
try {
|
|
616
|
+
// I1 fix (skill-review 2026-05-28): userDir was unused, dropped.
|
|
617
|
+
const r = await importAnthropicMemory({ projectRoot, dryRun, acceptAll });
|
|
618
|
+
if (r.action === 'error') {
|
|
619
|
+
console.error(`cmk import-anthropic-memory: error — ${(r.errors ?? []).join('; ')}`);
|
|
620
|
+
process.exitCode = 2;
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (r.reason === 'no-source') {
|
|
624
|
+
console.log(`cmk import-anthropic-memory: no Anthropic auto-memory found at ${r.sourcePath}`);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (r.mode === 'dry-run') {
|
|
628
|
+
console.log(`cmk import-anthropic-memory: dry-run — ${r.proposals.length} proposal(s), ${r.skipped} duplicate(s) skipped`);
|
|
629
|
+
for (const p of r.proposals) {
|
|
630
|
+
console.log(` + ${p.id}: ${p.text}`);
|
|
631
|
+
}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (r.mode === 'requires-confirmation') {
|
|
635
|
+
console.log(`cmk import-anthropic-memory: ${r.proposals.length} proposal(s) ready to apply.`);
|
|
636
|
+
console.log(' Re-run with --yes to apply, or --dry-run to inspect.');
|
|
637
|
+
for (const p of r.proposals) {
|
|
638
|
+
console.log(` + ${p.id}: ${p.text}`);
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
console.log(`cmk import-anthropic-memory: applied ${r.accepted} proposal(s), skipped ${r.skipped} duplicate(s)`);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
console.error(`cmk import-anthropic-memory: unexpected error: ${err?.message ?? err}`);
|
|
645
|
+
process.exitCode = 2;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function runTranscriptsDispatch(childName, options) {
|
|
650
|
+
if (childName === 'extract') {
|
|
651
|
+
return runTranscriptsExtract(options);
|
|
652
|
+
}
|
|
653
|
+
console.error(`cmk transcripts: ${NOTICE_PREFIX} (unknown sub-verb '${childName}')`);
|
|
654
|
+
process.exitCode = 2;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function runTranscriptsExtract(options) {
|
|
658
|
+
// Discover sessions per the flags + extract each into the output dir.
|
|
659
|
+
const projectRoot = resolvePath(process.cwd());
|
|
660
|
+
const outputDir = options?.output
|
|
661
|
+
? resolvePath(options.output)
|
|
662
|
+
: join(projectRoot, 'transcripts-extracted');
|
|
663
|
+
const includeThinking = options?.includeThinking === true;
|
|
664
|
+
let sessions;
|
|
665
|
+
try {
|
|
666
|
+
sessions = discoverSessions({
|
|
667
|
+
slug: options?.slug,
|
|
668
|
+
sessionUuidSuffix: options?.session,
|
|
669
|
+
sinceIso: options?.since,
|
|
670
|
+
});
|
|
671
|
+
} catch (err) {
|
|
672
|
+
console.error(`cmk transcripts extract: discovery error: ${err?.message ?? err}`);
|
|
673
|
+
process.exitCode = 2;
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (sessions.length === 0) {
|
|
677
|
+
// S1 fix (Task 38 skill-review 2026-05-28): specialize the message
|
|
678
|
+
// for --session-not-found so the user sees the filter that failed.
|
|
679
|
+
if (options?.session) {
|
|
680
|
+
console.error(
|
|
681
|
+
`cmk transcripts extract: no session matching --session ${options.session}`,
|
|
682
|
+
);
|
|
683
|
+
process.exitCode = 2;
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
console.log('cmk transcripts extract: no sessions found matching filter');
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (options?.session && sessions.length > 1) {
|
|
690
|
+
console.error(`cmk transcripts extract: ambiguous --session match (${sessions.length} candidates):`);
|
|
691
|
+
for (const s of sessions.slice(0, 10)) {
|
|
692
|
+
console.error(` ${s.slug}/${s.sessionId}.jsonl`);
|
|
693
|
+
}
|
|
694
|
+
process.exitCode = 2;
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
let totalTurns = 0;
|
|
698
|
+
let totalBytes = 0;
|
|
699
|
+
for (const s of sessions) {
|
|
700
|
+
const outputPath = join(outputDir, s.slug, `${s.sessionId}.md`);
|
|
701
|
+
try {
|
|
702
|
+
const r = extractTranscript({
|
|
703
|
+
inputPath: s.jsonlPath,
|
|
704
|
+
outputPath,
|
|
705
|
+
includeThinking,
|
|
706
|
+
});
|
|
707
|
+
if (r.action === 'error') {
|
|
708
|
+
console.error(` ${s.sessionId}: error — ${(r.errors ?? []).join('; ')}`);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
totalTurns += r.turnsKept;
|
|
712
|
+
totalBytes += r.outputSize;
|
|
713
|
+
console.log(` ${s.slug}/${s.sessionId}: ${r.turnsKept} turn(s) → ${outputPath}`);
|
|
714
|
+
} catch (err) {
|
|
715
|
+
console.error(` ${s.sessionId}: unexpected error: ${err?.message ?? err}`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
console.log(`cmk transcripts extract: processed ${sessions.length} session(s); ${totalTurns} total turns; ${(totalBytes / 1024 / 1024).toFixed(2)} MB written`);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function runCompress(options /* , command */) {
|
|
722
|
+
const lazy = options?.lazy === true;
|
|
723
|
+
if (!lazy) {
|
|
724
|
+
// S1 fix (skill-review 2026-05-28): exit 2 on missing --lazy so
|
|
725
|
+
// scripts can distinguish "command ran" from "command rejected its
|
|
726
|
+
// input". Matches NOTICE_PREFIX convention elsewhere in v0.1.0.
|
|
727
|
+
console.error(
|
|
728
|
+
`cmk compress: ${NOTICE_PREFIX} (the --lazy flag is required for v0.1.0; bare \`cmk compress\` is a v0.1.x candidate — see design §16)`,
|
|
729
|
+
);
|
|
730
|
+
process.exitCode = 2;
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const projectRoot = resolvePath(process.cwd());
|
|
734
|
+
const { HaikuViaAnthropicApi } = await import('./compressor.mjs');
|
|
735
|
+
try {
|
|
736
|
+
const backend = new HaikuViaAnthropicApi();
|
|
737
|
+
const r = await runLazyCompress({ projectRoot, backend });
|
|
738
|
+
if (r.action === 'error') {
|
|
739
|
+
console.error(
|
|
740
|
+
`cmk compress --lazy: error (${r.errorCategory ?? 'unknown'})${(r.errors && r.errors.length) ? `: ${r.errors.join('; ')}` : ''}`,
|
|
741
|
+
);
|
|
742
|
+
} else {
|
|
743
|
+
console.log(
|
|
744
|
+
`cmk compress --lazy: ${r.action}${r.reason ? ` (${r.reason})` : ''}${r.delegatedTo ? ` → ${r.delegatedTo}` : ''}`,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
} catch (err) {
|
|
748
|
+
console.error(`cmk compress --lazy: unexpected error: ${err?.message ?? err}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async function runMcpDispatch(childName) {
|
|
753
|
+
if (childName === 'serve') {
|
|
754
|
+
const projectRoot = resolvePath(process.cwd());
|
|
755
|
+
const userDir = join(homedir(), '.claude-memory-kit');
|
|
756
|
+
// ALL logs to stderr per design §10.1; stdout is reserved for
|
|
757
|
+
// JSON-RPC messages handled by the SDK's StdioServerTransport.
|
|
758
|
+
// Don't console.log() anything before/during the server's run.
|
|
759
|
+
try {
|
|
760
|
+
await runMcpServer({ projectRoot, userDir });
|
|
761
|
+
} catch (err) {
|
|
762
|
+
console.error(`cmk mcp serve: fatal — ${err?.message ?? err}`);
|
|
763
|
+
process.exitCode = 2;
|
|
764
|
+
}
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
console.error(`cmk mcp: ${NOTICE_PREFIX} (unknown sub-verb '${childName}')`);
|
|
768
|
+
process.exitCode = 2;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async function runQueueDispatch(childName) {
|
|
772
|
+
if (childName === 'conflicts') {
|
|
773
|
+
return runQueueConflicts();
|
|
774
|
+
}
|
|
775
|
+
if (childName === 'review') {
|
|
776
|
+
return runQueueReview();
|
|
777
|
+
}
|
|
778
|
+
console.log(`cmk queue: ${NOTICE_PREFIX} (unknown sub-verb '${childName}')`);
|
|
779
|
+
process.exitCode = 2;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Interactive resolver for `cmk queue conflicts`. Walks pending
|
|
784
|
+
* entries one-at-a-time, prints existing + proposed text, asks for
|
|
785
|
+
* one of `keep-old` / `keep-new` / `merge-both` / `skip`. Loops
|
|
786
|
+
* until the queue is empty or the user signals end-of-input.
|
|
787
|
+
*
|
|
788
|
+
* For v0.1.0 this resolves the PROJECT tier's conflicts queue (the
|
|
789
|
+
* canonical kit usage). User-tier / language-tier conflicts queues
|
|
790
|
+
* can be added when the kit's CLI gains explicit `--tier` selection.
|
|
791
|
+
*/
|
|
792
|
+
async function runQueueConflicts() {
|
|
793
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
794
|
+
const askOnce = (q) =>
|
|
795
|
+
new Promise((resolve) => {
|
|
796
|
+
rl.question(q, (answer) => resolve(answer));
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
const VALID_DECISIONS = new Set(['keep-old', 'keep-new', 'merge-both', 'skip']);
|
|
800
|
+
|
|
801
|
+
const prompter = async ({
|
|
802
|
+
proposedId,
|
|
803
|
+
proposedText,
|
|
804
|
+
proposedTrust,
|
|
805
|
+
existingId,
|
|
806
|
+
existingText,
|
|
807
|
+
existingTrust,
|
|
808
|
+
similarity,
|
|
809
|
+
}) => {
|
|
810
|
+
console.log('');
|
|
811
|
+
console.log('─── pending conflict ──────────────────────────────────────');
|
|
812
|
+
console.log(`existing (${existingId}, trust=${existingTrust}): ${existingText}`);
|
|
813
|
+
console.log(`proposed (${proposedId}, trust=${proposedTrust}): ${proposedText}`);
|
|
814
|
+
console.log(`similarity: ${Number(similarity).toFixed(4)}`);
|
|
815
|
+
let decision = '';
|
|
816
|
+
while (!VALID_DECISIONS.has(decision)) {
|
|
817
|
+
const answer = await askOnce(
|
|
818
|
+
` [keep-old / keep-new / merge-both / skip]: `,
|
|
819
|
+
);
|
|
820
|
+
decision = String(answer).trim();
|
|
821
|
+
if (!VALID_DECISIONS.has(decision)) {
|
|
822
|
+
console.log(
|
|
823
|
+
` unknown answer "${decision}" — please type one of: keep-old, keep-new, merge-both, skip`,
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return decision;
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
// merge-both wiring (Task 25b — closes Task 25's cross-layer
|
|
831
|
+
// composition gap). The proposed bullet from the conflict queue
|
|
832
|
+
// wasn't materialized as a Layer-2 per-fact file (it was routed to
|
|
833
|
+
// `queues/conflicts.md` instead of MEMORY.md). So we DON'T call
|
|
834
|
+
// `mergeFacts` (Layer 2); we call `mergeScratchpadBullets` (Layer 3)
|
|
835
|
+
// which operates directly on the scratchpad: combines the two
|
|
836
|
+
// bullet texts, writes a new merged bullet with a fresh canonical
|
|
837
|
+
// ID + provenance citing both sources, and mutates both originals'
|
|
838
|
+
// provenance to inject `superseded_by: <newId>`.
|
|
839
|
+
//
|
|
840
|
+
// For Task 25b's v0.1.0 ship, the merger assumes the kit's default
|
|
841
|
+
// scratchpad (MEMORY.md under `context/`). Section discovery: the
|
|
842
|
+
// queue entry written by `writeConflictEntry` does NOT capture the
|
|
843
|
+
// existing bullet's section heading — the merger receives `section`
|
|
844
|
+
// here as undefined from `resolveConflictQueue` (it doesn't pass
|
|
845
|
+
// through), and `mergeScratchpadBullets` falls back to
|
|
846
|
+
// `discoverSectionAt(lines, matchA.bulletIdx)` to find the heading
|
|
847
|
+
// by walking back from the existing bullet's position. That fallback
|
|
848
|
+
// is the documented contract for v0.1.0. Per-candidate section
|
|
849
|
+
// capture in `writeConflictEntry`'s queue entry is a v0.1.x
|
|
850
|
+
// candidate — see design §6.8 + §16.x notes for the trade-off.
|
|
851
|
+
const mergeFn = async ({
|
|
852
|
+
tier,
|
|
853
|
+
projectRoot,
|
|
854
|
+
userDir,
|
|
855
|
+
proposedId,
|
|
856
|
+
proposedText,
|
|
857
|
+
existingId,
|
|
858
|
+
existingText,
|
|
859
|
+
section,
|
|
860
|
+
}) => {
|
|
861
|
+
// Default scratchpad is MEMORY.md at the project tier. Section
|
|
862
|
+
// comes from the queue entry (which captured it at detect time).
|
|
863
|
+
const scratchpadPath = resolvePath(projectRoot, 'context', 'MEMORY.md');
|
|
864
|
+
const result = mergeScratchpadBullets({
|
|
865
|
+
tier,
|
|
866
|
+
projectRoot,
|
|
867
|
+
userDir,
|
|
868
|
+
scratchpadPath,
|
|
869
|
+
section,
|
|
870
|
+
idA: existingId,
|
|
871
|
+
idB: proposedId,
|
|
872
|
+
});
|
|
873
|
+
if (result.action === 'error') {
|
|
874
|
+
console.error(
|
|
875
|
+
`cmk queue conflicts: merge-both for ${existingId} + ${proposedId} failed: ${result.errors.join('; ')}`,
|
|
876
|
+
);
|
|
877
|
+
} else {
|
|
878
|
+
console.log(
|
|
879
|
+
` merge-both → ${existingId} + ${proposedId} merged into ${result.id}`,
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
try {
|
|
885
|
+
const result = await resolveConflictQueue({
|
|
886
|
+
tier: 'P',
|
|
887
|
+
projectRoot: process.cwd(),
|
|
888
|
+
prompter,
|
|
889
|
+
mergeFn,
|
|
890
|
+
});
|
|
891
|
+
if (result.action === 'error') {
|
|
892
|
+
for (const e of result.errors) console.error(`cmk queue conflicts: ${e}`);
|
|
893
|
+
process.exitCode = 2;
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
console.log('');
|
|
897
|
+
console.log(
|
|
898
|
+
`cmk queue conflicts: ${result.resolved} resolved (${result.kept_old} kept-old, ${result.kept_new} kept-new, ${result.merged} merged), ${result.skipped} skipped`,
|
|
899
|
+
);
|
|
900
|
+
} finally {
|
|
901
|
+
rl.close();
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Interactive resolver for `cmk queue review` (Task 26). Walks
|
|
907
|
+
* pending medium-trust auto-extract candidates one-at-a-time, prints
|
|
908
|
+
* the candidate text + provenance, asks for one of `promote` /
|
|
909
|
+
* `discard` / `skip`. Loops until the queue is empty or user signals
|
|
910
|
+
* end-of-input.
|
|
911
|
+
*
|
|
912
|
+
* Resolves the PROJECT tier's review queue (the canonical kit usage).
|
|
913
|
+
*/
|
|
914
|
+
async function runQueueReview() {
|
|
915
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
916
|
+
const askOnce = (q) =>
|
|
917
|
+
new Promise((resolve) => {
|
|
918
|
+
rl.question(q, (answer) => resolve(answer));
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
const VALID_DECISIONS = new Set(['promote', 'discard', 'skip']);
|
|
922
|
+
|
|
923
|
+
const prompter = async ({ id, text, ts, provenance }) => {
|
|
924
|
+
console.log('');
|
|
925
|
+
console.log('─── pending review ────────────────────────────────────────');
|
|
926
|
+
console.log(`id: ${id}`);
|
|
927
|
+
console.log(`ts: ${ts}`);
|
|
928
|
+
console.log(`text: ${text}`);
|
|
929
|
+
if (provenance) console.log(`prov: ${provenance.trim()}`);
|
|
930
|
+
let decision = '';
|
|
931
|
+
while (!VALID_DECISIONS.has(decision)) {
|
|
932
|
+
const answer = await askOnce(` [promote / discard / skip]: `);
|
|
933
|
+
decision = String(answer).trim();
|
|
934
|
+
if (!VALID_DECISIONS.has(decision)) {
|
|
935
|
+
console.log(
|
|
936
|
+
` unknown answer "${decision}" — please type one of: promote, discard, skip`,
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return decision;
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
try {
|
|
944
|
+
const result = await resolveReviewQueue({
|
|
945
|
+
tier: 'P',
|
|
946
|
+
projectRoot: process.cwd(),
|
|
947
|
+
prompter,
|
|
948
|
+
});
|
|
949
|
+
if (result.action === 'error') {
|
|
950
|
+
for (const e of result.errors) console.error(`cmk queue review: ${e}`);
|
|
951
|
+
process.exitCode = 2;
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
console.log('');
|
|
955
|
+
console.log(
|
|
956
|
+
`cmk queue review: ${result.promoted} promoted, ${result.discarded} discarded, ${result.skipped} skipped${result.errors && result.errors.length ? `, ${result.errors.length} errored` : ''}`,
|
|
957
|
+
);
|
|
958
|
+
if (result.errors && result.errors.length) {
|
|
959
|
+
for (const err of result.errors) {
|
|
960
|
+
console.error(
|
|
961
|
+
` error on ${err.id} (${err.decision}): ${err.errors.join('; ')}`,
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
} finally {
|
|
966
|
+
rl.close();
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/** Helper: build a stub action that prints the standard notice + exits 0. */
|
|
971
|
+
function stub(name, milestone, extra) {
|
|
972
|
+
return function action(/* args, options */) {
|
|
973
|
+
const tail = milestone === 'v0.1.x' ? `${milestone}` : `milestone ${milestone}`;
|
|
974
|
+
const detail = extra ? ` (${extra})` : '';
|
|
975
|
+
console.log(`cmk ${name}: ${NOTICE_PREFIX} (${tail})${detail}`);
|
|
976
|
+
// commander already returns to its caller; explicit exit not needed for
|
|
977
|
+
// stubs and would prevent the test harness from running multiple cases.
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* @typedef {Object} ArgSpec
|
|
983
|
+
* @property {string} flags - commander argument string, e.g. "<id>" or "[query...]"
|
|
984
|
+
* @property {string} description
|
|
985
|
+
*
|
|
986
|
+
* @typedef {Object} OptionSpec
|
|
987
|
+
* @property {string} flags - commander option flags, e.g. "--dry-run"
|
|
988
|
+
* @property {string} description
|
|
989
|
+
*
|
|
990
|
+
* @typedef {Object} SubcommandChild
|
|
991
|
+
* @property {string} name
|
|
992
|
+
* @property {string} description
|
|
993
|
+
* @property {ArgSpec[]=} argSpec
|
|
994
|
+
* @property {OptionSpec[]=} optionSpec
|
|
995
|
+
*
|
|
996
|
+
* @typedef {Object} Subcommand
|
|
997
|
+
* @property {string} name
|
|
998
|
+
* @property {string} description
|
|
999
|
+
* @property {string|number} milestone - tasks.md task number or "v0.1.x"
|
|
1000
|
+
* @property {ArgSpec[]=} argSpec
|
|
1001
|
+
* @property {OptionSpec[]=} optionSpec
|
|
1002
|
+
* @property {SubcommandChild[]=} children
|
|
1003
|
+
* @property {(name?: string, ...rest: any[]) => void} action
|
|
1004
|
+
*/
|
|
1005
|
+
|
|
1006
|
+
/** @type {Subcommand[]} */
|
|
1007
|
+
export const subcommands = [
|
|
1008
|
+
{
|
|
1009
|
+
name: 'install',
|
|
1010
|
+
description: 'cross-OS one-shot install — scaffold 3-tier dirs + inject .gitignore + drop kit CLAUDE.md block',
|
|
1011
|
+
milestone: 3,
|
|
1012
|
+
optionSpec: [
|
|
1013
|
+
{ flags: '--force', description: 'allow downgrade of an existing newer-version CLAUDE.md block' },
|
|
1014
|
+
],
|
|
1015
|
+
action: runInstall,
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
name: 'uninstall',
|
|
1019
|
+
description: 'remove the CLAUDE.md kit block (preserves everything else byte-for-byte)',
|
|
1020
|
+
milestone: 4,
|
|
1021
|
+
action: runUninstall,
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
name: 'init-user-tier',
|
|
1025
|
+
description: 'scaffold ~/.claude-memory-kit/ (honors $MEMORY_KIT_USER_DIR override)',
|
|
1026
|
+
milestone: 14,
|
|
1027
|
+
action: runInitUserTier,
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
name: 'search',
|
|
1031
|
+
description: 'search memory — hybrid keyword + optional semantic',
|
|
1032
|
+
milestone: 30,
|
|
1033
|
+
argSpec: [{ flags: '<query...>', description: 'query terms' }],
|
|
1034
|
+
optionSpec: [
|
|
1035
|
+
{ flags: '--mode <mode>', description: 'keyword | semantic | hybrid (default: keyword; semantic+hybrid need memsearch — Layer 5b install, not in v0.1.0)' },
|
|
1036
|
+
{ flags: '--min-trust <level>', description: 'low | medium | high' },
|
|
1037
|
+
{ flags: '--tier <tier>', description: 'U | P | L (filter to a single tier)' },
|
|
1038
|
+
{ flags: '--since <date>', description: 'ISO date — exclude observations older than this' },
|
|
1039
|
+
{ flags: '--limit <n>', description: 'max results (default: 20)' },
|
|
1040
|
+
{ flags: '--include-tombstoned', description: 'include deleted observations in results' },
|
|
1041
|
+
],
|
|
1042
|
+
action: runSearch,
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
name: 'reindex',
|
|
1046
|
+
description: 'rebuild the markdown INDEX.md pointer index for the project tier',
|
|
1047
|
+
milestone: 8,
|
|
1048
|
+
optionSpec: [
|
|
1049
|
+
{ flags: '--boot', description: 'incremental — re-index only changed files' },
|
|
1050
|
+
{ flags: '--full', description: 'drop the cache and rebuild from scratch' },
|
|
1051
|
+
],
|
|
1052
|
+
action: runReindex,
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
name: 'doctor',
|
|
1056
|
+
description: 'run health checks HC-1..HC-9; print structured report with self-repair commands',
|
|
1057
|
+
milestone: 37,
|
|
1058
|
+
action: runDoctorCli,
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
name: 'config',
|
|
1062
|
+
description: 'settings access (per design §7.2)',
|
|
1063
|
+
milestone: 'v0.1.x',
|
|
1064
|
+
optionSpec: [
|
|
1065
|
+
{ flags: '--show-origin <key>', description: 'print where each value comes from (project / user / local tier)' },
|
|
1066
|
+
],
|
|
1067
|
+
children: [
|
|
1068
|
+
{
|
|
1069
|
+
name: 'get',
|
|
1070
|
+
description: 'print the resolved value of a setting',
|
|
1071
|
+
argSpec: [{ flags: '<key>', description: 'setting key (dotted path)' }],
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
name: 'set',
|
|
1075
|
+
description: 'set a setting in the current tier',
|
|
1076
|
+
argSpec: [
|
|
1077
|
+
{ flags: '<key>', description: 'setting key (dotted path)' },
|
|
1078
|
+
{ flags: '<value>', description: 'new value' },
|
|
1079
|
+
],
|
|
1080
|
+
},
|
|
1081
|
+
],
|
|
1082
|
+
action: stub('config', 'v0.1.x'),
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
name: 'view',
|
|
1086
|
+
description: 'open a local markdown viewer at 127.0.0.1:37778',
|
|
1087
|
+
milestone: 'v0.1.x',
|
|
1088
|
+
optionSpec: [{ flags: '--port <n>', description: 'override default port 37778' }],
|
|
1089
|
+
action: stub('view', 'v0.1.x'),
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
name: 'import-anthropic-memory',
|
|
1093
|
+
description: "merge useful bullets from Anthropic's auto-memory into this project's MEMORY.md",
|
|
1094
|
+
milestone: 38,
|
|
1095
|
+
optionSpec: [
|
|
1096
|
+
{ flags: '--dry-run', description: 'print proposed additions without modifying files' },
|
|
1097
|
+
{ flags: '--yes', description: 'apply every proposal without prompting (v0.1.0 requires explicit --yes; interactive y/N is v0.1.x)' },
|
|
1098
|
+
],
|
|
1099
|
+
action: runImportAnthropicMemory,
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
name: 'transcripts',
|
|
1103
|
+
description: "extract clean markdown transcripts from Claude Code session jsonls under ~/.claude/projects/",
|
|
1104
|
+
milestone: 38,
|
|
1105
|
+
children: [
|
|
1106
|
+
{
|
|
1107
|
+
name: 'extract',
|
|
1108
|
+
description: 'extract one or more session jsonls into clean markdown',
|
|
1109
|
+
optionSpec: [
|
|
1110
|
+
{ flags: '--session <uuid-suffix>', description: 'extract a specific session by uuid suffix (substring match across all slugs)' },
|
|
1111
|
+
{ flags: '--slug <slug>', description: 'extract all sessions under a specific Anthropic slug' },
|
|
1112
|
+
{ flags: '--since <YYYY-MM-DD>', description: 'extract only sessions with mtime >= this date' },
|
|
1113
|
+
{ flags: '--output <dir>', description: 'output directory (default: <cwd>/transcripts-extracted/)' },
|
|
1114
|
+
{ flags: '--include-thinking', description: 'retain the agent\'s [thinking] blocks (omitted by default)' },
|
|
1115
|
+
],
|
|
1116
|
+
action: (options) => runTranscriptsDispatch('extract', options),
|
|
1117
|
+
},
|
|
1118
|
+
],
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
name: 'trust',
|
|
1122
|
+
description: 'manually override the trust level of an observation (fact file or scratchpad bullet)',
|
|
1123
|
+
milestone: 15,
|
|
1124
|
+
argSpec: [
|
|
1125
|
+
{ flags: '<id>', description: 'citation ID (e.g. P-S79MJHFN)' },
|
|
1126
|
+
{ flags: '<level>', description: 'low | medium | high' },
|
|
1127
|
+
],
|
|
1128
|
+
action: runTrust,
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
name: 'lessons',
|
|
1132
|
+
description: 'promote project-tier observations to the user-tier LESSONS.md',
|
|
1133
|
+
milestone: 'v0.1.x',
|
|
1134
|
+
children: [
|
|
1135
|
+
{
|
|
1136
|
+
name: 'promote',
|
|
1137
|
+
description: 'move a project observation to ~/.claude-memory-kit/LESSONS.md',
|
|
1138
|
+
argSpec: [{ flags: '<id>', description: 'citation ID' }],
|
|
1139
|
+
},
|
|
1140
|
+
],
|
|
1141
|
+
action: stub('lessons', 'v0.1.x'),
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
name: 'queue',
|
|
1145
|
+
description: 'review medium-trust auto-extracts and resolve conflicting observations',
|
|
1146
|
+
milestone: 25,
|
|
1147
|
+
children: [
|
|
1148
|
+
{ name: 'review', description: 'walk pending medium-trust auto-extracts; promote / discard / skip' },
|
|
1149
|
+
{ name: 'conflicts', description: 'walk pending conflicts; keep-old / keep-new / merge-both / skip' },
|
|
1150
|
+
],
|
|
1151
|
+
action: runQueueDispatch,
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
name: 'forget',
|
|
1155
|
+
description: 'tombstone an observation (preserves audit trail; never silent delete)',
|
|
1156
|
+
milestone: 9,
|
|
1157
|
+
argSpec: [{ flags: '<id-or-query>', description: 'citation ID or substring query against canonical text' }],
|
|
1158
|
+
optionSpec: [
|
|
1159
|
+
{ flags: '--yes', description: 'skip the interactive confirmation prompt (required in v0.1.0; interactive prompt is a v0.1.x follow-up)' },
|
|
1160
|
+
{ flags: '--reason <text>', description: 'deletion reason recorded in the tombstone frontmatter' },
|
|
1161
|
+
{ flags: '--deleted-by <enum>', description: 'who initiated the deletion (default: user-explicit)' },
|
|
1162
|
+
],
|
|
1163
|
+
action: runForget,
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
name: 'purge',
|
|
1167
|
+
description: 'permanent deletion of an observation — rare; bypasses the tombstone audit trail',
|
|
1168
|
+
milestone: 'v0.1.x',
|
|
1169
|
+
argSpec: [{ flags: '<id>', description: 'citation ID' }],
|
|
1170
|
+
optionSpec: [{ flags: '--hard', description: 'required confirmation flag' }],
|
|
1171
|
+
action: stub('purge', 'v0.1.x', 'use `cmk forget` for normal deletion; this is for emergencies only'),
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
name: 'roll',
|
|
1175
|
+
description: 'force-roll the rolling-window pipeline (same internals as SessionEnd / cron)',
|
|
1176
|
+
milestone: 39,
|
|
1177
|
+
optionSpec: [{ flags: '--scope <scope>', description: 'now (default — task 22 compress-session) | today (task 33 daily-distill) | recent (task 34 weekly-curate)' }],
|
|
1178
|
+
action: runRollCli,
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
name: 'repair',
|
|
1182
|
+
description: 'idempotent self-repair — re-register hooks, reset stale locks, rebuild index',
|
|
1183
|
+
milestone: 39,
|
|
1184
|
+
optionSpec: [
|
|
1185
|
+
{ flags: '--hooks', description: 're-register hooks from template (merges kit hooks into .claude/settings.json)' },
|
|
1186
|
+
{ flags: '--locks', description: 'clear stale locks (>1h old by default)' },
|
|
1187
|
+
{ flags: '--index', description: 'invoke `cmk reindex --full`' },
|
|
1188
|
+
{ flags: '--all', description: 'run all three repairs in order (default if no scope flag given)' },
|
|
1189
|
+
],
|
|
1190
|
+
action: runRepairCli,
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
name: 'daily-distill',
|
|
1194
|
+
description: 'run the daily-distill pipeline once (invoked by host scheduler; humans normally use `cmk register-crons`)',
|
|
1195
|
+
milestone: 33,
|
|
1196
|
+
action: runDailyDistill,
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
name: 'weekly-curate',
|
|
1200
|
+
description: 'run the weekly-curate pipeline once: archive today-*.md older than 7 days, dedup bullets, rebuild recent.md (invoked by host scheduler)',
|
|
1201
|
+
milestone: 34,
|
|
1202
|
+
action: runWeeklyCurate,
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
name: 'compress',
|
|
1206
|
+
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).',
|
|
1207
|
+
milestone: 35,
|
|
1208
|
+
optionSpec: [
|
|
1209
|
+
{ flags: '--lazy', description: 'run the lazy-compress cycle (the v0.1.0 supported invocation)' },
|
|
1210
|
+
],
|
|
1211
|
+
action: runCompress,
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
name: 'register-crons',
|
|
1215
|
+
description: 'register both daily-distill (23:00) and weekly-curate (Sun 09:00) cron jobs with the host scheduler',
|
|
1216
|
+
milestone: 33,
|
|
1217
|
+
optionSpec: [
|
|
1218
|
+
{ flags: '--dry-run', description: 'print the platform-detected command without executing' },
|
|
1219
|
+
{ flags: '--unregister', description: 'remove both daily-distill and weekly-curate entries instead of adding them' },
|
|
1220
|
+
],
|
|
1221
|
+
action: runRegisterCrons,
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
name: 'mcp',
|
|
1225
|
+
description: 'run the MCP server over stdio (invoked by Claude Code, not by humans)',
|
|
1226
|
+
milestone: 31,
|
|
1227
|
+
children: [{ name: 'serve', description: 'start the stdio MCP server; JSON-RPC on stdin/stdout' }],
|
|
1228
|
+
action: runMcpDispatch,
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
name: 'version',
|
|
1232
|
+
description: 'print the cmk version (alias for --version)',
|
|
1233
|
+
milestone: 'always',
|
|
1234
|
+
action: function action() {
|
|
1235
|
+
// version is special — never a stub. Print and continue.
|
|
1236
|
+
// The shared --version flag is wired in index.mjs; this verb
|
|
1237
|
+
// exists for parity with design §12 and prints the same string.
|
|
1238
|
+
// Implementation note: we resolve the version from package.json
|
|
1239
|
+
// already done at program-build time, so this can simply call into
|
|
1240
|
+
// the main flag handler via process.exit after printing.
|
|
1241
|
+
// For v0.1.0 stub-only milestone, defer the actual print to the
|
|
1242
|
+
// top-level --version handler.
|
|
1243
|
+
console.log(`cmk version: see \`cmk --version\``);
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
];
|
|
1247
|
+
|
|
1248
|
+
/** Names list — handy for tests + help-output assertions. */
|
|
1249
|
+
export const subcommandNames = subcommands.map((s) => s.name);
|
|
1250
|
+
|
|
1251
|
+
/** Notice string — exposed so tests can assert it appears in every stub output. */
|
|
1252
|
+
export const STUB_NOTICE_PREFIX = NOTICE_PREFIX;
|