pkm-mcp-server 1.2.0 → 1.3.0

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,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.3.0] - 2026-03-19
10
+
11
+ ### Added
12
+ - `pkm-mcp-server init` — interactive onboarding wizard that walks users through vault setup, template installation, folder structure, OpenAI API key configuration, and Claude Code registration in a single session
13
+ - `cli.js` — new CLI dispatcher with `init` and `--version` subcommands; existing MCP server behavior unchanged when run without arguments
14
+ - `templates/note.md` — minimal generic note template for users who prefer to define their own templates
15
+ - 36 unit tests for init wizard helpers
16
+
17
+ ### Changed
18
+ - `index.js` refactored to export `startServer()` for CLI dispatcher (backward compatible — `node index.js` still works)
19
+ - Entry point updated from `index.js` to `cli.js` in package.json (`bin`, `main`, `exports`)
20
+
21
+ ### Dependencies
22
+ - Added `@inquirer/prompts` for interactive CLI prompts
23
+
24
+ ## [1.2.1] - 2026-03-17
25
+
26
+ ### Added
27
+ - CI workflow for automated npm publishing via GitHub releases (OIDC trusted publishing, no tokens)
28
+
29
+ ### Fixed
30
+ - Hook setup instructions now included in main README Quick Start (previously only in `hooks/README.md`, invisible on npm)
31
+ - `hooks/` directory added to architecture file tree in README
32
+
9
33
  ## [1.2.0] - 2026-03-17
10
34
 
11
35
  ### Added
@@ -73,7 +97,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
73
97
  - Atomic file creation in `vault_write` (`wx` flag) prevents race conditions
74
98
  - Error messages sanitized to prevent leaking absolute vault paths
75
99
 
76
- [Unreleased]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.2.0...HEAD
100
+ [Unreleased]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.2.1...HEAD
101
+ [1.2.1]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.2.0...v1.2.1
77
102
  [1.2.0]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.1.0...v1.2.0
78
103
  [1.1.0]: https://github.com/AdrianV101/Obsidian-MCP/compare/v1.0.0...v1.1.0
