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/src/setup-wasm.js CHANGED
@@ -9,62 +9,57 @@ const require = createRequire(import.meta.url);
9
9
  const onnxWebPath = require.resolve('onnxruntime-web');
10
10
  const wasmDir = path.dirname(onnxWebPath);
11
11
 
12
- // Redirect native Node session creation to WebAssembly session creation
13
12
  ONNX_NODE.InferenceSession.create = ONNX_WEB.InferenceSession.create;
14
13
 
15
- // Override URL.createObjectURL to return file URL of the existing local file
16
14
  const originalCreateObjectURL = URL.createObjectURL;
17
- URL.createObjectURL = (blob) => {
15
+ const patchedCreateObjectURL = (blob) => {
18
16
  const type = blob.type || '';
19
17
  if (type.includes('javascript') || type.includes('mjs')) {
20
18
  const filePath = path.join(wasmDir, 'ort-wasm-simd-threaded.asyncify.mjs');
21
- const fileUrl = pathToFileURL(filePath).href;
22
- return fileUrl;
19
+ return pathToFileURL(filePath).href;
23
20
  }
24
21
  return originalCreateObjectURL(blob);
25
22
  };
23
+ URL.createObjectURL = patchedCreateObjectURL;
24
+
25
+ function readLocalFile(filePath, urlStr) {
26
+ const normalized = path.normalize(filePath);
27
+ const buffer = fs.readFileSync(normalized);
28
+ let contentType = 'application/octet-stream';
29
+ if (normalized.endsWith('.wasm')) contentType = 'application/wasm';
30
+ else if (normalized.endsWith('.mjs') || normalized.endsWith('.js')) contentType = 'text/javascript';
31
+ else if (normalized.endsWith('.onnx') || normalized.endsWith('.ort')) contentType = 'application/octet-stream';
32
+ return new Response(buffer, {
33
+ status: 200,
34
+ statusText: 'OK',
35
+ headers: { 'Content-Type': contentType }
36
+ });
37
+ }
26
38
 
27
- // Override global fetch to load ONNX WASM binaries and model files from local disk
28
39
  const originalFetch = globalThis.fetch;
29
- globalThis.fetch = async (url, options) => {
40
+ const patchedFetch = async (url, options) => {
30
41
  const urlStr = typeof url === 'string' ? url : url.url;
31
-
32
- let isLocal = false;
33
- let filePath = '';
34
-
35
- if (urlStr.startsWith('file://')) {
36
- isLocal = true;
37
- filePath = fileURLToPath(urlStr);
38
- } else if (!urlStr.startsWith('http://') && !urlStr.startsWith('https://') && !urlStr.startsWith('data:')) {
39
- isLocal = true;
40
- filePath = urlStr;
41
- }
42
-
43
- // Intercept onnxruntime-web CDN URLs and route them to node_modules/onnxruntime-web/dist
42
+
43
+ // onnxruntime-web WASM binaries — resolve from node_modules
44
44
  if (urlStr.includes('onnxruntime-web') || urlStr.includes('ort-wasm')) {
45
- isLocal = true;
46
45
  const filename = urlStr.split('/').pop().split('?')[0].split('#')[0];
47
- filePath = path.join(wasmDir, filename);
46
+ return readLocalFile(path.join(wasmDir, filename), urlStr);
48
47
  }
49
-
50
- if (isLocal) {
51
- filePath = path.normalize(filePath);
48
+
49
+ // file:// URLs — Node.js fetch does not support them natively
50
+ if (urlStr.startsWith('file://')) {
51
+ return readLocalFile(fileURLToPath(urlStr), urlStr);
52
+ }
53
+
54
+ // fallback for any non-http/https/data URL (onnxruntime internal schemes, bare paths)
55
+ if (!urlStr.startsWith('http://') && !urlStr.startsWith('https://') && !urlStr.startsWith('data:')) {
52
56
  try {
53
- const buffer = fs.readFileSync(filePath);
54
- let contentType = 'application/octet-stream';
55
- if (filePath.endsWith('.wasm')) contentType = 'application/wasm';
56
- else if (filePath.endsWith('.mjs') || filePath.endsWith('.js')) contentType = 'text/javascript';
57
-
58
- return new Response(buffer, {
59
- status: 200,
60
- statusText: 'OK',
61
- headers: { 'Content-Type': contentType }
62
- });
63
- } catch (err) {
64
- console.error('[persyst] Failed to read local file:', filePath, err.message);
65
- throw err;
57
+ return readLocalFile(urlStr, urlStr);
58
+ } catch (e) {
59
+ throw e;
66
60
  }
67
61
  }
68
-
62
+
69
63
  return originalFetch(url, options);
70
64
  };
65
+ globalThis.fetch = patchedFetch;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * text-utils.js — Shared text-processing helpers used across Persyst.
3
+ *
4
+ * Keeping these in one place avoids duplicated logic and divergent behavior
5
+ * between modules.
6
+ */
7
+
8
+ /**
9
+ * Compute Jaccard similarity between two text strings.
10
+ * Uses word-level tokenization for efficiency.
11
+ *
12
+ * @param {string} a - First text
13
+ * @param {string} b - Second text
14
+ * @returns {number} Similarity score between 0 and 1
15
+ */
16
+ export function jaccardSimilarity(a, b) {
17
+ if (!a || !b) return 0;
18
+
19
+ const wordsA = new Set(a.toLowerCase().split(/\s+/));
20
+ const wordsB = new Set(b.toLowerCase().split(/\s+/));
21
+
22
+ let intersection = 0;
23
+ for (const word of wordsA) {
24
+ if (wordsB.has(word)) intersection++;
25
+ }
26
+
27
+ const union = wordsA.size + wordsB.size - intersection;
28
+ return union === 0 ? 0 : intersection / union;
29
+ }
30
+
31
+ /**
32
+ * Compute Jaccard distance between two text strings.
33
+ * Distance = 1 - similarity, so 0 means identical and 1 means completely different.
34
+ *
35
+ * @param {string} a - First text
36
+ * @param {string} b - Second text
37
+ * @returns {number} Distance score between 0 and 1
38
+ */
39
+ export function jaccardDistance(a, b) {
40
+ return 1 - jaccardSimilarity(a, b);
41
+ }
42
+
43
+ /**
44
+ * Log informational messages to stderr only when PERSYST_DEBUG or DEBUG is enabled.
45
+ * Prevents MCP hosts (Cursor, Antigravity, VS Code) from treating startup info logs as MCP errors.
46
+ */
47
+ export function logInfo(...args) {
48
+ if (process.env.PERSYST_DEBUG || process.env.DEBUG) {
49
+ console.error(...args);
50
+ }
51
+ }
52
+
package/src/tools.js CHANGED
@@ -14,8 +14,10 @@
14
14
  import { z } from 'zod';
15
15
  import { generateEmbedding } from './embeddings.js';
16
16
  import db, {
17
+ stmts,
17
18
  insertMemory,
18
19
  insertVector,
20
+ redactSecrets,
19
21
  getMemory,
20
22
  updateMemoryContent,
21
23
  deleteMemory,
@@ -45,6 +47,7 @@ import db, {
45
47
  getNamespaceStats
46
48
  } from './database.js';
47
49
  import { searchHybrid, getOptimizedContext, consolidateMemories } from './search.js';
50
+ import { jaccardDistance } from './text-utils.js';
48
51
  import { getRecentCommits } from './git.js';
49
52
  import { verifyChainIntegrity } from './attestation.js';
50
53
  import { searchCache } from './cache.js';
@@ -109,24 +112,28 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
109
112
  try {
110
113
  const normalizedAgentId = agent_id ? agent_id.toLowerCase() : null;
111
114
 
115
+ // Redact secrets/credentials on write
116
+ const redactedContent = redactSecrets(content);
117
+
112
118
  // Bug 7 + Feature 4: Validate content size
113
- const validation = validateMemoryContent(content);
119
+ const validation = validateMemoryContent(redactedContent);
114
120
  if (!validation.valid) {
115
121
  return { error: validation.error };
116
122
  }
117
123
 
118
- // Derive namespace from agent_id and shared flag
119
- const namespace = (shared || !normalizedAgentId) ? 'shared' : normalizedAgentId;
124
+ // Derive namespace from agent_id, project env, and shared flag
125
+ const project = process.env.PERSYST_PROJECT;
126
+ const defaultNs = project || 'shared';
127
+ const namespace = (shared && !project) ? 'shared' : (normalizedAgentId || defaultNs);
120
128
 
121
129
  // Deduplication check (namespace-aware)
122
- const existing = getMemoryByContent(content, namespace);
130
+ const existing = getMemoryByContent(redactedContent, namespace);
123
131
  if (existing) {
124
132
  // Re-attribute provenance to the calling agent if it was previously auto-attributed to log-watcher
125
133
  const prov = getProvenance(existing.id);
126
134
  if (prov && (prov.source_id === 'antigravity-worker' || prov.source_id === 'user-dialogue') && normalizedAgentId) {
127
135
  try {
128
- db.prepare("UPDATE provenance SET source_type = 'agent', source_id = ?, confidence = 1.0 WHERE memory_id = ?")
129
- .run(normalizedAgentId, existing.id);
136
+ stmts.updateProvenanceOwner.run(normalizedAgentId, existing.id);
130
137
  incrementAgentStat(normalizedAgentId, 'created');
131
138
  } catch (e) {
132
139
  console.error(`[persyst] Re-attribute provenance error: ${e.message}`);
@@ -141,20 +148,20 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
141
148
  };
142
149
  }
143
150
 
144
- const id = insertMemory(content, importance, {
151
+ const id = insertMemory(redactedContent, importance, {
145
152
  source_type: normalizedAgentId ? 'agent' : 'manual',
146
153
  source_id: normalizedAgentId,
147
154
  confidence: 1.0
148
155
  }, namespace);
149
156
 
150
- const embedding = await generateEmbedding(content);
157
+ const embedding = await generateEmbedding(redactedContent);
151
158
  insertVector(id, embedding);
152
159
 
153
160
  // Feature 1: Invalidate search cache on write
154
161
  searchCache.invalidate();
155
162
 
156
163
  // Broadcast to SSE subscribers (HTTP gateway + SSE clients)
157
- memoryEventBus.emit('memory_added', { id, content, namespace, source: normalizedAgentId || 'manual' });
164
+ memoryEventBus.emit('memory_added', { id, content: redactedContent, namespace, source: normalizedAgentId || 'manual' });
158
165
 
159
166
  // Feature 2: Contradiction Detection
160
167
  let contradictions = [];
@@ -169,20 +176,20 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
169
176
  const existingMemory = getMemoryById(hitId, namespace);
170
177
  if (!existingMemory) continue;
171
178
 
172
- const jaccard = jaccardDistance(content, existingMemory.content);
179
+ const jaccard = jaccardDistance(redactedContent, existingMemory.content);
173
180
  // Contradiction: similar topic (high similarity), but differing key terms
174
181
  if (jaccard > 0 && jaccard < 0.65) {
175
182
  // Fetch provenances for trust calculation
176
183
  const oldProv = getProvenance(hitId);
177
184
  let oldReputation = 1.0;
178
185
  if (oldProv && oldProv.source_type === 'agent' && oldProv.source_id) {
179
- const agentRow = db.prepare('SELECT reputation_score FROM agent_stats WHERE agent_id = ?').get(oldProv.source_id);
186
+ const agentRow = stmts.getReputationScore.get(oldProv.source_id);
180
187
  if (agentRow) oldReputation = agentRow.reputation_score;
181
188
  }
182
189
 
183
190
  let newReputation = 1.0;
184
191
  if (normalizedAgentId) {
185
- const agentRow = db.prepare('SELECT reputation_score FROM agent_stats WHERE agent_id = ?').get(normalizedAgentId);
192
+ const agentRow = stmts.getReputationScore.get(normalizedAgentId);
186
193
  if (agentRow) newReputation = agentRow.reputation_score;
187
194
  }
188
195
 
@@ -191,7 +198,11 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
191
198
 
192
199
  const isSelfUpdate = oldProv && oldProv.source_type === 'agent' && oldProv.source_id === normalizedAgentId;
193
200
 
194
- if (isSelfUpdate || trustNew > trustOld) {
201
+ if (isSelfUpdate) {
202
+ continue; // Same agent: treat as complementary, not contradictory
203
+ }
204
+
205
+ if (trustNew > trustOld) {
195
206
  // New is preferred
196
207
  logContradiction(hitId, id, `Auto-detected contradiction: new memory is more trustworthy (similarity: ${sim.toFixed(3)}, content_diff: ${jaccard.toFixed(3)})`);
197
208
  contradictions.push({
@@ -300,9 +311,22 @@ export function registerTools(server) {
300
311
  },
301
312
  async ({ query, limit, agent_id, session_id }) => {
302
313
  try {
303
- // Derive namespace from agent_id (null = search all)
304
- const namespace = agent_id || null;
314
+ // Derive namespace from agent_id or PERSYST_PROJECT env
315
+ const namespace = agent_id || process.env.PERSYST_PROJECT || null;
305
316
  const results = await searchHybrid(query, limit, agent_id, session_id, namespace);
317
+
318
+ // Broadcast retrieval event to SSE subscribers and monitor
319
+ if (results && results.length > 0) {
320
+ memoryEventBus.emit('memory_retrieved', {
321
+ tool: 'search_memories',
322
+ query,
323
+ count: results.length,
324
+ agent_id: agent_id || 'unknown',
325
+ namespace: namespace || 'shared',
326
+ memory_ids: results.map(r => r.id)
327
+ });
328
+ }
329
+
306
330
  return text({
307
331
  results,
308
332
  count: results.length,
@@ -320,12 +344,25 @@ export function registerTools(server) {
320
344
  'get_memory',
321
345
  'Get a specific memory by its ID. Boosts its importance automatically.',
322
346
  {
323
- id: z.number().describe('Memory ID to retrieve')
347
+ id: z.number().describe('Memory ID to retrieve'),
348
+ agent_id: z.string().optional().describe('Agent ID — restricts access to this agent\'s namespace + shared')
324
349
  },
325
- async ({ id }) => {
350
+ async ({ id, agent_id }) => {
326
351
  try {
327
- const memory = getMemory(id);
352
+ const namespace = agent_id ? agent_id.toLowerCase() : null;
353
+ const memory = getMemory(id, namespace);
328
354
  if (!memory) return text({ error: `Memory #${id} not found` });
355
+
356
+ // Broadcast retrieval event
357
+ memoryEventBus.emit('memory_retrieved', {
358
+ tool: 'get_memory',
359
+ query: `#${id}`,
360
+ count: 1,
361
+ agent_id: agent_id || 'unknown',
362
+ namespace: memory.namespace || 'shared',
363
+ memory_ids: [id]
364
+ });
365
+
329
366
  return text(memory);
330
367
  } catch (err) {
331
368
  return text({ error: err.message });
@@ -346,13 +383,17 @@ export function registerTools(server) {
346
383
  try {
347
384
  const normalizedAgentId = agent_id ? agent_id.toLowerCase() : null;
348
385
 
386
+ // Redact secrets/credentials on update
387
+ const redactedContent = redactSecrets(content);
388
+
349
389
  // Bug 7 + Feature 4: Validate content size
350
- const validation = validateMemoryContent(content);
390
+ const validation = validateMemoryContent(redactedContent);
351
391
  if (!validation.valid) {
352
392
  return text({ error: validation.error });
353
393
  }
354
394
 
355
- const oldMemory = getMemory(id);
395
+ const namespace = normalizedAgentId;
396
+ const oldMemory = getMemory(id, namespace);
356
397
  if (!oldMemory) return text({ error: `Memory #${id} not found` });
357
398
 
358
399
  // Retrieve old agent_id from provenance
@@ -361,7 +402,7 @@ export function registerTools(server) {
361
402
 
362
403
  // Insert new version
363
404
  const newId = insertMemory(
364
- content,
405
+ redactedContent,
365
406
  oldMemory.importance_score,
366
407
  {
367
408
  source_type: resolvedAgentId ? 'agent' : 'manual',
@@ -372,7 +413,7 @@ export function registerTools(server) {
372
413
  id
373
414
  );
374
415
 
375
- const embedding = await generateEmbedding(content);
416
+ const embedding = await generateEmbedding(redactedContent);
376
417
  insertVector(newId, embedding);
377
418
 
378
419
  // Record contradiction and archive the old one
@@ -381,6 +422,9 @@ export function registerTools(server) {
381
422
  // Feature 1: Invalidate search cache on write
382
423
  searchCache.invalidate();
383
424
 
425
+ // Broadcast update to SSE subscribers
426
+ memoryEventBus.emit('memory_updated', { old_id: id, new_id: newId, namespace: oldMemory.namespace || 'shared' });
427
+
384
428
  return text({
385
429
  success: true,
386
430
  id: newId,
@@ -397,10 +441,15 @@ export function registerTools(server) {
397
441
  'delete_memory',
398
442
  'Permanently delete a memory by its ID.',
399
443
  {
400
- id: z.number().describe('Memory ID to delete')
444
+ id: z.number().describe('Memory ID to delete'),
445
+ agent_id: z.string().optional().describe('Agent ID — restricts deletion to this agent\'s namespace + shared')
401
446
  },
402
- async ({ id }) => {
447
+ async ({ id, agent_id }) => {
403
448
  try {
449
+ const namespace = agent_id ? agent_id.toLowerCase() : null;
450
+ const memory = getMemory(id, namespace);
451
+ if (!memory) return text({ error: `Memory #${id} not found` });
452
+
404
453
  const deleted = deleteMemory(id);
405
454
  if (!deleted) return text({ error: `Memory #${id} not found` });
406
455
 
@@ -408,7 +457,7 @@ export function registerTools(server) {
408
457
  searchCache.invalidate();
409
458
 
410
459
  // Broadcast deletion to SSE subscribers
411
- memoryEventBus.emit('memory_deleted', { id });
460
+ memoryEventBus.emit('memory_deleted', { id, namespace: memory.namespace || 'shared' });
412
461
 
413
462
  return text({ success: true, id, message: `Memory #${id} deleted` });
414
463
  } catch (err) {
@@ -427,7 +476,7 @@ export function registerTools(server) {
427
476
  },
428
477
  async ({ limit, agent_id }) => {
429
478
  try {
430
- const namespace = agent_id || null;
479
+ const namespace = agent_id || process.env.PERSYST_PROJECT || null;
431
480
  const memories = getRecentMemories(limit, namespace);
432
481
  return text({ memories, count: memories.length, namespace: namespace || 'all' });
433
482
  } catch (err) {
@@ -446,7 +495,7 @@ export function registerTools(server) {
446
495
  },
447
496
  async ({ limit, agent_id }) => {
448
497
  try {
449
- const namespace = agent_id || null;
498
+ const namespace = agent_id || process.env.PERSYST_PROJECT || null;
450
499
  const memories = getImportantMemories(limit, namespace);
451
500
  return text({ memories, count: memories.length, namespace: namespace || 'all' });
452
501
  } catch (err) {
@@ -482,7 +531,7 @@ export function registerTools(server) {
482
531
  source_type: 'git',
483
532
  source_id: commit.hash,
484
533
  confidence: 0.8
485
- });
534
+ }, process.env.PERSYST_PROJECT || 'shared');
486
535
 
487
536
  const embedding = await generateEmbedding(commit.fullText);
488
537
  insertVector(id, embedding);
@@ -545,14 +594,16 @@ export function registerTools(server) {
545
594
  {
546
595
  entity_name: z.string().describe('Name of the entity'),
547
596
  memory_id: z.number().describe('ID of the memory to link'),
548
- relation: z.string().default('mentions').describe('Relationship type')
597
+ relation: z.string().default('mentions').describe('Relationship type'),
598
+ agent_id: z.string().optional().describe('Agent ID — restricts linking to this agent\'s namespace + shared')
549
599
  },
550
- async ({ entity_name, memory_id, relation }) => {
600
+ async ({ entity_name, memory_id, relation, agent_id }) => {
551
601
  try {
602
+ const namespace = agent_id ? agent_id.toLowerCase() : null;
552
603
  const entity = getEntityByName(entity_name);
553
604
  if (!entity) return text({ error: `Entity "${entity_name}" not found.` });
554
605
 
555
- const memory = getMemory(memory_id);
606
+ const memory = getMemory(memory_id, namespace);
556
607
  if (!memory) return text({ error: `Memory #${memory_id} not found` });
557
608
 
558
609
  insertEdge(entity.id, memory_id, relation, 'entity', 'memory');
@@ -790,8 +841,23 @@ export function registerTools(server) {
790
841
  },
791
842
  async ({ query, max_tokens, agent_id, session_id, intent }) => {
792
843
  try {
793
- const namespace = agent_id || null;
844
+ const namespace = agent_id || process.env.PERSYST_PROJECT || null;
794
845
  const contextData = await getOptimizedContext(query, max_tokens, agent_id, session_id, namespace, intent);
846
+
847
+ // Broadcast context retrieval event
848
+ const retrievedCount = contextData?.memories?.length ?? 0;
849
+ if (retrievedCount > 0) {
850
+ memoryEventBus.emit('memory_retrieved', {
851
+ tool: 'get_optimized_context',
852
+ query,
853
+ count: retrievedCount,
854
+ agent_id: agent_id || 'unknown',
855
+ namespace: namespace || 'shared',
856
+ token_budget: max_tokens,
857
+ memory_ids: contextData.memories.map(m => m.id)
858
+ });
859
+ }
860
+
795
861
  return text(contextData);
796
862
  } catch (err) {
797
863
  return text({ error: err.message });
@@ -830,27 +896,6 @@ function text(data) {
830
896
  };
831
897
  }
832
898
 
833
- /**
834
- * Compute Jaccard distance between two text strings.
835
- * Used for contradiction detection — higher distance means more different content.
836
- * @param {string} a - First text
837
- * @param {string} b - Second text
838
- * @returns {number} Distance score between 0 (identical) and 1 (completely different)
839
- */
840
- function jaccardDistance(a, b) {
841
- const wordsA = new Set(a.toLowerCase().split(/\s+/));
842
- const wordsB = new Set(b.toLowerCase().split(/\s+/));
843
-
844
- let intersection = 0;
845
- for (const word of wordsA) {
846
- if (wordsB.has(word)) intersection++;
847
- }
848
-
849
- const union = wordsA.size + wordsB.size - intersection;
850
- if (union === 0) return 0;
851
- return 1 - (intersection / union);
852
- }
853
-
854
899
  /**
855
900
  * Compute word-level diff between two text strings using dynamic programming.
856
901
  * Highlights additions as [+added+] and deletions as [-deleted-].