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 +26 -1
- package/README.md +70 -3
- package/cli.js +26 -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 +458 -451
- package/init.js +577 -0
- package/package.json +8 -6
- package/sample-project/CLAUDE.md +15 -0
- package/templates/note.md +7 -0
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.
|
|
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
|
[](https://nodejs.org/)
|
|
6
6
|
[](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.
|
|
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
|
+
}
|
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
|
+
});
|