pkm-mcp-server 1.4.0 → 1.4.2

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,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.4.2] - 2026-03-21
10
+
11
+ ### Fixed
12
+ - SessionStart hook crash — `load-context.js` imported `extractFrontmatter` and `extractTailSections` from parent-relative paths (`../utils.js`, `../helpers.js`) that don't exist when hooks are copied to `~/.claude/hooks/pkm/`. Inlined all three functions to make hooks fully self-contained.
13
+
14
+ ## [1.4.1] - 2026-03-21
15
+
16
+ ### Fixed
17
+ - SessionStart hook crash when installed via `init` — `resolve-project.js` imported `resolvePath` from `../helpers.js` which doesn't exist at `~/.claude/hooks/pkm/`. Inlined the path security check to remove the external dependency.
18
+
9
19
  ## [1.4.0] - 2026-03-21
10
20
 
11
21
  ### Added
package/hooks/README.md CHANGED
@@ -9,14 +9,14 @@ The hook system consists of three hooks:
9
9
  | Hook | Event | Purpose |
10
10
  |------|-------|---------|
11
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 |
12
+ | `stop-sweep.js` | Stop | PKM librarian: creates structured, graph-linked vault notes from the latest exchange |
13
13
  | `capture-handler.sh` | PostToolUse | Explicit capture: creates structured vault notes when `vault_capture` is called |
14
14
 
15
15
  ### How it works
16
16
 
17
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.
18
+ - **Stop** (`stop-sweep.js`): Runs asynchronously after each assistant response. Resolves the current working directory to a vault project (no project = no captures). Spawns a background `claude -p` (Sonnet, 15 turns) that reads the transcript, classifies PKM-worthy content from the latest exchange, and creates properly structured vault notes in the project directory with wikilinks to related notes.
19
+ - **PostToolUse** (`capture-handler.sh`): Runs asynchronously after `vault_capture` tool use. Spawns a background `claude -p` (Opus, 30 turns) that creates a properly structured vault note (ADR, task, research note, or troubleshooting log) from the capture payload with graph links.
20
20
 
21
21
  ## Setup
22
22
 
@@ -51,7 +51,7 @@ Add the following to your `~/.claude/settings.json`:
51
51
  "hooks": [
52
52
  {
53
53
  "type": "command",
54
- "command": "VAULT_PATH=\"/path/to/your/vault\" /path/to/Obsidian-MCP/hooks/stop-sweep.sh",
54
+ "command": "VAULT_PATH=\"/path/to/your/vault\" node /path/to/Obsidian-MCP/hooks/stop-sweep.js",
55
55
  "async": true,
56
56
  "timeout": 10
57
57
  }
@@ -85,19 +85,19 @@ The hook system uses `type: "command"` hooks (not `type: "agent"`). The shell sc
85
85
 
86
86
  - The hook script exits immediately, satisfying the timeout constraint
87
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)
88
+ - `--max-turns` limits how many tool calls the background process can make (15 for stop-sweep, 30 for capture-handler)
89
89
 
90
90
  ### MCP config resolution
91
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.
92
+ The hook scripts resolve their own location to construct the path to `index.js`. `stop-sweep.js` uses the ES module pattern (`import.meta.url` `fileURLToPath` → `path.dirname`), while `capture-handler.sh` uses the POSIX pattern (`cd "$(dirname "$0")" && pwd -P`). Both work regardless of 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
93
 
94
94
  ### Async behavior
95
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.
96
+ Both `stop-sweep.js` 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
97
 
98
98
  ### Noise suppression and deduplication
99
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.
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. Deduplication is item-level: if the exchange contains a `vault_capture` call, the sweep reads its arguments and skips that specific item, but still captures other PKM-worthy content. The sweep also runs `vault_search` before creating notes to catch duplicates from concurrent sweep instances.
101
101
 
102
102
  ## Troubleshooting
103
103
 
@@ -111,7 +111,7 @@ The stop-sweep hook is conservative by design. It only looks at the last exchang
111
111
 
112
112
  - Check that `VAULT_PATH` is set correctly and the directory exists
