@lh8ppl/claude-memory-kit 0.3.5 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -50
- package/bin/cmk-approve-permission.mjs +62 -0
- package/bin/cmk-daily-distill.mjs +14 -0
- package/bin/cmk-guard-memory.mjs +57 -0
- package/bin/cmk-inject-context.mjs +12 -0
- package/bin/cmk-weekly-curate.mjs +12 -0
- package/package.json +4 -2
- package/src/agent-profile.mjs +115 -0
- package/src/agent-profiles.mjs +118 -0
- package/src/approve-permission.mjs +92 -0
- package/src/auto-extract.mjs +17 -10
- package/src/auto-persona.mjs +11 -4
- package/src/compaction-state.mjs +204 -0
- package/src/compress-session.mjs +13 -1
- package/src/config-core.mjs +7 -9
- package/src/decisions-journal.mjs +71 -3
- package/src/doctor.mjs +128 -5
- package/src/guard-memory.mjs +151 -0
- package/src/import-anthropic-memory.mjs +15 -1
- package/src/inject-context.mjs +42 -18
- package/src/install-agent.mjs +220 -0
- package/src/install-kiro.mjs +287 -0
- package/src/install.mjs +53 -7
- package/src/kiro-cli-agent.mjs +270 -0
- package/src/kiro-constants.mjs +19 -0
- package/src/kiro-hook-bin.mjs +105 -0
- package/src/kiro-hook-command.mjs +67 -0
- package/src/kiro-hook-dispatch.mjs +115 -0
- package/src/kiro-ide-hooks.mjs +219 -0
- package/src/kiro-permissions.mjs +175 -0
- package/src/kiro-skills.mjs +96 -0
- package/src/kiro-transcript.mjs +366 -0
- package/src/kiro-trusted-commands.mjs +130 -0
- package/src/lazy-compress.mjs +43 -110
- package/src/managed-block.mjs +138 -0
- package/src/memory-write.mjs +23 -8
- package/src/mutate-agent-config.mjs +243 -0
- package/src/read-json.mjs +43 -0
- package/src/register-crons.mjs +31 -0
- package/src/reindex.mjs +15 -2
- package/src/repair.mjs +39 -3
- package/src/result-shapes.mjs +8 -0
- package/src/review-queue.mjs +3 -0
- package/src/scratchpad.mjs +12 -2
- package/src/search.mjs +12 -5
- package/src/semantic-backend.mjs +7 -9
- package/src/settings-hooks.mjs +70 -3
- package/src/subcommands.mjs +360 -27
- package/src/tier-paths.mjs +82 -1
- package/src/weekly-curate.mjs +6 -2
- package/template/.claude/skills/memory-search/SKILL.md +14 -1
- package/template/.claude/skills/memory-write/SKILL.md +37 -1
- package/template/project/memory/INDEX.md.template +1 -1
package/src/subcommands.mjs
CHANGED
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
// asserts exactly what's exported here, so coverage stays automatic.
|
|
15
15
|
|
|
16
16
|
import { install as installAction, initUserTier as initUserTierAction } from './install.mjs';
|
|
17
|
+
import { installAgent } from './install-agent.mjs';
|
|
18
|
+
import { installKiro, uninstallKiro } from './install-kiro.mjs';
|
|
19
|
+
import { getAgentProfile, listAgentProfiles } from './agent-profiles.mjs';
|
|
20
|
+
import { runKiroHook } from './kiro-hook-bin.mjs';
|
|
21
|
+
import { readKiroTurn } from './kiro-transcript.mjs';
|
|
22
|
+
import { injectContext } from './inject-context.mjs';
|
|
23
|
+
import { captureTurn } from './capture-turn.mjs';
|
|
24
|
+
import { capturePrompt } from './capture-prompt.mjs';
|
|
25
|
+
import { observeEdit } from './observe-edit.mjs';
|
|
26
|
+
import { decideGuard } from './guard-memory.mjs';
|
|
17
27
|
import { removeClaudeMdBlock } from './claude-md.mjs';
|
|
18
28
|
import { reindex as reindexAction } from './reindex.mjs';
|
|
19
29
|
import { openIndexDb } from './index-db.mjs';
|
|
@@ -52,7 +62,7 @@ import {
|
|
|
52
62
|
} from './register-crons.mjs';
|
|
53
63
|
import { fileURLToPath } from 'node:url';
|
|
54
64
|
import { dirname } from 'node:path';
|
|
55
|
-
import { readFileSync } from 'node:fs';
|
|
65
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
56
66
|
|
|
57
67
|
const __filename_subcommands = fileURLToPath(import.meta.url);
|
|
58
68
|
const __dirname_subcommands = dirname(__filename_subcommands);
|
|
@@ -65,6 +75,27 @@ import { createInterface } from 'node:readline';
|
|
|
65
75
|
import { spawnSync } from 'node:child_process';
|
|
66
76
|
import { checkKitBinding } from './native-binding.mjs';
|
|
67
77
|
import { resolve as resolvePath, join, basename } from 'node:path';
|
|
78
|
+
import { resolveMcpProjectRoot, normalizeProjectPath } from './tier-paths.mjs';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the cmk-auto-extract.mjs bin path so the KIRO stop hook's captureTurn
|
|
82
|
+
* can spawn the detached auto-extract child (D-200). Mirrors the Claude Code bin's
|
|
83
|
+
* resolution (env override → sibling bin/): without this, runHook called
|
|
84
|
+
* captureTurn with no autoExtractPath and the in-module default is null, so
|
|
85
|
+
* spawnAutoExtract short-circuits 'no-auto-extract-path' — capture writes the
|
|
86
|
+
* transcript but NEVER extracts facts → no wedge promotion. The CLI's runHook
|
|
87
|
+
* lives in src/, so the bin is a `../bin/` sibling. A missing bin → null → no
|
|
88
|
+
* spawn (fail-safe for a never-crash hook); spawnAutoExtract logs the reason.
|
|
89
|
+
* @param {Record<string,string|undefined>} [env=process.env]
|
|
90
|
+
* @returns {string|null} absolute path to the bin, or null if it can't be found
|
|
91
|
+
*/
|
|
92
|
+
export function resolveKiroAutoExtractPath(env = process.env) {
|
|
93
|
+
if (env.CMK_AUTO_EXTRACT_PATH && env.CMK_AUTO_EXTRACT_PATH.trim() !== '') {
|
|
94
|
+
return env.CMK_AUTO_EXTRACT_PATH;
|
|
95
|
+
}
|
|
96
|
+
const sibling = join(__dirname_subcommands, '..', 'bin', 'cmk-auto-extract.mjs');
|
|
97
|
+
return existsSync(sibling) ? sibling : null;
|
|
98
|
+
}
|
|
68
99
|
|
|
69
100
|
const NOTICE_PREFIX = 'not yet implemented';
|
|
70
101
|
|
|
@@ -169,6 +200,16 @@ export async function runInstall(options /* , command */) {
|
|
|
169
200
|
// commander maps `--no-hooks` to options.hooks === false.
|
|
170
201
|
const noHooks = !!(options && options.hooks === false);
|
|
171
202
|
const verbose = !!(options && options.verbose);
|
|
203
|
+
|
|
204
|
+
// Task 50.F — cross-agent routing. Default is claude-code (the existing path,
|
|
205
|
+
// untouched). For another agent, scaffold the agent-neutral project tier via
|
|
206
|
+
// installAction with hooks OFF (the Claude-Code hook wiring doesn't apply), then
|
|
207
|
+
// wire THAT agent's legs (hooks + MCP + instruction file) via installAgent.
|
|
208
|
+
const ide = (options && options.ide) || 'claude-code';
|
|
209
|
+
if (ide !== 'claude-code') {
|
|
210
|
+
return runInstallForAgent({ ide, options, log, logError });
|
|
211
|
+
}
|
|
212
|
+
|
|
172
213
|
const result = await installAction({
|
|
173
214
|
force: !!(options && options.force),
|
|
174
215
|
noHooks,
|
|
@@ -254,20 +295,281 @@ export async function runInstall(options /* , command */) {
|
|
|
254
295
|
}
|
|
255
296
|
|
|
256
297
|
/**
|
|
257
|
-
*
|
|
258
|
-
* the project
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
298
|
+
* Task 50.F — `cmk install --ide <agent>` for a non-Claude-Code agent.
|
|
299
|
+
* Scaffolds the agent-neutral project tier (context/, CLAUDE.md block, gitignore —
|
|
300
|
+
* via installAction with hooks off, since the Claude-Code hook wiring doesn't
|
|
301
|
+
* apply), then wires THAT agent's legs (hooks + MCP + instruction file) via
|
|
302
|
+
* installAgent. One step — no second command (the user-friendly criterion).
|
|
262
303
|
*/
|
|
263
|
-
function
|
|
264
|
-
const
|
|
304
|
+
async function runInstallForAgent({ ide, options, log, logError }) {
|
|
305
|
+
const profile = getAgentProfile(ide);
|
|
306
|
+
if (!profile) {
|
|
307
|
+
const known = listAgentProfiles().map((p) => p.name).join(', ');
|
|
308
|
+
logError(`cmk install: unknown --ide '${ide}'. Supported: ${known}.`);
|
|
309
|
+
process.exitCode = 2;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 1) agent-neutral scaffold (context/ + the kit's own CLAUDE.md block live
|
|
314
|
+
// regardless of agent). Hooks OFF — the agent's hooks are wired in step 2.
|
|
315
|
+
// skipClaudeFiles: `.claude/skills/` + `CLAUDE.md` are Claude-Code-specific;
|
|
316
|
+
// a non-CC agent gets its instructions from its own surface (Kiro →
|
|
317
|
+
// .kiro/skills/ + .kiro/steering/ + AGENTS.md), so we must not leave dead
|
|
318
|
+
// Claude files on the project (D-188). An EXISTING Claude install's files
|
|
319
|
+
// are untouched — we just don't create fresh ones.
|
|
320
|
+
const scaffold = await installAction({
|
|
321
|
+
force: !!(options && options.force),
|
|
322
|
+
noHooks: true,
|
|
323
|
+
withSemantic: !!(options && options.withSemantic),
|
|
324
|
+
noSemantic: !!(options && options.semantic === false),
|
|
325
|
+
projectRoot: options?.cwd,
|
|
326
|
+
userTier: options?.userTier,
|
|
327
|
+
bindingProbe: options?.bindingProbe,
|
|
328
|
+
spawnNpm: options?.spawnNpm,
|
|
329
|
+
warmEmbedder: options?.warmEmbedder,
|
|
330
|
+
skipClaudeFiles: true,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const projectName = basename(scaffold.projectRoot);
|
|
334
|
+
|
|
335
|
+
// 2) wire the agent's surfaces. Kiro has its OWN orchestrator (D-182): five
|
|
336
|
+
// surfaces (MCP + steering + skills + IDE hooks + the CLI agent-config),
|
|
337
|
+
// not the generic installAgent's Claude-Code-shaped model.
|
|
338
|
+
if (ide === 'kiro') {
|
|
339
|
+
// awsDir: tests/sandboxes pass $MEMORY_KIT_AWS_DIR via options.awsDir to keep
|
|
340
|
+
// the CLI-agent leg out of the real ~/.aws; production leaves it undefined →
|
|
341
|
+
// the real ~/.kiro (where kiro-cli actually reads its agents).
|
|
342
|
+
const r = installKiro({ projectRoot: scaffold.projectRoot, awsDir: options?.awsDir });
|
|
343
|
+
if (r.action === 'error') {
|
|
344
|
+
for (const e of r.errors || []) {
|
|
345
|
+
logError(` error: Kiro ${e.surface}: ${(e.errors || []).join('; ')}`);
|
|
346
|
+
}
|
|
347
|
+
logError(
|
|
348
|
+
`cmk install: ${projectName} scaffolded but Kiro wiring failed (a config file could not be safely written — see above).`,
|
|
349
|
+
);
|
|
350
|
+
process.exitCode = 1;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
log(
|
|
354
|
+
`cmk install: ${projectName} ready for Kiro — context/ scaffolded; ${r.surfaces.join(' + ')} wired.`,
|
|
355
|
+
);
|
|
356
|
+
log(' Restart Kiro to activate the hooks (steering + skills + MCP are immediate).');
|
|
357
|
+
// The CLI agent-config (kiro-cli) is automatic only when cmk is the default
|
|
358
|
+
// agent. If the user already has a default, surface the one manual step.
|
|
359
|
+
if (r.cliDefaultAgent === 'skipped-existing') {
|
|
360
|
+
log(' Note: you already have a Kiro CLI default agent — the kit installed a `cmk` agent instead.');
|
|
361
|
+
log(' Run `kiro-cli --agent cmk`, or set `chat.defaultAgent` to `cmk`, for automatic CLI memory.');
|
|
362
|
+
}
|
|
363
|
+
if (scaffold.errors.length > 0) {
|
|
364
|
+
for (const e of scaffold.errors) logError(` error: ${e.path}: ${e.error}`);
|
|
365
|
+
process.exitCode = 1;
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Other agents: the generic per-profile installer.
|
|
371
|
+
const wired = installAgent({ projectRoot: scaffold.projectRoot, profile });
|
|
372
|
+
|
|
373
|
+
if (wired.action === 'error') {
|
|
374
|
+
for (const e of wired.errors || []) {
|
|
375
|
+
logError(` error: ${profile.displayName} ${e.leg}: ${(e.errors || []).join('; ')}`);
|
|
376
|
+
}
|
|
377
|
+
// Report which legs DID land (every leg is independently idempotent +
|
|
378
|
+
// touch-only, so a re-run after fixing the flagged file is safe).
|
|
379
|
+
const landed = Object.entries(wired.legs || {})
|
|
380
|
+
.filter(([, action]) => action && action !== 'error')
|
|
381
|
+
.map(([leg]) => leg);
|
|
382
|
+
if (landed.length) {
|
|
383
|
+
logError(` (${landed.join(' + ')} already wired; re-run after fixing the file above — safe, idempotent.)`);
|
|
384
|
+
}
|
|
385
|
+
logError(
|
|
386
|
+
`cmk install: ${projectName} scaffolded but ${profile.displayName} wiring failed (a config file could not be safely written — see above).`,
|
|
387
|
+
);
|
|
388
|
+
process.exitCode = 1;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Describe what THIS integration type actually wired (instruction-only writes
|
|
393
|
+
// just the instruction file; full agents wire hooks + MCP too).
|
|
394
|
+
const wiredLegs = [
|
|
395
|
+
wired.legs.instruction ? 'instruction file' : null,
|
|
396
|
+
wired.legs.mcp ? 'MCP' : null,
|
|
397
|
+
wired.legs.hooks ? 'hooks' : null,
|
|
398
|
+
].filter(Boolean);
|
|
399
|
+
log(
|
|
400
|
+
`cmk install: ${projectName} ready for ${profile.displayName} — context/ scaffolded, ${wiredLegs.join(' + ')} wired.`,
|
|
401
|
+
);
|
|
402
|
+
if (profile.integrationType === 'instruction-only') {
|
|
403
|
+
log(' Instruction-file only (no hooks/MCP) — a portable memory-awareness rung for tools that read AGENTS.md.');
|
|
404
|
+
} else {
|
|
405
|
+
log(' Restart the agent to activate. Complete install — one step, no separate command.');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (scaffold.errors.length > 0) {
|
|
409
|
+
for (const e of scaffold.errors) logError(` error: ${e.path}: ${e.error}`);
|
|
410
|
+
process.exitCode = 1;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Task 50.J/50.L — `cmk hook <event>` — the Kiro hook entrypoint.
|
|
416
|
+
*
|
|
417
|
+
* Kiro's IDE + CLI hooks call `cmk hook <event>` (agentSpawn / promptSubmit /
|
|
418
|
+
* stop / preToolUse). Unlike Claude Code's hook bins (which read a stdin JSON
|
|
419
|
+
* payload), Kiro passes context via argv + env + cwd + its transcript file
|
|
420
|
+
* (probe-verified, P-CJYGTQYR). runKiroHook adapts that to the kit's cores.
|
|
421
|
+
*
|
|
422
|
+
* ALWAYS exits 0 EXCEPT a deliberate preToolUse BLOCK (exit 2 → the memory
|
|
423
|
+
* delete-guardrail, D-192). A crashed hook still exits 0 (fail-open).
|
|
424
|
+
*/
|
|
425
|
+
/**
|
|
426
|
+
* Extract the about-to-run TOOL COMMAND from a preToolUse hook payload, for the
|
|
427
|
+
* memory delete-guardrail (D-192).
|
|
428
|
+
*
|
|
429
|
+
* Kiro's `preToolUse` delivers `{ tool_name, tool_input: { command } }` on STDIN
|
|
430
|
+
* — the SAME shape as Claude Code (VERIFIED from the real oh-my-kiro + vibekit
|
|
431
|
+
* preToolUse hooks: `cat | jq '.tool_input.command'`). So the production guard
|
|
432
|
+
* uses the `cmk-guard-memory` bin directly (it reads that stdin); this helper is
|
|
433
|
+
* the forward-compat path if a `cmk hook preToolUse` is ever wired instead. A
|
|
434
|
+
* `.command` fallback covers a flattened payload. No match → '' (fail-open).
|
|
435
|
+
*/
|
|
436
|
+
export function kiroToolCommand(payload, _cwd) {
|
|
437
|
+
const p = payload || {};
|
|
438
|
+
const cmd = p.tool_input?.command ?? p.command;
|
|
439
|
+
return typeof cmd === 'string' ? cmd : '';
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function runHook(event, _options = {}, _command, deps = {}) {
|
|
443
|
+
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
444
|
+
const logError = deps.logError ?? ((s) => process.stderr.write(`${s}\n`));
|
|
445
|
+
// The Kiro hook payload. Most events (agentSpawn/promptSubmit/stop/preToolUse)
|
|
446
|
+
// get their input from argv + env (USER_PROMPT) + cwd + transcript, NOT stdin —
|
|
447
|
+
// and runHook must NOT blocking-read fd 0 for them (a dangling-open `runCommand`
|
|
448
|
+
// stdin would hang the hook to its timeout — B1, the skill-review catch).
|
|
449
|
+
//
|
|
450
|
+
// postToolUse (50.N.2) is the ONE event whose data (tool_name/tool_input/
|
|
451
|
+
// tool_response) has no env fallback — it arrives as a STDIN JSON payload. We
|
|
452
|
+
// read stdin ONLY for postToolUse, TTY-guarded (readHookStdin returns '' on a
|
|
453
|
+
// TTY). Tests inject `deps.payload` to bypass the read entirely.
|
|
454
|
+
//
|
|
455
|
+
// WHY THIS IS SAFE (the precedent, not a guess): the kit ALREADY ships a
|
|
456
|
+
// stdin-reading hook in the SAME kiro-cli agent config — `cmk-guard-memory`
|
|
457
|
+
// (preToolUse) reads stdin the identical way (readHookStdin → readFileSync(0)).
|
|
458
|
+
// preToolUse and postToolUse are siblings in the same Amazon-Q hook contract,
|
|
459
|
+
// both delivering {tool_name, tool_input, …} on a piped-and-closed stdin. If
|
|
460
|
+
// Kiro left that stdin dangling-open, the already-merged guard would hang on
|
|
461
|
+
// every shell command — it doesn't. So this read rests on the same (verified-by-
|
|
462
|
+
// -contract-shape) assumption the guard already relies on, NOT a new risk.
|
|
463
|
+
// The 50.N.1 B1 fix removed a stdin read for events that have an ENV fallback
|
|
464
|
+
// (the prompt) — postToolUse has none, so the read is necessary here.
|
|
465
|
+
// VERIFICATION (surfaced, not buried): the live stdin-close + payload-shape
|
|
466
|
+
// assumption both this leg AND the preToolUse guard depend on is a BLOCKING
|
|
467
|
+
// cut-gate item (KG-observe, paired with KG-guard) — one real kiro-cli fs_write
|
|
468
|
+
// verifies both. Until that passes live, this leg is "structurally wired,
|
|
469
|
+
// live-unverified" (stated in the PR body + the 50.N.2 task, per the no-disclaimer
|
|
470
|
+
// -ships-latent rule).
|
|
471
|
+
let payload = deps.payload;
|
|
472
|
+
if (payload === undefined) {
|
|
473
|
+
if (event === 'postToolUse') {
|
|
474
|
+
try {
|
|
475
|
+
const readStdin = deps.readStdin ?? (() => readHookStdin({ isTTY: process.stdin.isTTY }));
|
|
476
|
+
const raw = readStdin();
|
|
477
|
+
payload = raw && raw.trim() !== '' ? JSON.parse(raw) : {};
|
|
478
|
+
} catch {
|
|
479
|
+
payload = {};
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
payload = {};
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const r = runKiroHook({
|
|
486
|
+
argv: [event],
|
|
487
|
+
cwd: deps.cwd ?? process.cwd(),
|
|
488
|
+
env: deps.env ?? process.env,
|
|
489
|
+
payload,
|
|
490
|
+
deps: {
|
|
491
|
+
readKiroTurn: deps.readKiroTurn ?? readKiroTurn,
|
|
492
|
+
// injectContext returns the assembled context string; normalize to {text}.
|
|
493
|
+
// userDir is passed through so cross-project user-tier memory surfaces on
|
|
494
|
+
// Kiro inject too (injectContext resolves $MEMORY_KIT_USER_DIR when userDir
|
|
495
|
+
// is absent, but pass it explicitly when the caller provides one).
|
|
496
|
+
inject: deps.inject ?? ((args) => {
|
|
497
|
+
const text = injectContext({ cwd: args.cwd, ...(args.userDir ? { userDir: args.userDir } : {}) });
|
|
498
|
+
return { ok: true, text: typeof text === 'string' ? text : text?.text ?? '' };
|
|
499
|
+
}),
|
|
500
|
+
// Pass a RESOLVED autoExtractPath so captureTurn can spawn the detached
|
|
501
|
+
// auto-extract child (D-200 — else no extraction, no wedge promotion). The
|
|
502
|
+
// `captureTurn` impl is injectable (deps.captureTurn) so a test can assert
|
|
503
|
+
// the autoExtractPath is forwarded WITHOUT replacing the whole capture dep
|
|
504
|
+
// (replacing `capture` would bypass the very wiring under test).
|
|
505
|
+
capture: deps.capture ?? ((args) => (deps.captureTurn ?? captureTurn)({
|
|
506
|
+
payload: args.payload,
|
|
507
|
+
projectRoot: args.projectRoot,
|
|
508
|
+
autoExtractPath: resolveKiroAutoExtractPath(deps.env ?? process.env),
|
|
509
|
+
})),
|
|
510
|
+
// 50.N.1 — prompt-capture on the prompt-submit events (the <private>-strip
|
|
511
|
+
// + transcript-append half of Claude Code's UserPromptSubmit). Best-effort;
|
|
512
|
+
// the dispatcher swallows a throw so inject still runs.
|
|
513
|
+
capturePrompt: deps.capturePrompt ?? ((args) => capturePrompt({
|
|
514
|
+
payload: args.payload,
|
|
515
|
+
projectRoot: args.projectRoot,
|
|
516
|
+
})),
|
|
517
|
+
// 50.N.2 — postToolUse → observe-edit (the file-edit observation leg). The
|
|
518
|
+
// bin maps Kiro's fs_write → Write before this runs. Best-effort.
|
|
519
|
+
observe: deps.observe ?? ((args) => observeEdit({
|
|
520
|
+
payload: args.payload,
|
|
521
|
+
projectRoot: args.projectRoot,
|
|
522
|
+
})),
|
|
523
|
+
// preToolUse → the memory delete-guardrail (D-192). Reads the about-to-run
|
|
524
|
+
// tool command out of the Kiro payload and returns {block, reason?}.
|
|
525
|
+
guard: deps.guard ?? ((args) => decideGuard(kiroToolCommand(args.payload, args.cwd))),
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
if (r.stdout) log(r.stdout);
|
|
529
|
+
if (r.stderr) logError(r.stderr);
|
|
530
|
+
// The always-exit-0 invariant holds for EVERY event EXCEPT a deliberate
|
|
531
|
+
// preToolUse BLOCK (exit 2 → Kiro blocks the tool, D-192). Honor the
|
|
532
|
+
// dispatcher's exitCode (0 for inject/capture/noop/error; 2 only on a guard
|
|
533
|
+
// block) instead of hardcoding 0 — a crashed guard still fails open (the
|
|
534
|
+
// dispatcher's catch returns exitCode 0).
|
|
535
|
+
process.exitCode = typeof r.exitCode === 'number' ? r.exitCode : 0;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* `cmk uninstall [--ide <agent>]` — remove ONE agent's kit-managed surface,
|
|
540
|
+
* scoped by `--ide` exactly like `cmk install` (D-189). Default (no flag)
|
|
541
|
+
* removes the Claude Code surface (the CLAUDE.md managed block); `--ide kiro`
|
|
542
|
+
* removes the Kiro surface (the .kiro/ managed blocks + skills + IDE hooks + the
|
|
543
|
+
* guarded ~/.aws CLI agent + the AGENTS.md block). BOTH are conservative: they
|
|
544
|
+
* NEVER touch context/, context.local/, the user tier, or .gitignore — the
|
|
545
|
+
* shared brain is sacred. Everything outside the kit's markers is byte-preserved.
|
|
546
|
+
*/
|
|
547
|
+
export function runUninstall(options /*, command */) {
|
|
548
|
+
const projectRoot = resolvePath((options && options.cwd) || process.cwd());
|
|
549
|
+
const ide = (options && options.ide) || 'claude-code';
|
|
550
|
+
// Injectable log sinks (mirror runInstall) so tests can capture output.
|
|
551
|
+
const log = options?.log ?? console.log;
|
|
552
|
+
const logError = options?.logError ?? console.error;
|
|
553
|
+
|
|
554
|
+
if (ide === 'kiro') {
|
|
555
|
+
const r = uninstallKiro({ projectRoot, awsDir: options?.awsDir });
|
|
556
|
+
log(
|
|
557
|
+
`cmk uninstall (kiro): ${r.changed ? 'removed the Kiro managed surface' : 'nothing to remove'} — context/ preserved.`,
|
|
558
|
+
);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (ide !== 'claude-code') {
|
|
562
|
+
logError(`cmk uninstall: unknown --ide '${ide}'. Supported: claude-code, kiro.`);
|
|
563
|
+
process.exitCode = 2;
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
265
567
|
const result = removeClaudeMdBlock({ projectRoot });
|
|
266
|
-
|
|
568
|
+
log(`cmk uninstall: CLAUDE.md=${result.action} (${result.path})`);
|
|
267
569
|
if (result.action === 'not-found') {
|
|
268
|
-
|
|
570
|
+
log(' (no kit-managed block found; CLAUDE.md left unchanged)');
|
|
269
571
|
} else if (result.action === 'no-file') {
|
|
270
|
-
|
|
572
|
+
log(' (no CLAUDE.md to uninstall from)');
|
|
271
573
|
}
|
|
272
574
|
}
|
|
273
575
|
|
|
@@ -381,7 +683,10 @@ function runLessonsPromote(id, options = {}) {
|
|
|
381
683
|
* --include-tombstoned (default false)
|
|
382
684
|
*/
|
|
383
685
|
async function runSearch(queryParts, options) {
|
|
384
|
-
|
|
686
|
+
// --project <dir> overrides cwd (the kiro-cli fix, Kiro #4579 — no `cd` prefix).
|
|
687
|
+
const projectRoot = options?.project
|
|
688
|
+
? resolvePath(normalizeProjectPath(options.project))
|
|
689
|
+
: resolvePath(process.cwd());
|
|
385
690
|
const userDir =
|
|
386
691
|
process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
|
|
387
692
|
const query = Array.isArray(queryParts) ? queryParts.join(' ') : queryParts;
|
|
@@ -763,7 +1068,13 @@ export function parseFactInput(options, { readFile, readStdin } = {}) {
|
|
|
763
1068
|
// (one model call, explicit path only). Commander awaits actions; the
|
|
764
1069
|
// terse-path tests were updated to await (contract change, intent preserved).
|
|
765
1070
|
export async function runRemember(textParts, options, deps = {}) {
|
|
766
|
-
|
|
1071
|
+
// --project <dir> overrides cwd (the kiro-cli fix, Kiro #4579): kiro-cli's
|
|
1072
|
+
// command allowlist rejects a `cd … && cmk remember …` prefix, so the kiro-cli
|
|
1073
|
+
// agent runs `cmk remember "<fact>" --project "<abs>"` with NO cd. normalize a
|
|
1074
|
+
// git-bash `/c/Temp` path the model may emit → `C:/Temp`.
|
|
1075
|
+
const projectRoot = options?.project
|
|
1076
|
+
? resolvePath(normalizeProjectPath(options.project))
|
|
1077
|
+
: (deps.projectRoot ?? resolvePath(process.cwd()));
|
|
767
1078
|
const userDir =
|
|
768
1079
|
deps.userDir ?? process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
|
|
769
1080
|
const log = deps.log ?? console.log;
|
|
@@ -1410,12 +1721,16 @@ export function runConfigCli(options /* , command */) {
|
|
|
1410
1721
|
async function runRepairCli(options /* , command */) {
|
|
1411
1722
|
const projectRoot = resolvePath(process.cwd());
|
|
1412
1723
|
const userDir = join(homedir(), '.claude-memory-kit');
|
|
1413
|
-
// Scope flags: --hooks / --locks / --index → run that one only.
|
|
1414
|
-
// --all OR no flag → run all
|
|
1724
|
+
// Scope flags: --hooks / --locks / --index / --format → run that one only.
|
|
1725
|
+
// --all OR no flag → run all of them.
|
|
1726
|
+
const only = (flag) =>
|
|
1727
|
+
options?.[flag] &&
|
|
1728
|
+
!['hooks', 'locks', 'index', 'format'].filter((f) => f !== flag).some((f) => options?.[f]);
|
|
1415
1729
|
let scope;
|
|
1416
|
-
if (
|
|
1417
|
-
else if (
|
|
1418
|
-
else if (
|
|
1730
|
+
if (only('hooks')) scope = 'hooks';
|
|
1731
|
+
else if (only('locks')) scope = 'locks';
|
|
1732
|
+
else if (only('index')) scope = 'index';
|
|
1733
|
+
else if (only('format')) scope = 'format';
|
|
1419
1734
|
else scope = 'all';
|
|
1420
1735
|
|
|
1421
1736
|
try {
|
|
@@ -1680,11 +1995,13 @@ async function runCompress(options /* , command */) {
|
|
|
1680
1995
|
|
|
1681
1996
|
async function runMcpDispatch(childName) {
|
|
1682
1997
|
if (childName === 'serve') {
|
|
1683
|
-
// Claude Code sets CLAUDE_PROJECT_DIR in
|
|
1684
|
-
//
|
|
1685
|
-
//
|
|
1686
|
-
//
|
|
1687
|
-
|
|
1998
|
+
// Which project does this server serve? Claude Code sets CLAUDE_PROJECT_DIR in
|
|
1999
|
+
// the spawned server's env; otherwise fall back to the launch cwd (or walk up
|
|
2000
|
+
// to the nearest `context/`). The kiro-cli surface does NOT use MCP at all (its
|
|
2001
|
+
// agent sets includeMcpJson:false — explicit memory goes through the `cmk
|
|
2002
|
+
// remember`/`cmk search` shell commands), so this server is the Claude Code +
|
|
2003
|
+
// Kiro IDE path. See resolveMcpProjectRoot in tier-paths.mjs.
|
|
2004
|
+
const projectRoot = resolveMcpProjectRoot();
|
|
1688
2005
|
const userDir = process.env.MEMORY_KIT_USER_DIR ?? join(homedir(), '.claude-memory-kit');
|
|
1689
2006
|
// ALL logs to stderr per design §10.1; stdout is reserved for
|
|
1690
2007
|
// JSON-RPC messages handled by the SDK's StdioServerTransport.
|
|
@@ -1966,6 +2283,7 @@ export const subcommands = [
|
|
|
1966
2283
|
description: 'cross-OS one-shot install — scaffold 3-tier dirs + inject .gitignore + drop kit CLAUDE.md block + wire Claude Code hooks',
|
|
1967
2284
|
milestone: 3,
|
|
1968
2285
|
optionSpec: [
|
|
2286
|
+
{ flags: '--ide <agent>', description: 'target agent: claude-code (default) | kiro — wires that agent\'s hooks + MCP + instruction file' },
|
|
1969
2287
|
{ flags: '--force', description: 'allow downgrade of an existing newer-version CLAUDE.md block' },
|
|
1970
2288
|
{ flags: '--no-hooks', description: 'scaffold only; do NOT wire hooks into .claude/settings.json' },
|
|
1971
2289
|
{ flags: '--with-semantic', description: 'enable semantic recall: install the local embedder (~260 MB once), default search to hybrid, pre-warm the model' },
|
|
@@ -1974,10 +2292,20 @@ export const subcommands = [
|
|
|
1974
2292
|
],
|
|
1975
2293
|
action: runInstall,
|
|
1976
2294
|
},
|
|
2295
|
+
{
|
|
2296
|
+
name: 'hook',
|
|
2297
|
+
description: 'Kiro hook entrypoint — `cmk hook <agentSpawn|promptSubmit|stop>` (inject/capture; called by Kiro IDE + CLI hooks, not by users)',
|
|
2298
|
+
milestone: 50,
|
|
2299
|
+
argSpec: [{ flags: '<event>', description: 'the Kiro lifecycle event: agentSpawn | promptSubmit | stop' }],
|
|
2300
|
+
action: runHook,
|
|
2301
|
+
},
|
|
1977
2302
|
{
|
|
1978
2303
|
name: 'uninstall',
|
|
1979
|
-
description: 'remove the
|
|
2304
|
+
description: 'remove the kit-managed surface (preserves everything else byte-for-byte; never touches context/)',
|
|
1980
2305
|
milestone: 4,
|
|
2306
|
+
optionSpec: [
|
|
2307
|
+
{ flags: '--ide <agent>', description: 'which agent to uninstall: claude-code (default) | kiro — removes only THAT agent\'s managed surface' },
|
|
2308
|
+
],
|
|
1981
2309
|
action: runUninstall,
|
|
1982
2310
|
},
|
|
1983
2311
|
{
|
|
@@ -2003,6 +2331,7 @@ export const subcommands = [
|
|
|
2003
2331
|
{ flags: '--links <a,b>', description: 'rich: related fact names for [[cross-links]]' },
|
|
2004
2332
|
{ flags: '--from-file <path>', description: 'rich: read the fact as a JSON object from a file — shell-safe (content never touches argv; the safe way to capture backtick/quote-heavy Why/How). JSON keys: text (required), why, how, type, title, links. Self-contained — other flags are ignored.' },
|
|
2005
2333
|
{ flags: '--json', description: 'rich: read the fact as a JSON object from stdin (pipe-safe, shell-safe) — same JSON keys as --from-file' },
|
|
2334
|
+
{ flags: '--project <dir>', description: 'project root to write to (default: cwd). Used by the kiro-cli agent — kiro-cli rejects a `cd … &&` prefix (Kiro #4579), so it passes the project root explicitly instead.' },
|
|
2006
2335
|
],
|
|
2007
2336
|
action: runRemember,
|
|
2008
2337
|
},
|
|
@@ -2019,6 +2348,7 @@ export const subcommands = [
|
|
|
2019
2348
|
{ flags: '--since <date>', description: 'ISO date — exclude observations older than this' },
|
|
2020
2349
|
{ flags: '--limit <n>', description: 'max results (default: 20)' },
|
|
2021
2350
|
{ flags: '--include-tombstoned', description: 'include deleted observations in results' },
|
|
2351
|
+
{ flags: '--project <dir>', description: 'project root to search (default: cwd). Used by the kiro-cli agent (no `cd` prefix — Kiro #4579).' },
|
|
2022
2352
|
],
|
|
2023
2353
|
action: runSearch,
|
|
2024
2354
|
},
|
|
@@ -2222,13 +2552,14 @@ export const subcommands = [
|
|
|
2222
2552
|
},
|
|
2223
2553
|
{
|
|
2224
2554
|
name: 'repair',
|
|
2225
|
-
description: 'idempotent self-repair — re-register hooks, reset stale locks, rebuild index',
|
|
2555
|
+
description: 'idempotent self-repair — re-register hooks, reset stale locks, rebuild index, lint-clean memory',
|
|
2226
2556
|
milestone: 39,
|
|
2227
2557
|
optionSpec: [
|
|
2228
2558
|
{ flags: '--hooks', description: 're-register hooks from template (merges kit hooks into .claude/settings.json)' },
|
|
2229
2559
|
{ flags: '--locks', description: 'clear stale locks (>1h old by default)' },
|
|
2230
2560
|
{ flags: '--index', description: 'invoke `cmk reindex --full`' },
|
|
2231
|
-
{ flags: '--
|
|
2561
|
+
{ flags: '--format', description: 'migrate committed memory markdown to the lint-clean format (DECISIONS.md headings)' },
|
|
2562
|
+
{ flags: '--all', description: 'run all repairs in order (default if no scope flag given)' },
|
|
2232
2563
|
],
|
|
2233
2564
|
action: runRepairCli,
|
|
2234
2565
|
},
|
|
@@ -2305,7 +2636,9 @@ export const subcommands = [
|
|
|
2305
2636
|
name: 'mcp',
|
|
2306
2637
|
description: 'run the MCP server over stdio (invoked by Claude Code, not by humans)',
|
|
2307
2638
|
milestone: 31,
|
|
2308
|
-
children: [
|
|
2639
|
+
children: [
|
|
2640
|
+
{ name: 'serve', description: 'start the stdio MCP server; JSON-RPC on stdin/stdout' },
|
|
2641
|
+
],
|
|
2309
2642
|
action: runMcpDispatch,
|
|
2310
2643
|
},
|
|
2311
2644
|
{
|
package/src/tier-paths.mjs
CHANGED
|
@@ -9,11 +9,92 @@
|
|
|
9
9
|
// resolveTierRoot({tier, projectRoot, userDir}) → absolute path
|
|
10
10
|
// resolveFactDir(tier, tierRoot) → absolute path to <memory|fragments>
|
|
11
11
|
|
|
12
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
12
13
|
import { homedir } from 'node:os';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
15
|
+
|
|
16
|
+
// Canonicalize a path for comparison: resolve 8.3 short names (Windows
|
|
17
|
+
// `TAMIR~1.BN-`) + symlinks to their real long form, so a short-name path and
|
|
18
|
+
// its long-name twin compare equal. Falls back to `resolve(p)` if the path
|
|
19
|
+
// doesn't exist (realpathSync throws on a missing path). Exported so the project
|
|
20
|
+
// discovery in inject-context.mjs shares ONE implementation (Task 168 — the
|
|
21
|
+
// home-boundary + canonicalize logic must not drift across the two walkers).
|
|
22
|
+
export function canonicalPath(p) {
|
|
23
|
+
try {
|
|
24
|
+
return realpathSync.native ? realpathSync.native(p) : realpathSync(p);
|
|
25
|
+
} catch {
|
|
26
|
+
return resolve(p);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Walk up from `cwd` to the nearest ancestor that has one of the `markers`
|
|
32
|
+
* subdirs (a kit-installed project), STOPPING at the home directory — a stray
|
|
33
|
+
* `~/context/` (test debris, or a `cmk` run that scaffolded in home) must NOT be
|
|
34
|
+
* served as a project from an unrelated subdir (Task 168). Returns the discovered
|
|
35
|
+
* root, or `resolve(cwd)` if none found below home.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} cwd starting directory
|
|
38
|
+
* @param {string[]} markers subdir names that mark a project root (e.g.
|
|
39
|
+
* ['context'] or ['context', 'context.local'])
|
|
40
|
+
*/
|
|
41
|
+
export function discoverRootUpward(cwd, markers = ['context']) {
|
|
42
|
+
const home = canonicalPath(homedir());
|
|
43
|
+
let dir = resolve(cwd);
|
|
44
|
+
// Defensive bound: walk no more than 64 ancestors.
|
|
45
|
+
for (let i = 0; i < 64; i++) {
|
|
46
|
+
const atHome = canonicalPath(dir) === home;
|
|
47
|
+
if (!atHome && markers.some((m) => existsSync(join(dir, m)))) return dir;
|
|
48
|
+
const parent = dirname(dir);
|
|
49
|
+
if (parent === dir) break; // reached the filesystem root
|
|
50
|
+
if (atHome) break; // do not climb above $HOME into a stray ancestor context/
|
|
51
|
+
dir = parent;
|
|
52
|
+
}
|
|
53
|
+
return resolve(cwd); // last resort
|
|
54
|
+
}
|
|
14
55
|
|
|
15
56
|
export const VALID_TIERS = new Set(['U', 'P', 'L']);
|
|
16
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Normalize a project path that may arrive in a unix/git-bash form. kiro-cli's
|
|
60
|
+
* model sometimes emits a `/c/Temp/proj` style path (git-bash) for `--project`,
|
|
61
|
+
* which Windows `resolve()` mangles. Convert a leading `/<drive>/` → `<DRIVE>:/`.
|
|
62
|
+
* A normal Windows or POSIX absolute path passes through unchanged. Used by
|
|
63
|
+
* `cmk remember`/`cmk search` --project (the kiro-cli explicit-memory path).
|
|
64
|
+
*/
|
|
65
|
+
export function normalizeProjectPath(p) {
|
|
66
|
+
if (typeof p !== 'string') return p;
|
|
67
|
+
const m = /^\/([a-zA-Z])\/(.*)$/.exec(p); // /c/Temp/proj → c , Temp/proj
|
|
68
|
+
if (m) return `${m[1].toUpperCase()}:/${m[2]}`;
|
|
69
|
+
return p;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve which project `cmk mcp serve` should serve (the Claude Code + Kiro IDE
|
|
74
|
+
* MCP path — kiro-cli doesn't use MCP). The MCP server is a long-lived child the
|
|
75
|
+
* agent launches, so it can't just trust cwd.
|
|
76
|
+
*
|
|
77
|
+
* Precedence: CLAUDE_PROJECT_DIR (Claude Code sets it in the spawned env) → walk
|
|
78
|
+
* UP from cwd to the nearest `context/` ancestor → cwd (last resort). Pure (env +
|
|
79
|
+
* cwd injected) so it's unit-testable without spawning.
|
|
80
|
+
*
|
|
81
|
+
* @param {object} [opts]
|
|
82
|
+
* @param {Record<string,string|undefined>} [opts.env=process.env]
|
|
83
|
+
* @param {string} [opts.cwd=process.cwd()]
|
|
84
|
+
* @returns {string} the resolved project root (absolute)
|
|
85
|
+
*/
|
|
86
|
+
export function resolveMcpProjectRoot({ env = process.env, cwd = process.cwd() } = {}) {
|
|
87
|
+
const fromClaude = env.CLAUDE_PROJECT_DIR;
|
|
88
|
+
if (fromClaude && fromClaude.trim() !== '') return resolve(fromClaude);
|
|
89
|
+
|
|
90
|
+
// Walk up to the nearest `context/`-bearing project, STOPPING at home (Task 168
|
|
91
|
+
// — a stray `~/context/` must not be served from an unrelated subdir; a real
|
|
92
|
+
// project's context/ lives below home, or via the explicit CLAUDE_PROJECT_DIR
|
|
93
|
+
// handled above). Shared with inject-context's discoverProjectRoot via the
|
|
94
|
+
// single discoverRootUpward implementation.
|
|
95
|
+
return discoverRootUpward(cwd, ['context']);
|
|
96
|
+
}
|
|
97
|
+
|
|
17
98
|
// Matches IDs produced by @lh8ppl/cmk-canonicalize.generateId(). Tier prefix +
|
|
18
99
|
// 8 chars from the custom 32-char base32 alphabet that excludes the six
|
|
19
100
|
// ambiguous chars (0, O, 1, l, I, 8). See design §3.1.
|
package/src/weekly-curate.mjs
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
} from './cooldown.mjs';
|
|
48
48
|
import { dailyDistill } from './daily-distill.mjs';
|
|
49
49
|
import { autoPersona } from './auto-persona.mjs';
|
|
50
|
+
import { trimTrailingNewlines } from './managed-block.mjs';
|
|
50
51
|
import { initUserTier } from './install.mjs';
|
|
51
52
|
import { autoDrainQueues } from './auto-drain.mjs';
|
|
52
53
|
|
|
@@ -449,8 +450,11 @@ export async function weeklyCurate({
|
|
|
449
450
|
// Append to archive.md (NOT overwrite — archive is append-only history).
|
|
450
451
|
const archivePath = archiveMdPath(projectRoot);
|
|
451
452
|
mkdirSync(join(projectRoot, ...SESSIONS_REL), { recursive: true });
|
|
452
|
-
|
|
453
|
-
|
|
453
|
+
// Lint-clean append (MD012 no-multiple-blanks): the old `dedupedOutput + suffix
|
|
454
|
+
// + '\n'` could yield THREE consecutive newlines when Haiku output already ended
|
|
455
|
+
// in a blank line. Trim trailing newlines (ReDoS-safe helper), then append
|
|
456
|
+
// exactly one blank-line separator (`\n\n`).
|
|
457
|
+
appendFileSync(archivePath, `${trimTrailingNewlines(dedupedOutput)}\n\n`, 'utf8');
|
|
454
458
|
|
|
455
459
|
// Delete OLD today-*.md files (audit retention via git history;
|
|
456
460
|
// committed tier per .gitignore.fragment).
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memory-search
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Searches the project's recorded memory (claude-memory-kit) — decisions,
|
|
5
|
+
conventions, architecture, the reasoning behind choices, and where things live
|
|
6
|
+
— and returns a curated, cited summary. Fire whenever the answer might be
|
|
7
|
+
something the project already established in past work, HOWEVER the question is
|
|
8
|
+
phrased — any prior decision, convention, rationale, or "how/where/why is it
|
|
9
|
+
this way" question, including oblique or roundabout asks ("why is everything so
|
|
10
|
+
spread out?", "remind me what we settled on for X", "how come these files are
|
|
11
|
+
tiny?"). Also fire when a "[claude-memory-kit] Memory available" hint appears on
|
|
12
|
+
the prompt. The examples are illustrative, not a checklist — prefer recalling
|
|
13
|
+
over re-deriving an answer from the code. The session-start snapshot is a
|
|
14
|
+
bounded index; this skill reaches the rest. Skip only when the question is
|
|
15
|
+
purely about uncommitted or just-edited live code that memory cannot know,
|
|
16
|
+
concerns this conversation only, or the user asked to ignore memory.
|
|
4
17
|
context: fork
|
|
5
18
|
allowed-tools: mcp__cmk__mk_search mcp__cmk__mk_get mcp__cmk__mk_timeline mcp__cmk__mk_recent_activity Bash(cmk search *) Bash(cmk get *) Bash(cmk timeline *) Bash(cmk recent-activity *)
|
|
6
19
|
---
|