@loreai/core 0.0.1 → 0.10.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.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -5
  3. package/dist/bun/agents-file.d.ts +59 -0
  4. package/dist/bun/agents-file.d.ts.map +1 -0
  5. package/dist/bun/config.d.ts +58 -0
  6. package/dist/bun/config.d.ts.map +1 -0
  7. package/dist/bun/curator.d.ts +35 -0
  8. package/dist/bun/curator.d.ts.map +1 -0
  9. package/dist/bun/db/driver.bun.d.ts +5 -0
  10. package/dist/bun/db/driver.bun.d.ts.map +1 -0
  11. package/dist/bun/db/driver.node.d.ts +15 -0
  12. package/dist/bun/db/driver.node.d.ts.map +1 -0
  13. package/dist/bun/db.d.ts +22 -0
  14. package/dist/bun/db.d.ts.map +1 -0
  15. package/dist/bun/distillation.d.ts +32 -0
  16. package/dist/bun/distillation.d.ts.map +1 -0
  17. package/dist/bun/embedding.d.ts +90 -0
  18. package/dist/bun/embedding.d.ts.map +1 -0
  19. package/dist/bun/gradient.d.ts +73 -0
  20. package/dist/bun/gradient.d.ts.map +1 -0
  21. package/dist/bun/index.d.ts +19 -0
  22. package/dist/bun/index.d.ts.map +1 -0
  23. package/dist/bun/index.js +28236 -0
  24. package/dist/bun/index.js.map +7 -0
  25. package/dist/bun/lat-reader.d.ts +69 -0
  26. package/dist/bun/lat-reader.d.ts.map +1 -0
  27. package/dist/bun/log.d.ts +17 -0
  28. package/dist/bun/log.d.ts.map +1 -0
  29. package/dist/bun/ltm.d.ts +138 -0
  30. package/dist/bun/ltm.d.ts.map +1 -0
  31. package/dist/bun/markdown.d.ts +37 -0
  32. package/dist/bun/markdown.d.ts.map +1 -0
  33. package/dist/bun/prompt.d.ts +47 -0
  34. package/dist/bun/prompt.d.ts.map +1 -0
  35. package/dist/bun/recall.d.ts +41 -0
  36. package/dist/bun/recall.d.ts.map +1 -0
  37. package/dist/bun/search.d.ts +113 -0
  38. package/dist/bun/search.d.ts.map +1 -0
  39. package/dist/bun/temporal.d.ts +66 -0
  40. package/dist/bun/temporal.d.ts.map +1 -0
  41. package/dist/bun/types.d.ts +180 -0
  42. package/dist/bun/types.d.ts.map +1 -0
  43. package/dist/bun/worker.d.ts +6 -0
  44. package/dist/bun/worker.d.ts.map +1 -0
  45. package/dist/node/agents-file.d.ts +59 -0
  46. package/dist/node/agents-file.d.ts.map +1 -0
  47. package/dist/node/config.d.ts +58 -0
  48. package/dist/node/config.d.ts.map +1 -0
  49. package/dist/node/curator.d.ts +35 -0
  50. package/dist/node/curator.d.ts.map +1 -0
  51. package/dist/node/db/driver.bun.d.ts +5 -0
  52. package/dist/node/db/driver.bun.d.ts.map +1 -0
  53. package/dist/node/db/driver.node.d.ts +15 -0
  54. package/dist/node/db/driver.node.d.ts.map +1 -0
  55. package/dist/node/db.d.ts +22 -0
  56. package/dist/node/db.d.ts.map +1 -0
  57. package/dist/node/distillation.d.ts +32 -0
  58. package/dist/node/distillation.d.ts.map +1 -0
  59. package/dist/node/embedding.d.ts +90 -0
  60. package/dist/node/embedding.d.ts.map +1 -0
  61. package/dist/node/gradient.d.ts +73 -0
  62. package/dist/node/gradient.d.ts.map +1 -0
  63. package/dist/node/index.d.ts +19 -0
  64. package/dist/node/index.d.ts.map +1 -0
  65. package/dist/node/index.js +28253 -0
  66. package/dist/node/index.js.map +7 -0
  67. package/dist/node/lat-reader.d.ts +69 -0
  68. package/dist/node/lat-reader.d.ts.map +1 -0
  69. package/dist/node/log.d.ts +17 -0
  70. package/dist/node/log.d.ts.map +1 -0
  71. package/dist/node/ltm.d.ts +138 -0
  72. package/dist/node/ltm.d.ts.map +1 -0
  73. package/dist/node/markdown.d.ts +37 -0
  74. package/dist/node/markdown.d.ts.map +1 -0
  75. package/dist/node/prompt.d.ts +47 -0
  76. package/dist/node/prompt.d.ts.map +1 -0
  77. package/dist/node/recall.d.ts +41 -0
  78. package/dist/node/recall.d.ts.map +1 -0
  79. package/dist/node/search.d.ts +113 -0
  80. package/dist/node/search.d.ts.map +1 -0
  81. package/dist/node/temporal.d.ts +66 -0
  82. package/dist/node/temporal.d.ts.map +1 -0
  83. package/dist/node/types.d.ts +180 -0
  84. package/dist/node/types.d.ts.map +1 -0
  85. package/dist/node/worker.d.ts +6 -0
  86. package/dist/node/worker.d.ts.map +1 -0
  87. package/dist/types/agents-file.d.ts +59 -0
  88. package/dist/types/agents-file.d.ts.map +1 -0
  89. package/dist/types/config.d.ts +58 -0
  90. package/dist/types/config.d.ts.map +1 -0
  91. package/dist/types/curator.d.ts +35 -0
  92. package/dist/types/curator.d.ts.map +1 -0
  93. package/dist/types/db/driver.bun.d.ts +5 -0
  94. package/dist/types/db/driver.bun.d.ts.map +1 -0
  95. package/dist/types/db/driver.node.d.ts +15 -0
  96. package/dist/types/db/driver.node.d.ts.map +1 -0
  97. package/dist/types/db.d.ts +22 -0
  98. package/dist/types/db.d.ts.map +1 -0
  99. package/dist/types/distillation.d.ts +32 -0
  100. package/dist/types/distillation.d.ts.map +1 -0
  101. package/dist/types/embedding.d.ts +90 -0
  102. package/dist/types/embedding.d.ts.map +1 -0
  103. package/dist/types/gradient.d.ts +73 -0
  104. package/dist/types/gradient.d.ts.map +1 -0
  105. package/dist/types/index.d.ts +19 -0
  106. package/dist/types/index.d.ts.map +1 -0
  107. package/dist/types/lat-reader.d.ts +69 -0
  108. package/dist/types/lat-reader.d.ts.map +1 -0
  109. package/dist/types/log.d.ts +17 -0
  110. package/dist/types/log.d.ts.map +1 -0
  111. package/dist/types/ltm.d.ts +138 -0
  112. package/dist/types/ltm.d.ts.map +1 -0
  113. package/dist/types/markdown.d.ts +37 -0
  114. package/dist/types/markdown.d.ts.map +1 -0
  115. package/dist/types/prompt.d.ts +47 -0
  116. package/dist/types/prompt.d.ts.map +1 -0
  117. package/dist/types/recall.d.ts +41 -0
  118. package/dist/types/recall.d.ts.map +1 -0
  119. package/dist/types/search.d.ts +113 -0
  120. package/dist/types/search.d.ts.map +1 -0
  121. package/dist/types/temporal.d.ts +66 -0
  122. package/dist/types/temporal.d.ts.map +1 -0
  123. package/dist/types/types.d.ts +180 -0
  124. package/dist/types/types.d.ts.map +1 -0
  125. package/dist/types/worker.d.ts +6 -0
  126. package/dist/types/worker.d.ts.map +1 -0
  127. package/package.json +48 -5
  128. package/src/agents-file.ts +406 -0
  129. package/src/config.ts +132 -0
  130. package/src/curator.ts +220 -0
  131. package/src/db/driver.bun.ts +18 -0
  132. package/src/db/driver.node.ts +54 -0
  133. package/src/db.ts +433 -0
  134. package/src/distillation.ts +433 -0
  135. package/src/embedding.ts +528 -0
  136. package/src/gradient.ts +1387 -0
  137. package/src/index.ts +109 -0
  138. package/src/lat-reader.ts +374 -0
  139. package/src/log.ts +27 -0
  140. package/src/ltm.ts +861 -0
  141. package/src/markdown.ts +129 -0
  142. package/src/prompt.ts +454 -0
  143. package/src/recall.ts +446 -0
  144. package/src/search.ts +330 -0
  145. package/src/temporal.ts +379 -0
  146. package/src/types.ts +199 -0
  147. package/src/worker.ts +26 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Host-agnostic message and part types for Lore's core memory engine.
