@plur-ai/core 0.5.1 → 0.6.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
@@ -1234,8 +1234,9 @@ declare function detectPlurStorage(explicitPath?: string): PlurPaths;
1234
1234
  declare class IndexedStorage {
1235
1235
  private dbPath;
1236
1236
  private engramsPath;
1237
+ private stores;
1237
1238
  private db;
1238
- constructor(engramsPath: string, dbPath: string);
1239
+ constructor(engramsPath: string, dbPath: string, stores?: StoreEntry[]);
1239
1240
  private getDb;
1240
1241
  /** Load all engrams from SQLite index. Auto-rebuilds if db missing. */
1241
1242
  loadAll(): Engram[];
@@ -1249,7 +1250,7 @@ declare class IndexedStorage {
1249
1250
  count(filter?: {
1250
1251
  status?: string;
1251
1252
  }): number;
1252
- /** Sync SQLite index from YAML source of truth. */
1253
+ /** Sync SQLite index from YAML source of truth (primary + all stores). */
1253
1254
  syncFromYaml(): void;
1254
1255
  /** Drop and rebuild the entire index from YAML. */
1255
1256
  reindex(): void;
@@ -1303,9 +1304,20 @@ declare class Plur {
1303
1304
  private paths;
1304
1305
  private config;
1305
1306
  private indexedStorage;
1307
+ private _engramCache;
1306
1308
  constructor(options?: {
1307
1309
  path?: string;
1308
1310
  });
1311
+ /**
1312
+ * Load engrams from primary store + all configured stores, with mtime-based caching.
1313
+ * Store engram IDs get namespaced: ENG-2026-0401-001 → ENG-DF-2026-0401-001.
1314
+ * Primary engrams are returned unchanged.
1315
+ */
1316
+ private _loadAllEngrams;
1317
+ /** Load engrams from a path with mtime-based caching */
1318
+ private _loadCached;
1319
+ /** Find which store owns an engram by ID. For namespaced IDs, strips prefix to find in store. */
1320
+ private _findEngramStore;
1309
1321
  /** Create engram, detect conflicts, save. Returns the created engram. */
1310
1322
  learn(statement: string, context?: LearnContext): Engram;
1311
1323
  /**
@@ -1328,7 +1340,7 @@ declare class Plur {
1328
1340
  recallExpanded(query: string, options: RecallOptions & {
1329
1341
  llm: LlmFunction;
1330
1342
  }): Promise<Engram[]>;
1331
- /** Get a single engram by ID, regardless of status. Returns null if not found. */
1343
+ /** Get a single engram by ID, regardless of status. Searches primary + all stores. */
1332
1344
  getById(id: string): Engram | null;
1333
1345
  /** List all active engrams, optionally filtered by scope/domain. No search — returns all matches. */
1334
1346
  list(options?: {
@@ -1345,7 +1357,7 @@ declare class Plur {
1345
1357
  /** Scored injection with embedding boost when available. Falls back to BM25 if embeddings not installed. */
1346
1358
  injectHybrid(task: string, options?: InjectOptions): Promise<InjectionResult>;
1347
1359
  private _formatInjection;
1348
- /** Update feedback_signals and adjust retrieval_strength. Searches packs if not found in personal engrams. */
1360
+ /** Update feedback_signals and adjust retrieval_strength. Searches primary, stores, then packs. */
1349
1361
  feedback(id: string, signal: 'positive' | 'negative' | 'neutral'): void;
1350
1362
  /** Save extracted meta-engrams to the engram store. Skips IDs that already exist. */
1351
1363
  saveMetaEngrams(metas: Engram[]): {
@@ -1354,7 +1366,7 @@ declare class Plur {
1354
1366
  };
1355
1367
  /** Update an existing engram in the store by ID. Returns true if found and updated. */
1356
1368
  updateEngram(updated: Engram): boolean;
1357
- /** Set engram status to 'retired'. */
1369
+ /** Set engram status to 'retired'. Supports primary and store engrams. */
1358
1370
  forget(id: string, reason?: string): void;
1359
1371
  /** Remove retired engrams from storage. Returns count of removed and remaining. */
1360
1372
  compact(): {
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);
411
426
  }
412
- for (const id of dbIds) {
413
- if (!yamlIds.has(id)) deleteStmt.run(id);
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
+ }
440
+ }
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
 
@@ -1978,13 +2011,79 @@ var Plur = class {
1978
2011
  paths;
1979
2012
  config;
1980
2013
  indexedStorage = null;
2014
+ _engramCache = /* @__PURE__ */ new Map();
1981
2015
  constructor(options) {
1982
2016
  this.paths = detectPlurStorage(options?.path);
1983
2017
  this.config = loadConfig(this.paths.config);
1984
2018
  if (this.config.index) {
1985
- this.indexedStorage = new IndexedStorage(this.paths.engrams, this.paths.db);
2019
+ this.indexedStorage = new IndexedStorage(this.paths.engrams, this.paths.db, this.config.stores);
1986
2020
  }
1987
2021
  }
2022
+ /**
2023
+ * Load engrams from primary store + all configured stores, with mtime-based caching.
2024
+ * Store engram IDs get namespaced: ENG-2026-0401-001 → ENG-DF-2026-0401-001.
2025
+ * Primary engrams are returned unchanged.
2026
+ */
2027
+ _loadAllEngrams() {
2028
+ const primary = this._loadCached(this.paths.engrams);
2029
+ const stores = this.config.stores ?? [];
2030
+ if (stores.length === 0) return primary;
2031
+ const all = [...primary];
2032
+ for (const store of stores) {
2033
+ const storeEngrams = this._loadCached(store.path);
2034
+ const prefix = storePrefix(store.scope);
2035
+ for (const e of storeEngrams) {
2036
+ if (e.scope !== "global" && e.scope !== store.scope && !e.scope.startsWith(store.scope)) {
2037
+ logger.debug(`Skipping engram ${e.id} from store ${store.scope}: scope mismatch (${e.scope})`);
2038
+ continue;
2039
+ }
2040
+ const cloned = { ...e };
2041
+ if (cloned.scope === "global") {
2042
+ cloned.scope = store.scope;
2043
+ }
2044
+ const originalId = cloned.id;
2045
+ cloned.id = cloned.id.replace(/^(ENG|ABS|META)-/, `$1-${prefix}-`);
2046
+ cloned._originalId = originalId;
2047
+ cloned._storeScope = store.scope;
2048
+ all.push(cloned);
2049
+ }
2050
+ }
2051
+ return all;
2052
+ }
2053
+ /** Load engrams from a path with mtime-based caching */
2054
+ _loadCached(path2) {
2055
+ let mtime = 0;
2056
+ try {
2057
+ mtime = fs3.statSync(path2).mtimeMs;
2058
+ } catch {
2059
+ return [];
2060
+ }
2061
+ const cached = this._engramCache.get(path2);
2062
+ if (cached && cached.mtime === mtime) return cached.engrams;
2063
+ const engrams = loadEngrams(path2);
2064
+ this._engramCache.set(path2, { mtime, engrams });
2065
+ return engrams;
2066
+ }
2067
+ /** Find which store owns an engram by ID. For namespaced IDs, strips prefix to find in store. */
2068
+ _findEngramStore(id) {
2069
+ const primaryEngrams = this._loadCached(this.paths.engrams);
2070
+ if (primaryEngrams.find((e) => e.id === id)) {
2071
+ return { path: this.paths.engrams, readonly: false, originalId: id };
2072
+ }
2073
+ const stores = this.config.stores ?? [];
2074
+ for (const store of stores) {
2075
+ const prefix = storePrefix(store.scope);
2076
+ const nsPattern = new RegExp(`^(ENG|ABS|META)-${prefix}-`);
2077
+ if (nsPattern.test(id)) {
2078
+ const originalId = id.replace(nsPattern, "$1-");
2079
+ const storeEngrams = this._loadCached(store.path);
2080
+ if (storeEngrams.find((e) => e.id === originalId)) {
2081
+ return { path: store.path, readonly: store.readonly ?? false, originalId };
2082
+ }
2083
+ }
2084
+ }
2085
+ return null;
2086
+ }
1988
2087
  /** Create engram, detect conflicts, save. Returns the created engram. */
1989
2088
  learn(statement, context) {
1990
2089
  if (!this.config.allow_secrets) {
@@ -1995,10 +2094,11 @@ var Plur = class {
1995
2094
  }
1996
2095
  return withLock(this.paths.engrams, () => {
1997
2096
  const engrams = loadEngrams(this.paths.engrams);
1998
- const id = generateEngramId(engrams);
2097
+ const allEngrams = this._loadAllEngrams();
2098
+ const id = generateEngramId(allEngrams);
1999
2099
  const scope = context?.scope ?? "global";
2000
2100
  const now = (/* @__PURE__ */ new Date()).toISOString();
2001
- const conflictingEngrams = detectConflicts({ statement, scope }, engrams);
2101
+ const conflictingEngrams = detectConflicts({ statement, scope }, allEngrams);
2002
2102
  const conflictIds = conflictingEngrams.map((e) => e.id);
2003
2103
  const engram = {
2004
2104
  id,
@@ -2091,9 +2191,9 @@ var Plur = class {
2091
2191
  this._reactivateResults(results);
2092
2192
  return results;
2093
2193
  }
2094
- /** Get a single engram by ID, regardless of status. Returns null if not found. */
2194
+ /** Get a single engram by ID, regardless of status. Searches primary + all stores. */
2095
2195
  getById(id) {
2096
- const engrams = loadEngrams(this.paths.engrams);
2196
+ const engrams = this._loadAllEngrams();
2097
2197
  return engrams.find((e) => e.id === id) ?? null;
2098
2198
  }
2099
2199
  /** List all active engrams, optionally filtered by scope/domain. No search — returns all matches. */
@@ -2110,7 +2210,7 @@ var Plur = class {
2110
2210
  domain: options?.domain
2111
2211
  });
2112
2212
  } else {
2113
- engrams = loadEngrams(this.paths.engrams);
2213
+ engrams = this._loadAllEngrams();
2114
2214
  engrams = engrams.filter((e) => e.status === "active");
2115
2215
  if (options?.domain) {
2116
2216
  engrams = engrams.filter((e) => e.domain?.startsWith(options.domain));
@@ -2136,9 +2236,12 @@ var Plur = class {
2136
2236
  /** Reactivate accessed engrams and update co-access associations */
2137
2237
  _reactivateResults(results) {
2138
2238
  if (results.length === 0) return;
2239
+ const isStoreEngram = (e) => e._originalId || /^(ENG|ABS|META)-[A-Z]{3}-/.test(e.id);
2240
+ const primaryResults = results.filter((e) => !isStoreEngram(e));
2241
+ if (primaryResults.length === 0) return;
2139
2242
  withLock(this.paths.engrams, () => {
2140
2243
  const allEngrams = loadEngrams(this.paths.engrams);
2141
- const resultIds = new Set(results.map((e) => e.id));
2244
+ const resultIds = new Set(primaryResults.map((e) => e.id));
2142
2245
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2143
2246
  let modified = false;
2144
2247
  for (const e of allEngrams) {
@@ -2194,7 +2297,7 @@ var Plur = class {
2194
2297
  async injectHybrid(task, options) {
2195
2298
  let embeddingBoosts;
2196
2299
  try {
2197
- const engrams = loadEngrams(this.paths.engrams).filter((e) => e.status === "active");
2300
+ const engrams = this._loadAllEngrams().filter((e) => e.status === "active");
2198
2301
  const results = await embeddingSearch(engrams, task, engrams.length, this.paths.root);
2199
2302
  if (results.length > 0) {
2200
2303
  embeddingBoosts = /* @__PURE__ */ new Map();
@@ -2207,7 +2310,7 @@ var Plur = class {
2207
2310
  return this._formatInjection(task, options, embeddingBoosts);
2208
2311
  }
2209
2312
  _formatInjection(task, options, embeddingBoosts) {
2210
- const engrams = loadEngrams(this.paths.engrams);
2313
+ const engrams = this._loadAllEngrams();
2211
2314
  const packs = loadAllPacks(this.paths.packs);
2212
2315
  const budget = options?.budget ?? this.config.injection_budget ?? 2e3;
2213
2316
  const result = selectAndSpread(
@@ -2247,7 +2350,7 @@ var Plur = class {
2247
2350
  injected_ids
2248
2351
  };
2249
2352
  }
2250
- /** Update feedback_signals and adjust retrieval_strength. Searches packs if not found in personal engrams. */
2353
+ /** Update feedback_signals and adjust retrieval_strength. Searches primary, stores, then packs. */
2251
2354
  feedback(id, signal) {
2252
2355
  const found = withLock(this.paths.engrams, () => {
2253
2356
  const engrams = loadEngrams(this.paths.engrams);
@@ -2267,6 +2370,29 @@ var Plur = class {
2267
2370
  return true;
2268
2371
  });
2269
2372
  if (found) return;
2373
+ const storeInfo = this._findEngramStore(id);
2374
+ if (storeInfo && storeInfo.path !== this.paths.engrams) {
2375
+ if (storeInfo.readonly) {
2376
+ throw new Error("Engram is in a readonly store");
2377
+ }
2378
+ const storeEngrams = loadEngrams(storeInfo.path);
2379
+ const engram = storeEngrams.find((e) => e.id === storeInfo.originalId);
2380
+ if (engram) {
2381
+ if (!engram.feedback_signals) {
2382
+ engram.feedback_signals = { positive: 0, negative: 0, neutral: 0 };
2383
+ }
2384
+ engram.feedback_signals[signal] += 1;
2385
+ if (signal === "positive") {
2386
+ engram.activation.retrieval_strength = Math.min(1, engram.activation.retrieval_strength + 0.05);
2387
+ } else if (signal === "negative") {
2388
+ engram.activation.retrieval_strength = Math.max(0, engram.activation.retrieval_strength - 0.1);
2389
+ }
2390
+ saveEngrams(storeInfo.path, storeEngrams);
2391
+ this._engramCache.delete(storeInfo.path);
2392
+ this._syncIndex();
2393
+ return;
2394
+ }
2395
+ }
2270
2396
  this._feedbackPack(id, signal);
2271
2397
  }
2272
2398
  /** Save extracted meta-engrams to the engram store. Skips IDs that already exist. */
@@ -2303,19 +2429,40 @@ var Plur = class {
2303
2429
  return true;
2304
2430
  });
2305
2431
  }
2306
- /** Set engram status to 'retired'. */
2432
+ /** Set engram status to 'retired'. Supports primary and store engrams. */
2307
2433
  forget(id, reason) {
2308
- withLock(this.paths.engrams, () => {
2434
+ const foundInPrimary = withLock(this.paths.engrams, () => {
2309
2435
  const engrams = loadEngrams(this.paths.engrams);
2310
2436
  const engram = engrams.find((e) => e.id === id);
2311
- if (!engram) throw new Error(`Engram not found: ${id}`);
2437
+ if (!engram) return false;
2312
2438
  engram.status = "retired";
2313
2439
  if (reason && !engram.rationale) {
2314
2440
  engram.rationale = `Retired: ${reason}`;
2315
2441
  }
2316
2442
  saveEngrams(this.paths.engrams, engrams);
2317
2443
  this._syncIndex();
2444
+ return true;
2318
2445
  });
2446
+ if (foundInPrimary) return;
2447
+ const storeInfo = this._findEngramStore(id);
2448
+ if (storeInfo && storeInfo.path !== this.paths.engrams) {
2449
+ if (storeInfo.readonly) {
2450
+ throw new Error("Cannot retire engram from readonly store");
2451
+ }
2452
+ const storeEngrams = loadEngrams(storeInfo.path);
2453
+ const engram = storeEngrams.find((e) => e.id === storeInfo.originalId);
2454
+ if (engram) {
2455
+ engram.status = "retired";
2456
+ if (reason && !engram.rationale) {
2457
+ engram.rationale = `Retired: ${reason}`;
2458
+ }
2459
+ saveEngrams(storeInfo.path, storeEngrams);
2460
+ this._engramCache.delete(storeInfo.path);
2461
+ this._syncIndex();
2462
+ return;
2463
+ }
2464
+ }
2465
+ throw new Error(`Engram not found: ${id}`);
2319
2466
  }
2320
2467
  /** Remove retired engrams from storage. Returns count of removed and remaining. */
2321
2468
  compact() {
@@ -2333,7 +2480,7 @@ var Plur = class {
2333
2480
  /** Rebuild SQLite index from YAML source of truth. Only works when index: true. */
2334
2481
  reindex() {
2335
2482
  if (!this.indexedStorage) {
2336
- this.indexedStorage = new IndexedStorage(this.paths.engrams, this.paths.db);
2483
+ this.indexedStorage = new IndexedStorage(this.paths.engrams, this.paths.db, this.config.stores);
2337
2484
  }
2338
2485
  this.indexedStorage.reindex();
2339
2486
  }
@@ -2430,7 +2577,7 @@ var Plur = class {
2430
2577
  }
2431
2578
  /** Return system health info. */
2432
2579
  status() {
2433
- const engrams = loadEngrams(this.paths.engrams);
2580
+ const engrams = this._loadAllEngrams();
2434
2581
  const episodes = queryTimeline(this.paths.episodes);
2435
2582
  const packs = listPacks(this.paths.packs);
2436
2583
  return {
@@ -2470,12 +2617,12 @@ var Plur = class {
2470
2617
  scope: "global",
2471
2618
  shared: false,
2472
2619
  readonly: false,
2473
- engram_count: loadEngrams(this.paths.engrams).filter((e) => e.status !== "retired").length
2620
+ engram_count: this._loadCached(this.paths.engrams).filter((e) => e.status !== "retired").length
2474
2621
  };
2475
2622
  const additional = stores.map((s) => {
2476
2623
  let count = 0;
2477
2624
  try {
2478
- count = loadEngrams(s.path).filter((e) => e.status !== "retired").length;
2625
+ count = this._loadCached(s.path).filter((e) => e.status !== "retired").length;
2479
2626
  } catch {
2480
2627
  }
2481
2628
  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.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",