pkm-mcp-server 1.1.1 → 1.2.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,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.2.1] - 2026-03-17
10
+
11
+ ### Added
12
+ - CI workflow for automated npm publishing via GitHub releases (OIDC trusted publishing, no tokens)
13
+
14
+ ### Fixed
15
+ - Hook setup instructions now included in main README Quick Start (previously only in `hooks/README.md`, invisible on npm)
16
+ - `hooks/` directory added to architecture file tree in README
17
+
18
+ ## [1.2.0] - 2026-03-17
19
+
20
+ ### Added
21
+ - `vault_capture` tool — signal a PKM-worthy capture (decision, task, research, bug); returns immediately while a background hook creates the note
22
+ - PKM hook scripts for Claude Code integration:
23
+ - SessionStart hook (`session-start.js`) — loads project context from vault automatically
24
+ - PostToolUse hook (`capture-handler.sh`) — spawns Sonnet agent for explicit `vault_capture` calls
25
+ - Stop hook (`stop-sweep.sh`) — spawns Haiku agent for passive decision/task sweep at session end
26
+ - Enum validation for task `status` (pending/active/done/cancelled) and `priority` (low/normal/high/urgent) fields in `vault_write` and `vault_update_frontmatter` — non-task note types are unaffected
27
+
28
+ ### Changed
29
+ - `sqlite-vec` upgraded from pinned alpha (0.1.7-alpha.2) to stable release (0.1.7)
30
+ - `@modelcontextprotocol/sdk` updated to 1.27.1
31
+ - `better-sqlite3` updated to 12.8.0
32
+
33
+ ### Security
34
+ - Resolved 7 transitive dependency vulnerabilities (hono, @hono/node-server, ajv, express-rate-limit, flatted, minimatch, qs)
35
+
9
36
  ## [1.1.0] - 2026-02-23
10
37
 
11
38
  ### Changed
@@ -55,6 +82,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55
82
  - Atomic file creation in `vault_write` (`wx` flag) prevents race conditions
56
83
  - Error messages sanitized to prevent leaking absolute vault paths
57
84
 
58
- [Unreleased]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.1.0...HEAD
85
+ [Unreleased]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.2.1...HEAD
86
+ [1.2.1]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.2.0...v1.2.1
87
+ [1.2.0]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.1.0...v1.2.0
59
88
  [1.1.0]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.0.0...v1.1.0
60
89
  [1.0.0]: https://github.com/AdrianV101/Obsidian-MCP/releases/tag/v1.0.0
package/README.md CHANGED
@@ -36,12 +36,12 @@ https://github.com/user-attachments/assets/58ad9c9b-d987-4728-89e7-33de20b73a38
36
36
  | `vault_recent` | Recently modified files |
37
37
  | `vault_links` | Wikilink analysis (incoming/outgoing) |
38
38
  | `vault_neighborhood` | Graph exploration via BFS wikilink traversal |
39
- | `vault_query` | Query notes by YAML frontmatter (type, status, tags, dates, custom fields, sorting) |
39
+ | `vault_query` | Query notes by YAML frontmatter (type, status, tags/tags_any, dates, custom fields, sorting) |
40
40
  | `vault_tags` | Discover tags with counts; folder scoping, glob filters, inline tag parsing |
41
41
  | `vault_activity` | Session activity log for cross-conversation memory |
42
42
  | `vault_trash` | Soft-delete to `.trash/` (Obsidian convention), warns about broken incoming links |
43
43
  | `vault_move` | Move/rename files with automatic wikilink updating across vault |
44
- | `vault_update_frontmatter` | Atomic YAML frontmatter updates (set, create, remove fields) |
44
+ | `vault_update_frontmatter` | Atomic YAML frontmatter updates (set, create, remove fields; validates enum fields by note type) |
45
45
 
46
46
  ### Fuzzy Path Resolution
47
47
 
@@ -148,6 +148,67 @@ Add your OpenAI API key to the env block:
148
148
 
149
149
  This enables `vault_semantic_search` and `vault_suggest_links`. Uses `text-embedding-3-large` with a SQLite + sqlite-vec index stored at `.obsidian/semantic-index.db`. The index rebuilds automatically — delete the DB file to force a full re-embed.
150
150
 
