@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
|
@@ -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":"AAAA;;;;;;;;;;;GAWG;AAMH,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1B,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QACN,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC;KACxC,CAAC;CACH,CAAC;AAEF,sCAAsC;AACtC,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,oBAAoB,CAAC;AAMjE,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,4EAA4E;IAC5E,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,oBAAoB,GACpB,oBAAoB,GACpB,sBAAsB,GACtB,kBAAkB,CAAC;AAEvB,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,aAAa,CAAC;CACtB,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,MAAM,QAAQ,GAAG,YAAY,GAAG,iBAAiB,GAAG,YAAY,GAAG,eAAe,CAAC;AAEzF;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAGF,wBAAgB,UAAU,CAAC,CAAC,EAAE,QAAQ,GAAG,CAAC,IAAI,YAAY,CAEzD;AACD,wBAAgB,eAAe,CAAC,CAAC,EAAE,QAAQ,GAAG,CAAC,IAAI,iBAAiB,CAEnE;AACD,wBAAgB,UAAU,CAAC,CAAC,EAAE,QAAQ,GAAG,CAAC,IAAI,YAAY,CAEzD;AAMD,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,CAAC;AAMF;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,SAAS;IACxB;;;;;;;OAOG;IACH,MAAM,CACJ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE;QACL,oCAAoC;QACpC,KAAK,CAAC,EAAE;YAAE,UAAU,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC;QAChD;;;WAGG;QACH,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GACA,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC3B"}
|
|
@@ -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
|
|
4
|
-
"
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"
|
|
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
|
+
}
|