@plur-ai/core 0.5.2 → 0.7.0
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/dist/index.d.ts +77 -22
- package/dist/index.js +409 -67
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -545,13 +545,60 @@ declare const PackManifestSchema: z.ZodObject<{
|
|
|
545
545
|
}>;
|
|
546
546
|
type PackManifest = z.infer<typeof PackManifestSchema>;
|
|
547
547
|
|
|
548
|
+
interface InstallResult {
|
|
549
|
+
installed: number;
|
|
550
|
+
name: string;
|
|
551
|
+
conflicts: ConflictItem[];
|
|
552
|
+
}
|
|
553
|
+
interface ConflictItem {
|
|
554
|
+
pack_engram_id: string;
|
|
555
|
+
pack_statement: string;
|
|
556
|
+
existing_engram_id: string;
|
|
557
|
+
existing_statement: string;
|
|
558
|
+
type: 'contradiction' | 'duplicate';
|
|
559
|
+
}
|
|
560
|
+
declare function installPack(packsDir: string, source: string, existingEngrams?: Engram[]): InstallResult;
|
|
561
|
+
interface UninstallResult {
|
|
562
|
+
name: string;
|
|
563
|
+
removed: boolean;
|
|
564
|
+
engram_count: number;
|
|
565
|
+
}
|
|
566
|
+
declare function uninstallPack(packsDir: string, name: string): UninstallResult;
|
|
548
567
|
interface PackInfo {
|
|
549
568
|
name: string;
|
|
550
569
|
path: string;
|
|
551
570
|
engram_count: number;
|
|
552
571
|
manifest?: PackManifest;
|
|
572
|
+
integrity?: string;
|
|
553
573
|
}
|
|
554
574
|
declare function listPacks(packsDir: string): PackInfo[];
|
|
575
|
+
interface ExportOptions {
|
|
576
|
+
name: string;
|
|
577
|
+
version: string;
|
|
578
|
+
description?: string;
|
|
579
|
+
creator?: string;
|
|
580
|
+
domain?: string;
|
|
581
|
+
scope?: string;
|
|
582
|
+
tags?: string[];
|
|
583
|
+
type?: string;
|
|
584
|
+
}
|
|
585
|
+
interface PrivacyScanResult {
|
|
586
|
+
clean: boolean;
|
|
587
|
+
issues: PrivacyIssue[];
|
|
588
|
+
}
|
|
589
|
+
interface PrivacyIssue {
|
|
590
|
+
engram_id: string;
|
|
591
|
+
type: 'secret' | 'private_visibility' | 'personal_path' | 'email' | 'ip_address';
|
|
592
|
+
detail: string;
|
|
593
|
+
}
|
|
594
|
+
interface ExportResult {
|
|
595
|
+
path: string;
|
|
596
|
+
engram_count: number;
|
|
597
|
+
privacy: PrivacyScanResult;
|
|
598
|
+
match_terms: string[];
|
|
599
|
+
integrity: string;
|
|
600
|
+
}
|
|
601
|
+
declare function exportPack(engrams: Engram[], outputDir: string, manifest: ExportOptions): ExportResult;
|
|
555
602
|
|
|
556
603
|
interface SyncStatus {
|
|
557
604
|
initialized: boolean;
|
|
@@ -804,14 +851,14 @@ declare const StructuralTemplateSchema: z.ZodObject<{
|
|
|
804
851
|
goal_type: string;
|
|
805
852
|
constraint_type: string;
|
|
806
853
|
outcome_type: string;
|
|
807
|
-
structure_type: "goal-constraint-outcome" | "feedback-loop" | "causal-chain" | "
|
|
854
|
+
structure_type: "recursive" | "goal-constraint-outcome" | "feedback-loop" | "causal-chain" | "tradeoff" | "freeform";
|
|
808
855
|
freeform_structure?: string | undefined;
|
|
809
856
|
}, {
|
|
810
857
|
template: string;
|
|
811
858
|
goal_type: string;
|
|
812
859
|
constraint_type: string;
|
|
813
860
|
outcome_type: string;
|
|
814
|
-
structure_type?: "goal-constraint-outcome" | "feedback-loop" | "causal-chain" | "
|
|
861
|
+
structure_type?: "recursive" | "goal-constraint-outcome" | "feedback-loop" | "causal-chain" | "tradeoff" | "freeform" | undefined;
|
|
815
862
|
freeform_structure?: string | undefined;
|
|
816
863
|
}>;
|
|
817
864
|
declare const EvidenceEntrySchema: z.ZodObject<{
|
|
@@ -904,14 +951,14 @@ declare const MetaFieldSchema: z.ZodObject<{
|
|
|
904
951
|
goal_type: string;
|
|
905
952
|
constraint_type: string;
|
|
906
953
|
outcome_type: string;
|
|
907
|
-
structure_type: "goal-constraint-outcome" | "feedback-loop" | "causal-chain" | "
|
|
954
|
+
structure_type: "recursive" | "goal-constraint-outcome" | "feedback-loop" | "causal-chain" | "tradeoff" | "freeform";
|
|
908
955
|
freeform_structure?: string | undefined;
|
|
909
956
|
}, {
|
|
910
957
|
template: string;
|
|
911
958
|
goal_type: string;
|
|
912
959
|
constraint_type: string;
|
|
913
960
|
outcome_type: string;
|
|
914
|
-
structure_type?: "goal-constraint-outcome" | "feedback-loop" | "causal-chain" | "
|
|
961
|
+
structure_type?: "recursive" | "goal-constraint-outcome" | "feedback-loop" | "causal-chain" | "tradeoff" | "freeform" | undefined;
|
|
915
962
|
freeform_structure?: string | undefined;
|
|
916
963
|
}>;
|
|
917
964
|
evidence: z.ZodArray<z.ZodObject<{
|
|
@@ -1003,7 +1050,7 @@ declare const MetaFieldSchema: z.ZodObject<{
|
|
|
1003
1050
|
goal_type: string;
|
|
1004
1051
|
constraint_type: string;
|
|
1005
1052
|
outcome_type: string;
|
|
1006
|
-
structure_type: "goal-constraint-outcome" | "feedback-loop" | "causal-chain" | "
|
|
1053
|
+
structure_type: "recursive" | "goal-constraint-outcome" | "feedback-loop" | "causal-chain" | "tradeoff" | "freeform";
|
|
1007
1054
|
freeform_structure?: string | undefined;
|
|
1008
1055
|
};
|
|
1009
1056
|
evidence: {
|
|
@@ -1042,7 +1089,7 @@ declare const MetaFieldSchema: z.ZodObject<{
|
|
|
1042
1089
|
goal_type: string;
|
|
1043
1090
|
constraint_type: string;
|
|
1044
1091
|
outcome_type: string;
|
|
1045
|
-
structure_type?: "goal-constraint-outcome" | "feedback-loop" | "causal-chain" | "
|
|
1092
|
+
structure_type?: "recursive" | "goal-constraint-outcome" | "feedback-loop" | "causal-chain" | "tradeoff" | "freeform" | undefined;
|
|
1046
1093
|
freeform_structure?: string | undefined;
|
|
1047
1094
|
};
|
|
1048
1095
|
evidence: {
|
|
@@ -1234,8 +1281,9 @@ declare function detectPlurStorage(explicitPath?: string): PlurPaths;
|
|
|
1234
1281
|
declare class IndexedStorage {
|
|
1235
1282
|
private dbPath;
|
|
1236
1283
|
private engramsPath;
|
|
1284
|
+
private stores;
|
|
1237
1285
|
private db;
|
|
1238
|
-
constructor(engramsPath: string, dbPath: string);
|
|
1286
|
+
constructor(engramsPath: string, dbPath: string, stores?: StoreEntry[]);
|
|
1239
1287
|
private getDb;
|
|
1240
1288
|
/** Load all engrams from SQLite index. Auto-rebuilds if db missing. */
|
|
1241
1289
|
loadAll(): Engram[];
|
|
@@ -1249,7 +1297,7 @@ declare class IndexedStorage {
|
|
|
1249
1297
|
count(filter?: {
|
|
1250
1298
|
status?: string;
|
|
1251
1299
|
}): number;
|
|
1252
|
-
/** Sync SQLite index from YAML source of truth. */
|
|
1300
|
+
/** Sync SQLite index from YAML source of truth (primary + all stores). */
|
|
1253
1301
|
syncFromYaml(): void;
|
|
1254
1302
|
/** Drop and rebuild the entire index from YAML. */
|
|
1255
1303
|
reindex(): void;
|
|
@@ -1303,9 +1351,20 @@ declare class Plur {
|
|
|
1303
1351
|
private paths;
|
|
1304
1352
|
private config;
|
|
1305
1353
|
private indexedStorage;
|
|
1354
|
+
private _engramCache;
|
|
1306
1355
|
constructor(options?: {
|
|
1307
1356
|
path?: string;
|
|
1308
1357
|
});
|
|
1358
|
+
/**
|
|
1359
|
+
* Load engrams from primary store + all configured stores, with mtime-based caching.
|
|
1360
|
+
* Store engram IDs get namespaced: ENG-2026-0401-001 → ENG-DF-2026-0401-001.
|
|
1361
|
+
* Primary engrams are returned unchanged.
|
|
1362
|
+
*/
|
|
1363
|
+
private _loadAllEngrams;
|
|
1364
|
+
/** Load engrams from a path with mtime-based caching */
|
|
1365
|
+
private _loadCached;
|
|
1366
|
+
/** Find which store owns an engram by ID. For namespaced IDs, strips prefix to find in store. */
|
|
1367
|
+
private _findEngramStore;
|
|
1309
1368
|
/** Create engram, detect conflicts, save. Returns the created engram. */
|
|
1310
1369
|
learn(statement: string, context?: LearnContext): Engram;
|
|
1311
1370
|
/**
|
|
@@ -1328,7 +1387,7 @@ declare class Plur {
|
|
|
1328
1387
|
recallExpanded(query: string, options: RecallOptions & {
|
|
1329
1388
|
llm: LlmFunction;
|
|
1330
1389
|
}): Promise<Engram[]>;
|
|
1331
|
-
/** Get a single engram by ID, regardless of status.
|
|
1390
|
+
/** Get a single engram by ID, regardless of status. Searches primary + all stores. */
|
|
1332
1391
|
getById(id: string): Engram | null;
|
|
1333
1392
|
/** List all active engrams, optionally filtered by scope/domain. No search — returns all matches. */
|
|
1334
1393
|
list(options?: {
|
|
@@ -1345,7 +1404,7 @@ declare class Plur {
|
|
|
1345
1404
|
/** Scored injection with embedding boost when available. Falls back to BM25 if embeddings not installed. */
|
|
1346
1405
|
injectHybrid(task: string, options?: InjectOptions): Promise<InjectionResult>;
|
|
1347
1406
|
private _formatInjection;
|
|
1348
|
-
/** Update feedback_signals and adjust retrieval_strength. Searches
|
|
1407
|
+
/** Update feedback_signals and adjust retrieval_strength. Searches primary, stores, then packs. */
|
|
1349
1408
|
feedback(id: string, signal: 'positive' | 'negative' | 'neutral'): void;
|
|
1350
1409
|
/** Save extracted meta-engrams to the engram store. Skips IDs that already exist. */
|
|
1351
1410
|
saveMetaEngrams(metas: Engram[]): {
|
|
@@ -1354,7 +1413,7 @@ declare class Plur {
|
|
|
1354
1413
|
};
|
|
1355
1414
|
/** Update an existing engram in the store by ID. Returns true if found and updated. */
|
|
1356
1415
|
updateEngram(updated: Engram): boolean;
|
|
1357
|
-
/** Set engram status to 'retired'. */
|
|
1416
|
+
/** Set engram status to 'retired'. Supports primary and store engrams. */
|
|
1358
1417
|
forget(id: string, reason?: string): void;
|
|
1359
1418
|
/** Remove retired engrams from storage. Returns count of removed and remaining. */
|
|
1360
1419
|
compact(): {
|
|
@@ -1373,22 +1432,18 @@ declare class Plur {
|
|
|
1373
1432
|
timeline(query?: TimelineQuery): Episode[];
|
|
1374
1433
|
/** Rule-based extraction of engram candidates from content. */
|
|
1375
1434
|
ingest(content: string, options?: IngestOptions): IngestCandidate[];
|
|
1376
|
-
/** Install a pack from a source path. */
|
|
1377
|
-
installPack(source: string):
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
/** Export engrams as a shareable pack. */
|
|
1435
|
+
/** Install a pack from a source path. Detects conflicts with existing engrams. */
|
|
1436
|
+
installPack(source: string): ReturnType<typeof installPack>;
|
|
1437
|
+
/** Uninstall a pack by name. */
|
|
1438
|
+
uninstallPack(name: string): ReturnType<typeof uninstallPack>;
|
|
1439
|
+
/** Export engrams as a shareable pack with privacy scanning and integrity hash. */
|
|
1382
1440
|
exportPack(engrams: Engram[], outputDir: string, manifest: {
|
|
1383
1441
|
name: string;
|
|
1384
1442
|
version: string;
|
|
1385
1443
|
description?: string;
|
|
1386
1444
|
creator?: string;
|
|
1387
|
-
}):
|
|
1388
|
-
|
|
1389
|
-
engram_count: number;
|
|
1390
|
-
};
|
|
1391
|
-
/** List all installed packs. */
|
|
1445
|
+
}): ReturnType<typeof exportPack>;
|
|
1446
|
+
/** List all installed packs (with integrity hashes). */
|
|
1392
1447
|
listPacks(): ReturnType<typeof listPacks>;
|
|
1393
1448
|
/** Sync engrams to git. Initializes repo on first call, commits + push/pull on subsequent calls. */
|
|
1394
1449
|
sync(remote?: string): SyncResult;
|
package/dist/index.js
CHANGED
|
@@ -295,6 +295,16 @@ function loadAllPacks(packsDir) {
|
|
|
295
295
|
}
|
|
296
296
|
return packs;
|
|
297
297
|
}
|
|
298
|
+
function storePrefix(scope) {
|
|
299
|
+
const parts = scope.split(/[:\-_./]/).filter(Boolean);
|
|
300
|
+
if (parts.length >= 2) {
|
|
301
|
+
const p2 = parts[1];
|
|
302
|
+
return (parts[0][0] + p2[0] + (p2[1] || p2[0])).toUpperCase();
|
|
303
|
+
}
|
|
304
|
+
const w = parts[0] || scope;
|
|
305
|
+
if (w.length >= 3) return (w[0] + w[Math.floor(w.length / 2)] + w[w.length - 1]).toUpperCase();
|
|
306
|
+
return (w[0] + (w[1] || w[0]) + (w[2] || w[0])).toUpperCase();
|
|
307
|
+
}
|
|
298
308
|
function generateEngramId(existing) {
|
|
299
309
|
const now = /* @__PURE__ */ new Date();
|
|
300
310
|
const date = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
@@ -322,10 +332,12 @@ function getDatabase() {
|
|
|
322
332
|
var IndexedStorage = class {
|
|
323
333
|
dbPath;
|
|
324
334
|
engramsPath;
|
|
335
|
+
stores;
|
|
325
336
|
db = null;
|
|
326
|
-
constructor(engramsPath, dbPath) {
|
|
337
|
+
constructor(engramsPath, dbPath, stores) {
|
|
327
338
|
this.engramsPath = engramsPath;
|
|
328
339
|
this.dbPath = dbPath;
|
|
340
|
+
this.stores = stores ?? [];
|
|
329
341
|
}
|
|
330
342
|
getDb() {
|
|
331
343
|
if (!this.db) {
|
|
@@ -345,6 +357,11 @@ var IndexedStorage = class {
|
|
|
345
357
|
CREATE INDEX IF NOT EXISTS idx_scope ON engrams(scope);
|
|
346
358
|
CREATE INDEX IF NOT EXISTS idx_domain ON engrams(domain);
|
|
347
359
|
`);
|
|
360
|
+
try {
|
|
361
|
+
this.db.exec("ALTER TABLE engrams ADD COLUMN source TEXT NOT NULL DEFAULT 'primary'");
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_source ON engrams(source)");
|
|
348
365
|
}
|
|
349
366
|
return this.db;
|
|
350
367
|
}
|
|
@@ -392,25 +409,41 @@ var IndexedStorage = class {
|
|
|
392
409
|
}
|
|
393
410
|
return db.prepare("SELECT COUNT(*) as c FROM engrams").get().c;
|
|
394
411
|
}
|
|
395
|
-
/** Sync SQLite index from YAML source of truth. */
|
|
412
|
+
/** Sync SQLite index from YAML source of truth (primary + all stores). */
|
|
396
413
|
syncFromYaml() {
|
|
397
|
-
const engrams = loadEngrams(this.engramsPath);
|
|
398
414
|
const db = this.getDb();
|
|
399
415
|
const upsert = db.prepare(`
|
|
400
|
-
INSERT OR REPLACE INTO engrams (id, status, scope, domain, last_accessed, data)
|
|
401
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
416
|
+
INSERT OR REPLACE INTO engrams (id, status, scope, domain, last_accessed, data, source)
|
|
417
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
402
418
|
`);
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
const dbIds = new Set(
|
|
406
|
-
db.prepare("SELECT id FROM engrams").all().map((r) => r.id)
|
|
407
|
-
);
|
|
419
|
+
const allSyncedIds = /* @__PURE__ */ new Set();
|
|
420
|
+
const validSources = /* @__PURE__ */ new Set(["primary"]);
|
|
408
421
|
const tx = db.transaction(() => {
|
|
409
|
-
|
|
410
|
-
|
|
422
|
+
const primaryEngrams = loadEngrams(this.engramsPath);
|
|
423
|
+
for (const e of primaryEngrams) {
|
|
424
|
+
upsert.run(e.id, e.status, e.scope, e.domain ?? null, e.activation.last_accessed, JSON.stringify(e), "primary");
|
|
425
|
+
allSyncedIds.add(e.id);
|
|
426
|
+
}
|
|
427
|
+
for (const store of this.stores) {
|
|
428
|
+
validSources.add(store.path);
|
|
429
|
+
const storeEngrams = loadEngrams(store.path);
|
|
430
|
+
const prefix = storePrefix(store.scope);
|
|
431
|
+
for (const e of storeEngrams) {
|
|
432
|
+
if (e.scope !== "global" && e.scope !== store.scope && !e.scope.startsWith(store.scope)) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const nsId = e.id.replace(/^(ENG|ABS|META)-/, `$1-${prefix}-`);
|
|
436
|
+
const scope = e.scope === "global" ? store.scope : e.scope;
|
|
437
|
+
upsert.run(nsId, e.status, scope, e.domain ?? null, e.activation.last_accessed, JSON.stringify({ ...e, id: nsId, scope }), store.path);
|
|
438
|
+
allSyncedIds.add(nsId);
|
|
439
|
+
}
|
|
411
440
|
}
|
|
412
|
-
|
|
413
|
-
|
|
441
|
+
const dbRows = db.prepare("SELECT id, source FROM engrams").all();
|
|
442
|
+
const deleteStmt = db.prepare("DELETE FROM engrams WHERE id = ?");
|
|
443
|
+
for (const row of dbRows) {
|
|
444
|
+
if (!allSyncedIds.has(row.id)) {
|
|
445
|
+
deleteStmt.run(row.id);
|
|
446
|
+
}
|
|
414
447
|
}
|
|
415
448
|
});
|
|
416
449
|
tx();
|
|
@@ -456,7 +489,7 @@ var PlurConfigSchema = z3.object({
|
|
|
456
489
|
co_access: z3.boolean().default(true)
|
|
457
490
|
}).default({}),
|
|
458
491
|
allow_secrets: z3.boolean().default(false),
|
|
459
|
-
index: z3.boolean().default(
|
|
492
|
+
index: z3.boolean().default(true),
|
|
460
493
|
stores: z3.array(StoreEntrySchema).default([])
|
|
461
494
|
}).partial();
|
|
462
495
|
|
|
@@ -1029,25 +1062,110 @@ async function expandedSearch(engrams, query, limit, llm, storagePath) {
|
|
|
1029
1062
|
// src/packs.ts
|
|
1030
1063
|
import * as fs2 from "fs";
|
|
1031
1064
|
import * as path from "path";
|
|
1065
|
+
import * as crypto from "crypto";
|
|
1032
1066
|
import yaml4 from "js-yaml";
|
|
1033
|
-
|
|
1067
|
+
|
|
1068
|
+
// src/secrets.ts
|
|
1069
|
+
var SECRET_PATTERNS = [
|
|
1070
|
+
{ name: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/ },
|
|
1071
|
+
{ name: "aws_secret_key", regex: /(?:aws_secret_access_key|secret_access_key)\s*[=:]\s*[A-Za-z0-9/+=]{40}/i },
|
|
1072
|
+
{ name: "generic_api_key", regex: /(?:^|[^a-z])(sk|pk)[-_][a-z0-9]{20,}/i },
|
|
1073
|
+
{ name: "api_key_assignment", regex: /(?:api[_-]?key|api[_-]?secret|secret[_-]?key)\s*[=:]\s*\S{20,}/i },
|
|
1074
|
+
{ name: "password_assignment", regex: /password\s*[=:]\s*\S{8,}/i },
|
|
1075
|
+
{ name: "connection_string", regex: /(?:postgres|mysql|mongodb|redis):\/\/\S+/ },
|
|
1076
|
+
{ name: "jwt", regex: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}/ },
|
|
1077
|
+
{ name: "private_key", regex: /-----BEGIN\s+\S+\s+PRIVATE KEY-----/ },
|
|
1078
|
+
{ name: "bearer_token", regex: /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/ }
|
|
1079
|
+
];
|
|
1080
|
+
function detectSecrets(text) {
|
|
1081
|
+
const matches = [];
|
|
1082
|
+
for (const { name, regex } of SECRET_PATTERNS) {
|
|
1083
|
+
const m = text.match(regex);
|
|
1084
|
+
if (m) {
|
|
1085
|
+
matches.push({ pattern: name, match: m[0].slice(0, 20) + "..." });
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return matches;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// src/packs.ts
|
|
1092
|
+
function detectConflicts2(newEngrams, existingEngrams) {
|
|
1093
|
+
const conflicts = [];
|
|
1094
|
+
for (const ne of newEngrams) {
|
|
1095
|
+
for (const ee of existingEngrams) {
|
|
1096
|
+
const nNorm = ne.statement.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1097
|
+
const eNorm = ee.statement.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1098
|
+
if (nNorm === eNorm) {
|
|
1099
|
+
conflicts.push({
|
|
1100
|
+
pack_engram_id: ne.id,
|
|
1101
|
+
pack_statement: ne.statement.slice(0, 120),
|
|
1102
|
+
existing_engram_id: ee.id,
|
|
1103
|
+
existing_statement: ee.statement.slice(0, 120),
|
|
1104
|
+
type: "duplicate"
|
|
1105
|
+
});
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (ne.domain && ee.domain && ne.domain === ee.domain) {
|
|
1109
|
+
const nHasNever = /\b(never|don't|do not|avoid|stop)\b/i.test(ne.statement);
|
|
1110
|
+
const eHasNever = /\b(never|don't|do not|avoid|stop)\b/i.test(ee.statement);
|
|
1111
|
+
const nHasAlways = /\b(always|must|should|prefer|use)\b/i.test(ne.statement);
|
|
1112
|
+
const eHasAlways = /\b(always|must|should|prefer|use)\b/i.test(ee.statement);
|
|
1113
|
+
if (nHasNever && eHasAlways || nHasAlways && eHasNever) {
|
|
1114
|
+
const nWords = new Set(nNorm.split(" ").filter((w) => w.length > 4));
|
|
1115
|
+
const eWords = new Set(eNorm.split(" ").filter((w) => w.length > 4));
|
|
1116
|
+
const overlap = [...nWords].filter((w) => eWords.has(w));
|
|
1117
|
+
if (overlap.length >= 2) {
|
|
1118
|
+
conflicts.push({
|
|
1119
|
+
pack_engram_id: ne.id,
|
|
1120
|
+
pack_statement: ne.statement.slice(0, 120),
|
|
1121
|
+
existing_engram_id: ee.id,
|
|
1122
|
+
existing_statement: ee.statement.slice(0, 120),
|
|
1123
|
+
type: "contradiction"
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return conflicts;
|
|
1131
|
+
}
|
|
1132
|
+
function installPack(packsDir, source, existingEngrams) {
|
|
1034
1133
|
if (!fs2.existsSync(source)) throw new Error(`Pack source not found: ${source}`);
|
|
1035
1134
|
const sourceName = path.basename(source);
|
|
1036
1135
|
const destDir = path.join(packsDir, sourceName);
|
|
1037
1136
|
if (!fs2.existsSync(destDir)) fs2.mkdirSync(destDir, { recursive: true });
|
|
1038
1137
|
const files = fs2.readdirSync(source);
|
|
1039
|
-
let copied = 0;
|
|
1040
1138
|
for (const file of files) {
|
|
1041
1139
|
const srcPath = path.join(source, file);
|
|
1042
1140
|
const destPath = path.join(destDir, file);
|
|
1043
1141
|
if (fs2.statSync(srcPath).isFile()) {
|
|
1044
1142
|
fs2.copyFileSync(srcPath, destPath);
|
|
1045
|
-
copied++;
|
|
1046
1143
|
}
|
|
1047
1144
|
}
|
|
1048
1145
|
const engramsPath = path.join(destDir, "engrams.yaml");
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1146
|
+
const newEngrams = fs2.existsSync(engramsPath) ? loadEngrams(engramsPath) : [];
|
|
1147
|
+
const conflicts = existingEngrams ? detectConflicts2(newEngrams, existingEngrams) : [];
|
|
1148
|
+
return { installed: newEngrams.length, name: sourceName, conflicts };
|
|
1149
|
+
}
|
|
1150
|
+
function uninstallPack(packsDir, name) {
|
|
1151
|
+
let packDir = path.join(packsDir, name);
|
|
1152
|
+
if (!fs2.existsSync(packDir)) {
|
|
1153
|
+
const entries = fs2.existsSync(packsDir) ? fs2.readdirSync(packsDir) : [];
|
|
1154
|
+
const match = entries.find((e) => e.toLowerCase() === name.toLowerCase());
|
|
1155
|
+
if (match) {
|
|
1156
|
+
packDir = path.join(packsDir, match);
|
|
1157
|
+
} else {
|
|
1158
|
+
throw new Error(`Pack not found: ${name}. Use 'plur packs list' to see installed packs.`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
const engramsPath = path.join(packDir, "engrams.yaml");
|
|
1162
|
+
let count = 0;
|
|
1163
|
+
try {
|
|
1164
|
+
count = loadEngrams(engramsPath).length;
|
|
1165
|
+
} catch {
|
|
1166
|
+
}
|
|
1167
|
+
fs2.rmSync(packDir, { recursive: true, force: true });
|
|
1168
|
+
return { name, removed: true, engram_count: count };
|
|
1051
1169
|
}
|
|
1052
1170
|
function listPacks(packsDir) {
|
|
1053
1171
|
if (!fs2.existsSync(packsDir)) return [];
|
|
@@ -1061,7 +1179,8 @@ function listPacks(packsDir) {
|
|
|
1061
1179
|
name: pack.manifest.name,
|
|
1062
1180
|
path: packDir,
|
|
1063
1181
|
engram_count: pack.engrams.length,
|
|
1064
|
-
manifest: pack.manifest
|
|
1182
|
+
manifest: pack.manifest,
|
|
1183
|
+
integrity: computePackHash(packDir)
|
|
1065
1184
|
});
|
|
1066
1185
|
} catch {
|
|
1067
1186
|
const engramsPath = path.join(packDir, "engrams.yaml");
|
|
@@ -1075,7 +1194,83 @@ function listPacks(packsDir) {
|
|
|
1075
1194
|
}
|
|
1076
1195
|
return result;
|
|
1077
1196
|
}
|
|
1197
|
+
var PERSONAL_PATH_RE = /(?:\/Users\/\w+|\/home\/\w+|~\/|C:\\Users\\\w+)/;
|
|
1198
|
+
var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
1199
|
+
var IP_RE = /\b(?:10|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b/;
|
|
1200
|
+
function scanPrivacy(engrams) {
|
|
1201
|
+
const issues = [];
|
|
1202
|
+
for (const e of engrams) {
|
|
1203
|
+
if (e.visibility === "private") {
|
|
1204
|
+
issues.push({
|
|
1205
|
+
engram_id: e.id,
|
|
1206
|
+
type: "private_visibility",
|
|
1207
|
+
detail: `Engram marked as private \u2014 skipped from export`
|
|
1208
|
+
});
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
const text = `${e.statement} ${e.rationale ?? ""} ${e.source ?? ""}`;
|
|
1212
|
+
const secrets = detectSecrets(text);
|
|
1213
|
+
for (const s of secrets) {
|
|
1214
|
+
issues.push({
|
|
1215
|
+
engram_id: e.id,
|
|
1216
|
+
type: "secret",
|
|
1217
|
+
detail: `${s.pattern}: ${s.match}`
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
if (PERSONAL_PATH_RE.test(text)) {
|
|
1221
|
+
issues.push({
|
|
1222
|
+
engram_id: e.id,
|
|
1223
|
+
type: "personal_path",
|
|
1224
|
+
detail: `Contains personal path: ${text.match(PERSONAL_PATH_RE)?.[0]}`
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
const emailMatch = text.match(EMAIL_RE);
|
|
1228
|
+
if (emailMatch) {
|
|
1229
|
+
issues.push({
|
|
1230
|
+
engram_id: e.id,
|
|
1231
|
+
type: "email",
|
|
1232
|
+
detail: `Contains email: ${emailMatch[0]}`
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
const ipMatch = text.match(IP_RE);
|
|
1236
|
+
if (ipMatch) {
|
|
1237
|
+
issues.push({
|
|
1238
|
+
engram_id: e.id,
|
|
1239
|
+
type: "ip_address",
|
|
1240
|
+
detail: `Contains private IP: ${ipMatch[0]}`
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return { clean: issues.length === 0, issues };
|
|
1245
|
+
}
|
|
1246
|
+
function deriveMatchTerms(engrams) {
|
|
1247
|
+
const termCounts = /* @__PURE__ */ new Map();
|
|
1248
|
+
for (const e of engrams) {
|
|
1249
|
+
if (e.tags) {
|
|
1250
|
+
for (const t of e.tags) {
|
|
1251
|
+
termCounts.set(t, (termCounts.get(t) || 0) + 1);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if (e.domain) {
|
|
1255
|
+
for (const part of e.domain.split(".")) {
|
|
1256
|
+
if (part.length > 2) {
|
|
1257
|
+
termCounts.set(part, (termCounts.get(part) || 0) + 1);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (e.type) {
|
|
1262
|
+
termCounts.set(e.type, (termCounts.get(e.type) || 0) + 1);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return [...termCounts.entries()].filter(([, count]) => count >= 2).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([term]) => term);
|
|
1266
|
+
}
|
|
1078
1267
|
function exportPack(engrams, outputDir, manifest) {
|
|
1268
|
+
const allPrivacy = scanPrivacy(engrams);
|
|
1269
|
+
const blockedIds = new Set(
|
|
1270
|
+
allPrivacy.issues.filter((i) => i.type === "secret" || i.type === "private_visibility").map((i) => i.engram_id)
|
|
1271
|
+
);
|
|
1272
|
+
const safeEngrams = engrams.filter((e) => !blockedIds.has(e.id));
|
|
1273
|
+
const matchTerms = deriveMatchTerms(safeEngrams);
|
|
1079
1274
|
if (!fs2.existsSync(outputDir)) fs2.mkdirSync(outputDir, { recursive: true });
|
|
1080
1275
|
const frontmatter = yaml4.dump({
|
|
1081
1276
|
name: manifest.name,
|
|
@@ -1084,8 +1279,8 @@ function exportPack(engrams, outputDir, manifest) {
|
|
|
1084
1279
|
creator: manifest.creator,
|
|
1085
1280
|
metadata: {
|
|
1086
1281
|
injection_policy: "on_match",
|
|
1087
|
-
match_terms:
|
|
1088
|
-
engram_count:
|
|
1282
|
+
match_terms: matchTerms,
|
|
1283
|
+
engram_count: safeEngrams.length
|
|
1089
1284
|
}
|
|
1090
1285
|
});
|
|
1091
1286
|
fs2.writeFileSync(
|
|
@@ -1098,32 +1293,60 @@ ${frontmatter}---
|
|
|
1098
1293
|
${manifest.description || ""}
|
|
1099
1294
|
`
|
|
1100
1295
|
);
|
|
1101
|
-
const
|
|
1296
|
+
const exportEngrams = safeEngrams.map((e) => {
|
|
1297
|
+
const cleaned = { ...e };
|
|
1298
|
+
if (cleaned.relations) {
|
|
1299
|
+
cleaned.relations = {
|
|
1300
|
+
...cleaned.relations,
|
|
1301
|
+
conflicts: [],
|
|
1302
|
+
related: []
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
if (cleaned.associations) {
|
|
1306
|
+
cleaned.associations = [];
|
|
1307
|
+
}
|
|
1308
|
+
if (cleaned.knowledge_anchors) {
|
|
1309
|
+
cleaned.knowledge_anchors = [];
|
|
1310
|
+
}
|
|
1311
|
+
if (cleaned.activation) {
|
|
1312
|
+
cleaned.activation = {
|
|
1313
|
+
...cleaned.activation,
|
|
1314
|
+
frequency: 0,
|
|
1315
|
+
retrieval_strength: 0.7
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
if (cleaned.feedback_signals) {
|
|
1319
|
+
cleaned.feedback_signals = { positive: 0, negative: 0, neutral: 0 };
|
|
1320
|
+
}
|
|
1321
|
+
return cleaned;
|
|
1322
|
+
});
|
|
1323
|
+
const content = yaml4.dump({ engrams: exportEngrams }, { lineWidth: 120, noRefs: true, quotingType: '"' });
|
|
1102
1324
|
fs2.writeFileSync(path.join(outputDir, "engrams.yaml"), content);
|
|
1103
|
-
|
|
1325
|
+
const integrity = computePackHash(outputDir);
|
|
1326
|
+
fs2.writeFileSync(path.join(outputDir, "INTEGRITY"), `sha256:${integrity}
|
|
1327
|
+
`);
|
|
1328
|
+
return {
|
|
1329
|
+
path: outputDir,
|
|
1330
|
+
engram_count: safeEngrams.length,
|
|
1331
|
+
privacy: allPrivacy,
|
|
1332
|
+
match_terms: matchTerms,
|
|
1333
|
+
integrity: `sha256:${integrity}`
|
|
1334
|
+
};
|
|
1104
1335
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
{ name: "connection_string", regex: /(?:postgres|mysql|mongodb|redis):\/\/\S+/ },
|
|
1114
|
-
{ name: "jwt", regex: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}/ },
|
|
1115
|
-
{ name: "private_key", regex: /-----BEGIN\s+\S+\s+PRIVATE KEY-----/ },
|
|
1116
|
-
{ name: "bearer_token", regex: /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/ }
|
|
1117
|
-
];
|
|
1118
|
-
function detectSecrets(text) {
|
|
1119
|
-
const matches = [];
|
|
1120
|
-
for (const { name, regex } of SECRET_PATTERNS) {
|
|
1121
|
-
const m = text.match(regex);
|
|
1122
|
-
if (m) {
|
|
1123
|
-
matches.push({ pattern: name, match: m[0].slice(0, 20) + "..." });
|
|
1124
|
-
}
|
|
1336
|
+
function computePackHash(packDir) {
|
|
1337
|
+
const hash = crypto.createHash("sha256");
|
|
1338
|
+
const skillMd = path.join(packDir, "SKILL.md");
|
|
1339
|
+
const manifestYaml = path.join(packDir, "manifest.yaml");
|
|
1340
|
+
if (fs2.existsSync(skillMd)) {
|
|
1341
|
+
hash.update(fs2.readFileSync(skillMd));
|
|
1342
|
+
} else if (fs2.existsSync(manifestYaml)) {
|
|
1343
|
+
hash.update(fs2.readFileSync(manifestYaml));
|
|
1125
1344
|
}
|
|
1126
|
-
|
|
1345
|
+
const engramsPath = path.join(packDir, "engrams.yaml");
|
|
1346
|
+
if (fs2.existsSync(engramsPath)) {
|
|
1347
|
+
hash.update(fs2.readFileSync(engramsPath));
|
|
1348
|
+
}
|
|
1349
|
+
return hash.digest("hex");
|
|
1127
1350
|
}
|
|
1128
1351
|
|
|
1129
1352
|
// src/meta/sanitize.ts
|
|
@@ -1978,13 +2201,79 @@ var Plur = class {
|
|
|
1978
2201
|
paths;
|
|
1979
2202
|
config;
|
|
1980
2203
|
indexedStorage = null;
|
|
2204
|
+
_engramCache = /* @__PURE__ */ new Map();
|
|
1981
2205
|
constructor(options) {
|
|
1982
2206
|
this.paths = detectPlurStorage(options?.path);
|
|
1983
2207
|
this.config = loadConfig(this.paths.config);
|
|
1984
2208
|
if (this.config.index) {
|
|
1985
|
-
this.indexedStorage = new IndexedStorage(this.paths.engrams, this.paths.db);
|
|
2209
|
+
this.indexedStorage = new IndexedStorage(this.paths.engrams, this.paths.db, this.config.stores);
|
|
1986
2210
|
}
|
|
1987
2211
|
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Load engrams from primary store + all configured stores, with mtime-based caching.
|
|
2214
|
+
* Store engram IDs get namespaced: ENG-2026-0401-001 → ENG-DF-2026-0401-001.
|
|
2215
|
+
* Primary engrams are returned unchanged.
|
|
2216
|
+
*/
|
|
2217
|
+
_loadAllEngrams() {
|
|
2218
|
+
const primary = this._loadCached(this.paths.engrams);
|
|
2219
|
+
const stores = this.config.stores ?? [];
|
|
2220
|
+
if (stores.length === 0) return primary;
|
|
2221
|
+
const all = [...primary];
|
|
2222
|
+
for (const store of stores) {
|
|
2223
|
+
const storeEngrams = this._loadCached(store.path);
|
|
2224
|
+
const prefix = storePrefix(store.scope);
|
|
2225
|
+
for (const e of storeEngrams) {
|
|
2226
|
+
if (e.scope !== "global" && e.scope !== store.scope && !e.scope.startsWith(store.scope)) {
|
|
2227
|
+
logger.debug(`Skipping engram ${e.id} from store ${store.scope}: scope mismatch (${e.scope})`);
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
const cloned = { ...e };
|
|
2231
|
+
if (cloned.scope === "global") {
|
|
2232
|
+
cloned.scope = store.scope;
|
|
2233
|
+
}
|
|
2234
|
+
const originalId = cloned.id;
|
|
2235
|
+
cloned.id = cloned.id.replace(/^(ENG|ABS|META)-/, `$1-${prefix}-`);
|
|
2236
|
+
cloned._originalId = originalId;
|
|
2237
|
+
cloned._storeScope = store.scope;
|
|
2238
|
+
all.push(cloned);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
return all;
|
|
2242
|
+
}
|
|
2243
|
+
/** Load engrams from a path with mtime-based caching */
|
|
2244
|
+
_loadCached(path2) {
|
|
2245
|
+
let mtime = 0;
|
|
2246
|
+
try {
|
|
2247
|
+
mtime = fs3.statSync(path2).mtimeMs;
|
|
2248
|
+
} catch {
|
|
2249
|
+
return [];
|
|
2250
|
+
}
|
|
2251
|
+
const cached = this._engramCache.get(path2);
|
|
2252
|
+
if (cached && cached.mtime === mtime) return cached.engrams;
|
|
2253
|
+
const engrams = loadEngrams(path2);
|
|
2254
|
+
this._engramCache.set(path2, { mtime, engrams });
|
|
2255
|
+
return engrams;
|
|
2256
|
+
}
|
|
2257
|
+
/** Find which store owns an engram by ID. For namespaced IDs, strips prefix to find in store. */
|
|
2258
|
+
_findEngramStore(id) {
|
|
2259
|
+
const primaryEngrams = this._loadCached(this.paths.engrams);
|
|
2260
|
+
if (primaryEngrams.find((e) => e.id === id)) {
|
|
2261
|
+
return { path: this.paths.engrams, readonly: false, originalId: id };
|
|
2262
|
+
}
|
|
2263
|
+
const stores = this.config.stores ?? [];
|
|
2264
|
+
for (const store of stores) {
|
|
2265
|
+
const prefix = storePrefix(store.scope);
|
|
2266
|
+
const nsPattern = new RegExp(`^(ENG|ABS|META)-${prefix}-`);
|
|
2267
|
+
if (nsPattern.test(id)) {
|
|
2268
|
+
const originalId = id.replace(nsPattern, "$1-");
|
|
2269
|
+
const storeEngrams = this._loadCached(store.path);
|
|
2270
|
+
if (storeEngrams.find((e) => e.id === originalId)) {
|
|
2271
|
+
return { path: store.path, readonly: store.readonly ?? false, originalId };
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
return null;
|
|
2276
|
+
}
|
|
1988
2277
|
/** Create engram, detect conflicts, save. Returns the created engram. */
|
|
1989
2278
|
learn(statement, context) {
|
|
1990
2279
|
if (!this.config.allow_secrets) {
|
|
@@ -1995,10 +2284,11 @@ var Plur = class {
|
|
|
1995
2284
|
}
|
|
1996
2285
|
return withLock(this.paths.engrams, () => {
|
|
1997
2286
|
const engrams = loadEngrams(this.paths.engrams);
|
|
1998
|
-
const
|
|
2287
|
+
const allEngrams = this._loadAllEngrams();
|
|
2288
|
+
const id = generateEngramId(allEngrams);
|
|
1999
2289
|
const scope = context?.scope ?? "global";
|
|
2000
2290
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2001
|
-
const conflictingEngrams = detectConflicts({ statement, scope },
|
|
2291
|
+
const conflictingEngrams = detectConflicts({ statement, scope }, allEngrams);
|
|
2002
2292
|
const conflictIds = conflictingEngrams.map((e) => e.id);
|
|
2003
2293
|
const engram = {
|
|
2004
2294
|
id,
|
|
@@ -2091,9 +2381,9 @@ var Plur = class {
|
|
|
2091
2381
|
this._reactivateResults(results);
|
|
2092
2382
|
return results;
|
|
2093
2383
|
}
|
|
2094
|
-
/** Get a single engram by ID, regardless of status.
|
|
2384
|
+
/** Get a single engram by ID, regardless of status. Searches primary + all stores. */
|
|
2095
2385
|
getById(id) {
|
|
2096
|
-
const engrams =
|
|
2386
|
+
const engrams = this._loadAllEngrams();
|
|
2097
2387
|
return engrams.find((e) => e.id === id) ?? null;
|
|
2098
2388
|
}
|
|
2099
2389
|
/** List all active engrams, optionally filtered by scope/domain. No search — returns all matches. */
|
|
@@ -2110,7 +2400,7 @@ var Plur = class {
|
|
|
2110
2400
|
domain: options?.domain
|
|
2111
2401
|
});
|
|
2112
2402
|
} else {
|
|
2113
|
-
engrams =
|
|
2403
|
+
engrams = this._loadAllEngrams();
|
|
2114
2404
|
engrams = engrams.filter((e) => e.status === "active");
|
|
2115
2405
|
if (options?.domain) {
|
|
2116
2406
|
engrams = engrams.filter((e) => e.domain?.startsWith(options.domain));
|
|
@@ -2136,9 +2426,12 @@ var Plur = class {
|
|
|
2136
2426
|
/** Reactivate accessed engrams and update co-access associations */
|
|
2137
2427
|
_reactivateResults(results) {
|
|
2138
2428
|
if (results.length === 0) return;
|
|
2429
|
+
const isStoreEngram = (e) => e._originalId || /^(ENG|ABS|META)-[A-Z]{3}-/.test(e.id);
|
|
2430
|
+
const primaryResults = results.filter((e) => !isStoreEngram(e));
|
|
2431
|
+
if (primaryResults.length === 0) return;
|
|
2139
2432
|
withLock(this.paths.engrams, () => {
|
|
2140
2433
|
const allEngrams = loadEngrams(this.paths.engrams);
|
|
2141
|
-
const resultIds = new Set(
|
|
2434
|
+
const resultIds = new Set(primaryResults.map((e) => e.id));
|
|
2142
2435
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2143
2436
|
let modified = false;
|
|
2144
2437
|
for (const e of allEngrams) {
|
|
@@ -2194,7 +2487,7 @@ var Plur = class {
|
|
|
2194
2487
|
async injectHybrid(task, options) {
|
|
2195
2488
|
let embeddingBoosts;
|
|
2196
2489
|
try {
|
|
2197
|
-
const engrams =
|
|
2490
|
+
const engrams = this._loadAllEngrams().filter((e) => e.status === "active");
|
|
2198
2491
|
const results = await embeddingSearch(engrams, task, engrams.length, this.paths.root);
|
|
2199
2492
|
if (results.length > 0) {
|
|
2200
2493
|
embeddingBoosts = /* @__PURE__ */ new Map();
|
|
@@ -2207,7 +2500,7 @@ var Plur = class {
|
|
|
2207
2500
|
return this._formatInjection(task, options, embeddingBoosts);
|
|
2208
2501
|
}
|
|
2209
2502
|
_formatInjection(task, options, embeddingBoosts) {
|
|
2210
|
-
const engrams =
|
|
2503
|
+
const engrams = this._loadAllEngrams();
|
|
2211
2504
|
const packs = loadAllPacks(this.paths.packs);
|
|
2212
2505
|
const budget = options?.budget ?? this.config.injection_budget ?? 2e3;
|
|
2213
2506
|
const result = selectAndSpread(
|
|
@@ -2247,7 +2540,7 @@ var Plur = class {
|
|
|
2247
2540
|
injected_ids
|
|
2248
2541
|
};
|
|
2249
2542
|
}
|
|
2250
|
-
/** Update feedback_signals and adjust retrieval_strength. Searches
|
|
2543
|
+
/** Update feedback_signals and adjust retrieval_strength. Searches primary, stores, then packs. */
|
|
2251
2544
|
feedback(id, signal) {
|
|
2252
2545
|
const found = withLock(this.paths.engrams, () => {
|
|
2253
2546
|
const engrams = loadEngrams(this.paths.engrams);
|
|
@@ -2267,6 +2560,29 @@ var Plur = class {
|
|
|
2267
2560
|
return true;
|
|
2268
2561
|
});
|
|
2269
2562
|
if (found) return;
|
|
2563
|
+
const storeInfo = this._findEngramStore(id);
|
|
2564
|
+
if (storeInfo && storeInfo.path !== this.paths.engrams) {
|
|
2565
|
+
if (storeInfo.readonly) {
|
|
2566
|
+
throw new Error("Engram is in a readonly store");
|
|
2567
|
+
}
|
|
2568
|
+
const storeEngrams = loadEngrams(storeInfo.path);
|
|
2569
|
+
const engram = storeEngrams.find((e) => e.id === storeInfo.originalId);
|
|
2570
|
+
if (engram) {
|
|
2571
|
+
if (!engram.feedback_signals) {
|
|
2572
|
+
engram.feedback_signals = { positive: 0, negative: 0, neutral: 0 };
|
|
2573
|
+
}
|
|
2574
|
+
engram.feedback_signals[signal] += 1;
|
|
2575
|
+
if (signal === "positive") {
|
|
2576
|
+
engram.activation.retrieval_strength = Math.min(1, engram.activation.retrieval_strength + 0.05);
|
|
2577
|
+
} else if (signal === "negative") {
|
|
2578
|
+
engram.activation.retrieval_strength = Math.max(0, engram.activation.retrieval_strength - 0.1);
|
|
2579
|
+
}
|
|
2580
|
+
saveEngrams(storeInfo.path, storeEngrams);
|
|
2581
|
+
this._engramCache.delete(storeInfo.path);
|
|
2582
|
+
this._syncIndex();
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2270
2586
|
this._feedbackPack(id, signal);
|
|
2271
2587
|
}
|
|
2272
2588
|
/** Save extracted meta-engrams to the engram store. Skips IDs that already exist. */
|
|
@@ -2303,19 +2619,40 @@ var Plur = class {
|
|
|
2303
2619
|
return true;
|
|
2304
2620
|
});
|
|
2305
2621
|
}
|
|
2306
|
-
/** Set engram status to 'retired'. */
|
|
2622
|
+
/** Set engram status to 'retired'. Supports primary and store engrams. */
|
|
2307
2623
|
forget(id, reason) {
|
|
2308
|
-
withLock(this.paths.engrams, () => {
|
|
2624
|
+
const foundInPrimary = withLock(this.paths.engrams, () => {
|
|
2309
2625
|
const engrams = loadEngrams(this.paths.engrams);
|
|
2310
2626
|
const engram = engrams.find((e) => e.id === id);
|
|
2311
|
-
if (!engram)
|
|
2627
|
+
if (!engram) return false;
|
|
2312
2628
|
engram.status = "retired";
|
|
2313
2629
|
if (reason && !engram.rationale) {
|
|
2314
2630
|
engram.rationale = `Retired: ${reason}`;
|
|
2315
2631
|
}
|
|
2316
2632
|
saveEngrams(this.paths.engrams, engrams);
|
|
2317
2633
|
this._syncIndex();
|
|
2634
|
+
return true;
|
|
2318
2635
|
});
|
|
2636
|
+
if (foundInPrimary) return;
|
|
2637
|
+
const storeInfo = this._findEngramStore(id);
|
|
2638
|
+
if (storeInfo && storeInfo.path !== this.paths.engrams) {
|
|
2639
|
+
if (storeInfo.readonly) {
|
|
2640
|
+
throw new Error("Cannot retire engram from readonly store");
|
|
2641
|
+
}
|
|
2642
|
+
const storeEngrams = loadEngrams(storeInfo.path);
|
|
2643
|
+
const engram = storeEngrams.find((e) => e.id === storeInfo.originalId);
|
|
2644
|
+
if (engram) {
|
|
2645
|
+
engram.status = "retired";
|
|
2646
|
+
if (reason && !engram.rationale) {
|
|
2647
|
+
engram.rationale = `Retired: ${reason}`;
|
|
2648
|
+
}
|
|
2649
|
+
saveEngrams(storeInfo.path, storeEngrams);
|
|
2650
|
+
this._engramCache.delete(storeInfo.path);
|
|
2651
|
+
this._syncIndex();
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
throw new Error(`Engram not found: ${id}`);
|
|
2319
2656
|
}
|
|
2320
2657
|
/** Remove retired engrams from storage. Returns count of removed and remaining. */
|
|
2321
2658
|
compact() {
|
|
@@ -2333,7 +2670,7 @@ var Plur = class {
|
|
|
2333
2670
|
/** Rebuild SQLite index from YAML source of truth. Only works when index: true. */
|
|
2334
2671
|
reindex() {
|
|
2335
2672
|
if (!this.indexedStorage) {
|
|
2336
|
-
this.indexedStorage = new IndexedStorage(this.paths.engrams, this.paths.db);
|
|
2673
|
+
this.indexedStorage = new IndexedStorage(this.paths.engrams, this.paths.db, this.config.stores);
|
|
2337
2674
|
}
|
|
2338
2675
|
this.indexedStorage.reindex();
|
|
2339
2676
|
}
|
|
@@ -2408,15 +2745,20 @@ var Plur = class {
|
|
|
2408
2745
|
}
|
|
2409
2746
|
return candidates;
|
|
2410
2747
|
}
|
|
2411
|
-
/** Install a pack from a source path. */
|
|
2748
|
+
/** Install a pack from a source path. Detects conflicts with existing engrams. */
|
|
2412
2749
|
installPack(source) {
|
|
2413
|
-
|
|
2750
|
+
const existing = this._loadAllEngrams();
|
|
2751
|
+
return installPack(this.paths.packs, source, existing);
|
|
2752
|
+
}
|
|
2753
|
+
/** Uninstall a pack by name. */
|
|
2754
|
+
uninstallPack(name) {
|
|
2755
|
+
return uninstallPack(this.paths.packs, name);
|
|
2414
2756
|
}
|
|
2415
|
-
/** Export engrams as a shareable pack. */
|
|
2757
|
+
/** Export engrams as a shareable pack with privacy scanning and integrity hash. */
|
|
2416
2758
|
exportPack(engrams, outputDir, manifest) {
|
|
2417
2759
|
return exportPack(engrams, outputDir, manifest);
|
|
2418
2760
|
}
|
|
2419
|
-
/** List all installed packs. */
|
|
2761
|
+
/** List all installed packs (with integrity hashes). */
|
|
2420
2762
|
listPacks() {
|
|
2421
2763
|
return listPacks(this.paths.packs);
|
|
2422
2764
|
}
|
|
@@ -2430,7 +2772,7 @@ var Plur = class {
|
|
|
2430
2772
|
}
|
|
2431
2773
|
/** Return system health info. */
|
|
2432
2774
|
status() {
|
|
2433
|
-
const engrams =
|
|
2775
|
+
const engrams = this._loadAllEngrams();
|
|
2434
2776
|
const episodes = queryTimeline(this.paths.episodes);
|
|
2435
2777
|
const packs = listPacks(this.paths.packs);
|
|
2436
2778
|
return {
|
|
@@ -2470,12 +2812,12 @@ var Plur = class {
|
|
|
2470
2812
|
scope: "global",
|
|
2471
2813
|
shared: false,
|
|
2472
2814
|
readonly: false,
|
|
2473
|
-
engram_count:
|
|
2815
|
+
engram_count: this._loadCached(this.paths.engrams).filter((e) => e.status !== "retired").length
|
|
2474
2816
|
};
|
|
2475
2817
|
const additional = stores.map((s) => {
|
|
2476
2818
|
let count = 0;
|
|
2477
2819
|
try {
|
|
2478
|
-
count =
|
|
2820
|
+
count = this._loadCached(s.path).filter((e) => e.status !== "retired").length;
|
|
2479
2821
|
} catch {
|
|
2480
2822
|
}
|
|
2481
2823
|
return { ...s, engram_count: count };
|