persyst-mcp 2.2.4 → 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 +64 -2
- package/bin/export.js +116 -0
- package/bin/import.js +160 -0
- package/bin/init.js +168 -32
- package/bin/mcp.js +7 -0
- package/hooks/persyst-hook.js +9 -10
- package/index.js +42 -12
- package/package.json +15 -10
- package/src/attestation.js +49 -28
- package/src/database.js +229 -36
- package/src/events.js +19 -0
- package/src/extractor-heuristic.js +505 -324
- package/src/sdk.d.ts +175 -0
- package/src/sdk.js +218 -0
- package/src/search.js +144 -83
- package/src/server.js +766 -93
- package/src/setup-wasm.js +34 -39
- package/src/text-utils.js +41 -0
- package/src/tools.js +58 -46
- package/src/watcher.js +174 -50
package/src/watcher.js
CHANGED
|
@@ -8,23 +8,26 @@
|
|
|
8
8
|
|
|
9
9
|
import { join, resolve } from 'path';
|
|
10
10
|
import { homedir } from 'os';
|
|
11
|
-
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, openSync, readSync, closeSync } from 'fs';
|
|
12
12
|
import {
|
|
13
13
|
getWatchPosition,
|
|
14
14
|
upsertWatchPosition,
|
|
15
15
|
insertMemory,
|
|
16
16
|
insertVector,
|
|
17
|
-
memoryExists
|
|
17
|
+
memoryExists,
|
|
18
|
+
deleteMemory
|
|
18
19
|
} from './database.js';
|
|
19
20
|
import { generateEmbedding } from './embeddings.js';
|
|
20
|
-
import { extractHeuristic } from './extractor-heuristic.js';
|
|
21
|
+
import { extractHeuristic, hasExtractableSignals } from './extractor-heuristic.js';
|
|
21
22
|
import { searchHybrid } from './search.js';
|
|
22
23
|
import { searchCache } from './cache.js';
|
|
24
|
+
import { memoryEventBus } from './events.js';
|
|
25
|
+
import chokidar from 'chokidar';
|
|
23
26
|
|
|
24
|
-
// Config path: ~/.persyst/config.json
|
|
25
|
-
const CONFIG_FILE = join(homedir(), '.persyst', 'config.json');
|
|
27
|
+
// Config path: ~/.persyst/config.json (overridable for tests)
|
|
28
|
+
const CONFIG_FILE = process.env.PERSYST_CONFIG_FILE || join(homedir(), '.persyst', 'config.json');
|
|
26
29
|
|
|
27
|
-
let
|
|
30
|
+
let chokidarWatcher = null;
|
|
28
31
|
const DEDUP_THRESHOLD = 0.80;
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -86,25 +89,47 @@ async function processJsonlFile(filePath) {
|
|
|
86
89
|
|
|
87
90
|
if (stat.size <= lastPos) return;
|
|
88
91
|
|
|
89
|
-
// Read only new content appended to the file
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
// Read only new content appended to the file (Bug C fix)
|
|
93
|
+
const length = stat.size - lastPos;
|
|
94
|
+
let text = '';
|
|
95
|
+
if (length > 0) {
|
|
96
|
+
const newContentBuffer = Buffer.alloc(length);
|
|
97
|
+
const fd = openSync(filePath, 'r');
|
|
98
|
+
try {
|
|
99
|
+
readSync(fd, newContentBuffer, 0, length, lastPos);
|
|
100
|
+
} finally {
|
|
101
|
+
closeSync(fd);
|
|
102
|
+
}
|
|
103
|
+
text = newContentBuffer.toString('utf8');
|
|
104
|
+
}
|
|
93
105
|
|
|
94
106
|
const lines = text.split('\n');
|
|
95
107
|
let addedCount = 0;
|
|
108
|
+
let processedOffset = lastPos;
|
|
96
109
|
|
|
97
|
-
for (
|
|
98
|
-
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
const line = lines[i];
|
|
112
|
+
const isLastLine = i === lines.length - 1;
|
|
113
|
+
|
|
114
|
+
// Empty trailing line after a newline is expected; skip it without treating it as partial.
|
|
115
|
+
if (!line.trim()) {
|
|
116
|
+
if (!isLastLine) processedOffset += line.length + 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
99
119
|
|
|
100
120
|
let record;
|
|
101
121
|
try {
|
|
102
122
|
record = JSON.parse(line);
|
|
103
123
|
} catch (_) {
|
|
104
|
-
//
|
|
124
|
+
// If the last line fails to parse, it may be partially written. Leave processedOffset
|
|
125
|
+
// before this line so the next scan re-reads it from the start.
|
|
126
|
+
if (!isLastLine) processedOffset += line.length + 1;
|
|
105
127
|
continue;
|
|
106
128
|
}
|
|
107
129
|
|
|
130
|
+
// Commit the bytes for this line (including the newline that produced the split).
|
|
131
|
+
processedOffset += line.length + 1;
|
|
132
|
+
|
|
108
133
|
// Check if it's user prompt or assistant response
|
|
109
134
|
if (
|
|
110
135
|
record.content &&
|
|
@@ -112,30 +137,39 @@ async function processJsonlFile(filePath) {
|
|
|
112
137
|
) {
|
|
113
138
|
// Strip XML/markdown wrapper tags (like <USER_REQUEST> or <ADDITIONAL_METADATA>)
|
|
114
139
|
const cleanText = record.content.replace(/<[^>]+>[\s\S]*?<\/[^>]+>/g, '').trim();
|
|
115
|
-
if (cleanText.length < 15) continue;
|
|
140
|
+
if (cleanText.length < 15 || !hasExtractableSignals(cleanText)) continue;
|
|
116
141
|
|
|
117
142
|
const facts = extractHeuristic(cleanText);
|
|
118
143
|
for (const fact of facts) {
|
|
119
|
-
// Verify against exact duplicate
|
|
120
|
-
if (memoryExists(fact.content)) continue;
|
|
144
|
+
// Verify against exact duplicate (Bug A fix: check namespace 'shared')
|
|
145
|
+
if (memoryExists(fact.content, 'shared')) continue;
|
|
121
146
|
|
|
122
|
-
// Verify against semantic similarity
|
|
123
|
-
const similar = await searchHybrid(fact.content, 1);
|
|
147
|
+
// Verify against semantic similarity (Bug B fix: check namespace 'shared')
|
|
148
|
+
const similar = await searchHybrid(fact.content, 1, null, null, 'shared');
|
|
124
149
|
if (similar.length > 0 && parseFloat(similar[0].similarity) >= DEDUP_THRESHOLD) {
|
|
125
150
|
continue;
|
|
126
151
|
}
|
|
127
152
|
|
|
128
|
-
// Insert memory with provenance
|
|
153
|
+
// Insert memory with provenance (written to 'shared' by default)
|
|
129
154
|
const id = insertMemory(fact.content, fact.confidence, {
|
|
130
155
|
source_type: 'agent',
|
|
131
156
|
source_id: record.source === 'MODEL' ? 'antigravity-worker' : 'user-dialogue',
|
|
132
157
|
confidence: fact.confidence
|
|
133
158
|
});
|
|
134
159
|
|
|
135
|
-
|
|
136
|
-
|
|
160
|
+
try {
|
|
161
|
+
const embedding = await generateEmbedding(fact.content);
|
|
162
|
+
insertVector(id, embedding);
|
|
163
|
+
} catch (embedErr) {
|
|
164
|
+
console.error(`[persyst-watcher] Embedding failed for fact #${id}: ${embedErr.message}`);
|
|
165
|
+
// Clean up: delete the memory so we don't have orphaned entries
|
|
166
|
+
try { deleteMemory(id); } catch (_) {}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
137
170
|
addedCount++;
|
|
138
171
|
console.error(`[persyst-watcher] Auto-extracted fact: "${fact.content}" (Memory #${id})`);
|
|
172
|
+
memoryEventBus.emit('memory_added', { id, content: fact.content, namespace: 'shared', source: 'watcher-antigravity' });
|
|
139
173
|
}
|
|
140
174
|
}
|
|
141
175
|
}
|
|
@@ -144,10 +178,13 @@ async function processJsonlFile(filePath) {
|
|
|
144
178
|
searchCache.invalidate();
|
|
145
179
|
}
|
|
146
180
|
|
|
147
|
-
// Persist
|
|
148
|
-
|
|
181
|
+
// Persist the byte offset up to the last successfully parsed complete line.
|
|
182
|
+
// Do not advance past an incomplete trailing line so it is re-read on the next scan.
|
|
183
|
+
upsertWatchPosition(filePath, processedOffset);
|
|
184
|
+
return addedCount;
|
|
149
185
|
} catch (err) {
|
|
150
186
|
console.error(`[persyst-watcher] Failed to process JSONL file ${filePath}: ${err.message}`);
|
|
187
|
+
return 0;
|
|
151
188
|
}
|
|
152
189
|
}
|
|
153
190
|
|
|
@@ -175,29 +212,41 @@ async function processJsonFile(filePath) {
|
|
|
175
212
|
// Process only newly added messages
|
|
176
213
|
for (let i = lastMsgCount; i < history.length; i++) {
|
|
177
214
|
const msg = history[i];
|
|
178
|
-
if (!msg.content || typeof msg.content !== 'string') continue;
|
|
215
|
+
if (!msg.content || typeof msg.content !== 'string' || !hasExtractableSignals(msg.content)) continue;
|
|
179
216
|
|
|
180
217
|
// Filter out system message structures
|
|
181
218
|
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
182
219
|
const facts = extractHeuristic(msg.content);
|
|
183
220
|
for (const fact of facts) {
|
|
184
|
-
|
|
221
|
+
// Verify against exact duplicate (Bug A fix: check namespace 'shared')
|
|
222
|
+
if (memoryExists(fact.content, 'shared')) continue;
|
|
185
223
|
|
|
186
|
-
|
|
224
|
+
// Verify against semantic similarity (Bug B fix: check namespace 'shared')
|
|
225
|
+
const similar = await searchHybrid(fact.content, 1, null, null, 'shared');
|
|
187
226
|
if (similar.length > 0 && parseFloat(similar[0].similarity) >= DEDUP_THRESHOLD) {
|
|
188
227
|
continue;
|
|
189
228
|
}
|
|
190
229
|
|
|
230
|
+
// Insert memory with provenance (written to 'shared' by default)
|
|
191
231
|
const id = insertMemory(fact.content, fact.confidence, {
|
|
192
232
|
source_type: 'agent',
|
|
193
233
|
source_id: msg.role === 'assistant' ? 'roo-worker' : 'user-dialogue',
|
|
194
234
|
confidence: fact.confidence
|
|
195
235
|
});
|
|
196
236
|
|
|
197
|
-
|
|
198
|
-
|
|
237
|
+
try {
|
|
238
|
+
const embedding = await generateEmbedding(fact.content);
|
|
239
|
+
insertVector(id, embedding);
|
|
240
|
+
} catch (embedErr) {
|
|
241
|
+
console.error(`[persyst-watcher] Embedding failed for fact #${id}: ${embedErr.message}`);
|
|
242
|
+
// Clean up: delete the memory so we don't have orphaned entries
|
|
243
|
+
try { deleteMemory(id); } catch (_) {}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
199
247
|
addedCount++;
|
|
200
248
|
console.error(`[persyst-watcher] Auto-extracted fact: "${fact.content}" (Memory #${id})`);
|
|
249
|
+
memoryEventBus.emit('memory_added', { id, content: fact.content, namespace: 'shared', source: 'watcher-roo' });
|
|
201
250
|
}
|
|
202
251
|
}
|
|
203
252
|
}
|
|
@@ -208,8 +257,10 @@ async function processJsonFile(filePath) {
|
|
|
208
257
|
|
|
209
258
|
// Persist message count index
|
|
210
259
|
upsertWatchPosition(filePath, history.length);
|
|
260
|
+
return addedCount;
|
|
211
261
|
} catch (err) {
|
|
212
262
|
console.error(`[persyst-watcher] Failed to process JSON file ${filePath}: ${err.message}`);
|
|
263
|
+
return 0;
|
|
213
264
|
}
|
|
214
265
|
}
|
|
215
266
|
|
|
@@ -248,6 +299,7 @@ function findFiles(dir, ext, depth = 3) {
|
|
|
248
299
|
*/
|
|
249
300
|
export async function scanDirectories() {
|
|
250
301
|
const watchDirs = loadWatchedDirs();
|
|
302
|
+
let totalAdded = 0;
|
|
251
303
|
|
|
252
304
|
for (const dir of watchDirs) {
|
|
253
305
|
if (!existsSync(dir)) continue;
|
|
@@ -255,7 +307,7 @@ export async function scanDirectories() {
|
|
|
255
307
|
// Scan for JSONL (Antigravity transcripts)
|
|
256
308
|
const jsonlFiles = findFiles(dir, 'transcript.jsonl', 3);
|
|
257
309
|
for (const file of jsonlFiles) {
|
|
258
|
-
await processJsonlFile(file);
|
|
310
|
+
totalAdded += await processJsonlFile(file);
|
|
259
311
|
}
|
|
260
312
|
|
|
261
313
|
// Scan for JSON (Roo Code / Cline task files)
|
|
@@ -263,44 +315,116 @@ export async function scanDirectories() {
|
|
|
263
315
|
for (const file of jsonFiles) {
|
|
264
316
|
// Avoid processing general configurations/settings files
|
|
265
317
|
if (file.includes('tasks')) {
|
|
266
|
-
await processJsonFile(file);
|
|
318
|
+
totalAdded += await processJsonFile(file);
|
|
267
319
|
}
|
|
268
320
|
}
|
|
269
321
|
}
|
|
322
|
+
|
|
323
|
+
// Auto-consolidate memories if new ones were added to keep prompt context slim
|
|
324
|
+
if (totalAdded > 0) {
|
|
325
|
+
try {
|
|
326
|
+
console.error(`[persyst-watcher] Running automatic memory consolidation sweep...`);
|
|
327
|
+
const { consolidateMemories } = await import('./search.js');
|
|
328
|
+
const report = await consolidateMemories();
|
|
329
|
+
console.error(`[persyst-watcher] Auto-consolidation complete: merged ${report.consolidated_groups} duplicate groups.`);
|
|
330
|
+
} catch (e) {
|
|
331
|
+
console.error(`[persyst-watcher] Auto-consolidation failed: ${e.message}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Run periodic auto-expiry check on every folder scan (fast query)
|
|
336
|
+
try {
|
|
337
|
+
const { archiveExpiredMemories } = await import('./database.js');
|
|
338
|
+
archiveExpiredMemories();
|
|
339
|
+
} catch (e) {
|
|
340
|
+
console.error(`[persyst-watcher] Auto-expiry execution failed: ${e.message}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Handle a file addition or modification event from Chokidar.
|
|
346
|
+
* @param {string} filePath
|
|
347
|
+
*/
|
|
348
|
+
async function handleFileChange(filePath) {
|
|
349
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
350
|
+
let addedCount = 0;
|
|
351
|
+
|
|
352
|
+
if (normalizedPath.endsWith('transcript.jsonl')) {
|
|
353
|
+
addedCount = await processJsonlFile(filePath);
|
|
354
|
+
} else if (normalizedPath.endsWith('.json') && normalizedPath.includes('tasks')) {
|
|
355
|
+
addedCount = await processJsonFile(filePath);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (addedCount > 0) {
|
|
359
|
+
try {
|
|
360
|
+
console.error(`[persyst-watcher] Running automatic memory consolidation sweep...`);
|
|
361
|
+
const { consolidateMemories } = await import('./search.js');
|
|
362
|
+
const report = await consolidateMemories();
|
|
363
|
+
console.error(`[persyst-watcher] Auto-consolidation complete: merged ${report.consolidated_groups} duplicate groups.`);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
console.error(`[persyst-watcher] Auto-consolidation failed: ${e.message}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Run periodic auto-expiry check on every change (fast query)
|
|
370
|
+
try {
|
|
371
|
+
const { archiveExpiredMemories } = await import('./database.js');
|
|
372
|
+
archiveExpiredMemories();
|
|
373
|
+
} catch (e) {
|
|
374
|
+
console.error(`[persyst-watcher] Auto-expiry execution failed: ${e.message}`);
|
|
375
|
+
}
|
|
270
376
|
}
|
|
271
377
|
|
|
272
378
|
/**
|
|
273
379
|
* Start the background log watcher daemon.
|
|
274
380
|
*/
|
|
275
381
|
export function startWatcher() {
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
console.error('[persyst-watcher] Starting background log watcher daemon...');
|
|
279
|
-
// Warm up config/paths
|
|
280
|
-
loadWatchedDirs();
|
|
382
|
+
if (chokidarWatcher) return;
|
|
281
383
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
console.error(`[persyst-watcher] Initial scan failed: ${err.message}`);
|
|
285
|
-
});
|
|
384
|
+
console.error('[persyst-watcher] Starting background log watcher daemon (Chokidar)...');
|
|
385
|
+
const watchDirs = loadWatchedDirs();
|
|
286
386
|
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
387
|
+
// Run initial scan, then start watching
|
|
388
|
+
scanDirectories()
|
|
389
|
+
.catch(err => {
|
|
390
|
+
console.error(`[persyst-watcher] Initial scan failed: ${err.message}`);
|
|
391
|
+
})
|
|
392
|
+
.then(() => {
|
|
393
|
+
if (chokidarWatcher) return;
|
|
394
|
+
chokidarWatcher = chokidar.watch(watchDirs, {
|
|
395
|
+
persistent: true,
|
|
396
|
+
ignoreInitial: true, // we already ran scanDirectories
|
|
397
|
+
awaitWriteFinish: {
|
|
398
|
+
stabilityThreshold: 300,
|
|
399
|
+
pollInterval: 100
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
chokidarWatcher.on('add', filePath => {
|
|
404
|
+
handleFileChange(filePath).catch(err => {
|
|
405
|
+
console.error(`[persyst-watcher] Error handling added file ${filePath}:`, err);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
chokidarWatcher.on('change', filePath => {
|
|
410
|
+
handleFileChange(filePath).catch(err => {
|
|
411
|
+
console.error(`[persyst-watcher] Error handling changed file ${filePath}:`, err);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
chokidarWatcher.on('error', err => {
|
|
416
|
+
console.error(`[persyst-watcher] Chokidar watcher error: ${err.message}`);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
295
419
|
}
|
|
296
420
|
|
|
297
421
|
/**
|
|
298
422
|
* Stop the background log watcher daemon.
|
|
299
423
|
*/
|
|
300
424
|
export function stopWatcher() {
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
425
|
+
if (chokidarWatcher) {
|
|
426
|
+
chokidarWatcher.close().catch(() => {});
|
|
427
|
+
chokidarWatcher = null;
|
|
304
428
|
console.error('[persyst-watcher] Background log watcher daemon stopped.');
|
|
305
429
|
}
|
|
306
430
|
}
|