@loreai/core 0.17.1 → 0.18.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 (235) hide show
  1. package/dist/bun/agents-file.d.ts +4 -0
  2. package/dist/bun/agents-file.d.ts.map +1 -1
  3. package/dist/bun/config.d.ts +2 -0
  4. package/dist/bun/config.d.ts.map +1 -1
  5. package/dist/bun/curator.d.ts +45 -0
  6. package/dist/bun/curator.d.ts.map +1 -1
  7. package/dist/bun/data-dir.d.ts +18 -0
  8. package/dist/bun/data-dir.d.ts.map +1 -0
  9. package/dist/bun/db.d.ts +12 -0
  10. package/dist/bun/db.d.ts.map +1 -1
  11. package/dist/bun/distillation.d.ts.map +1 -1
  12. package/dist/bun/embedding-vendor.d.ts +22 -38
  13. package/dist/bun/embedding-vendor.d.ts.map +1 -1
  14. package/dist/bun/embedding-worker-types.d.ts +17 -12
  15. package/dist/bun/embedding-worker-types.d.ts.map +1 -1
  16. package/dist/bun/embedding-worker.d.ts +9 -2
  17. package/dist/bun/embedding-worker.d.ts.map +1 -1
  18. package/dist/bun/embedding-worker.js +38864 -33
  19. package/dist/bun/embedding-worker.js.map +4 -4
  20. package/dist/bun/embedding.d.ts +30 -22
  21. package/dist/bun/embedding.d.ts.map +1 -1
  22. package/dist/bun/gradient.d.ts +8 -1
  23. package/dist/bun/gradient.d.ts.map +1 -1
  24. package/dist/bun/import/detect.d.ts +14 -0
  25. package/dist/bun/import/detect.d.ts.map +1 -0
  26. package/dist/bun/import/extract.d.ts +43 -0
  27. package/dist/bun/import/extract.d.ts.map +1 -0
  28. package/dist/bun/import/history.d.ts +40 -0
  29. package/dist/bun/import/history.d.ts.map +1 -0
  30. package/dist/bun/import/index.d.ts +17 -0
  31. package/dist/bun/import/index.d.ts.map +1 -0
  32. package/dist/bun/import/providers/aider.d.ts +2 -0
  33. package/dist/bun/import/providers/aider.d.ts.map +1 -0
  34. package/dist/bun/import/providers/claude-code.d.ts +2 -0
  35. package/dist/bun/import/providers/claude-code.d.ts.map +1 -0
  36. package/dist/bun/import/providers/cline.d.ts +2 -0
  37. package/dist/bun/import/providers/cline.d.ts.map +1 -0
  38. package/dist/bun/import/providers/codex.d.ts +2 -0
  39. package/dist/bun/import/providers/codex.d.ts.map +1 -0
  40. package/dist/bun/import/providers/continue.d.ts +2 -0
  41. package/dist/bun/import/providers/continue.d.ts.map +1 -0
  42. package/dist/bun/import/providers/index.d.ts +19 -0
  43. package/dist/bun/import/providers/index.d.ts.map +1 -0
  44. package/dist/bun/import/providers/opencode.d.ts +2 -0
  45. package/dist/bun/import/providers/opencode.d.ts.map +1 -0
  46. package/dist/bun/import/providers/pi.d.ts +2 -0
  47. package/dist/bun/import/providers/pi.d.ts.map +1 -0
  48. package/dist/bun/import/types.d.ts +82 -0
  49. package/dist/bun/import/types.d.ts.map +1 -0
  50. package/dist/bun/index.d.ts +4 -1
  51. package/dist/bun/index.d.ts.map +1 -1
  52. package/dist/bun/index.js +2217 -224
  53. package/dist/bun/index.js.map +4 -4
  54. package/dist/bun/instruction-detect.d.ts +66 -0
  55. package/dist/bun/instruction-detect.d.ts.map +1 -0
  56. package/dist/bun/log.d.ts +9 -0
  57. package/dist/bun/log.d.ts.map +1 -1
  58. package/dist/bun/ltm.d.ts +40 -0
  59. package/dist/bun/ltm.d.ts.map +1 -1
  60. package/dist/bun/pattern-extract.d.ts +7 -0
  61. package/dist/bun/pattern-extract.d.ts.map +1 -1
  62. package/dist/bun/prompt.d.ts +1 -1
  63. package/dist/bun/prompt.d.ts.map +1 -1
  64. package/dist/bun/recall.d.ts.map +1 -1
  65. package/dist/bun/search.d.ts +5 -3
  66. package/dist/bun/search.d.ts.map +1 -1
  67. package/dist/bun/temporal.d.ts.map +1 -1
  68. package/dist/bun/types.d.ts +1 -1
  69. package/dist/node/agents-file.d.ts +4 -0
  70. package/dist/node/agents-file.d.ts.map +1 -1
  71. package/dist/node/config.d.ts +2 -0
  72. package/dist/node/config.d.ts.map +1 -1
  73. package/dist/node/curator.d.ts +45 -0
  74. package/dist/node/curator.d.ts.map +1 -1
  75. package/dist/node/data-dir.d.ts +18 -0
  76. package/dist/node/data-dir.d.ts.map +1 -0
  77. package/dist/node/db.d.ts +12 -0
  78. package/dist/node/db.d.ts.map +1 -1
  79. package/dist/node/distillation.d.ts.map +1 -1
  80. package/dist/node/embedding-vendor.d.ts +22 -38
  81. package/dist/node/embedding-vendor.d.ts.map +1 -1
  82. package/dist/node/embedding-worker-types.d.ts +17 -12
  83. package/dist/node/embedding-worker-types.d.ts.map +1 -1
  84. package/dist/node/embedding-worker.d.ts +9 -2
  85. package/dist/node/embedding-worker.d.ts.map +1 -1
  86. package/dist/node/embedding-worker.js +38864 -33
  87. package/dist/node/embedding-worker.js.map +4 -4
  88. package/dist/node/embedding.d.ts +30 -22
  89. package/dist/node/embedding.d.ts.map +1 -1
  90. package/dist/node/gradient.d.ts +8 -1
  91. package/dist/node/gradient.d.ts.map +1 -1
  92. package/dist/node/import/detect.d.ts +14 -0
  93. package/dist/node/import/detect.d.ts.map +1 -0
  94. package/dist/node/import/extract.d.ts +43 -0
  95. package/dist/node/import/extract.d.ts.map +1 -0
  96. package/dist/node/import/history.d.ts +40 -0
  97. package/dist/node/import/history.d.ts.map +1 -0
  98. package/dist/node/import/index.d.ts +17 -0
  99. package/dist/node/import/index.d.ts.map +1 -0
  100. package/dist/node/import/providers/aider.d.ts +2 -0
  101. package/dist/node/import/providers/aider.d.ts.map +1 -0
  102. package/dist/node/import/providers/claude-code.d.ts +2 -0
  103. package/dist/node/import/providers/claude-code.d.ts.map +1 -0
  104. package/dist/node/import/providers/cline.d.ts +2 -0
  105. package/dist/node/import/providers/cline.d.ts.map +1 -0
  106. package/dist/node/import/providers/codex.d.ts +2 -0
  107. package/dist/node/import/providers/codex.d.ts.map +1 -0
  108. package/dist/node/import/providers/continue.d.ts +2 -0
  109. package/dist/node/import/providers/continue.d.ts.map +1 -0
  110. package/dist/node/import/providers/index.d.ts +19 -0
  111. package/dist/node/import/providers/index.d.ts.map +1 -0
  112. package/dist/node/import/providers/opencode.d.ts +2 -0
  113. package/dist/node/import/providers/opencode.d.ts.map +1 -0
  114. package/dist/node/import/providers/pi.d.ts +2 -0
  115. package/dist/node/import/providers/pi.d.ts.map +1 -0
  116. package/dist/node/import/types.d.ts +82 -0
  117. package/dist/node/import/types.d.ts.map +1 -0
  118. package/dist/node/index.d.ts +4 -1
  119. package/dist/node/index.d.ts.map +1 -1
  120. package/dist/node/index.js +2217 -224
  121. package/dist/node/index.js.map +4 -4
  122. package/dist/node/instruction-detect.d.ts +66 -0
  123. package/dist/node/instruction-detect.d.ts.map +1 -0
  124. package/dist/node/log.d.ts +9 -0
  125. package/dist/node/log.d.ts.map +1 -1
  126. package/dist/node/ltm.d.ts +40 -0
  127. package/dist/node/ltm.d.ts.map +1 -1
  128. package/dist/node/pattern-extract.d.ts +7 -0
  129. package/dist/node/pattern-extract.d.ts.map +1 -1
  130. package/dist/node/prompt.d.ts +1 -1
  131. package/dist/node/prompt.d.ts.map +1 -1
  132. package/dist/node/recall.d.ts.map +1 -1
  133. package/dist/node/search.d.ts +5 -3
  134. package/dist/node/search.d.ts.map +1 -1
  135. package/dist/node/temporal.d.ts.map +1 -1
  136. package/dist/node/types.d.ts +1 -1
  137. package/dist/types/agents-file.d.ts +4 -0
  138. package/dist/types/agents-file.d.ts.map +1 -1
  139. package/dist/types/config.d.ts +2 -0
  140. package/dist/types/config.d.ts.map +1 -1
  141. package/dist/types/curator.d.ts +45 -0
  142. package/dist/types/curator.d.ts.map +1 -1
  143. package/dist/types/data-dir.d.ts +18 -0
  144. package/dist/types/data-dir.d.ts.map +1 -0
  145. package/dist/types/db.d.ts +12 -0
  146. package/dist/types/db.d.ts.map +1 -1
  147. package/dist/types/distillation.d.ts.map +1 -1
  148. package/dist/types/embedding-vendor.d.ts +22 -38
  149. package/dist/types/embedding-vendor.d.ts.map +1 -1
  150. package/dist/types/embedding-worker-types.d.ts +17 -12
  151. package/dist/types/embedding-worker-types.d.ts.map +1 -1
  152. package/dist/types/embedding-worker.d.ts +9 -2
  153. package/dist/types/embedding-worker.d.ts.map +1 -1
  154. package/dist/types/embedding.d.ts +30 -22
  155. package/dist/types/embedding.d.ts.map +1 -1
  156. package/dist/types/gradient.d.ts +8 -1
  157. package/dist/types/gradient.d.ts.map +1 -1
  158. package/dist/types/import/detect.d.ts +14 -0
  159. package/dist/types/import/detect.d.ts.map +1 -0
  160. package/dist/types/import/extract.d.ts +43 -0
  161. package/dist/types/import/extract.d.ts.map +1 -0
  162. package/dist/types/import/history.d.ts +40 -0
  163. package/dist/types/import/history.d.ts.map +1 -0
  164. package/dist/types/import/index.d.ts +17 -0
  165. package/dist/types/import/index.d.ts.map +1 -0
  166. package/dist/types/import/providers/aider.d.ts +2 -0
  167. package/dist/types/import/providers/aider.d.ts.map +1 -0
  168. package/dist/types/import/providers/claude-code.d.ts +2 -0
  169. package/dist/types/import/providers/claude-code.d.ts.map +1 -0
  170. package/dist/types/import/providers/cline.d.ts +2 -0
  171. package/dist/types/import/providers/cline.d.ts.map +1 -0
  172. package/dist/types/import/providers/codex.d.ts +2 -0
  173. package/dist/types/import/providers/codex.d.ts.map +1 -0
  174. package/dist/types/import/providers/continue.d.ts +2 -0
  175. package/dist/types/import/providers/continue.d.ts.map +1 -0
  176. package/dist/types/import/providers/index.d.ts +19 -0
  177. package/dist/types/import/providers/index.d.ts.map +1 -0
  178. package/dist/types/import/providers/opencode.d.ts +2 -0
  179. package/dist/types/import/providers/opencode.d.ts.map +1 -0
  180. package/dist/types/import/providers/pi.d.ts +2 -0
  181. package/dist/types/import/providers/pi.d.ts.map +1 -0
  182. package/dist/types/import/types.d.ts +82 -0
  183. package/dist/types/import/types.d.ts.map +1 -0
  184. package/dist/types/index.d.ts +4 -1
  185. package/dist/types/index.d.ts.map +1 -1
  186. package/dist/types/instruction-detect.d.ts +66 -0
  187. package/dist/types/instruction-detect.d.ts.map +1 -0
  188. package/dist/types/log.d.ts +9 -0
  189. package/dist/types/log.d.ts.map +1 -1
  190. package/dist/types/ltm.d.ts +40 -0
  191. package/dist/types/ltm.d.ts.map +1 -1
  192. package/dist/types/pattern-extract.d.ts +7 -0
  193. package/dist/types/pattern-extract.d.ts.map +1 -1
  194. package/dist/types/prompt.d.ts +1 -1
  195. package/dist/types/prompt.d.ts.map +1 -1
  196. package/dist/types/recall.d.ts.map +1 -1
  197. package/dist/types/search.d.ts +5 -3
  198. package/dist/types/search.d.ts.map +1 -1
  199. package/dist/types/temporal.d.ts.map +1 -1
  200. package/dist/types/types.d.ts +1 -1
  201. package/package.json +2 -4
  202. package/src/agents-file.ts +41 -13
  203. package/src/config.ts +31 -18
  204. package/src/curator.ts +111 -75
  205. package/src/data-dir.ts +76 -0
  206. package/src/db.ts +110 -11
  207. package/src/distillation.ts +10 -2
  208. package/src/embedding-vendor.ts +23 -40
  209. package/src/embedding-worker-types.ts +19 -11
  210. package/src/embedding-worker.ts +111 -47
  211. package/src/embedding.ts +196 -171
  212. package/src/gradient.ts +9 -1
  213. package/src/import/detect.ts +37 -0
  214. package/src/import/extract.ts +137 -0
  215. package/src/import/history.ts +99 -0
  216. package/src/import/index.ts +45 -0
  217. package/src/import/providers/aider.ts +207 -0
  218. package/src/import/providers/claude-code.ts +339 -0
  219. package/src/import/providers/cline.ts +324 -0
  220. package/src/import/providers/codex.ts +369 -0
  221. package/src/import/providers/continue.ts +304 -0
  222. package/src/import/providers/index.ts +32 -0
  223. package/src/import/providers/opencode.ts +272 -0
  224. package/src/import/providers/pi.ts +332 -0
  225. package/src/import/types.ts +91 -0
  226. package/src/index.ts +5 -0
  227. package/src/instruction-detect.ts +275 -0
  228. package/src/log.ts +91 -3
  229. package/src/ltm.ts +316 -3
  230. package/src/pattern-extract.ts +41 -0
  231. package/src/prompt.ts +7 -1
  232. package/src/recall.ts +43 -5
  233. package/src/search.ts +7 -5
  234. package/src/temporal.ts +8 -6
  235. package/src/types.ts +1 -1