113
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`
114
+ - Test the script manually: `echo '{"cwd":"/tmp","transcript_path":"/tmp/test","session_id":"abc123"}' | VAULT_PATH="/path/to/vault" node ./hooks/stop-sweep.js`
115
115
 
116
116
  ### Background process not running
117
117
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
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.
3
+ # Runs async after vault_capture returns. Spawns claude -p with Opus
4
+ # to create a properly structured vault note with graph links.
5
5
 
6
6
  set -euo pipefail
7
7
 
@@ -37,7 +37,11 @@ fi
37
37
 
38
38
  # MCP config for obsidian-pkm server
39
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}")
40
+ MCP_CONFIG=$(node -e "
41
+ const env = { VAULT_PATH: process.argv[2] };
42
+ if (process.env.OPENAI_API_KEY) env.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
43
+ console.log(JSON.stringify({ mcpServers: { 'obsidian-pkm': { command: 'node', args: [process.argv[1]], env } } }));
44
+ " "$SCRIPT_DIR/../index.js" "${VAULT_PATH:-$HOME/Documents/PKM}")
41
45
 
42
46
  # Build prompt via Node.js to avoid shell injection from user content
43
47
  PROMPT_FILE=$(mktemp)
@@ -71,11 +75,15 @@ const prompt = \`You are a PKM note creation agent. Your job is NOT done until t
71
75
 
72
76
  4. Read the note one final time to confirm no placeholder text remains.
73
77
 
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.\`;
78
+ 5. Use vault_suggest_links with the note's content to find related notes. If the tool is available, add [[wikilinks]] to 2-3 of the most relevant suggestions in the note body using vault_edit.
79
+
80
+ 6. Use vault_search to verify no very similar note already exists. If a near-duplicate is found, consider appending to the existing note instead of creating a new one.
81
+
82
+ 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 6.\`;
75
83
  require('fs').writeFileSync(process.argv[2], prompt);
76
84
  " "$TOOL_INPUT" "$PROMPT_FILE"
77
85
 
78
86
  # 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 &
87
+ nohup claude -p --model opus --mcp-config "$MCP_CONFIG" --max-turns 30 --allowedTools "mcp__obsidian-pkm__vault_read mcp__obsidian-pkm__vault_peek mcp__obsidian-pkm__vault_write mcp__obsidian-pkm__vault_append mcp__obsidian-pkm__vault_edit mcp__obsidian-pkm__vault_update_frontmatter mcp__obsidian-pkm__vault_search mcp__obsidian-pkm__vault_semantic_search mcp__obsidian-pkm__vault_suggest_links mcp__obsidian-pkm__vault_list mcp__obsidian-pkm__vault_recent mcp__obsidian-pkm__vault_links mcp__obsidian-pkm__vault_neighborhood mcp__obsidian-pkm__vault_query mcp__obsidian-pkm__vault_tags mcp__obsidian-pkm__vault_activity mcp__obsidian-pkm__vault_move" < "$PROMPT_FILE" >> "$LOG_DIR/capture-$(date +%Y%m%d-%H%M%S).log" 2>&1 &
80
88
 
81
89
  exit 0
@@ -1,7 +1,62 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
- import { extractFrontmatter } from "../utils.js";
4
- import { extractTailSections } from "../helpers.js";
3
+
4
+ /**
5
+ * Extract YAML frontmatter as simple key-value pairs.
6
+ * Lightweight parser — only handles scalar values (sufficient for status/priority).
7
+ */
8
+ function extractFrontmatter(content) {
9
+ if (!content.startsWith("---")) return null;
10
+ const endIndex = content.indexOf("\n---", 3);
11
+ if (endIndex === -1) return null;
12
+ const yamlContent = content.slice(4, endIndex);
13
+ const result = {};
14
+ for (const line of yamlContent.split("\n")) {
15
+ const match = line.match(/^(\w+):\s*(.+)$/);
16
+ if (match) result[match[1]] = match[2].trim();
17
+ }
18
+ return result;
19
+ }
20
+
21
+ function parseHeadingLevel(line) {
22
+ const match = line.match(/^(#{1,6})\s/);
23
+ return match ? match[1].length : 0;
24
+ }
25
+
26
+ /**
27
+ * Extract the last N sections at a given heading level from markdown content.
28
+ */
29
+ function extractTailSections(content, n, level) {
30
+ let frontmatter = "";
31
+ let body = content;
32
+ if (content.startsWith("---")) {
33
+ const endIndex = content.indexOf("\n---", 3);
34
+ if (endIndex !== -1) {
35
+ frontmatter = content.slice(0, endIndex + 4);
36
+ body = content.slice(endIndex + 4);
37
+ }
38
+ }
39
+
40
+ const lines = body.split("\n");
41
+ const headingPositions = [];
42
+ let offset = 0;
43
+ for (const line of lines) {
44
+ if (parseHeadingLevel(line) === level) {
45
+ headingPositions.push(offset);
46
+ }
47
+ offset += line.length + 1;
48
+ }
49
+
50
+ if (headingPositions.length === 0) {
51
+ return content;
52
+ }
53
+
54
+ const startIdx = Math.max(0, headingPositions.length - n);
55
+ const sliceStart = headingPositions[startIdx];
56
+ const tail = body.slice(sliceStart);
57
+
58
+ return frontmatter + (frontmatter && !frontmatter.endsWith("\n") ? "\n" : "") + tail;
59
+ }
5
60
 
6
61
  export async function loadProjectContext(vaultPath, projectPath) {
7
62
  const projectName = path.basename(projectPath);
@@ -1,6 +1,12 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
- import { resolvePath } from "../helpers.js";
3
+
4
+ function assertPathWithinVault(relativePath, vaultPath) {
5
+ const resolved = path.resolve(vaultPath, relativePath);
6
+ if (resolved !== vaultPath && !resolved.startsWith(vaultPath + path.sep)) {
7
+ throw new Error("Path escapes vault directory");
8
+ }
9
+ }
4
10
 
5
11
  export async function resolveProject(cwd, vaultPath) {
6
12
  try {
@@ -35,7 +41,7 @@ export async function resolveProject(cwd, vaultPath) {
35
41
  if (match) {
36
42
  const annotatedPath = match[1].trim();
37
43
  try {
38
- resolvePath(annotatedPath, vaultPath);
44
+ assertPathWithinVault(annotatedPath, vaultPath);
39
45
  } catch (e) {
40
46
  if (e.message === "Path escapes vault directory") {
41
47
  return { error: `CLAUDE.md annotation escapes vault directory: ${annotatedPath}` };
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { appendFileSync, closeSync, mkdirSync, openSync } from "node:fs";
5
+ import { access } from "node:fs/promises";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { resolveProject } from "./resolve-project.js";
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const VAULT_PATH = process.env.VAULT_PATH;
12
+ const LOG_DIR = VAULT_PATH ? join(VAULT_PATH, ".obsidian", "hook-logs") : null;
13
+
14
+ function logError(message) {
15
+ if (!LOG_DIR) {
16
+ console.error(`stop-sweep: ${message}`);
17
+ return;
18
+ }
19
+ try {
20
+ mkdirSync(LOG_DIR, { recursive: true });
21
+ appendFileSync(join(LOG_DIR, "sweep-errors.log"), `${new Date().toISOString()} ${message}\n`);
22
+ } catch {
23
+ console.error(`stop-sweep: ${message}`);
24
+ }
25
+ }
26
+
27
+ async function main() {
28
+ // Read hook input from stdin
29
+ let inputJson = "";
30
+ for await (const chunk of process.stdin) {
31
+ inputJson += chunk;
32
+ }
33
+
34
+ let input;
35
+ try {
36
+ input = JSON.parse(inputJson);
37
+ } catch {
38
+ logError("could not parse hook input JSON");
39
+ return;
40
+ }
41
+
42
+ const { transcript_path, session_id, cwd } = input;
43
+
44
+ // Skip if no transcript
45
+ if (!transcript_path) return;
46
+ try {
47
+ await access(transcript_path);
48
+ } catch {
49
+ return; // transcript file missing — normal for non-conversation triggers
50
+ }
51
+
52
+ if (!VAULT_PATH) {
53
+ console.error("stop-sweep: VAULT_PATH not set");
54
+ return;
55
+ }
56
+
57
+ if (!cwd) {
58
+ logError("hook input missing 'cwd' field");
59
+ return;
60
+ }
61
+
62
+ // Resolve project — no project, no captures
63
+ const { projectPath, error } = await resolveProject(cwd, VAULT_PATH);
64
+ if (error || !projectPath) return;
65
+
66
+ // Build MCP config
67
+ const indexPath = join(__dirname, "..", "index.js");
68
+ const mcpEnv = { VAULT_PATH };
69
+ if (process.env.OPENAI_API_KEY) {
70
+ mcpEnv.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
71
+ }
72
+ const mcpConfig = JSON.stringify({
73
+ mcpServers: {
74
+ "obsidian-pkm": {
75
+ command: "node",
76
+ args: [indexPath],
77
+ env: mcpEnv,
78
+ },
79
+ },
80
+ });
81
+
82
+ const prompt = `You are a PKM librarian agent. Your job is to identify PKM-worthy content from the most recent conversation exchange and file it correctly in the vault.
83
+
84
+ ## Context
85
+
86
+ - Project: ${projectPath}
87
+ - Session: ${session_id}
88
+ - Transcript: ${transcript_path}
89
+
90
+ ## Step 1: Read the transcript
91
+
92
+ Read the file at ${transcript_path}. Find the LAST user message and LAST assistant response — this is the current exchange. All prior messages are historical context only. Do NOT capture anything from prior messages.
93
+
94
+ ## Step 2: Classify what's PKM-worthy
95
+
96
+ Be conservative. Most exchanges produce NOTHING worth capturing. Skip:
97
+ - Trivial Q&A, clarifications, implementation details obvious from code
98
+ - Anything restating existing project context
99
+ - Content already handled by a vault_capture call in the same exchange
100
+ (read the vault_capture arguments to see what type/title/content was captured;
101
+ skip that specific item, but still capture other PKM-worthy content)
102
+
103
+ Before creating any note, use vault_search to check if a very similar note already exists — another sweep instance may have captured the same content from a prior exchange in a rapid conversation.
104
+
105
+ If you find something worth capturing, classify it:
106
+
107
+ | Content Type | Action |
108
+ |---|---|
109
+ | New task identified | Create task note: vault_write(template: "task", path: "${projectPath}/tasks/{kebab-title}.md") |
110
+ | Task completed/status changed | Find existing task via vault_query, then vault_update_frontmatter to update status |
111
+ | Significant decision agreed upon | Create ADR: vault_write(template: "adr", path: "${projectPath}/development/decisions/ADR-NNN-{kebab-title}.md") — use vault_list on the decisions directory to find the next ADR number |
112
+ | Research finding / gotcha | Create research note: vault_write(template: "research-note", path: "${projectPath}/research/{kebab-title}.md") |
113
+ | Bug root cause documented | Create troubleshooting log: vault_write(template: "troubleshooting-log", path: "${projectPath}/development/debug/{kebab-title}.md") |
114
+ | Minor implementation detail | SKIP — it's in the code/commits |
115
+
116
+ ## Step 3: Create/update notes properly
117
+
118
+ For every note you create:
119
+ 1. Use vault_write with the correct template
120
+ 2. Use vault_edit to replace ALL template placeholders with real content
121
+ 3. Use vault_suggest_links to find related notes (skip gracefully if unavailable)
122
+ 4. Add [[wikilinks]] to the related notes in the body
123
+ 5. Verify the note has no placeholder text remaining
124
+
125
+ For task updates:
126
+ 1. Use vault_query to find the task by title/tags
127
+ 2. Use vault_update_frontmatter to update status/priority
128
+ 3. Optionally vault_append to add context about what changed
129
+
130
+ ## Step 4: Quality check
131
+
132
+ If you created any notes, read each one back to confirm:
133
+ - No template placeholders remain
134
+ - Wikilinks are present to related notes (if vault_suggest_links was available)
135
+ - Frontmatter is complete and valid
136
+
137
+ If nothing is PKM-worthy, do nothing. Doing nothing is the correct default.`;
138
+
139
+ // All vault tools except vault_trash and vault_capture
140
+ const allowedTools = [
141
+ "vault_read", "vault_peek", "vault_write", "vault_append", "vault_edit",
142
+ "vault_update_frontmatter", "vault_search", "vault_semantic_search",
143
+ "vault_suggest_links", "vault_list", "vault_recent", "vault_links",
144
+ "vault_neighborhood", "vault_query", "vault_tags", "vault_activity", "vault_move",
145
+ ].map(t => `mcp__obsidian-pkm__${t}`).join(" ");
146
+
147
+ // Ensure log directory exists
148
+ mkdirSync(LOG_DIR, { recursive: true });
149
+ const logFile = join(LOG_DIR, `sweep-${new Date().toISOString().replace(/[:.]/g, "").slice(0, 15)}.log`);
150
+ const fd = openSync(logFile, "a");
151
+
152
+ const child = spawn("claude", [
153
+ "-p",
154
+ "--model", "sonnet",
155
+ "--mcp-config", mcpConfig,
156
+ "--max-turns", "15",
157
+ "--allowedTools", allowedTools,
158
+ ], {
159
+ detached: true,
160
+ stdio: ["pipe", fd, fd],
161
+ });
162
+
163
+ child.on("error", (err) => {
164
+ logError(`spawn failed: ${err.message}`);
165
+ });
166
+
167
+ // Pipe prompt to stdin
168
+ child.stdin.write(prompt);
169
+ child.stdin.end();
170
+
171
+ closeSync(fd);
172
+
173
+ // Detach — let the background process run independently
174
+ child.unref();
175
+ }
176
+
177
+ main().catch((err) => {
178
+ logError(`uncaught: ${err.message}`);
179
+ process.exit(0);
180
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pkm-mcp-server",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "MCP server for Obsidian vault integration with Claude Code — 19 tools for notes, search, and graph traversal",
5
5
  "main": "cli.js",
6
6
  "exports": {
@@ -1,81 +0,0 @@
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