151
+ ### 4. Enable PKM Hooks (optional)
152
+
153
+ The hook system adds automatic context loading at session start and passive knowledge capture during coding. Requires the [Claude CLI](https://docs.anthropic.com/en/docs/claude-cli) installed and authenticated.
154
+
155
+ Add to your `~/.claude/settings.json` (alongside the `mcpServers` block):
156
+
157
+ ```json
158
+ {
159
+ "hooks": {
160
+ "SessionStart": [
161
+ {
162
+ "matcher": "startup|clear|compact",
163
+ "hooks": [
164
+ {
165
+ "type": "command",
166
+ "command": "VAULT_PATH=\"/path/to/your/vault\" node /path/to/Obsidian-MCP/hooks/session-start.js",
167
+ "timeout": 15,
168
+ "statusMessage": "Loading PKM project context..."
169
+ }
170
+ ]
171
+ }
172
+ ],
173
+ "Stop": [
174
+ {
175
+ "hooks": [
176
+ {
177
+ "type": "command",
178
+ "command": "VAULT_PATH=\"/path/to/your/vault\" /path/to/Obsidian-MCP/hooks/stop-sweep.sh",
179
+ "async": true,
180
+ "timeout": 10
181
+ }
182
+ ]
183
+ }
184
+ ],
185
+ "PostToolUse": [
186
+ {
187
+ "matcher": "mcp__obsidian-pkm__vault_capture",
188
+ "hooks": [
189
+ {
190
+ "type": "command",
191
+ "command": "VAULT_PATH=\"/path/to/your/vault\" /path/to/Obsidian-MCP/hooks/capture-handler.sh",
192
+ "async": true,
193
+ "timeout": 10
194
+ }
195
+ ]
196
+ }
197
+ ]
198
+ }
199
+ }
200
+ ```
201
+
202
+ Replace `/path/to/your/vault` with your Obsidian vault path and `/path/to/Obsidian-MCP` with the path to this repo (or the global npm install location).
203
+
204
+ | Hook | Event | What it does |
205
+ |------|-------|--------------|
206
+ | `session-start.js` | SessionStart | Loads project context (index, devlog, active tasks) at session start |
207
+ | `stop-sweep.sh` | Stop | Scans each exchange for PKM-worthy decisions/tasks, appends to daily captures |
208
+ | `capture-handler.sh` | PostToolUse | Creates structured vault notes when `vault_capture` is called |
209
+
210
+ See [hooks/README.md](hooks/README.md) for architecture details and troubleshooting.
211
+
151
212
  ## Vault Structure
152
213
 
153
214
  The server works with any Obsidian vault. The included templates assume this layout:
@@ -203,6 +264,7 @@ graph LR
203
264
  ├── embeddings.js # Semantic index (OpenAI embeddings, SQLite + sqlite-vec)
204
265
  ├── activity.js # Activity log (session tracking, SQLite)
205
266
  ├── utils.js # Shared utilities (frontmatter parsing, file listing)
267
+ ├── hooks/ # Claude Code hooks (session context, passive capture)
206
268
  ├── templates/ # Obsidian note templates
207
269
  └── sample-project/ # Sample CLAUDE.md for your repos
208
270
  ```
@@ -211,7 +273,7 @@ All paths passed to tools are relative to vault root. The server includes path s
211
273
 
212
274
  ## How It Works
213
275
 
214
- **Note creation** is template-based. `vault_write` loads templates from `05-Templates/`, substitutes Templater-compatible variables (`<% tp.date.now("YYYY-MM-DD") %>`, `<% tp.file.title %>`), and validates required frontmatter fields (`type`, `created`, `tags`).
276
+ **Note creation** is template-based. `vault_write` loads templates from `05-Templates/`, substitutes Templater-compatible variables (`<% tp.date.now("YYYY-MM-DD") %>`, `<% tp.file.title %>`), and validates required frontmatter fields (`type`, `created`, `tags`). Optional frontmatter fields — `status`, `priority`, `project`, `deciders`, `due`, `source` — can be set per template type. Task notes enforce enum validation on `status` (pending/active/done/cancelled) and `priority` (low/normal/high/urgent).
215
277
 
216
278
  **Semantic search** embeds notes on startup and watches for changes via `fs.watch`. Long notes are chunked by `##` headings. The index is a regenerable cache stored in `.obsidian/` so it syncs across machines via Obsidian Sync. The initial sync runs in the background — search is available immediately but may return incomplete results until sync finishes (a progress message is shown).
217
279
 
package/handlers.js CHANGED
@@ -848,6 +848,21 @@ export async function createHandlers({ vaultPath, templateRegistry, semanticInde
848
848
  };
849
849
  }
850
850
 
851
+ async function handleCapture(args) {
852
+ const { type, title, content } = args;
853
+ if (!type || !title || !content) {
854
+ throw new Error(
855
+ `vault_capture requires type, title, and content. Got: type=${type || "(missing)"}, title=${title || "(missing)"}, content=${content ? "provided" : "(missing)"}`
856
+ );
857
+ }
858
+ return {
859
+ content: [{
860
+ type: "text",
861
+ text: `Capture queued: [${type}] ${title}`
862
+ }]
863
+ };
864
+ }
865
+
851
866
  return new Map([
852
867
  ["vault_read", handleRead],
853
868
  ["vault_write", handleWrite],
@@ -867,5 +882,6 @@ export async function createHandlers({ vaultPath, templateRegistry, semanticInde
867
882
  ["vault_trash", handleTrash],
868
883
  ["vault_move", handleMove],
869
884
  ["vault_update_frontmatter", handleUpdateFrontmatter],
885
+ ["vault_capture", handleCapture],
870
886
  ]);
871
887
  }
@@ -0,0 +1,125 @@
1
+ # PKM Hook System
2
+
3
+ Claude Code hooks that enable automatic knowledge capture during coding sessions.
4
+
5
+ ## Overview
6
+
7
+ The hook system consists of three hooks:
8
+
9
+ | Hook | Event | Purpose |
10
+ |------|-------|---------|
11
+ | `session-start.js` | SessionStart | Loads project context from the vault at the start of each session |
12
+ | `stop-sweep.sh` | Stop | Passive capture: scans the latest exchange for PKM-worthy content |
13
+ | `capture-handler.sh` | PostToolUse | Explicit capture: creates structured vault notes when `vault_capture` is called |
14
+
15
+ ### How it works
16
+
17
+ - **SessionStart** (`session-start.js`): Runs synchronously. Resolves the current working directory to a vault project, reads the project index, recent devlog entries, and active tasks, then injects them as context.
18
+ - **Stop** (`stop-sweep.sh`): Runs asynchronously after each assistant response. Spawns a background `claude -p` (Haiku) that reads the transcript, identifies decisions/tasks/findings from the latest exchange, and appends them to a daily captures file in `00-Inbox/`.
19
+ - **PostToolUse** (`capture-handler.sh`): Runs asynchronously after `vault_capture` tool use. Spawns a background `claude -p` (Sonnet) that creates a properly structured vault note (ADR, task, research note, or troubleshooting log) from the capture payload.
20
+
21
+ ## Setup
22
+
23
+ ### Prerequisites
24
+
25
+ - [Claude CLI](https://docs.anthropic.com/en/docs/claude-cli) installed and authenticated
26
+ - This repository cloned locally
27
+ - An Obsidian vault following the [vault structure convention](../CLAUDE.md#vault-structure-convention)
28
+
29
+ ### Configuration
30
+
31
+ Add the following to your `~/.claude/settings.json`:
32
+
33
+ ```json
34
+ {
35
+ "hooks": {
36
+ "SessionStart": [
37
+ {
38
+ "matcher": "startup|clear|compact",
39
+ "hooks": [
40
+ {
41
+ "type": "command",
42
+ "command": "VAULT_PATH=\"/path/to/your/vault\" node /path/to/Obsidian-MCP/hooks/session-start.js",
43
+ "timeout": 15,
44
+ "statusMessage": "Loading PKM project context..."
45
+ }
46
+ ]
47
+ }
48
+ ],
49
+ "Stop": [
50
+ {
51
+ "hooks": [
52
+ {
53
+ "type": "command",
54
+ "command": "VAULT_PATH=\"/path/to/your/vault\" /path/to/Obsidian-MCP/hooks/stop-sweep.sh",
55
+ "async": true,
56
+ "timeout": 10
57
+ }
58
+ ]
59
+ }
60
+ ],
61
+ "PostToolUse": [
62
+ {
63
+ "matcher": "mcp__obsidian-pkm__vault_capture",
64
+ "hooks": [
65
+ {
66
+ "type": "command",
67
+ "command": "VAULT_PATH=\"/path/to/your/vault\" /path/to/Obsidian-MCP/hooks/capture-handler.sh",
68
+ "async": true,
69
+ "timeout": 10
70
+ }
71
+ ]
72
+ }
73
+ ]
74
+ }
75
+ }
76
+ ```
77
+
78
+ Replace `/path/to/your/vault` with the absolute path to your Obsidian vault (e.g., `~/Documents/PKM`), and `/path/to/Obsidian-MCP` with the absolute path to this repository.
79
+
80
+ ## Architecture Notes
81
+
82
+ ### Command hooks with background subprocesses
83
+
84
+ The hook system uses `type: "command"` hooks (not `type: "agent"`). The shell scripts themselves exit quickly (within the configured timeout), but they spawn `claude -p` as a detached background process via `nohup ... &`. This means:
85
+
86
+ - The hook script exits immediately, satisfying the timeout constraint
87
+ - The `claude -p` process continues running independently, doing the actual vault work
88
+ - `--max-turns` limits how many tool calls the background process can make (5 for stop-sweep, 25 for capture-handler)
89
+
90
+ ### MCP config resolution
91
+
92
+ The shell scripts use `cd "$(dirname "$0")" && pwd -P` to resolve their own location, then construct the path to `index.js` relative to the script directory. This means the scripts work regardless of the current working directory. The `VAULT_PATH` environment variable must be set in the hook command because hook scripts run outside the MCP server process.
93
+
94
+ ### Async behavior
95
+
96
+ Both `stop-sweep.sh` and `capture-handler.sh` use `async: true` in the hook config. This means Claude Code does not wait for the hook script to finish before continuing. The script starts, spawns the background `claude -p` process, and exits. The background process runs to completion on its own.
97
+
98
+ ### Noise suppression and deduplication
99
+
100
+ The stop-sweep hook is conservative by design. It only looks at the last exchange (one user message + one assistant response) and skips trivial interactions. If the exchange already contains an explicit `vault_capture` call, the stop-sweep skips that content to avoid duplication with the capture-handler.
101
+
102
+ ## Troubleshooting
103
+
104
+ ### Hook not firing
105
+
106
+ - Verify the hook config is valid JSON in `~/.claude/settings.json`
107
+ - Check that the `matcher` pattern matches (SessionStart uses `"startup|clear|compact"`, PostToolUse uses the full MCP tool name)
108
+ - Ensure the script paths are absolute
109
+
110
+ ### Script errors
111
+
112
+ - Check that `VAULT_PATH` is set correctly and the directory exists
113
+ - Verify `node` is available in the hook's PATH
114
+ - Test the script manually: `echo '{"cwd":"/tmp","transcript_path":"/tmp/test","session_id":"abc123"}' | VAULT_PATH="/path/to/vault" ./hooks/stop-sweep.sh`
115
+
116
+ ### Background process not running
117
+
118
+ - Check for `claude` CLI in PATH
119
+ - Look for `claude -p` processes: `ps aux | grep 'claude -p'`
120
+ - Test `claude -p` directly: `echo "say hello" | claude -p --model sonnet`
121
+ - Check logs in `$VAULT_PATH/.obsidian/hook-logs/` for background process output
122
+
123
+ ### macOS compatibility
124
+
125
+ The scripts use POSIX-compatible `cd "$(dirname "$0")" && pwd -P` for path resolution, so they work on both Linux and macOS without additional dependencies.
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # PostToolUse hook: explicit PKM capture via vault_capture tool
3
+ # Runs async after vault_capture returns. Spawns claude -p with Sonnet
4
+ # to create a properly structured vault note.
5
+
6
+ set -euo pipefail
7
+
8
+ LOG_DIR="${VAULT_PATH:?}/.obsidian/hook-logs"
9
+ mkdir -p "$LOG_DIR"
10
+ PROMPT_FILE=""
11
+ cleanup() { [ -n "$PROMPT_FILE" ] && rm -f "$PROMPT_FILE"; }
12
+ trap 'echo "capture-handler: failed at line $LINENO" >> "$LOG_DIR/capture-errors.log"; cleanup' ERR
13
+ trap cleanup EXIT
14
+
15
+ # Read hook input from stdin
16
+ INPUT=$(cat)
17
+
18
+ # Extract tool_input fields (buffer all stdin before parsing)
19
+ eval "$(echo "$INPUT" | node -e "
20
+ let b='';
21
+ process.stdin.on('data',c=>b+=c);
22
+ process.stdin.on('end',()=>{
23
+ const j=JSON.parse(b);
24
+ const ti=j.tool_input||{};
25
+ console.log('TOOL_INPUT='+JSON.stringify(JSON.stringify(ti)));
26
+ console.log('CAPTURE_TYPE='+JSON.stringify(ti.type||''));
27
+ console.log('CAPTURE_TITLE='+JSON.stringify(ti.title||''));
28
+ console.log('CAPTURE_CONTENT='+JSON.stringify(ti.content||''));
29
+ })
30
+ ")"
31
+
32
+ # Skip if missing required fields
33
+ if [ -z "$CAPTURE_TYPE" ] || [ -z "$CAPTURE_TITLE" ] || [ -z "$CAPTURE_CONTENT" ]; then
34
+ echo "capture-handler: skipping - missing required fields (type='$CAPTURE_TYPE')" >&2
35
+ exit 0
36
+ fi
37
+
38
+ # MCP config for obsidian-pkm server
39
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd -P)
40
+ MCP_CONFIG=$(node -e "console.log(JSON.stringify({mcpServers:{'obsidian-pkm':{command:'node',args:[process.argv[1]],env:{VAULT_PATH:process.argv[2]}}}}))" "$SCRIPT_DIR/../index.js" "${VAULT_PATH:-$HOME/Documents/PKM}")
41
+
42
+ # Build prompt via Node.js to avoid shell injection from user content
43
+ PROMPT_FILE=$(mktemp)
44
+ node -e "
45
+ const ti = JSON.parse(process.argv[1]);
46
+ const project = ti.project
47
+ ? 'The project is: ' + ti.project
48
+ : 'The project is not specified. Check vault_activity recent entries to infer the active project.';
49
+ const prompt = \`You are a PKM note creation agent. Your job is NOT done until the note has real content — not template placeholders.
50
+
51
+ ## What to capture
52
+
53
+ - Type: \${ti.type}
54
+ - Title: \${ti.title}
55
+ - Content: \${ti.content}
56
+ - Priority: \${ti.priority || 'normal'}
57
+ - \${project}
58
+
59
+ ## Required steps (you must do ALL of these)
60
+
61
+ 1. Create the note with vault_write using the appropriate template:
62
+ - research → template 'research-note', path: 01-Projects/{project}/research/{kebab-title}.md
63
+ - adr → template 'adr', path: 01-Projects/{project}/development/decisions/ADR-NNN-{kebab-title}.md (use vault_list to get next number)
64
+ - task → template 'task', path: 01-Projects/{project}/tasks/{kebab-title}.md (vault_query first to check for duplicates)
65
+ - bug → template 'troubleshooting-log', path: 01-Projects/{project}/development/debug/{kebab-title}.md
66
+ If vault_write fails because the file exists, use a different filename.
67
+
68
+ 2. Read the created note with vault_read.
69
+
70
+ 3. Use vault_edit to replace EVERY template placeholder with real content derived from the Title and Content above. For example, replace 'Brief description of the technology, tool, or concept.' with an actual description. Do this for EACH section — you will need multiple vault_edit calls.
71
+
72
+ 4. Read the note one final time to confirm no placeholder text remains.
73
+
74
+ CRITICAL: If you stop after step 1 or 2, you have FAILED. The note will contain useless placeholder text like 'Brief description of the technology, tool, or concept.' which is worse than no note at all. You MUST reach step 4.\`;
75
+ require('fs').writeFileSync(process.argv[2], prompt);
76
+ " "$TOOL_INPUT" "$PROMPT_FILE"
77
+
78
+ # Spawn claude -p in background with logging
79
+ nohup claude -p --model sonnet --mcp-config "$MCP_CONFIG" --max-turns 25 --allowedTools "mcp__obsidian-pkm__vault_write mcp__obsidian-pkm__vault_read mcp__obsidian-pkm__vault_edit mcp__obsidian-pkm__vault_append mcp__obsidian-pkm__vault_query mcp__obsidian-pkm__vault_list mcp__obsidian-pkm__vault_update_frontmatter mcp__obsidian-pkm__vault_activity" < "$PROMPT_FILE" >> "$LOG_DIR/capture-$(date +%Y%m%d-%H%M%S).log" 2>&1 &
80
+
81
+ exit 0
@@ -0,0 +1,64 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { extractFrontmatter } from "../utils.js";
4
+ import { extractTailSections } from "../helpers.js";
5
+
6
+ export async function loadProjectContext(vaultPath, projectPath) {
7
+ const projectName = path.basename(projectPath);
8
+ const projectDir = path.join(vaultPath, projectPath);
9
+ const sections = [];
10
+
11
+ sections.push(`## PKM Project Context: ${projectName}`);
12
+
13
+ try {
14
+ const indexContent = await fs.readFile(path.join(projectDir, "_index.md"), "utf-8");
15
+ sections.push(`### Project Index\n${indexContent}`);
16
+ } catch (e) {
17
+ if (e.code !== "ENOENT") console.error("PKM load-context: error reading _index.md:", e.message);
18
+ }
19
+
20
+ try {
21
+ const devlogContent = await fs.readFile(
22
+ path.join(projectDir, "development", "devlog.md"), "utf-8"
23
+ );
24
+ const tailSections = extractTailSections(devlogContent, 3, 2);
25
+ sections.push(`### Recent Development Activity\n${tailSections}`);
26
+ } catch (e) {
27
+ if (e.code !== "ENOENT") console.error("PKM load-context: error reading devlog:", e.message);
28
+ }
29
+
30
+ const tasks = [];
31
+ try {
32
+ const taskDir = path.join(projectDir, "tasks");
33
+ const entries = await fs.readdir(taskDir);
34
+ for (const entry of entries) {
35
+ if (!entry.endsWith(".md")) continue;
36
+ const content = await fs.readFile(path.join(taskDir, entry), "utf-8");
37
+ const fm = extractFrontmatter(content);
38
+ if (!fm || (fm.status !== "active" && fm.status !== "pending")) continue;
39
+
40
+ const bodyStart = content.indexOf("\n---", 3);
41
+ const body = bodyStart !== -1 ? content.slice(bodyStart + 4).trim() : content;
42
+ const lines = body.split("\n");
43
+ const titleLine = lines.find(l => l.startsWith("# "));
44
+ const title = titleLine ? titleLine.slice(2).trim() : entry.replace(".md", "");
45
+ const descLines = lines
46
+ .filter(l => l.trim() && !l.startsWith("#"))
47
+ .slice(0, 2)
48
+ .map(l => ` ${l.trim()}`)
49
+ .join("\n");
50
+
51
+ tasks.push(`- ${title} (status: ${fm.status}, priority: ${fm.priority || "normal"})\n${descLines}`);
52
+ }
53
+ } catch (e) {
54
+ if (e.code !== "ENOENT") console.error("PKM load-context: error reading tasks:", e.message);
55
+ }
56
+
57
+ if (tasks.length > 0) {
58
+ sections.push(`### Active Tasks\n${tasks.join("\n")}`);
59
+ } else {
60
+ sections.push("### Active Tasks\nNo active tasks");
61
+ }
62
+
63
+ return sections.join("\n\n");
64
+ }
@@ -0,0 +1,67 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { resolvePath } from "../helpers.js";
4
+
5
+ export async function resolveProject(cwd, vaultPath) {
6
+ try {
7
+ await fs.access(vaultPath);
8
+ } catch (e) {
9
+ if (e.code === "ENOENT") {
10
+ return { error: `VAULT_PATH does not exist: ${vaultPath}` };
11
+ }
12
+ return { error: `Cannot access VAULT_PATH (${e.code}): ${vaultPath}` };
13
+ }
14
+
15
+ const projectsDir = path.join(vaultPath, "01-Projects");
16
+ const cwdBasename = path.basename(cwd).toLowerCase();
17
+
18
+ try {
19
+ const entries = await fs.readdir(projectsDir, { withFileTypes: true });
20
+ for (const entry of entries) {
21
+ if (entry.isDirectory() && entry.name.toLowerCase() === cwdBasename) {
22
+ return { projectPath: `01-Projects/${entry.name}` };
23
+ }
24
+ }
25
+ } catch (e) {
26
+ if (e.code !== "ENOENT") {
27
+ return { error: `Error reading 01-Projects/: ${e.message}` };
28
+ }
29
+ // 01-Projects/ doesn't exist -- fall through to CLAUDE.md check
30
+ }
31
+
32
+ try {
33
+ const claudeMd = await fs.readFile(path.join(cwd, "CLAUDE.md"), "utf-8");
34
+ const match = claudeMd.match(/^#\s+PKM:\s*(.+)$/m);
35
+ if (match) {
36
+ const annotatedPath = match[1].trim();
37
+ try {
38
+ resolvePath(annotatedPath, vaultPath);
39
+ } catch (e) {
40
+ if (e.message === "Path escapes vault directory") {
41
+ return { error: `CLAUDE.md annotation escapes vault directory: ${annotatedPath}` };
42
+ }
43
+ throw e;
44
+ }
45
+ try {
46
+ await fs.access(path.join(vaultPath, annotatedPath));
47
+ return { projectPath: annotatedPath };
48
+ } catch (e) {
49
+ if (e.code === "ENOENT") {
50
+ return { error: `CLAUDE.md annotation points to non-existent vault path: ${annotatedPath}` };
51
+ }
52
+ return { error: `Cannot access annotated vault path (${e.code}): ${annotatedPath}` };
53
+ }
54
+ }
55
+ } catch (e) {
56
+ if (e.code !== "ENOENT" && e.code !== "EACCES") {
57
+ return { error: `Error reading CLAUDE.md: ${e.message}` };
58
+ }
59
+ // No CLAUDE.md or not readable -- fall through
60
+ }
61
+
62
+ return {
63
+ error: `No vault project found for "${path.basename(cwd)}". ` +
64
+ `To fix: ensure your project folder name matches the repo name in 01-Projects/, ` +
65
+ `or add "# PKM: 01-Projects/YourProject" to your project's CLAUDE.md.`
66
+ };
67
+ }
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolveProject } from "./resolve-project.js";
4
+ import { loadProjectContext } from "./load-context.js";
5
+
6
+ const VAULT_PATH = process.env.VAULT_PATH;
7
+
8
+ async function main() {
9
+ let inputJson = "";
10
+ for await (const chunk of process.stdin) {
11
+ inputJson += chunk;
12
+ }
13
+
14
+ let input;
15
+ try {
16
+ input = JSON.parse(inputJson);
17
+ } catch {
18
+ const output = {
19
+ hookSpecificOutput: {
20
+ hookEventName: "SessionStart",
21
+ additionalContext: "PKM hook error: could not parse hook input JSON."
22
+ }
23
+ };
24
+ console.log(JSON.stringify(output));
25
+ process.exit(0);
26
+ }
27
+
28
+ const { cwd } = input;
29
+
30
+ if (!cwd || typeof cwd !== "string") {
31
+ const output = {
32
+ hookSpecificOutput: {
33
+ hookEventName: "SessionStart",
34
+ additionalContext: "PKM hook error: hook input missing 'cwd' field."
35
+ }
36
+ };
37
+ console.log(JSON.stringify(output));
38
+ process.exit(0);
39
+ }
40
+
41
+ if (!VAULT_PATH) {
42
+ const output = {
43
+ hookSpecificOutput: {
44
+ hookEventName: "SessionStart",
45
+ additionalContext: "PKM hook warning: VAULT_PATH environment variable not set. Vault context unavailable."
46
+ }
47
+ };
48
+ console.log(JSON.stringify(output));
49
+ process.exit(0);
50
+ }
51
+
52
+ const { projectPath, error } = await resolveProject(cwd, VAULT_PATH);
53
+
54
+ if (error) {
55
+ const output = {
56
+ hookSpecificOutput: {
57
+ hookEventName: "SessionStart",
58
+ additionalContext: `PKM: ${error}`
59
+ }
60
+ };
61
+ console.log(JSON.stringify(output));
62
+ process.exit(0);
63
+ }
64
+
65
+ let context;
66
+ try {
67
+ context = await loadProjectContext(VAULT_PATH, projectPath);
68
+ } catch (e) {
69
+ const output = {
70
+ hookSpecificOutput: {
71
+ hookEventName: "SessionStart",
72
+ additionalContext: `PKM hook error: failed to load project context: ${e.message}`
73
+ }
74
+ };
75
+ console.log(JSON.stringify(output));
76
+ process.exit(0);
77
+ }
78
+
79
+ const output = {
80
+ hookSpecificOutput: {
81
+ hookEventName: "SessionStart",
82
+ additionalContext: context
83
+ }
84
+ };
85
+ console.log(JSON.stringify(output));
86
+ }
87
+
88
+ main().catch((err) => {
89
+ console.error(`PKM SessionStart hook error: ${err.message}`);
90
+ process.exit(1);
91
+ });
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # Stop hook: passive PKM capture sweep
3
+ # Runs async after each Claude response. Spawns claude -p with Haiku
4
+ # to analyze the transcript and capture decisions/tasks/findings.
5
+
6
+ set -euo pipefail
7
+
8
+ LOG_DIR="${VAULT_PATH:?}/.obsidian/hook-logs"
9
+ mkdir -p "$LOG_DIR"
10
+ trap 'echo "stop-sweep: failed at line $LINENO" >> "$LOG_DIR/sweep-errors.log"' ERR
11
+
12
+ # Read hook input from stdin
13
+ INPUT=$(cat)
14
+
15
+ eval "$(echo "$INPUT" | node -e "
16
+ let b='';
17
+ process.stdin.on('data',c=>b+=c);
18
+ process.stdin.on('end',()=>{
19
+ const j=JSON.parse(b);
20
+ console.log('TRANSCRIPT_PATH='+JSON.stringify(j.transcript_path||''));
21
+ console.log('SESSION_ID='+JSON.stringify(j.session_id||''));
22
+ console.log('CWD='+JSON.stringify(j.cwd||''));
23
+ })
24
+ ")"
25
+
26
+ # Skip if no transcript
27
+ if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
28
+ echo "stop-sweep: skipping - transcript_path empty or missing ('$TRANSCRIPT_PATH')" >&2
29
+ exit 0
30
+ fi
31
+
32
+ # MCP config for obsidian-pkm server
33
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd -P)
34
+ MCP_CONFIG=$(node -e "console.log(JSON.stringify({mcpServers:{'obsidian-pkm':{command:'node',args:[process.argv[1]],env:{VAULT_PATH:process.argv[2]}}}}))" "$SCRIPT_DIR/../index.js" "${VAULT_PATH:-$HOME/Documents/PKM}")
35
+
36
+ PROJECT_NAME=$(basename "$CWD")
37
+ SESSION_SHORT=$(echo "$SESSION_ID" | cut -c1-8)
38
+ TODAY=$(date +%Y-%m-%d)
39
+ NOW=$(date +%H:%M)
40
+
41
+ # Build the prompt
42
+ PROMPT="You are a PKM passive capture agent. Your job is to identify decisions, task changes, and research findings from the most recent conversation exchange and append them to the vault staging inbox.
43
+
44
+ ## Transcript
45
+
46
+ Read the file at: $TRANSCRIPT_PATH
47
+
48
+ Find the LAST user message and LAST assistant message at the end of the file — these are the current exchange. One exchange = one contiguous user message + one contiguous assistant response (the last complete turn pair). All prior messages are historical context only. Do NOT capture anything from prior messages.
49
+
50
+ ## What to Capture
51
+
52
+ 1. **Decisions**: Technical or architectural choices that were AGREED upon (not proposed, not still being discussed)
53
+ 2. **Task changes**: New tasks identified, tasks completed, priority changes, blockers discovered
54
+ 3. **Research findings**: Patterns discovered, library behaviors documented, gotchas found
55
+
56
+ ## Noise Suppression
57
+
58
+ Be conservative. Skip: trivial exchanges, clarification Q&A that hasn't resolved, implementation details obvious from code, anything restating existing project context. When in doubt, don't capture.
59
+
60
+ ## Deduplication
61
+
62
+ If the assistant message in the current exchange contains a vault_capture tool call, the content of that capture is already being handled by the explicit capture agent. Do NOT also capture it in the staging inbox — skip it to avoid semantic duplication.
63
+
64
+ ## Output
65
+
66
+ If you find PKM-worthy content, use vault_append to add entries to 00-Inbox/captures-${TODAY}.md. If the file doesn't exist, create it first with vault_write (template: fleeting-note, tags: [capture, auto]). Each entry format:
67
+
68
+ ## ${NOW} — {Category}: {Title}
69
+
70
+ {1-3 sentence description}
71
+
72
+ **Source:** ${PROJECT_NAME}, session ${SESSION_SHORT}
73
+
74
+ ---
75
+
76
+ If nothing is PKM-worthy, do nothing."
77
+
78
+ # Spawn claude -p in background with logging
79
+ echo "$PROMPT" | nohup claude -p --model haiku --mcp-config "$MCP_CONFIG" --max-turns 5 --allowedTools "mcp__obsidian-pkm__vault_write mcp__obsidian-pkm__vault_append mcp__obsidian-pkm__vault_read" >> "$LOG_DIR/sweep-$(date +%Y%m%d-%H%M%S).log" 2>&1 &
80
+
81
+ exit 0
package/index.js CHANGED
@@ -344,6 +344,40 @@ Pass custom <%...%> variables via the 'variables' parameter.`,
344
344
  },
345
345
  required: ["old_path", "new_path"]
346
346
  }
347
+ },
348
+ {
349
+ name: "vault_capture",
350
+ description: "Signal that something is worth capturing in the PKM vault. " +
351
+ "Returns immediately — a background agent handles the actual note creation. " +
352
+ "Use this when you identify a decision, task, or research finding worth preserving.",
353
+ inputSchema: {
354
+ type: "object",
355
+ properties: {
356
+ type: {
357
+ type: "string",
358
+ enum: ["adr", "task", "research", "bug"],
359
+ description: "The type of capture: adr (decision), task, research (finding/pattern), bug (issue/fix)"
360
+ },
361
+ title: {
362
+ type: "string",
363
+ description: "Brief descriptive title (e.g., 'Use sqlite-vec over Chroma')"
364
+ },
365
+ content: {
366
+ type: "string",
367
+ description: "The substance of the capture — context, rationale, details. 1-5 sentences."
368
+ },
369
+ priority: {
370
+ type: "string",
371
+ enum: ["low", "normal", "high", "urgent"],
372
+ description: "Priority level (tasks only, default: normal)"
373
+ },
374
+ project: {
375
+ type: "string",
376
+ description: "Project name for vault routing (e.g., 'Obsidian-MCP'). If omitted, inferred from session context."
377
+ }
378
+ },
379
+ required: ["type", "title", "content"]
380
+ }
347
381
  }
348
382
  ];
349
383
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pkm-mcp-server",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "MCP server for Obsidian vault integration with Claude Code — 18 tools for notes, search, and graph traversal",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -17,6 +17,7 @@
17
17
  "*.js",
18
18
  "!eslint.config.js",
19
19
  "CHANGELOG.md",
20
+ "hooks/",
20
21
  "templates/",
21
22
  "sample-project/"
22
23
  ],
@@ -54,7 +55,7 @@
54
55
  "@modelcontextprotocol/sdk": "^1.0.0",
55
56
  "better-sqlite3": "^12.6.2",
56
57
  "js-yaml": "^4.1.0",
57
- "sqlite-vec": "0.1.7-alpha.2"
58
+ "sqlite-vec": "^0.1.7"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@eslint/js": "^10.0.1",