3
+ *
4
+ * These replace the direct dependency on `@opencode-ai/sdk`'s `Message` and
5
+ * `Part` types so the core can run under any host (OpenCode, Pi, future ACP
6
+ * server, etc.). Each host adapter converts between its native types and these
7
+ * Lore-internal types at the hook boundary.
8
+ *
9
+ * The type surface is intentionally minimal — only the fields that Lore's
10
+ * runtime code actually reads/writes are included. Fields that only exist for
11
+ * the host's UI or for features Lore doesn't touch are omitted.
12
+ */
13
+ export type LoreUserMessage = {
14
+ id: string;
15
+ sessionID: string;
16
+ role: "user";
17
+ time: {
18
+ created: number;
19
+ };
20
+ /** Agent name (e.g. "build", "plan"). Host-specific; stored as metadata. */
21
+ agent: string;
22
+ /** Model used for this turn. Stored as metadata. */
23
+ model: {
24
+ providerID: string;
25
+ modelID: string;
26
+ };
27
+ };
28
+ export type LoreAssistantMessage = {
29
+ id: string;
30
+ sessionID: string;
31
+ role: "assistant";
32
+ time: {
33
+ created: number;
34
+ };
35
+ parentID: string;
36
+ modelID: string;
37
+ providerID: string;
38
+ mode: string;
39
+ path: {
40
+ cwd: string;
41
+ root: string;
42
+ };
43
+ cost: number;
44
+ tokens: {
45
+ input: number;
46
+ output: number;
47
+ reasoning: number;
48
+ cache: {
49
+ read: number;
50
+ write: number;
51
+ };
52
+ };
53
+ };
54
+ /** Discriminated union on `.role`. */
55
+ export type LoreMessage = LoreUserMessage | LoreAssistantMessage;
56
+ export type LoreTextPart = {
57
+ id: string;
58
+ sessionID: string;
59
+ messageID: string;
60
+ type: "text";
61
+ text: string;
62
+ /** Marks Lore-injected synthetic messages (e.g. distilled prefix). */
63
+ synthetic?: boolean;
64
+ /** Optional timing info — present on real messages, faked on synthetics. */
65
+ time?: {
66
+ start: number;
67
+ end?: number;
68
+ };
69
+ };
70
+ export type LoreReasoningPart = {
71
+ id: string;
72
+ sessionID: string;
73
+ messageID: string;
74
+ type: "reasoning";
75
+ text: string;
76
+ };
77
+ export type LoreToolStatePending = {
78
+ status: "pending";
79
+ input: unknown;
80
+ };
81
+ export type LoreToolStateRunning = {
82
+ status: "running";
83
+ input: unknown;
84
+ metadata?: unknown;
85
+ time: {
86
+ start: number;
87
+ };
88
+ };
89
+ export type LoreToolStateCompleted = {
90
+ status: "completed";
91
+ input: unknown;
92
+ output: string;
93
+ metadata?: unknown;
94
+ time: {
95
+ start: number;
96
+ end: number;
97
+ };
98
+ };
99
+ export type LoreToolStateError = {
100
+ status: "error";
101
+ input: unknown;
102
+ error: string;
103
+ metadata?: unknown;
104
+ time: {
105
+ start: number;
106
+ end: number;
107
+ };
108
+ };
109
+ export type LoreToolState = LoreToolStatePending | LoreToolStateRunning | LoreToolStateCompleted | LoreToolStateError;
110
+ export type LoreToolPart = {
111
+ id: string;
112
+ sessionID: string;
113
+ messageID: string;
114
+ type: "tool";
115
+ tool: string;
116
+ callID: string;
117
+ state: LoreToolState;
118
+ };
119
+ /**
120
+ * Discriminated union on `.type`.
121
+ *
122
+ * Only `text`, `reasoning`, and `tool` are processed by Lore's core logic.
123
+ * All other part types (step-start, snapshot, patch, agent, retry, etc.) flow
124
+ * through untouched — they hit the `else` branch with a flat 20-token estimate
125
+ * in `estimateParts()` and are preserved as-is in the message transform.
126
+ *
127
+ * For type-safe narrowing, use `isToolPart()` / `isTextPart()` helpers below.
128
+ */
129
+ export type LorePart = LoreTextPart | LoreReasoningPart | LoreToolPart | LoreGenericPart;
130
+ /**
131
+ * Passthrough for host-specific part types that Lore doesn't process.
132
+ * The `type` field is typed as `string` since Lore only cares that it's not
133
+ * one of the three known types.
134
+ */
135
+ export type LoreGenericPart = {
136
+ type: string;
137
+ [key: string]: unknown;
138
+ };
139
+ export declare function isTextPart(p: LorePart): p is LoreTextPart;
140
+ export declare function isReasoningPart(p: LorePart): p is LoreReasoningPart;
141
+ export declare function isToolPart(p: LorePart): p is LoreToolPart;
142
+ export type LoreMessageWithParts = {
143
+ info: LoreMessage;
144
+ parts: LorePart[];
145
+ };
146
+ /**
147
+ * Abstract interface for single-turn LLM prompt→response.
148
+ *
149
+ * All of Lore's background LLM work (distillation, curation, query expansion)
150
+ * is single-turn: one system+user message in, one text response out. No tool
151
+ * calling, no multi-turn. This interface captures that minimal surface.
152
+ *
153
+ * Host adapters implement this:
154
+ * - OpenCode: wraps `client.session.create()` + `client.session.prompt()`
155
+ * - Pi: wraps `complete()` from `@mariozechner/pi-ai`
156
+ * - Standalone: direct `fetch()` to provider APIs
157
+ */
158
+ export interface LLMClient {
159
+ /**
160
+ * Send a single prompt and return the text response.
161
+ *
162
+ * @param system System prompt text
163
+ * @param user User message text
164
+ * @param opts Optional model selection and worker identification
165
+ * @returns The assistant's text response, or null on failure
166
+ */
167
+ prompt(system: string, user: string, opts?: {
168
+ /** Override model for this call. */
169
+ model?: {
170
+ providerID: string;
171
+ modelID: string;
172
+ };
173
+ /**
174
+ * Opaque worker identifier used by the host to route the request
175
+ * (e.g. OpenCode uses this as the session agent name).
176
+ */
177
+ workerID?: string;
178
+ }): Promise<string | null>;
179
+ }
180
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export type { LLMClient } from "./types";
2
+ /** Set of ALL worker session IDs across distillation, curator, and query expansion.
3
+ * Used by shouldSkip() in host adapters to avoid storing/distilling worker messages. */
4
+ export declare const workerSessionIDs: Set<string>;
5
+ export declare function isWorkerSession(sessionID: string): boolean;
6
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/worker.ts"],"names":[],"mappings":"AAaA,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAMzC;yFACyF;AACzF,eAAO,MAAM,gBAAgB,aAAoB,CAAC;AAElD,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE1D"}
package/package.json CHANGED
@@ -1,16 +1,59 @@
1
1
  {
2
2
  "name": "@loreai/core",
3
- "version": "0.0.1",
4
- "description": "Placeholder — real release follows via CI. See https://github.com/BYK/loreai",
3
+ "version": "0.10.0",
4
+ "type": "module",
5
5
  "license": "MIT",
6
- "author": "BYK",
6
+ "description": "Shared memory engine for Lore — three-tier storage, distillation, gradient context management",
7
+ "main": "./dist/node/index.js",
8
+ "types": "./dist/node/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "bun": "./src/index.ts",
12
+ "default": "./dist/node/index.js"
13
+ }
14
+ },
15
+ "imports": {
16
+ "#db/driver": {
17
+ "bun": "./src/db/driver.bun.ts",
18
+ "default": "./src/db/driver.node.ts"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "typecheck": "tsc --noEmit",
23
+ "build": "bun run script/build.ts"
24
+ },
25
+ "dependencies": {
26
+ "remark": "^15.0.1",
27
+ "uuidv7": "^1.1.0",
28
+ "zod": "^4.3.6"
29
+ },
30
+ "devDependencies": {
31
+ "@types/mdast": "^4.0.4"
32
+ },
33
+ "files": [
34
+ "src/",
35
+ "dist/",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "engines": {
40
+ "node": ">=22.5"
41
+ },
7
42
  "repository": {
8
43
  "type": "git",
9
44
  "url": "git+https://github.com/BYK/loreai.git",
10
45
  "directory": "packages/core"
11
46
  },
12
- "homepage": "https://github.com/BYK/loreai",
13
47
  "publishConfig": {
14
48
  "access": "public"
15
- }
49
+ },
50
+ "keywords": [
51
+ "lore",
52
+ "memory",
53
+ "llm",
54
+ "sqlite",
55
+ "fts5",
56
+ "distillation"
57
+ ],
58
+ "author": "BYK"
16
59
  }
