@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.
- package/LICENSE +21 -0
- package/README.md +26 -5
- package/dist/bun/agents-file.d.ts +59 -0
- package/dist/bun/agents-file.d.ts.map +1 -0
- package/dist/bun/config.d.ts +58 -0
- package/dist/bun/config.d.ts.map +1 -0
- package/dist/bun/curator.d.ts +35 -0
- package/dist/bun/curator.d.ts.map +1 -0
- package/dist/bun/db/driver.bun.d.ts +5 -0
- package/dist/bun/db/driver.bun.d.ts.map +1 -0
- package/dist/bun/db/driver.node.d.ts +15 -0
- package/dist/bun/db/driver.node.d.ts.map +1 -0
- package/dist/bun/db.d.ts +22 -0
- package/dist/bun/db.d.ts.map +1 -0
- package/dist/bun/distillation.d.ts +32 -0
- package/dist/bun/distillation.d.ts.map +1 -0
- package/dist/bun/embedding.d.ts +90 -0
- package/dist/bun/embedding.d.ts.map +1 -0
- package/dist/bun/gradient.d.ts +73 -0
- package/dist/bun/gradient.d.ts.map +1 -0
- package/dist/bun/index.d.ts +19 -0
- package/dist/bun/index.d.ts.map +1 -0
- package/dist/bun/index.js +28236 -0
- package/dist/bun/index.js.map +7 -0
- package/dist/bun/lat-reader.d.ts +69 -0
- package/dist/bun/lat-reader.d.ts.map +1 -0
- package/dist/bun/log.d.ts +17 -0
- package/dist/bun/log.d.ts.map +1 -0
- package/dist/bun/ltm.d.ts +138 -0
- package/dist/bun/ltm.d.ts.map +1 -0
- package/dist/bun/markdown.d.ts +37 -0
- package/dist/bun/markdown.d.ts.map +1 -0
- package/dist/bun/prompt.d.ts +47 -0
- package/dist/bun/prompt.d.ts.map +1 -0
- package/dist/bun/recall.d.ts +41 -0
- package/dist/bun/recall.d.ts.map +1 -0
- package/dist/bun/search.d.ts +113 -0
- package/dist/bun/search.d.ts.map +1 -0
- package/dist/bun/temporal.d.ts +66 -0
- package/dist/bun/temporal.d.ts.map +1 -0
- package/dist/bun/types.d.ts +180 -0
- package/dist/bun/types.d.ts.map +1 -0
- package/dist/bun/worker.d.ts +6 -0
- package/dist/bun/worker.d.ts.map +1 -0
- package/dist/node/agents-file.d.ts +59 -0
- package/dist/node/agents-file.d.ts.map +1 -0
- package/dist/node/config.d.ts +58 -0
- package/dist/node/config.d.ts.map +1 -0
- package/dist/node/curator.d.ts +35 -0
- package/dist/node/curator.d.ts.map +1 -0
- package/dist/node/db/driver.bun.d.ts +5 -0
- package/dist/node/db/driver.bun.d.ts.map +1 -0
- package/dist/node/db/driver.node.d.ts +15 -0
- package/dist/node/db/driver.node.d.ts.map +1 -0
- package/dist/node/db.d.ts +22 -0
- package/dist/node/db.d.ts.map +1 -0
- package/dist/node/distillation.d.ts +32 -0
- package/dist/node/distillation.d.ts.map +1 -0
- package/dist/node/embedding.d.ts +90 -0
- package/dist/node/embedding.d.ts.map +1 -0
- package/dist/node/gradient.d.ts +73 -0
- package/dist/node/gradient.d.ts.map +1 -0
- package/dist/node/index.d.ts +19 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +28253 -0
- package/dist/node/index.js.map +7 -0
- package/dist/node/lat-reader.d.ts +69 -0
- package/dist/node/lat-reader.d.ts.map +1 -0
- package/dist/node/log.d.ts +17 -0
- package/dist/node/log.d.ts.map +1 -0
- package/dist/node/ltm.d.ts +138 -0
- package/dist/node/ltm.d.ts.map +1 -0
- package/dist/node/markdown.d.ts +37 -0
- package/dist/node/markdown.d.ts.map +1 -0
- package/dist/node/prompt.d.ts +47 -0
- package/dist/node/prompt.d.ts.map +1 -0
- package/dist/node/recall.d.ts +41 -0
- package/dist/node/recall.d.ts.map +1 -0
- package/dist/node/search.d.ts +113 -0
- package/dist/node/search.d.ts.map +1 -0
- package/dist/node/temporal.d.ts +66 -0
- package/dist/node/temporal.d.ts.map +1 -0
- package/dist/node/types.d.ts +180 -0
- package/dist/node/types.d.ts.map +1 -0
- package/dist/node/worker.d.ts +6 -0
- package/dist/node/worker.d.ts.map +1 -0
- package/dist/types/agents-file.d.ts +59 -0
- package/dist/types/agents-file.d.ts.map +1 -0
- package/dist/types/config.d.ts +58 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/curator.d.ts +35 -0
- package/dist/types/curator.d.ts.map +1 -0
- package/dist/types/db/driver.bun.d.ts +5 -0
- package/dist/types/db/driver.bun.d.ts.map +1 -0
- package/dist/types/db/driver.node.d.ts +15 -0
- package/dist/types/db/driver.node.d.ts.map +1 -0
- package/dist/types/db.d.ts +22 -0
- package/dist/types/db.d.ts.map +1 -0
- package/dist/types/distillation.d.ts +32 -0
- package/dist/types/distillation.d.ts.map +1 -0
- package/dist/types/embedding.d.ts +90 -0
- package/dist/types/embedding.d.ts.map +1 -0
- package/dist/types/gradient.d.ts +73 -0
- package/dist/types/gradient.d.ts.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/lat-reader.d.ts +69 -0
- package/dist/types/lat-reader.d.ts.map +1 -0
- package/dist/types/log.d.ts +17 -0
- package/dist/types/log.d.ts.map +1 -0
- package/dist/types/ltm.d.ts +138 -0
- package/dist/types/ltm.d.ts.map +1 -0
- package/dist/types/markdown.d.ts +37 -0
- package/dist/types/markdown.d.ts.map +1 -0
- package/dist/types/prompt.d.ts +47 -0
- package/dist/types/prompt.d.ts.map +1 -0
- package/dist/types/recall.d.ts +41 -0
- package/dist/types/recall.d.ts.map +1 -0
- package/dist/types/search.d.ts +113 -0
- package/dist/types/search.d.ts.map +1 -0
- package/dist/types/temporal.d.ts +66 -0
- package/dist/types/temporal.d.ts.map +1 -0
- package/dist/types/types.d.ts +180 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/worker.d.ts +6 -0
- package/dist/types/worker.d.ts.map +1 -0
- package/package.json +48 -5
- package/src/agents-file.ts +406 -0
- package/src/config.ts +132 -0
- package/src/curator.ts +220 -0
- package/src/db/driver.bun.ts +18 -0
- package/src/db/driver.node.ts +54 -0
- package/src/db.ts +433 -0
- package/src/distillation.ts +433 -0
- package/src/embedding.ts +528 -0
- package/src/gradient.ts +1387 -0
- package/src/index.ts +109 -0
- package/src/lat-reader.ts +374 -0
- package/src/log.ts +27 -0
- package/src/ltm.ts +861 -0
- package/src/markdown.ts +129 -0
- package/src/prompt.ts +454 -0
- package/src/recall.ts +446 -0
- package/src/search.ts +330 -0
- package/src/temporal.ts +379 -0
- package/src/types.ts +199 -0
- 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
|
+
}
|