persyst-mcp 2.2.6 → 2.2.7
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 +94 -128
- package/bin/export.js +4 -4
- package/bin/extract.js +8 -8
- package/bin/import.js +15 -15
- package/bin/init.js +44 -33
- package/bin/mcp.js +1 -5
- package/bin/monitor.js +511 -0
- package/bin/setup.js +9 -9
- package/package.json +12 -8
- package/src/cache.js +3 -1
- package/src/database.js +4 -2
- package/src/embeddings.js +4 -2
- package/src/events.js +2 -0
- package/src/search.js +2 -2
- package/src/server.js +179 -151
- package/src/text-utils.js +11 -0
- package/src/tools.js +49 -8
- package/src/watcher.js +12 -13
package/bin/monitor.js
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* persyst-monitor — Real-Time Terminal Activity Monitor
|
|
5
|
+
*
|
|
6
|
+
* Connects to the Persyst HTTP gateway (default: http://127.0.0.1:4321) and
|
|
7
|
+
* streams all memory events live to your terminal. Also polls /health and
|
|
8
|
+
* /stats every 10 seconds to show a running context snapshot.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* npx persyst-mcp monitor
|
|
12
|
+
* node bin/monitor.js
|
|
13
|
+
* node bin/monitor.js --port 4321
|
|
14
|
+
* node bin/monitor.js --context (snapshot only, no event stream)
|
|
15
|
+
*
|
|
16
|
+
* Requirements:
|
|
17
|
+
* - Persyst server must be running (npx persyst-mcp OR node index.js)
|
|
18
|
+
* - Server gateway port 4321 must be accessible (http://127.0.0.1:4321)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import http from 'http';
|
|
22
|
+
|
|
23
|
+
// ============================================================
|
|
24
|
+
// CONFIG
|
|
25
|
+
// ============================================================
|
|
26
|
+
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
const PORT_ARG = args.find(a => a.startsWith('--port='))?.split('=')[1]
|
|
29
|
+
|| (args.indexOf('--port') !== -1 ? args[args.indexOf('--port') + 1] : null);
|
|
30
|
+
const PORT = parseInt(PORT_ARG || process.env.PORT || '4321', 10);
|
|
31
|
+
const HOST = process.env.PERSYST_HOST || '127.0.0.1';
|
|
32
|
+
const BASE_URL = `http://${HOST}:${PORT}`;
|
|
33
|
+
const CONTEXT_ONLY = args.includes('--context');
|
|
34
|
+
|
|
35
|
+
// ============================================================
|
|
36
|
+
// TERMINAL UTILITIES (No external deps — raw ANSI)
|
|
37
|
+
// ============================================================
|
|
38
|
+
|
|
39
|
+
const ANSI = {
|
|
40
|
+
reset: '\x1b[0m',
|
|
41
|
+
bold: '\x1b[1m',
|
|
42
|
+
dim: '\x1b[2m',
|
|
43
|
+
green: '\x1b[32m',
|
|
44
|
+
yellow: '\x1b[33m',
|
|
45
|
+
cyan: '\x1b[36m',
|
|
46
|
+
white: '\x1b[37m',
|
|
47
|
+
red: '\x1b[31m',
|
|
48
|
+
magenta: '\x1b[35m',
|
|
49
|
+
blue: '\x1b[34m',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function c(ansi, text) { return `${ansi}${text}${ANSI.reset}`; }
|
|
53
|
+
function bold(t) { return c(ANSI.bold, t); }
|
|
54
|
+
function dim(t) { return c(ANSI.dim, t); }
|
|
55
|
+
function green(t) { return c(ANSI.green, t); }
|
|
56
|
+
function yellow(t) { return c(ANSI.yellow, t); }
|
|
57
|
+
function cyan(t) { return c(ANSI.cyan, t); }
|
|
58
|
+
function red(t) { return c(ANSI.red, t); }
|
|
59
|
+
function magenta(t) { return c(ANSI.magenta, t); }
|
|
60
|
+
function blue(t) { return c(ANSI.blue, t); }
|
|
61
|
+
function white(t) { return c(ANSI.white, t); }
|
|
62
|
+
|
|
63
|
+
function hr(char = '-', width = 72) {
|
|
64
|
+
return dim(char.repeat(width));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function timestamp() {
|
|
68
|
+
return new Date().toLocaleTimeString('en-US', {
|
|
69
|
+
hour12: false,
|
|
70
|
+
hour: '2-digit',
|
|
71
|
+
minute: '2-digit',
|
|
72
|
+
second: '2-digit'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isoNow() {
|
|
77
|
+
return new Date().toLocaleString('en-US', { hour12: false }).replace(',', '');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function truncate(str, maxLen = 80) {
|
|
81
|
+
if (!str) return '';
|
|
82
|
+
const clean = str.replace(/\n/g, ' ').trim();
|
|
83
|
+
return clean.length > maxLen ? clean.slice(0, maxLen - 3) + '...' : clean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function estimateRawTokens(memories) {
|
|
87
|
+
return memories * 150; // avg ~150 tokens per memory
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatUptime(seconds) {
|
|
91
|
+
const h = Math.floor(seconds / 3600);
|
|
92
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
93
|
+
const s = seconds % 60;
|
|
94
|
+
if (h > 0) return `${h}h ${m}m ${s}s`;
|
|
95
|
+
if (m > 0) return `${m}m ${s}s`;
|
|
96
|
+
return `${s}s`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================
|
|
100
|
+
// HTTP HELPERS
|
|
101
|
+
// ============================================================
|
|
102
|
+
|
|
103
|
+
function get(path) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const req = http.get(`${BASE_URL}${path}`, { timeout: 5000 }, (res) => {
|
|
106
|
+
let body = '';
|
|
107
|
+
res.on('data', d => body += d);
|
|
108
|
+
res.on('end', () => {
|
|
109
|
+
try { resolve(JSON.parse(body)); }
|
|
110
|
+
catch (e) { reject(new Error(`Invalid JSON from ${path}`)); }
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
114
|
+
req.on('error', reject);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================================
|
|
119
|
+
// SESSION COUNTERS
|
|
120
|
+
// ============================================================
|
|
121
|
+
|
|
122
|
+
const session = {
|
|
123
|
+
saved: 0,
|
|
124
|
+
retrieved: 0,
|
|
125
|
+
deleted: 0,
|
|
126
|
+
updated: 0,
|
|
127
|
+
consolidated: 0,
|
|
128
|
+
watcher: 0,
|
|
129
|
+
startTime: Date.now(),
|
|
130
|
+
connected: false,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// ============================================================
|
|
134
|
+
// BANNER
|
|
135
|
+
// ============================================================
|
|
136
|
+
|
|
137
|
+
function printBanner(health) {
|
|
138
|
+
const version = health?.version || '2.x';
|
|
139
|
+
const memories = health?.memories ?? '?';
|
|
140
|
+
|
|
141
|
+
console.log('');
|
|
142
|
+
console.log(bold(cyan(' ======================================================================')));
|
|
143
|
+
console.log(bold(cyan(' PERSYST LIVE ACTIVITY MONITOR')));
|
|
144
|
+
console.log(dim(` v${version} | ${BASE_URL} | ${isoNow()}`));
|
|
145
|
+
console.log(bold(cyan(' ======================================================================')));
|
|
146
|
+
console.log('');
|
|
147
|
+
console.log(` ${bold('Gateway:')} ${cyan(BASE_URL)}`);
|
|
148
|
+
console.log(` ${bold('Memories:')} ${bold(green(String(memories)))} active records in database`);
|
|
149
|
+
console.log('');
|
|
150
|
+
console.log(hr('='));
|
|
151
|
+
console.log('');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================================
|
|
155
|
+
// STATS PANEL
|
|
156
|
+
// ============================================================
|
|
157
|
+
|
|
158
|
+
function printStatsPanel(health, stats) {
|
|
159
|
+
const uptime = health?.uptime_seconds ?? 0;
|
|
160
|
+
const memories = health?.memories ?? 0;
|
|
161
|
+
const sseConns = health?.sse_clients ?? 0;
|
|
162
|
+
const elapsed = Math.floor((Date.now() - session.startTime) / 1000);
|
|
163
|
+
|
|
164
|
+
const rawTokens = estimateRawTokens(memories);
|
|
165
|
+
const compressedTokens = Math.round(rawTokens * 0.055); // ~94.5% compression
|
|
166
|
+
|
|
167
|
+
const namespaces = stats?.namespaces || [];
|
|
168
|
+
const agents = stats?.agents || [];
|
|
169
|
+
|
|
170
|
+
console.log('');
|
|
171
|
+
console.log(hr('='));
|
|
172
|
+
console.log(` ${bold(cyan('LIVE STATS'))} ${dim('[' + timestamp() + ']')}`);
|
|
173
|
+
console.log(hr('='));
|
|
174
|
+
|
|
175
|
+
// Context metrics
|
|
176
|
+
console.log(` ${bold('Active Memories:')} ${bold(green(String(memories)))}`);
|
|
177
|
+
console.log(` ${bold('Est. Raw Tokens:')} ~${bold(rawTokens.toLocaleString())} ${dim('(avg 150 tokens/memory)')}`);
|
|
178
|
+
console.log(` ${bold('Compressed Budget:')} ~${bold(compressedTokens.toLocaleString())} ${dim('(94.5% reduction via graph-hop compression)')}`);
|
|
179
|
+
console.log(` ${bold('Server Uptime:')} ${formatUptime(uptime)}`);
|
|
180
|
+
console.log(` ${bold('SSE Subscribers:')} ${sseConns}`);
|
|
181
|
+
|
|
182
|
+
// Namespace breakdown
|
|
183
|
+
if (namespaces.length > 0) {
|
|
184
|
+
console.log('');
|
|
185
|
+
console.log(` ${bold('Namespace Breakdown:')}`);
|
|
186
|
+
const total = namespaces.reduce((sum, ns) => sum + ns.count, 0) || 1;
|
|
187
|
+
for (const ns of namespaces) {
|
|
188
|
+
const pct = Math.round((ns.count / total) * 20);
|
|
189
|
+
const bar = '#'.repeat(Math.max(1, pct));
|
|
190
|
+
const name = (ns.namespace || 'shared').padEnd(30);
|
|
191
|
+
console.log(` ${cyan(name)} ${green(bar.padEnd(20))} ${bold(String(ns.count))} memories`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Agent reputation ledger
|
|
196
|
+
if (agents.length > 0) {
|
|
197
|
+
console.log('');
|
|
198
|
+
console.log(` ${bold('Agent Reputation Ledger:')}`);
|
|
199
|
+
console.log(` ${'Agent ID'.padEnd(32)} ${'Created'.padEnd(10)} ${'Confirmed'.padEnd(12)} ${'Contradicted'.padEnd(14)} Trust`);
|
|
200
|
+
console.log(` ${dim('-'.repeat(72))}`);
|
|
201
|
+
for (const a of agents.slice(0, 6)) {
|
|
202
|
+
const score = parseFloat(a.reputation_score).toFixed(2);
|
|
203
|
+
const scoreCol = parseFloat(score) >= 0.9 ? green : parseFloat(score) >= 0.7 ? yellow : red;
|
|
204
|
+
console.log(
|
|
205
|
+
` ${(a.agent_id || 'unknown').padEnd(32)}` +
|
|
206
|
+
` ${String(a.memories_created).padEnd(10)}` +
|
|
207
|
+
` ${String(a.memories_confirmed).padEnd(12)}` +
|
|
208
|
+
` ${String(a.memories_contradicted).padEnd(14)}` +
|
|
209
|
+
` ${scoreCol(score)}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Session summary
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log(` ${bold('Session Activity:')} ${dim('(' + formatUptime(elapsed) + ' monitoring)')}`);
|
|
217
|
+
console.log(
|
|
218
|
+
` ${green('[SAVED]')} ${bold(String(session.saved).padEnd(6))}` +
|
|
219
|
+
` ${cyan('[RETRIEVED]')} ${bold(String(session.retrieved).padEnd(6))}` +
|
|
220
|
+
` ${yellow('[UPDATED]')} ${bold(String(session.updated).padEnd(6))}` +
|
|
221
|
+
` ${red('[DELETED]')} ${bold(String(session.deleted).padEnd(6))}` +
|
|
222
|
+
` ${magenta('[WATCHER]')} ${bold(String(session.watcher))}` +
|
|
223
|
+
` ${blue('[MERGED]')} ${bold(String(session.consolidated))}`
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
console.log(hr('='));
|
|
227
|
+
console.log('');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ============================================================
|
|
231
|
+
// EVENT PRINTERS
|
|
232
|
+
// ============================================================
|
|
233
|
+
|
|
234
|
+
function printMemorySaved(data) {
|
|
235
|
+
session.saved++;
|
|
236
|
+
const isWatcher = (data.source || '').startsWith('watcher');
|
|
237
|
+
if (isWatcher) session.watcher++;
|
|
238
|
+
|
|
239
|
+
const tag = isWatcher
|
|
240
|
+
? bold(magenta('[WATCHER AUTO-SAVE]'))
|
|
241
|
+
: bold(green('[MEMORY SAVED] '));
|
|
242
|
+
const ns = data.namespace || 'shared';
|
|
243
|
+
const source = data.source || 'unknown';
|
|
244
|
+
const estTok = data.content ? Math.ceil(data.content.length / 4) : 0;
|
|
245
|
+
|
|
246
|
+
console.log(` ${tag} ${dim(timestamp())}`);
|
|
247
|
+
console.log(` ${bold('Memory ID:')} #${data.id}`);
|
|
248
|
+
console.log(` ${bold('Source:')} ${cyan(source)}`);
|
|
249
|
+
console.log(` ${bold('Namespace:')} ${ns}`);
|
|
250
|
+
if (data.content) {
|
|
251
|
+
console.log(` ${bold('Content:')} ${dim(truncate(data.content, 90))}`);
|
|
252
|
+
console.log(` ${bold('Est. Tokens:')} ~${estTok}`);
|
|
253
|
+
}
|
|
254
|
+
console.log(hr('-', 60));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function printMemoryDeleted(data) {
|
|
258
|
+
session.deleted++;
|
|
259
|
+
console.log(` ${bold(red('[MEMORY DELETED] '))} ${dim(timestamp())}`);
|
|
260
|
+
console.log(` ${bold('Memory ID:')} #${data.id}`);
|
|
261
|
+
console.log(` ${bold('Namespace:')} ${data.namespace || 'shared'}`);
|
|
262
|
+
console.log(hr('-', 60));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function printMemoryRetrieved(data) {
|
|
266
|
+
session.retrieved++;
|
|
267
|
+
const tool = data.tool || 'unknown';
|
|
268
|
+
const agent = data.agent_id || 'unknown';
|
|
269
|
+
const count = data.count ?? 0;
|
|
270
|
+
const query = data.query || '';
|
|
271
|
+
const ns = data.namespace || 'shared';
|
|
272
|
+
const ids = Array.isArray(data.memory_ids) ? data.memory_ids.join(', #') : '';
|
|
273
|
+
const hasBudget = data.token_budget !== undefined;
|
|
274
|
+
|
|
275
|
+
console.log(` ${bold(cyan('[MEMORY RETRIEVED] '))} ${dim(timestamp())}`);
|
|
276
|
+
console.log(` ${bold('Tool:')} ${cyan(tool)}`);
|
|
277
|
+
console.log(` ${bold('Agent:')} ${agent}`);
|
|
278
|
+
console.log(` ${bold('Query:')} ${dim(truncate(query, 70))}`);
|
|
279
|
+
console.log(` ${bold('Results:')} ${bold(green(String(count)))} memories injected`);
|
|
280
|
+
if (ids) {
|
|
281
|
+
console.log(` ${bold('Memory IDs:')} #${ids}`);
|
|
282
|
+
}
|
|
283
|
+
console.log(` ${bold('Namespace:')} ${ns}`);
|
|
284
|
+
if (hasBudget) {
|
|
285
|
+
console.log(` ${bold('Token Budget:')} ${data.token_budget.toLocaleString()}`);
|
|
286
|
+
}
|
|
287
|
+
console.log(hr('-', 60));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function printMemoryUpdated(data) {
|
|
291
|
+
session.updated++;
|
|
292
|
+
console.log(` ${bold(yellow('[MEMORY UPDATED] '))} ${dim(timestamp())}`);
|
|
293
|
+
console.log(` ${bold('Old ID:')} #${data.old_id} -> New ID: #${data.new_id}`);
|
|
294
|
+
console.log(` ${bold('Namespace:')} ${data.namespace || 'shared'}`);
|
|
295
|
+
console.log(hr('-', 60));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function printConsolidated(data) {
|
|
299
|
+
session.consolidated++;
|
|
300
|
+
console.log(` ${bold(blue('[CONSOLIDATION] '))} ${dim(timestamp())}`);
|
|
301
|
+
console.log(` ${bold('Groups merged:')} ${data.consolidated_groups ?? '?'}`);
|
|
302
|
+
if (data.details) {
|
|
303
|
+
console.log(` ${bold('Details:')} ${dim(truncate(JSON.stringify(data.details), 80))}`);
|
|
304
|
+
}
|
|
305
|
+
console.log(hr('-', 60));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ============================================================
|
|
309
|
+
// SSE PARSER (raw http — no third-party dependency needed)
|
|
310
|
+
// ============================================================
|
|
311
|
+
|
|
312
|
+
function connectSSE(onEvent, onError, onConnected) {
|
|
313
|
+
const req = http.get({
|
|
314
|
+
hostname: HOST,
|
|
315
|
+
port: PORT,
|
|
316
|
+
path: '/events',
|
|
317
|
+
headers: { 'Accept': 'text/event-stream', 'Cache-Control': 'no-cache' }
|
|
318
|
+
}, (res) => {
|
|
319
|
+
if (res.statusCode !== 200) {
|
|
320
|
+
onError(new Error(`SSE returned HTTP ${res.statusCode}`));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
onConnected();
|
|
325
|
+
|
|
326
|
+
let buffer = '';
|
|
327
|
+
let curEvent = '';
|
|
328
|
+
let curData = '';
|
|
329
|
+
|
|
330
|
+
res.setEncoding('utf8');
|
|
331
|
+
res.on('data', (chunk) => {
|
|
332
|
+
buffer += chunk;
|
|
333
|
+
const lines = buffer.split('\n');
|
|
334
|
+
buffer = lines.pop(); // keep incomplete last line
|
|
335
|
+
|
|
336
|
+
for (const line of lines) {
|
|
337
|
+
const t = line.trimEnd();
|
|
338
|
+
if (t === '') {
|
|
339
|
+
// dispatch
|
|
340
|
+
if (curData) onEvent(curEvent || 'message', curData);
|
|
341
|
+
curEvent = '';
|
|
342
|
+
curData = '';
|
|
343
|
+
} else if (t.startsWith('event:')) {
|
|
344
|
+
curEvent = t.slice(6).trim();
|
|
345
|
+
} else if (t.startsWith('data:')) {
|
|
346
|
+
curData = t.slice(5).trim();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
res.on('end', () => onError(new Error('SSE stream closed by server')));
|
|
352
|
+
res.on('error', onError);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
req.on('error', onError);
|
|
356
|
+
req.setTimeout(0); // no timeout — persistent connection
|
|
357
|
+
return req;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ============================================================
|
|
361
|
+
// STATS POLLER
|
|
362
|
+
// ============================================================
|
|
363
|
+
|
|
364
|
+
async function pollStats() {
|
|
365
|
+
try {
|
|
366
|
+
const [health, stats] = await Promise.all([get('/health'), get('/stats')]);
|
|
367
|
+
printStatsPanel(health, stats);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
console.log(` ${bold(yellow('[WARN]'))} Stats poll failed: ${dim(err.message)}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ============================================================
|
|
374
|
+
// CONTEXT SNAPSHOT MODE (--context flag)
|
|
375
|
+
// ============================================================
|
|
376
|
+
|
|
377
|
+
async function runContextSnapshot() {
|
|
378
|
+
console.log('');
|
|
379
|
+
console.log(bold(cyan(' PERSYST CONTEXT SNAPSHOT')));
|
|
380
|
+
console.log(hr('='));
|
|
381
|
+
try {
|
|
382
|
+
const [health, stats] = await Promise.all([get('/health'), get('/stats')]);
|
|
383
|
+
printStatsPanel(health, stats);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
console.log(` ${red('[ERROR]')} Cannot reach Persyst server at ${BASE_URL}`);
|
|
386
|
+
console.log(` ${dim('Make sure persyst-mcp is running: npx persyst-mcp')}`);
|
|
387
|
+
console.log(` ${dim('Error: ' + err.message)}`);
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
process.exit(0);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ============================================================
|
|
394
|
+
// RECONNECT LOGIC
|
|
395
|
+
// ============================================================
|
|
396
|
+
|
|
397
|
+
let reconnectDelay = 2000;
|
|
398
|
+
let statsInterval = null;
|
|
399
|
+
|
|
400
|
+
function reconnect() {
|
|
401
|
+
session.connected = false;
|
|
402
|
+
if (statsInterval) { clearInterval(statsInterval); statsInterval = null; }
|
|
403
|
+
reconnectDelay = Math.min(reconnectDelay * 1.5, 30000);
|
|
404
|
+
const delaySec = Math.round(reconnectDelay / 1000);
|
|
405
|
+
console.log(` ${yellow('[RECONNECT]')} Retrying in ${delaySec}s...`);
|
|
406
|
+
setTimeout(startMonitor, reconnectDelay);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function startMonitor() {
|
|
410
|
+
connectSSE(
|
|
411
|
+
// onEvent
|
|
412
|
+
(eventName, rawData) => {
|
|
413
|
+
let data = {};
|
|
414
|
+
try { data = JSON.parse(rawData); } catch (_) {}
|
|
415
|
+
|
|
416
|
+
switch (eventName) {
|
|
417
|
+
case 'connected': break; // handled in onConnected
|
|
418
|
+
case 'memory_added': printMemorySaved(data); break;
|
|
419
|
+
case 'memory_retrieved': printMemoryRetrieved(data); break;
|
|
420
|
+
case 'memory_deleted': printMemoryDeleted(data); break;
|
|
421
|
+
case 'memory_updated': printMemoryUpdated(data); break;
|
|
422
|
+
case 'memories_consolidated': printConsolidated(data); break;
|
|
423
|
+
default:
|
|
424
|
+
if (eventName !== 'message') {
|
|
425
|
+
console.log(` ${dim('[EVENT]')} ${eventName}: ${dim(truncate(rawData, 60))}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
// onError
|
|
431
|
+
(err) => {
|
|
432
|
+
console.log('');
|
|
433
|
+
console.log(` ${red('[DISCONNECTED]')} ${err.message}`);
|
|
434
|
+
reconnect();
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
// onConnected
|
|
438
|
+
async () => {
|
|
439
|
+
reconnectDelay = 2000;
|
|
440
|
+
session.connected = true;
|
|
441
|
+
|
|
442
|
+
let health = null;
|
|
443
|
+
try { health = await get('/health'); } catch (_) {}
|
|
444
|
+
printBanner(health);
|
|
445
|
+
|
|
446
|
+
await pollStats();
|
|
447
|
+
|
|
448
|
+
if (statsInterval) clearInterval(statsInterval);
|
|
449
|
+
statsInterval = setInterval(pollStats, 10000);
|
|
450
|
+
|
|
451
|
+
console.log(` ${dim('Listening for real-time events... Press Ctrl+C to stop')}`);
|
|
452
|
+
console.log('');
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ============================================================
|
|
458
|
+
// GRACEFUL SHUTDOWN
|
|
459
|
+
// ============================================================
|
|
460
|
+
|
|
461
|
+
process.on('SIGINT', () => {
|
|
462
|
+
const elapsed = Math.floor((Date.now() - session.startTime) / 1000);
|
|
463
|
+
console.log('');
|
|
464
|
+
console.log(hr('='));
|
|
465
|
+
console.log(` ${bold(cyan('MONITOR SESSION SUMMARY'))}`);
|
|
466
|
+
console.log(` Session duration: ${formatUptime(elapsed)}`);
|
|
467
|
+
console.log(` Memories saved: ${bold(String(session.saved))}`);
|
|
468
|
+
console.log(` Memories retrieved: ${bold(String(session.retrieved))}`);
|
|
469
|
+
console.log(` Memories updated: ${bold(String(session.updated))}`);
|
|
470
|
+
console.log(` Memories deleted: ${bold(String(session.deleted))}`);
|
|
471
|
+
console.log(` Watcher captures: ${bold(String(session.watcher))}`);
|
|
472
|
+
console.log(` Consolidations: ${bold(String(session.consolidated))}`);
|
|
473
|
+
console.log(hr('='));
|
|
474
|
+
console.log('');
|
|
475
|
+
if (statsInterval) clearInterval(statsInterval);
|
|
476
|
+
process.exit(0);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// ============================================================
|
|
480
|
+
// ENTRY POINT
|
|
481
|
+
// ============================================================
|
|
482
|
+
|
|
483
|
+
async function main() {
|
|
484
|
+
console.log('');
|
|
485
|
+
console.log(` ${bold('Persyst Monitor')} — connecting to ${cyan(BASE_URL)}...`);
|
|
486
|
+
|
|
487
|
+
if (CONTEXT_ONLY) {
|
|
488
|
+
await runContextSnapshot();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Verify server is reachable before entering SSE loop
|
|
493
|
+
try {
|
|
494
|
+
await get('/health');
|
|
495
|
+
} catch (err) {
|
|
496
|
+
console.log('');
|
|
497
|
+
console.log(` ${red('[ERROR]')} Cannot reach Persyst server at ${cyan(BASE_URL)}`);
|
|
498
|
+
console.log('');
|
|
499
|
+
console.log(` ${bold('Start the server first:')}`);
|
|
500
|
+
console.log(` npx persyst-mcp`);
|
|
501
|
+
console.log(` node index.js`);
|
|
502
|
+
console.log('');
|
|
503
|
+
console.log(` ${dim('Error: ' + err.message)}`);
|
|
504
|
+
console.log('');
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
startMonitor();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
main();
|
package/bin/setup.js
CHANGED
|
@@ -125,19 +125,19 @@ function mergeHookSettings(existing) {
|
|
|
125
125
|
|
|
126
126
|
function run() {
|
|
127
127
|
console.log('');
|
|
128
|
-
console.log('
|
|
128
|
+
console.log(' Persyst — Claude Code Hook Setup');
|
|
129
129
|
console.log(' ════════════════════════════════════');
|
|
130
130
|
console.log('');
|
|
131
131
|
|
|
132
132
|
// Step 1: Verify hook source exists
|
|
133
133
|
if (!existsSync(HOOK_SOURCE)) {
|
|
134
|
-
console.error(`
|
|
135
|
-
console.error('
|
|
134
|
+
console.error(` [ERROR] Hook source not found at: ${HOOK_SOURCE}`);
|
|
135
|
+
console.error(' Make sure you are running this from the persyst-mcp package.');
|
|
136
136
|
process.exit(1);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// Step 2: Copy and template hook file to ~/.persyst/hooks/
|
|
140
|
-
console.log('
|
|
140
|
+
console.log(' [1/2] Installing and templating hook script...');
|
|
141
141
|
ensureDir(PERSYST_HOOKS_DIR);
|
|
142
142
|
const INDEX_PATH = resolve(__dirname, '..', 'index.js');
|
|
143
143
|
const WORKER_PATH = resolve(__dirname, '..', 'bin', 'extract-worker.js');
|
|
@@ -145,30 +145,30 @@ function run() {
|
|
|
145
145
|
hookContent = hookContent.replace('{{PERSYST_INDEX_PATH}}', INDEX_PATH.replace(/\\/g, '/'));
|
|
146
146
|
hookContent = hookContent.replace('{{PERSYST_WORKER_PATH}}', WORKER_PATH.replace(/\\/g, '/'));
|
|
147
147
|
writeFileSync(HOOK_DEST, hookContent, 'utf8');
|
|
148
|
-
console.log(`
|
|
148
|
+
console.log(` [OK] Copied & templated to ${HOOK_DEST}`);
|
|
149
149
|
|
|
150
150
|
// Step 3: Merge into ~/.claude/settings.json
|
|
151
151
|
console.log('');
|
|
152
|
-
console.log('
|
|
152
|
+
console.log(' [2/2] Configuring Claude Code...');
|
|
153
153
|
ensureDir(CLAUDE_DIR);
|
|
154
154
|
|
|
155
155
|
const existingSettings = readJsonFile(CLAUDE_SETTINGS);
|
|
156
156
|
const mergedSettings = mergeHookSettings(existingSettings);
|
|
157
157
|
|
|
158
158
|
writeFileSync(CLAUDE_SETTINGS, JSON.stringify(mergedSettings, null, 2) + '\n', 'utf8');
|
|
159
|
-
console.log(`
|
|
159
|
+
console.log(` [OK] Updated ${CLAUDE_SETTINGS}`);
|
|
160
160
|
|
|
161
161
|
// Step 4: Print success
|
|
162
162
|
console.log('');
|
|
163
163
|
console.log(' ════════════════════════════════════');
|
|
164
|
-
console.log('
|
|
164
|
+
console.log(' [OK] Setup complete!');
|
|
165
165
|
console.log('');
|
|
166
166
|
console.log(' Persyst will now automatically:');
|
|
167
167
|
console.log(' • Load your stored memories when Claude Code starts');
|
|
168
168
|
console.log(' • Search for relevant context on every prompt');
|
|
169
169
|
console.log(' • Index your git commits into the memory database');
|
|
170
170
|
console.log('');
|
|
171
|
-
console.log('
|
|
171
|
+
console.log(' [INFO] Restart Claude Code to activate the hooks.');
|
|
172
172
|
console.log('');
|
|
173
173
|
console.log(' Memory database: ~/.persyst/persyst.db');
|
|
174
174
|
console.log(' Hook script: ~/.persyst/hooks/persyst-hook.js');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "persyst-mcp",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.7",
|
|
4
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",
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"bin": {
|
|
15
|
-
"persyst-mcp": "
|
|
16
|
-
"persyst-setup": "
|
|
17
|
-
"persyst-init": "
|
|
18
|
-
"persyst": "
|
|
15
|
+
"persyst-mcp": "index.js",
|
|
16
|
+
"persyst-setup": "bin/setup.js",
|
|
17
|
+
"persyst-init": "bin/init.js",
|
|
18
|
+
"persyst-monitor": "bin/monitor.js",
|
|
19
|
+
"persyst": "index.js"
|
|
19
20
|
},
|
|
20
21
|
"engines": {
|
|
21
22
|
"node": ">=18.0.0"
|
|
@@ -31,7 +32,10 @@
|
|
|
31
32
|
"scripts": {
|
|
32
33
|
"start": "node index.js",
|
|
33
34
|
"test": "cross-env NODE_ENV=test node test/smoke.js",
|
|
35
|
+
"test:stress": "node test/production_stress_suite.js",
|
|
34
36
|
"test:heavy": "node test/run_sequentially.js",
|
|
37
|
+
"monitor": "node bin/monitor.js",
|
|
38
|
+
"monitor:context": "node bin/monitor.js --context",
|
|
35
39
|
"worker": "node bin/extract-worker.js",
|
|
36
40
|
"extract": "node bin/extract.js"
|
|
37
41
|
},
|
|
@@ -54,12 +58,12 @@
|
|
|
54
58
|
"license": "MIT",
|
|
55
59
|
"repository": {
|
|
56
60
|
"type": "git",
|
|
57
|
-
"url": "git+https://github.com/ZayniBaloch/
|
|
61
|
+
"url": "git+https://github.com/ZayniBaloch/persyst.git"
|
|
58
62
|
},
|
|
59
63
|
"bugs": {
|
|
60
|
-
"url": "https://github.com/ZayniBaloch/
|
|
64
|
+
"url": "https://github.com/ZayniBaloch/persyst/issues"
|
|
61
65
|
},
|
|
62
|
-
"homepage": "https://github.com/ZayniBaloch/
|
|
66
|
+
"homepage": "https://github.com/ZayniBaloch/persyst#readme",
|
|
63
67
|
"dependencies": {
|
|
64
68
|
"@huggingface/transformers": "^4.2.0",
|
|
65
69
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
package/src/cache.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* - Full invalidation on write operations
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { logInfo } from './text-utils.js';
|
|
14
|
+
|
|
13
15
|
/**
|
|
14
16
|
* Simple LRU (Least Recently Used) cache with TTL support.
|
|
15
17
|
*/
|
|
@@ -97,7 +99,7 @@ export class LRUCache {
|
|
|
97
99
|
const size = this.cache.size;
|
|
98
100
|
this.cache.clear();
|
|
99
101
|
if (size > 0) {
|
|
100
|
-
|
|
102
|
+
logInfo(`[persyst-cache] Invalidated ${size} cached entries`);
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
|
package/src/database.js
CHANGED
|
@@ -17,6 +17,8 @@ import { join } from 'path';
|
|
|
17
17
|
import { homedir } from 'os';
|
|
18
18
|
import { mkdirSync } from 'fs';
|
|
19
19
|
|
|
20
|
+
import { logInfo } from './text-utils.js';
|
|
21
|
+
|
|
20
22
|
// ============================================================
|
|
21
23
|
// DATABASE LOCATION
|
|
22
24
|
// Store in ~/.persyst/ per default to persist across sessions
|
|
@@ -41,7 +43,7 @@ db.pragma('cache_size = -64000'); // 64MB cache size
|
|
|
41
43
|
// Load sqlite-vec BEFORE creating any vec0 tables
|
|
42
44
|
sqliteVec.load(db);
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
logInfo(`[persyst] Database: ${DB_PATH}`);
|
|
45
47
|
|
|
46
48
|
// ============================================================
|
|
47
49
|
// CREATE TABLES & SCHEMA MIGRATIONS
|
|
@@ -229,7 +231,7 @@ db.exec(`
|
|
|
229
231
|
)
|
|
230
232
|
`);
|
|
231
233
|
|
|
232
|
-
|
|
234
|
+
logInfo('[persyst] Schema initialized ✓');
|
|
233
235
|
|
|
234
236
|
// ============================================================
|
|
235
237
|
// PREPARED STATEMENTS
|
package/src/embeddings.js
CHANGED
|
@@ -19,6 +19,8 @@ env.useWasmCache = false;
|
|
|
19
19
|
// The embedding pipeline (lazy-loaded on first use)
|
|
20
20
|
let extractor = null;
|
|
21
21
|
|
|
22
|
+
import { logInfo } from './text-utils.js';
|
|
23
|
+
|
|
22
24
|
/**
|
|
23
25
|
* Load the embedding model. Called automatically on first use.
|
|
24
26
|
* First run downloads the model (~50MB). Subsequent runs use cache.
|
|
@@ -26,9 +28,9 @@ let extractor = null;
|
|
|
26
28
|
async function loadModel() {
|
|
27
29
|
if (extractor) return;
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
logInfo('[persyst] Loading embedding model (first run downloads ~50MB)...');
|
|
30
32
|
extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
|
|
31
|
-
|
|
33
|
+
logInfo('[persyst] Embedding model loaded ✓');
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|