@lh8ppl/claude-memory-kit 0.4.0 → 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.
@@ -58,6 +58,7 @@ import {
58
58
  } from 'node:fs';
59
59
  import { dirname, join } from 'node:path';
60
60
  import { stripBom } from './read-json.mjs';
61
+ import { MCP_AUTO_APPROVE } from './kiro-constants.mjs';
61
62
 
62
63
  /**
63
64
  * Canonical npm-route hooks block. Shell form (no `args`), PATH-resolved
@@ -65,6 +66,20 @@ import { stripBom } from './read-json.mjs';
65
66
  * (modulo command form) plugin/hooks/hooks.json.
66
67
  */
67
68
  export const KIT_HOOKS_BLOCK = Object.freeze({
69
+ // PermissionRequest — the prompt-free auto-approver (Task 172, the v0.4.1
70
+ // cut-gate fix). Fires when Claude Code is about to show a permission dialog
71
+ // for one of the kit's OWN surfaces and answers "allow" so capture/recall stay
72
+ // seamless. Two matchers: the kit's MCP tools (`mcp__cmk__.*`) and the Skill
73
+ // tool (the "Use skill?" prompt). The handler (cmk-approve-permission)
74
+ // self-checks the tool name and approves ONLY kit tools/skills — the matcher
75
+ // is the first narrowing, the handler's check is the second (defence in depth).
76
+ // Needed because CC 2.1.x stopped honouring `permissions.allow` MCP rules +
77
+ // skill `allowed-tools` for these prompts (anthropics/claude-code#17499,
78
+ // #18837→#14956); the PermissionRequest hook is the documented, working path.
79
+ PermissionRequest: [
80
+ { matcher: 'mcp__cmk__.*', hooks: [{ type: 'command', command: 'cmk-approve-permission', timeout: 5 }] },
81
+ { matcher: 'Skill', hooks: [{ type: 'command', command: 'cmk-approve-permission', timeout: 5 }] },
82
+ ],
68
83
  // PreToolUse — the memory delete-guardrail (D-192). Blocks a destructive
69
84
  // shell command (rm / Remove-Item / git clean …) aimed at a memory path
70
85
  // BEFORE it runs. The only kit hook that can exit non-zero (2 = block).
@@ -86,6 +101,7 @@ export const KIT_HOOKS_BLOCK = Object.freeze({
86
101
  */
87
102
  export const KIT_COMMAND_TOKENS = Object.freeze([
88
103
  'cmk-version-check',
104
+ 'cmk-approve-permission',
89
105
  'cmk-guard-memory',
90
106
  'cmk-inject-context',
91
107
  'cmk-capture-prompt',
@@ -212,7 +228,33 @@ export function writeKitHooks(settingsPath) {
212
228
  // Task 133: memory-search joins memory-write — every scaffolded skill needs
213
229
  // its own allow entry or the model's invocation trips a "Use skill?" prompt
214
230
  // (the Task-90 class; 75.1 scaffolded the skill but missed this).
215
- const KIT_ALLOW = ['Bash(cmk:*)', 'Skill(memory-write)', 'Skill(memory-search)', 'mcp__cmk__*'];
231
+ // Task 169 (the v0.4.1 cut-gate find, 2026-06-26): Claude Code 2.1.x changed
232
+ // skill-permission matching — the bare `Skill(memory-write)` rule alone no
233
+ // longer suppressed the "Use skill?" prompt; when the user clicked "allow for
234
+ // this project", Claude Code wrote BOTH `Skill(memory-write)` AND
235
+ // `Skill(memory-write:*)` into settings.local.json. So the kit emits BOTH forms
236
+ // (bare for older CC, `:*` for 2.1.x+) — pending a docs re-verification of the
237
+ // current skill-permission syntax.
238
+ // Task 171 (v0.4.1 cut-gate ground-truth, 2026-06-26): the `mcp__cmk__*` server
239
+ // WILDCARD no longer suppresses the per-tool approval prompt on Claude Code
240
+ // 2.1.x — a DIRECT `mk_remember` call (the model using the MCP tool outside the
241
+ // skill's own `allowed-tools`) prompts "Do you want to proceed with
242
+ // mcp__cmk__mk_remember?", and CC writes the SPECIFIC tool name when allowed.
243
+ // (CC 2.1.x churned permission matching heavily — multiple changelog entries
244
+ // closed wildcard auto-approve holes, e.g. 2.1.145 "permission-prompt bypass".)
245
+ // The wildcard worked on older CC; it doesn't now. So allow-list each SPECIFIC
246
+ // cmk MCP tool (the canonical 11 in MCP_AUTO_APPROVE — shared with the Kiro
247
+ // pre-trust) AND keep `mcp__cmk__*` (harmless + future-proof). This is the same
248
+ // upstream-format-tracking the kit does for the Skill rule (D-209) and Kiro.
249
+ const KIT_ALLOW = [
250
+ 'Bash(cmk:*)',
251
+ 'Skill(memory-write)',
252
+ 'Skill(memory-write:*)',
253
+ 'Skill(memory-search)',
254
+ 'Skill(memory-search:*)',
255
+ 'mcp__cmk__*',
256
+ ...MCP_AUTO_APPROVE.map((tool) => `mcp__cmk__${tool}`),
257
+ ];
216
258
  if (!settings.permissions || typeof settings.permissions !== 'object') {
217
259
  settings.permissions = {};
218
260
  }
@@ -225,6 +267,21 @@ export function writeKitHooks(settingsPath) {
225
267
  }
226
268
  }
227
269
 
270
+ // Task 172: pre-approve the kit's OWN project-scoped `.mcp.json` server so its
271
+ // tools connect without the per-project "approve this MCP server?" prompt.
272
+ // `enabledMcpjsonServers` names specific servers to approve (NOT
273
+ // `enableAllProjectMcpServers`, which would blanket-approve EVERY server in
274
+ // `.mcp.json` — too broad for a kit shipped to others; we vouch only for our
275
+ // own server). This clears the SERVER-approval gate; the PermissionRequest
276
+ // hook above clears the per-TOOL gate. Idempotent + preserves any servers the
277
+ // user already approved.
278
+ if (!Array.isArray(settings.enabledMcpjsonServers)) {
279
+ settings.enabledMcpjsonServers = [];
280
+ }
281
+ if (!settings.enabledMcpjsonServers.includes('cmk')) {
282
+ settings.enabledMcpjsonServers.push('cmk');
283
+ }
284
+
228
285
  const after = JSON.stringify(settings);
229
286
  const changed = before !== after;
230
287
 
@@ -9,10 +9,50 @@
9
9
  // resolveTierRoot({tier, projectRoot, userDir}) → absolute path
10
10
  // resolveFactDir(tier, tierRoot) → absolute path to <memory|fragments>
11
11
 
12
- import { existsSync } from 'node:fs';
12
+ import { existsSync, realpathSync } from 'node:fs';
13
13
  import { homedir } from 'node:os';
14
14
  import { dirname, join, resolve } from 'node:path';
15
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
+ }
55
+
16
56
  export const VALID_TIERS = new Set(['U', 'P', 'L']);
17
57
 
18
58
  /**
@@ -47,18 +87,12 @@ export function resolveMcpProjectRoot({ env = process.env, cwd = process.cwd() }
47
87
  const fromClaude = env.CLAUDE_PROJECT_DIR;
48
88
  if (fromClaude && fromClaude.trim() !== '') return resolve(fromClaude);
49
89
 
50
- // walk up from cwd to the nearest dir that has a context/ subdir (an installed
51
- // kit project), stopping at the filesystem root.
52
- let dir = resolve(cwd);
53
- // eslint-disable-next-line no-constant-condition
54
- while (true) {
55
- if (existsSync(join(dir, 'context'))) return dir;
56
- const parent = dirname(dir);
57
- if (parent === dir) break; // reached the root
58
- dir = parent;
59
- }
60
-
61
- return resolve(cwd); // last resort
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']);
62
96
  }
63
97
 
64
98
  // Matches IDs produced by @lh8ppl/cmk-canonicalize.generateId(). Tier prefix +