@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 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" | "recursive" | "tradeoff" | "freeform";
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" | "recursive" | "tradeoff" | "freeform" | undefined;
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" | "recursive" | "tradeoff" | "freeform";
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" | "recursive" | "tradeoff" | "freeform" | undefined;
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" | "recursive" | "tradeoff" | "freeform";
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" | "recursive" | "tradeoff" | "freeform" | undefined;
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. Returns null if not found. */
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 packs if not found in personal engrams. */
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
- installed: number;
1379
- name: string;
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
- path: string;
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 deleteStmt = db.prepare("DELETE FROM engrams WHERE id = ?");
404
- const yamlIds = new Set(engrams.map((e) => e.id));
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
- for (const e of engrams) {
410
- upsert.run(e.id, e.status, e.scope, e.domain ?? null, e.activation.last_accessed, JSON.stringify(e));
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
- for (const id of dbIds) {
413
- if (!yamlIds.has(id)) deleteStmt.run(id);
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(false),
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
- function installPack(packsDir, source) {
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 engrams = fs2.existsSync(engramsPath) ? loadEngrams(engramsPath) : [];
1050
- return { installed: engrams.length, name: sourceName };
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: engrams.length
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 content = yaml4.dump({ engrams }, { lineWidth: 120, noRefs: true, quotingType: '"' });
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
- return { path: outputDir, engram_count: engrams.length };
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
- // src/secrets.ts
1107
- var SECRET_PATTERNS = [
1108
- { name: "aws_access_key", regex: /AKIA[0-9A-Z]{16}/ },
1109
- { name: "aws_secret_key", regex: /(?:aws_secret_access_key|secret_access_key)\s*[=:]\s*[A-Za-z0-9/+=]{40}/i },
1110
- { name: "generic_api_key", regex: /(?:^|[^a-z])(sk|pk)[-_][a-z0-9]{20,}/i },
1111
- { name: "api_key_assignment", regex: /(?:api[_-]?key|api[_-]?secret|secret[_-]?key)\s*[=:]\s*\S{20,}/i },
1112
- { name: "password_assignment", regex: /password\s*[=:]\s*\S{8,}/i },
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
- return matches;
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 id = generateEngramId(engrams);
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 }, engrams);
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. Returns null if not found. */
2384
+ /** Get a single engram by ID, regardless of status. Searches primary + all stores. */
2095
2385
  getById(id) {
2096
- const engrams = loadEngrams(this.paths.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 = loadEngrams(this.paths.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(results.map((e) => e.id));
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 = loadEngrams(this.paths.engrams).filter((e) => e.status === "active");
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 = loadEngrams(this.paths.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 packs if not found in personal engrams. */
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) throw new Error(`Engram not found: ${id}`);
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
- return installPack(this.paths.packs, source);
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 = loadEngrams(this.paths.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: loadEngrams(this.paths.engrams).filter((e) => e.status !== "retired").length
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 = loadEngrams(s.path).filter((e) => e.status !== "retired").length;
2820
+ count = this._loadCached(s.path).filter((e) => e.status !== "retired").length;
2479
2821
  } catch {
2480
2822
  }
2481
2823
  return { ...s, engram_count: count };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plur-ai/core",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",