@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
package/src/index.ts ADDED
@@ -0,0 +1,109 @@
1
+ // @loreai/core — shared memory engine for Lore.
2
+ //
3
+ // This barrel re-exports every core module so hosts (the OpenCode plugin, the
4
+ // Pi extension, or any future adapter) can import from a single entry:
5
+ //
6
+ // import { ltm, temporal, gradient, ... } from "@loreai/core"
7
+ //
8
+ // Modules that are intentionally not re-exported:
9
+ // - `db.ts` internals are exposed via specific functions (db(), ensureProject(), etc.)
10
+ // - No Plugin/Hooks surface — those live in host-specific packages.
11
+
12
+ export * as temporal from "./temporal";
13
+ export * as ltm from "./ltm";
14
+ export * as distillation from "./distillation";
15
+ export * as curator from "./curator";
16
+ export * as embedding from "./embedding";
17
+ export * as latReader from "./lat-reader";
18
+ export * as log from "./log";
19
+
20
+ export {
21
+ runRecall,
22
+ RECALL_TOOL_DESCRIPTION,
23
+ RECALL_PARAM_DESCRIPTIONS,
24
+ type RecallInput,
25
+ type RecallResult,
26
+ type RecallScope,
27
+ type ScoredDistillation,
28
+ } from "./recall";
29
+
30
+ export type {
31
+ LoreMessage,
32
+ LoreUserMessage,
33
+ LoreAssistantMessage,
34
+ LorePart,
35
+ LoreTextPart,
36
+ LoreReasoningPart,
37
+ LoreToolPart,
38
+ LoreGenericPart,
39
+ LoreToolState,
40
+ LoreToolStatePending,
41
+ LoreToolStateRunning,
42
+ LoreToolStateCompleted,
43
+ LoreToolStateError,
44
+ LoreMessageWithParts,
45
+ LLMClient,
46
+ } from "./types";
47
+ export { isTextPart, isReasoningPart, isToolPart } from "./types";
48
+
49
+ export { load, config, type LoreConfig } from "./config";
50
+ export {
51
+ db,
52
+ ensureProject,
53
+ isFirstRun,
54
+ projectId,
55
+ projectName,
56
+ loadForceMinLayer,
57
+ saveForceMinLayer,
58
+ close,
59
+ } from "./db";
60
+ export {
61
+ transform,
62
+ setModelLimits,
63
+ needsUrgentDistillation,
64
+ calibrate,
65
+ setLtmTokens,
66
+ getLtmTokens,
67
+ getLtmBudget,
68
+ setForceMinLayer,
69
+ getLastTransformedCount,
70
+ getLastTransformEstimate,
71
+ } from "./gradient";
72
+ export {
73
+ formatKnowledge,
74
+ formatDistillations,
75
+ DISTILLATION_SYSTEM,
76
+ distillationUser,
77
+ RECURSIVE_SYSTEM,
78
+ recursiveUser,
79
+ CURATOR_SYSTEM,
80
+ curatorUser,
81
+ CONSOLIDATION_SYSTEM,
82
+ consolidationUser,
83
+ QUERY_EXPANSION_SYSTEM,
84
+ } from "./prompt";
85
+ export { shouldImport, importFromFile, exportToFile } from "./agents-file";
86
+ export { workerSessionIDs, isWorkerSession } from "./worker";
87
+ export {
88
+ ftsQuery,
89
+ ftsQueryOr,
90
+ EMPTY_QUERY,
91
+ reciprocalRankFusion,
92
+ expandQuery,
93
+ extractTopTerms,
94
+ } from "./search";
95
+ export {
96
+ serialize,
97
+ inline,
98
+ h,
99
+ p,
100
+ ul,
101
+ lip,
102
+ liph,
103
+ t,
104
+ root,
105
+ strong,
106
+ normalize,
107
+ sanitizeSurrogates,
108
+ unescapeMarkdown,
109
+ } from "./markdown";
@@ -0,0 +1,374 @@
1
+ /**
2
+ * lat.md reader — indexes lat.md/ directory sections for recall integration.
3
+ *
4
+ * When a project has a `lat.md/` directory (from the lat.md knowledge graph tool),
5
+ * this module parses the markdown files, extracts hierarchical sections, and stores
6
+ * them in SQLite with FTS5 indexing. Sections are included in recall results via
7
+ * RRF fusion and in LTM system-prompt injection via session-context scoring.
8
+ *
9
+ * Change detection uses SHA-256 content hashes per file — unchanged files are skipped.
10
+ */
11
+
12
+ import { readdirSync, readFileSync, existsSync, statSync } from "fs";
13
+ import { join, relative, basename } from "path";
14
+ import { remark } from "remark";
15
+ import type { Root, Heading, Paragraph, Text } from "mdast";
16
+ import { db, ensureProject } from "./db";
17
+ import { sha256 } from "#db/driver";
18
+ import { ftsQuery, ftsQueryOr, extractTopTerms, EMPTY_QUERY } from "./search";
19
+ import * as log from "./log";
20
+
21
+ const processor = remark();
22
+
23
+ // ~3 chars per token — same heuristic as ltm.ts
24
+ function estimateTokens(text: string): number {
25
+ return Math.ceil(text.length / 3);
26
+ }
27
+
28
+ export type LatSection = {
29
+ id: string;
30
+ project_id: string;
31
+ file: string;
32
+ heading: string;
33
+ depth: number;
34
+ content: string;
35
+ content_hash: string;
36
+ first_paragraph: string | null;
37
+ updated_at: number;
38
+ };
39
+
40
+ export type ScoredLatSection = LatSection & { rank: number };
41
+
42
+ // ---- Section parsing ----
43
+
44
+ type ParsedSection = {
45
+ id: string;
46
+ file: string;
47
+ heading: string;
48
+ depth: number;
49
+ content: string;
50
+ first_paragraph: string | null;
51
+ };
52
+
53
+ /** Extract heading text from an mdast Heading node. */
54
+ function headingText(node: Heading): string {
55
+ return node.children
56
+ .filter((c): c is Text => c.type === "text")
57
+ .map((c) => c.value)
58
+ .join("");
59
+ }
60
+
61
+ /** Extract inline text from a paragraph (flattening nested phrasing). */
62
+ function paragraphText(node: Paragraph): string {
63
+ const parts: string[] = [];
64
+ for (const child of node.children) {
65
+ if (child.type === "text") parts.push(child.value);
66
+ else if (child.type === "inlineCode") parts.push("`" + child.value + "`");
67
+ else if ("children" in child) {
68
+ for (const gc of child.children) {
69
+ if ("value" in gc && typeof gc.value === "string") parts.push(gc.value);
70
+ }
71
+ }
72
+ }
73
+ return parts.join("");
74
+ }
75
+
76
+ /**
77
+ * Parse a single markdown file into sections.
78
+ * Each heading creates a section; content is everything between headings.
79
+ * Section IDs use the lat.md convention: `file#Heading#SubHeading`.
80
+ */
81
+ export function parseSections(filePath: string, content: string, projectRoot: string): ParsedSection[] {
82
+ const tree = processor.parse(content) as Root;
83
+ const fileRel = relative(projectRoot, filePath).replace(/\.md$/, "");
84
+ const lines = content.split("\n");
85
+
86
+ // Collect headings with positions
87
+ const headings: Array<{ node: Heading; text: string; line: number; depth: number }> = [];
88
+ for (const node of tree.children) {
89
+ if (node.type === "heading" && node.position) {
90
+ headings.push({
91
+ node,
92
+ text: headingText(node),
93
+ line: node.position.start.line,
94
+ depth: node.depth,
95
+ });
96
+ }
97
+ }
98
+
99
+ if (!headings.length) return [];
100
+
101
+ // Build hierarchical IDs using a depth stack (same algorithm as lat.md's lattice.ts)
102
+ const stack: Array<{ id: string; depth: number }> = [];
103
+ const sections: ParsedSection[] = [];
104
+
105
+ for (let i = 0; i < headings.length; i++) {
106
+ const { text, depth, line } = headings[i];
107
+
108
+ // Pop stack until we find a parent with smaller depth
109
+ while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
110
+ stack.pop();
111
+ }
112
+
113
+ const parent = stack.length > 0 ? stack[stack.length - 1] : null;
114
+ const id = parent ? `${parent.id}#${text}` : `${fileRel}#${text}`;
115
+
116
+ stack.push({ id, depth });
117
+
118
+ // Content: lines from after this heading to before the next heading (or EOF)
119
+ const startLine = line; // 1-indexed
120
+ const endLine = i + 1 < headings.length ? headings[i + 1].line - 1 : lines.length;
121
+
122
+ // Skip the heading line itself, collect content
123
+ const contentLines = lines.slice(startLine, endLine);
124
+ const sectionContent = contentLines.join("\n").trim();
125
+
126
+ // First paragraph: find the first paragraph node after this heading
127
+ let firstParagraph: string | null = null;
128
+ for (const node of tree.children) {
129
+ if (!node.position) continue;
130
+ if (node.position.start.line <= startLine) continue;
131
+ if (i + 1 < headings.length && node.position.start.line >= headings[i + 1].line) break;
132
+ if (node.type === "paragraph") {
133
+ const text = paragraphText(node);
134
+ firstParagraph = text.length > 250 ? text.slice(0, 250) : text;
135
+ break;
136
+ }
137
+ }
138
+
139
+ sections.push({
140
+ id,
141
+ file: fileRel,
142
+ heading: text,
143
+ depth,
144
+ content: sectionContent,
145
+ first_paragraph: firstParagraph,
146
+ });
147
+ }
148
+
149
+ return sections;
150
+ }
151
+
152
+ // ---- File discovery ----
153
+
154
+ /** Recursively list all .md files in a directory. */
155
+ function listMarkdownFiles(dir: string): string[] {
156
+ const results: string[] = [];
157
+ try {
158
+ const entries = readdirSync(dir, { withFileTypes: true });
159
+ for (const entry of entries) {
160
+ const fullPath = join(dir, entry.name);
161
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
162
+ results.push(...listMarkdownFiles(fullPath));
163
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
164
+ results.push(fullPath);
165
+ }
166
+ }
167
+ } catch {
168
+ // Directory not readable — skip
169
+ }
170
+ return results.sort();
171
+ }
172
+
173
+ /** Compute SHA-256 hash of file content for change detection. */
174
+ function contentHash(content: string): string {
175
+ return sha256(content);
176
+ }
177
+
178
+ // ---- Public API ----
179
+
180
+ /** Check if a project has a lat.md/ directory. */
181
+ export function hasLatDir(projectPath: string): boolean {
182
+ const latDir = join(projectPath, "lat.md");
183
+ return existsSync(latDir) && statSync(latDir).isDirectory();
184
+ }
185
+
186
+ /**
187
+ * Refresh the lat_sections cache for a project.
188
+ * Scans lat.md/ directory, parses markdown files, and upserts sections.
189
+ * Skips files whose content hash hasn't changed since last scan.
190
+ * Removes sections from files that no longer exist.
191
+ *
192
+ * @returns Number of sections updated/inserted
193
+ */
194
+ export function refresh(projectPath: string): number {
195
+ const latDir = join(projectPath, "lat.md");
196
+ if (!existsSync(latDir) || !statSync(latDir).isDirectory()) return 0;
197
+
198
+ const pid = ensureProject(projectPath);
199
+ const files = listMarkdownFiles(latDir);
200
+ let upserted = 0;
201
+
202
+ // Track which files we've seen for cleanup
203
+ const seenFiles = new Set<string>();
204
+
205
+ const upsertStmt = db().query(
206
+ `INSERT OR REPLACE INTO lat_sections (id, project_id, file, heading, depth, content, content_hash, first_paragraph, updated_at)
207
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
208
+ );
209
+
210
+ for (const filePath of files) {
211
+ let content: string;
212
+ try {
213
+ content = readFileSync(filePath, "utf-8");
214
+ } catch {
215
+ continue;
216
+ }
217
+
218
+ const fileRel = relative(projectPath, filePath);
219
+ seenFiles.add(fileRel);
220
+ const hash = contentHash(content);
221
+
222
+ // Check if any section from this file already has this hash
223
+ const existing = db()
224
+ .query("SELECT content_hash FROM lat_sections WHERE project_id = ? AND file = ? LIMIT 1")
225
+ .get(pid, fileRel.replace(/\.md$/, "")) as { content_hash: string } | null;
226
+
227
+ if (existing && existing.content_hash === hash) {
228
+ continue; // File unchanged
229
+ }
230
+
231
+ // Delete old sections for this file before inserting new ones
232
+ db()
233
+ .query("DELETE FROM lat_sections WHERE project_id = ? AND file = ?")
234
+ .run(pid, fileRel.replace(/\.md$/, ""));
235
+
236
+ const sections = parseSections(filePath, content, projectPath);
237
+ const now = Date.now();
238
+
239
+ for (const section of sections) {
240
+ upsertStmt.run(
241
+ section.id,
242
+ pid,
243
+ section.file,
244
+ section.heading,
245
+ section.depth,
246
+ section.content,
247
+ hash,
248
+ section.first_paragraph,
249
+ now,
250
+ );
251
+ upserted++;
252
+ }
253
+ }
254
+
255
+ // Cleanup: remove sections from files that no longer exist
256
+ const seenFileStems = new Set([...seenFiles].map((f) => f.replace(/\.md$/, "")));
257
+ const allFiles = db()
258
+ .query("SELECT DISTINCT file FROM lat_sections WHERE project_id = ?")
259
+ .all(pid) as Array<{ file: string }>;
260
+
261
+ for (const row of allFiles) {
262
+ if (!seenFileStems.has(row.file)) {
263
+ db().query("DELETE FROM lat_sections WHERE project_id = ? AND file = ?").run(pid, row.file);
264
+ log.info(`lat-reader: removed sections for deleted file ${row.file}`);
265
+ }
266
+ }
267
+
268
+ if (upserted > 0) {
269
+ log.info(`lat-reader: indexed ${upserted} sections from ${files.length} files`);
270
+ }
271
+
272
+ return upserted;
273
+ }
274
+
275
+ /**
276
+ * Search lat sections by FTS5 with BM25 scoring.
277
+ * Uses AND-then-OR fallback (same pattern as knowledge search).
278
+ */
279
+ export function searchScored(input: {
280
+ query: string;
281
+ projectPath: string;
282
+ limit?: number;
283
+ }): ScoredLatSection[] {
284
+ const limit = input.limit ?? 10;
285
+ const q = ftsQuery(input.query);
286
+ if (q === EMPTY_QUERY) return [];
287
+
288
+ const pid = ensureProject(input.projectPath);
289
+
290
+ const ftsSQL = `SELECT s.id, s.project_id, s.file, s.heading, s.depth, s.content,
291
+ s.content_hash, s.first_paragraph, s.updated_at,
292
+ bm25(lat_sections_fts, 6.0, 2.0) as rank
293
+ FROM lat_sections s
294
+ JOIN lat_sections_fts f ON s.rowid = f.rowid
295
+ WHERE lat_sections_fts MATCH ?
296
+ AND s.project_id = ?
297
+ ORDER BY rank LIMIT ?`;
298
+
299
+ try {
300
+ const results = db().query(ftsSQL).all(q, pid, limit) as ScoredLatSection[];
301
+ if (results.length) return results;
302
+
303
+ // AND returned nothing — try OR fallback
304
+ const qOr = ftsQueryOr(input.query);
305
+ if (qOr === EMPTY_QUERY) return [];
306
+ return db().query(ftsSQL).all(qOr, pid, limit) as ScoredLatSection[];
307
+ } catch {
308
+ return [];
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Score lat sections against session context for LTM injection.
314
+ * Uses OR-based FTS5 BM25 (same approach as ltm.ts scoreEntriesFTS).
315
+ *
316
+ * @returns Scored entries sorted by score descending, capped at maxTokens budget
317
+ */
318
+ export function scoreForSession(
319
+ projectPath: string,
320
+ sessionContext: string,
321
+ maxTokens: number,
322
+ ): LatSection[] {
323
+ if (!hasLatDir(projectPath)) return [];
324
+
325
+ const pid = ensureProject(projectPath);
326
+ const terms = extractTopTerms(sessionContext);
327
+ if (!terms.length) return [];
328
+
329
+ const q = terms.map((t) => `${t}*`).join(" OR ");
330
+
331
+ let results: Array<LatSection & { rank: number }>;
332
+ try {
333
+ results = db()
334
+ .query(
335
+ `SELECT s.id, s.project_id, s.file, s.heading, s.depth, s.content,
336
+ s.content_hash, s.first_paragraph, s.updated_at,
337
+ bm25(lat_sections_fts, 6.0, 2.0) as rank
338
+ FROM lat_sections s
339
+ JOIN lat_sections_fts f ON s.rowid = f.rowid
340
+ WHERE lat_sections_fts MATCH ?
341
+ AND s.project_id = ?
342
+ ORDER BY rank`,
343
+ )
344
+ .all(q, pid) as Array<LatSection & { rank: number }>;
345
+ } catch {
346
+ return [];
347
+ }
348
+
349
+ if (!results.length) return [];
350
+
351
+ // Greedy-pack into token budget
352
+ const HEADER_OVERHEAD = 10;
353
+ let used = HEADER_OVERHEAD;
354
+ const packed: LatSection[] = [];
355
+
356
+ for (const entry of results) {
357
+ if (used >= maxTokens) break;
358
+ const cost = estimateTokens(entry.heading + (entry.first_paragraph ?? entry.content)) + 5;
359
+ if (used + cost > maxTokens) continue;
360
+ packed.push(entry);
361
+ used += cost;
362
+ }
363
+
364
+ return packed;
365
+ }
366
+
367
+ /** Count lat sections for a project. */
368
+ export function count(projectPath: string): number {
369
+ const pid = ensureProject(projectPath);
370
+ const row = db()
371
+ .query("SELECT COUNT(*) as cnt FROM lat_sections WHERE project_id = ?")
372
+ .get(pid) as { cnt: number };
373
+ return row.cnt;
374
+ }
package/src/log.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Lightweight logger that suppresses informational messages by default.
3
+ *
4
+ * In TUI mode, all stderr output renders as red "error" text — confusing
5
+ * for routine status messages like "incremental distillation" or "pruned
6
+ * temporal messages". Only actual errors should be visible by default.
7
+ *
8
+ * Set LORE_DEBUG=1 to see informational messages (useful when debugging
9
+ * the plugin itself).
10
+ */
11
+
12
+ const isDebug = !!process.env.LORE_DEBUG;
13
+
14
+ /** Log an informational status message. Suppressed unless LORE_DEBUG=1. */
15
+ export function info(...args: unknown[]): void {
16
+ if (isDebug) console.error("[lore]", ...args);
17
+ }
18
+
19
+ /** Log a warning. Suppressed unless LORE_DEBUG=1. */
20
+ export function warn(...args: unknown[]): void {
21
+ if (isDebug) console.error("[lore] WARN:", ...args);
22
+ }
23
+
24
+ /** Log an error. Always visible — these indicate real failures. */
25
+ export function error(...args: unknown[]): void {
26
+ console.error("[lore]", ...args);
27
+ }