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/README.md +103 -114
- package/bin/export.js +4 -4
- package/bin/extract.js +8 -8
- package/bin/import.js +15 -15
- package/bin/init.js +185 -38
- package/bin/mcp.js +3 -0
- package/bin/monitor.js +511 -0
- package/bin/setup.js +9 -9
- package/index.js +31 -11
- package/package.json +10 -11
- package/src/attestation.js +49 -28
- package/src/cache.js +3 -1
- package/src/database.js +227 -34
- package/src/embeddings.js +4 -2
- package/src/events.js +2 -0
- package/src/extractor-heuristic.js +5 -2
- package/src/sdk.js +4 -3
- package/src/search.js +55 -84
- package/src/server.js +884 -723
- package/src/setup-wasm.js +34 -39
- package/src/text-utils.js +52 -0
- package/src/tools.js +98 -53
- package/src/watcher.js +157 -49
package/src/attestation.js
CHANGED
|
@@ -9,7 +9,7 @@ import crypto from 'crypto';
|
|
|
9
9
|
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
|
|
10
10
|
import { join } from 'path';
|
|
11
11
|
import { homedir } from 'os';
|
|
12
|
-
import db, { getLastAttestation, insertAttestation, getAttestationById } from './database.js';
|
|
12
|
+
import db, { stmts, getLastAttestation, insertAttestation, getAttestationById } from './database.js';
|
|
13
13
|
|
|
14
14
|
const KEYS_DIR = join(homedir(), '.persyst', 'keys');
|
|
15
15
|
|
|
@@ -69,7 +69,7 @@ export function createAttestation(query, memories, agentId = null, sessionId = n
|
|
|
69
69
|
return {
|
|
70
70
|
id: m.id,
|
|
71
71
|
content_hash: contentHash,
|
|
72
|
-
score: parseFloat(scoreVal)
|
|
72
|
+
score: Math.round(parseFloat(scoreVal) * 10000)
|
|
73
73
|
};
|
|
74
74
|
});
|
|
75
75
|
|
|
@@ -135,6 +135,22 @@ export function verifyAttestationRecord(attestation) {
|
|
|
135
135
|
};
|
|
136
136
|
|
|
137
137
|
const dataToSign = JSON.stringify(doc);
|
|
138
|
+
const fullRecord = {
|
|
139
|
+
...doc,
|
|
140
|
+
signature: attestation.signature
|
|
141
|
+
};
|
|
142
|
+
const computedHash = crypto.createHash('sha256').update(JSON.stringify(fullRecord)).digest('hex');
|
|
143
|
+
|
|
144
|
+
// Check hash first — if it matches, doc reconstruction is correct
|
|
145
|
+
const hashMatch = computedHash === attestation.hash;
|
|
146
|
+
if (!hashMatch) {
|
|
147
|
+
console.error('[persyst-attest] HASH MISMATCH for', attestation.attestation_id);
|
|
148
|
+
console.error('[persyst-attest] stored hash:', attestation.hash);
|
|
149
|
+
console.error('[persyst-attest] computed hash:', computedHash);
|
|
150
|
+
console.error('[persyst-attest] doc:', JSON.stringify(doc));
|
|
151
|
+
return { valid: false, error: 'Attestation hash mismatch' };
|
|
152
|
+
}
|
|
153
|
+
|
|
138
154
|
const publicKey = getPublicKey();
|
|
139
155
|
|
|
140
156
|
// Verify signature
|
|
@@ -150,20 +166,14 @@ export function verifyAttestationRecord(attestation) {
|
|
|
150
166
|
);
|
|
151
167
|
|
|
152
168
|
if (!isSignatureValid) {
|
|
169
|
+
console.error('[persyst-attest] SIG VERIFY FAIL for', attestation.attestation_id);
|
|
170
|
+
console.error('[persyst-attest] Hash matches but signature invalid');
|
|
171
|
+
console.error('[persyst-attest] dataToSign:', dataToSign);
|
|
172
|
+
console.error('[persyst-attest] signature:', attestation.signature);
|
|
173
|
+
console.error('[persyst-attest] public key:', publicKey);
|
|
153
174
|
return { valid: false, error: 'Signature verification failed' };
|
|
154
175
|
}
|
|
155
176
|
|
|
156
|
-
// Verify hash integrity
|
|
157
|
-
const fullRecord = {
|
|
158
|
-
...doc,
|
|
159
|
-
signature: attestation.signature
|
|
160
|
-
};
|
|
161
|
-
const computedHash = crypto.createHash('sha256').update(JSON.stringify(fullRecord)).digest('hex');
|
|
162
|
-
|
|
163
|
-
if (computedHash !== attestation.hash) {
|
|
164
|
-
return { valid: false, error: 'Attestation hash mismatch' };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
177
|
return { valid: true };
|
|
168
178
|
} catch (err) {
|
|
169
179
|
return { valid: false, error: err.message };
|
|
@@ -171,7 +181,10 @@ export function verifyAttestationRecord(attestation) {
|
|
|
171
181
|
}
|
|
172
182
|
|
|
173
183
|
/**
|
|
174
|
-
*
|
|
184
|
+
* Iteratively verifies signature and chain integrity.
|
|
185
|
+
* Walks backwards from the target attestation to the genesis link,
|
|
186
|
+
* confirming each previous_hash matches the predecessor's actual hash
|
|
187
|
+
* and that sequence order strictly increases.
|
|
175
188
|
*/
|
|
176
189
|
export function verifyChainIntegrity(attestationId) {
|
|
177
190
|
const att = getAttestationById(attestationId);
|
|
@@ -184,29 +197,37 @@ export function verifyChainIntegrity(attestationId) {
|
|
|
184
197
|
return selfVerify;
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
200
|
+
// Iterative chain walk — no recursion, no stack overflow risk
|
|
201
|
+
const MAX_CHAIN_DEPTH = 10000;
|
|
202
|
+
let current = att;
|
|
203
|
+
let depth = 0;
|
|
204
|
+
|
|
205
|
+
while (current.previous_hash) {
|
|
206
|
+
if (depth >= MAX_CHAIN_DEPTH) {
|
|
207
|
+
return { valid: false, error: 'Broken chain: chain length exceeds maximum' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const prevAtt = stmts.getAttestationByHash.get(current.previous_hash);
|
|
190
211
|
if (!prevAtt) {
|
|
191
|
-
return { valid: false, error: `Broken chain: Previous attestation with hash ${
|
|
212
|
+
return { valid: false, error: `Broken chain: Previous attestation with hash ${current.previous_hash} not found` };
|
|
192
213
|
}
|
|
193
214
|
|
|
194
|
-
if (prevAtt.
|
|
195
|
-
return { valid: false, error:
|
|
215
|
+
if (prevAtt.hash !== current.previous_hash) {
|
|
216
|
+
return { valid: false, error: 'Broken chain: previous_hash does not match predecessor hash' };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (prevAtt.id >= current.id) {
|
|
220
|
+
return { valid: false, error: 'Broken chain: Invalid sequence order' };
|
|
196
221
|
}
|
|
197
222
|
|
|
198
223
|
const prevVerify = verifyAttestationRecord(prevAtt);
|
|
199
224
|
if (!prevVerify.valid) {
|
|
200
225
|
return { valid: false, error: `Broken chain: Previous link is invalid: ${prevVerify.error}` };
|
|
201
226
|
}
|
|
202
|
-
}
|
|
203
227
|
|
|
204
|
-
|
|
205
|
-
|
|
228
|
+
current = prevAtt;
|
|
229
|
+
depth++;
|
|
230
|
+
}
|
|
206
231
|
|
|
207
|
-
|
|
208
|
-
* Helper to fetch attestation by hash since it's not exposed globally.
|
|
209
|
-
*/
|
|
210
|
-
function getAttestationByHash(hash) {
|
|
211
|
-
return db.prepare('SELECT * FROM attestations WHERE hash = ?').get(hash) || null;
|
|
232
|
+
return { valid: true, attestation: current };
|
|
212
233
|
}
|
package/src/cache.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* - Full invalidation on write operations
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { logInfo } from './text-utils.js';
|
|
14
|
+
|
|
13
15
|
/**
|
|
14
16
|
* Simple LRU (Least Recently Used) cache with TTL support.
|
|
15
17
|
*/
|
|
@@ -97,7 +99,7 @@ export class LRUCache {
|
|
|
97
99
|
const size = this.cache.size;
|
|
98
100
|
this.cache.clear();
|
|
99
101
|
if (size > 0) {
|
|
100
|
-
|
|
102
|
+
logInfo(`[persyst-cache] Invalidated ${size} cached entries`);
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
|
package/src/database.js
CHANGED
|
@@ -17,6 +17,8 @@ import { join } from 'path';
|
|
|
17
17
|
import { homedir } from 'os';
|
|
18
18
|
import { mkdirSync } from 'fs';
|
|
19
19
|
|
|
20
|
+
import { logInfo } from './text-utils.js';
|
|
21
|
+
|
|
20
22
|
// ============================================================
|
|
21
23
|
// DATABASE LOCATION
|
|
22
24
|
// Store in ~/.persyst/ per default to persist across sessions
|
|
@@ -41,7 +43,7 @@ db.pragma('cache_size = -64000'); // 64MB cache size
|
|
|
41
43
|
// Load sqlite-vec BEFORE creating any vec0 tables
|
|
42
44
|
sqliteVec.load(db);
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
logInfo(`[persyst] Database: ${DB_PATH}`);
|
|
45
47
|
|
|
46
48
|
// ============================================================
|
|
47
49
|
// CREATE TABLES & SCHEMA MIGRATIONS
|
|
@@ -63,27 +65,32 @@ db.exec(`
|
|
|
63
65
|
`);
|
|
64
66
|
|
|
65
67
|
// --- Migrations for bi-temporal validity on existing tables ---
|
|
66
|
-
|
|
68
|
+
function columnExists(table, name) {
|
|
69
|
+
const info = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
70
|
+
return info.some(col => col.name === name);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!columnExists('memories', 'valid_from')) {
|
|
67
74
|
db.exec('ALTER TABLE memories ADD COLUMN valid_from INTEGER DEFAULT (unixepoch())');
|
|
68
|
-
}
|
|
75
|
+
}
|
|
69
76
|
|
|
70
|
-
|
|
77
|
+
if (!columnExists('memories', 'valid_until')) {
|
|
71
78
|
db.exec('ALTER TABLE memories ADD COLUMN valid_until INTEGER DEFAULT NULL');
|
|
72
|
-
}
|
|
79
|
+
}
|
|
73
80
|
|
|
74
|
-
|
|
81
|
+
if (!columnExists('memories', 'assertion_time')) {
|
|
75
82
|
db.exec('ALTER TABLE memories ADD COLUMN assertion_time INTEGER DEFAULT (unixepoch())');
|
|
76
|
-
}
|
|
83
|
+
}
|
|
77
84
|
|
|
78
85
|
// --- Migration: add namespace column for per-agent isolation ---
|
|
79
|
-
|
|
86
|
+
if (!columnExists('memories', 'namespace')) {
|
|
80
87
|
db.exec("ALTER TABLE memories ADD COLUMN namespace TEXT DEFAULT 'shared'");
|
|
81
|
-
}
|
|
88
|
+
}
|
|
82
89
|
|
|
83
90
|
// --- Migration: add parent_id column for history tracing ---
|
|
84
|
-
|
|
91
|
+
if (!columnExists('memories', 'parent_id')) {
|
|
85
92
|
db.exec('ALTER TABLE memories ADD COLUMN parent_id INTEGER DEFAULT NULL');
|
|
86
|
-
}
|
|
93
|
+
}
|
|
87
94
|
|
|
88
95
|
// --- Index on namespace for fast filtered queries ---
|
|
89
96
|
try {
|
|
@@ -224,7 +231,7 @@ db.exec(`
|
|
|
224
231
|
)
|
|
225
232
|
`);
|
|
226
233
|
|
|
227
|
-
|
|
234
|
+
logInfo('[persyst] Schema initialized ✓');
|
|
228
235
|
|
|
229
236
|
// ============================================================
|
|
230
237
|
// PREPARED STATEMENTS
|
|
@@ -367,6 +374,11 @@ const stmts = {
|
|
|
367
374
|
deleteEntity: db.prepare(
|
|
368
375
|
'DELETE FROM entities WHERE id = ?'
|
|
369
376
|
),
|
|
377
|
+
deleteEdgesByEntity: db.prepare(
|
|
378
|
+
`DELETE FROM edges WHERE
|
|
379
|
+
(source_id = ? AND source_type = 'entity') OR
|
|
380
|
+
(target_id = ? AND target_type = 'entity')`
|
|
381
|
+
),
|
|
370
382
|
|
|
371
383
|
// -- Edges --
|
|
372
384
|
insertEdge: db.prepare(
|
|
@@ -426,9 +438,171 @@ const stmts = {
|
|
|
426
438
|
INSERT INTO watched_files (file_path, last_position)
|
|
427
439
|
VALUES (?, ?)
|
|
428
440
|
ON CONFLICT(file_path) DO UPDATE SET last_position = excluded.last_position, updated_at = unixepoch()
|
|
441
|
+
`),
|
|
442
|
+
|
|
443
|
+
// -- Internal lookups (pre-compiled for hot-loop use) --
|
|
444
|
+
getAttestationByHash: db.prepare(
|
|
445
|
+
'SELECT * FROM attestations WHERE hash = ?'
|
|
446
|
+
),
|
|
447
|
+
getMemoryParentId: db.prepare(
|
|
448
|
+
'SELECT parent_id FROM memories WHERE id = ?'
|
|
449
|
+
),
|
|
450
|
+
getMemoryChildren: db.prepare(
|
|
451
|
+
'SELECT id FROM memories WHERE parent_id = ?'
|
|
452
|
+
),
|
|
453
|
+
getMemoryContentById: db.prepare(
|
|
454
|
+
'SELECT content FROM memories WHERE id = ?'
|
|
455
|
+
),
|
|
456
|
+
getMemoryByIdRaw: db.prepare(
|
|
457
|
+
'SELECT * FROM memories WHERE id = ? AND valid_until IS NULL'
|
|
458
|
+
),
|
|
459
|
+
getMemoryLikeContent: db.prepare(
|
|
460
|
+
'SELECT id FROM memories WHERE content LIKE ? AND valid_until IS NULL'
|
|
461
|
+
),
|
|
462
|
+
getVecByRowId: db.prepare(
|
|
463
|
+
'SELECT embedding FROM memories_vec WHERE rowid = ?'
|
|
464
|
+
),
|
|
465
|
+
updateMemoryParentId: db.prepare(
|
|
466
|
+
'UPDATE memories SET parent_id = ? WHERE id = ?'
|
|
467
|
+
),
|
|
468
|
+
deleteProvenanceByMemoryId: db.prepare(
|
|
469
|
+
'DELETE FROM provenance WHERE memory_id = ?'
|
|
470
|
+
),
|
|
471
|
+
deleteContradictionsByMemoryId: db.prepare(
|
|
472
|
+
'DELETE FROM contradictions WHERE old_memory_id = ? OR new_memory_id = ?'
|
|
473
|
+
),
|
|
474
|
+
getReputationScore: db.prepare(
|
|
475
|
+
'SELECT reputation_score FROM agent_stats WHERE agent_id = ?'
|
|
476
|
+
),
|
|
477
|
+
updateProvenanceOwner: db.prepare(
|
|
478
|
+
"UPDATE provenance SET source_type = 'agent', source_id = ?, confidence = 1.0 WHERE memory_id = ?"
|
|
479
|
+
),
|
|
480
|
+
archiveMemoryById: db.prepare(
|
|
481
|
+
'UPDATE memories SET valid_until = unixepoch() WHERE id = ?'
|
|
482
|
+
),
|
|
483
|
+
getEdgesBySourceAndType: db.prepare(`
|
|
484
|
+
SELECT * FROM edges
|
|
485
|
+
WHERE (source_id = ? AND source_type = ?)
|
|
486
|
+
OR (target_id = ? AND target_type = ?)
|
|
487
|
+
`),
|
|
488
|
+
getMemoriesByEntityEdges: db.prepare(`
|
|
489
|
+
SELECT * FROM edges
|
|
490
|
+
WHERE (source_id = ? AND source_type = 'entity' AND target_type = 'memory')
|
|
491
|
+
OR (target_id = ? AND target_type = 'entity' AND source_type = 'memory')
|
|
492
|
+
`),
|
|
493
|
+
consolidateVecSearch: db.prepare(`
|
|
494
|
+
SELECT rowid AS id, distance
|
|
495
|
+
FROM memories_vec
|
|
496
|
+
WHERE embedding MATCH ?
|
|
497
|
+
AND k = 30
|
|
498
|
+
`),
|
|
499
|
+
archiveAndInsertContradiction: db.prepare(
|
|
500
|
+
'UPDATE memories SET valid_until = unixepoch() WHERE id = ?'
|
|
501
|
+
),
|
|
502
|
+
archiveExpiredTransientMemories: db.prepare(`
|
|
503
|
+
UPDATE memories
|
|
504
|
+
SET valid_until = unixepoch()
|
|
505
|
+
WHERE valid_until IS NULL
|
|
506
|
+
AND (content LIKE 'Reminder:%' OR content LIKE 'Note:%')
|
|
507
|
+
AND (unixepoch() - created_at) > 1209600
|
|
429
508
|
`)
|
|
430
509
|
};
|
|
431
510
|
|
|
511
|
+
export { stmts };
|
|
512
|
+
|
|
513
|
+
// ============================================================
|
|
514
|
+
// SECRET DETECTION & REDACTION HELPERS
|
|
515
|
+
// ============================================================
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Detects sensitive/credential patterns in a string and replaces values with [REDACTED].
|
|
519
|
+
* @param {string} content - The content to sanitize
|
|
520
|
+
* @returns {string} Sanitized content
|
|
521
|
+
*/
|
|
522
|
+
export function redactSecrets(content) {
|
|
523
|
+
if (!content || typeof content !== 'string') return content;
|
|
524
|
+
|
|
525
|
+
let redacted = content;
|
|
526
|
+
|
|
527
|
+
// 1. Redact credentials in connection strings / URIs
|
|
528
|
+
// Matches scheme://user:pass@host and scheme://:pass@host
|
|
529
|
+
const connectionStringRegex = /\b([a-zA-Z0-9+.-]+:\/\/)([^/:\s]*):([^@/:\s]+)(@[^/\s]+)/gi;
|
|
530
|
+
redacted = redacted.replace(connectionStringRegex, (match, protocol, user, pass, host) => {
|
|
531
|
+
return protocol + user + ':[REDACTED]' + host;
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// 2. Redact key-value pairs matching credentials (retaining key/operator, redacting value)
|
|
535
|
+
// Supports single-quoted, double-quoted, and unquoted values (non-whitespace).
|
|
536
|
+
const kvRegex = /['"]?\b(api[_-]?key|secret[_-]?key|secret|password|passwd|pwd|passphrase|auth[_-]?token|access[_-]?token|client[_-]?secret|private[_-]?key|auth|access|client|aws|gcp|google|stripe|github|openai|vercel|heroku|slack|ssh[_-]?(?:key|password|passphrase|pass)?|credential|aws_secret|secret_access_key|aws_access_key|ssh_passphrase|ssh_password|ssh_key_pass)\b['"]?\s*(?:key|token|secret|password|pwd|passwd|value|string|id)?(?:\b|(?<=['"]))\s*([:=]|is|of|to|set\s+to|\(|\buses\b)\s*(?:'([^']{6,2048})'|"([^"]{6,2048})"|([^\s]+(?:\n(?![a-zA-Z0-9_-]+\s*[:=])(?=[^\s]+(?:\n|$))[^\s]+)*))/gi;
|
|
537
|
+
|
|
538
|
+
redacted = redacted.replace(kvRegex, (match, key, op, sqVal, dqVal, uqVal) => {
|
|
539
|
+
const val = sqVal || dqVal || uqVal;
|
|
540
|
+
if (!val) return match;
|
|
541
|
+
|
|
542
|
+
// Strip trailing parenthesis if operator is '(' and value has trailing parenthesis
|
|
543
|
+
let cleanVal = val;
|
|
544
|
+
if (op === '(' && val.endsWith(')')) {
|
|
545
|
+
cleanVal = val.slice(0, -1);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const lastIdx = match.lastIndexOf(cleanVal);
|
|
549
|
+
if (lastIdx !== -1) {
|
|
550
|
+
return match.slice(0, lastIdx) + '[REDACTED]' + match.slice(lastIdx + cleanVal.length);
|
|
551
|
+
}
|
|
552
|
+
return match;
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// 3. Redact standalone common API keys and tokens
|
|
556
|
+
const standalonePatterns = [
|
|
557
|
+
/\b(sk-[a-zA-Z0-9]{48})\b/g, // OpenAI
|
|
558
|
+
/\b(sk-proj-[a-zA-Z0-9-]{40,})\b/g, // OpenAI project
|
|
559
|
+
/\b(gh[pous]_[a-zA-Z0-9]{36,255})\b/g, // GitHub PAT/Fine-grained
|
|
560
|
+
/\b(xox[bapr]-[0-9]{12}-[a-zA-Z0-9]{24})\b/g, // Slack token
|
|
561
|
+
/\b(AIzaSy[A-Za-z0-9_-]{33})\b/g, // Google API key
|
|
562
|
+
/\b((?:sk|rk|pk)_(?:live|test)_[0-9a-zA-Z]{24,32})\b/g, // Stripe key
|
|
563
|
+
/\b(AKIA[0-9A-Z]{16,40})\b/gi, // AWS Access Key ID (case-insensitive)
|
|
564
|
+
/\b(ASCA[0-9A-Z]{16,40})\b/gi, // AWS ASCA Key
|
|
565
|
+
/\b(npm_[a-zA-Z0-9]{36,255})\b/g, // npm token
|
|
566
|
+
/\b(ey[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,})\b/g, // JWT token
|
|
567
|
+
/-----BEGIN[A-Z0-9\s_-]+PRIVATE\s+KEY[A-Z0-9\s_-]*-----\s*[\s\S]*?-----END[A-Z0-9\s_-]+PRIVATE\s+KEY[A-Z0-9\s_-]*-----/gi, // PEM private key
|
|
568
|
+
];
|
|
569
|
+
|
|
570
|
+
for (const pattern of standalonePatterns) {
|
|
571
|
+
redacted = redacted.replace(pattern, '[REDACTED]');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// 4. Robust credential shape heuristic (independent of strict key-value punctuation)
|
|
575
|
+
const containsCredKeyword = /\b(password|passwd|pwd|passphrase|pass|secret|token|api|credential|auth|ssh|aws)\b/i.test(redacted);
|
|
576
|
+
if (containsCredKeyword) {
|
|
577
|
+
// Match password-like tokens: length 6 to 64, containing both letters and digits/symbols
|
|
578
|
+
const tokenRegex = /\b([a-zA-Z0-9_@#$%^&*+!~\-]{6,64})(?!\w)/g;
|
|
579
|
+
|
|
580
|
+
redacted = redacted.replace(tokenRegex, (match) => {
|
|
581
|
+
// Skip common query words and technical keywords
|
|
582
|
+
if (/^(password|passwd|pwd|passphrase|pass|secret|token|api|credential|auth|ssh|uses|with|key|from|here|what|have|this|that|your|same|then|want|more|base64|base64-like|base64-encoded|sha256|sha1|md5|aes256|aes128|utf8|utf-8|url|uri|ipv4|ipv6|http|https|sha-256|sha-1)$/i.test(match)) {
|
|
583
|
+
return match;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const hasLetter = /[a-zA-Z]/.test(match);
|
|
587
|
+
const hasDigitOrSpecialSymbol = /[0-9@#$%^&*+=!~]/.test(match);
|
|
588
|
+
|
|
589
|
+
if (hasLetter && hasDigitOrSpecialSymbol) {
|
|
590
|
+
// Require length >= 8 or containing special symbols/mixed case digits
|
|
591
|
+
const isStrongSecretCandidate = match.length >= 8 ||
|
|
592
|
+
(/[^a-zA-Z0-9_]/.test(match)) ||
|
|
593
|
+
(/[A-Z]/.test(match) && /[0-9]/.test(match));
|
|
594
|
+
|
|
595
|
+
if (isStrongSecretCandidate) {
|
|
596
|
+
return '[REDACTED]';
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return match;
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return redacted;
|
|
604
|
+
}
|
|
605
|
+
|
|
432
606
|
// ============================================================
|
|
433
607
|
// CRUD FUNCTIONS
|
|
434
608
|
// Simple, one-purpose functions. No magic.
|
|
@@ -443,11 +617,12 @@ const stmts = {
|
|
|
443
617
|
* @returns {number} The new memory's ID
|
|
444
618
|
*/
|
|
445
619
|
export function insertMemory(content, importance = 1.0, provenanceInfo = null, namespace = 'shared', parentId = null) {
|
|
446
|
-
|
|
620
|
+
const redactedContent = redactSecrets(content);
|
|
621
|
+
if (redactedContent && redactedContent.length > 10000) {
|
|
447
622
|
throw new Error('Memory content exceeds maximum length of 10000 characters.');
|
|
448
623
|
}
|
|
449
624
|
const clampedImportance = Math.max(0.0, Math.min(1.0, Math.round(importance * 10000) / 10000));
|
|
450
|
-
const result = stmts.insertMemory.run(
|
|
625
|
+
const result = stmts.insertMemory.run(redactedContent, clampedImportance, namespace || 'shared', parentId);
|
|
451
626
|
const id = Number(result.lastInsertRowid);
|
|
452
627
|
|
|
453
628
|
// Provenance Info handling
|
|
@@ -484,13 +659,12 @@ export function insertVector(id, embedding) {
|
|
|
484
659
|
* @returns {object|null} The memory row, or null if not found
|
|
485
660
|
*/
|
|
486
661
|
export function getMemory(id, namespace = null) {
|
|
487
|
-
const memory = namespace
|
|
488
|
-
? stmts.
|
|
489
|
-
: stmts.
|
|
662
|
+
const memory = (namespace === 'all' || namespace === null)
|
|
663
|
+
? stmts.getById.get(id)
|
|
664
|
+
: stmts.getByIdNs.get(id, namespace);
|
|
490
665
|
if (memory) {
|
|
491
666
|
boostMemory(id);
|
|
492
|
-
|
|
493
|
-
memory.provenance = prov;
|
|
667
|
+
memory.provenance = getProvenance(id);
|
|
494
668
|
}
|
|
495
669
|
return memory || null;
|
|
496
670
|
}
|
|
@@ -514,10 +688,9 @@ export function getAnyMemoryById(id) {
|
|
|
514
688
|
* @returns {object|null} The memory row, or null if not found
|
|
515
689
|
*/
|
|
516
690
|
export function getMemoryById(id, namespace = null) {
|
|
517
|
-
const
|
|
518
|
-
const memory = ns === 'all'
|
|
691
|
+
const memory = (namespace === 'all' || namespace === null)
|
|
519
692
|
? stmts.getById.get(id)
|
|
520
|
-
: stmts.getByIdNs.get(id,
|
|
693
|
+
: stmts.getByIdNs.get(id, namespace);
|
|
521
694
|
if (memory) {
|
|
522
695
|
memory.provenance = getProvenance(id);
|
|
523
696
|
}
|
|
@@ -530,7 +703,8 @@ export function getMemoryById(id, namespace = null) {
|
|
|
530
703
|
* @returns {boolean} true if the memory existed and was updated
|
|
531
704
|
*/
|
|
532
705
|
export function updateMemoryContent(id, content) {
|
|
533
|
-
const
|
|
706
|
+
const redactedContent = redactSecrets(content);
|
|
707
|
+
const result = stmts.updateContent.run(redactedContent, id);
|
|
534
708
|
return result.changes > 0;
|
|
535
709
|
}
|
|
536
710
|
|
|
@@ -550,8 +724,8 @@ export function deleteMemory(id) {
|
|
|
550
724
|
stmts.deleteEdgesByMemory.run(id, id);
|
|
551
725
|
deleteVec(id); // Remove vector first (no cascades on virtual tables)
|
|
552
726
|
try {
|
|
553
|
-
|
|
554
|
-
|
|
727
|
+
stmts.deleteProvenanceByMemoryId.run(id);
|
|
728
|
+
stmts.deleteContradictionsByMemoryId.run(id, id);
|
|
555
729
|
} catch (e) {
|
|
556
730
|
console.error(`[persyst] Clean up provenance/contradictions error: ${e.message}`);
|
|
557
731
|
}
|
|
@@ -698,6 +872,7 @@ export function getAllEntities(limit = 50) {
|
|
|
698
872
|
* Delete an entity and its edges.
|
|
699
873
|
*/
|
|
700
874
|
export function deleteEntity(id) {
|
|
875
|
+
stmts.deleteEdgesByEntity.run(id, id);
|
|
701
876
|
stmts.deleteEntity.run(id);
|
|
702
877
|
}
|
|
703
878
|
|
|
@@ -712,11 +887,7 @@ export function insertEdge(sourceId, targetId, relation, sourceType, targetType)
|
|
|
712
887
|
* Get all memories linked to an entity.
|
|
713
888
|
*/
|
|
714
889
|
export function getMemoriesByEntity(entityId) {
|
|
715
|
-
const edges =
|
|
716
|
-
SELECT * FROM edges
|
|
717
|
-
WHERE (source_id = ? AND source_type = 'entity' AND target_type = 'memory')
|
|
718
|
-
OR (target_id = ? AND target_type = 'entity' AND source_type = 'memory')
|
|
719
|
-
`).all(entityId, entityId);
|
|
890
|
+
const edges = stmts.getMemoriesByEntityEdges.all(entityId, entityId);
|
|
720
891
|
const memoryIds = edges.map(e => e.source_type === 'memory' ? e.source_id : e.target_id);
|
|
721
892
|
return memoryIds.map(id => stmts.getById.get(id)).filter(Boolean);
|
|
722
893
|
}
|
|
@@ -779,7 +950,7 @@ export function getMemoryByContent(content, namespace = null) {
|
|
|
779
950
|
const row = namespace
|
|
780
951
|
? stmts.findMemoryByContentNs.get(content, namespace)
|
|
781
952
|
: stmts.findMemoryByContent.get(content);
|
|
782
|
-
return row ? getMemoryById(row.id) : null;
|
|
953
|
+
return row ? getMemoryById(row.id, namespace) : null;
|
|
783
954
|
}
|
|
784
955
|
|
|
785
956
|
// ============================================================
|
|
@@ -797,7 +968,7 @@ export function logContradiction(oldMemoryId, newMemoryId, reason = '') {
|
|
|
797
968
|
try {
|
|
798
969
|
const parentId = Math.min(oldMemoryId, newMemoryId);
|
|
799
970
|
const childId = Math.max(oldMemoryId, newMemoryId);
|
|
800
|
-
|
|
971
|
+
stmts.updateMemoryParentId.run(parentId, childId);
|
|
801
972
|
} catch (e) {
|
|
802
973
|
console.error(`[persyst] Failed to set parent_id on contradiction: ${e.message}`);
|
|
803
974
|
}
|
|
@@ -910,13 +1081,13 @@ export function getMemoryHistoryChain(memoryId) {
|
|
|
910
1081
|
versions.add(currentId);
|
|
911
1082
|
|
|
912
1083
|
// 1. Find parent (ancestor) from memories table
|
|
913
|
-
const row =
|
|
1084
|
+
const row = stmts.getMemoryParentId.get(currentId);
|
|
914
1085
|
if (row && row.parent_id !== null) {
|
|
915
1086
|
if (!versions.has(row.parent_id)) queue.push(row.parent_id);
|
|
916
1087
|
}
|
|
917
1088
|
|
|
918
1089
|
// 2. Find children (descendants) from memories table
|
|
919
|
-
const children =
|
|
1090
|
+
const children = stmts.getMemoryChildren.all(currentId);
|
|
920
1091
|
for (const child of children) {
|
|
921
1092
|
if (!versions.has(child.id)) queue.push(child.id);
|
|
922
1093
|
}
|
|
@@ -993,6 +1164,23 @@ export function upsertWatchPosition(filePath, position) {
|
|
|
993
1164
|
// CLEANUP
|
|
994
1165
|
// ============================================================
|
|
995
1166
|
|
|
1167
|
+
/**
|
|
1168
|
+
* Archive transient memories (reminders and notes) older than 14 days.
|
|
1169
|
+
* Returns the count of archived memories.
|
|
1170
|
+
*/
|
|
1171
|
+
export function archiveExpiredMemories() {
|
|
1172
|
+
try {
|
|
1173
|
+
const info = stmts.archiveExpiredTransientMemories.run();
|
|
1174
|
+
if (info.changes > 0) {
|
|
1175
|
+
console.error(`[persyst] Archived ${info.changes} expired transient memories (Note/Reminder older than 14 days).`);
|
|
1176
|
+
}
|
|
1177
|
+
return info.changes;
|
|
1178
|
+
} catch (e) {
|
|
1179
|
+
console.error(`[persyst] Failed to archive expired memories: ${e.message}`);
|
|
1180
|
+
return 0;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
996
1184
|
/**
|
|
997
1185
|
* Close the database connection. Call on shutdown.
|
|
998
1186
|
*/
|
|
@@ -1001,4 +1189,9 @@ export function closeDatabase() {
|
|
|
1001
1189
|
console.error('[persyst] Database closed');
|
|
1002
1190
|
}
|
|
1003
1191
|
|
|
1192
|
+
// Run auto-expiry cleanup on database startup to prune transient bloat immediately
|
|
1193
|
+
try {
|
|
1194
|
+
archiveExpiredMemories();
|
|
1195
|
+
} catch (_) {}
|
|
1196
|
+
|
|
1004
1197
|
export default db;
|
package/src/embeddings.js
CHANGED
|
@@ -19,6 +19,8 @@ env.useWasmCache = false;
|
|
|
19
19
|
// The embedding pipeline (lazy-loaded on first use)
|
|
20
20
|
let extractor = null;
|
|
21
21
|
|
|
22
|
+
import { logInfo } from './text-utils.js';
|
|
23
|
+
|
|
22
24
|
/**
|
|
23
25
|
* Load the embedding model. Called automatically on first use.
|
|
24
26
|
* First run downloads the model (~50MB). Subsequent runs use cache.
|
|
@@ -26,9 +28,9 @@ let extractor = null;
|
|
|
26
28
|
async function loadModel() {
|
|
27
29
|
if (extractor) return;
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
logInfo('[persyst] Loading embedding model (first run downloads ~50MB)...');
|
|
30
32
|
extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
|
|
31
|
-
|
|
33
|
+
logInfo('[persyst] Embedding model loaded ✓');
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
package/src/events.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* Events emitted:
|
|
9
9
|
* memory_added { id, content, namespace, source }
|
|
10
10
|
* memory_deleted { id }
|
|
11
|
+
* memory_updated { old_id, new_id, namespace }
|
|
12
|
+
* memory_retrieved { tool, query, count, agent_id, namespace, memory_ids, token_budget? }
|
|
11
13
|
* memories_consolidated { consolidated_groups, details }
|
|
12
14
|
*/
|
|
13
15
|
|
|
@@ -425,15 +425,18 @@ export function extractHeuristic(text, options = {}) {
|
|
|
425
425
|
return [];
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
// Strip all markdown fenced code blocks to prevent extracting facts from example code/logs
|
|
429
|
+
const cleanSourceText = text.replace(/```[\s\S]*?```/g, '');
|
|
430
|
+
|
|
428
431
|
// --- Step 1: Explicit saves (highest priority, no filter) ---
|
|
429
|
-
const explicitFacts = extractExplicitSaves(
|
|
432
|
+
const explicitFacts = extractExplicitSaves(cleanSourceText);
|
|
430
433
|
|
|
431
434
|
// --- Step 2: Implicit pattern matching (filtered, tech-required) ---
|
|
432
435
|
const implicitFacts = [];
|
|
433
436
|
const seen = new Set(explicitFacts.map(f => f.content.toLowerCase().replace(/\s+/g, ' ').trim()));
|
|
434
437
|
|
|
435
438
|
// Process line-by-line to filter code/noise
|
|
436
|
-
const lines =
|
|
439
|
+
const lines = cleanSourceText.split('\n');
|
|
437
440
|
const cleanLines = lines.filter(line => !isNoiseLine(line));
|
|
438
441
|
const cleanText = cleanLines.join('\n');
|
|
439
442
|
|
package/src/sdk.js
CHANGED
|
@@ -101,17 +101,18 @@ export class Persyst {
|
|
|
101
101
|
* @private
|
|
102
102
|
*/
|
|
103
103
|
async _trackLibrary({ content, importance, agent_id, session_id, shared }) {
|
|
104
|
-
const { insertMemory, insertVector } = await import('./database.js');
|
|
104
|
+
const { insertMemory, insertVector, redactSecrets } = await import('./database.js');
|
|
105
105
|
const { generateEmbedding } = await import('./embeddings.js');
|
|
106
106
|
|
|
107
107
|
const namespace = shared ? 'shared' : agent_id;
|
|
108
|
-
const
|
|
108
|
+
const redactedContent = redactSecrets ? redactSecrets(content) : content;
|
|
109
|
+
const id = insertMemory(redactedContent, importance, {
|
|
109
110
|
source_type: 'api',
|
|
110
111
|
source_id: agent_id,
|
|
111
112
|
confidence: 1.0
|
|
112
113
|
}, namespace);
|
|
113
114
|
|
|
114
|
-
const embedding = await generateEmbedding(
|
|
115
|
+
const embedding = await generateEmbedding(redactedContent);
|
|
115
116
|
insertVector(id, embedding);
|
|
116
117
|
return { success: true, id };
|
|
117
118
|
}
|