persyst-mcp 2.0.0 → 2.1.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/bin/aider.js +204 -0
- package/bin/ingest.js +82 -0
- package/bin/init.js +173 -0
- package/bin/setup.js +168 -0
- package/hooks/persyst-hook.js +234 -0
- package/index.js +29 -7
- package/package.json +8 -2
- package/src/search.js +15 -2
- package/src/server.js +1 -1
package/bin/aider.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* persyst-aider — Aider wrapper with automatic Persyst memory injection
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx persyst-aider [aider-args...]
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* npx persyst-aider --model anthropic/claude-sonnet-4
|
|
11
|
+
* npx persyst-aider --model openai/gpt-4o --auto-commits
|
|
12
|
+
*
|
|
13
|
+
* How it works:
|
|
14
|
+
* 1. On startup: connects to Persyst, ingests recent git commits
|
|
15
|
+
* 2. Before each prompt: queries Persyst for relevant memories, prepends context
|
|
16
|
+
* 3. On exit: ingests any new git commits created during the session
|
|
17
|
+
*
|
|
18
|
+
* Design decisions:
|
|
19
|
+
* - Only enriches prompts > 15 chars (skip "y", "ok", "/run", etc.)
|
|
20
|
+
* - Does NOT parse Aider's output (too fragile with ANSI codes, streaming, etc.)
|
|
21
|
+
* - Passes all args directly to Aider — fully transparent proxy
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawn } from 'child_process';
|
|
25
|
+
import { createInterface } from 'readline';
|
|
26
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
27
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
28
|
+
import { fileURLToPath } from 'url';
|
|
29
|
+
import { dirname, resolve } from 'path';
|
|
30
|
+
|
|
31
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
32
|
+
const __dirname = dirname(__filename);
|
|
33
|
+
|
|
34
|
+
const MIN_PROMPT_LENGTH = 15;
|
|
35
|
+
const CWD = process.cwd();
|
|
36
|
+
|
|
37
|
+
// ============================================================
|
|
38
|
+
// MCP CLIENT
|
|
39
|
+
// ============================================================
|
|
40
|
+
|
|
41
|
+
let persystClient = null;
|
|
42
|
+
|
|
43
|
+
async function connectToPersyst() {
|
|
44
|
+
if (persystClient) return persystClient;
|
|
45
|
+
|
|
46
|
+
const persystPath = resolve(__dirname, '..', 'index.js');
|
|
47
|
+
|
|
48
|
+
const transport = new StdioClientTransport({
|
|
49
|
+
command: 'node',
|
|
50
|
+
args: [persystPath]
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
persystClient = new Client({
|
|
54
|
+
name: 'persyst-aider',
|
|
55
|
+
version: '1.0.0'
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await persystClient.connect(transport);
|
|
59
|
+
return persystClient;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function callTool(toolName, args) {
|
|
63
|
+
const client = await connectToPersyst();
|
|
64
|
+
const result = await client.callTool({ name: toolName, arguments: args });
|
|
65
|
+
if (result.content && result.content[0] && result.content[0].text) {
|
|
66
|
+
return JSON.parse(result.content[0].text);
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function closePersyst() {
|
|
72
|
+
if (persystClient) {
|
|
73
|
+
try { await persystClient.close(); } catch (_) {}
|
|
74
|
+
persystClient = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================================
|
|
79
|
+
// MEMORY FUNCTIONS
|
|
80
|
+
// ============================================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Ingest recent git commits from the current directory.
|
|
84
|
+
*/
|
|
85
|
+
async function ingestGitCommits() {
|
|
86
|
+
try {
|
|
87
|
+
const result = await callTool('ingest_git_commits', {
|
|
88
|
+
repo_path: CWD,
|
|
89
|
+
count: 15
|
|
90
|
+
});
|
|
91
|
+
if (result && result.added > 0) {
|
|
92
|
+
console.error(`[persyst] Ingested ${result.added} git commits into memory`);
|
|
93
|
+
}
|
|
94
|
+
} catch (_) {
|
|
95
|
+
// Not a git repo or Persyst unavailable — silent
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Search for memories relevant to the user's prompt.
|
|
101
|
+
* Returns a formatted context string or null.
|
|
102
|
+
*/
|
|
103
|
+
async function getMemoryContext(prompt) {
|
|
104
|
+
try {
|
|
105
|
+
const result = await callTool('search_memories', {
|
|
106
|
+
query: prompt.slice(0, 200),
|
|
107
|
+
limit: 5,
|
|
108
|
+
agent_id: 'aider'
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!result || !result.results || result.results.length === 0) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const lines = ['[Persyst Memory — auto-retrieved context]'];
|
|
116
|
+
for (const mem of result.results) {
|
|
117
|
+
lines.push(`• ${mem.content}`);
|
|
118
|
+
}
|
|
119
|
+
lines.push('[End Memory]');
|
|
120
|
+
lines.push('');
|
|
121
|
+
|
|
122
|
+
return lines.join('\n');
|
|
123
|
+
} catch (_) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================================
|
|
129
|
+
// MAIN
|
|
130
|
+
// ============================================================
|
|
131
|
+
|
|
132
|
+
async function main() {
|
|
133
|
+
const aiderArgs = process.argv.slice(2);
|
|
134
|
+
|
|
135
|
+
// Check if Aider is available
|
|
136
|
+
console.error('[persyst] Starting Aider with Persyst memory...');
|
|
137
|
+
|
|
138
|
+
// Step 1: Connect to Persyst and ingest git history
|
|
139
|
+
try {
|
|
140
|
+
await connectToPersyst();
|
|
141
|
+
console.error('[persyst] Connected to memory server ✓');
|
|
142
|
+
await ingestGitCommits();
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error(`[persyst] Warning: Could not connect to memory server: ${err.message}`);
|
|
145
|
+
console.error('[persyst] Aider will run without memory injection.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Step 2: Spawn Aider as a child process
|
|
149
|
+
const aider = spawn('aider', aiderArgs, {
|
|
150
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
151
|
+
shell: true,
|
|
152
|
+
cwd: CWD
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Step 3: Set up stdin interception
|
|
156
|
+
const rl = createInterface({
|
|
157
|
+
input: process.stdin,
|
|
158
|
+
terminal: false
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
rl.on('line', async (line) => {
|
|
162
|
+
const trimmed = line.trim();
|
|
163
|
+
|
|
164
|
+
// Only enrich prompts that are long enough to be real questions
|
|
165
|
+
if (trimmed.length >= MIN_PROMPT_LENGTH && persystClient) {
|
|
166
|
+
try {
|
|
167
|
+
const context = await getMemoryContext(trimmed);
|
|
168
|
+
if (context) {
|
|
169
|
+
// Prepend memory context to the prompt
|
|
170
|
+
aider.stdin.write(context);
|
|
171
|
+
console.error(`[persyst] Injected ${context.split('\n').length - 3} memories`);
|
|
172
|
+
}
|
|
173
|
+
} catch (_) {
|
|
174
|
+
// Memory injection failed — just pass through the original prompt
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Always forward the original line to Aider
|
|
179
|
+
aider.stdin.write(line + '\n');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Handle user Ctrl+C
|
|
183
|
+
process.on('SIGINT', () => {
|
|
184
|
+
aider.kill('SIGINT');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Step 4: On Aider exit, ingest any new commits and clean up
|
|
188
|
+
aider.on('close', async (code) => {
|
|
189
|
+
console.error('[persyst] Aider session ended. Indexing new commits...');
|
|
190
|
+
await ingestGitCommits();
|
|
191
|
+
await closePersyst();
|
|
192
|
+
process.exit(code || 0);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Handle stdin close (user closed terminal)
|
|
196
|
+
rl.on('close', () => {
|
|
197
|
+
aider.stdin.end();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
main().catch(err => {
|
|
202
|
+
console.error(`[persyst] Fatal error: ${err.message}`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
});
|
package/bin/ingest.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* persyst-ingest — Direct Git Commit Ingester
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx persyst-mcp ingest [repo_path] [count]
|
|
8
|
+
*
|
|
9
|
+
* This script runs directly without starting the MCP server, allowing
|
|
10
|
+
* git hooks or direct CLI commands to populate the memory database.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getRecentCommits } from '../src/git.js';
|
|
14
|
+
import {
|
|
15
|
+
insertMemory,
|
|
16
|
+
insertVector,
|
|
17
|
+
insertEntity,
|
|
18
|
+
insertEdge,
|
|
19
|
+
memoryExistsByHashPrefix
|
|
20
|
+
} from '../src/database.js';
|
|
21
|
+
import { generateEmbedding } from '../src/embeddings.js';
|
|
22
|
+
import { searchCache } from '../src/cache.js';
|
|
23
|
+
|
|
24
|
+
const repoPath = process.argv[2] || process.cwd();
|
|
25
|
+
const count = parseInt(process.argv[3], 10) || 10;
|
|
26
|
+
|
|
27
|
+
async function run() {
|
|
28
|
+
console.log(`[persyst] Ingesting git commits for: ${repoPath}`);
|
|
29
|
+
try {
|
|
30
|
+
const commits = await getRecentCommits(repoPath, count);
|
|
31
|
+
let added = 0;
|
|
32
|
+
let skipped = 0;
|
|
33
|
+
|
|
34
|
+
for (const commit of commits) {
|
|
35
|
+
const hashPrefix = commit.hash.slice(0, 7);
|
|
36
|
+
// Check if commit already exists in memories
|
|
37
|
+
if (memoryExistsByHashPrefix(`[${hashPrefix}]%`)) {
|
|
38
|
+
skipped++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Insert memory with git provenance
|
|
43
|
+
const id = insertMemory(commit.fullText, commit.importance, {
|
|
44
|
+
source_type: 'git',
|
|
45
|
+
source_id: commit.hash,
|
|
46
|
+
confidence: 0.8
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Generate embedding vector and store
|
|
50
|
+
const embedding = await generateEmbedding(commit.fullText);
|
|
51
|
+
insertVector(id, embedding);
|
|
52
|
+
|
|
53
|
+
// Link Author entity
|
|
54
|
+
const authorId = insertEntity(commit.author, 'person');
|
|
55
|
+
if (authorId) {
|
|
56
|
+
insertEdge(authorId, id, 'authored', 'entity', 'memory');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Link Files Touched
|
|
60
|
+
for (const file of commit.files) {
|
|
61
|
+
const fileId = insertEntity(file, 'file');
|
|
62
|
+
if (fileId) {
|
|
63
|
+
insertEdge(fileId, id, 'touches', 'entity', 'memory');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
added++;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (added > 0) {
|
|
71
|
+
searchCache.invalidate();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(`[persyst] Success: Ingested ${added} commits (${skipped} already existed)`);
|
|
75
|
+
process.exit(0);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(`[persyst] Ingestion failed: ${err.message}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
run();
|
package/bin/init.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
- This ensures you are aware of existing codebase architecture, constraints, preferences, or past decisions before writing code.
|
|
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
|
+
- Handle Contradictions: Persyst handles contradiction detection automatically. If a new fact contradicts an old memory, Persyst will flag it.
|
|
46
|
+
- 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.
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const GENERAL_GUIDE = `# Persyst General Agent Integration Guide
|
|
50
|
+
|
|
51
|
+
This workspace is configured with the Persyst local-first memory server.
|
|
52
|
+
|
|
53
|
+
## How to Configure the MCP Server in VS Code / Cursor / Antigravity
|
|
54
|
+
|
|
55
|
+
Add the following configuration to your IDE's MCP Server settings:
|
|
56
|
+
|
|
57
|
+
- **Server Name:** \`persyst\`
|
|
58
|
+
- **Type:** \`command\`
|
|
59
|
+
- **Command:** \`npx\`
|
|
60
|
+
- **Arguments:** \`["-y", "persyst-mcp"]\`
|
|
61
|
+
|
|
62
|
+
Alternatively, if you have installed the package globally (\`npm install -g persyst-mcp\`), you can configure:
|
|
63
|
+
- **Command:** \`persyst-mcp\`
|
|
64
|
+
- **Arguments:** \`[]\`
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Copy-Paste System Prompt Instructions
|
|
69
|
+
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:
|
|
70
|
+
|
|
71
|
+
\`\`\`markdown
|
|
72
|
+
${RULE_CONTENT.trim()}
|
|
73
|
+
\`\`\`
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
// ============================================================
|
|
77
|
+
// HELPERS
|
|
78
|
+
// ============================================================
|
|
79
|
+
|
|
80
|
+
function setupRuleFile(filePath, fileName) {
|
|
81
|
+
let content = RULE_CONTENT;
|
|
82
|
+
let action = 'Created';
|
|
83
|
+
|
|
84
|
+
if (existsSync(filePath)) {
|
|
85
|
+
const existing = readFileSync(filePath, 'utf8');
|
|
86
|
+
if (existing.includes(INSTRUCTION_HEADER)) {
|
|
87
|
+
console.log(` ℹ️ ${fileName} already has Persyst rules configured (skipped).`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
content = existing + '\n' + RULE_CONTENT;
|
|
91
|
+
action = 'Appended to';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
writeFileSync(filePath, content.trim() + '\n', 'utf8');
|
|
95
|
+
console.log(` ✅ ${action} ${fileName}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================
|
|
99
|
+
// MAIN
|
|
100
|
+
// ============================================================
|
|
101
|
+
|
|
102
|
+
function run() {
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log(' 🧠 Persyst — Workspace Rules Setup');
|
|
105
|
+
console.log(' ════════════════════════════════════');
|
|
106
|
+
console.log('');
|
|
107
|
+
|
|
108
|
+
const cwd = process.cwd();
|
|
109
|
+
console.log(` 📁 Target workspace: ${cwd}`);
|
|
110
|
+
console.log('');
|
|
111
|
+
|
|
112
|
+
// 1. Create/Append Cursor Rules
|
|
113
|
+
const cursorRulesPath = join(cwd, '.cursorrules');
|
|
114
|
+
setupRuleFile(cursorRulesPath, '.cursorrules');
|
|
115
|
+
|
|
116
|
+
// 2. Create/Append Windsurf Rules
|
|
117
|
+
const windsurfRulesPath = join(cwd, '.windsurfrules');
|
|
118
|
+
setupRuleFile(windsurfRulesPath, '.windsurfrules');
|
|
119
|
+
|
|
120
|
+
// 3. Create General Guide File
|
|
121
|
+
const generalGuidePath = join(cwd, '.persystrules.md');
|
|
122
|
+
writeFileSync(generalGuidePath, GENERAL_GUIDE.trim() + '\n', 'utf8');
|
|
123
|
+
console.log(' ✅ Created .persystrules.md (General Guide)');
|
|
124
|
+
|
|
125
|
+
// 4. Configure Git post-commit hook for automatic commit ingestion
|
|
126
|
+
const gitDir = join(cwd, '.git');
|
|
127
|
+
if (existsSync(gitDir)) {
|
|
128
|
+
const hooksDir = join(gitDir, 'hooks');
|
|
129
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
130
|
+
const postCommitPath = join(hooksDir, 'post-commit');
|
|
131
|
+
const localPersystPath = resolve(__dirname, '..', 'index.js').replace(/\\/g, '/');
|
|
132
|
+
|
|
133
|
+
const hookContent = `#!/bin/sh
|
|
134
|
+
# Persyst Git Commit Ingestion Hook
|
|
135
|
+
# Automatically ingests recent commits into Persyst memory on every commit.
|
|
136
|
+
|
|
137
|
+
# Local project path fallback for development
|
|
138
|
+
LOCAL_PERSYST="${localPersystPath}"
|
|
139
|
+
|
|
140
|
+
if [ -f "$LOCAL_PERSYST" ]; then
|
|
141
|
+
node "$LOCAL_PERSYST" ingest "$PWD" 5 >/dev/null 2>&1 || true
|
|
142
|
+
else
|
|
143
|
+
npx persyst-mcp ingest "$PWD" 5 >/dev/null 2>&1 || true
|
|
144
|
+
fi
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
writeFileSync(postCommitPath, hookContent, { mode: 0o755 });
|
|
148
|
+
try {
|
|
149
|
+
chmodSync(postCommitPath, 0o755);
|
|
150
|
+
} catch (_) {}
|
|
151
|
+
console.log(' ✅ Configured Git post-commit hook for auto-ingestion');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 5. Print Success & Configuration Help
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log(' ════════════════════════════════════');
|
|
157
|
+
console.log(' ✅ Rules and Git hooks initialization complete!');
|
|
158
|
+
console.log('');
|
|
159
|
+
console.log(' To connect the memory server to Cursor, Antigravity, or VS Code:');
|
|
160
|
+
console.log(' 1. Open your IDE Settings -> MCP (Model Context Protocol).');
|
|
161
|
+
console.log(' 2. Add a new command server:');
|
|
162
|
+
console.log(' • Name: persyst');
|
|
163
|
+
console.log(' • Command: npx');
|
|
164
|
+
console.log(' • Arguments: -y persyst-mcp');
|
|
165
|
+
console.log('');
|
|
166
|
+
console.log(' The rules we generated will guide the AI agents in this workspace to:');
|
|
167
|
+
console.log(' • Proactively search memory before answering prompts.');
|
|
168
|
+
console.log(' • Log milestone achievements and user preferences.');
|
|
169
|
+
console.log(' • Keep the memory clean ("no bad data").');
|
|
170
|
+
console.log('');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
run();
|
package/bin/setup.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* persyst-setup — One-command installer for Persyst Claude Code hooks
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx persyst-mcp setup
|
|
8
|
+
*
|
|
9
|
+
* What it does:
|
|
10
|
+
* 1. Copies persyst-hook.js to ~/.persyst/hooks/
|
|
11
|
+
* 2. Creates or merges ~/.claude/settings.json with hook registrations
|
|
12
|
+
* 3. Prints success message with instructions
|
|
13
|
+
*
|
|
14
|
+
* Design:
|
|
15
|
+
* - Non-destructive: merges with existing settings, never overwrites
|
|
16
|
+
* - Cross-platform: works on Windows, macOS, and Linux
|
|
17
|
+
* - Idempotent: safe to run multiple times
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'fs';
|
|
21
|
+
import { join, resolve, dirname } from 'path';
|
|
22
|
+
import { homedir } from 'os';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = dirname(__filename);
|
|
27
|
+
|
|
28
|
+
// ============================================================
|
|
29
|
+
// PATHS
|
|
30
|
+
// ============================================================
|
|
31
|
+
|
|
32
|
+
const HOME = homedir();
|
|
33
|
+
const PERSYST_DIR = join(HOME, '.persyst');
|
|
34
|
+
const PERSYST_HOOKS_DIR = join(PERSYST_DIR, 'hooks');
|
|
35
|
+
const HOOK_DEST = join(PERSYST_HOOKS_DIR, 'persyst-hook.js');
|
|
36
|
+
|
|
37
|
+
const CLAUDE_DIR = join(HOME, '.claude');
|
|
38
|
+
const CLAUDE_SETTINGS = join(CLAUDE_DIR, 'settings.json');
|
|
39
|
+
|
|
40
|
+
// Source hook file — shipped with the npm package
|
|
41
|
+
const HOOK_SOURCE = resolve(__dirname, '..', 'hooks', 'persyst-hook.js');
|
|
42
|
+
|
|
43
|
+
// ============================================================
|
|
44
|
+
// HOOK CONFIGURATION
|
|
45
|
+
// ============================================================
|
|
46
|
+
|
|
47
|
+
const HOOK_ENTRY = {
|
|
48
|
+
type: 'command',
|
|
49
|
+
command: `node "${HOOK_DEST}"`
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const HOOK_CONFIG = {
|
|
53
|
+
SessionStart: [
|
|
54
|
+
{
|
|
55
|
+
matcher: '',
|
|
56
|
+
hooks: [{ ...HOOK_ENTRY }]
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
UserPromptSubmit: [
|
|
60
|
+
{
|
|
61
|
+
matcher: '',
|
|
62
|
+
hooks: [{ ...HOOK_ENTRY, timeout: 10 }]
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// HELPERS
|
|
69
|
+
// ============================================================
|
|
70
|
+
|
|
71
|
+
function ensureDir(dir) {
|
|
72
|
+
if (!existsSync(dir)) {
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readJsonFile(filePath) {
|
|
78
|
+
try {
|
|
79
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
80
|
+
return JSON.parse(raw);
|
|
81
|
+
} catch {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Merge Persyst hook entries into existing settings.
|
|
88
|
+
* Does NOT overwrite existing hooks — appends Persyst entries if not already present.
|
|
89
|
+
*/
|
|
90
|
+
function mergeHookSettings(existing) {
|
|
91
|
+
const settings = { ...existing };
|
|
92
|
+
if (!settings.hooks) {
|
|
93
|
+
settings.hooks = {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const [eventName, hookGroups] of Object.entries(HOOK_CONFIG)) {
|
|
97
|
+
if (!settings.hooks[eventName]) {
|
|
98
|
+
settings.hooks[eventName] = [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if a Persyst hook is already registered
|
|
102
|
+
const alreadyRegistered = settings.hooks[eventName].some(group =>
|
|
103
|
+
group.hooks && group.hooks.some(h =>
|
|
104
|
+
h.command && h.command.includes('persyst-hook')
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (!alreadyRegistered) {
|
|
109
|
+
settings.hooks[eventName].push(...hookGroups);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return settings;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================================
|
|
117
|
+
// MAIN
|
|
118
|
+
// ============================================================
|
|
119
|
+
|
|
120
|
+
function run() {
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(' 🧠 Persyst — Claude Code Hook Setup');
|
|
123
|
+
console.log(' ════════════════════════════════════');
|
|
124
|
+
console.log('');
|
|
125
|
+
|
|
126
|
+
// Step 1: Verify hook source exists
|
|
127
|
+
if (!existsSync(HOOK_SOURCE)) {
|
|
128
|
+
console.error(` ❌ Hook source not found at: ${HOOK_SOURCE}`);
|
|
129
|
+
console.error(' Make sure you are running this from the persyst-mcp package.');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Step 2: Copy hook file to ~/.persyst/hooks/
|
|
134
|
+
console.log(' 📁 Installing hook script...');
|
|
135
|
+
ensureDir(PERSYST_HOOKS_DIR);
|
|
136
|
+
copyFileSync(HOOK_SOURCE, HOOK_DEST);
|
|
137
|
+
console.log(` ✅ Copied to ${HOOK_DEST}`);
|
|
138
|
+
|
|
139
|
+
// Step 3: Merge into ~/.claude/settings.json
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log(' ⚙️ Configuring Claude Code...');
|
|
142
|
+
ensureDir(CLAUDE_DIR);
|
|
143
|
+
|
|
144
|
+
const existingSettings = readJsonFile(CLAUDE_SETTINGS);
|
|
145
|
+
const mergedSettings = mergeHookSettings(existingSettings);
|
|
146
|
+
|
|
147
|
+
writeFileSync(CLAUDE_SETTINGS, JSON.stringify(mergedSettings, null, 2) + '\n', 'utf8');
|
|
148
|
+
console.log(` ✅ Updated ${CLAUDE_SETTINGS}`);
|
|
149
|
+
|
|
150
|
+
// Step 4: Print success
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log(' ════════════════════════════════════');
|
|
153
|
+
console.log(' ✅ Setup complete!');
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log(' Persyst will now automatically:');
|
|
156
|
+
console.log(' • Load your stored memories when Claude Code starts');
|
|
157
|
+
console.log(' • Search for relevant context on every prompt');
|
|
158
|
+
console.log(' • Index your git commits into the memory database');
|
|
159
|
+
console.log('');
|
|
160
|
+
console.log(' ⚡ Restart Claude Code to activate the hooks.');
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(' Memory database: ~/.persyst/persyst.db');
|
|
163
|
+
console.log(' Hook script: ~/.persyst/hooks/persyst-hook.js');
|
|
164
|
+
console.log(' Claude settings: ~/.claude/settings.json');
|
|
165
|
+
console.log('');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
run();
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* persyst-hook.js — Claude Code Hook for Persyst Memory
|
|
5
|
+
*
|
|
6
|
+
* Automatically injects relevant memories into Claude Code's context
|
|
7
|
+
* on SessionStart and UserPromptSubmit events.
|
|
8
|
+
*
|
|
9
|
+
* How it works:
|
|
10
|
+
* 1. Claude Code sends a JSON payload on stdin with hook_event_name, session_id, cwd, etc.
|
|
11
|
+
* 2. This script connects to the Persyst MCP server via StdioClientTransport.
|
|
12
|
+
* 3. It calls get_optimized_context or search_memories to retrieve relevant memories.
|
|
13
|
+
* 4. It returns a JSON response on stdout with additionalContext for Claude Code to inject.
|
|
14
|
+
*
|
|
15
|
+
* Installation:
|
|
16
|
+
* npx persyst-mcp setup
|
|
17
|
+
*
|
|
18
|
+
* Manual registration in ~/.claude/settings.json:
|
|
19
|
+
* { "hooks": { "SessionStart": [...], "UserPromptSubmit": [...] } }
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
23
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
24
|
+
import { fileURLToPath } from 'url';
|
|
25
|
+
import { dirname, resolve } from 'path';
|
|
26
|
+
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = dirname(__filename);
|
|
29
|
+
|
|
30
|
+
// Minimum prompt length to trigger memory search (skip "y", "ok", "/run", etc.)
|
|
31
|
+
const MIN_PROMPT_LENGTH = 15;
|
|
32
|
+
|
|
33
|
+
// Maximum time to wait for Persyst MCP connection (ms)
|
|
34
|
+
const CONNECTION_TIMEOUT = 8000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read the full JSON payload from stdin.
|
|
38
|
+
* Claude Code sends the hook context as a single JSON object.
|
|
39
|
+
*/
|
|
40
|
+
function readStdin() {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
let data = '';
|
|
43
|
+
process.stdin.setEncoding('utf8');
|
|
44
|
+
process.stdin.on('data', chunk => { data += chunk; });
|
|
45
|
+
process.stdin.on('end', () => {
|
|
46
|
+
try {
|
|
47
|
+
resolve(JSON.parse(data));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
reject(new Error(`Failed to parse stdin JSON: ${e.message}`));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
process.stdin.on('error', reject);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Connect to the Persyst MCP server as a client.
|
|
58
|
+
* Uses StdioClientTransport to spawn and communicate with the server.
|
|
59
|
+
*/
|
|
60
|
+
async function connectToPersyst() {
|
|
61
|
+
// Resolve the path to Persyst's index.js relative to this hook file
|
|
62
|
+
const persystPath = resolve(__dirname, '..', 'index.js');
|
|
63
|
+
|
|
64
|
+
const transport = new StdioClientTransport({
|
|
65
|
+
command: 'node',
|
|
66
|
+
args: [persystPath]
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const client = new Client({
|
|
70
|
+
name: 'persyst-hook',
|
|
71
|
+
version: '1.0.0'
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Connect with a timeout
|
|
75
|
+
await Promise.race([
|
|
76
|
+
client.connect(transport),
|
|
77
|
+
new Promise((_, reject) =>
|
|
78
|
+
setTimeout(() => reject(new Error('Persyst connection timeout')), CONNECTION_TIMEOUT)
|
|
79
|
+
)
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
return client;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Call a Persyst MCP tool and parse the JSON result.
|
|
87
|
+
*/
|
|
88
|
+
async function callTool(client, toolName, args) {
|
|
89
|
+
const result = await client.callTool({ name: toolName, arguments: args });
|
|
90
|
+
if (result.content && result.content[0] && result.content[0].text) {
|
|
91
|
+
return JSON.parse(result.content[0].text);
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Handle SessionStart: load project-wide context and ingest git history.
|
|
98
|
+
*/
|
|
99
|
+
async function handleSessionStart(client, input) {
|
|
100
|
+
const cwd = input.cwd || process.cwd();
|
|
101
|
+
const repoName = cwd.replace(/\\/g, '/').split('/').pop();
|
|
102
|
+
|
|
103
|
+
// 1. Get project-wide memory context
|
|
104
|
+
const contextResult = await callTool(client, 'get_optimized_context', {
|
|
105
|
+
query: `Project ${repoName} conventions, architecture, user preferences, coding rules`,
|
|
106
|
+
max_tokens: 2000,
|
|
107
|
+
agent_id: 'claude-code',
|
|
108
|
+
session_id: input.session_id || undefined
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 2. Ingest recent git commits (best-effort, don't fail if not a git repo)
|
|
112
|
+
try {
|
|
113
|
+
await callTool(client, 'ingest_git_commits', {
|
|
114
|
+
repo_path: cwd,
|
|
115
|
+
count: 15
|
|
116
|
+
});
|
|
117
|
+
} catch (_) {
|
|
118
|
+
// Not a git repo or git not available — that's fine
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 3. Build the additional context string
|
|
122
|
+
let additionalContext = '';
|
|
123
|
+
if (contextResult && contextResult.context) {
|
|
124
|
+
additionalContext = contextResult.context;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 4. Get memory count for status line
|
|
128
|
+
let memoryCount = 0;
|
|
129
|
+
try {
|
|
130
|
+
const recentResult = await callTool(client, 'get_recent_memories', { limit: 1 });
|
|
131
|
+
if (recentResult && recentResult.count !== undefined) {
|
|
132
|
+
// The count from get_recent is just the returned count, not total
|
|
133
|
+
// Use a search to estimate total active memories
|
|
134
|
+
const importantResult = await callTool(client, 'get_important_memories', { limit: 100 });
|
|
135
|
+
memoryCount = importantResult?.count || 0;
|
|
136
|
+
}
|
|
137
|
+
} catch (_) {
|
|
138
|
+
// Non-critical
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (additionalContext) {
|
|
142
|
+
additionalContext = `[Persyst Memory: ${memoryCount} memories loaded for project "${repoName}"]\n${additionalContext}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
hookSpecificOutput: {
|
|
147
|
+
hookEventName: 'SessionStart',
|
|
148
|
+
additionalContext: additionalContext || undefined
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Handle UserPromptSubmit: search for memories relevant to the user's prompt.
|
|
155
|
+
*/
|
|
156
|
+
async function handleUserPromptSubmit(client, input) {
|
|
157
|
+
const prompt = input.prompt || '';
|
|
158
|
+
|
|
159
|
+
// Skip trivial prompts (commands, confirmations, short inputs)
|
|
160
|
+
if (prompt.trim().length < MIN_PROMPT_LENGTH) {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Use search_memories for speed on per-prompt lookups (faster than get_optimized_context)
|
|
165
|
+
const searchResult = await callTool(client, 'search_memories', {
|
|
166
|
+
query: prompt.slice(0, 200), // Truncate very long prompts for search efficiency
|
|
167
|
+
limit: 5,
|
|
168
|
+
agent_id: 'claude-code',
|
|
169
|
+
session_id: input.session_id || undefined
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!searchResult || !searchResult.results || searchResult.results.length === 0) {
|
|
173
|
+
return {};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Format memories as context
|
|
177
|
+
let contextLines = ['=== PERSYST MEMORY (auto-retrieved) ==='];
|
|
178
|
+
for (const mem of searchResult.results) {
|
|
179
|
+
contextLines.push(`• [Memory #${mem.id}] ${mem.content}`);
|
|
180
|
+
}
|
|
181
|
+
contextLines.push('=== END MEMORY ===');
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
hookSpecificOutput: {
|
|
185
|
+
hookEventName: 'UserPromptSubmit',
|
|
186
|
+
additionalContext: contextLines.join('\n')
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Main entry point.
|
|
193
|
+
*/
|
|
194
|
+
async function main() {
|
|
195
|
+
let client = null;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const input = await readStdin();
|
|
199
|
+
const eventName = input.hook_event_name;
|
|
200
|
+
|
|
201
|
+
// Only handle events we care about
|
|
202
|
+
if (eventName !== 'SessionStart' && eventName !== 'UserPromptSubmit') {
|
|
203
|
+
console.log(JSON.stringify({}));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Connect to Persyst
|
|
208
|
+
client = await connectToPersyst();
|
|
209
|
+
|
|
210
|
+
let response;
|
|
211
|
+
if (eventName === 'SessionStart') {
|
|
212
|
+
response = await handleSessionStart(client, input);
|
|
213
|
+
} else if (eventName === 'UserPromptSubmit') {
|
|
214
|
+
response = await handleUserPromptSubmit(client, input);
|
|
215
|
+
} else {
|
|
216
|
+
response = {};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log(JSON.stringify(response));
|
|
220
|
+
|
|
221
|
+
} catch (err) {
|
|
222
|
+
// Hooks must NEVER break Claude Code — always fail silently
|
|
223
|
+
console.error(`[persyst-hook] Error: ${err.message}`);
|
|
224
|
+
console.log(JSON.stringify({}));
|
|
225
|
+
} finally {
|
|
226
|
+
// Clean up MCP connection
|
|
227
|
+
if (client) {
|
|
228
|
+
try { await client.close(); } catch (_) {}
|
|
229
|
+
}
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
main();
|
package/index.js
CHANGED
|
@@ -7,14 +7,36 @@
|
|
|
7
7
|
* Starts the MCP server on stdio transport.
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
|
-
* node index.js (direct)
|
|
11
|
-
* npx persyst-mcp (via npm)
|
|
10
|
+
* node index.js (direct — starts MCP server)
|
|
11
|
+
* npx persyst-mcp (via npm — starts MCP server)
|
|
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)
|
|
12
15
|
* persyst-mcp (if installed globally)
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
// Handle subcommands before starting the server
|
|
19
|
+
const subcommand = process.argv[2];
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
})
|
|
21
|
+
if (subcommand === 'setup') {
|
|
22
|
+
// Delegate to the setup CLI
|
|
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 {
|
|
36
|
+
// Default: start the MCP server
|
|
37
|
+
const { startServer } = await import('./src/server.js');
|
|
38
|
+
await startServer().catch(err => {
|
|
39
|
+
console.error('❌ Persyst failed to start:', err.message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
});
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "persyst-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
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
|
-
"persyst-mcp": "index.js"
|
|
8
|
+
"persyst-mcp": "index.js",
|
|
9
|
+
"persyst-setup": "bin/setup.js",
|
|
10
|
+
"persyst-aider": "bin/aider.js",
|
|
11
|
+
"persyst-init": "bin/init.js",
|
|
12
|
+
"persyst-ingest": "bin/ingest.js"
|
|
9
13
|
},
|
|
10
14
|
"engines": {
|
|
11
15
|
"node": ">=18.0.0"
|
|
@@ -13,6 +17,8 @@
|
|
|
13
17
|
"files": [
|
|
14
18
|
"index.js",
|
|
15
19
|
"src/",
|
|
20
|
+
"bin/",
|
|
21
|
+
"hooks/",
|
|
16
22
|
"README.md",
|
|
17
23
|
"LICENSE"
|
|
18
24
|
],
|
package/src/search.js
CHANGED
|
@@ -19,6 +19,8 @@ import { generateEmbedding } from './embeddings.js';
|
|
|
19
19
|
import { createAttestation } from './attestation.js';
|
|
20
20
|
import { searchCache, LRUCache } from './cache.js';
|
|
21
21
|
|
|
22
|
+
let lastDataVersion = 0;
|
|
23
|
+
|
|
22
24
|
/**
|
|
23
25
|
* Search memories using both keyword and semantic strategies.
|
|
24
26
|
* Results are cached in the LRU cache for repeated queries.
|
|
@@ -30,6 +32,17 @@ import { searchCache, LRUCache } from './cache.js';
|
|
|
30
32
|
* @returns {Promise<Array>} Ranked search results (with .attestation property attached)
|
|
31
33
|
*/
|
|
32
34
|
export async function searchHybrid(queryText, limit = 5, agentId = null, sessionId = null) {
|
|
35
|
+
// Sync in-memory cache with external DB changes using sqlite data_version
|
|
36
|
+
try {
|
|
37
|
+
const currentDataVersion = db.pragma('data_version', { simple: true });
|
|
38
|
+
if (currentDataVersion !== lastDataVersion) {
|
|
39
|
+
searchCache.invalidate();
|
|
40
|
+
lastDataVersion = currentDataVersion;
|
|
41
|
+
}
|
|
42
|
+
} catch (_) {
|
|
43
|
+
// Fallback if pragma fails
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
// --- Check LRU cache first (Feature 1) ---
|
|
34
47
|
const cacheKey = LRUCache.key(queryText, limit);
|
|
35
48
|
const cached = searchCache.get(cacheKey);
|
|
@@ -64,8 +77,8 @@ export async function searchHybrid(queryText, limit = 5, agentId = null, session
|
|
|
64
77
|
keyword_match: isKeywordMatch
|
|
65
78
|
};
|
|
66
79
|
})
|
|
67
|
-
// Filter out low similarity semantic matches if they have no keyword match (threshold 0.
|
|
68
|
-
.filter(r => r.keyword_match || r.similarity >= 0.
|
|
80
|
+
// Filter out low similarity semantic matches if they have no keyword match (threshold 0.30)
|
|
81
|
+
.filter(r => r.keyword_match || r.similarity >= 0.30);
|
|
69
82
|
|
|
70
83
|
// Add keyword-only hits that semantic search missed
|
|
71
84
|
const semanticIds = new Set(semanticResults.map(r => r.id));
|