pkm-mcp-server 1.3.3 → 1.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/CHANGELOG.md CHANGED
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.4.1] - 2026-03-21
10
+
11
+ ### Fixed
12
+ - SessionStart hook crash when installed via `init` — `resolve-project.js` imported `resolvePath` from `../helpers.js` which doesn't exist at `~/.claude/hooks/pkm/`. Inlined the path security check to remove the external dependency.
13
+
14
+ ## [1.4.0] - 2026-03-21
15
+
16
+ ### Added
17
+ - `pkm-mcp-server init` now configures PKM hooks for Claude Code — copies hook scripts to `~/.claude/hooks/pkm/` and writes hook entries to `~/.claude/settings.json`
18
+ - Individual opt-in for each hook: project context loading (SessionStart), passive capture (Stop), and explicit capture (PostToolUse)
19
+ - Merge-aware settings.json writing — preserves unrelated hooks, replaces PKM entries on re-run
20
+ - Hook script MCP config patching — works for both npx and cloned-repo installations
21
+ - 32 new unit tests for hook setup functions (`patchMcpConfig`, `isPkmHookEntry`, `buildHookEntries`, `mergeHooksIntoSettings`, `copyHooks`)
22
+
9
23
  ## [1.3.3] - 2026-03-21
10
24
 
11
25
  ### Fixed
package/README.md CHANGED
@@ -91,12 +91,34 @@ npm install -g pkm-mcp-server
91
91
  pkm-mcp-server init
92
92
  ```
93
93
 
94
- The setup wizard walks you through:
95
- 1. Vault path (existing or new)
96
- 2. Note templates (full set, minimal, or skip)
97
- 3. PARA folder structure
98
- 4. OpenAI API key for semantic search (optional)
99
- 5. Automatic registration with Claude Code
94
+ The setup wizard walks you through 5 steps. Nothing is written until you confirm each step, and you can press Ctrl+C at any time to cancel.
95
+
96
+ **Step 1 — Vault path.** Point to an existing Obsidian vault or create a new one. The wizard resolves `~`, `$HOME`, and relative paths automatically. Safety checks prevent using system directories (`/`, `/home`, etc.) as a vault. For existing non-empty directories you can use it as-is, create a subfolder inside it, or wipe it (with triple confirmation). You'll be offered an optional backup before any changes — this creates a timestamped copy next to the vault (e.g. `PKM-backup-2026-03-21T14-30-00/`).
97
+
98
+ **Step 2 Note templates.** Copies template files into `<vault>/05-Templates/`. Three options:
99
+ - **Full set** all 13 templates (`adr`, `daily-note`, `devlog`, `fleeting-note`, `literature-note`, `meeting-notes`, `moc`, `note`, `permanent-note`, `project-index`, `research-note`, `task`, `troubleshooting-log`)
100
+ - **Minimal** — just `note.md` (a single generic template)
101
+ - **Skip** — for users with their own templates
102
+
103
+ Existing templates are never overwritten.
104
+
105
+ **Step 3 — PARA folder structure.** Creates 7 top-level folders with `_index.md` stubs:
106
+
107
+ | Folder | Purpose |
108
+ |--------|---------|
109
+ | `00-Inbox/` | Quick captures and unsorted notes |
110
+ | `01-Projects/` | Active project folders |
111
+ | `02-Areas/` | Ongoing areas of responsibility |
112
+ | `03-Resources/` | Reference material and reusable knowledge |
113
+ | `04-Archive/` | Completed or inactive items |
114
+ | `05-Templates/` | Note templates |
115
+ | `06-System/` | System configuration and metadata |
116
+
117
+ Each `_index.md` has `type: moc` frontmatter. Existing folders and index files are skipped.
118
+
119
+ **Step 4 — OpenAI API key (optional).** Enables `vault_semantic_search` and `vault_suggest_links`. The key is stored only in your Claude Code configuration (`~/.claude.json`) and is used solely for generating text embeddings. You can add this later — see [Enable Semantic Search](#3-enable-semantic-search-optional).
120
+
121
+ **Step 5 — Claude Code registration.** Registers the MCP server via `claude mcp add -s user`. If `obsidian-pkm` is already registered, you'll be asked whether to overwrite. The exact command is shown for confirmation before running. If the `claude` CLI is not found on PATH, the wizard prints the manual registration command instead.
100
122
 
101
123
  Restart Claude Code after setup. The server provides all tools except semantic search out of the box.
102
124
 
@@ -109,6 +131,8 @@ npm install
109
131
  node cli.js init
110
132
  ```
