persyst-mcp 2.2.4 → 2.2.5
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 +39 -0
- package/bin/export.js +116 -0
- package/bin/import.js +160 -0
- package/hooks/persyst-hook.js +9 -10
- package/index.js +11 -1
- package/package.json +13 -3
- package/src/database.js +6 -4
- package/src/events.js +19 -0
- package/src/extractor-heuristic.js +502 -324
- package/src/sdk.d.ts +175 -0
- package/src/sdk.js +217 -0
- package/src/search.js +92 -2
- package/src/server.js +723 -183
- package/src/tools.js +11 -3
- package/src/watcher.js +27 -12
package/README.md
CHANGED
|
@@ -116,6 +116,13 @@ Add Persyst to your Antigravity agent configuration file at `~/.gemini/antigravi
|
|
|
116
116
|
| `delete_memory` | Delete a memory and clean up edges | `id` (number) |
|
|
117
117
|
| `get_recent_memories` | Get latest memories | `limit` (number) |
|
|
118
118
|
| `get_important_memories` | Get by importance score | `limit` (number) |
|
|
119
|
+
| `get_optimized_context` | Get compressed, ranked context block | `query` (string), `max_tokens` (number) |
|
|
120
|
+
| `ingest_git_commits` | Import recent git commits as memories | `repo_path` (string), `count` (number) |
|
|
121
|
+
| `consolidate_memories` | Merge highly similar duplicate memories | — |
|
|
122
|
+
| `get_memory_history` | Retrieve all versions of a memory | `query` (string) |
|
|
123
|
+
| `get_agent_stats` | Agent reputation stats | — |
|
|
124
|
+
| `export_audit_log` | Export attestation audit log | `start_date`, `end_date` (ISO8601) |
|
|
125
|
+
| `verify_attestation` | Verify Ed25519 signature chain | `attestation_id` (string) |
|
|
119
126
|
|
|
120
127
|
---
|
|
121
128
|
|
|
@@ -159,6 +166,38 @@ Do not run `npx` with `sudo`. If you run into permission issues, ensure your npm
|
|
|
159
166
|
|
|
160
167
|
---
|
|
161
168
|
|
|
169
|
+
## Backup & Migration
|
|
170
|
+
|
|
171
|
+
Persyst includes built-in JSONL export/import commands for portable memory backup and cross-machine migration.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Export all memories to a file
|
|
175
|
+
npx persyst-mcp export
|
|
176
|
+
# → persyst-export-<timestamp>.jsonl
|
|
177
|
+
|
|
178
|
+
# Export to a specific file
|
|
179
|
+
npx persyst-mcp export my-backup.jsonl
|
|
180
|
+
|
|
181
|
+
# Preview what would be imported (dry run)
|
|
182
|
+
npx persyst-mcp import my-backup.jsonl --dry-run
|
|
183
|
+
|
|
184
|
+
# Import memories (skips exact & semantic duplicates automatically)
|
|
185
|
+
npx persyst-mcp import my-backup.jsonl
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Roadmap & Future Directions
|
|
191
|
+
|
|
192
|
+
Persyst is built for the privacy-focused solo developer. We are actively hardening the local-first experience before introducing network dependencies.
|
|
193
|
+
|
|
194
|
+
* **File-Based Sync** ✅ **Done**: `persyst-export` / `persyst-import` JSONL commands for backup and migration.
|
|
195
|
+
* **IDE Integrations**: First-class extensions for Cursor, VS Code, and Aider configuration helper commands.
|
|
196
|
+
* **True P2P Sync (Roadmap)**: Peer-to-peer secure sync between developer devices without relying on central cloud servers.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
162
200
|
## License
|
|
163
201
|
|
|
164
202
|
MIT License. See [LICENSE](LICENSE) for details.
|
|
203
|
+
|
package/bin/export.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* export.js — Persyst Memory JSONL Export CLI
|
|
5
|
+
*
|
|
6
|
+
* Exports all active memories to a portable JSONL file for backup or migration.
|
|
7
|
+
* Each line is a JSON object representing one memory with its full metadata.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* persyst-export → exports to persyst-export-<timestamp>.jsonl
|
|
11
|
+
* persyst-export memories.jsonl → exports to memories.jsonl
|
|
12
|
+
* persyst-export --namespace=shared → exports only the shared namespace
|
|
13
|
+
* persyst-export --all → includes archived (valid_until IS NOT NULL) memories
|
|
14
|
+
*
|
|
15
|
+
* The output format is designed to be imported back via `persyst-import`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createWriteStream } from 'fs';
|
|
19
|
+
import db, { closeDatabase } from '../src/database.js';
|
|
20
|
+
|
|
21
|
+
// ============================================================
|
|
22
|
+
// ARG PARSING
|
|
23
|
+
// ============================================================
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const outputFile = args.find(a => !a.startsWith('--')) || `persyst-export-${Date.now()}.jsonl`;
|
|
27
|
+
const namespace = (args.find(a => a.startsWith('--namespace=')) || '').replace('--namespace=', '') || null;
|
|
28
|
+
const includeArchived = args.includes('--all');
|
|
29
|
+
|
|
30
|
+
// ============================================================
|
|
31
|
+
// EXPORT
|
|
32
|
+
// ============================================================
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const conditions = [];
|
|
36
|
+
const params = [];
|
|
37
|
+
|
|
38
|
+
if (!includeArchived) {
|
|
39
|
+
conditions.push('m.valid_until IS NULL');
|
|
40
|
+
}
|
|
41
|
+
if (namespace) {
|
|
42
|
+
conditions.push("(m.namespace = ? OR m.namespace = 'shared')");
|
|
43
|
+
params.push(namespace);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
47
|
+
|
|
48
|
+
const query = `
|
|
49
|
+
SELECT
|
|
50
|
+
m.id,
|
|
51
|
+
m.content,
|
|
52
|
+
m.importance_score,
|
|
53
|
+
m.namespace,
|
|
54
|
+
m.created_at,
|
|
55
|
+
m.last_accessed,
|
|
56
|
+
m.access_count,
|
|
57
|
+
m.parent_id,
|
|
58
|
+
m.valid_until,
|
|
59
|
+
p.source_type,
|
|
60
|
+
p.source_id,
|
|
61
|
+
p.confidence
|
|
62
|
+
FROM memories m
|
|
63
|
+
LEFT JOIN provenance p ON p.memory_id = m.id
|
|
64
|
+
${whereClause}
|
|
65
|
+
ORDER BY m.id ASC
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
const rows = params.length > 0 ? db.prepare(query).all(...params) : db.prepare(query).all();
|
|
69
|
+
|
|
70
|
+
const out = createWriteStream(outputFile, { encoding: 'utf8' });
|
|
71
|
+
|
|
72
|
+
let count = 0;
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
const record = {
|
|
75
|
+
id: row.id,
|
|
76
|
+
content: row.content,
|
|
77
|
+
importance_score: row.importance_score,
|
|
78
|
+
namespace: row.namespace || 'shared',
|
|
79
|
+
created_at: row.created_at,
|
|
80
|
+
last_accessed: row.last_accessed,
|
|
81
|
+
access_count: row.access_count,
|
|
82
|
+
parent_id: row.parent_id ?? null,
|
|
83
|
+
valid_until: row.valid_until ?? null,
|
|
84
|
+
provenance: row.source_type
|
|
85
|
+
? {
|
|
86
|
+
source_type: row.source_type,
|
|
87
|
+
source_id: row.source_id ?? null,
|
|
88
|
+
confidence: row.confidence ?? 1.0
|
|
89
|
+
}
|
|
90
|
+
: null
|
|
91
|
+
};
|
|
92
|
+
out.write(JSON.stringify(record) + '\n');
|
|
93
|
+
count++;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await new Promise((resolve, reject) => {
|
|
97
|
+
out.end((err) => {
|
|
98
|
+
if (err) reject(err);
|
|
99
|
+
else resolve();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
console.log(`✅ Exported ${count} memories to: ${outputFile}`);
|
|
104
|
+
if (namespace) {
|
|
105
|
+
console.log(` Namespace filter: "${namespace}" + shared`);
|
|
106
|
+
}
|
|
107
|
+
if (includeArchived) {
|
|
108
|
+
console.log(' Includes archived (superseded) memories.');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(`❌ Export failed: ${err.message}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
} finally {
|
|
115
|
+
closeDatabase();
|
|
116
|
+
}
|
package/bin/import.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* import.js — Persyst Memory JSONL Import CLI
|
|
5
|
+
*
|
|
6
|
+
* Imports memories from a JSONL file created by `persyst-export`.
|
|
7
|
+
* Regenerates vector embeddings for each imported memory.
|
|
8
|
+
* Skips duplicates using both exact-content and semantic similarity checks.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* persyst-import memories.jsonl
|
|
12
|
+
* persyst-import memories.jsonl --dry-run → preview without writing
|
|
13
|
+
* persyst-import memories.jsonl --namespace=shared → force all into shared namespace
|
|
14
|
+
* persyst-import memories.jsonl --skip-embeddings → skip re-generating embeddings (fast, no semantic search for these)
|
|
15
|
+
*
|
|
16
|
+
* Compatible with the JSONL format produced by `persyst-export`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createReadStream } from 'fs';
|
|
20
|
+
import { createInterface } from 'readline';
|
|
21
|
+
import db, {
|
|
22
|
+
insertMemory,
|
|
23
|
+
insertVector,
|
|
24
|
+
memoryExists,
|
|
25
|
+
closeDatabase
|
|
26
|
+
} from '../src/database.js';
|
|
27
|
+
import { generateEmbedding } from '../src/embeddings.js';
|
|
28
|
+
import { searchHybrid } from '../src/search.js';
|
|
29
|
+
|
|
30
|
+
// ============================================================
|
|
31
|
+
// ARG PARSING
|
|
32
|
+
// ============================================================
|
|
33
|
+
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
const inputFile = args.find(a => !a.startsWith('--'));
|
|
36
|
+
const isDryRun = args.includes('--dry-run');
|
|
37
|
+
const forceNamespace = (args.find(a => a.startsWith('--namespace=')) || '').replace('--namespace=', '') || null;
|
|
38
|
+
const skipEmbeddings = args.includes('--skip-embeddings');
|
|
39
|
+
|
|
40
|
+
const DEDUP_THRESHOLD = 0.85;
|
|
41
|
+
|
|
42
|
+
if (!inputFile) {
|
|
43
|
+
console.error('❌ Usage: persyst-import <file.jsonl> [--dry-run] [--namespace=<ns>] [--skip-embeddings]');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================
|
|
48
|
+
// MAIN
|
|
49
|
+
// ============================================================
|
|
50
|
+
|
|
51
|
+
async function main() {
|
|
52
|
+
console.log(`📥 Persyst Import${isDryRun ? ' (DRY RUN — nothing will be written)' : ''}`);
|
|
53
|
+
console.log(` Source: ${inputFile}`);
|
|
54
|
+
if (forceNamespace) console.log(` Forcing namespace: "${forceNamespace}"`);
|
|
55
|
+
if (skipEmbeddings) console.log(' Skipping embedding regeneration.');
|
|
56
|
+
console.log('');
|
|
57
|
+
|
|
58
|
+
const rl = createInterface({
|
|
59
|
+
input: createReadStream(inputFile, { encoding: 'utf8' }),
|
|
60
|
+
crlfDelay: Infinity
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let lineNum = 0;
|
|
64
|
+
let imported = 0;
|
|
65
|
+
let skipped = 0;
|
|
66
|
+
let errors = 0;
|
|
67
|
+
|
|
68
|
+
for await (const line of rl) {
|
|
69
|
+
lineNum++;
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (!trimmed) continue;
|
|
72
|
+
|
|
73
|
+
let record;
|
|
74
|
+
try {
|
|
75
|
+
record = JSON.parse(trimmed);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(` ⚠️ Line ${lineNum}: Invalid JSON — skipping`);
|
|
78
|
+
errors++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { content, importance_score = 1.0, namespace, provenance, valid_until } = record;
|
|
83
|
+
|
|
84
|
+
if (!content || typeof content !== 'string' || content.trim().length === 0) {
|
|
85
|
+
console.error(` ⚠️ Line ${lineNum}: Empty content — skipping`);
|
|
86
|
+
errors++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Skip archived memories (unless they have a parent_id, we skip them anyway)
|
|
91
|
+
if (valid_until !== null && valid_until !== undefined) {
|
|
92
|
+
skipped++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const targetNamespace = forceNamespace || namespace || 'shared';
|
|
97
|
+
|
|
98
|
+
// --- Dedup: exact content match ---
|
|
99
|
+
if (memoryExists(content, targetNamespace)) {
|
|
100
|
+
console.log(` ⏭️ Line ${lineNum}: Already exists — skipping "${content.slice(0, 60)}..."`);
|
|
101
|
+
skipped++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Dedup: semantic similarity ---
|
|
106
|
+
if (!skipEmbeddings) {
|
|
107
|
+
try {
|
|
108
|
+
const similar = await searchHybrid(content, 1, null, null, targetNamespace);
|
|
109
|
+
if (similar.length > 0 && parseFloat(similar[0].similarity) >= DEDUP_THRESHOLD) {
|
|
110
|
+
console.log(` ⏭️ Line ${lineNum}: Semantically similar to #${similar[0].id} (sim=${similar[0].similarity}) — skipping`);
|
|
111
|
+
skipped++;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
} catch (_) {
|
|
115
|
+
// Non-critical: proceed with import if semantic check fails
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (isDryRun) {
|
|
120
|
+
console.log(` ✅ Would import: "${content.slice(0, 80)}${content.length > 80 ? '...' : ''}" → ns="${targetNamespace}"`);
|
|
121
|
+
imported++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Write to DB ---
|
|
126
|
+
try {
|
|
127
|
+
const prov = provenance || { source_type: 'import', source_id: 'persyst-import', confidence: 1.0 };
|
|
128
|
+
const id = insertMemory(content, importance_score, prov, targetNamespace);
|
|
129
|
+
|
|
130
|
+
if (!skipEmbeddings) {
|
|
131
|
+
const embedding = await generateEmbedding(content);
|
|
132
|
+
insertVector(id, embedding);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(` ✅ Imported #${id}: "${content.slice(0, 70)}${content.length > 70 ? '...' : ''}"`);
|
|
136
|
+
imported++;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(` ❌ Line ${lineNum}: Failed to insert — ${err.message}`);
|
|
139
|
+
errors++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log('═'.repeat(50));
|
|
145
|
+
if (isDryRun) {
|
|
146
|
+
console.log(`📊 Dry run complete: ${imported} would import, ${skipped} skipped, ${errors} errors`);
|
|
147
|
+
} else {
|
|
148
|
+
console.log(`📊 Import complete: ${imported} imported, ${skipped} skipped, ${errors} errors`);
|
|
149
|
+
}
|
|
150
|
+
console.log('═'.repeat(50));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main()
|
|
154
|
+
.catch(err => {
|
|
155
|
+
console.error(`❌ Import crashed: ${err.message}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
})
|
|
158
|
+
.finally(() => {
|
|
159
|
+
closeDatabase();
|
|
160
|
+
});
|
package/hooks/persyst-hook.js
CHANGED
|
@@ -281,12 +281,11 @@ function spawnWorker() {
|
|
|
281
281
|
/**
|
|
282
282
|
* Handle SessionStart: load project-wide context and ingest git history.
|
|
283
283
|
*/
|
|
284
|
-
async function handleSessionStart(
|
|
284
|
+
async function handleSessionStart(input) {
|
|
285
285
|
const cwd = input.cwd || process.cwd();
|
|
286
286
|
const repoName = cwd.replace(/\\/g, '/').split('/').pop();
|
|
287
287
|
|
|
288
|
-
|
|
289
|
-
const contextResult = await callTool(client, 'get_optimized_context', {
|
|
288
|
+
const contextResult = await callTool(null, 'get_optimized_context', {
|
|
290
289
|
query: `Project ${repoName} conventions, architecture, user preferences, coding rules`,
|
|
291
290
|
max_tokens: 2000,
|
|
292
291
|
agent_id: 'claude-code',
|
|
@@ -295,7 +294,7 @@ async function handleSessionStart(client, input) {
|
|
|
295
294
|
|
|
296
295
|
// 2. Ingest recent git commits (best-effort, don't fail if not a git repo)
|
|
297
296
|
try {
|
|
298
|
-
await callTool(
|
|
297
|
+
await callTool(null, 'ingest_git_commits', {
|
|
299
298
|
repo_path: cwd,
|
|
300
299
|
count: 15
|
|
301
300
|
});
|
|
@@ -312,11 +311,11 @@ async function handleSessionStart(client, input) {
|
|
|
312
311
|
// 4. Get memory count for status line
|
|
313
312
|
let memoryCount = 0;
|
|
314
313
|
try {
|
|
315
|
-
const recentResult = await callTool(
|
|
314
|
+
const recentResult = await callTool(null, 'get_recent_memories', { limit: 1 });
|
|
316
315
|
if (recentResult && recentResult.count !== undefined) {
|
|
317
316
|
// The count from get_recent is just the returned count, not total
|
|
318
317
|
// Use a search to estimate total active memories
|
|
319
|
-
const importantResult = await callTool(
|
|
318
|
+
const importantResult = await callTool(null, 'get_important_memories', { limit: 100 });
|
|
320
319
|
memoryCount = importantResult?.count || 0;
|
|
321
320
|
}
|
|
322
321
|
} catch (_) {
|
|
@@ -339,7 +338,7 @@ async function handleSessionStart(client, input) {
|
|
|
339
338
|
* Handle UserPromptSubmit: search for memories relevant to the user's prompt.
|
|
340
339
|
* Also runs Tier 2 heuristic extraction inline (zero-cost).
|
|
341
340
|
*/
|
|
342
|
-
async function handleUserPromptSubmit(
|
|
341
|
+
async function handleUserPromptSubmit(input) {
|
|
343
342
|
const prompt = input.prompt || '';
|
|
344
343
|
|
|
345
344
|
// Skip trivial prompts (commands, confirmations, short inputs)
|
|
@@ -367,7 +366,7 @@ async function handleUserPromptSubmit(client, input) {
|
|
|
367
366
|
|
|
368
367
|
// --- Memory Retrieval (existing behavior) ---
|
|
369
368
|
// Use search_memories for speed on per-prompt lookups (faster than get_optimized_context)
|
|
370
|
-
const searchResult = await callTool(
|
|
369
|
+
const searchResult = await callTool(null, 'search_memories', {
|
|
371
370
|
query: prompt.slice(0, 200), // Truncate very long prompts for search efficiency
|
|
372
371
|
limit: 5,
|
|
373
372
|
agent_id: 'claude-code',
|
|
@@ -448,11 +447,11 @@ async function main() {
|
|
|
448
447
|
|
|
449
448
|
let response;
|
|
450
449
|
if (eventName === 'SessionStart') {
|
|
451
|
-
response = await handleSessionStart(
|
|
450
|
+
response = await handleSessionStart(input);
|
|
452
451
|
} else if (eventName === 'UserPromptSubmit') {
|
|
453
452
|
// Apply hard timeout for prompt-time hook execution
|
|
454
453
|
response = await Promise.race([
|
|
455
|
-
handleUserPromptSubmit(
|
|
454
|
+
handleUserPromptSubmit(input),
|
|
456
455
|
new Promise((resolve) =>
|
|
457
456
|
setTimeout(() => {
|
|
458
457
|
process.stderr.write(`[persyst-hook] UserPromptSubmit hit ${MAX_HOOK_LATENCY_MS}ms timeout, returning partial.\n`);
|
package/index.js
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
// If running inside Bun (like Qwen's internal runtime), spawn Node.js instead
|
|
19
19
|
if (process.versions.bun && !process.env.PERSYST_RUN_BY_NODE) {
|
|
20
20
|
const { spawn } = await import('child_process');
|
|
21
|
-
|
|
21
|
+
// Prefer NODE env var (set by nvm/fnm/volta), then fall back to 'node' on PATH
|
|
22
|
+
const nodeExec = process.env.NODE || 'node';
|
|
23
|
+
const child = spawn(nodeExec, [
|
|
22
24
|
process.argv[1],
|
|
23
25
|
...process.argv.slice(2)
|
|
24
26
|
], {
|
|
@@ -80,6 +82,14 @@ if (subcommand === 'setup') {
|
|
|
80
82
|
} else if (subcommand === 'worker') {
|
|
81
83
|
// Run the background extraction worker directly
|
|
82
84
|
await import('./bin/extract-worker.js');
|
|
85
|
+
} else if (subcommand === 'export') {
|
|
86
|
+
// Export memories to a JSONL file
|
|
87
|
+
process.argv.splice(2, 1);
|
|
88
|
+
await import('./bin/export.js');
|
|
89
|
+
} else if (subcommand === 'import') {
|
|
90
|
+
// Import memories from a JSONL file
|
|
91
|
+
process.argv.splice(2, 1);
|
|
92
|
+
await import('./bin/import.js');
|
|
83
93
|
} else {
|
|
84
94
|
// Default: start the MCP server
|
|
85
95
|
const { startServer } = await import('./src/server.js');
|
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "persyst-mcp",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.5",
|
|
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
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./sdk": {
|
|
10
|
+
"types": "./src/sdk.d.ts",
|
|
11
|
+
"default": "./src/sdk.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
7
14
|
"bin": {
|
|
8
15
|
"persyst-mcp": "index.js",
|
|
9
16
|
"persyst-setup": "bin/setup.js",
|
|
@@ -11,7 +18,9 @@
|
|
|
11
18
|
"persyst-init": "bin/init.js",
|
|
12
19
|
"persyst-ingest": "bin/ingest.js",
|
|
13
20
|
"persyst-extract": "bin/extract.js",
|
|
14
|
-
"persyst-worker": "bin/extract-worker.js"
|
|
21
|
+
"persyst-worker": "bin/extract-worker.js",
|
|
22
|
+
"persyst-export": "bin/export.js",
|
|
23
|
+
"persyst-import": "bin/import.js"
|
|
15
24
|
},
|
|
16
25
|
"engines": {
|
|
17
26
|
"node": ">=18.0.0"
|
|
@@ -27,7 +36,7 @@
|
|
|
27
36
|
"scripts": {
|
|
28
37
|
"start": "node index.js",
|
|
29
38
|
"test": "cross-env NODE_ENV=test node test/smoke.js",
|
|
30
|
-
"test:heavy": "
|
|
39
|
+
"test:heavy": "node test/run_sequentially.js",
|
|
31
40
|
"worker": "node bin/extract-worker.js",
|
|
32
41
|
"extract": "node bin/extract.js"
|
|
33
42
|
},
|
|
@@ -60,6 +69,7 @@
|
|
|
60
69
|
"@huggingface/transformers": "^4.2.0",
|
|
61
70
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
62
71
|
"better-sqlite3": "^12.10.0",
|
|
72
|
+
"chokidar": "^5.0.0",
|
|
63
73
|
"sqlite-vec": "^0.1.9",
|
|
64
74
|
"zod": "^3.23.0"
|
|
65
75
|
},
|
package/src/database.js
CHANGED
|
@@ -34,6 +34,9 @@ const db = new Database(DB_PATH);
|
|
|
34
34
|
db.pragma('journal_mode = WAL'); // Better performance for concurrent reads
|
|
35
35
|
db.pragma('foreign_keys = ON'); // Enforce referential integrity
|
|
36
36
|
db.pragma('mmap_size = 268435456'); // 256MB memory-mapped I/O for faster reads
|
|
37
|
+
db.pragma('synchronous = NORMAL'); // Performance boost for WAL mode
|
|
38
|
+
db.pragma('temp_store = MEMORY'); // Keep temp tables in memory
|
|
39
|
+
db.pragma('cache_size = -64000'); // 64MB cache size
|
|
37
40
|
|
|
38
41
|
// Load sqlite-vec BEFORE creating any vec0 tables
|
|
39
42
|
sqliteVec.load(db);
|
|
@@ -330,7 +333,7 @@ const stmts = {
|
|
|
330
333
|
decay: db.prepare(`
|
|
331
334
|
UPDATE memories
|
|
332
335
|
SET importance_score = ROUND(MAX(importance_score * 0.95, 0.0), 4)
|
|
333
|
-
WHERE (
|
|
336
|
+
WHERE (unixepoch() - last_accessed) > 604800
|
|
334
337
|
`),
|
|
335
338
|
|
|
336
339
|
// -- Search --
|
|
@@ -615,8 +618,7 @@ export function boostMemory(id) {
|
|
|
615
618
|
* Called automatically every hour by the server.
|
|
616
619
|
*/
|
|
617
620
|
export function applyTemporalDecay() {
|
|
618
|
-
const
|
|
619
|
-
const result = stmts.decay.run(now);
|
|
621
|
+
const result = stmts.decay.run();
|
|
620
622
|
if (result.changes > 0) {
|
|
621
623
|
console.error(`[persyst] Decay applied to ${result.changes} memories`);
|
|
622
624
|
}
|
|
@@ -749,7 +751,7 @@ export function memoryExistsByHashPrefix(pattern) {
|
|
|
749
751
|
* @returns {number}
|
|
750
752
|
*/
|
|
751
753
|
export function getActiveMemoryCount(namespace = null) {
|
|
752
|
-
if (namespace) {
|
|
754
|
+
if (namespace && namespace !== 'all') {
|
|
753
755
|
return stmts.getActiveMemoryCountNs.get(namespace).count;
|
|
754
756
|
}
|
|
755
757
|
return stmts.getActiveMemoryCount.get().count;
|
package/src/events.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* events.js — Persyst In-Process Memory Event Bus
|
|
3
|
+
*
|
|
4
|
+
* A shared EventEmitter used by the HTTP gateway (SSE broadcasting),
|
|
5
|
+
* the log watcher, and the MCP tool handlers to signal memory changes
|
|
6
|
+
* in real-time without tight coupling.
|
|
7
|
+
*
|
|
8
|
+
* Events emitted:
|
|
9
|
+
* memory_added { id, content, namespace, source }
|
|
10
|
+
* memory_deleted { id }
|
|
11
|
+
* memories_consolidated { consolidated_groups, details }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
|
|
16
|
+
export const memoryEventBus = new EventEmitter();
|
|
17
|
+
|
|
18
|
+
// Support large swarms with many simultaneous SSE subscribers
|
|
19
|
+
memoryEventBus.setMaxListeners(500);
|