@@ -0,0 +1,406 @@
1
+ /**
2
+ * agents-file.ts — AGENTS.md export/import/sync for lore.
3
+ *
4
+ * Lore owns a clearly delimited section inside the file, bounded by HTML
5
+ * comment markers. Everything outside those markers is preserved verbatim.
6
+ * Each knowledge entry is preceded by a hidden <!-- lore:UUID --> comment so
7
+ * the same entry can be tracked across machines and merge conflicts resolved
8
+ * without duplication.
9
+ */
10
+
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
12
+ import { dirname } from "path";
13
+ import * as ltm from "./ltm";
14
+ import { serialize, inline, h, ul, liph, strong, t, root, unescapeMarkdown } from "./markdown";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Constants
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export const LORE_SECTION_START =
21
+ "<!-- This section is maintained by the coding agent via lore (https://github.com/BYK/loreai) -->";
22
+ export const LORE_SECTION_END = "<!-- End lore-managed section -->";
23
+
24
+ /**
25
+ * All known start-marker variants, ordered newest-first.
26
+ * When we renamed the marker in the past, old files kept the old text.
27
+ * splitFile() matches any of these so it can strip all lore sections
28
+ * regardless of which marker version was used to write them.
29
+ */
30
+ const ALL_START_MARKERS = [
31
+ LORE_SECTION_START,
32
+ // Pre-rename URL (BYK/opencode-lore → BYK/loreai).
33
+ "<!-- This section is maintained by the coding agent via lore (https://github.com/BYK/opencode-lore) -->",
34
+ "<!-- This section is auto-maintained by lore (https://github.com/BYK/opencode-lore) -->",
35
+ ] as const;
36
+
37
+ /** Regex matching a valid UUID (v4 or v7) — 8-4-4-4-12 hex groups. */
38
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
39
+
40
+ /** Matches `<!-- lore:UUID -->` tracking markers. */
41
+ const MARKER_RE = /^<!--\s*lore:([0-9a-f-]+)\s*-->$/;
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Types
45
+ // ---------------------------------------------------------------------------
46
+
47
+ export type ParsedFileEntry = {
48
+ /** UUID from `<!-- lore:UUID -->` marker, or null for hand-written entries. */
49
+ id: string | null;
50
+ category: string;
51
+ title: string;
52
+ content: string;
53
+ };
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Section extraction helpers
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /**
60
+ * Split file content into three parts: before, lore section body, after.
61
+ * Returns null for section body when no lore markers are found.
62
+ *
63
+ * Handles multiple lore sections (from duplication bugs) and all known
64
+ * start-marker variants (old + new text) by:
65
+ * - Collecting every lore section span in the file
66
+ * - Returning `before` = content before the first section
67
+ * - Returning `after` = content after the last section (all intermediate
68
+ * sections are discarded)
69
+ * - Returning `section` = body of the first section found (for import
70
+ * and shouldImport to read the canonical content)
71
+ *
72
+ * This is self-healing: a file with N duplicate sections will be collapsed
73
+ * to exactly one on the next exportToFile() call.
74
+ */
75
+ function splitFile(fileContent: string): {
76
+ before: string;
77
+ section: string | null;
78
+ after: string;
79
+ } {
80
+ // Collect every lore section span in the file, matching all known
81
+ // start-marker variants (current + historical renamed markers).
82
+ // Each span records: where the section body begins/ends and where the
83
+ // full span (including end-marker) ends.
84
+ type Span = { markerStart: number; bodyStart: number; bodyEnd: number; spanEnd: number };
85
+ const spans: Span[] = [];
86
+
87
+ let searchFrom = 0;
88
+ while (searchFrom < fileContent.length) {
89
+ // Find the earliest occurrence of any known start marker
90
+ let markerStart = -1;
91
+ let markerLen = 0;
92
+ for (const marker of ALL_START_MARKERS) {
93
+ const idx = fileContent.indexOf(marker, searchFrom);
94
+ if (idx !== -1 && (markerStart === -1 || idx < markerStart)) {
95
+ markerStart = idx;
96
+ markerLen = marker.length;
97
+ }
98
+ }
99
+ if (markerStart === -1) break; // no more start markers
100
+
101
+ const bodyStart = markerStart + markerLen;
102
+ const endIdx = fileContent.indexOf(LORE_SECTION_END, bodyStart);
103
+ if (endIdx === -1) {
104
+ // Unclosed section — consume to EOF
105
+ spans.push({ markerStart, bodyStart, bodyEnd: fileContent.length, spanEnd: fileContent.length });
106
+ break;
107
+ }
108
+
109
+ spans.push({ markerStart, bodyStart, bodyEnd: endIdx, spanEnd: endIdx + LORE_SECTION_END.length });
110
+ searchFrom = endIdx + LORE_SECTION_END.length;
111
+ }
112
+
113
+ if (spans.length === 0) {
114
+ return { before: fileContent, section: null, after: "" };
115
+ }
116
+
117
+ // before = everything before the first lore section (start marker not included)
118
+ // section = body of the first section (used by shouldImport and importFromFile)
119
+ // after = everything after the LAST lore section's end marker
120
+ // Any intermediate duplicate sections are discarded.
121
+ const before = fileContent.slice(0, spans[0].markerStart);
122
+ const section = fileContent.slice(spans[0].bodyStart, spans[0].bodyEnd);
123
+ const after = fileContent.slice(spans[spans.length - 1].spanEnd);
124
+
125
+ return { before, section, after };
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Parse entries from a lore section body (or any markdown block)
130
+ // ---------------------------------------------------------------------------
131
+
132
+ /**
133
+ * Extract ParsedFileEntry objects from a markdown section body.
134
+ * Handles:
135
+ * - `<!-- lore:UUID -->` markers before bullet points → id set
136
+ * - Bare bullet points without markers → id null
137
+ * - Category derived from the nearest preceding `### Heading`
138
+ * - Malformed or non-UUID markers → id null (hand-written)
139
+ * - Duplicate UUIDs → both returned; caller deduplicates
140
+ */
141
+ export function parseEntriesFromSection(section: string): ParsedFileEntry[] {
142
+ const lines = section.split("\n");
143
+ const entries: ParsedFileEntry[] = [];
144
+ let currentCategory = "pattern";
145
+ let pendingId: string | null = null;
146
+
147
+ for (const raw of lines) {
148
+ const line = raw.trim();
149
+
150
+ // Category heading: ### Decision / ### Gotcha / etc.
151
+ const headingMatch = line.match(/^###\s+(.+)$/);
152
+ if (headingMatch) {
153
+ currentCategory = headingMatch[1].toLowerCase();
154
+ pendingId = null;
155
+ continue;
156
+ }
157
+
158
+ // Marker line: <!-- lore:UUID -->
159
+ const markerMatch = line.match(MARKER_RE);
160
+ if (markerMatch) {
161
+ const candidate = markerMatch[1];
162
+ pendingId = UUID_RE.test(candidate) ? candidate : null;
163
+ continue;
164
+ }
165
+
166
+ // Bullet entry: * **Title**: Content
167
+ const bulletMatch = line.match(/^\*\s+\*\*(.+?)\*\*:\s*(.+)$/);
168
+ if (bulletMatch) {
169
+ // Unescape remark's markdown escapes (e.g. \< → <, \\ → \).
170
+ // Without this, each export/import cycle doubles the backslash-escapes,
171
+ // exponentially inflating stored content.
172
+ entries.push({
173
+ id: pendingId,
174
+ category: currentCategory,
175
+ title: unescapeMarkdown(bulletMatch[1].trim()),
176
+ content: unescapeMarkdown(bulletMatch[2].trim()),
177
+ });
178
+ pendingId = null; // consume the pending marker
179
+ continue;
180
+ }
181
+
182
+ // Any non-matching non-empty line resets the pending marker
183
+ if (line !== "" && !line.startsWith("##") && !line.startsWith("<!--")) {
184
+ pendingId = null;
185
+ }
186
+ }
187
+
188
+ return entries;
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Content hash (for change detection)
193
+ // ---------------------------------------------------------------------------
194
+
195
+ function hashSection(section: string): string {
196
+ let h = 0;
197
+ for (let i = 0; i < section.length; i++) {
198
+ h = (Math.imul(31, h) + section.charCodeAt(i)) | 0;
199
+ }
200
+ // Convert to unsigned hex string
201
+ return (h >>> 0).toString(16).padStart(8, "0");
202
+ }
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // Build the lore section body from DB entries
206
+ // ---------------------------------------------------------------------------
207
+
208
+ function buildSection(projectPath: string): string {
209
+ // Export only project-specific entries (cross_project=0, project_id = this project).
210
+ // Cross-project entries live in the shared DB on each machine and don't belong
211
+ // in a per-project AGENTS.md — including them would inflate the file with
212
+ // unrelated knowledge from every other project the user has worked on.
213
+ const entries = ltm.forProject(projectPath, false);
214
+ if (!entries.length) {
215
+ return "\n";
216
+ }
217
+
218
+ // Group entries by category, preserving DB order (confidence DESC, updated_at DESC).
219
+ const grouped = new Map<string, typeof entries>();
220
+ for (const e of entries) {
221
+ const group = grouped.get(e.category) ?? [];
222
+ group.push(e);
223
+ grouped.set(e.category, group);
224
+ }
225
+
226
+ // Build the section body by iterating entries directly, emitting each entry
227
+ // with its own <!-- lore:UUID --> marker. This avoids the title-based Map
228
+ // deduplication bug where multiple entries with the same title all got the
229
+ // same UUID marker from the last Map.set() winner.
230
+ //
231
+ // Merge-friendliness: entries within each category are sorted alphabetically
232
+ // by title (case-insensitive) so the ordering is deterministic across all
233
+ // machines regardless of DB timestamps. Blank lines between entries give
234
+ // git unique context lines to anchor changes -- two branches adding entries
235
+ // with different titles insert at different positions and auto-merge.
236
+ const out: string[] = [""];
237
+
238
+ // Section heading
239
+ out.push("## Long-term Knowledge");
240
+
241
+ for (const [category, items] of [...grouped.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
242
+ out.push("");
243
+ out.push(`### ${category.charAt(0).toUpperCase() + category.slice(1)}`);
244
+ out.push("");
245
+
246
+ // Sort entries alphabetically by title for deterministic, merge-friendly output.
247
+ const sorted = [...items].sort((a, b) =>
248
+ a.title.localeCompare(b.title, undefined, { sensitivity: "base" }),
249
+ );
250
+
251
+ for (let i = 0; i < sorted.length; i++) {
252
+ if (i > 0) out.push(""); // blank line between entries for git context
253
+ out.push(`<!-- lore:${sorted[i].id} -->`);
254
+ // Render the bullet using remark serializer for proper markdown escaping.
255
+ // serialize(root(ul([liph(...)]))) produces "* **Title**: content\n".
256
+ // Trim the trailing newline since we join with \n ourselves.
257
+ const bullet = serialize(
258
+ root(ul([liph(strong(inline(sorted[i].title)), t(": " + inline(sorted[i].content)))]))
259
+ ).trimEnd();
260
+ out.push(bullet);
261
+ }
262
+ }
263
+
264
+ out.push("");
265
+ return out.join("\n");
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // Export
270
+ // ---------------------------------------------------------------------------
271
+
272
+ /**
273
+ * Write current knowledge entries into the AGENTS.md file, preserving all
274
+ * non-lore content. Creates the file if it doesn't exist.
275
+ */
276
+ export function exportToFile(input: {
277
+ projectPath: string;
278
+ filePath: string;
279
+ }): void {
280
+ const sectionBody = buildSection(input.projectPath);
281
+ const newSection =
282
+ LORE_SECTION_START + sectionBody + LORE_SECTION_END + "\n";
283
+
284
+ let fileContent = "";
285
+ if (existsSync(input.filePath)) {
286
+ fileContent = readFileSync(input.filePath, "utf8");
287
+ }
288
+
289
+ const { before, after } = splitFile(fileContent);
290
+
291
+ // Ensure there's a blank line separator before the section when appending
292
+ const prefix = before.trimEnd();
293
+ const prefixWithSep = prefix.length > 0 ? prefix + "\n\n" : "";
294
+ const suffix = after.trimStart();
295
+ const suffixWithSep = suffix.length > 0 ? "\n" + suffix : "";
296
+
297
+ const result = prefixWithSep + newSection + suffixWithSep;
298
+
299
+ mkdirSync(dirname(input.filePath), { recursive: true });
300
+ writeFileSync(input.filePath, result, "utf8");
301
+ }
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // shouldImport
305
+ // ---------------------------------------------------------------------------
306
+
307
+ /**
308
+ * Returns true if the file needs to be imported:
309
+ * - File exists and has never been processed (no lore markers)
310
+ * - File exists and its lore section differs from what lore would currently produce
311
+ */
312
+ export function shouldImport(input: {
313
+ projectPath: string;
314
+ filePath: string;
315
+ }): boolean {
316
+ if (!existsSync(input.filePath)) return false;
317
+
318
+ const fileContent = readFileSync(input.filePath, "utf8");
319
+ const { section } = splitFile(fileContent);
320
+
321
+ if (section === null) {
322
+ // No lore markers — this is a hand-written file that hasn't been imported
323
+ return fileContent.trim().length > 0;
324
+ }
325
+
326
+ // Compare the file's lore section body against what we'd produce now
327
+ const expected = buildSection(input.projectPath);
328
+ return hashSection(section) !== hashSection(expected);
329
+ }
330
+
331
+ // ---------------------------------------------------------------------------
332
+ // Import
333
+ // ---------------------------------------------------------------------------
334
+
335
+ /**
336
+ * Import knowledge entries from the agents file into the local DB.
337
+ *
338
+ * Behaviour per entry:
339
+ * - Known UUID (already in DB) → update content if it changed (manual edit)
340
+ * - Unknown UUID (other machine)→ create with that exact ID
341
+ * - No UUID (hand-written) → create with a new UUIDv7
342
+ * - Duplicate UUID in same file → first occurrence wins, rest ignored
343
+ */
344
+ export function importFromFile(input: {
345
+ projectPath: string;
346
+ filePath: string;
347
+ }): void {
348
+ if (!existsSync(input.filePath)) return;
349
+
350
+ const fileContent = readFileSync(input.filePath, "utf8");
351
+ const { section, before } = splitFile(fileContent);
352
+
353
+ // Determine what to parse:
354
+ // - If lore markers exist: parse ONLY the lore section body (avoid re-importing our own output)
355
+ // - If no markers: parse the full file (first-time hand-written AGENTS.md import)
356
+ const textToParse = section ?? fileContent;
357
+
358
+ const fileEntries = parseEntriesFromSection(textToParse);
359
+ if (!fileEntries.length) return;
360
+
361
+ const seenIds = new Set<string>();
362
+
363
+ for (const entry of fileEntries) {
364
+ if (entry.id !== null) {
365
+ // Deduplicate: if same UUID appears twice in file, first wins
366
+ if (seenIds.has(entry.id)) continue;
367
+ seenIds.add(entry.id);
368
+
369
+ const existing = ltm.get(entry.id);
370
+ if (existing) {
371
+ // Known entry — update only if content changed (manual edit in file)
372
+ if (existing.content !== entry.content) {
373
+ ltm.update(entry.id, { content: entry.content });
374
+ }
375
+ } else {
376
+ // Unknown UUID — entry came from another machine, preserve its ID
377
+ ltm.create({
378
+ projectPath: input.projectPath,
379
+ category: entry.category,
380
+ title: entry.title,
381
+ content: entry.content,
382
+ scope: "project",
383
+ crossProject: false,
384
+ id: entry.id,
385
+ });
386
+ }
387
+ } else {
388
+ // Hand-written entry — create with a new UUIDv7
389
+ // Check for a near-duplicate by title to avoid double-import on re-runs
390
+ const existing = ltm.forProject(input.projectPath, true);
391
+ const titleMatch = existing.find(
392
+ (e) => e.title.toLowerCase() === entry.title.toLowerCase(),
393
+ );
394
+ if (!titleMatch) {
395
+ ltm.create({
396
+ projectPath: input.projectPath,
397
+ category: entry.category,
398
+ title: entry.title,
399
+ content: entry.content,
400
+ scope: "project",
401
+ crossProject: false,
402
+ });
403
+ }
404
+ }
405
+ }
406
+ }