persyst-mcp 2.1.0 → 2.1.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/bin/extract-worker.js +387 -0
- package/bin/extract.js +185 -0
- package/bin/ingest.js +82 -0
- package/bin/init.js +174 -0
- package/bin/setup.js +9 -4
- package/hooks/persyst-hook.js +195 -10
- package/index.js +20 -0
- package/package.json +9 -3
- package/src/database.js +84 -16
- package/src/extractor-heuristic.js +250 -0
- package/src/search.js +31 -10
- package/src/server.js +1 -1
- package/src/tools.js +40 -26
package/bin/init.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* persyst-init — Workspace rules generator for VS Code-based IDEs (Cursor, Windsurf, Antigravity)
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx persyst-mcp init
|
|
8
|
+
*
|
|
9
|
+
* What it does:
|
|
10
|
+
* 1. Safely creates or appends system instructions to `.cursorrules`
|
|
11
|
+
* 2. Safely creates or appends system instructions to `.windsurfrules`
|
|
12
|
+
* 3. Creates a general `.persystrules.md` copy-pasteable guide
|
|
13
|
+
* 4. Prints instructions on configuring MCP servers in Cursor/VS Code/Antigravity
|
|
14
|
+
*
|
|
15
|
+
* Design:
|
|
16
|
+
* - Non-destructive: checks for existing content before appending to avoid duplication
|
|
17
|
+
* - Idempotent: safe to run multiple times
|
|
18
|
+
* - Localized: targets the current working directory (project root)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
|
|
22
|
+
import { join, resolve, dirname } from 'path';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = dirname(__filename);
|
|
27
|
+
|
|
28
|
+
// ============================================================
|
|
29
|
+
// SYSTEM INSTRUCTION CONTENT
|
|
30
|
+
// ============================================================
|
|
31
|
+
|
|
32
|
+
const INSTRUCTION_HEADER = '# Persyst Memory Integration';
|
|
33
|
+
|
|
34
|
+
const RULE_CONTENT = `
|
|
35
|
+
${INSTRUCTION_HEADER}
|
|
36
|
+
You are integrated with Persyst, a local-first MCP memory server that stores user preferences, project guidelines, context, and decisions.
|
|
37
|
+
|
|
38
|
+
## Proactive Memory Retrieval (CRITICAL)
|
|
39
|
+
- Before responding to the user's prompt or executing any task, you MUST invoke the \`persyst\` MCP server's \`search_memories\` or \`get_optimized_context\` tool.
|
|
40
|
+
- Extract relevant search terms from the user's prompt (e.g. if the user says "update the database schema", query "database", "schema", "sqlite", "table").
|
|
41
|
+
- Provide your agent name (e.g. \`cursor-agent\`, \`roo-code\`) as the \`agent_id\` parameter when searching to query your private namespace + shared project context.
|
|
42
|
+
|
|
43
|
+
## Proactive Memory Storage (CRITICAL)
|
|
44
|
+
- Record Milestones: When you successfully implement a feature, fix a bug, or make an architectural decision, call the \`add_memory\` tool to store a summary of the change.
|
|
45
|
+
- Agentic Swarms & Namespaces: If you are part of a multi-agent swarm or need private partition, pass your agent name as \`agent_id\` and set \`shared: false\` to store private memories. For general project guidelines and files, leave \`shared: true\` (default) so other agents can access them.
|
|
46
|
+
- Handle Contradictions: Persyst handles contradiction detection automatically. If a new fact contradicts an old memory, Persyst will flag it.
|
|
47
|
+
- Quality Over Quantity: Do NOT store trivial facts, temporary conversation noise, or duplicate data. "Bad data is worse than no data". Only store long-term architecture decisions, project details, and explicit user preferences.
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const GENERAL_GUIDE = `# Persyst General Agent Integration Guide
|
|
51
|
+
|
|
52
|
+
This workspace is configured with the Persyst local-first memory server.
|
|
53
|
+
|
|
54
|
+
## How to Configure the MCP Server in VS Code / Cursor / Antigravity
|
|
55
|
+
|
|
56
|
+
Add the following configuration to your IDE's MCP Server settings:
|
|
57
|
+
|
|
58
|
+
- **Server Name:** \`persyst\`
|
|
59
|
+
- **Type:** \`command\`
|
|
60
|
+
- **Command:** \`npx\`
|
|
61
|
+
- **Arguments:** \`["-y", "persyst-mcp"]\`
|
|
62
|
+
|
|
63
|
+
Alternatively, if you have installed the package globally (\`npm install -g persyst-mcp\`), you can configure:
|
|
64
|
+
- **Command:** \`persyst-mcp\`
|
|
65
|
+
- **Arguments:** \`[]\`
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Copy-Paste System Prompt Instructions
|
|
70
|
+
If your agent does not read \`.cursorrules\` or \`.windsurfrules\` natively, copy and paste the following prompt into the agent's Custom Instructions, System Prompt, or System Rules:
|
|
71
|
+
|
|
72
|
+
\`\`\`markdown
|
|
73
|
+
${RULE_CONTENT.trim()}
|
|
74
|
+
\`\`\`
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
// ============================================================
|
|
78
|
+
// HELPERS
|
|
79
|
+
// ============================================================
|
|
80
|
+
|
|
81
|
+
function setupRuleFile(filePath, fileName) {
|
|
82
|
+
let content = RULE_CONTENT;
|
|
83
|
+
let action = 'Created';
|
|
84
|
+
|
|
85
|
+
if (existsSync(filePath)) {
|
|
86
|
+
const existing = readFileSync(filePath, 'utf8');
|
|
87
|
+
if (existing.includes(INSTRUCTION_HEADER)) {
|
|
88
|
+
console.log(` ℹ️ ${fileName} already has Persyst rules configured (skipped).`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
content = existing + '\n' + RULE_CONTENT;
|
|
92
|
+
action = 'Appended to';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
writeFileSync(filePath, content.trim() + '\n', 'utf8');
|
|
96
|
+
console.log(` ✅ ${action} ${fileName}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================
|
|
100
|
+
// MAIN
|
|
101
|
+
// ============================================================
|
|
102
|
+
|
|
103
|
+
function run() {
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(' 🧠 Persyst — Workspace Rules Setup');
|
|
106
|
+
console.log(' ════════════════════════════════════');
|
|
107
|
+
console.log('');
|
|
108
|
+
|
|
109
|
+
const cwd = process.cwd();
|
|
110
|
+
console.log(` 📁 Target workspace: ${cwd}`);
|
|
111
|
+
console.log('');
|
|
112
|
+
|
|
113
|
+
// 1. Create/Append Cursor Rules
|
|
114
|
+
const cursorRulesPath = join(cwd, '.cursorrules');
|
|
115
|
+
setupRuleFile(cursorRulesPath, '.cursorrules');
|
|
116
|
+
|
|
117
|
+
// 2. Create/Append Windsurf Rules
|
|
118
|
+
const windsurfRulesPath = join(cwd, '.windsurfrules');
|
|
119
|
+
setupRuleFile(windsurfRulesPath, '.windsurfrules');
|
|
120
|
+
|
|
121
|
+
// 3. Create General Guide File
|
|
122
|
+
const generalGuidePath = join(cwd, '.persystrules.md');
|
|
123
|
+
writeFileSync(generalGuidePath, GENERAL_GUIDE.trim() + '\n', 'utf8');
|
|
124
|
+
console.log(' ✅ Created .persystrules.md (General Guide)');
|
|
125
|
+
|
|
126
|
+
// 4. Configure Git post-commit hook for automatic commit ingestion
|
|
127
|
+
const gitDir = join(cwd, '.git');
|
|
128
|
+
if (existsSync(gitDir)) {
|
|
129
|
+
const hooksDir = join(gitDir, 'hooks');
|
|
130
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
131
|
+
const postCommitPath = join(hooksDir, 'post-commit');
|
|
132
|
+
const localPersystPath = resolve(__dirname, '..', 'index.js').replace(/\\/g, '/');
|
|
133
|
+
|
|
134
|
+
const hookContent = `#!/bin/sh
|
|
135
|
+
# Persyst Git Commit Ingestion Hook
|
|
136
|
+
# Automatically ingests recent commits into Persyst memory on every commit.
|
|
137
|
+
|
|
138
|
+
# Local project path fallback for development
|
|
139
|
+
LOCAL_PERSYST="${localPersystPath}"
|
|
140
|
+
|
|
141
|
+
if [ -f "$LOCAL_PERSYST" ]; then
|
|
142
|
+
node "$LOCAL_PERSYST" ingest "$PWD" 5 >/dev/null 2>&1 || true
|
|
143
|
+
else
|
|
144
|
+
npx persyst-mcp ingest "$PWD" 5 >/dev/null 2>&1 || true
|
|
145
|
+
fi
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
writeFileSync(postCommitPath, hookContent, { mode: 0o755 });
|
|
149
|
+
try {
|
|
150
|
+
chmodSync(postCommitPath, 0o755);
|
|
151
|
+
} catch (_) {}
|
|
152
|
+
console.log(' ✅ Configured Git post-commit hook for auto-ingestion');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 5. Print Success & Configuration Help
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(' ════════════════════════════════════');
|
|
158
|
+
console.log(' ✅ Rules and Git hooks initialization complete!');
|
|
159
|
+
console.log('');
|
|
160
|
+
console.log(' To connect the memory server to Cursor, Antigravity, or VS Code:');
|
|
161
|
+
console.log(' 1. Open your IDE Settings -> MCP (Model Context Protocol).');
|
|
162
|
+
console.log(' 2. Add a new command server:');
|
|
163
|
+
console.log(' • Name: persyst');
|
|
164
|
+
console.log(' • Command: npx');
|
|
165
|
+
console.log(' • Arguments: -y persyst-mcp');
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log(' The rules we generated will guide the AI agents in this workspace to:');
|
|
168
|
+
console.log(' • Proactively search memory before answering prompts.');
|
|
169
|
+
console.log(' • Log milestone achievements and user preferences.');
|
|
170
|
+
console.log(' • Keep the memory clean ("no bad data").');
|
|
171
|
+
console.log('');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
run();
|
package/bin/setup.js
CHANGED
|
@@ -130,11 +130,16 @@ function run() {
|
|
|
130
130
|
process.exit(1);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
// Step 2: Copy hook file to ~/.persyst/hooks/
|
|
134
|
-
console.log(' 📁 Installing hook script...');
|
|
133
|
+
// Step 2: Copy and template hook file to ~/.persyst/hooks/
|
|
134
|
+
console.log(' 📁 Installing and templating hook script...');
|
|
135
135
|
ensureDir(PERSYST_HOOKS_DIR);
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
const INDEX_PATH = resolve(__dirname, '..', 'index.js');
|
|
137
|
+
const WORKER_PATH = resolve(__dirname, '..', 'bin', 'extract-worker.js');
|
|
138
|
+
let hookContent = readFileSync(HOOK_SOURCE, 'utf8');
|
|
139
|
+
hookContent = hookContent.replace('{{PERSYST_INDEX_PATH}}', INDEX_PATH.replace(/\\/g, '/'));
|
|
140
|
+
hookContent = hookContent.replace('{{PERSYST_WORKER_PATH}}', WORKER_PATH.replace(/\\/g, '/'));
|
|
141
|
+
writeFileSync(HOOK_DEST, hookContent, 'utf8');
|
|
142
|
+
console.log(` ✅ Copied & templated to ${HOOK_DEST}`);
|
|
138
143
|
|
|
139
144
|
// Step 3: Merge into ~/.claude/settings.json
|
|
140
145
|
console.log('');
|
package/hooks/persyst-hook.js
CHANGED
|
@@ -1,38 +1,66 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* persyst-hook.js — Claude Code Hook for Persyst Memory
|
|
4
|
+
* persyst-hook.js — Claude Code Hook for Persyst Memory (PAMP-Enhanced)
|
|
5
5
|
*
|
|
6
6
|
* Automatically injects relevant memories into Claude Code's context
|
|
7
|
-
* on SessionStart and UserPromptSubmit events
|
|
7
|
+
* on SessionStart and UserPromptSubmit events, and queues conversation
|
|
8
|
+
* turns for async background extraction on Stop events.
|
|
9
|
+
*
|
|
10
|
+
* PAMP Integration (Persyst Auto-Memory Pipeline):
|
|
11
|
+
* - Tier 1: Agent-explicit add_memory calls (existing, unchanged)
|
|
12
|
+
* - Tier 2: Heuristic regex extraction on UserPromptSubmit (sync, zero-cost)
|
|
13
|
+
* - Tier 3: Async LLM extraction via background worker (spawned on Stop)
|
|
8
14
|
*
|
|
9
15
|
* How it works:
|
|
10
|
-
* 1. Claude Code sends a JSON payload on stdin with hook_event_name, session_id,
|
|
16
|
+
* 1. Claude Code sends a JSON payload on stdin with hook_event_name, session_id, etc.
|
|
11
17
|
* 2. This script connects to the Persyst MCP server via StdioClientTransport.
|
|
12
18
|
* 3. It calls get_optimized_context or search_memories to retrieve relevant memories.
|
|
13
19
|
* 4. It returns a JSON response on stdout with additionalContext for Claude Code to inject.
|
|
20
|
+
* 5. On Stop: queues the conversation text for background LLM extraction.
|
|
14
21
|
*
|
|
15
22
|
* Installation:
|
|
16
23
|
* npx persyst-mcp setup
|
|
17
24
|
*
|
|
18
25
|
* Manual registration in ~/.claude/settings.json:
|
|
19
|
-
* { "hooks": { "SessionStart": [...], "UserPromptSubmit": [...] } }
|
|
26
|
+
* { "hooks": { "SessionStart": [...], "UserPromptSubmit": [...], "Stop": [...] } }
|
|
20
27
|
*/
|
|
21
28
|
|
|
22
29
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
23
30
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
24
31
|
import { fileURLToPath } from 'url';
|
|
25
|
-
import { dirname, resolve } from 'path';
|
|
32
|
+
import { dirname, resolve, join } from 'path';
|
|
33
|
+
import { spawn } from 'child_process';
|
|
34
|
+
import { writeFileSync, readdirSync, mkdirSync, existsSync } from 'fs';
|
|
35
|
+
import { homedir } from 'os';
|
|
26
36
|
|
|
27
37
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
38
|
const __dirname = dirname(__filename);
|
|
29
39
|
|
|
40
|
+
// ============================================================
|
|
41
|
+
// CONFIGURATION
|
|
42
|
+
// ============================================================
|
|
43
|
+
|
|
30
44
|
// Minimum prompt length to trigger memory search (skip "y", "ok", "/run", etc.)
|
|
31
45
|
const MIN_PROMPT_LENGTH = 15;
|
|
32
46
|
|
|
33
47
|
// Maximum time to wait for Persyst MCP connection (ms)
|
|
34
48
|
const CONNECTION_TIMEOUT = 8000;
|
|
35
49
|
|
|
50
|
+
// Hard timeout for the entire hook execution (ms)
|
|
51
|
+
// Claude Code will kill the hook if it exceeds this
|
|
52
|
+
const MAX_HOOK_LATENCY_MS = 500;
|
|
53
|
+
|
|
54
|
+
// Maximum active queue jobs before skipping worker spawn
|
|
55
|
+
const MAX_QUEUE_JOBS = 20;
|
|
56
|
+
|
|
57
|
+
// Queue directory for background extraction jobs
|
|
58
|
+
const QUEUE_DIR = join(homedir(), '.persyst', 'queue');
|
|
59
|
+
|
|
60
|
+
// ============================================================
|
|
61
|
+
// STDIN READER
|
|
62
|
+
// ============================================================
|
|
63
|
+
|
|
36
64
|
/**
|
|
37
65
|
* Read the full JSON payload from stdin.
|
|
38
66
|
* Claude Code sends the hook context as a single JSON object.
|
|
@@ -53,13 +81,20 @@ function readStdin() {
|
|
|
53
81
|
});
|
|
54
82
|
}
|
|
55
83
|
|
|
84
|
+
// ============================================================
|
|
85
|
+
// MCP CLIENT CONNECTION
|
|
86
|
+
// ============================================================
|
|
87
|
+
|
|
56
88
|
/**
|
|
57
89
|
* Connect to the Persyst MCP server as a client.
|
|
58
90
|
* Uses StdioClientTransport to spawn and communicate with the server.
|
|
59
91
|
*/
|
|
60
92
|
async function connectToPersyst() {
|
|
61
|
-
// Resolve the path to Persyst's index.js
|
|
62
|
-
|
|
93
|
+
// Resolve the path to Persyst's index.js
|
|
94
|
+
let persystPath = '{{PERSYST_INDEX_PATH}}';
|
|
95
|
+
if (persystPath.startsWith('{{')) {
|
|
96
|
+
persystPath = resolve(__dirname, '..', 'index.js');
|
|
97
|
+
}
|
|
63
98
|
|
|
64
99
|
const transport = new StdioClientTransport({
|
|
65
100
|
command: 'node',
|
|
@@ -93,6 +128,89 @@ async function callTool(client, toolName, args) {
|
|
|
93
128
|
return null;
|
|
94
129
|
}
|
|
95
130
|
|
|
131
|
+
// ============================================================
|
|
132
|
+
// PAMP: QUEUE MANAGEMENT
|
|
133
|
+
// ============================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Count active job files in the queue directory.
|
|
137
|
+
* Used for worker pool protection — don't spawn if overloaded.
|
|
138
|
+
* @returns {number}
|
|
139
|
+
*/
|
|
140
|
+
function countQueueJobs() {
|
|
141
|
+
try {
|
|
142
|
+
if (!existsSync(QUEUE_DIR)) return 0;
|
|
143
|
+
return readdirSync(QUEUE_DIR).filter(f => f.endsWith('.json')).length;
|
|
144
|
+
} catch (_) {
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Write a conversation turn to the extraction queue.
|
|
151
|
+
* @param {string} text - The conversation text to extract from
|
|
152
|
+
* @param {Object} meta - Metadata (session_id, agent_id, etc.)
|
|
153
|
+
*/
|
|
154
|
+
function enqueueJob(text, meta = {}) {
|
|
155
|
+
try {
|
|
156
|
+
mkdirSync(QUEUE_DIR, { recursive: true });
|
|
157
|
+
|
|
158
|
+
const jobId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
159
|
+
const jobFile = join(QUEUE_DIR, `${jobId}.json`);
|
|
160
|
+
|
|
161
|
+
writeFileSync(jobFile, JSON.stringify({
|
|
162
|
+
text,
|
|
163
|
+
session_id: meta.session_id || null,
|
|
164
|
+
agent_id: meta.agent_id || 'claude-code',
|
|
165
|
+
namespace: meta.namespace || 'shared',
|
|
166
|
+
cwd: meta.cwd || null,
|
|
167
|
+
queued_at: new Date().toISOString(),
|
|
168
|
+
_retries: 0
|
|
169
|
+
}, null, 2));
|
|
170
|
+
|
|
171
|
+
return jobId;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
// Non-critical — log and continue
|
|
174
|
+
process.stderr.write(`[persyst-hook] Queue write failed: ${err.message}\n`);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Spawn the background extraction worker as a detached process.
|
|
181
|
+
* The worker runs independently — hook doesn't wait for it.
|
|
182
|
+
*/
|
|
183
|
+
function spawnWorker() {
|
|
184
|
+
// Check queue depth first
|
|
185
|
+
const queueDepth = countQueueJobs();
|
|
186
|
+
if (queueDepth > MAX_QUEUE_JOBS) {
|
|
187
|
+
process.stderr.write(`[persyst-hook] Queue overloaded (${queueDepth} jobs), skipping worker spawn.\n`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
let workerPath = '{{PERSYST_WORKER_PATH}}';
|
|
193
|
+
if (workerPath.startsWith('{{')) {
|
|
194
|
+
workerPath = resolve(__dirname, '..', 'bin', 'extract-worker.js');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const child = spawn('node', [workerPath], {
|
|
198
|
+
detached: true,
|
|
199
|
+
stdio: 'ignore',
|
|
200
|
+
env: { ...process.env }
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Unref so the hook can exit without waiting for the worker
|
|
204
|
+
child.unref();
|
|
205
|
+
} catch (err) {
|
|
206
|
+
process.stderr.write(`[persyst-hook] Worker spawn failed: ${err.message}\n`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ============================================================
|
|
211
|
+
// EVENT HANDLERS
|
|
212
|
+
// ============================================================
|
|
213
|
+
|
|
96
214
|
/**
|
|
97
215
|
* Handle SessionStart: load project-wide context and ingest git history.
|
|
98
216
|
*/
|
|
@@ -152,6 +270,7 @@ async function handleSessionStart(client, input) {
|
|
|
152
270
|
|
|
153
271
|
/**
|
|
154
272
|
* Handle UserPromptSubmit: search for memories relevant to the user's prompt.
|
|
273
|
+
* Also runs Tier 2 heuristic extraction inline (zero-cost).
|
|
155
274
|
*/
|
|
156
275
|
async function handleUserPromptSubmit(client, input) {
|
|
157
276
|
const prompt = input.prompt || '';
|
|
@@ -161,6 +280,25 @@ async function handleUserPromptSubmit(client, input) {
|
|
|
161
280
|
return {};
|
|
162
281
|
}
|
|
163
282
|
|
|
283
|
+
// --- Tier 2: Run heuristic extraction inline (sync, zero-cost) ---
|
|
284
|
+
// We don't store results here — we queue them alongside the LLM job.
|
|
285
|
+
// This just detects if there's extractable signal in the prompt.
|
|
286
|
+
let heuristicFacts = [];
|
|
287
|
+
try {
|
|
288
|
+
const { extractHeuristic } = await import('../src/extractor-heuristic.js');
|
|
289
|
+
heuristicFacts = extractHeuristic(prompt);
|
|
290
|
+
} catch (_) {
|
|
291
|
+
// Heuristic module not available — Tier 3 will handle it
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Queue the prompt for Tier 3 background extraction (non-blocking)
|
|
295
|
+
enqueueJob(prompt, {
|
|
296
|
+
session_id: input.session_id,
|
|
297
|
+
agent_id: 'claude-code',
|
|
298
|
+
cwd: input.cwd
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// --- Memory Retrieval (existing behavior) ---
|
|
164
302
|
// Use search_memories for speed on per-prompt lookups (faster than get_optimized_context)
|
|
165
303
|
const searchResult = await callTool(client, 'search_memories', {
|
|
166
304
|
query: prompt.slice(0, 200), // Truncate very long prompts for search efficiency
|
|
@@ -178,6 +316,13 @@ async function handleUserPromptSubmit(client, input) {
|
|
|
178
316
|
for (const mem of searchResult.results) {
|
|
179
317
|
contextLines.push(`• [Memory #${mem.id}] ${mem.content}`);
|
|
180
318
|
}
|
|
319
|
+
|
|
320
|
+
// Add heuristic extraction notice if any facts were detected
|
|
321
|
+
if (heuristicFacts.length > 0) {
|
|
322
|
+
contextLines.push('');
|
|
323
|
+
contextLines.push(`[PAMP: ${heuristicFacts.length} fact signal(s) detected, queued for extraction]`);
|
|
324
|
+
}
|
|
325
|
+
|
|
181
326
|
contextLines.push('=== END MEMORY ===');
|
|
182
327
|
|
|
183
328
|
return {
|
|
@@ -189,8 +334,31 @@ async function handleUserPromptSubmit(client, input) {
|
|
|
189
334
|
}
|
|
190
335
|
|
|
191
336
|
/**
|
|
192
|
-
*
|
|
337
|
+
* Handle Stop: queue the final conversation turn for background extraction
|
|
338
|
+
* and spawn the worker to process the queue.
|
|
193
339
|
*/
|
|
340
|
+
async function handleStop(input) {
|
|
341
|
+
// The Stop event may include conversation_turns or transcript data
|
|
342
|
+
const transcript = input.transcript || input.conversation || '';
|
|
343
|
+
|
|
344
|
+
if (transcript && typeof transcript === 'string' && transcript.length > MIN_PROMPT_LENGTH) {
|
|
345
|
+
enqueueJob(transcript, {
|
|
346
|
+
session_id: input.session_id,
|
|
347
|
+
agent_id: 'claude-code',
|
|
348
|
+
cwd: input.cwd
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Spawn background worker to process all queued jobs
|
|
353
|
+
spawnWorker();
|
|
354
|
+
|
|
355
|
+
return {};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ============================================================
|
|
359
|
+
// MAIN ENTRY POINT
|
|
360
|
+
// ============================================================
|
|
361
|
+
|
|
194
362
|
async function main() {
|
|
195
363
|
let client = null;
|
|
196
364
|
|
|
@@ -198,20 +366,37 @@ async function main() {
|
|
|
198
366
|
const input = await readStdin();
|
|
199
367
|
const eventName = input.hook_event_name;
|
|
200
368
|
|
|
369
|
+
// Handle Stop event without MCP connection (just queue + spawn)
|
|
370
|
+
if (eventName === 'Stop') {
|
|
371
|
+
const response = await handleStop(input);
|
|
372
|
+
console.log(JSON.stringify(response));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
201
376
|
// Only handle events we care about
|
|
202
377
|
if (eventName !== 'SessionStart' && eventName !== 'UserPromptSubmit') {
|
|
203
378
|
console.log(JSON.stringify({}));
|
|
204
379
|
return;
|
|
205
380
|
}
|
|
206
381
|
|
|
207
|
-
// Connect to Persyst
|
|
382
|
+
// Connect to Persyst with hard timeout
|
|
383
|
+
const hookStart = Date.now();
|
|
208
384
|
client = await connectToPersyst();
|
|
209
385
|
|
|
210
386
|
let response;
|
|
211
387
|
if (eventName === 'SessionStart') {
|
|
212
388
|
response = await handleSessionStart(client, input);
|
|
213
389
|
} else if (eventName === 'UserPromptSubmit') {
|
|
214
|
-
|
|
390
|
+
// Apply hard timeout for prompt-time hook execution
|
|
391
|
+
response = await Promise.race([
|
|
392
|
+
handleUserPromptSubmit(client, input),
|
|
393
|
+
new Promise((resolve) =>
|
|
394
|
+
setTimeout(() => {
|
|
395
|
+
process.stderr.write(`[persyst-hook] UserPromptSubmit hit ${MAX_HOOK_LATENCY_MS}ms timeout, returning partial.\n`);
|
|
396
|
+
resolve({});
|
|
397
|
+
}, MAX_HOOK_LATENCY_MS - (Date.now() - hookStart))
|
|
398
|
+
)
|
|
399
|
+
]);
|
|
215
400
|
} else {
|
|
216
401
|
response = {};
|
|
217
402
|
}
|
package/index.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* node index.js (direct — starts MCP server)
|
|
11
11
|
* npx persyst-mcp (via npm — starts MCP server)
|
|
12
12
|
* npx persyst-mcp setup (install Claude Code hooks)
|
|
13
|
+
* npx persyst-mcp init (initialize workspace rules & git hooks)
|
|
14
|
+
* npx persyst-mcp ingest (manually ingest git commits)
|
|
13
15
|
* persyst-mcp (if installed globally)
|
|
14
16
|
*/
|
|
15
17
|
|
|
@@ -19,6 +21,24 @@ const subcommand = process.argv[2];
|
|
|
19
21
|
if (subcommand === 'setup') {
|
|
20
22
|
// Delegate to the setup CLI
|
|
21
23
|
await import('./bin/setup.js');
|
|
24
|
+
} else if (subcommand === 'aider') {
|
|
25
|
+
// Shift 'aider' from process.argv so aider.js gets the correct arguments
|
|
26
|
+
process.argv.splice(2, 1);
|
|
27
|
+
await import('./bin/aider.js');
|
|
28
|
+
} else if (subcommand === 'init') {
|
|
29
|
+
// Delegate to the rules init CLI
|
|
30
|
+
await import('./bin/init.js');
|
|
31
|
+
} else if (subcommand === 'ingest') {
|
|
32
|
+
// Shift 'ingest' from process.argv so ingest.js gets the correct arguments
|
|
33
|
+
process.argv.splice(2, 1);
|
|
34
|
+
await import('./bin/ingest.js');
|
|
35
|
+
} else if (subcommand === 'extract') {
|
|
36
|
+
// Shift 'extract' from process.argv so extract.js gets the correct arguments
|
|
37
|
+
process.argv.splice(2, 1);
|
|
38
|
+
await import('./bin/extract.js');
|
|
39
|
+
} else if (subcommand === 'worker') {
|
|
40
|
+
// Run the background extraction worker directly
|
|
41
|
+
await import('./bin/extract-worker.js');
|
|
22
42
|
} else {
|
|
23
43
|
// Default: start the MCP server
|
|
24
44
|
const { startServer } = await import('./src/server.js');
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "persyst-mcp",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Local-first MCP memory server with hybrid keyword + semantic search for coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"persyst-mcp": "index.js",
|
|
9
9
|
"persyst-setup": "bin/setup.js",
|
|
10
|
-
"persyst-aider": "bin/aider.js"
|
|
10
|
+
"persyst-aider": "bin/aider.js",
|
|
11
|
+
"persyst-init": "bin/init.js",
|
|
12
|
+
"persyst-ingest": "bin/ingest.js",
|
|
13
|
+
"persyst-extract": "bin/extract.js",
|
|
14
|
+
"persyst-worker": "bin/extract-worker.js"
|
|
11
15
|
},
|
|
12
16
|
"engines": {
|
|
13
17
|
"node": ">=18.0.0"
|
|
@@ -23,7 +27,9 @@
|
|
|
23
27
|
"scripts": {
|
|
24
28
|
"start": "node index.js",
|
|
25
29
|
"test": "node test/smoke.js",
|
|
26
|
-
"test:heavy": "cross-env NODE_ENV=test node --test test/test_*.js"
|
|
30
|
+
"test:heavy": "cross-env NODE_ENV=test node --test test/test_*.js",
|
|
31
|
+
"worker": "node bin/extract-worker.js",
|
|
32
|
+
"extract": "node bin/extract.js"
|
|
27
33
|
},
|
|
28
34
|
"keywords": [
|
|
29
35
|
"mcp",
|