persyst-mcp 2.2.5 → 2.2.6
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/README.md +25 -2
- package/bin/init.js +168 -32
- package/bin/mcp.js +7 -0
- package/index.js +31 -11
- package/package.json +6 -11
- package/src/attestation.js +49 -28
- package/src/database.js +223 -32
- package/src/extractor-heuristic.js +5 -2
- package/src/sdk.js +4 -3
- package/src/search.js +54 -83
- package/src/server.js +856 -723
- package/src/setup-wasm.js +34 -39
- package/src/text-utils.js +41 -0
- package/src/tools.js +49 -45
- package/src/watcher.js +147 -38
package/README.md
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
# Persyst
|
|
2
2
|
|
|
3
|
-
**Local-first MCP memory
|
|
3
|
+
**Local-first, compliance-grade MCP memory layer for regulated enterprise coding teams using AI assistants.**
|
|
4
4
|
|
|
5
|
-
Persyst gives AI coding agents (Claude Code, Cursor, VS Code, Aider,
|
|
5
|
+
Persyst gives AI coding agents (Claude Code, Cursor, VS Code, Aider, Continue.dev, Antigravity) persistent memory across sessions. It stores memories in a local SQLite database with hybrid keyword + semantic search — operating 100% offline with zero cloud egress.
|
|
6
|
+
|
|
7
|
+
## Compliance-Grade Security Features
|
|
8
|
+
|
|
9
|
+
Persyst is built from the ground up for highly regulated enterprise environments (finance, healthcare, defense) subject to **SOC 2**, **HIPAA**, and the **EU AI Act**:
|
|
10
|
+
|
|
11
|
+
* **100% Data Residency (Zero-Egress)**: All vector calculations, full-text searches, and model inferences run locally on the developer's workstation. No database records or context data ever leave the local machine. Bypasses Business Associate Agreement (BAA) complexity for HIPAA.
|
|
12
|
+
* **Cryptographic Chain of Custody**: Every context retrieval generates an Ed25519 cryptographic signature sealing the query and retrieved memory hashes. Each attestation is chained to the previous one via SHA-256 hash chains, creating a tamper-evident audit ledger verifiable by security teams.
|
|
13
|
+
* **Automatic Secret Redaction**: Scans incoming log files and text writes to redact high-entropy secrets (API keys, JWTs, database strings, private keys) before they reach the persistent database.
|
|
14
|
+
* **Reactive File Watching**: Integrates `chokidar` for instant event-driven scanning of agent transcript folders, guaranteeing that your memories are synchronized immediately after each agent interaction.
|
|
15
|
+
|
|
16
|
+
*Read more in our compliance mapping guides:*
|
|
17
|
+
- [SOC 2 Type II Controls](file:///c:/Users/Super/Desktop/Peryst/compliance/SOC2-controls.md)
|
|
18
|
+
- [HIPAA Mapping & PHI Boundaries](file:///c:/Users/Super/Desktop/Peryst/compliance/HIPAA-mapping.md)
|
|
19
|
+
- [EU AI Act Article 13 Transparency](file:///c:/Users/Super/Desktop/Peryst/compliance/EU-AI-Act-Article13.md)
|
|
20
|
+
- [Compliance Audit Trail Sample](file:///c:/Users/Super/Desktop/Peryst/compliance/audit-trail-sample.md)
|
|
6
21
|
|
|
7
22
|
## How It Works
|
|
8
23
|
|
|
@@ -16,6 +31,14 @@ Your AI Agent ←→ MCP (stdio) ←→ Persyst ←→ SQLite (local)
|
|
|
16
31
|
|
|
17
32
|
> 🚨 **First-Run Note**: On the first start, Persyst will automatically download the local embedding model (`all-MiniLM-L6-v2` ~50MB). This can take 30-60 seconds depending on your connection. The server will log `Loading embedding model...` and then proceed normally.
|
|
18
33
|
|
|
34
|
+
### Passive Recording vs. Active Retrieval
|
|
35
|
+
|
|
36
|
+
> ⚠️ **Honest Note on Agent Integration**: Persyst operates in two complementary modes:
|
|
37
|
+
> 1. **Passive Recording**: The file watcher automatically extracts and saves memories from your agent conversation transcripts in the background.
|
|
38
|
+
> 2. **Active Retrieval**: The AI agent calls `search_memories` or `get_optimized_context` to fetch relevant context.
|
|
39
|
+
>
|
|
40
|
+
> The IDE itself does not automatically inject retrieved memories into prompt inputs unless configured to do so via workspace rules (e.g. `.cursorrules`, `.windsurfrules`, `.agents/AGENTS.md`) or custom system prompt builders. To ensure the agent utilizes its memory, make sure your agent instructions direct it to query the database.
|
|
41
|
+
|
|
19
42
|
---
|
|
20
43
|
|
|
21
44
|
## Quick Start
|
package/bin/init.js
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* persyst-init — Workspace rules generator
|
|
4
|
+
* persyst-init — Workspace rules generator and global IDE configuration builder
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* npx persyst-mcp init
|
|
8
|
+
* npx persyst-mcp init --mcp cursor,aider
|
|
8
9
|
*
|
|
9
10
|
* What it does:
|
|
10
|
-
* 1. Safely creates or appends system instructions to `.cursorrules`
|
|
11
|
-
* 2.
|
|
12
|
-
* 3.
|
|
13
|
-
* 4.
|
|
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)
|
|
11
|
+
* 1. Safely creates or appends system instructions to `.cursorrules` and `.windsurfrules`
|
|
12
|
+
* 2. Creates a general `.persystrules.md` workspace guide
|
|
13
|
+
* 3. Configures Git post-commit hook for auto-ingestion
|
|
14
|
+
* 4. Generates cryptographic Ed25519 keys inside ~/.persyst
|
|
15
|
+
* 5. Automatically detects and configures global settings for Cursor, Aider, Claude Code, and Continue
|
|
19
16
|
*/
|
|
20
17
|
|
|
21
18
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
|
|
22
19
|
import { join, resolve, dirname } from 'path';
|
|
20
|
+
import { homedir } from 'os';
|
|
23
21
|
import { fileURLToPath } from 'url';
|
|
22
|
+
import { execSync } from 'child_process';
|
|
23
|
+
import { initializeKeys } from '../src/attestation.js';
|
|
24
24
|
|
|
25
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
26
|
const __dirname = dirname(__filename);
|
|
27
27
|
|
|
28
|
+
const CONFIG_DIR = join(homedir(), '.persyst');
|
|
29
|
+
const PERSYST_DB = join(CONFIG_DIR, 'persyst.db');
|
|
30
|
+
|
|
28
31
|
// ============================================================
|
|
29
32
|
// SYSTEM INSTRUCTION CONTENT
|
|
30
33
|
// ============================================================
|
|
@@ -46,6 +49,10 @@ You are integrated with Persyst, a local-first MCP memory server that stores use
|
|
|
46
49
|
- Handle Contradictions: Persyst handles contradiction detection automatically. If a new fact contradicts an old memory, Persyst will flag it.
|
|
47
50
|
- 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
51
|
|
|
52
|
+
## Explicit User Save Requests
|
|
53
|
+
- If the user explicitly asks you to remember, save, or keep a note of a fact (e.g., "Remember that John handles deployment", "remind me that staging is flaky"), call the \`add_memory\` tool immediately with that content.
|
|
54
|
+
- Bypassing Tech Filters: Explicit user requests bypass the programming keyword filters. Ensure they are captured verbatim.
|
|
55
|
+
|
|
49
56
|
## Mandatory Completion Checklist (HARD CONSTRAINT)
|
|
50
57
|
Before writing your final response declaring a task, feature, or bug fix complete:
|
|
51
58
|
1. Ask yourself: "Did I implement a feature, fix a bug, configure a tool, or discover a project rule?"
|
|
@@ -82,7 +89,7 @@ ${RULE_CONTENT.trim()}
|
|
|
82
89
|
`;
|
|
83
90
|
|
|
84
91
|
// ============================================================
|
|
85
|
-
// HELPERS
|
|
92
|
+
// WORKSPACE HELPERS
|
|
86
93
|
// ============================================================
|
|
87
94
|
|
|
88
95
|
function setupRuleFile(filePath, fileName) {
|
|
@@ -104,33 +111,153 @@ function setupRuleFile(filePath, fileName) {
|
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
// ============================================================
|
|
107
|
-
//
|
|
114
|
+
// GLOBAL CONFIG WRITERS
|
|
115
|
+
// ============================================================
|
|
116
|
+
|
|
117
|
+
function detectEditors() {
|
|
118
|
+
const editors = [];
|
|
119
|
+
const home = homedir();
|
|
120
|
+
|
|
121
|
+
// Cursor
|
|
122
|
+
const cursorDir = join(home, '.cursor');
|
|
123
|
+
const winCursorDir = join(home, 'AppData', 'Roaming', 'Cursor');
|
|
124
|
+
if (existsSync(cursorDir) || existsSync(winCursorDir) || existsSync('/Applications/Cursor.app') || existsSync(join(home, 'AppData', 'Local', 'Programs', 'cursor'))) {
|
|
125
|
+
editors.push('cursor');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Aider
|
|
129
|
+
try {
|
|
130
|
+
execSync('aider --version', { stdio: 'ignore' });
|
|
131
|
+
editors.push('aider');
|
|
132
|
+
} catch (_) {}
|
|
133
|
+
|
|
134
|
+
// Claude Code
|
|
135
|
+
const claudeDir = join(home, '.claude');
|
|
136
|
+
if (existsSync(claudeDir) || existsSync('/Applications/Claude Code.app')) {
|
|
137
|
+
editors.push('claude-code');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Continue.dev
|
|
141
|
+
const continueConfig = join(home, '.continue', 'config.json');
|
|
142
|
+
if (existsSync(continueConfig)) {
|
|
143
|
+
editors.push('continue');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return editors;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function writeCursorConfig() {
|
|
150
|
+
const cursorMcp = join(homedir(), '.cursor', 'mcp.json');
|
|
151
|
+
try {
|
|
152
|
+
const config = existsSync(cursorMcp) ? JSON.parse(readFileSync(cursorMcp, 'utf8')) : {};
|
|
153
|
+
config.mcpServers = config.mcpServers || {};
|
|
154
|
+
config.mcpServers.persyst = {
|
|
155
|
+
"command": "npx",
|
|
156
|
+
"args": ["-y", "persyst-mcp"],
|
|
157
|
+
"env": { "PERSYST_DB": PERSYST_DB }
|
|
158
|
+
};
|
|
159
|
+
mkdirSync(dirname(cursorMcp), { recursive: true });
|
|
160
|
+
writeFileSync(cursorMcp, JSON.stringify(config, null, 2));
|
|
161
|
+
console.log(' ✅ Cursor MCP config written to ~/.cursor/mcp.json');
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(` ✗ Failed to configure Cursor: ${err.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function writeAiderConfig() {
|
|
168
|
+
const aiderYml = join(homedir(), '.aider.conf.yml');
|
|
169
|
+
try {
|
|
170
|
+
let content = '';
|
|
171
|
+
if (existsSync(aiderYml)) {
|
|
172
|
+
content = readFileSync(aiderYml, 'utf8');
|
|
173
|
+
}
|
|
174
|
+
if (!content.includes('persyst')) {
|
|
175
|
+
content += `\n# Persyst MCP integration\nmcp:\n - name: persyst\n cmd: npx\n args: ["-y", "persyst-mcp"]\n env:\n PERSYST_DB: ${PERSYST_DB}\n`;
|
|
176
|
+
writeFileSync(aiderYml, content);
|
|
177
|
+
console.log(' ✅ Aider MCP config appended to ~/.aider.conf.yml');
|
|
178
|
+
} else {
|
|
179
|
+
console.log(' ℹ️ Aider already has Persyst configured (skipped).');
|
|
180
|
+
}
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error(` ✗ Failed to configure Aider: ${err.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function writeClaudeCodeConfig() {
|
|
187
|
+
const claudeJson = join(homedir(), '.claude.json');
|
|
188
|
+
try {
|
|
189
|
+
const config = existsSync(claudeJson) ? JSON.parse(readFileSync(claudeJson, 'utf8')) : {};
|
|
190
|
+
config.mcpServers = config.mcpServers || {};
|
|
191
|
+
config.mcpServers.persyst = {
|
|
192
|
+
"command": "npx",
|
|
193
|
+
"args": ["-y", "persyst-mcp"],
|
|
194
|
+
"env": { "PERSYST_DB": PERSYST_DB }
|
|
195
|
+
};
|
|
196
|
+
writeFileSync(claudeJson, JSON.stringify(config, null, 2));
|
|
197
|
+
console.log(' ✅ Claude Code MCP config written to ~/.claude.json');
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error(` ✗ Failed to configure Claude Code: ${err.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function writeContinueConfig() {
|
|
204
|
+
const continueConfig = join(homedir(), '.continue', 'config.json');
|
|
205
|
+
try {
|
|
206
|
+
const config = existsSync(continueConfig) ? JSON.parse(readFileSync(continueConfig, 'utf8')) : {};
|
|
207
|
+
config.mcpServers = config.mcpServers || [];
|
|
208
|
+
// Remove existing persyst entry
|
|
209
|
+
config.mcpServers = config.mcpServers.filter(s => s.name !== 'persyst');
|
|
210
|
+
config.mcpServers.push({
|
|
211
|
+
"name": "persyst",
|
|
212
|
+
"command": "npx",
|
|
213
|
+
"args": ["-y", "persyst-mcp"],
|
|
214
|
+
"env": { "PERSYST_DB": PERSYST_DB }
|
|
215
|
+
});
|
|
216
|
+
mkdirSync(dirname(continueConfig), { recursive: true });
|
|
217
|
+
writeFileSync(continueConfig, JSON.stringify(config, null, 2));
|
|
218
|
+
console.log(' ✅ Continue.dev MCP config written to ~/.continue/config.json');
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error(` ✗ Failed to configure Continue.dev: ${err.message}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ============================================================
|
|
225
|
+
// MAIN RUNNER
|
|
108
226
|
// ============================================================
|
|
109
227
|
|
|
110
228
|
function run() {
|
|
111
229
|
console.log('');
|
|
112
|
-
console.log(' 🧠 Persyst — Workspace
|
|
113
|
-
console.log('
|
|
230
|
+
console.log(' 🧠 Persyst — Workspace & Editor Setup');
|
|
231
|
+
console.log(' ══════════════════════════════════════');
|
|
114
232
|
console.log('');
|
|
115
233
|
|
|
116
234
|
const cwd = process.cwd();
|
|
117
235
|
console.log(` 📁 Target workspace: ${cwd}`);
|
|
118
|
-
console.log('');
|
|
119
236
|
|
|
120
|
-
// 1.
|
|
237
|
+
// 1. Initialize local configuration folder and attestations
|
|
238
|
+
console.log(' ⚙️ Initializing keypairs & DB folders...');
|
|
239
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
240
|
+
initializeKeys();
|
|
241
|
+
console.log(' ✅ Cryptographic keypairs generated');
|
|
242
|
+
|
|
243
|
+
// 2. Local workspace configurations
|
|
244
|
+
console.log('');
|
|
245
|
+
console.log(' 📄 Initializing workspace rule files...');
|
|
246
|
+
|
|
121
247
|
const cursorRulesPath = join(cwd, '.cursorrules');
|
|
122
248
|
setupRuleFile(cursorRulesPath, '.cursorrules');
|
|
123
249
|
|
|
124
|
-
// 2. Create/Append Windsurf Rules
|
|
125
250
|
const windsurfRulesPath = join(cwd, '.windsurfrules');
|
|
126
251
|
setupRuleFile(windsurfRulesPath, '.windsurfrules');
|
|
127
252
|
|
|
128
|
-
|
|
253
|
+
const clineRulesPath = join(cwd, '.clinerules');
|
|
254
|
+
setupRuleFile(clineRulesPath, '.clinerules');
|
|
255
|
+
|
|
129
256
|
const generalGuidePath = join(cwd, '.persystrules.md');
|
|
130
257
|
writeFileSync(generalGuidePath, GENERAL_GUIDE.trim() + '\n', 'utf8');
|
|
131
258
|
console.log(' ✅ Created .persystrules.md (General Guide)');
|
|
132
259
|
|
|
133
|
-
//
|
|
260
|
+
// 3. Git post-commit hook
|
|
134
261
|
const gitDir = join(cwd, '.git');
|
|
135
262
|
if (existsSync(gitDir)) {
|
|
136
263
|
const hooksDir = join(gitDir, 'hooks');
|
|
@@ -159,22 +286,31 @@ fi
|
|
|
159
286
|
console.log(' ✅ Configured Git post-commit hook for auto-ingestion');
|
|
160
287
|
}
|
|
161
288
|
|
|
162
|
-
//
|
|
289
|
+
// 4. Global editor configurations
|
|
163
290
|
console.log('');
|
|
164
|
-
console.log('
|
|
165
|
-
|
|
291
|
+
console.log(' 💻 Initializing global IDE configurations...');
|
|
292
|
+
|
|
293
|
+
const args = process.argv.slice(2);
|
|
294
|
+
const mcpFlag = args.find(a => a.startsWith('--mcp='));
|
|
295
|
+
const requestedEditors = mcpFlag ? mcpFlag.split('=')[1].split(',') : [];
|
|
296
|
+
|
|
297
|
+
const editors = requestedEditors.length > 0 ? requestedEditors : detectEditors();
|
|
298
|
+
console.log(` Detected editors/environments: ${editors.join(', ') || 'none'}`);
|
|
299
|
+
|
|
300
|
+
if (editors.includes('cursor')) writeCursorConfig();
|
|
301
|
+
if (editors.includes('aider')) writeAiderConfig();
|
|
302
|
+
if (editors.includes('claude-code')) writeClaudeCodeConfig();
|
|
303
|
+
if (editors.includes('continue')) writeContinueConfig();
|
|
304
|
+
|
|
305
|
+
// 5. Final self-test and notes
|
|
166
306
|
console.log('');
|
|
167
|
-
console.log('
|
|
168
|
-
console.log('
|
|
169
|
-
console.log(' 2. Add a new command server:');
|
|
170
|
-
console.log(' • Name: persyst');
|
|
171
|
-
console.log(' • Command: npx');
|
|
172
|
-
console.log(' • Arguments: -y persyst-mcp');
|
|
307
|
+
console.log(' ══════════════════════════════════════');
|
|
308
|
+
console.log(' 🎉 Setup complete! Persyst is fully configured.');
|
|
173
309
|
console.log('');
|
|
174
|
-
console.log('
|
|
175
|
-
console.log('
|
|
176
|
-
console.log('
|
|
177
|
-
console.log('
|
|
310
|
+
console.log(' Next steps:');
|
|
311
|
+
console.log(' 1. Restart your editor to load the new MCP configurations.');
|
|
312
|
+
console.log(' 2. Test gateway connection:');
|
|
313
|
+
console.log(' curl http://127.0.0.1:4321/health');
|
|
178
314
|
console.log('');
|
|
179
315
|
}
|
|
180
316
|
|
package/bin/mcp.js
ADDED
package/index.js
CHANGED
|
@@ -39,22 +39,42 @@ if (process.versions.bun && !process.env.PERSYST_RUN_BY_NODE) {
|
|
|
39
39
|
|
|
40
40
|
// Fix PATH on Windows if running in environments like Qwen Desktop that override PATH
|
|
41
41
|
if (process.platform === 'win32') {
|
|
42
|
-
const nodeBin = 'C:\\Program Files\\nodejs';
|
|
43
|
-
const gitBin = 'C:\\Program Files\\Git\\cmd';
|
|
44
|
-
const systemBin = 'C:\\WINDOWS\\system32;C:\\WINDOWS';
|
|
45
|
-
|
|
46
42
|
const currentPath = process.env.PATH || '';
|
|
47
43
|
const paths = currentPath.split(';');
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
|
|
45
|
+
// Dynamically find node and git on PATH using where.exe
|
|
46
|
+
const { execFileSync } = await import('child_process');
|
|
47
|
+
for (const cmd of ['node', 'git']) {
|
|
48
|
+
try {
|
|
49
|
+
const result = execFileSync('where.exe', [cmd], { encoding: 'utf8', timeout: 2000 });
|
|
50
|
+
const binDir = result.trim().split('\r\n')[0].trim();
|
|
51
|
+
if (binDir) {
|
|
52
|
+
const dir = binDir.substring(0, binDir.lastIndexOf('\\'));
|
|
53
|
+
if (dir && !paths.some(p => p.toLowerCase() === dir.toLowerCase())) {
|
|
54
|
+
paths.push(dir);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// where.exe failed — fall back to common paths
|
|
59
|
+
if (cmd === 'node') {
|
|
60
|
+
for (const p of ['C:\\Program Files\\nodejs', process.env.NVM_SYMLINK, `${process.env.USERPROFILE}\\AppData\\Roaming\\nvm\\v20.11.0`].filter(Boolean)) {
|
|
61
|
+
if (!paths.some(ex => ex.toLowerCase() === p.toLowerCase())) paths.push(p);
|
|
62
|
+
}
|
|
63
|
+
} else if (cmd === 'git') {
|
|
64
|
+
for (const p of ['C:\\Program Files\\Git\\cmd', 'C:\\Program Files\\Git\\bin', `${process.env.LOCALAPPDATA}\\Programs\\Git\\cmd`].filter(Boolean)) {
|
|
65
|
+
if (!paths.some(ex => ex.toLowerCase() === p.toLowerCase())) paths.push(p);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Ensure system folders are present
|
|
72
|
+
const systemBin = 'C:\\WINDOWS\\system32;C:\\WINDOWS';
|
|
53
73
|
const sysPaths = systemBin.split(';');
|
|
54
74
|
sysPaths.forEach(p => {
|
|
55
|
-
if (!paths.
|
|
75
|
+
if (!paths.some(ex => ex.toLowerCase() === p.toLowerCase())) paths.push(p);
|
|
56
76
|
});
|
|
57
|
-
|
|
77
|
+
|
|
58
78
|
process.env.PATH = paths.join(';');
|
|
59
79
|
}
|
|
60
80
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "persyst-mcp",
|
|
3
|
-
"version": "2.2.
|
|
4
|
-
"description": "Local-first MCP memory
|
|
3
|
+
"version": "2.2.6",
|
|
4
|
+
"description": "Local-first, compliance-grade MCP memory layer with hybrid keyword + semantic search for coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
@@ -12,15 +12,10 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"bin": {
|
|
15
|
-
"persyst-mcp": "
|
|
16
|
-
"persyst-setup": "bin/setup.js",
|
|
17
|
-
"persyst-
|
|
18
|
-
"persyst
|
|
19
|
-
"persyst-ingest": "bin/ingest.js",
|
|
20
|
-
"persyst-extract": "bin/extract.js",
|
|
21
|
-
"persyst-worker": "bin/extract-worker.js",
|
|
22
|
-
"persyst-export": "bin/export.js",
|
|
23
|
-
"persyst-import": "bin/import.js"
|
|
15
|
+
"persyst-mcp": "./bin/mcp.js",
|
|
16
|
+
"persyst-setup": "./bin/setup.js",
|
|
17
|
+
"persyst-init": "./bin/init.js",
|
|
18
|
+
"persyst": "./index.js"
|
|
24
19
|
},
|
|
25
20
|
"engines": {
|
|
26
21
|
"node": ">=18.0.0"
|
package/src/attestation.js
CHANGED
|
@@ -9,7 +9,7 @@ import crypto from 'crypto';
|
|
|
9
9
|
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
|
|
10
10
|
import { join } from 'path';
|
|
11
11
|
import { homedir } from 'os';
|
|
12
|
-
import db, { getLastAttestation, insertAttestation, getAttestationById } from './database.js';
|
|
12
|
+
import db, { stmts, getLastAttestation, insertAttestation, getAttestationById } from './database.js';
|
|
13
13
|
|
|
14
14
|
const KEYS_DIR = join(homedir(), '.persyst', 'keys');
|
|
15
15
|
|
|
@@ -69,7 +69,7 @@ export function createAttestation(query, memories, agentId = null, sessionId = n
|
|
|
69
69
|
return {
|
|
70
70
|
id: m.id,
|
|
71
71
|
content_hash: contentHash,
|
|
72
|
-
score: parseFloat(scoreVal)
|
|
72
|
+
score: Math.round(parseFloat(scoreVal) * 10000)
|
|
73
73
|
};
|
|
74
74
|
});
|
|
75
75
|
|
|
@@ -135,6 +135,22 @@ export function verifyAttestationRecord(attestation) {
|
|
|
135
135
|
};
|
|
136
136
|
|
|
137
137
|
const dataToSign = JSON.stringify(doc);
|
|
138
|
+
const fullRecord = {
|
|
139
|
+
...doc,
|
|
140
|
+
signature: attestation.signature
|
|
141
|
+
};
|
|
142
|
+
const computedHash = crypto.createHash('sha256').update(JSON.stringify(fullRecord)).digest('hex');
|
|
143
|
+
|
|
144
|
+
// Check hash first — if it matches, doc reconstruction is correct
|
|
145
|
+
const hashMatch = computedHash === attestation.hash;
|
|
146
|
+
if (!hashMatch) {
|
|
147
|
+
console.error('[persyst-attest] HASH MISMATCH for', attestation.attestation_id);
|
|
148
|
+
console.error('[persyst-attest] stored hash:', attestation.hash);
|
|
149
|
+
console.error('[persyst-attest] computed hash:', computedHash);
|
|
150
|
+
console.error('[persyst-attest] doc:', JSON.stringify(doc));
|
|
151
|
+
return { valid: false, error: 'Attestation hash mismatch' };
|
|
152
|
+
}
|
|
153
|
+
|
|
138
154
|
const publicKey = getPublicKey();
|
|
139
155
|
|
|
140
156
|
// Verify signature
|
|
@@ -150,20 +166,14 @@ export function verifyAttestationRecord(attestation) {
|
|
|
150
166
|
);
|
|
151
167
|
|
|
152
168
|
if (!isSignatureValid) {
|
|
169
|
+
console.error('[persyst-attest] SIG VERIFY FAIL for', attestation.attestation_id);
|
|
170
|
+
console.error('[persyst-attest] Hash matches but signature invalid');
|
|
171
|
+
console.error('[persyst-attest] dataToSign:', dataToSign);
|
|
172
|
+
console.error('[persyst-attest] signature:', attestation.signature);
|
|
173
|
+
console.error('[persyst-attest] public key:', publicKey);
|
|
153
174
|
return { valid: false, error: 'Signature verification failed' };
|
|
154
175
|
}
|
|
155
176
|
|
|
156
|
-
// Verify hash integrity
|
|
157
|
-
const fullRecord = {
|
|
158
|
-
...doc,
|
|
159
|
-
signature: attestation.signature
|
|
160
|
-
};
|
|
161
|
-
const computedHash = crypto.createHash('sha256').update(JSON.stringify(fullRecord)).digest('hex');
|
|
162
|
-
|
|
163
|
-
if (computedHash !== attestation.hash) {
|
|
164
|
-
return { valid: false, error: 'Attestation hash mismatch' };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
177
|
return { valid: true };
|
|
168
178
|
} catch (err) {
|
|
169
179
|
return { valid: false, error: err.message };
|
|
@@ -171,7 +181,10 @@ export function verifyAttestationRecord(attestation) {
|
|
|
171
181
|
}
|
|
172
182
|
|
|
173
183
|
/**
|
|
174
|
-
*
|
|
184
|
+
* Iteratively verifies signature and chain integrity.
|
|
185
|
+
* Walks backwards from the target attestation to the genesis link,
|
|
186
|
+
* confirming each previous_hash matches the predecessor's actual hash
|
|
187
|
+
* and that sequence order strictly increases.
|
|
175
188
|
*/
|
|
176
189
|
export function verifyChainIntegrity(attestationId) {
|
|
177
190
|
const att = getAttestationById(attestationId);
|
|
@@ -184,29 +197,37 @@ export function verifyChainIntegrity(attestationId) {
|
|
|
184
197
|
return selfVerify;
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
200
|
+
// Iterative chain walk — no recursion, no stack overflow risk
|
|
201
|
+
const MAX_CHAIN_DEPTH = 10000;
|
|
202
|
+
let current = att;
|
|
203
|
+
let depth = 0;
|
|
204
|
+
|
|
205
|
+
while (current.previous_hash) {
|
|
206
|
+
if (depth >= MAX_CHAIN_DEPTH) {
|
|
207
|
+
return { valid: false, error: 'Broken chain: chain length exceeds maximum' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const prevAtt = stmts.getAttestationByHash.get(current.previous_hash);
|
|
190
211
|
if (!prevAtt) {
|
|
191
|
-
return { valid: false, error: `Broken chain: Previous attestation with hash ${
|
|
212
|
+
return { valid: false, error: `Broken chain: Previous attestation with hash ${current.previous_hash} not found` };
|
|
192
213
|
}
|
|
193
214
|
|
|
194
|
-
if (prevAtt.
|
|
195
|
-
return { valid: false, error:
|
|
215
|
+
if (prevAtt.hash !== current.previous_hash) {
|
|
216
|
+
return { valid: false, error: 'Broken chain: previous_hash does not match predecessor hash' };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (prevAtt.id >= current.id) {
|
|
220
|
+
return { valid: false, error: 'Broken chain: Invalid sequence order' };
|
|
196
221
|
}
|
|
197
222
|
|
|
198
223
|
const prevVerify = verifyAttestationRecord(prevAtt);
|
|
199
224
|
if (!prevVerify.valid) {
|
|
200
225
|
return { valid: false, error: `Broken chain: Previous link is invalid: ${prevVerify.error}` };
|
|
201
226
|
}
|
|
202
|
-
}
|
|
203
227
|
|
|
204
|
-
|
|
205
|
-
|
|
228
|
+
current = prevAtt;
|
|
229
|
+
depth++;
|
|
230
|
+
}
|
|
206
231
|
|
|
207
|
-
|
|
208
|
-
* Helper to fetch attestation by hash since it's not exposed globally.
|
|
209
|
-
*/
|
|
210
|
-
function getAttestationByHash(hash) {
|
|
211
|
-
return db.prepare('SELECT * FROM attestations WHERE hash = ?').get(hash) || null;
|
|
232
|
+
return { valid: true, attestation: current };
|
|
212
233
|
}
|