pkm-mcp-server 1.4.0 → 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,11 @@ 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
+
9
14
  ## [1.4.0] - 2026-03-21
10
15
 
11
16
  ### Added
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pkm-mcp-server",
3
- "version": "1.4.0",
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": {