persyst-mcp 2.2.4 → 2.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,41 @@
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
+ }
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,9 +47,11 @@ 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';
54
+ import { memoryEventBus } from './events.js';
51
55
 
52
56
  // ============================================================
53
57
  // CONSTANTS
@@ -108,8 +112,11 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
108
112
  try {
109
113
  const normalizedAgentId = agent_id ? agent_id.toLowerCase() : null;
110
114
 
115
+ // Redact secrets/credentials on write
116
+ const redactedContent = redactSecrets(content);
117
+
111
118
  // Bug 7 + Feature 4: Validate content size
112
- const validation = validateMemoryContent(content);
119
+ const validation = validateMemoryContent(redactedContent);
113
120
  if (!validation.valid) {
114
121
  return { error: validation.error };
115
122
  }
@@ -118,14 +125,13 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
118
125
  const namespace = (shared || !normalizedAgentId) ? 'shared' : normalizedAgentId;
119
126
 
120
127
  // Deduplication check (namespace-aware)
121
- const existing = getMemoryByContent(content, namespace);
128
+ const existing = getMemoryByContent(redactedContent, namespace);
122
129
  if (existing) {
123
130
  // Re-attribute provenance to the calling agent if it was previously auto-attributed to log-watcher
124
131
  const prov = getProvenance(existing.id);
125
132
  if (prov && (prov.source_id === 'antigravity-worker' || prov.source_id === 'user-dialogue') && normalizedAgentId) {
126
133
  try {
127
- db.prepare("UPDATE provenance SET source_type = 'agent', source_id = ?, confidence = 1.0 WHERE memory_id = ?")
128
- .run(normalizedAgentId, existing.id);
134
+ stmts.updateProvenanceOwner.run(normalizedAgentId, existing.id);
129
135
  incrementAgentStat(normalizedAgentId, 'created');
130
136
  } catch (e) {
131
137
  console.error(`[persyst] Re-attribute provenance error: ${e.message}`);
@@ -140,18 +146,21 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
140
146
  };
141
147
  }
142
148
 
143
- const id = insertMemory(content, importance, {
149
+ const id = insertMemory(redactedContent, importance, {
144
150
  source_type: normalizedAgentId ? 'agent' : 'manual',
145
151
  source_id: normalizedAgentId,
146
152
  confidence: 1.0
147
153
  }, namespace);
148
154
 
149
- const embedding = await generateEmbedding(content);
155
+ const embedding = await generateEmbedding(redactedContent);
150
156
  insertVector(id, embedding);
151
157
 
152
158
  // Feature 1: Invalidate search cache on write
153
159
  searchCache.invalidate();
154
160
 
161
+ // Broadcast to SSE subscribers (HTTP gateway + SSE clients)
162
+ memoryEventBus.emit('memory_added', { id, content: redactedContent, namespace, source: normalizedAgentId || 'manual' });
163
+
155
164
  // Feature 2: Contradiction Detection
156
165
  let contradictions = [];
157
166
  try {
@@ -165,20 +174,20 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
165
174
  const existingMemory = getMemoryById(hitId, namespace);
166
175
  if (!existingMemory) continue;
167
176
 
168
- const jaccard = jaccardDistance(content, existingMemory.content);
177
+ const jaccard = jaccardDistance(redactedContent, existingMemory.content);
169
178
  // Contradiction: similar topic (high similarity), but differing key terms
170
179
  if (jaccard > 0 && jaccard < 0.65) {
171
180
  // Fetch provenances for trust calculation
172
181
  const oldProv = getProvenance(hitId);
173
182
  let oldReputation = 1.0;
174
183
  if (oldProv && oldProv.source_type === 'agent' && oldProv.source_id) {
175
- const agentRow = db.prepare('SELECT reputation_score FROM agent_stats WHERE agent_id = ?').get(oldProv.source_id);
184
+ const agentRow = stmts.getReputationScore.get(oldProv.source_id);
176
185
  if (agentRow) oldReputation = agentRow.reputation_score;
177
186
  }
178
187
 
179
188
  let newReputation = 1.0;
180
189
  if (normalizedAgentId) {
181
- const agentRow = db.prepare('SELECT reputation_score FROM agent_stats WHERE agent_id = ?').get(normalizedAgentId);
190
+ const agentRow = stmts.getReputationScore.get(normalizedAgentId);
182
191
  if (agentRow) newReputation = agentRow.reputation_score;
183
192
  }
184
193
 
@@ -187,7 +196,11 @@ export async function addMemoryInternal({ content, importance = 1.0, agent_id, s
187
196
 
188
197
  const isSelfUpdate = oldProv && oldProv.source_type === 'agent' && oldProv.source_id === normalizedAgentId;
189
198
 
190
- if (isSelfUpdate || trustNew > trustOld) {
199
+ if (isSelfUpdate) {
200
+ continue; // Same agent: treat as complementary, not contradictory
201
+ }
202
+
203
+ if (trustNew > trustOld) {
191
204
  // New is preferred
192
205
  logContradiction(hitId, id, `Auto-detected contradiction: new memory is more trustworthy (similarity: ${sim.toFixed(3)}, content_diff: ${jaccard.toFixed(3)})`);
193
206
  contradictions.push({
@@ -316,11 +329,13 @@ export function registerTools(server) {
316
329
  'get_memory',
317
330
  'Get a specific memory by its ID. Boosts its importance automatically.',
318
331
  {
319
- id: z.number().describe('Memory ID to retrieve')
332
+ id: z.number().describe('Memory ID to retrieve'),
333
+ agent_id: z.string().optional().describe('Agent ID — restricts access to this agent\'s namespace + shared')
320
334
  },
321
- async ({ id }) => {
335
+ async ({ id, agent_id }) => {
322
336
  try {
323
- const memory = getMemory(id);
337
+ const namespace = agent_id ? agent_id.toLowerCase() : null;
338
+ const memory = getMemory(id, namespace);
324
339
  if (!memory) return text({ error: `Memory #${id} not found` });
325
340
  return text(memory);
326
341
  } catch (err) {
@@ -342,13 +357,17 @@ export function registerTools(server) {
342
357
  try {
343
358
  const normalizedAgentId = agent_id ? agent_id.toLowerCase() : null;
344
359
 
360
+ // Redact secrets/credentials on update
361
+ const redactedContent = redactSecrets(content);
362
+
345
363
  // Bug 7 + Feature 4: Validate content size
346
- const validation = validateMemoryContent(content);
364
+ const validation = validateMemoryContent(redactedContent);
347
365
  if (!validation.valid) {
348
366
  return text({ error: validation.error });
349
367
  }
350
368
 
351
- const oldMemory = getMemory(id);
369
+ const namespace = normalizedAgentId;
370
+ const oldMemory = getMemory(id, namespace);
352
371
  if (!oldMemory) return text({ error: `Memory #${id} not found` });
353
372
 
354
373
  // Retrieve old agent_id from provenance
@@ -357,7 +376,7 @@ export function registerTools(server) {
357
376
 
358
377
  // Insert new version
359
378
  const newId = insertMemory(
360
- content,
379
+ redactedContent,
361
380
  oldMemory.importance_score,
362
381
  {
363
382
  source_type: resolvedAgentId ? 'agent' : 'manual',
@@ -368,7 +387,7 @@ export function registerTools(server) {
368
387
  id
369
388
  );
370
389
 
371
- const embedding = await generateEmbedding(content);
390
+ const embedding = await generateEmbedding(redactedContent);
372
391
  insertVector(newId, embedding);
373
392
 
374
393
  // Record contradiction and archive the old one
@@ -377,6 +396,9 @@ export function registerTools(server) {
377
396
  // Feature 1: Invalidate search cache on write
378
397
  searchCache.invalidate();
379
398
 
399
+ // Broadcast update to SSE subscribers
400
+ memoryEventBus.emit('memory_updated', { old_id: id, new_id: newId, namespace: oldMemory.namespace || 'shared' });
401
+
380
402
  return text({
381
403
  success: true,
382
404
  id: newId,
@@ -393,16 +415,24 @@ export function registerTools(server) {
393
415
  'delete_memory',
394
416
  'Permanently delete a memory by its ID.',
395
417
  {
396
- id: z.number().describe('Memory ID to delete')
418
+ id: z.number().describe('Memory ID to delete'),
419
+ agent_id: z.string().optional().describe('Agent ID — restricts deletion to this agent\'s namespace + shared')
397
420
  },
398
- async ({ id }) => {
421
+ async ({ id, agent_id }) => {
399
422
  try {
423
+ const namespace = agent_id ? agent_id.toLowerCase() : null;
424
+ const memory = getMemory(id, namespace);
425
+ if (!memory) return text({ error: `Memory #${id} not found` });
426
+
400
427
  const deleted = deleteMemory(id);
401
428
  if (!deleted) return text({ error: `Memory #${id} not found` });
402
429
 
403
430
  // Feature 1: Invalidate search cache on write
404
431
  searchCache.invalidate();
405
432
 
433
+ // Broadcast deletion to SSE subscribers
434
+ memoryEventBus.emit('memory_deleted', { id, namespace: memory.namespace || 'shared' });
435
+
406
436
  return text({ success: true, id, message: `Memory #${id} deleted` });
407
437
  } catch (err) {
408
438
  return text({ error: err.message });
@@ -538,14 +568,16 @@ export function registerTools(server) {
538
568
  {
539
569
  entity_name: z.string().describe('Name of the entity'),
540
570
  memory_id: z.number().describe('ID of the memory to link'),
541
- relation: z.string().default('mentions').describe('Relationship type')
571
+ relation: z.string().default('mentions').describe('Relationship type'),
572
+ agent_id: z.string().optional().describe('Agent ID — restricts linking to this agent\'s namespace + shared')
542
573
  },
543
- async ({ entity_name, memory_id, relation }) => {
574
+ async ({ entity_name, memory_id, relation, agent_id }) => {
544
575
  try {
576
+ const namespace = agent_id ? agent_id.toLowerCase() : null;
545
577
  const entity = getEntityByName(entity_name);
546
578
  if (!entity) return text({ error: `Entity "${entity_name}" not found.` });
547
579
 
548
- const memory = getMemory(memory_id);
580
+ const memory = getMemory(memory_id, namespace);
549
581
  if (!memory) return text({ error: `Memory #${memory_id} not found` });
550
582
 
551
583
  insertEdge(entity.id, memory_id, relation, 'entity', 'memory');
@@ -778,12 +810,13 @@ export function registerTools(server) {
778
810
  query: z.string().describe('The search query context'),
779
811
  max_tokens: z.number().default(4000).describe('Token budget for LLM context compression (default: 4000)'),
780
812
  agent_id: z.string().optional().describe('Agent ID requesting context — filters to this agent\'s namespace + shared'),
781
- session_id: z.string().optional().describe('Session ID')
813
+ session_id: z.string().optional().describe('Session ID'),
814
+ intent: z.string().optional().describe('The active task intent / category (e.g. debugging, ui_styling, database_management)')
782
815
  },
783
- async ({ query, max_tokens, agent_id, session_id }) => {
816
+ async ({ query, max_tokens, agent_id, session_id, intent }) => {
784
817
  try {
785
818
  const namespace = agent_id || null;
786
- const contextData = await getOptimizedContext(query, max_tokens, agent_id, session_id, namespace);
819
+ const contextData = await getOptimizedContext(query, max_tokens, agent_id, session_id, namespace, intent);
787
820
  return text(contextData);
788
821
  } catch (err) {
789
822
  return text({ error: err.message });
@@ -822,27 +855,6 @@ function text(data) {
822
855
  };
823
856
  }
824
857
 
825
- /**
826
- * Compute Jaccard distance between two text strings.
827
- * Used for contradiction detection — higher distance means more different content.
828
- * @param {string} a - First text
829
- * @param {string} b - Second text
830
- * @returns {number} Distance score between 0 (identical) and 1 (completely different)
831
- */
832
- function jaccardDistance(a, b) {
833
- const wordsA = new Set(a.toLowerCase().split(/\s+/));
834
- const wordsB = new Set(b.toLowerCase().split(/\s+/));
835
-
836
- let intersection = 0;
837
- for (const word of wordsA) {
838
- if (wordsB.has(word)) intersection++;
839
- }
840
-
841
- const union = wordsA.size + wordsB.size - intersection;
842
- if (union === 0) return 0;
843
- return 1 - (intersection / union);
844
- }
845
-
846
858
  /**
847
859
  * Compute word-level diff between two text strings using dynamic programming.
848
860
  * Highlights additions as [+added+] and deletions as [-deleted-].