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/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(' 🧠 Persyst — Claude Code Hook Setup');
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(` Hook source not found at: ${HOOK_SOURCE}`);
135
- console.error(' Make sure you are running this from the persyst-mcp package.');
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(' 📁 Installing and templating hook script...');
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(` Copied & templated to ${HOOK_DEST}`);
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(' ⚙️ Configuring Claude Code...');
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(` Updated ${CLAUDE_SETTINGS}`);
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(' Setup complete!');
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(' Restart Claude Code to activate the hooks.');
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.6",
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": "./bin/mcp.js",
16
- "persyst-setup": "./bin/setup.js",
17
- "persyst-init": "./bin/init.js",
18
- "persyst": "./index.js"
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/Peryst.git"
61
+ "url": "git+https://github.com/ZayniBaloch/persyst.git"
58
62
  },
59
63
  "bugs": {
60
- "url": "https://github.com/ZayniBaloch/Peryst/issues"
64
+ "url": "https://github.com/ZayniBaloch/persyst/issues"
61
65
  },
62
- "homepage": "https://github.com/ZayniBaloch/Peryst#readme",
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
- console.error(`[persyst-cache] Invalidated ${size} cached entries`);
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
- console.error(`[persyst] Database: ${DB_PATH}`);
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
- console.error('[persyst] Schema initialized ✓');
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
- console.error('[persyst] Loading embedding model (first run downloads ~50MB)...');
31
+ logInfo('[persyst] Loading embedding model (first run downloads ~50MB)...');
30
32
  extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
31
- console.error('[persyst] Embedding model loaded ✓');
33
+ logInfo('[persyst] Embedding model loaded ✓');
32
34
  }
33
35
 
34
36
  /**