package/src/index.ts CHANGED
@@ -18,7 +18,9 @@ export * as embedding from "./embedding";
18
18
  export * as embeddingVendor from "./embedding-vendor";
19
19
  export * as latReader from "./lat-reader";
20
20
  export * as patternExtract from "./pattern-extract";
21
+ export * as instructionDetect from "./instruction-detect";
21
22
  export * as log from "./log";
23
+ export * as conversationImport from "./import";
22
24
 
23
25
  export {
24
26
  runRecall,
@@ -53,11 +55,14 @@ export type {
53
55
  } from "./types";
54
56
  export { isTextPart, isReasoningPart, isToolPart } from "./types";
55
57
 
58
+ export { dataDir } from "./data-dir";
56
59
  export { load, config, type LoreConfig } from "./config";
57
60
  export {
58
61
  db,
59
62
  dbPath,
60
63
  ensureProject,
64
+ getLastImportAt,
65
+ setLastImportAt,
61
66
  isFirstRun,
62
67
  projectId,
63
68
  projectName,
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Cross-session repeated-instruction detection.
3
+ *
4
+ * Identifies instruction-like user messages in the current session
5
+ * and searches for similar instructions in prior sessions using both
6
+ * embedding-based vector search (semantic similarity) and FTS5 (exact terms).
7
+ *
8
+ * When an instruction appears in N+ prior sessions, it's flagged as a
9
+ * strong LTM candidate and formatted as additional context for the curator.
10
+ *
11
+ * This module does NOT auto-create knowledge entries — it augments the
12
+ * curator's input so the LLM can make the final judgment call on whether
13
+ * a repeated instruction warrants a persistent preference entry.
14
+ */
15
+
16
+ import { db, ensureProject } from "./db";
17
+ import * as temporal from "./temporal";
18
+ import * as embedding from "./embedding";
19
+ import { filterTerms, ftsQueryOr, EMPTY_QUERY } from "./search";
20
+ import * as log from "./log";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Configuration
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** Minimum distinct prior sessions to consider an instruction "repeated". */
27
+ const DEFAULT_REPETITION_THRESHOLD = 2;
28
+
29
+ /** Minimum cosine similarity for a vector search hit to count. */
30
+ const VECTOR_SIMILARITY_THRESHOLD = 0.5;
31
+
32
+ /** Maximum number of instruction candidates to process per curation run. */
33
+ const MAX_CANDIDATES = 5;
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Instruction candidate extraction
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /**
40
+ * Patterns that identify instruction-like language in raw user messages.
41
+ * These are intentionally broader than the distillation patterns in
42
+ * pattern-extract.ts because they match the user's raw words, not the
43
+ * observer's normalized phrasing.
44
+ */
45
+ const INSTRUCTION_PATTERNS: RegExp[] = [
46
+ /\balways\b (.{10,80}?)(?:\.|,|!|$)/gi,
47
+ /\bnever\b (.{10,80}?)(?:\.|,|!|$)/gi,
48
+ /\bmake sure to (.{10,80}?)(?:\.|,|!|$)/gi,
49
+ /\bdon'?t forget (?:to )?(.{10,80}?)(?:\.|,|!|$)/gi,
50
+ /\bplease (?:always |make sure (?:to )?)(.{10,80}?)(?:\.|,|!|$)/gi,
51
+ /\bI (?:want|need|prefer|expect) (?:you to )?(.{10,80}?)(?:\.|,|!|$)/gi,
52
+ ];
53
+
54
+ export type InstructionCandidate = {
55
+ /** The matched instruction text. */
56
+ text: string;
57
+ /** Session this candidate was found in. */
58
+ sessionID: string;
59
+ };
60
+
61
+ export type RepeatedInstruction = {
62
+ /** The instruction text from the current session. */
63
+ instruction: string;
64
+ /** Number of distinct prior sessions containing similar instructions. */
65
+ priorSessionCount: number;
66
+ };
67
+
68
+ /**
69
+ * Extract instruction-like phrases from user messages.
70
+ * Scans raw user message content for instruction keywords and returns
71
+ * deduplicated candidates.
72
+ */
73
+ export function extractInstructionCandidates(
74
+ messages: Array<{ role: string; content: string; session_id: string }>,
75
+ ): InstructionCandidate[] {
76
+ const candidates: InstructionCandidate[] = [];
77
+ const seen = new Set<string>();
78
+
79
+ for (const msg of messages) {
80
+ if (msg.role !== "user") continue;
81
+
82
+ for (const pattern of INSTRUCTION_PATTERNS) {
83
+ pattern.lastIndex = 0;
84
+ let match: RegExpMatchArray | null;
85
+ while ((match = pattern.exec(msg.content)) !== null) {
86
+ const text = match[1]?.trim();
87
+ if (!text || text.length < 10) continue;
88
+
89
+ // Dedup by lowercased text within this extraction
90
+ const key = text.toLowerCase();
91
+ if (seen.has(key)) continue;
92
+ seen.add(key);
93
+
94
+ candidates.push({
95
+ text,
96
+ sessionID: msg.session_id,
97
+ });
98
+
99
+ // Cap total candidates to bound search cost
100
+ if (candidates.length >= MAX_CANDIDATES) return candidates;
101
+ }
102
+ }
103
+ }
104
+
105
+ return candidates;
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Cross-session search
110
+ // ---------------------------------------------------------------------------
111
+
112
+ /**
113
+ * Search for similar instructions in OTHER sessions via distillation
114
+ * embeddings (semantic) and FTS5 (keyword). Returns instructions that
115
+ * appear in >= threshold prior sessions.
116
+ */
117
+ export async function findRepeatedInstructions(input: {
118
+ projectPath: string;
119
+ currentSessionID: string;
120
+ candidates: InstructionCandidate[];
121
+ threshold?: number;
122
+ }): Promise<RepeatedInstruction[]> {
123
+ const threshold = input.threshold ?? DEFAULT_REPETITION_THRESHOLD;
124
+ if (!input.candidates.length) return [];
125
+
126
+ const pid = ensureProject(input.projectPath);
127
+
128
+ // Batch-embed all candidate texts in a single call (1×RTT instead of N×RTT)
129
+ let candidateEmbeddings: Float32Array[] = [];
130
+ if (embedding.isAvailable()) {
131
+ try {
132
+ candidateEmbeddings = await embedding.embed(
133
+ input.candidates.map((c) => c.text),
134
+ "query",
135
+ );
136
+ } catch (err) {
137
+ log.warn("instruction-detect: batch embedding failed:", err);
138
+ }
139
+ }
140
+
141
+ const results: RepeatedInstruction[] = [];
142
+
143
+ for (let i = 0; i < input.candidates.length; i++) {
144
+ const candidate = input.candidates[i];
145
+ const sessionIDs = new Set<string>();
146
+
147
+ // Path A: Vector search (when embeddings succeeded)
148
+ if (candidateEmbeddings.length > i) {
149
+ const hits = embedding.vectorSearchAllDistillations(candidateEmbeddings[i], pid, 20);
150
+ for (const hit of hits) {
151
+ if (
152
+ hit.similarity >= VECTOR_SIMILARITY_THRESHOLD &&
153
+ hit.session_id !== input.currentSessionID
154
+ ) {
155
+ sessionIDs.add(hit.session_id);
156
+ }
157
+ }
158
+ }
159
+
160
+ // Path B: FTS fallback (always runs to complement vector search)
161
+ const terms = filterTerms(candidate.text);
162
+ if (terms.length >= 2) {
163
+ // Cap at 5 terms to keep queries focused
164
+ const searchText = terms.slice(0, 5).join(" ");
165
+ const ftsHits = searchDistillationsFTS(pid, searchText);
166
+ for (const hit of ftsHits) {
167
+ if (hit.session_id !== input.currentSessionID) {
168
+ sessionIDs.add(hit.session_id);
169
+ }
170
+ }
171
+ }
172
+
173
+ if (sessionIDs.size >= threshold) {
174
+ results.push({
175
+ instruction: candidate.text,
176
+ priorSessionCount: sessionIDs.size,
177
+ });
178
+ }
179
+ }
180
+
181
+ return results;
182
+ }
183
+
184
+ /**
185
+ * Simple FTS5 search over distillation observations, returning session_id
186
+ * for cross-session counting. Searches all distillations (including archived).
187
+ *
188
+ * Uses OR semantics — we want to find any distillation mentioning any of
189
+ * the instruction's key terms, since paraphrased instructions may share
190
+ * only some terms. This is a recall-oriented search (find all possible
191
+ * matches), not a precision-oriented one.
192
+ *
193
+ * @param projectId The resolved project ID (from ensureProject).
194
+ * @param rawQuery Raw search text — will be converted to OR-based FTS expression.
195
+ */
196
+ function searchDistillationsFTS(
197
+ projectId: string,
198
+ rawQuery: string,
199
+ ): Array<{ id: string; session_id: string }> {
200
+ const matchExpr = ftsQueryOr(rawQuery);
201
+ if (matchExpr === EMPTY_QUERY) return [];
202
+
203
+ const sql = `SELECT d.id, d.session_id
204
+ FROM distillation_fts f
205
+ CROSS JOIN distillations d ON d.rowid = f.rowid
206
+ WHERE distillation_fts MATCH ?
207
+ AND d.project_id = ?
208
+ ORDER BY rank LIMIT 30`;
209
+
210
+ try {
211
+ return db().query(sql).all(matchExpr, projectId) as Array<{
212
+ id: string;
213
+ session_id: string;
214
+ }>;
215
+ } catch (err) {
216
+ log.warn("instruction-detect: FTS search failed:", err);
217
+ return [];
218
+ }
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Curator context formatting
223
+ // ---------------------------------------------------------------------------
224
+
225
+ /**
226
+ * Format repeated instructions as additional context for the curator prompt.
227
+ * Returns empty string if no repeated instructions found.
228
+ */
229
+ export function formatForCurator(instructions: RepeatedInstruction[]): string {
230
+ if (!instructions.length) return "";
231
+
232
+ const lines = instructions.map(
233
+ (i) =>
234
+ `- "${i.instruction}" (seen in ${i.priorSessionCount} prior session${i.priorSessionCount !== 1 ? "s" : ""})`,
235
+ );
236
+
237
+ return `\n\n---\nCROSS-SESSION REPEATED INSTRUCTIONS (high-confidence preference candidates):\nThe following user instructions have appeared in multiple prior sessions. These are strong candidates for "preference" entries:\n${lines.join("\n")}`;
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Full pipeline
242
+ // ---------------------------------------------------------------------------
243
+
244
+ /**
245
+ * Full detection pipeline: extract candidates from current session →
246
+ * search for repetitions across other sessions → format for curator.
247
+ *
248
+ * Returns a string to append to the curator's user prompt, or "" if
249
+ * nothing was found. Safe to call even when embeddings are unavailable
250
+ * (falls back to FTS-only).
251
+ */
252
+ export async function detectAndFormat(input: {
253
+ projectPath: string;
254
+ sessionID: string;
255
+ threshold?: number;
256
+ }): Promise<string> {
257
+ const messages = temporal.bySession(input.projectPath, input.sessionID);
258
+ const candidates = extractInstructionCandidates(messages);
259
+ if (!candidates.length) return "";
260
+
261
+ const repeated = await findRepeatedInstructions({
262
+ projectPath: input.projectPath,
263
+ currentSessionID: input.sessionID,
264
+ candidates,
265
+ threshold: input.threshold,
266
+ });
267
+
268
+ if (repeated.length) {
269
+ log.info(
270
+ `instruction-detect: ${repeated.length} repeated instruction(s) found across sessions`,
271
+ );
272
+ }
273
+
274
+ return formatForCurator(repeated);
275
+ }
package/src/log.ts CHANGED
@@ -14,8 +14,19 @@
14
14
  * When registered, every log call (regardless of `isDebug`) also forwards
15
15
  * to the sink. This is used by the gateway to bridge logs → Sentry without
16
16
  * adding a Sentry dependency to `@loreai/core`.
17
+ *
18
+ * ## File logging
19
+ *
20
+ * All log calls (info, warn, error) are written to a persistent log file
21
+ * at `~/.local/share/lore/lore.log` regardless of `LORE_DEBUG`.
22
+ * The file is rotated when it exceeds 5 MB (single `.log.1` backup).
23
+ * Use `lore logs` to view; disabled during tests (`NODE_ENV=test`).
17
24
  */
18
25
 
26
+ import { appendFileSync, renameSync, statSync, mkdirSync } from "node:fs";
27
+ import { join } from "node:path";
28
+ import { dataDir } from "./data-dir";
29
+
19
30
  // ---------------------------------------------------------------------------
20
31
  // Sink — optional external log consumer (e.g. Sentry)
21
32
  // ---------------------------------------------------------------------------
@@ -56,6 +67,77 @@ function findError(args: unknown[]): Error | undefined {
56
67
  return undefined;
57
68
  }
58
69
 
70
+ // ---------------------------------------------------------------------------
71
+ // File sink — persistent log file, independent of LORE_DEBUG
72
+ // ---------------------------------------------------------------------------
73
+
74
+ const LOG_MAX_BYTES = 5 * 1024 * 1024; // 5 MB
75
+ const ROTATION_CHECK_INTERVAL = 1000; // check size every N writes
76
+
77
+ let logPath: string | undefined;
78
+ let logPathResolved = false;
79
+ let writeCount = 0;
80
+
81
+ /**
82
+ * Resolve the log file path. Returns `undefined` in test environments
83
+ * or if the directory cannot be created.
84
+ */
85
+ function resolveLogPath(): string | undefined {
86
+ if (process.env.NODE_ENV === "test") return undefined;
87
+ try {
88
+ const dir = dataDir();
89
+ mkdirSync(dir, { recursive: true });
90
+ return join(dir, "lore.log");
91
+ } catch {
92
+ return undefined;
93
+ }
94
+ }
95
+
96
+ /** Return the resolved log file path (or `undefined` if unavailable). */
97
+ export function logFilePath(): string | undefined {
98
+ if (!logPathResolved) {
99
+ logPath = resolveLogPath();
100
+ logPathResolved = true;
101
+ }
102
+ return logPath;
103
+ }
104
+
105
+ /** Rotate the log file if it exceeds the size cap. */
106
+ function maybeRotate(): void {
107
+ if (!logPath) return;
108
+ try {
109
+ const stat = statSync(logPath);
110
+ if (stat.size > LOG_MAX_BYTES) {
111
+ renameSync(logPath, logPath + ".1");
112
+ }
113
+ } catch {
114
+ // File doesn't exist yet or stat failed — fine
115
+ }
116
+ }
117
+
118
+ /** Append a single log line to the persistent log file. */
119
+ function writeToFile(level: string, message: string): void {
120
+ const path = logFilePath();
121
+ if (!path) return;
122
+
123
+ // Periodic rotation check
124
+ if (++writeCount % ROTATION_CHECK_INTERVAL === 0) {
125
+ maybeRotate();
126
+ }
127
+
128
+ const ts = new Date().toISOString();
129
+ const tag = level.toUpperCase().padEnd(5);
130
+ // Flatten multiline messages for clean tail -f output
131
+ const flat = message.replace(/\n/g, "\\n");
132
+ const line = `${ts} [${tag}] ${flat}\n`;
133
+
134
+ try {
135
+ appendFileSync(path, line);
136
+ } catch {
137
+ // Silently degrade — logging failure shouldn't crash the app
138
+ }
139
+ }
140
+
59
141
  // ---------------------------------------------------------------------------
60
142
  // Public API
61
143
  // ---------------------------------------------------------------------------
@@ -63,19 +145,25 @@ function findError(args: unknown[]): Error | undefined {
63
145
  /** Log an informational status message. Suppressed unless LORE_DEBUG=1. */
64
146
  export function info(...args: unknown[]): void {
65
147
  if (isDebug) console.error("[lore]", ...args);
66
- sink?.info(formatArgs(args));
148
+ const msg = formatArgs(args);
149
+ sink?.info(msg);
150
+ writeToFile("info", msg);
67
151
  }
68
152
 
69
153
  /** Log a warning. Suppressed unless LORE_DEBUG=1. */
70
154
  export function warn(...args: unknown[]): void {
71
155
  if (isDebug) console.error("[lore] WARN:", ...args);
72
- sink?.warn(formatArgs(args));
156
+ const msg = formatArgs(args);
157
+ sink?.warn(msg);
158
+ writeToFile("warn", msg);
73
159
  }
74
160
 
75
161
  /** Log an error. Always visible — these indicate real failures. */
76
162
  export function error(...args: unknown[]): void {
77
163
  console.error("[lore]", ...args);
78
- sink?.error(formatArgs(args));
164
+ const msg = formatArgs(args);
165
+ sink?.error(msg);
166
+ writeToFile("error", msg);
79
167
 
80
168
  const err = findError(args);
81
169
  if (err) sink?.captureException(err);