111
133
 
134
+ You can also run the wizard without a global install: `npx pkm-mcp-server init`.
135
+
112
136
  ### 2. Manual Registration (alternative)
113
137
 
114
138
  If you prefer to skip the wizard, register directly with the Claude CLI:
@@ -222,9 +246,9 @@ Vault/
222
246
 
223
247
  ### Templates
224
248
 
225
- Copy the files from `templates/` into your vault's `05-Templates/` folder. `vault_write` loads all `.md` files from that directory at startup and enforces frontmatter on every note created.
249
+ `vault_write` loads all `.md` files from `05-Templates/` at startup and enforces frontmatter on every note created. The setup wizard (`pkm-mcp-server init`) installs these automatically — or you can copy the files from `templates/` manually.
226
250
 
227
- Included templates: `project-index`, `adr`, `devlog`, `permanent-note`, `research-note`, `troubleshooting-log`, `fleeting-note`, `literature-note`, `meeting-notes`, `moc`, `daily-note`, `task`. Add your own templates to `05-Templates/` and they become available to `vault_write` automatically.
251
+ 13 included templates: `adr`, `daily-note`, `devlog`, `fleeting-note`, `literature-note`, `meeting-notes`, `moc`, `note`, `permanent-note`, `project-index`, `research-note`, `task`, `troubleshooting-log`. Add your own templates to `05-Templates/` and they become available to `vault_write` automatically.
228
252
 
229
253
  ### CLAUDE.md for Your Projects
230
254
 
@@ -1,6 +1,12 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
- import { resolvePath } from "../helpers.js";
3
+
4
+ function assertPathWithinVault(relativePath, vaultPath) {
5
+ const resolved = path.resolve(vaultPath, relativePath);
6
+ if (resolved !== vaultPath && !resolved.startsWith(vaultPath + path.sep)) {
7
+ throw new Error("Path escapes vault directory");
8
+ }
9
+ }
4
10
 
