pkm-mcp-server 1.2.0 → 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,15 @@ 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
+
9
18
  ## [1.2.0] - 2026-03-17
10
19
 
11
20
  ### Added
@@ -73,7 +82,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
73
82
  - Atomic file creation in `vault_write` (`wx` flag) prevents race conditions
74
83
  - Error messages sanitized to prevent leaking absolute vault paths
75
84
 
76
- [Unreleased]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.2.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
77
87
  [1.2.0]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.1.0...v1.2.0
78
88
  [1.1.0]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.0.0...v1.1.0
79
89
  [1.0.0]: https://github.com/AdrianV101/Obsidian-MCP/releases/tag/v1.0.0
package/README.md CHANGED
@@ -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
  ```
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pkm-mcp-server",
3
- "version": "1.2.0",
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
  ],