persyst-mcp 2.2.5 → 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/index.js CHANGED
@@ -39,22 +39,42 @@ if (process.versions.bun && !process.env.PERSYST_RUN_BY_NODE) {
39
39
 
40
40
  // Fix PATH on Windows if running in environments like Qwen Desktop that override PATH
41
41
  if (process.platform === 'win32') {
42
- const nodeBin = 'C:\\Program Files\\nodejs';
43
- const gitBin = 'C:\\Program Files\\Git\\cmd';
44
- const systemBin = 'C:\\WINDOWS\\system32;C:\\WINDOWS';
45
-
46
42
  const currentPath = process.env.PATH || '';
47
43
  const paths = currentPath.split(';');
48
-
49
- if (!paths.includes(nodeBin)) paths.push(nodeBin);
50
- if (!paths.includes(gitBin)) paths.push(gitBin);
51
-
52
- // Make sure system folders are there
44
+
45
+ // Dynamically find node and git on PATH using where.exe
46
+ const { execFileSync } = await import('child_process');
47
+ for (const cmd of ['node', 'git']) {
48
+ try {
49
+ const result = execFileSync('where.exe', [cmd], { encoding: 'utf8', timeout: 2000 });
50
+ const binDir = result.trim().split('\r\n')[0].trim();
51
+ if (binDir) {
52
+ const dir = binDir.substring(0, binDir.lastIndexOf('\\'));
53
+ if (dir && !paths.some(p => p.toLowerCase() === dir.toLowerCase())) {
54
+ paths.push(dir);
55
+ }
56
+ }
57
+ } catch {
58
+ // where.exe failed — fall back to common paths
59
+ if (cmd === 'node') {
60
+ for (const p of ['C:\\Program Files\\nodejs', process.env.NVM_SYMLINK, `${process.env.USERPROFILE}\\AppData\\Roaming\\nvm\\v20.11.0`].filter(Boolean)) {
61
+ if (!paths.some(ex => ex.toLowerCase() === p.toLowerCase())) paths.push(p);
62
+ }
63
+ } else if (cmd === 'git') {
64
+ for (const p of ['C:\\Program Files\\Git\\cmd', 'C:\\Program Files\\Git\\bin', `${process.env.LOCALAPPDATA}\\Programs\\Git\\cmd`].filter(Boolean)) {
65
+ if (!paths.some(ex => ex.toLowerCase() === p.toLowerCase())) paths.push(p);
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ // Ensure system folders are present
72
+ const systemBin = 'C:\\WINDOWS\\system32;C:\\WINDOWS';
53
73
  const sysPaths = systemBin.split(';');
54
74
  sysPaths.forEach(p => {
55
- if (!paths.includes(p)) paths.push(p);
75
+ if (!paths.some(ex => ex.toLowerCase() === p.toLowerCase())) paths.push(p);
56
76
  });
57
-
77
+
58
78
  process.env.PATH = paths.join(';');
59
79
  }
60
80
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "persyst-mcp",
3
- "version": "2.2.5",
4
- "description": "Local-first MCP memory server with hybrid keyword + semantic search for coding agents",
3
+ "version": "2.2.7",
4
+ "description": "Local-first, compliance-grade MCP memory layer with hybrid keyword + semantic search for coding agents",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
@@ -14,13 +14,9 @@
14
14
  "bin": {
15
15
  "persyst-mcp": "index.js",
16
16
  "persyst-setup": "bin/setup.js",
17
- "persyst-aider": "bin/aider.js",
18
17
  "persyst-init": "bin/init.js",
19
- "persyst-ingest": "bin/ingest.js",
20
- "persyst-extract": "bin/extract.js",
21
- "persyst-worker": "bin/extract-worker.js",
22
- "persyst-export": "bin/export.js",
23
- "persyst-import": "bin/import.js"
18
+ "persyst-monitor": "bin/monitor.js",
19
+ "persyst": "index.js"
24
20
  },
25
21
  "engines": {
26
22
  "node": ">=18.0.0"
@@ -36,7 +32,10 @@
36
32
  "scripts": {
37
33
  "start": "node index.js",
38
34
  "test": "cross-env NODE_ENV=test node test/smoke.js",
35
+ "test:stress": "node test/production_stress_suite.js",
39
36
  "test:heavy": "node test/run_sequentially.js",
37
+ "monitor": "node bin/monitor.js",
38
+ "monitor:context": "node bin/monitor.js --context",
40
39
  "worker": "node bin/extract-worker.js",
41
40
  "extract": "node bin/extract.js"
42
41
  },
@@ -59,12 +58,12 @@
59
58
  "license": "MIT",
60
59
  "repository": {
61
60
  "type": "git",
62
- "url": "git+https://github.com/ZayniBaloch/Peryst.git"
61
+ "url": "git+https://github.com/ZayniBaloch/persyst.git"
63
62
  },
64
63
  "bugs": {
65
- "url": "https://github.com/ZayniBaloch/Peryst/issues"
64
+ "url": "https://github.com/ZayniBaloch/persyst/issues"
66
65
  },
67
- "homepage": "https://github.com/ZayniBaloch/Peryst#readme",
66
+ "homepage": "https://github.com/ZayniBaloch/persyst#readme",
68
67
  "dependencies": {
69
68
  "@huggingface/transformers": "^4.2.0",
70
69
  "@modelcontextprotocol/sdk": "^1.29.0",