5
11
  export async function resolveProject(cwd, vaultPath) {
6
12
  try {
@@ -35,7 +41,7 @@ export async function resolveProject(cwd, vaultPath) {
35
41
  if (match) {
36
42
  const annotatedPath = match[1].trim();
37
43
  try {
38
- resolvePath(annotatedPath, vaultPath);
44
+ assertPathWithinVault(annotatedPath, vaultPath);
39
45
  } catch (e) {
40
46
  if (e.message === "Path escapes vault directory") {
41
47
  return { error: `CLAUDE.md annotation escapes vault directory: ${annotatedPath}` };
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { appendFileSync, createWriteStream, mkdirSync } from "node:fs";
5
+ import { access } from "node:fs/promises";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { resolveProject } from "./resolve-project.js";
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const VAULT_PATH = process.env.VAULT_PATH;
12
+ const LOG_DIR = VAULT_PATH ? join(VAULT_PATH, ".obsidian", "hook-logs") : null;
13
+
14
+ function logError(message) {
15
+ if (!LOG_DIR) {
16
+ console.error(`stop-sweep: ${message}`);
17
+ return;
18
+ }
19
+ try {
20
+ mkdirSync(LOG_DIR, { recursive: true });
21
+ appendFileSync(join(LOG_DIR, "sweep-errors.log"), `${new Date().toISOString()} ${message}\n`);
22
+ } catch {
23
+ console.error(`stop-sweep: ${message}`);
24
+ }
25
+ }
26
+
27
+ async function main() {
28
+ // Read hook input from stdin
29
+ let inputJson = "";
30
+ for await (const chunk of process.stdin) {
31
+ inputJson += chunk;
32
+ }
33
+
34
+ let input;
35
+ try {
36
+ input = JSON.parse(inputJson);
37
+ } catch {
38
+ logError("could not parse hook input JSON");
39
+ return;
40
+ }
41
+
42
+ const { transcript_path, session_id, cwd } = input;
43
+
44
+ // Skip if no transcript
45
+ if (!transcript_path) return;
46
+ try {
47
+ await access(transcript_path);
48
+ } catch {
49
+ return; // transcript file missing — normal for non-conversation triggers
50
+ }
51
+
52
+ if (!VAULT_PATH) {
53
+ console.error("stop-sweep: VAULT_PATH not set");
54
+ return;
55
+ }
56
+
57
+ if (!cwd) {
58
+ logError("hook input missing 'cwd' field");
59
+ return;
60
+ }
61
+
62
+ // Resolve project — no project, no captures
63
+ const { projectPath, error } = await resolveProject(cwd, VAULT_PATH);
64
+ if (error || !projectPath) return;
65
+
66
+ // Build MCP config
67
+ const indexPath = join(__dirname, "..", "index.js");
68
+ const mcpEnv = { VAULT_PATH };
69
+ if (process.env.OPENAI_API_KEY) {
70
+ mcpEnv.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
71
+ }
72
+ const mcpConfig = JSON.stringify({
73
+ mcpServers: {
74
+ "obsidian-pkm": {
75
+ command: "node",
76
+ args: [indexPath],
77
+ env: mcpEnv,
78
+ },
79
+ },
80
+ });
81
+
82
+ const prompt = `You are a PKM librarian agent. Your job is to identify PKM-worthy content from the most recent conversation exchange and file it correctly in the vault.
83
+
84
+ ## Context
85
+
86
+ - Project: ${projectPath}
87
+ - Session: ${session_id}
88
+ - Transcript: ${transcript_path}
89
+
90
+ ## Step 1: Read the transcript
91
+
92
+ Read the file at ${transcript_path}. Find the LAST user message and LAST assistant response — this is the current exchange. All prior messages are historical context only. Do NOT capture anything from prior messages.
93
+
94
+ ## Step 2: Classify what's PKM-worthy
95
+
96
+ Be conservative. Most exchanges produce NOTHING worth capturing. Skip:
97
+ - Trivial Q&A, clarifications, implementation details obvious from code
98
+ - Anything restating existing project context
99
+ - Content already handled by a vault_capture call in the same exchange
100
+ (read the vault_capture arguments to see what type/title/content was captured;
101
+ skip that specific item, but still capture other PKM-worthy content)
102
+
103
+ Before creating any note, use vault_search to check if a very similar note already exists — another sweep instance may have captured the same content from a prior exchange in a rapid conversation.
104
+
105
+ If you find something worth capturing, classify it:
106
+
107
+ | Content Type | Action |
108
+ |---|---|
109
+ | New task identified | Create task note: vault_write(template: "task", path: "${projectPath}/tasks/{kebab-title}.md") |
110
+ | Task completed/status changed | Find existing task via vault_query, then vault_update_frontmatter to update status |
111
+ | Significant decision agreed upon | Create ADR: vault_write(template: "adr", path: "${projectPath}/development/decisions/ADR-NNN-{kebab-title}.md") — use vault_list on the decisions directory to find the next ADR number |
112
+ | Research finding / gotcha | Create research note: vault_write(template: "research-note", path: "${projectPath}/research/{kebab-title}.md") |
113
+ | Bug root cause documented | Create troubleshooting log: vault_write(template: "troubleshooting-log", path: "${projectPath}/development/debug/{kebab-title}.md") |
114
+ | Minor implementation detail | SKIP — it's in the code/commits |
115
+
116
+ ## Step 3: Create/update notes properly
117
+
118
+ For every note you create:
119
+ 1. Use vault_write with the correct template
120
+ 2. Use vault_edit to replace ALL template placeholders with real content
121
+ 3. Use vault_suggest_links to find related notes (skip gracefully if unavailable)
122
+ 4. Add [[wikilinks]] to the related notes in the body
123
+ 5. Verify the note has no placeholder text remaining
124
+
125
+ For task updates:
126
+ 1. Use vault_query to find the task by title/tags
127
+ 2. Use vault_update_frontmatter to update status/priority
128
+ 3. Optionally vault_append to add context about what changed
129
+
130
+ ## Step 4: Quality check
131
+
132
+ If you created any notes, read each one back to confirm:
133
+ - No template placeholders remain
134
+ - Wikilinks are present to related notes (if vault_suggest_links was available)
135
+ - Frontmatter is complete and valid
136
+
137
+ If nothing is PKM-worthy, do nothing. Doing nothing is the correct default.`;
138
+
139
+ // All vault tools except vault_trash and vault_capture
140
+ const allowedTools = [
141
+ "vault_read", "vault_peek", "vault_write", "vault_append", "vault_edit",
142
+ "vault_update_frontmatter", "vault_search", "vault_semantic_search",
143
+ "vault_suggest_links", "vault_list", "vault_recent", "vault_links",
144
+ "vault_neighborhood", "vault_query", "vault_tags", "vault_activity", "vault_move",
145
+ ].map(t => `mcp__obsidian-pkm__${t}`).join(" ");
146
+
147
+ // Ensure log directory exists
148
+ mkdirSync(LOG_DIR, { recursive: true });
149
+ const logFile = join(LOG_DIR, `sweep-${new Date().toISOString().replace(/[:.]/g, "").slice(0, 15)}.log`);
150
+
151
+ const child = spawn("claude", [
152
+ "-p",
153
+ "--model", "sonnet",
154
+ "--mcp-config", mcpConfig,
155
+ "--max-turns", "15",
156
+ "--allowedTools", allowedTools,
157
+ ], {
158
+ detached: true,
159
+ stdio: ["pipe", "pipe", "pipe"],
160
+ });
161
+
162
+ // Pipe prompt to stdin
163
+ child.stdin.write(prompt);
164
+ child.stdin.end();
165
+
166
+ // Log stdout and stderr to file
167
+ const logStream = createWriteStream(logFile, { flags: "a" });
168
+ child.stdout.pipe(logStream);
169
+ child.stderr.pipe(logStream);
170
+
171
+ // Detach — let the background process run independently
172
+ child.unref();
173
+ }
174
+
175
+ main().catch((err) => {
176
+ logError(`uncaught: ${err.message}`);
177
+ process.exit(0);
178
+ });
package/init.js CHANGED
@@ -198,6 +198,160 @@ export function detectInstallType(filePath) {
198
198
  return { command: "node", args: [cliPath] };
199
199
  }
200
200
 
201
+ /**
202
+ * Replace the MCP_CONFIG= line in a shell script with a pre-baked JSON config.
203
+ * @param {string} scriptContent - Shell script content
204
+ * @param {{ command: string, args: string[] }} installType - Server command
205
+ * @returns {string} Patched script content
206
+ */
207
+ export function patchMcpConfig(scriptContent, installType) {
208
+ const argsJson = JSON.stringify(installType.args);
209
+ const replacement = `MCP_CONFIG='{"mcpServers":{"obsidian-pkm":{"command":"${installType.command}","args":${argsJson},"env":{"VAULT_PATH":"'"$VAULT_PATH"'"}}}}'`;
210
+ const lines = scriptContent.split("\n");
211
+ const patched = lines.map(line => line.startsWith("MCP_CONFIG=") ? replacement : line);
212
+ return patched.join("\n");
213
+ }
214
+
215
+ const PKM_HOOK_BASENAMES = new Set(["session-start.js", "stop-sweep.sh", "capture-handler.sh"]);
216
+
217
+ /**
218
+ * Detect if a hook entry is a PKM hook (by path substring or script basename).
219
+ * @param {object} entry - A hook event entry with `hooks` array
220
+ * @returns {boolean}
221
+ */
222
+ export function isPkmHookEntry(entry) {
223
+ if (!entry.hooks || !Array.isArray(entry.hooks)) return false;
224
+ return entry.hooks.some(h => {
225
+ const cmd = h.command || "";
226
+ if (cmd.includes("hooks/pkm/")) return true;
227
+ for (const basename of PKM_HOOK_BASENAMES) {
228
+ if (cmd.includes(basename)) return true;
229
+ }
230
+ return false;
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Build settings.json hook config entries for enabled hooks.
236
+ * @param {string} vaultPath - Absolute vault path
237
+ * @param {string} hooksDir - Absolute path to installed hooks directory
238
+ * @param {{ sessionStart: boolean, stopSweep: boolean, captureHandler: boolean }} enabledHooks
239
+ * @returns {Object} Hook entries keyed by event name (SessionStart, Stop, PostToolUse)
240
+ */
241
+ export function buildHookEntries(vaultPath, hooksDir, enabledHooks) {
242
+ const entries = {};
243
+
244
+ if (enabledHooks.sessionStart) {
245
+ entries.SessionStart = [{
246
+ matcher: "startup|clear|compact",
247
+ hooks: [{
248
+ type: "command",
249
+ command: `VAULT_PATH="${vaultPath}" node ${path.join(hooksDir, "session-start.js")}`,
250
+ timeout: 15,
251
+ statusMessage: "Loading PKM project context...",
252
+ }],
253
+ }];
254
+ }
255
+
256
+ if (enabledHooks.stopSweep) {
257
+ entries.Stop = [{
258
+ hooks: [{
259
+ type: "command",
260
+ command: `VAULT_PATH="${vaultPath}" ${path.join(hooksDir, "stop-sweep.sh")}`,
261
+ async: true,
262
+ timeout: 10,
263
+ }],
264
+ }];
265
+ }
266
+
267
+ if (enabledHooks.captureHandler) {
268
+ entries.PostToolUse = [{
269
+ matcher: "mcp__obsidian-pkm__vault_capture",
270
+ hooks: [{
271
+ type: "command",
272
+ command: `VAULT_PATH="${vaultPath}" ${path.join(hooksDir, "capture-handler.sh")}`,
273
+ async: true,
274
+ timeout: 10,
275
+ }],
276
+ }];
277
+ }
278
+
279
+ return entries;
280
+ }
281
+
282
+ /**
283
+ * Merge PKM hook entries into ~/.claude/settings.json.
284
+ * @param {string} settingsPath - Path to settings.json
285
+ * @param {Object} hookEntries - Entries to add, keyed by event name
286
+ * @param {string[]} disabledEvents - Event names where PKM hooks should be removed
287
+ * @returns {Promise<{ error?: string }>}
288
+ */
289
+ export async function mergeHooksIntoSettings(settingsPath, hookEntries, disabledEvents) {
290
+ let settings = {};
291
+
292
+ try {
293
+ const raw = await fs.readFile(settingsPath, "utf8");
294
+ try {
295
+ settings = JSON.parse(raw);
296
+ } catch {
297
+ return { error: `${settingsPath} contains invalid JSON. Fix it manually and re-run init.` };
298
+ }
299
+ } catch (e) {
300
+ if (e.code !== "ENOENT") throw e;
301
+ // File doesn't exist — start fresh
302
+ }
303
+
304
+ if (!settings.hooks) settings.hooks = {};
305
+
306
+ // Add/replace enabled hooks
307
+ for (const [eventName, entries] of Object.entries(hookEntries)) {
308
+ if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
309
+ // Remove existing PKM entries
310
+ settings.hooks[eventName] = settings.hooks[eventName].filter(e => !isPkmHookEntry(e));
311
+ // Append new entries
312
+ settings.hooks[eventName].push(...entries);
313
+ }
314
+
315
+ // Remove disabled hooks
316
+ for (const eventName of disabledEvents) {
317
+ if (!settings.hooks[eventName]) continue;
318
+ settings.hooks[eventName] = settings.hooks[eventName].filter(e => !isPkmHookEntry(e));
319
+ if (settings.hooks[eventName].length === 0) {
320
+ delete settings.hooks[eventName];
321
+ }
322
+ }
323
+
324
+ await fs.mkdir(path.dirname(settingsPath), { recursive: true });
325
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n");
326
+ return {};
327
+ }
328
+
329
+ const HOOK_FILES = ["session-start.js", "resolve-project.js", "load-context.js", "stop-sweep.sh", "capture-handler.sh"];
330
+ const SHELL_HOOKS = new Set(["stop-sweep.sh", "capture-handler.sh"]);
331
+
332
+ /**
333
+ * Copy hook scripts to destination, patching shell scripts with correct MCP config.
334
+ * @param {string} src - Source hooks directory
335
+ * @param {string} dest - Destination directory (e.g. ~/.claude/hooks/pkm/)
336
+ * @param {{ command: string, args: string[] }} installType - Server command for MCP config patching
337
+ */
338
+ export async function copyHooks(src, dest, installType) {
339
+ await fs.mkdir(dest, { recursive: true });
340
+
341
+ for (const file of HOOK_FILES) {
342
+ const srcFile = path.join(src, file);
343
+ const destFile = path.join(dest, file);
344
+
345
+ if (SHELL_HOOKS.has(file)) {
346
+ let content = await fs.readFile(srcFile, "utf8");
347
+ content = patchMcpConfig(content, installType);
348
+ await fs.writeFile(destFile, content, { mode: 0o755 });
349
+ } else {
350
+ await fs.copyFile(srcFile, destFile);
351
+ }
352
+ }
353
+ }
354
+
201
355
  const SYSTEM_DIRS = new Set(["/", "/home", "/usr", "/var", "/etc", "/tmp", "/opt", "/bin", "/sbin"]);
202
356
 
203
357
  function formatBytes(bytes) {
@@ -222,13 +376,14 @@ export async function runInit() {
222
376
  pkm-mcp-server setup wizard
223
377
 
224
378
  This will walk you through setting up your Obsidian vault for use with the
225
- PKM MCP server. You'll be asked about 5 things:
379
+ PKM MCP server. You'll be asked about 6 things:
226
380
 
227
381
  1. Where your vault is (or where to create one)
228
382
  2. Whether to install note templates
229
383
  3. Whether to set up the recommended folder structure
230
384
  4. An optional OpenAI API key for semantic search
231
385
  5. Registering the server with Claude Code
386
+ 6. Setting up PKM hooks for Claude Code
232
387
 
233
388
  Nothing is written until you confirm each step. Press Ctrl+C at any time to cancel.
234
389
  `);
@@ -316,6 +471,7 @@ Nothing is written until you confirm each step. Press Ctrl+C at any time to canc
316
471
  }
317
472
  // ── Step 6: Registration ──
318
473
  const hasClaude = await checkClaudeCli();
474
+ let hasMcpRegistration = false;
319
475
  if (!hasClaude) {
320
476
  const installType = detectInstallType();
321
477
  const manualCmd = `claude mcp add -s user -e VAULT_PATH=${vaultPath} -- obsidian-pkm ${installType.command} ${installType.args.join(" ")}`;
@@ -335,6 +491,7 @@ Nothing is written until you confirm each step. Press Ctrl+C at any time to canc
335
491
  if (!overwrite) {
336
492
  console.log(" Registration skipped.\n");
337
493
  skipRegistration = true;
494
+ hasMcpRegistration = true;
338
495
  } else {
339
496
  // Remove existing before re-adding
340
497
  try {
@@ -360,6 +517,7 @@ Nothing is written until you confirm each step. Press Ctrl+C at any time to canc
360
517
  await execFileAsync("claude", addArgs);
361
518
  console.log(" MCP server registered with Claude Code");
362
519
  steps.push("MCP server: registered");
520
+ hasMcpRegistration = true;
363
521
  } catch (regErr) {
364
522
  console.error(`\n Registration failed: ${regErr.message}`);
365
523
  if (regErr.stderr) console.error(` ${regErr.stderr.trim()}`);
@@ -377,7 +535,98 @@ Nothing is written until you confirm each step. Press Ctrl+C at any time to canc
377
535
  }
378
536
  }
379
537
 
380
- // ── Step 7: Summary ──
538
+ const skipHooks = !hasClaude || !hasMcpRegistration;
539
+
540
+ // ── Step 7: PKM Hooks ──
541
+ const hooksDir = path.join(os.homedir(), ".claude", "hooks", "pkm");
542
+ const bundledHooksDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "hooks");
543
+ let hooksSummary;
544
+
545
+ if (skipHooks) {
546
+ const reason = !hasClaude ? "Claude CLI not found" : "no MCP registration";
547
+ hooksSummary = `skipped (${reason})`;
548
+ steps.push(`Hooks: skipped (${reason})`);
549
+ } else {
550
+ // Check for existing hook files
551
+ let hookFilesExist = false;
552
+ try {
553
+ const existing = await fs.readdir(hooksDir);
554
+ hookFilesExist = existing.length > 0;
555
+ } catch { /* doesn't exist */ }
556
+
557
+ let doCopyHooks = true;
558
+ if (hookFilesExist) {
559
+ doCopyHooks = await confirmPrompt({
560
+ message: "PKM hook scripts are already installed. Overwrite with current version?",
561
+ default: false,
562
+ });
563
+ }
564
+
565
+ if (doCopyHooks) {
566
+ try {
567
+ const installType = detectInstallType();
568
+ await copyHooks(bundledHooksDir, hooksDir, installType);
569
+ console.log(` Hook scripts installed to ${hooksDir}`);
570
+ } catch (copyErr) {
571
+ console.error(`\n Hook file installation failed: ${copyErr.message}`);
572
+ const skipHookSetup = await confirmPrompt({ message: "Skip hooks and finish setup?", default: true });
573
+ if (skipHookSetup) {
574
+ hooksSummary = "skipped (file copy failed)";
575
+ steps.push("Hooks: skipped (file copy failed)");
576
+ } else {
577
+ throw copyErr;
578
+ }
579
+ }
580
+ }
581
+
582
+ // Only proceed to config if we haven't set hooksSummary (i.e., no error)
583
+ if (!hooksSummary) {
584
+ const enableSessionStart = await confirmPrompt({
585
+ message: "Enable project context loading? Automatically loads your project's index, recent devlog entries, and active tasks at the start of each Claude Code session.",
586
+ default: true,
587
+ });
588
+
589
+ const enableStopSweep = await confirmPrompt({
590
+ message: "Enable passive capture? After each Claude response, a background agent scans the conversation for decisions, task changes, and research findings, and stages them in your vault's inbox.",
591
+ default: true,
592
+ });
593
+
594
+ const enableCaptureHandler = await confirmPrompt({
595
+ message: "Enable explicit capture? When Claude calls vault_capture, a background agent creates a properly structured vault note (ADR, task, research note, or bug report) from the capture payload.",
596
+ default: true,
597
+ });
598
+
599
+ const enabledHooks = {
600
+ sessionStart: enableSessionStart,
601
+ stopSweep: enableStopSweep,
602
+ captureHandler: enableCaptureHandler,
603
+ };
604
+
605
+ const hookEntries = buildHookEntries(vaultPath, hooksDir, enabledHooks);
606
+ const disabledEvents = [];
607
+ if (!enableSessionStart) disabledEvents.push("SessionStart");
608
+ if (!enableStopSweep) disabledEvents.push("Stop");
609
+ if (!enableCaptureHandler) disabledEvents.push("PostToolUse");
610
+
611
+ const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
612
+ const mergeResult = await mergeHooksIntoSettings(settingsPath, hookEntries, disabledEvents);
613
+
614
+ if (mergeResult.error) {
615
+ console.error(`\n ${mergeResult.error}`);
616
+ hooksSummary = "skipped (settings.json error)";
617
+ } else {
618
+ const names = [];
619
+ if (enableSessionStart) names.push("context-loading");
620
+ if (enableStopSweep) names.push("passive-capture");
621
+ if (enableCaptureHandler) names.push("explicit-capture");
622
+ hooksSummary = names.length > 0 ? names.join(", ") : "none";
623
+ console.log(` Hooks configured: ${hooksSummary}`);
624
+ }
625
+ steps.push(`Hooks: ${hooksSummary}`);
626
+ }
627
+ }
628
+
629
+ // ── Step 8: Summary ──
381
630
  // Build summary lines based on what was actually done
382
631
  const templateSummary = templateMode === "skip"
383
632
  ? "Skipped"
@@ -402,6 +651,7 @@ Setup complete!
402
651
  Folders: ${folderSummary}
403
652
  Semantic: ${semanticSummary}
404
653
  Claude Code: ${registrationSummary}
654
+ Hooks: ${hooksSummary}
405
655
 
406
656
  To verify, restart Claude Code and try:
407
657
  "List the folders in my vault"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pkm-mcp-server",
3
- "version": "1.3.3",
3
+ "version": "1.4.1",
4
4
  "description": "MCP server for Obsidian vault integration with Claude Code — 19 tools for notes, search, and graph traversal",
5
5
  "main": "cli.js",
6
6
  "exports": {