@plur-ai/core 0.1.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.js ADDED
@@ -0,0 +1,962 @@
1
+ // src/storage.ts
2
+ import { existsSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ function detectPlurStorage(explicitPath) {
6
+ const root = explicitPath || process.env.PLUR_PATH || join(homedir(), ".plur");
7
+ if (!existsSync(root)) mkdirSync(root, { recursive: true });
8
+ const packsDir = join(root, "packs");
9
+ if (!existsSync(packsDir)) mkdirSync(packsDir, { recursive: true });
10
+ return {
11
+ root,
12
+ engrams: join(root, "engrams.yaml"),
13
+ episodes: join(root, "episodes.yaml"),
14
+ candidates: join(root, "candidates.yaml"),
15
+ packs: packsDir,
16
+ exchange: join(root, "exchange"),
17
+ config: join(root, "config.yaml")
18
+ };
19
+ }
20
+
21
+ // src/config.ts
22
+ import { existsSync as existsSync2, readFileSync } from "fs";
23
+ import yaml from "js-yaml";
24
+
25
+ // src/schemas/config.ts
26
+ import { z } from "zod";
27
+ var PlurConfigSchema = z.object({
28
+ auto_learn: z.boolean().default(true),
29
+ auto_capture: z.boolean().default(true),
30
+ injection_budget: z.number().default(2e3),
31
+ decay_enabled: z.boolean().default(true),
32
+ decay_threshold: z.number().default(0.15),
33
+ packs: z.array(z.string()).default([]),
34
+ injection: z.object({
35
+ spread_cap: z.number().default(3),
36
+ spread_budget: z.number().default(480),
37
+ co_access: z.boolean().default(true)
38
+ }).default({})
39
+ }).partial();
40
+
41
+ // src/config.ts
42
+ function loadConfig(configPath) {
43
+ if (!existsSync2(configPath)) return PlurConfigSchema.parse({});
44
+ try {
45
+ const raw = yaml.load(readFileSync(configPath, "utf8"));
46
+ return PlurConfigSchema.parse(raw ?? {});
47
+ } catch {
48
+ return PlurConfigSchema.parse({});
49
+ }
50
+ }
51
+
52
+ // src/engrams.ts
53
+ import * as fs from "fs";
54
+ import * as yaml2 from "js-yaml";
55
+
56
+ // src/schemas/engram.ts
57
+ import { z as z2 } from "zod";
58
+ var ActivationSchema = z2.object({
59
+ retrieval_strength: z2.number().min(0).max(1),
60
+ storage_strength: z2.number().min(0).max(1),
61
+ frequency: z2.number().int().min(0),
62
+ last_accessed: z2.string()
63
+ });
64
+ var KnowledgeTypeSchema = z2.object({
65
+ memory_class: z2.enum(["semantic", "episodic", "procedural", "metacognitive"]),
66
+ cognitive_level: z2.enum(["remember", "understand", "apply", "analyze", "evaluate", "create"])
67
+ });
68
+ var KnowledgeAnchorSchema = z2.object({
69
+ path: z2.string(),
70
+ relevance: z2.enum(["primary", "supporting", "example"]).default("supporting"),
71
+ snippet: z2.string().max(200).optional(),
72
+ snippet_extracted_at: z2.string().optional()
73
+ });
74
+ var AssociationSchema = z2.object({
75
+ target_type: z2.enum(["engram", "document"]),
76
+ target: z2.string(),
77
+ strength: z2.number().min(0).max(0.95),
78
+ type: z2.enum(["semantic", "temporal", "causal", "co_accessed"]),
79
+ updated_at: z2.string().optional()
80
+ });
81
+ var DualCodingSchema = z2.object({
82
+ example: z2.string().optional(),
83
+ analogy: z2.string().optional()
84
+ }).refine(
85
+ (d) => d.example || d.analogy,
86
+ "At least one of example or analogy must be provided"
87
+ );
88
+ var RelationsSchema = z2.object({
89
+ broader: z2.array(z2.string()).default([]),
90
+ narrower: z2.array(z2.string()).default([]),
91
+ related: z2.array(z2.string()).default([]),
92
+ conflicts: z2.array(z2.string()).default([])
93
+ });
94
+ var ProvenanceSchema = z2.object({
95
+ origin: z2.string(),
96
+ chain: z2.array(z2.string()).default([]),
97
+ signature: z2.string().nullable().default(null),
98
+ license: z2.string().default("cc-by-sa-4.0")
99
+ });
100
+ var FeedbackSignalsSchema = z2.object({
101
+ positive: z2.number().int().default(0),
102
+ negative: z2.number().int().default(0),
103
+ neutral: z2.number().int().default(0)
104
+ });
105
+ var EngramSchema = z2.object({
106
+ id: z2.string().regex(/^(ENG|ABS)-[A-Za-z0-9-]+$/),
107
+ version: z2.number().int().min(1).default(2),
108
+ status: z2.enum(["active", "dormant", "retired", "candidate"]),
109
+ consolidated: z2.boolean().default(false),
110
+ type: z2.enum(["behavioral", "terminological", "procedural", "architectural"]),
111
+ scope: z2.string(),
112
+ visibility: z2.enum(["private", "public", "template"]).default("private"),
113
+ statement: z2.string().min(1),
114
+ rationale: z2.string().optional(),
115
+ contraindications: z2.array(z2.string()).optional(),
116
+ source_patterns: z2.array(z2.string()).optional(),
117
+ derivation_count: z2.number().int().min(0).default(1),
118
+ knowledge_type: KnowledgeTypeSchema.optional(),
119
+ domain: z2.string().optional(),
120
+ relations: RelationsSchema.optional(),
121
+ activation: ActivationSchema.default({ retrieval_strength: 0.7, storage_strength: 1, frequency: 0, last_accessed: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) }),
122
+ provenance: ProvenanceSchema.optional(),
123
+ feedback_signals: FeedbackSignalsSchema.default({ positive: 0, negative: 0, neutral: 0 }),
124
+ knowledge_anchors: z2.array(KnowledgeAnchorSchema).default([]),
125
+ associations: z2.array(AssociationSchema).default([]),
126
+ dual_coding: DualCodingSchema.optional(),
127
+ tags: z2.array(z2.string()).default([]),
128
+ pack: z2.string().nullable().default(null),
129
+ abstract: z2.string().nullable().default(null),
130
+ derived_from: z2.string().nullable().default(null)
131
+ });
132
+
133
+ // src/schemas/pack.ts
134
+ import { z as z3 } from "zod";
135
+ var PackManifestSchema = z3.object({
136
+ name: z3.string(),
137
+ version: z3.string(),
138
+ description: z3.string().optional(),
139
+ creator: z3.string().optional(),
140
+ license: z3.string().default("cc-by-sa-4.0"),
141
+ tags: z3.array(z3.string()).default([]),
142
+ metadata: z3.object({
143
+ id: z3.string().optional(),
144
+ injection_policy: z3.enum(["on_match", "on_request", "always"]).default("on_match"),
145
+ match_terms: z3.array(z3.string()).default([]),
146
+ domain: z3.string().optional(),
147
+ engram_count: z3.number().optional()
148
+ }).optional(),
149
+ "x-datacore": z3.object({
150
+ id: z3.string(),
151
+ injection_policy: z3.enum(["on_match", "on_request"]),
152
+ match_terms: z3.array(z3.string()).default([]),
153
+ domain: z3.string().optional(),
154
+ engram_count: z3.number().int().min(0)
155
+ }).optional()
156
+ });
157
+
158
+ // src/logger.ts
159
+ var level = process.env.PLUR_LOG_LEVEL || "warning";
160
+ var levels = { debug: 0, info: 1, warning: 2, error: 3 };
161
+ var threshold = levels[level] ?? 2;
162
+ var logger = {
163
+ debug: (...args) => {
164
+ if (threshold <= 0) console.error("[plur:debug]", ...args);
165
+ },
166
+ info: (...args) => {
167
+ if (threshold <= 1) console.error("[plur:info]", ...args);
168
+ },
169
+ warning: (...args) => {
170
+ if (threshold <= 2) console.error("[plur:warning]", ...args);
171
+ },
172
+ error: (...args) => {
173
+ if (threshold <= 3) console.error("[plur:error]", ...args);
174
+ }
175
+ };
176
+
177
+ // src/engrams.ts
178
+ function loadEngrams(filePath) {
179
+ if (!fs.existsSync(filePath)) return [];
180
+ try {
181
+ const raw = yaml2.load(fs.readFileSync(filePath, "utf8"));
182
+ if (!raw?.engrams || !Array.isArray(raw.engrams)) return [];
183
+ const valid = [];
184
+ let skipped = 0;
185
+ for (const entry of raw.engrams) {
186
+ const result = EngramSchema.safeParse(entry);
187
+ if (result.success) valid.push(result.data);
188
+ else skipped++;
189
+ }
190
+ if (skipped > 0) logger.warning(`Skipped ${skipped} invalid engram(s) in ${filePath}`);
191
+ return valid;
192
+ } catch (err) {
193
+ logger.error(`Failed to parse engrams file ${filePath}: ${err}`);
194
+ return [];
195
+ }
196
+ }
197
+ function saveEngrams(filePath, engrams) {
198
+ const content = yaml2.dump({ engrams }, { lineWidth: 120, noRefs: true, quotingType: '"' });
199
+ fs.writeFileSync(filePath, content);
200
+ }
201
+ function parseSkillMdFrontmatter(filePath) {
202
+ const content = fs.readFileSync(filePath, "utf8");
203
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
204
+ if (!match) throw new Error(`No frontmatter found in ${filePath}`);
205
+ return yaml2.load(match[1]);
206
+ }
207
+ function loadPack(packDir) {
208
+ const skillMdPath = `${packDir}/SKILL.md`;
209
+ const manifestYamlPath = `${packDir}/manifest.yaml`;
210
+ const engramsPath = `${packDir}/engrams.yaml`;
211
+ let rawManifest;
212
+ if (fs.existsSync(skillMdPath)) {
213
+ rawManifest = parseSkillMdFrontmatter(skillMdPath);
214
+ } else if (fs.existsSync(manifestYamlPath)) {
215
+ rawManifest = yaml2.load(fs.readFileSync(manifestYamlPath, "utf8"));
216
+ } else {
217
+ throw new Error(`No SKILL.md or manifest.yaml found in ${packDir}`);
218
+ }
219
+ const manifest = PackManifestSchema.parse(rawManifest);
220
+ const engrams = loadEngrams(engramsPath);
221
+ return { manifest, engrams };
222
+ }
223
+ function loadAllPacks(packsDir) {
224
+ if (!fs.existsSync(packsDir)) return [];
225
+ const packs = [];
226
+ for (const entry of fs.readdirSync(packsDir)) {
227
+ const packDir = `${packsDir}/${entry}`;
228
+ if (!fs.statSync(packDir).isDirectory()) continue;
229
+ if (!fs.existsSync(`${packDir}/SKILL.md`) && !fs.existsSync(`${packDir}/manifest.yaml`)) continue;
230
+ try {
231
+ packs.push(loadPack(packDir));
232
+ } catch (err) {
233
+ logger.warning(`Failed to load pack ${entry}: ${err}`);
234
+ }
235
+ }
236
+ return packs;
237
+ }
238
+ function generateEngramId(existing) {
239
+ const now = /* @__PURE__ */ new Date();
240
+ const date = now.toISOString().slice(0, 10).replace(/-/g, "");
241
+ const prefix = `ENG-${date.slice(0, 4)}-${date.slice(4)}-`;
242
+ const existingNums = existing.filter((e) => e.id.startsWith(prefix)).map((e) => parseInt(e.id.slice(prefix.length), 10)).filter((n) => !isNaN(n));
243
+ const next = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1;
244
+ return `${prefix}${String(next).padStart(3, "0")}`;
245
+ }
246
+
247
+ // src/fts.ts
248
+ var STOP_WORDS = /* @__PURE__ */ new Set([
249
+ "the",
250
+ "and",
251
+ "for",
252
+ "that",
253
+ "this",
254
+ "with",
255
+ "from",
256
+ "are",
257
+ "was",
258
+ "were",
259
+ "been",
260
+ "have",
261
+ "has",
262
+ "not",
263
+ "but",
264
+ "its",
265
+ "you",
266
+ "your",
267
+ "can",
268
+ "will",
269
+ "should",
270
+ "would",
271
+ "could",
272
+ "may",
273
+ "might"
274
+ ]);
275
+ function ftsTokenize(text) {
276
+ return text.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 2).filter((w) => !STOP_WORDS.has(w));
277
+ }
278
+ function ftsScore(engram, queryTokens) {
279
+ const statementTokens = ftsTokenize(engram.statement);
280
+ const domainTokens = engram.domain ? ftsTokenize(engram.domain.replace(/\./g, " ")) : [];
281
+ const tagTokens = engram.tags.map((t) => t.toLowerCase());
282
+ const allTerms = [...statementTokens, ...domainTokens, ...tagTokens];
283
+ let matches = 0;
284
+ for (const qt of queryTokens) {
285
+ if (allTerms.some((t) => t.includes(qt) || qt.includes(t))) matches++;
286
+ }
287
+ return queryTokens.length > 0 ? matches / queryTokens.length : 0;
288
+ }
289
+ function searchEngrams(engrams, query, limit = 20) {
290
+ const queryTokens = ftsTokenize(query);
291
+ if (queryTokens.length === 0) return [];
292
+ return engrams.map((e) => ({ engram: e, score: ftsScore(e, queryTokens) })).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((r) => r.engram);
293
+ }
294
+
295
+ // src/decay.ts
296
+ var DECAY_RATE = 0.05;
297
+ var FLOOR = 0.05;
298
+ var MS_PER_DAY = 864e5;
299
+ function decayedStrength(retrievalStrength, daysSinceAccess, lambda = DECAY_RATE) {
300
+ return FLOOR + (retrievalStrength - FLOOR) * Math.exp(-lambda * daysSinceAccess);
301
+ }
302
+ function daysSince(lastAccessed, now) {
303
+ const last = new Date(lastAccessed);
304
+ const current = now || /* @__PURE__ */ new Date();
305
+ return Math.max(0, Math.floor((current.getTime() - last.getTime()) / MS_PER_DAY));
306
+ }
307
+ function reactivate(currentStrength) {
308
+ return Math.min(1, currentStrength + 0.1);
309
+ }
310
+ function decayedCoAccessStrength(strength, daysSinceUpdate, lambda = 0.01) {
311
+ const floor = 0.02;
312
+ return floor + (strength - floor) * Math.exp(-lambda * daysSinceUpdate);
313
+ }
314
+
315
+ // src/inject.ts
316
+ var DEFAULT_MAX_TOKENS = 8e3;
317
+ var DEFAULT_MIN_RELEVANCE = 0.3;
318
+ var MAX_PER_PACK = 5;
319
+ var MAX_PER_DOMAIN = 10;
320
+ var DIP19_CONSIDER_MAX = 5;
321
+ var DIP19_CONSIDER_BUDGET = 200;
322
+ function getPackMetadata(manifest) {
323
+ const meta = manifest["x-datacore"] || manifest.metadata;
324
+ return {
325
+ injection_policy: meta?.injection_policy ?? "on_match",
326
+ match_terms: meta?.match_terms ?? []
327
+ };
328
+ }
329
+ function estimateTokens(engram) {
330
+ const { keyword_match: _km, raw_score: _rs, score: _s, associations: _a, ...wire } = engram;
331
+ const serialized = JSON.stringify(wire);
332
+ return Math.ceil(serialized.length / 4);
333
+ }
334
+ function tokenize(text) {
335
+ return new Set(text.toLowerCase().split(/\W+/).filter((w) => w.length > 2));
336
+ }
337
+ function anchorBoost(engram, taskWords) {
338
+ if (!engram.knowledge_anchors?.length) return 0;
339
+ const threshold2 = taskWords.size <= 1 ? 1 : 2;
340
+ let boost = 0;
341
+ for (const anchor of engram.knowledge_anchors) {
342
+ if (!anchor.snippet) continue;
343
+ const snippetWords = tokenize(anchor.snippet);
344
+ let overlap = 0;
345
+ for (const word of taskWords) {
346
+ if (snippetWords.has(word)) overlap++;
347
+ }
348
+ if (overlap >= threshold2) boost += 0.5;
349
+ }
350
+ return Math.min(boost, 2);
351
+ }
352
+ function flattenRelations(engram) {
353
+ if (!engram.relations) return [];
354
+ const associations = [];
355
+ for (const id of engram.relations.broader) {
356
+ associations.push({ target_type: "engram", target: id, type: "semantic", strength: 0.5 });
357
+ }
358
+ for (const id of engram.relations.narrower) {
359
+ associations.push({ target_type: "engram", target: id, type: "semantic", strength: 0.5 });
360
+ }
361
+ for (const id of engram.relations.related) {
362
+ associations.push({ target_type: "engram", target: id, type: "semantic", strength: 0.5 });
363
+ }
364
+ return associations;
365
+ }
366
+ function stripAssociations(engram) {
367
+ const { associations: _, ...rest } = engram;
368
+ return rest;
369
+ }
370
+ function stripScoring(engram) {
371
+ const { keyword_match: _, raw_score: _r, score: _s, ...rest } = engram;
372
+ return rest;
373
+ }
374
+ function scoreEngram(engram, promptLower, promptWords, packMatchTerms, scopeFilter, isPack) {
375
+ if (scopeFilter) {
376
+ if (scopeFilter === "global") {
377
+ if (engram.scope !== "global") return 0;
378
+ } else if (!engram.scope.startsWith(scopeFilter) && engram.scope !== "global") {
379
+ return 0;
380
+ }
381
+ }
382
+ let termHits = 0;
383
+ for (const term of packMatchTerms) {
384
+ if (promptLower.includes(term.toLowerCase())) termHits++;
385
+ }
386
+ for (const tag of engram.tags) {
387
+ if (promptWords.has(tag.toLowerCase())) termHits++;
388
+ }
389
+ if (engram.domain) {
390
+ for (const part of engram.domain.split(/[./]/)) {
391
+ if (promptWords.has(part.toLowerCase())) termHits++;
392
+ }
393
+ }
394
+ const statementWords = new Set(engram.statement.toLowerCase().split(/\W+/).filter((w) => w.length > 2));
395
+ for (const word of promptWords) {
396
+ if (statementWords.has(word)) termHits += 0.5;
397
+ }
398
+ if (termHits === 0) return 0;
399
+ const rs = isPack ? engram.activation.retrieval_strength : decayedStrength(engram.activation.retrieval_strength, daysSince(engram.activation.last_accessed));
400
+ let score = termHits * rs;
401
+ const feedback = engram.feedback_signals;
402
+ if (feedback) {
403
+ const netFeedback = feedback.positive - feedback.negative;
404
+ if (netFeedback > 0) score *= 1 + Math.min(netFeedback * 0.05, 0.3);
405
+ else if (netFeedback < 0) score *= Math.max(1 + netFeedback * 0.1, 0.5);
406
+ }
407
+ if (engram.consolidated) score *= 1.1;
408
+ return score;
409
+ }
410
+ function fillTokenBudget(scored, maxTokens) {
411
+ const result = [];
412
+ const packCounts = /* @__PURE__ */ new Map();
413
+ const domainCounts = /* @__PURE__ */ new Map();
414
+ let tokensUsed = 0;
415
+ for (const engram of scored) {
416
+ const cost = estimateTokens(engram);
417
+ if (tokensUsed + cost > maxTokens) continue;
418
+ const pack = engram.pack ?? "__personal__";
419
+ const packCount = packCounts.get(pack) ?? 0;
420
+ if (packCount >= MAX_PER_PACK && pack !== "__personal__") continue;
421
+ const domain = engram.domain ?? "__none__";
422
+ const topDomain = domain.split(".")[0];
423
+ const domainCount = domainCounts.get(topDomain) ?? 0;
424
+ if (domainCount >= MAX_PER_DOMAIN) continue;
425
+ result.push(engram);
426
+ tokensUsed += cost;
427
+ packCounts.set(pack, packCount + 1);
428
+ domainCounts.set(topDomain, domainCount + 1);
429
+ }
430
+ return { selected: result, tokens_used: tokensUsed };
431
+ }
432
+ function selectAndSpread(ctx, personalEngrams, packs, config) {
433
+ const spreadCap = config?.spread_cap ?? 3;
434
+ const spreadBudget = config?.spread_budget ?? 480;
435
+ const promptLower = ctx.prompt.toLowerCase();
436
+ const promptWords = new Set(promptLower.split(/\W+/).filter((w) => w.length > 2));
437
+ const maxTokens = ctx.maxTokens ?? DEFAULT_MAX_TOKENS;
438
+ const minRelevance = ctx.minRelevance ?? DEFAULT_MIN_RELEVANCE;
439
+ const engramMap = /* @__PURE__ */ new Map();
440
+ const scored = [];
441
+ for (const engram of personalEngrams) {
442
+ if (engram.status !== "active") continue;
443
+ engramMap.set(engram.id, engram);
444
+ const raw = scoreEngram(engram, promptLower, promptWords, [], ctx.scope, false);
445
+ if (raw > 0) {
446
+ scored.push({ ...engram, keyword_match: raw, raw_score: raw, score: raw });
447
+ }
448
+ }
449
+ for (const pack of packs) {
450
+ const packMeta = getPackMetadata(pack.manifest);
451
+ if (packMeta.injection_policy === "on_request") continue;
452
+ const matchTerms = packMeta.match_terms;
453
+ for (const engram of pack.engrams) {
454
+ if (engram.status !== "active") continue;
455
+ engramMap.set(engram.id, engram);
456
+ const raw = scoreEngram(engram, promptLower, promptWords, matchTerms, ctx.scope, true);
457
+ if (raw > 0) {
458
+ scored.push({ ...engram, keyword_match: raw, raw_score: raw, score: raw });
459
+ }
460
+ }
461
+ }
462
+ const maxKm = Math.max(...scored.map((e) => e.keyword_match), 1);
463
+ for (const e of scored) {
464
+ e.keyword_match = e.keyword_match / maxKm * 10;
465
+ }
466
+ const aBoosts = /* @__PURE__ */ new Map();
467
+ for (const e of scored) {
468
+ const aBoost = anchorBoost(e, promptWords);
469
+ aBoosts.set(e.id, aBoost);
470
+ e.score = e.keyword_match + aBoost;
471
+ }
472
+ const filtered = scored.filter((s) => s.score >= minRelevance);
473
+ filtered.sort((a, b) => b.score - a.score);
474
+ const { selected: directives, tokens_used: directiveTokens } = fillTokenBudget(filtered, maxTokens);
475
+ const directiveIds = new Set(directives.map((e) => e.id));
476
+ const directivePackCounts = /* @__PURE__ */ new Map();
477
+ for (const e of directives) {
478
+ const pack = e.pack ?? "__personal__";
479
+ directivePackCounts.set(pack, (directivePackCounts.get(pack) ?? 0) + 1);
480
+ }
481
+ const dip19Remainder = filtered.filter((e) => {
482
+ if (directiveIds.has(e.id)) return false;
483
+ const pack = e.pack ?? "__personal__";
484
+ if (pack !== "__personal__" && (directivePackCounts.get(pack) ?? 0) >= MAX_PER_PACK) return false;
485
+ return true;
486
+ });
487
+ const { selected: dip19Consider } = fillTokenBudget(
488
+ dip19Remainder,
489
+ DIP19_CONSIDER_BUDGET
490
+ );
491
+ const dip19Pool = dip19Consider.slice(0, DIP19_CONSIDER_MAX);
492
+ const dip19PoolTokens = dip19Pool.reduce((acc, e) => acc + estimateTokens(e), 0);
493
+ if (directives.length === 0 && dip19Pool.length === 0) {
494
+ return {
495
+ directives: [],
496
+ consider: [],
497
+ tokens_used: { directives: 0, consider: 0 }
498
+ };
499
+ }
500
+ const maxFirstPass = Math.max(...directives.map((e) => e.score), 1);
501
+ const visited = new Set(directives.map((e) => e.id));
502
+ for (const e of dip19Pool) visited.add(e.id);
503
+ const spreadCandidates = [];
504
+ let spreadTokens = 0;
505
+ for (const directive of directives) {
506
+ const assocs = directive.associations?.length ? directive.associations : flattenRelations(directive);
507
+ for (const assoc of assocs) {
508
+ if (assoc.target_type !== "engram") continue;
509
+ if (visited.has(assoc.target)) continue;
510
+ const target = engramMap.get(assoc.target);
511
+ if (!target || target.status !== "active") continue;
512
+ const effectiveStrength = assoc.type === "co_accessed" && assoc.updated_at ? decayedCoAccessStrength(assoc.strength, daysSince(assoc.updated_at)) : assoc.strength;
513
+ if (effectiveStrength <= 0) continue;
514
+ const spreadScore = directive.score / maxFirstPass * effectiveStrength;
515
+ if (spreadScore < minRelevance * 0.5) continue;
516
+ const spreadEngram = {
517
+ ...target,
518
+ keyword_match: 0,
519
+ raw_score: 0,
520
+ score: spreadScore
521
+ };
522
+ const cost = estimateTokens(spreadEngram);
523
+ if (spreadTokens + cost > spreadBudget) continue;
524
+ if (spreadCandidates.length >= spreadCap) break;
525
+ spreadCandidates.push(spreadEngram);
526
+ spreadTokens += cost;
527
+ visited.add(assoc.target);
528
+ }
529
+ }
530
+ const allConsider = [...dip19Pool, ...spreadCandidates];
531
+ const agentDirectives = directives.map(stripAssociations);
532
+ const agentConsider = allConsider.map(stripAssociations);
533
+ const wireDirectives = agentDirectives.map(stripScoring);
534
+ const wireConsider = agentConsider.map(stripScoring);
535
+ const considerTokens = dip19PoolTokens + spreadTokens;
536
+ return {
537
+ directives: wireDirectives,
538
+ consider: wireConsider,
539
+ tokens_used: { directives: directiveTokens, consider: considerTokens }
540
+ };
541
+ }
542
+
543
+ // src/episodes.ts
544
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
545
+ import yaml3 from "js-yaml";
546
+ function generateEpisodeId() {
547
+ const ts = Date.now();
548
+ const rand = Math.random().toString(36).slice(2, 6);
549
+ return `EP-${ts}-${rand}`;
550
+ }
551
+ function captureEpisode(path2, summary, context) {
552
+ const episodes = loadEpisodes(path2);
553
+ const episode = {
554
+ id: generateEpisodeId(),
555
+ summary,
556
+ agent: context?.agent,
557
+ channel: context?.channel,
558
+ session_id: context?.session_id,
559
+ tags: context?.tags,
560
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
561
+ };
562
+ episodes.push(episode);
563
+ writeFileSync2(path2, yaml3.dump(episodes, { lineWidth: 120, noRefs: true }), "utf8");
564
+ return episode;
565
+ }
566
+ function queryTimeline(path2, query) {
567
+ let episodes = loadEpisodes(path2);
568
+ if (query?.since) episodes = episodes.filter((e) => new Date(e.timestamp) >= query.since);
569
+ if (query?.until) episodes = episodes.filter((e) => new Date(e.timestamp) <= query.until);
570
+ if (query?.agent) episodes = episodes.filter((e) => e.agent === query.agent);
571
+ if (query?.channel) episodes = episodes.filter((e) => e.channel === query.channel);
572
+ if (query?.search) {
573
+ const terms = query.search.toLowerCase().split(/\s+/);
574
+ episodes = episodes.filter((e) => terms.some((t) => e.summary.toLowerCase().includes(t)));
575
+ }
576
+ return episodes;
577
+ }
578
+ function loadEpisodes(path2) {
579
+ if (!existsSync4(path2)) return [];
580
+ try {
581
+ const raw = yaml3.load(readFileSync3(path2, "utf8"));
582
+ return Array.isArray(raw) ? raw : [];
583
+ } catch {
584
+ return [];
585
+ }
586
+ }
587
+
588
+ // src/conflict.ts
589
+ function detectConflicts(newEngram, existing, threshold2 = 0.4) {
590
+ const newScope = newEngram.scope || "global";
591
+ const newTokens = ftsTokenize(newEngram.statement);
592
+ if (newTokens.length === 0) return [];
593
+ return existing.filter((e) => {
594
+ if (e.status !== "active") return false;
595
+ if ((e.scope || "global") !== newScope) return false;
596
+ const score = ftsScore(e, newTokens);
597
+ return score >= threshold2;
598
+ });
599
+ }
600
+
601
+ // src/agentic-search.ts
602
+ async function agenticSearch(engrams, query, limit, llm) {
603
+ const candidates = searchEngrams(engrams, query, Math.min(30, engrams.length));
604
+ if (candidates.length === 0) return [];
605
+ if (candidates.length <= limit) {
606
+ return agenticRerank(candidates, query, limit, llm);
607
+ }
608
+ return agenticRerank(candidates, query, limit, llm);
609
+ }
610
+ async function agenticRerank(candidates, query, limit, llm) {
611
+ const numbered = candidates.map((e, i) => `${i + 1}. [${e.id}] ${e.statement}`).join("\n");
612
+ const prompt = `You are a memory retrieval system. Given a query and a list of memories, select the ${limit} most relevant memories. Return ONLY the numbers of the relevant memories, comma-separated, in order of relevance (most relevant first).
613
+
614
+ Query: "${query}"
615
+
616
+ Memories:
617
+ ${numbered}
618
+
619
+ Rules:
620
+ - Select at most ${limit} memories
621
+ - Only select memories that are actually relevant to answering the query
622
+ - If fewer than ${limit} memories are relevant, return fewer
623
+ - Return ONLY comma-separated numbers, nothing else (e.g., "3,7,1,12")
624
+ - If no memories are relevant, return "none"`;
625
+ try {
626
+ const response = await llm(prompt);
627
+ const text = response.trim();
628
+ if (text.toLowerCase() === "none") return [];
629
+ const indices = text.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((i) => !isNaN(i) && i >= 0 && i < candidates.length);
630
+ const seen = /* @__PURE__ */ new Set();
631
+ const unique = [];
632
+ for (const i of indices) {
633
+ if (!seen.has(i)) {
634
+ seen.add(i);
635
+ unique.push(i);
636
+ }
637
+ }
638
+ return unique.slice(0, limit).map((i) => candidates[i]);
639
+ } catch {
640
+ return candidates.slice(0, limit);
641
+ }
642
+ }
643
+
644
+ // src/packs.ts
645
+ import * as fs2 from "fs";
646
+ import * as path from "path";
647
+ import yaml4 from "js-yaml";
648
+ function installPack(packsDir, source) {
649
+ if (!fs2.existsSync(source)) throw new Error(`Pack source not found: ${source}`);
650
+ const sourceName = path.basename(source);
651
+ const destDir = path.join(packsDir, sourceName);
652
+ if (!fs2.existsSync(destDir)) fs2.mkdirSync(destDir, { recursive: true });
653
+ const files = fs2.readdirSync(source);
654
+ let copied = 0;
655
+ for (const file of files) {
656
+ const srcPath = path.join(source, file);
657
+ const destPath = path.join(destDir, file);
658
+ if (fs2.statSync(srcPath).isFile()) {
659
+ fs2.copyFileSync(srcPath, destPath);
660
+ copied++;
661
+ }
662
+ }
663
+ const engramsPath = path.join(destDir, "engrams.yaml");
664
+ const engrams = fs2.existsSync(engramsPath) ? loadEngrams(engramsPath) : [];
665
+ return { installed: engrams.length, name: sourceName };
666
+ }
667
+ function listPacks(packsDir) {
668
+ if (!fs2.existsSync(packsDir)) return [];
669
+ const result = [];
670
+ for (const entry of fs2.readdirSync(packsDir)) {
671
+ const packDir = path.join(packsDir, entry);
672
+ if (!fs2.statSync(packDir).isDirectory()) continue;
673
+ try {
674
+ const pack = loadPack(packDir);
675
+ result.push({
676
+ name: pack.manifest.name,
677
+ path: packDir,
678
+ engram_count: pack.engrams.length,
679
+ manifest: pack.manifest
680
+ });
681
+ } catch {
682
+ const engramsPath = path.join(packDir, "engrams.yaml");
683
+ const engrams = fs2.existsSync(engramsPath) ? loadEngrams(engramsPath) : [];
684
+ result.push({
685
+ name: entry,
686
+ path: packDir,
687
+ engram_count: engrams.length
688
+ });
689
+ }
690
+ }
691
+ return result;
692
+ }
693
+ function exportPack(engrams, outputDir, manifest) {
694
+ if (!fs2.existsSync(outputDir)) fs2.mkdirSync(outputDir, { recursive: true });
695
+ const frontmatter = yaml4.dump({
696
+ name: manifest.name,
697
+ version: manifest.version,
698
+ description: manifest.description,
699
+ creator: manifest.creator,
700
+ metadata: {
701
+ injection_policy: "on_match",
702
+ match_terms: [],
703
+ engram_count: engrams.length
704
+ }
705
+ });
706
+ fs2.writeFileSync(
707
+ path.join(outputDir, "SKILL.md"),
708
+ `---
709
+ ${frontmatter}---
710
+
711
+ # ${manifest.name}
712
+
713
+ ${manifest.description || ""}
714
+ `
715
+ );
716
+ const content = yaml4.dump({ engrams }, { lineWidth: 120, noRefs: true, quotingType: '"' });
717
+ fs2.writeFileSync(path.join(outputDir, "engrams.yaml"), content);
718
+ return { path: outputDir, engram_count: engrams.length };
719
+ }
720
+
721
+ // src/index.ts
722
+ var INGEST_PATTERNS = [
723
+ { re: /(?:we decided|the decision is|agreed to)\s+(.+?)\.?$/gim, type: "architectural" },
724
+ { re: /(?:always|never|must|should)\s+(.+?)\.?$/gim, type: "behavioral" },
725
+ { re: /(?:the convention is|the rule is|the pattern is)\s+(.+?)\.?$/gim, type: "procedural" },
726
+ { re: /(?:use|prefer)\s+(\w+)\s+(?:for|over|instead of)\s+(.+?)\.?$/gim, type: "behavioral" },
727
+ { re: /(?:important|note|remember):\s*(.+?)\.?$/gim, type: "behavioral" }
728
+ ];
729
+ var Plur = class {
730
+ paths;
731
+ config;
732
+ constructor(options) {
733
+ this.paths = detectPlurStorage(options?.path);
734
+ this.config = loadConfig(this.paths.config);
735
+ }
736
+ /** Create engram, detect conflicts, save. Returns the created engram. */
737
+ learn(statement, context) {
738
+ const engrams = loadEngrams(this.paths.engrams);
739
+ const id = generateEngramId(engrams);
740
+ const scope = context?.scope ?? "global";
741
+ const now = (/* @__PURE__ */ new Date()).toISOString();
742
+ const conflictingEngrams = detectConflicts({ statement, scope }, engrams);
743
+ const conflictIds = conflictingEngrams.map((e) => e.id);
744
+ const engram = {
745
+ id,
746
+ version: 2,
747
+ status: "active",
748
+ consolidated: false,
749
+ type: context?.type ?? "behavioral",
750
+ scope,
751
+ visibility: "private",
752
+ statement,
753
+ domain: context?.domain,
754
+ activation: {
755
+ retrieval_strength: 0.7,
756
+ storage_strength: 1,
757
+ frequency: 0,
758
+ last_accessed: now.slice(0, 10)
759
+ },
760
+ feedback_signals: { positive: 0, negative: 0, neutral: 0 },
761
+ knowledge_anchors: [],
762
+ associations: [],
763
+ derivation_count: 1,
764
+ tags: [],
765
+ pack: null,
766
+ abstract: null,
767
+ derived_from: null,
768
+ relations: conflictIds.length > 0 ? {
769
+ broader: [],
770
+ narrower: [],
771
+ related: [],
772
+ conflicts: conflictIds
773
+ } : void 0
774
+ };
775
+ engrams.push(engram);
776
+ saveEngrams(this.paths.engrams, engrams);
777
+ return engram;
778
+ }
779
+ /**
780
+ * Search engrams, filter by scope/domain/strength, reactivate accessed.
781
+ * Supports two modes:
782
+ * - 'fast' (default): BM25 keyword search, instant, no API calls
783
+ * - 'agentic': LLM-assisted semantic search, higher accuracy, requires llm function
784
+ */
785
+ /** Search engrams using fast BM25 keyword matching. Sync, no API calls. */
786
+ recall(query, options) {
787
+ const filtered = this._filterEngrams(options);
788
+ const limit = options?.limit ?? 20;
789
+ const results = searchEngrams(filtered, query, limit);
790
+ this._reactivateResults(results);
791
+ return results;
792
+ }
793
+ /** Search engrams using LLM-assisted semantic filtering. Async, requires llm function. */
794
+ async recallAsync(query, options) {
795
+ const filtered = this._filterEngrams(options);
796
+ const limit = options?.limit ?? 20;
797
+ const results = await agenticSearch(filtered, query, limit, options.llm);
798
+ this._reactivateResults(results);
799
+ return results;
800
+ }
801
+ /** Filter engrams by scope/domain/strength (shared by both modes) */
802
+ _filterEngrams(options) {
803
+ let engrams = loadEngrams(this.paths.engrams);
804
+ engrams = engrams.filter((e) => e.status === "active");
805
+ if (options?.domain) {
806
+ engrams = engrams.filter((e) => e.domain?.startsWith(options.domain));
807
+ }
808
+ if (options?.min_strength !== void 0) {
809
+ engrams = engrams.filter((e) => e.activation.retrieval_strength >= options.min_strength);
810
+ }
811
+ if (options?.scope) {
812
+ const scope = options.scope;
813
+ engrams = engrams.filter(
814
+ (e) => e.scope === "global" || e.scope === scope || e.scope.startsWith(scope)
815
+ );
816
+ }
817
+ return engrams;
818
+ }
819
+ /** Reactivate accessed engrams (bump retrieval strength, frequency, last_accessed) */
820
+ _reactivateResults(results) {
821
+ if (results.length === 0) return;
822
+ const allEngrams = loadEngrams(this.paths.engrams);
823
+ const resultIds = new Set(results.map((e) => e.id));
824
+ let modified = false;
825
+ for (const e of allEngrams) {
826
+ if (resultIds.has(e.id)) {
827
+ e.activation.retrieval_strength = reactivate(e.activation.retrieval_strength);
828
+ e.activation.last_accessed = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
829
+ e.activation.frequency += 1;
830
+ modified = true;
831
+ }
832
+ }
833
+ if (modified) saveEngrams(this.paths.engrams, allEngrams);
834
+ }
835
+ /** Scored injection within token budget. Returns formatted strings. */
836
+ inject(task, options) {
837
+ const engrams = loadEngrams(this.paths.engrams);
838
+ const packs = loadAllPacks(this.paths.packs);
839
+ const budget = options?.budget ?? this.config.injection_budget ?? 2e3;
840
+ const result = selectAndSpread(
841
+ {
842
+ prompt: task,
843
+ scope: options?.scope,
844
+ maxTokens: budget
845
+ },
846
+ engrams,
847
+ packs,
848
+ {
849
+ spread_cap: this.config.injection?.spread_cap,
850
+ spread_budget: this.config.injection?.spread_budget
851
+ }
852
+ );
853
+ const formatEngrams = (wires) => {
854
+ if (wires.length === 0) return "";
855
+ return wires.map((e) => `[${e.id}] ${e.statement}`).join("\n");
856
+ };
857
+ const directivesStr = formatEngrams(result.directives);
858
+ const considerStr = formatEngrams(result.consider);
859
+ const count = result.directives.length + result.consider.length;
860
+ const tokensUsed = result.tokens_used.directives + result.tokens_used.consider;
861
+ return {
862
+ directives: directivesStr,
863
+ consider: considerStr,
864
+ count,
865
+ tokens_used: tokensUsed
866
+ };
867
+ }
868
+ /** Update feedback_signals and adjust retrieval_strength. */
869
+ feedback(id, signal) {
870
+ const engrams = loadEngrams(this.paths.engrams);
871
+ const engram = engrams.find((e) => e.id === id);
872
+ if (!engram) throw new Error(`Engram not found: ${id}`);
873
+ if (!engram.feedback_signals) {
874
+ engram.feedback_signals = { positive: 0, negative: 0, neutral: 0 };
875
+ }
876
+ engram.feedback_signals[signal] += 1;
877
+ if (signal === "positive") {
878
+ engram.activation.retrieval_strength = Math.min(1, engram.activation.retrieval_strength + 0.05);
879
+ } else if (signal === "negative") {
880
+ engram.activation.retrieval_strength = Math.max(0, engram.activation.retrieval_strength - 0.1);
881
+ }
882
+ saveEngrams(this.paths.engrams, engrams);
883
+ }
884
+ /** Set engram status to 'retired'. */
885
+ forget(id, reason) {
886
+ const engrams = loadEngrams(this.paths.engrams);
887
+ const engram = engrams.find((e) => e.id === id);
888
+ if (!engram) throw new Error(`Engram not found: ${id}`);
889
+ engram.status = "retired";
890
+ if (reason && !engram.rationale) {
891
+ engram.rationale = `Retired: ${reason}`;
892
+ }
893
+ saveEngrams(this.paths.engrams, engrams);
894
+ }
895
+ /** Capture an episodic memory. */
896
+ capture(summary, context) {
897
+ return captureEpisode(this.paths.episodes, summary, context);
898
+ }
899
+ /** Query the episode timeline. */
900
+ timeline(query) {
901
+ return queryTimeline(this.paths.episodes, query);
902
+ }
903
+ /** Rule-based extraction of engram candidates from content. */
904
+ ingest(content, options) {
905
+ const candidates = [];
906
+ const seen = /* @__PURE__ */ new Set();
907
+ for (const { re, type } of INGEST_PATTERNS) {
908
+ re.lastIndex = 0;
909
+ let match;
910
+ while ((match = re.exec(content)) !== null) {
911
+ const captured = match.slice(1).filter(Boolean).join(" ").trim();
912
+ if (!captured || captured.length < 5) continue;
913
+ if (seen.has(captured.toLowerCase())) continue;
914
+ seen.add(captured.toLowerCase());
915
+ candidates.push({
916
+ statement: captured,
917
+ type,
918
+ source: options?.source
919
+ });
920
+ }
921
+ }
922
+ if (!options?.extract_only && candidates.length > 0) {
923
+ for (const candidate of candidates) {
924
+ this.learn(candidate.statement, {
925
+ type: candidate.type,
926
+ scope: options?.scope ?? "global",
927
+ domain: options?.domain,
928
+ source: candidate.source
929
+ });
930
+ }
931
+ }
932
+ return candidates;
933
+ }
934
+ /** Install a pack from a source path. */
935
+ installPack(source) {
936
+ return installPack(this.paths.packs, source);
937
+ }
938
+ /** Export engrams as a shareable pack. */
939
+ exportPack(engrams, outputDir, manifest) {
940
+ return exportPack(engrams, outputDir, manifest);
941
+ }
942
+ /** List all installed packs. */
943
+ listPacks() {
944
+ return listPacks(this.paths.packs);
945
+ }
946
+ /** Return system health info. */
947
+ status() {
948
+ const engrams = loadEngrams(this.paths.engrams);
949
+ const episodes = queryTimeline(this.paths.episodes);
950
+ const packs = listPacks(this.paths.packs);
951
+ return {
952
+ engram_count: engrams.filter((e) => e.status !== "retired").length,
953
+ episode_count: episodes.length,
954
+ pack_count: packs.length,
955
+ storage_root: this.paths.root,
956
+ config: this.config
957
+ };
958
+ }
959
+ };
960
+ export {
961
+ Plur
962
+ };