@plur-ai/core 0.9.4 → 0.9.5

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
@@ -635,19 +635,45 @@ declare const EpisodeSchema: z.ZodObject<{
635
635
  }>;
636
636
  type Episode = z.infer<typeof EpisodeSchema>;
637
637
 
638
- declare const StoreEntrySchema: z.ZodObject<{
639
- path: z.ZodString;
638
+ /**
639
+ * A store can be either:
640
+ * - filesystem (path) — historical default; YAML or SQLite
641
+ * - remote (url + token) — speaks to a PLUR Enterprise server over HTTP
642
+ * Exactly one of path/url must be present.
643
+ */
644
+ declare const StoreEntrySchema: z.ZodEffects<z.ZodObject<{
645
+ path: z.ZodOptional<z.ZodString>;
646
+ url: z.ZodOptional<z.ZodString>;
647
+ token: z.ZodOptional<z.ZodString>;
640
648
  scope: z.ZodString;
641
649
  shared: z.ZodDefault<z.ZodBoolean>;
642
650
  readonly: z.ZodDefault<z.ZodBoolean>;
643
651
  }, "strip", z.ZodTypeAny, {
644
- path: string;
645
652
  scope: string;
646
653
  shared: boolean;
647
654
  readonly: boolean;
655
+ path?: string | undefined;
656
+ url?: string | undefined;
657
+ token?: string | undefined;
658
+ }, {
659
+ scope: string;
660
+ path?: string | undefined;
661
+ url?: string | undefined;
662
+ token?: string | undefined;
663
+ shared?: boolean | undefined;
664
+ readonly?: boolean | undefined;
665
+ }>, {
666
+ scope: string;
667
+ shared: boolean;
668
+ readonly: boolean;
669
+ path?: string | undefined;
670
+ url?: string | undefined;
671
+ token?: string | undefined;
648
672
  }, {
649
- path: string;
650
673
  scope: string;
674
+ path?: string | undefined;
675
+ url?: string | undefined;
676
+ token?: string | undefined;
651
677
  shared?: boolean | undefined;
652
678
  readonly?: boolean | undefined;
653
679
  }>;
@@ -705,19 +731,39 @@ declare const PlurConfigSchema: z.ZodObject<{
705
731
  }, {
706
732
  enabled?: boolean | undefined;
707
733
  }>>>;
708
- stores: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodObject<{
709
- path: z.ZodString;
734
+ stores: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodObject<{
735
+ path: z.ZodOptional<z.ZodString>;
736
+ url: z.ZodOptional<z.ZodString>;
737
+ token: z.ZodOptional<z.ZodString>;
710
738
  scope: z.ZodString;
711
739
  shared: z.ZodDefault<z.ZodBoolean>;
712
740
  readonly: z.ZodDefault<z.ZodBoolean>;
713
741
  }, "strip", z.ZodTypeAny, {
714
- path: string;
715
742
  scope: string;
716
743
  shared: boolean;
717
744
  readonly: boolean;
745
+ path?: string | undefined;
746
+ url?: string | undefined;
747
+ token?: string | undefined;
748
+ }, {
749
+ scope: string;
750
+ path?: string | undefined;
751
+ url?: string | undefined;
752
+ token?: string | undefined;
753
+ shared?: boolean | undefined;
754
+ readonly?: boolean | undefined;
755
+ }>, {
756
+ scope: string;
757
+ shared: boolean;
758
+ readonly: boolean;
759
+ path?: string | undefined;
760
+ url?: string | undefined;
761
+ token?: string | undefined;
718
762
  }, {
719
- path: string;
720
763
  scope: string;
764
+ path?: string | undefined;
765
+ url?: string | undefined;
766
+ token?: string | undefined;
721
767
  shared?: boolean | undefined;
722
768
  readonly?: boolean | undefined;
723
769
  }>, "many">>>;
@@ -778,10 +824,12 @@ declare const PlurConfigSchema: z.ZodObject<{
778
824
  enabled?: boolean | undefined;
779
825
  } | undefined;
780
826
  stores?: {
781
- path: string;
782
827
  scope: string;
783
828
  shared: boolean;
784
829
  readonly: boolean;
830
+ path?: string | undefined;
831
+ url?: string | undefined;
832
+ token?: string | undefined;
785
833
  }[] | undefined;
786
834
  profile?: {
787
835
  enabled?: boolean | undefined;
@@ -821,8 +869,10 @@ declare const PlurConfigSchema: z.ZodObject<{
821
869
  enabled?: boolean | undefined;
822
870
  } | undefined;
823
871
  stores?: {
824
- path: string;
825
872
  scope: string;
873
+ path?: string | undefined;
874
+ url?: string | undefined;
875
+ token?: string | undefined;
826
876
  shared?: boolean | undefined;
827
877
  readonly?: boolean | undefined;
828
878
  }[] | undefined;
@@ -2175,6 +2225,21 @@ declare class Plur {
2175
2225
  private _loadAllEngrams;
2176
2226
  /** Load engrams from a path with mtime-based caching */
2177
2227
  private _loadCached;
2228
+ /**
2229
+ * Per-instance pool of RemoteStore drivers, keyed by url+scope.
2230
+ * RemoteStore holds its own internal TTL cache so repeated load()
2231
+ * within ttlMs returns the same array without a network call.
2232
+ *
2233
+ * Note `_loadAllEngrams` is sync but RemoteStore.load() is async.
2234
+ * We bridge that by returning whatever's in the driver's cache
2235
+ * synchronously and triggering a background refresh on cache miss.
2236
+ * The first call after server start returns [] for that store; the
2237
+ * call after the first refresh sees the data. For our pilot this
2238
+ * is acceptable — recall is expected to be tried more than once
2239
+ * in any real session.
2240
+ */
2241
+ private _remoteStores;
2242
+ private _loadRemoteCached;
2178
2243
  /**
2179
2244
  * Write engrams to disk and invalidate the cache for that path.
2180
2245
  *
@@ -2350,10 +2415,23 @@ declare class Plur {
2350
2415
  }>;
2351
2416
  /** Return system health info. */
2352
2417
  status(): StatusResult;
2353
- /** Register an additional engram store. */
2418
+ /**
2419
+ * Register an additional engram store.
2420
+ *
2421
+ * Two shapes — exactly one of `pathOrUrl` semantics applies:
2422
+ * - filesystem (default): pass a path. `options.url` undefined.
2423
+ * - remote (PLUR Enterprise / any compatible REST API):
2424
+ * pass any string for the first arg (it goes into a slot we
2425
+ * never read), set `options.url` + `options.token`.
2426
+ *
2427
+ * Backwards compatible: existing call sites that pass a filesystem
2428
+ * path keep working.
2429
+ */
2354
2430
  addStore(storePath: string, scope: string, options?: {
2355
2431
  shared?: boolean;
2356
2432
  readonly?: boolean;
2433
+ url?: string;
2434
+ token?: string;
2357
2435
  }): void;
2358
2436
  /**
2359
2437
  * Auto-discover .plur/engrams.yaml in CWD and parent dirs (up to git root).
@@ -2366,7 +2444,8 @@ declare class Plur {
2366
2444
  }>;
2367
2445
  /** List all configured stores. */
2368
2446
  listStores(): Array<{
2369
- path: string;
2447
+ path?: string;
2448
+ url?: string;
2370
2449
  scope: string;
2371
2450
  shared: boolean;
2372
2451
  readonly: boolean;
package/dist/index.js CHANGED
@@ -177,6 +177,7 @@ var IndexedStorage = class {
177
177
  allSyncedIds.add(e.id);
178
178
  }
179
179
  for (const store of this.stores) {
180
+ if (!store.path) continue;
180
181
  validSources.add(store.path);
181
182
  const storeEngrams = loadEngrams(store.path);
182
183
  const prefix = storePrefix(store.scope);
@@ -223,11 +224,17 @@ import yaml from "js-yaml";
223
224
  // src/schemas/config.ts
224
225
  import { z } from "zod";
225
226
  var StoreEntrySchema = z.object({
226
- path: z.string(),
227
+ path: z.string().optional(),
228
+ url: z.string().url().optional(),
229
+ token: z.string().optional(),
230
+ // Bearer for remote stores; ignored for path
227
231
  scope: z.string(),
228
232
  shared: z.boolean().default(false),
229
233
  readonly: z.boolean().default(false)
230
- });
234
+ }).refine(
235
+ (s) => Boolean(s.path) !== Boolean(s.url),
236
+ { message: "StoreEntry requires exactly one of path or url" }
237
+ );
231
238
  var LlmTierConfigSchema = z.object({
232
239
  dedup_tier: z.enum(["fast", "balanced", "thorough"]).default("fast"),
233
240
  profile_tier: z.enum(["fast", "balanced", "thorough"]).default("balanced"),
@@ -1516,6 +1523,137 @@ function computePackHash(packDir) {
1516
1523
  return hash.digest("hex");
1517
1524
  }
1518
1525
 
1526
+ // src/store/remote-store.ts
1527
+ var RemoteStore = class {
1528
+ constructor(url, token, scope, opts = {}) {
1529
+ this.url = url;
1530
+ this.token = token;
1531
+ this.scope = scope;
1532
+ this.opts = opts;
1533
+ }
1534
+ cache = null;
1535
+ inFlight = null;
1536
+ get apiBase() {
1537
+ return this.url.replace(/\/sse\/?$/, "").replace(/\/$/, "") + "/api/v1";
1538
+ }
1539
+ get ttlMs() {
1540
+ return this.opts.ttlMs ?? 6e4;
1541
+ }
1542
+ headers(extra = {}) {
1543
+ return {
1544
+ Authorization: `Bearer ${this.token}`,
1545
+ Accept: "application/json",
1546
+ ...extra
1547
+ };
1548
+ }
1549
+ /**
1550
+ * Load all engrams visible to this token at this scope. Cached up to
1551
+ * ttlMs; in-flight calls deduplicate to avoid thundering-herd on
1552
+ * the remote when 5 things ask for engrams at once.
1553
+ */
1554
+ async load() {
1555
+ const now = Date.now();
1556
+ if (this.cache && now - this.cache.ts < this.ttlMs) return this.cache.engrams;
1557
+ if (this.inFlight) return this.inFlight;
1558
+ this.inFlight = (async () => {
1559
+ try {
1560
+ const all = [];
1561
+ let offset = 0;
1562
+ const limit = 200;
1563
+ const maxPages = 50;
1564
+ for (let i = 0; i < maxPages; i++) {
1565
+ const u = `${this.apiBase}/engrams?scope=${encodeURIComponent(this.scope)}&limit=${limit}&offset=${offset}`;
1566
+ const r = await fetch(u, { headers: this.headers() });
1567
+ if (!r.ok) {
1568
+ if (r.status >= 500) console.error(`[plur:remote-store] ${this.url} returned ${r.status} loading scope ${this.scope}`);
1569
+ break;
1570
+ }
1571
+ const body = await r.json();
1572
+ for (const row of body.rows) {
1573
+ const d = row.data ?? {};
1574
+ all.push({
1575
+ id: row.id,
1576
+ scope: row.scope,
1577
+ status: row.status,
1578
+ ...d
1579
+ });
1580
+ }
1581
+ if (all.length >= body.total_count || body.rows.length < limit) break;
1582
+ offset += limit;
1583
+ }
1584
+ this.cache = { ts: Date.now(), engrams: all };
1585
+ return all;
1586
+ } catch (err) {
1587
+ console.error(`[plur:remote-store] ${this.url} load failed: ${err.message}`);
1588
+ return this.cache?.engrams ?? [];
1589
+ } finally {
1590
+ this.inFlight = null;
1591
+ }
1592
+ })();
1593
+ return this.inFlight;
1594
+ }
1595
+ /**
1596
+ * Append a single engram to the remote store. POST /api/v1/engrams
1597
+ * carries statement + scope + domain + type — the server handles
1598
+ * ID assignment, content_hash, status.
1599
+ */
1600
+ async append(engram) {
1601
+ const body = JSON.stringify({
1602
+ statement: engram.statement,
1603
+ scope: engram.scope,
1604
+ domain: engram.domain,
1605
+ type: engram.type
1606
+ });
1607
+ const r = await fetch(`${this.apiBase}/engrams`, {
1608
+ method: "POST",
1609
+ headers: this.headers({ "Content-Type": "application/json" }),
1610
+ body
1611
+ });
1612
+ if (!r.ok) {
1613
+ const text = await r.text().catch(() => "");
1614
+ throw new Error(`Remote store append failed: ${r.status} ${text}`);
1615
+ }
1616
+ this.cache = null;
1617
+ }
1618
+ /**
1619
+ * `save(all)` — used by migrations to bulk-replace. Not supported
1620
+ * on remote: the server keeps an audit trail and we don't want a
1621
+ * single client to be able to nuke + replace the whole store. Throws.
1622
+ */
1623
+ async save(_engrams) {
1624
+ throw new Error("Remote store does not support bulk save() \u2014 use append()/remove() per engram");
1625
+ }
1626
+ async getById(id) {
1627
+ try {
1628
+ const r = await fetch(`${this.apiBase}/engrams/${encodeURIComponent(id)}`, { headers: this.headers() });
1629
+ if (r.status === 404) return null;
1630
+ if (!r.ok) return null;
1631
+ const row = await r.json();
1632
+ return { id: row.id, scope: row.scope, status: row.status, ...row.data ?? {} };
1633
+ } catch {
1634
+ return null;
1635
+ }
1636
+ }
1637
+ /** Remove → DELETE /api/v1/engrams/:id (server soft-retires). */
1638
+ async remove(id) {
1639
+ const r = await fetch(`${this.apiBase}/engrams/${encodeURIComponent(id)}`, {
1640
+ method: "DELETE",
1641
+ headers: this.headers()
1642
+ });
1643
+ if (!r.ok) return false;
1644
+ this.cache = null;
1645
+ return true;
1646
+ }
1647
+ async count(filter) {
1648
+ const all = await this.load();
1649
+ if (filter?.status) return all.filter((e) => e.status === filter.status).length;
1650
+ return all.length;
1651
+ }
1652
+ async close() {
1653
+ this.cache = null;
1654
+ }
1655
+ };
1656
+
1519
1657
  // src/meta/sanitize.ts
1520
1658
  function sanitizeForPrompt(text) {
1521
1659
  return text.replace(/```/g, "~~~").replace(/\n{3,}/g, "\n\n").replace(/^(system|assistant|user):/gim, "$1 -").slice(0, 2e3);
@@ -3220,7 +3358,7 @@ var Plur = class {
3220
3358
  const stores = this.config.stores ?? [];
3221
3359
  const all = [...primary];
3222
3360
  for (const store of stores) {
3223
- const storeEngrams = this._loadCached(store.path);
3361
+ const storeEngrams = store.url ? this._loadRemoteCached(store) : this._loadCached(store.path);
3224
3362
  const prefix = storePrefix(store.scope);
3225
3363
  for (const e of storeEngrams) {
3226
3364
  if (e.scope !== "global" && e.scope !== store.scope && !e.scope.startsWith(store.scope)) {
@@ -3263,6 +3401,32 @@ var Plur = class {
3263
3401
  this._engramCache.set(path3, { mtime, engrams });
3264
3402
  return engrams;
3265
3403
  }
3404
+ /**
3405
+ * Per-instance pool of RemoteStore drivers, keyed by url+scope.
3406
+ * RemoteStore holds its own internal TTL cache so repeated load()
3407
+ * within ttlMs returns the same array without a network call.
3408
+ *
3409
+ * Note `_loadAllEngrams` is sync but RemoteStore.load() is async.
3410
+ * We bridge that by returning whatever's in the driver's cache
3411
+ * synchronously and triggering a background refresh on cache miss.
3412
+ * The first call after server start returns [] for that store; the
3413
+ * call after the first refresh sees the data. For our pilot this
3414
+ * is acceptable — recall is expected to be tried more than once
3415
+ * in any real session.
3416
+ */
3417
+ _remoteStores = /* @__PURE__ */ new Map();
3418
+ _loadRemoteCached(store) {
3419
+ const key = `${store.url}::${store.scope}`;
3420
+ let driver = this._remoteStores.get(key);
3421
+ if (!driver) {
3422
+ driver = new RemoteStore(store.url, store.token ?? "", store.scope);
3423
+ this._remoteStores.set(key, driver);
3424
+ }
3425
+ const cached = driver.cache;
3426
+ void driver.load().catch(() => {
3427
+ });
3428
+ return cached?.engrams ?? [];
3429
+ }
3266
3430
  /**
3267
3431
  * Write engrams to disk and invalidate the cache for that path.
3268
3432
  *
@@ -3286,6 +3450,7 @@ var Plur = class {
3286
3450
  }
3287
3451
  const stores = this.config.stores ?? [];
3288
3452
  for (const store of stores) {
3453
+ if (!store.path) continue;
3289
3454
  const prefix = storePrefix(store.scope);
3290
3455
  const nsPattern = new RegExp(`^(ENG|ABS|META)-${prefix}-`);
3291
3456
  if (nsPattern.test(id)) {
@@ -4130,17 +4295,38 @@ Generate an improved version of the procedure that prevents this failure. Return
4130
4295
  versioned_engram_count: versionedCount
4131
4296
  };
4132
4297
  }
4133
- /** Register an additional engram store. */
4298
+ /**
4299
+ * Register an additional engram store.
4300
+ *
4301
+ * Two shapes — exactly one of `pathOrUrl` semantics applies:
4302
+ * - filesystem (default): pass a path. `options.url` undefined.
4303
+ * - remote (PLUR Enterprise / any compatible REST API):
4304
+ * pass any string for the first arg (it goes into a slot we
4305
+ * never read), set `options.url` + `options.token`.
4306
+ *
4307
+ * Backwards compatible: existing call sites that pass a filesystem
4308
+ * path keep working.
4309
+ */
4134
4310
  addStore(storePath, scope, options) {
4135
4311
  const config = loadConfig(this.paths.config);
4136
- const existing = config.stores?.find((s) => s.path === storePath);
4312
+ const isRemote = Boolean(options?.url);
4313
+ const dedupKey = isRemote ? options.url : storePath;
4314
+ const existing = config.stores?.find((s) => isRemote ? s.url === dedupKey : s.path === dedupKey);
4137
4315
  if (existing) return;
4138
- const stores = [...config.stores ?? [], {
4316
+ const newEntry = isRemote ? {
4317
+ url: options.url,
4318
+ token: options.token,
4319
+ scope,
4320
+ shared: options?.shared ?? true,
4321
+ // remote stores are shared by definition
4322
+ readonly: options?.readonly ?? false
4323
+ } : {
4139
4324
  path: storePath,
4140
4325
  scope,
4141
4326
  shared: options?.shared ?? false,
4142
4327
  readonly: options?.readonly ?? false
4143
- }];
4328
+ };
4329
+ const stores = [...config.stores ?? [], newEntry];
4144
4330
  let configData = {};
4145
4331
  try {
4146
4332
  const raw = fs4.readFileSync(this.paths.config, "utf8");
@@ -4208,11 +4394,25 @@ Generate an improved version of the procedure that prevents this failure. Return
4208
4394
  };
4209
4395
  const additional = stores.map((s) => {
4210
4396
  let count = 0;
4211
- try {
4212
- count = this._loadCached(s.path).filter((e) => e.status !== "retired").length;
4213
- } catch {
4397
+ if (s.url) {
4398
+ try {
4399
+ count = this._loadRemoteCached(s).filter((e) => e.status !== "retired").length;
4400
+ } catch {
4401
+ }
4402
+ } else if (s.path) {
4403
+ try {
4404
+ count = this._loadCached(s.path).filter((e) => e.status !== "retired").length;
4405
+ } catch {
4406
+ }
4214
4407
  }
4215
- return { ...s, engram_count: count };
4408
+ return {
4409
+ path: s.path,
4410
+ url: s.url,
4411
+ scope: s.scope,
4412
+ shared: s.shared,
4413
+ readonly: s.readonly,
4414
+ engram_count: count
4415
+ };
4216
4416
  });
4217
4417
  return [primary, ...additional];
4218
4418
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plur-ai/core",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",