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 +30 -1
- package/README.md +65 -3
- package/handlers.js +16 -0
- package/hooks/README.md +125 -0
- package/hooks/capture-handler.sh +81 -0
- package/hooks/load-context.js +64 -0
- package/hooks/resolve-project.js +67 -0
- package/hooks/session-start.js +91 -0
- package/hooks/stop-sweep.sh +81 -0
- package/index.js +34 -0
- package/package.json +3 -2
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
|
|
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
|
}
|
package/hooks/README.md
ADDED
|
@@ -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.
|
|
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
|
|
58
|
+
"sqlite-vec": "^0.1.7"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@eslint/js": "^10.0.1",
|