79
104
  [1.0.0]: https://github.com/AdrianV101/Obsidian-MCP/releases/tag/v1.0.0
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Node.js >= 20](https://img.shields.io/badge/Node.js-%3E%3D20-green.svg)](https://nodejs.org/)
6
6
  [![CI](https://github.com/AdrianV101/Obsidian-MCP/actions/workflows/ci.yml/badge.svg)](https://github.com/AdrianV101/Obsidian-MCP/actions/workflows/ci.yml)
7
7
 
8
- An MCP (Model Context Protocol) server that gives Claude Code full read/write access to your Obsidian vault. 18 tools for note CRUD, full-text search, semantic search, graph traversal, metadata queries, and session activity tracking. Published on npm as [`pkm-mcp-server`](https://www.npmjs.com/package/pkm-mcp-server).
8
+ An MCP (Model Context Protocol) server that gives Claude Code full read/write access to your Obsidian vault. 19 tools for note CRUD, full-text search, semantic search, graph traversal, metadata queries, session activity tracking, and passive knowledge capture. Published on npm as [`pkm-mcp-server`](https://www.npmjs.com/package/pkm-mcp-server).
9
9
 
10
10
  ## Why
11
11
 
@@ -42,6 +42,7 @@ https://github.com/user-attachments/assets/58ad9c9b-d987-4728-89e7-33de20b73a38
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
44
  | `vault_update_frontmatter` | Atomic YAML frontmatter updates (set, create, remove fields; validates enum fields by note type) |
45
+ | `vault_capture` | Signal a PKM-worthy capture (decision, task, research, bug); returns immediately, background hook creates the note |
45
46
 
46
47
  ### Fuzzy Path Resolution
47
48
 
@@ -55,7 +56,7 @@ vault_read({ path: "devlog.md" })
55
56
  // Same result — .md extension is optional
56
57
 
57
58
  vault_links({ path: "alpha" })
58
- // Works on vault_links, vault_neighborhood, vault_suggest_links too
59
+ // Works on vault_peek, vault_links, vault_neighborhood, vault_suggest_links too
59
60
  ```
60
61
 
61
62
  Folder-scoped tools accept partial folder names:
@@ -148,6 +149,67 @@ Add your OpenAI API key to the env block:
148
149
 
149
150
  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
151
 
152
+ ### 4. Enable PKM Hooks (optional)
153
+
154
+ 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.
155
+
156
+ Add to your `~/.claude/settings.json` (alongside the `mcpServers` block):
157
+
158
+ ```json
159
+ {
160
+ "hooks": {
161
+ "SessionStart": [
162
+ {
163
+ "matcher": "startup|clear|compact",
164
+ "hooks": [
165
+ {
166
+ "type": "command",
167
+ "command": "VAULT_PATH=\"/path/to/your/vault\" node /path/to/Obsidian-MCP/hooks/session-start.js",
168
+ "timeout": 15,
169
+ "statusMessage": "Loading PKM project context..."
170
+ }
171
+ ]
172
+ }
173
+ ],
174
+ "Stop": [
175
+ {
176
+ "hooks": [
177
+ {
178
+ "type": "command",
179
+ "command": "VAULT_PATH=\"/path/to/your/vault\" /path/to/Obsidian-MCP/hooks/stop-sweep.sh",
180
+ "async": true,
181
+ "timeout": 10
182
+ }
183
+ ]
184
+ }
185
+ ],
186
+ "PostToolUse": [
187
+ {
188
+ "matcher": "mcp__obsidian-pkm__vault_capture",
189
+ "hooks": [
190
+ {
191
+ "type": "command",
192
+ "command": "VAULT_PATH=\"/path/to/your/vault\" /path/to/Obsidian-MCP/hooks/capture-handler.sh",
193
+ "async": true,
194
+ "timeout": 10
195
+ }
196
+ ]
197
+ }
198
+ ]
199
+ }
200
+ }
201
+ ```
202
+
203
+ 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).
204
+
205
+ | Hook | Event | What it does |
206
+ |------|-------|--------------|
207
+ | `session-start.js` | SessionStart | Loads project context (index, devlog, active tasks) at session start |
208
+ | `stop-sweep.sh` | Stop | Scans each exchange for PKM-worthy decisions/tasks, appends to daily captures |
209
+ | `capture-handler.sh` | PostToolUse | Creates structured vault notes when `vault_capture` is called |
210
+
211
+ See [hooks/README.md](hooks/README.md) for architecture details and troubleshooting.
212
+
151
213
  ## Vault Structure
152
214
 
153
215
  The server works with any Obsidian vault. The included templates assume this layout:
@@ -203,6 +265,7 @@ graph LR
203
265
  ├── embeddings.js # Semantic index (OpenAI embeddings, SQLite + sqlite-vec)
204
266
  ├── activity.js # Activity log (session tracking, SQLite)
205
267
  ├── utils.js # Shared utilities (frontmatter parsing, file listing)
268
+ ├── hooks/ # Claude Code hooks (session context, passive capture)
206
269
  ├── templates/ # Obsidian note templates
207
270
  └── sample-project/ # Sample CLAUDE.md for your repos
208
271
  ```
@@ -217,7 +280,9 @@ All paths passed to tools are relative to vault root. The server includes path s
217
280
 
218
281
  **Graph exploration** resolves `[[wikilinks]]` to file paths (handling aliases, headings, and ambiguous basenames), then does BFS traversal to return notes grouped by hop distance.
219
282
 
220
- **Activity logging** records every tool call with timestamps and session IDs, enabling Claude to recall what happened in previous conversations.
283
+ **Activity logging** records every tool call (except `vault_activity` itself) with timestamps and session IDs, enabling Claude to recall what happened in previous conversations.
284
+
285
+ **Passive capture** uses `vault_capture` to signal that something is worth persisting (a decision, task, research finding, or bug). The tool returns immediately — a PostToolUse hook spawns a background agent that creates the structured vault note. Combined with the Stop hook (which sweeps each session for un-captured decisions and tasks), this keeps the vault up to date without interrupting the coding flow.
221
286
 
222
287
  ## Troubleshooting
223
288
 
@@ -240,6 +305,8 @@ Check your Node version with `node -v`. The file watcher uses `fs.watch({ recurs
240
305
 
241
306
  Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, code style guidelines, and the pull request process before submitting changes.
242
307
 
308
+ See [CHANGELOG.md](CHANGELOG.md) for release history and [SECURITY.md](SECURITY.md) to report vulnerabilities.
309
+
243
310
  ## License
244
311
 
245
312
  MIT
package/cli.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from "module";
4
+
5
+ const subcommand = process.argv[2];
6
+
7
+ try {
8
+ if (subcommand === "init") {
9
+ const { runInit } = await import("./init.js");
10
+ await runInit();
11
+ } else if (subcommand === "--version" || subcommand === "-v") {
12
+ const require = createRequire(import.meta.url);
13
+ const { version } = require("./package.json");
14
+ console.log(`pkm-mcp-server v${version}`);
15
+ } else if (!subcommand) {
16
+ const { startServer } = await import("./index.js");
17
+ await startServer();
18
+ } else {
19
+ console.error(`Unknown command: ${subcommand}`);
20
+ console.error("Usage: pkm-mcp-server [init]");
21
+ process.exit(1);
22
+ }
23
+ } catch (e) {
24
+ console.error(`Fatal: ${e.message}`);
25
+ process.exit(1);
26
+ }
@@ -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
+ });