@loreai/core 0.17.1 → 0.19.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 (248) 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 +85 -0
  10. package/dist/bun/db.d.ts.map +1 -1
  11. package/dist/bun/distillation.d.ts +2 -13
  12. package/dist/bun/distillation.d.ts.map +1 -1
  13. package/dist/bun/embedding-vendor.d.ts +22 -38
  14. package/dist/bun/embedding-vendor.d.ts.map +1 -1
  15. package/dist/bun/embedding-worker-types.d.ts +17 -12
  16. package/dist/bun/embedding-worker-types.d.ts.map +1 -1
  17. package/dist/bun/embedding-worker.d.ts +9 -2
  18. package/dist/bun/embedding-worker.d.ts.map +1 -1
  19. package/dist/bun/embedding-worker.js +38864 -33
  20. package/dist/bun/embedding-worker.js.map +4 -4
  21. package/dist/bun/embedding.d.ts +35 -23
  22. package/dist/bun/embedding.d.ts.map +1 -1
  23. package/dist/bun/gradient.d.ts +17 -1
  24. package/dist/bun/gradient.d.ts.map +1 -1
  25. package/dist/bun/import/detect.d.ts +14 -0
  26. package/dist/bun/import/detect.d.ts.map +1 -0
  27. package/dist/bun/import/extract.d.ts +43 -0
  28. package/dist/bun/import/extract.d.ts.map +1 -0
  29. package/dist/bun/import/history.d.ts +40 -0
  30. package/dist/bun/import/history.d.ts.map +1 -0
  31. package/dist/bun/import/index.d.ts +17 -0
  32. package/dist/bun/import/index.d.ts.map +1 -0
  33. package/dist/bun/import/providers/aider.d.ts +2 -0
  34. package/dist/bun/import/providers/aider.d.ts.map +1 -0
  35. package/dist/bun/import/providers/claude-code.d.ts +2 -0
  36. package/dist/bun/import/providers/claude-code.d.ts.map +1 -0
  37. package/dist/bun/import/providers/cline.d.ts +2 -0
  38. package/dist/bun/import/providers/cline.d.ts.map +1 -0
  39. package/dist/bun/import/providers/codex.d.ts +2 -0
  40. package/dist/bun/import/providers/codex.d.ts.map +1 -0
  41. package/dist/bun/import/providers/continue.d.ts +2 -0
  42. package/dist/bun/import/providers/continue.d.ts.map +1 -0
  43. package/dist/bun/import/providers/index.d.ts +19 -0
  44. package/dist/bun/import/providers/index.d.ts.map +1 -0
  45. package/dist/bun/import/providers/opencode.d.ts +2 -0
  46. package/dist/bun/import/providers/opencode.d.ts.map +1 -0
  47. package/dist/bun/import/providers/pi.d.ts +2 -0
  48. package/dist/bun/import/providers/pi.d.ts.map +1 -0
  49. package/dist/bun/import/types.d.ts +82 -0
  50. package/dist/bun/import/types.d.ts.map +1 -0
  51. package/dist/bun/index.d.ts +5 -2
  52. package/dist/bun/index.d.ts.map +1 -1
  53. package/dist/bun/index.js +3150 -439
  54. package/dist/bun/index.js.map +4 -4
  55. package/dist/bun/instruction-detect.d.ts +66 -0
  56. package/dist/bun/instruction-detect.d.ts.map +1 -0
  57. package/dist/bun/log.d.ts +9 -0
  58. package/dist/bun/log.d.ts.map +1 -1
  59. package/dist/bun/ltm.d.ts +139 -5
  60. package/dist/bun/ltm.d.ts.map +1 -1
  61. package/dist/bun/pattern-extract.d.ts +7 -0
  62. package/dist/bun/pattern-extract.d.ts.map +1 -1
  63. package/dist/bun/prompt.d.ts +1 -1
  64. package/dist/bun/prompt.d.ts.map +1 -1
  65. package/dist/bun/recall.d.ts.map +1 -1
  66. package/dist/bun/search.d.ts +5 -3
  67. package/dist/bun/search.d.ts.map +1 -1
  68. package/dist/bun/session-limiter.d.ts +26 -0
  69. package/dist/bun/session-limiter.d.ts.map +1 -0
  70. package/dist/bun/temporal.d.ts +2 -0
  71. package/dist/bun/temporal.d.ts.map +1 -1
  72. package/dist/bun/types.d.ts +1 -1
  73. package/dist/node/agents-file.d.ts +4 -0
  74. package/dist/node/agents-file.d.ts.map +1 -1
  75. package/dist/node/config.d.ts +2 -0
  76. package/dist/node/config.d.ts.map +1 -1
  77. package/dist/node/curator.d.ts +45 -0
  78. package/dist/node/curator.d.ts.map +1 -1
  79. package/dist/node/data-dir.d.ts +18 -0
  80. package/dist/node/data-dir.d.ts.map +1 -0
  81. package/dist/node/db.d.ts +85 -0
  82. package/dist/node/db.d.ts.map +1 -1
  83. package/dist/node/distillation.d.ts +2 -13
  84. package/dist/node/distillation.d.ts.map +1 -1
  85. package/dist/node/embedding-vendor.d.ts +22 -38
  86. package/dist/node/embedding-vendor.d.ts.map +1 -1
  87. package/dist/node/embedding-worker-types.d.ts +17 -12
  88. package/dist/node/embedding-worker-types.d.ts.map +1 -1
  89. package/dist/node/embedding-worker.d.ts +9 -2
  90. package/dist/node/embedding-worker.d.ts.map +1 -1
  91. package/dist/node/embedding-worker.js +38864 -33
  92. package/dist/node/embedding-worker.js.map +4 -4
  93. package/dist/node/embedding.d.ts +35 -23
  94. package/dist/node/embedding.d.ts.map +1 -1
  95. package/dist/node/gradient.d.ts +17 -1
  96. package/dist/node/gradient.d.ts.map +1 -1
  97. package/dist/node/import/detect.d.ts +14 -0
  98. package/dist/node/import/detect.d.ts.map +1 -0
  99. package/dist/node/import/extract.d.ts +43 -0
  100. package/dist/node/import/extract.d.ts.map +1 -0
  101. package/dist/node/import/history.d.ts +40 -0
  102. package/dist/node/import/history.d.ts.map +1 -0
  103. package/dist/node/import/index.d.ts +17 -0
  104. package/dist/node/import/index.d.ts.map +1 -0
  105. package/dist/node/import/providers/aider.d.ts +2 -0
  106. package/dist/node/import/providers/aider.d.ts.map +1 -0
  107. package/dist/node/import/providers/claude-code.d.ts +2 -0
  108. package/dist/node/import/providers/claude-code.d.ts.map +1 -0
  109. package/dist/node/import/providers/cline.d.ts +2 -0
  110. package/dist/node/import/providers/cline.d.ts.map +1 -0
  111. package/dist/node/import/providers/codex.d.ts +2 -0
  112. package/dist/node/import/providers/codex.d.ts.map +1 -0
  113. package/dist/node/import/providers/continue.d.ts +2 -0
  114. package/dist/node/import/providers/continue.d.ts.map +1 -0
  115. package/dist/node/import/providers/index.d.ts +19 -0
  116. package/dist/node/import/providers/index.d.ts.map +1 -0
  117. package/dist/node/import/providers/opencode.d.ts +2 -0
  118. package/dist/node/import/providers/opencode.d.ts.map +1 -0
  119. package/dist/node/import/providers/pi.d.ts +2 -0
  120. package/dist/node/import/providers/pi.d.ts.map +1 -0
  121. package/dist/node/import/types.d.ts +82 -0
  122. package/dist/node/import/types.d.ts.map +1 -0
  123. package/dist/node/index.d.ts +5 -2
  124. package/dist/node/index.d.ts.map +1 -1
  125. package/dist/node/index.js +3150 -439
  126. package/dist/node/index.js.map +4 -4
  127. package/dist/node/instruction-detect.d.ts +66 -0
  128. package/dist/node/instruction-detect.d.ts.map +1 -0
  129. package/dist/node/log.d.ts +9 -0
  130. package/dist/node/log.d.ts.map +1 -1
  131. package/dist/node/ltm.d.ts +139 -5
  132. package/dist/node/ltm.d.ts.map +1 -1
  133. package/dist/node/pattern-extract.d.ts +7 -0
  134. package/dist/node/pattern-extract.d.ts.map +1 -1
  135. package/dist/node/prompt.d.ts +1 -1
  136. package/dist/node/prompt.d.ts.map +1 -1
  137. package/dist/node/recall.d.ts.map +1 -1
  138. package/dist/node/search.d.ts +5 -3
  139. package/dist/node/search.d.ts.map +1 -1
  140. package/dist/node/session-limiter.d.ts +26 -0
  141. package/dist/node/session-limiter.d.ts.map +1 -0
  142. package/dist/node/temporal.d.ts +2 -0
  143. package/dist/node/temporal.d.ts.map +1 -1
  144. package/dist/node/types.d.ts +1 -1
  145. package/dist/types/agents-file.d.ts +4 -0
  146. package/dist/types/agents-file.d.ts.map +1 -1
  147. package/dist/types/config.d.ts +2 -0
  148. package/dist/types/config.d.ts.map +1 -1
  149. package/dist/types/curator.d.ts +45 -0
  150. package/dist/types/curator.d.ts.map +1 -1
  151. package/dist/types/data-dir.d.ts +18 -0
  152. package/dist/types/data-dir.d.ts.map +1 -0
  153. package/dist/types/db.d.ts +85 -0
  154. package/dist/types/db.d.ts.map +1 -1
  155. package/dist/types/distillation.d.ts +2 -13
  156. package/dist/types/distillation.d.ts.map +1 -1
  157. package/dist/types/embedding-vendor.d.ts +22 -38
  158. package/dist/types/embedding-vendor.d.ts.map +1 -1
  159. package/dist/types/embedding-worker-types.d.ts +17 -12
  160. package/dist/types/embedding-worker-types.d.ts.map +1 -1
  161. package/dist/types/embedding-worker.d.ts +9 -2
  162. package/dist/types/embedding-worker.d.ts.map +1 -1
  163. package/dist/types/embedding.d.ts +35 -23
  164. package/dist/types/embedding.d.ts.map +1 -1
  165. package/dist/types/gradient.d.ts +17 -1
  166. package/dist/types/gradient.d.ts.map +1 -1
  167. package/dist/types/import/detect.d.ts +14 -0
  168. package/dist/types/import/detect.d.ts.map +1 -0
  169. package/dist/types/import/extract.d.ts +43 -0
  170. package/dist/types/import/extract.d.ts.map +1 -0
  171. package/dist/types/import/history.d.ts +40 -0
  172. package/dist/types/import/history.d.ts.map +1 -0
  173. package/dist/types/import/index.d.ts +17 -0
  174. package/dist/types/import/index.d.ts.map +1 -0
  175. package/dist/types/import/providers/aider.d.ts +2 -0
  176. package/dist/types/import/providers/aider.d.ts.map +1 -0
  177. package/dist/types/import/providers/claude-code.d.ts +2 -0
  178. package/dist/types/import/providers/claude-code.d.ts.map +1 -0
  179. package/dist/types/import/providers/cline.d.ts +2 -0
  180. package/dist/types/import/providers/cline.d.ts.map +1 -0
  181. package/dist/types/import/providers/codex.d.ts +2 -0
  182. package/dist/types/import/providers/codex.d.ts.map +1 -0
  183. package/dist/types/import/providers/continue.d.ts +2 -0
  184. package/dist/types/import/providers/continue.d.ts.map +1 -0
  185. package/dist/types/import/providers/index.d.ts +19 -0
  186. package/dist/types/import/providers/index.d.ts.map +1 -0
  187. package/dist/types/import/providers/opencode.d.ts +2 -0
  188. package/dist/types/import/providers/opencode.d.ts.map +1 -0
  189. package/dist/types/import/providers/pi.d.ts +2 -0
  190. package/dist/types/import/providers/pi.d.ts.map +1 -0
  191. package/dist/types/import/types.d.ts +82 -0
  192. package/dist/types/import/types.d.ts.map +1 -0
  193. package/dist/types/index.d.ts +5 -2
  194. package/dist/types/index.d.ts.map +1 -1
  195. package/dist/types/instruction-detect.d.ts +66 -0
  196. package/dist/types/instruction-detect.d.ts.map +1 -0
  197. package/dist/types/log.d.ts +9 -0
  198. package/dist/types/log.d.ts.map +1 -1
  199. package/dist/types/ltm.d.ts +139 -5
  200. package/dist/types/ltm.d.ts.map +1 -1
  201. package/dist/types/pattern-extract.d.ts +7 -0
  202. package/dist/types/pattern-extract.d.ts.map +1 -1
  203. package/dist/types/prompt.d.ts +1 -1
  204. package/dist/types/prompt.d.ts.map +1 -1
  205. package/dist/types/recall.d.ts.map +1 -1
  206. package/dist/types/search.d.ts +5 -3
  207. package/dist/types/search.d.ts.map +1 -1
  208. package/dist/types/session-limiter.d.ts +26 -0
  209. package/dist/types/session-limiter.d.ts.map +1 -0
  210. package/dist/types/temporal.d.ts +2 -0
  211. package/dist/types/temporal.d.ts.map +1 -1
  212. package/dist/types/types.d.ts +1 -1
  213. package/package.json +3 -4
  214. package/src/agents-file.ts +41 -13
  215. package/src/config.ts +31 -18
  216. package/src/curator.ts +163 -75
  217. package/src/data-dir.ts +76 -0
  218. package/src/db.ts +457 -11
  219. package/src/distillation.ts +65 -16
  220. package/src/embedding-vendor.ts +23 -40
  221. package/src/embedding-worker-types.ts +19 -11
  222. package/src/embedding-worker.ts +111 -47
  223. package/src/embedding.ts +224 -174
  224. package/src/gradient.ts +192 -75
  225. package/src/import/detect.ts +37 -0
  226. package/src/import/extract.ts +137 -0
  227. package/src/import/history.ts +99 -0
  228. package/src/import/index.ts +45 -0
  229. package/src/import/providers/aider.ts +207 -0
  230. package/src/import/providers/claude-code.ts +339 -0
  231. package/src/import/providers/cline.ts +324 -0
  232. package/src/import/providers/codex.ts +369 -0
  233. package/src/import/providers/continue.ts +304 -0
  234. package/src/import/providers/index.ts +32 -0
  235. package/src/import/providers/opencode.ts +272 -0
  236. package/src/import/providers/pi.ts +332 -0
  237. package/src/import/types.ts +91 -0
  238. package/src/index.ts +13 -0
  239. package/src/instruction-detect.ts +275 -0
  240. package/src/log.ts +91 -3
  241. package/src/ltm.ts +789 -41
  242. package/src/pattern-extract.ts +41 -0
  243. package/src/prompt.ts +7 -1
  244. package/src/recall.ts +43 -5
  245. package/src/search.ts +7 -5
  246. package/src/session-limiter.ts +47 -0
  247. package/src/temporal.ts +18 -6
  248. package/src/types.ts +1 -1
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Conversation import system — detects and imports knowledge from
3
+ * external AI coding agent conversation histories.
4
+ */
5
+
6
+ // Types
7
+ export type {
8
+ ConversationChunk,
9
+ DetectedSession,
10
+ DetectionResult,
11
+ AgentHistoryProvider,
12
+ } from "./types";
13
+
14
+ // Detection
15
+ export { detectAll } from "./detect";
16
+
17
+ // Provider registry
18
+ export {
19
+ registerProvider,
20
+ getProviders,
21
+ getProvider,
22
+ clearProviders,
23
+ } from "./providers";
24
+
25
+ // Extraction (lazy — avoid pulling in LLM/curator deps for detection-only use)
26
+ export { extractKnowledge, type ExtractionProgress, type ExtractionResult } from "./extract";
27
+
28
+ // Idempotency
29
+ export {
30
+ isImported,
31
+ recordImport,
32
+ computeHash,
33
+ listImports,
34
+ type ImportRecord,
35
+ } from "./history";
36
+
37
+ // Register built-in providers on first import.
38
+ // Each provider module calls registerProvider() at load time.
39
+ import "./providers/claude-code";
40
+ import "./providers/codex";
41
+ import "./providers/opencode";
42
+ import "./providers/cline";
43
+ import "./providers/continue";
44
+ import "./providers/pi";
45
+ import "./providers/aider";
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Aider conversation history provider.
3
+ *
4
+ * Reads from Aider's per-project chat history file:
5
+ * <project-dir>/.aider.chat.history.md
6
+ *
7
+ * Format: Markdown with role headers like "#### user" / "#### assistant"
8
+ * separated by horizontal rules (---).
9
+ */
10
+ import { existsSync, readFileSync, statSync } from "fs";
11
+ import { join } from "path";
12
+ import type { AgentHistoryProvider, ConversationChunk, DetectedSession } from "../types";
13
+ import { registerProvider } from "./index";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Constants
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const HISTORY_FILE = ".aider.chat.history.md";
20
+ const DEFAULT_MAX_TOKENS = 12288;
21
+
22
+ // Aider uses "#### role" headers and "---" separators
23
+ const ROLE_HEADER_RE = /^####\s+(user|assistant|system)\s*$/i;
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Helpers
27
+ // ---------------------------------------------------------------------------
28
+
29
+ function estimateTokens(text: string): number {
30
+ return Math.ceil(text.length / 3);
31
+ }
32
+
33
+ type ParsedMessage = {
34
+ role: string;
35
+ text: string;
36
+ };
37
+
38
+ /**
39
+ * Parse Aider's markdown chat history into messages.
40
+ *
41
+ * Format:
42
+ * ```
43
+ * #### user
44
+ * message text here
45
+ *
46
+ * #### assistant
47
+ * response text here
48
+ * ```
49
+ *
50
+ * Messages are separated by `---` or by new `#### role` headers.
51
+ */
52
+ function parseAiderHistory(content: string): ParsedMessage[] {
53
+ const lines = content.split("\n");
54
+ const messages: ParsedMessage[] = [];
55
+ let currentRole: string | null = null;
56
+ let currentLines: string[] = [];
57
+
58
+ const flush = () => {
59
+ if (currentRole && currentLines.length > 0) {
60
+ const text = currentLines.join("\n").trim();
61
+ if (text) {
62
+ messages.push({ role: currentRole, text });
63
+ }
64
+ }
65
+ currentLines = [];
66
+ };
67
+
68
+ for (const line of lines) {
69
+ // Check for role header
70
+ const match = ROLE_HEADER_RE.exec(line);
71
+ if (match) {
72
+ flush();
73
+ currentRole = match[1].toLowerCase();
74
+ continue;
75
+ }
76
+
77
+ // Check for separator — starts a new conversation turn
78
+ if (line.trim() === "---") {
79
+ flush();
80
+ currentRole = null;
81
+ continue;
82
+ }
83
+
84
+ // Accumulate content if we're in a message
85
+ if (currentRole) {
86
+ currentLines.push(line);
87
+ }
88
+ }
89
+
90
+ // Flush final message
91
+ flush();
92
+
93
+ return messages;
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Provider implementation
98
+ // ---------------------------------------------------------------------------
99
+
100
+ const aiderProvider: AgentHistoryProvider = {
101
+ name: "aider",
102
+ displayName: "Aider",
103
+
104
+ detect(projectPath: string): DetectedSession[] {
105
+ const filePath = join(projectPath, HISTORY_FILE);
106
+ if (!existsSync(filePath)) return [];
107
+
108
+ let stat;
109
+ try {
110
+ stat = statSync(filePath);
111
+ } catch {
112
+ return [];
113
+ }
114
+
115
+ if (!stat.isFile() || stat.size === 0) return [];
116
+
117
+ // Quick scan to count messages without full parsing
118
+ let content: string;
119
+ try {
120
+ content = readFileSync(filePath, "utf-8");
121
+ } catch {
122
+ return [];
123
+ }
124
+
125
+ const messages = parseAiderHistory(content);
126
+ if (messages.length < 3) return [];
127
+
128
+ const estimatedTokens = estimateTokens(content);
129
+
130
+ return [
131
+ {
132
+ id: filePath,
133
+ label: `Chat history (${messages.length} messages, ${Math.round(stat.size / 1024)}KB)`,
134
+ startedAt: stat.birthtimeMs || stat.ctimeMs,
135
+ lastActivityAt: stat.mtimeMs,
136
+ estimatedTokens,
137
+ messageCount: messages.length,
138
+ },
139
+ ];
140
+ },
141
+
142
+ readChunks(
143
+ projectPath: string,
144
+ sessionIds: string[],
145
+ maxTokens: number = DEFAULT_MAX_TOKENS,
146
+ ): ConversationChunk[] {
147
+ const chunks: ConversationChunk[] = [];
148
+
149
+ for (const filePath of sessionIds) {
150
+ let content: string;
151
+ try {
152
+ content = readFileSync(filePath, "utf-8");
153
+ } catch {
154
+ continue;
155
+ }
156
+
157
+ const messages = parseAiderHistory(content);
158
+ if (messages.length === 0) continue;
159
+
160
+ // Get file mtime for timestamp
161
+ let fileTimestamp: number;
162
+ try {
163
+ fileTimestamp = statSync(filePath).mtimeMs;
164
+ } catch {
165
+ fileTimestamp = Date.now();
166
+ }
167
+
168
+ // Build chunks respecting maxTokens boundaries
169
+ let currentTexts: string[] = [];
170
+ let currentTokens = 0;
171
+ let chunkIndex = 0;
172
+
173
+ const flushChunk = () => {
174
+ if (currentTexts.length === 0) return;
175
+ chunkIndex++;
176
+ const text = currentTexts.join("\n\n");
177
+ chunks.push({
178
+ label: `Aider history (${chunkIndex})`,
179
+ text,
180
+ estimatedTokens: estimateTokens(text),
181
+ timestamp: fileTimestamp,
182
+ });
183
+ currentTexts = [];
184
+ currentTokens = 0;
185
+ };
186
+
187
+ for (const msg of messages) {
188
+ const formatted = `[${msg.role}] ${msg.text}`;
189
+ const msgTokens = estimateTokens(formatted);
190
+
191
+ if (currentTokens > 0 && currentTokens + msgTokens > maxTokens) {
192
+ flushChunk();
193
+ }
194
+
195
+ currentTexts.push(formatted);
196
+ currentTokens += msgTokens;
197
+ }
198
+
199
+ flushChunk();
200
+ }
201
+
202
+ return chunks;
203
+ },
204
+ };
205
+
206
+ // Auto-register on import
207
+ registerProvider(aiderProvider);
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Claude Code conversation history provider.
3
+ *
4
+ * Reads JSONL session files from ~/.claude/projects/<mangled-path>/<uuid>.jsonl
5
+ * Path mangling: project path with "/" replaced by "-"
6
+ * e.g. /home/byk/Code/foo → -home-byk-Code-foo
7
+ */
8
+ import { readdirSync, readFileSync, statSync } from "fs";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+ import type { AgentHistoryProvider, ConversationChunk, DetectedSession } from "../types";
12
+ import { registerProvider } from "./index";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // JSONL line types (only the fields we read)
16
+ // ---------------------------------------------------------------------------
17
+
18
+ type ClaudeCodeLine =
19
+ | {
20
+ type: "user";
21
+ message: { role: "user"; content: string | ContentBlock[] };
22
+ uuid: string;
23
+ timestamp: string;
24
+ sessionId: string;
25
+ }
26
+ | {
27
+ type: "assistant";
28
+ message: {
29
+ role: "assistant";
30
+ content: ContentBlock[];
31
+ model?: string;
32
+ };
33
+ uuid: string;
34
+ timestamp: string;
35
+ sessionId: string;
36
+ }
37
+ | { type: string; timestamp?: string; sessionId?: string };
38
+
39
+ type ContentBlock =
40
+ | { type: "text"; text: string }
41
+ | { type: "thinking"; thinking: string }
42
+ | { type: "tool_use"; name: string; input: Record<string, unknown> }
43
+ | { type: "tool_result"; tool_use_id: string; content: string | ContentBlock[] }
44
+ | { type: string };
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Constants
48
+ // ---------------------------------------------------------------------------
49
+
50
+ const CLAUDE_DIR = join(homedir(), ".claude", "projects");
51
+ const MAX_TOOL_OUTPUT_CHARS = 500;
52
+ const DEFAULT_MAX_TOKENS = 12288;
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Helpers
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /** Mangle a project path for Claude Code's directory naming. */
59
+ function manglePath(projectPath: string): string {
60
+ return projectPath.replace(/\//g, "-");
61
+ }
62
+
63
+ /** Estimate tokens from text length. */
64
+ function estimateTokens(text: string): number {
65
+ return Math.ceil(text.length / 3);
66
+ }
67
+
68
+ /** Truncate text to a max length, appending "..." if truncated. */
69
+ function truncate(text: string, max: number): string {
70
+ if (text.length <= max) return text;
71
+ return text.slice(0, max) + "...";
72
+ }
73
+
74
+ /** Extract text content from a single content block. */
75
+ function blockToText(block: ContentBlock): string | null {
76
+ switch (block.type) {
77
+ case "text":
78
+ return (block as { type: "text"; text: string }).text;
79
+ case "tool_use": {
80
+ const tu = block as { type: "tool_use"; name: string; input: Record<string, unknown> };
81
+ // Summarize tool input compactly
82
+ const inputSummary = truncate(JSON.stringify(tu.input), MAX_TOOL_OUTPUT_CHARS);
83
+ return `[tool: ${tu.name}] ${inputSummary}`;
84
+ }
85
+ case "tool_result": {
86
+ const tr = block as {
87
+ type: "tool_result";
88
+ content: string | ContentBlock[];
89
+ };
90
+ let content: string;
91
+ if (typeof tr.content === "string") {
92
+ content = tr.content;
93
+ } else if (Array.isArray(tr.content)) {
94
+ content = tr.content
95
+ .map((b) => {
96
+ if (b.type === "text") return (b as { type: "text"; text: string }).text;
97
+ return "";
98
+ })
99
+ .filter(Boolean)
100
+ .join("\n");
101
+ } else {
102
+ content = "";
103
+ }
104
+ return content ? `[tool_result] ${truncate(content, MAX_TOOL_OUTPUT_CHARS)}` : null;
105
+ }
106
+ case "thinking":
107
+ // Skip thinking/reasoning blocks entirely
108
+ return null;
109
+ default:
110
+ return null;
111
+ }
112
+ }
113
+
114
+ /** Extract conversation text from a parsed JSONL line. */
115
+ function lineToText(parsed: ClaudeCodeLine): string | null {
116
+ if (parsed.type === "user") {
117
+ const msg = parsed as Extract<ClaudeCodeLine, { type: "user" }>;
118
+ const content = msg.message.content;
119
+ if (typeof content === "string") {
120
+ return `[user] ${content}`;
121
+ }
122
+ // Array content — extract text blocks, tool_result blocks
123
+ const parts = (content as ContentBlock[])
124
+ .map(blockToText)
125
+ .filter(Boolean) as string[];
126
+ return parts.length > 0 ? `[user] ${parts.join("\n")}` : null;
127
+ }
128
+
129
+ if (parsed.type === "assistant") {
130
+ const msg = parsed as Extract<ClaudeCodeLine, { type: "assistant" }>;
131
+ const blocks = msg.message.content;
132
+ if (!Array.isArray(blocks)) return null;
133
+ const parts = blocks.map(blockToText).filter(Boolean) as string[];
134
+ return parts.length > 0 ? `[assistant] ${parts.join("\n")}` : null;
135
+ }
136
+
137
+ return null;
138
+ }
139
+
140
+ /** Parse a JSONL file into typed lines, skipping malformed lines. */
141
+ function parseJSONL(filePath: string): ClaudeCodeLine[] {
142
+ const raw = readFileSync(filePath, "utf-8");
143
+ const lines: ClaudeCodeLine[] = [];
144
+ for (const line of raw.split("\n")) {
145
+ if (!line.trim()) continue;
146
+ try {
147
+ lines.push(JSON.parse(line) as ClaudeCodeLine);
148
+ } catch {
149
+ // Skip malformed lines
150
+ }
151
+ }
152
+ return lines;
153
+ }
154
+
155
+ /**
156
+ * Get metadata from a session file without reading the full contents.
157
+ * Reads first and last few lines for timestamps and session ID.
158
+ */
159
+ function getSessionMetadata(
160
+ filePath: string,
161
+ ): {
162
+ sessionId: string;
163
+ startedAt: number;
164
+ lastActivityAt: number;
165
+ messageCount: number;
166
+ estimatedTokens: number;
167
+ } | null {
168
+ let raw: string;
169
+ try {
170
+ raw = readFileSync(filePath, "utf-8");
171
+ } catch {
172
+ return null;
173
+ }
174
+
175
+ const lines = raw.split("\n").filter((l) => l.trim());
176
+ if (lines.length === 0) return null;
177
+
178
+ let sessionId: string | undefined;
179
+ let startedAt = Infinity;
180
+ let lastActivityAt = 0;
181
+ let messageCount = 0;
182
+
183
+ for (const line of lines) {
184
+ try {
185
+ const parsed = JSON.parse(line) as ClaudeCodeLine;
186
+ if (parsed.sessionId && !sessionId) sessionId = parsed.sessionId;
187
+
188
+ if (parsed.timestamp) {
189
+ const ts = new Date(parsed.timestamp).getTime();
190
+ if (!Number.isNaN(ts)) {
191
+ if (ts < startedAt) startedAt = ts;
192
+ if (ts > lastActivityAt) lastActivityAt = ts;
193
+ }
194
+ }
195
+
196
+ if (parsed.type === "user" || parsed.type === "assistant") {
197
+ messageCount++;
198
+ }
199
+ } catch {
200
+ // Skip malformed
201
+ }
202
+ }
203
+
204
+ if (!sessionId || messageCount === 0) return null;
205
+
206
+ // Estimate tokens from file size (rough: ~3 chars per token, but JSONL
207
+ // has structural overhead so use ~5 chars per token for files)
208
+ const fileSize = raw.length;
209
+ const estimatedTokens = Math.ceil(fileSize / 5);
210
+
211
+ return {
212
+ sessionId,
213
+ startedAt: startedAt === Infinity ? Date.now() : startedAt,
214
+ lastActivityAt,
215
+ messageCount,
216
+ estimatedTokens,
217
+ };
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Provider implementation
222
+ // ---------------------------------------------------------------------------
223
+
224
+ const claudeCodeProvider: AgentHistoryProvider = {
225
+ name: "claude-code",
226
+ displayName: "Claude Code",
227
+
228
+ detect(projectPath: string): DetectedSession[] {
229
+ const mangled = manglePath(projectPath);
230
+ const dir = join(CLAUDE_DIR, mangled);
231
+
232
+ let entries: string[];
233
+ try {
234
+ entries = readdirSync(dir);
235
+ } catch {
236
+ return []; // Directory doesn't exist
237
+ }
238
+
239
+ const sessions: DetectedSession[] = [];
240
+ for (const entry of entries) {
241
+ if (!entry.endsWith(".jsonl")) continue;
242
+
243
+ const filePath = join(dir, entry);
244
+ try {
245
+ const stat = statSync(filePath);
246
+ if (!stat.isFile()) continue;
247
+ } catch {
248
+ continue;
249
+ }
250
+
251
+ const meta = getSessionMetadata(filePath);
252
+ if (!meta) continue;
253
+
254
+ // Skip trivially small sessions (< 3 messages)
255
+ if (meta.messageCount < 3) continue;
256
+
257
+ const dateStr = new Date(meta.startedAt).toISOString().slice(0, 10);
258
+ sessions.push({
259
+ id: filePath,
260
+ label: `${dateStr} (${meta.messageCount} messages)`,
261
+ startedAt: meta.startedAt,
262
+ lastActivityAt: meta.lastActivityAt,
263
+ estimatedTokens: meta.estimatedTokens,
264
+ messageCount: meta.messageCount,
265
+ });
266
+ }
267
+
268
+ // Sort by most recent first
269
+ return sessions.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
270
+ },
271
+
272
+ readChunks(
273
+ _projectPath: string,
274
+ sessionIds: string[],
275
+ maxTokens: number = DEFAULT_MAX_TOKENS,
276
+ ): ConversationChunk[] {
277
+ const chunks: ConversationChunk[] = [];
278
+
279
+ for (const filePath of sessionIds) {
280
+ const lines = parseJSONL(filePath);
281
+
282
+ // Extract conversation messages as text
283
+ const messages: { text: string; timestamp: number }[] = [];
284
+ for (const line of lines) {
285
+ const text = lineToText(line);
286
+ if (!text) continue;
287
+
288
+ const ts = "timestamp" in line && line.timestamp
289
+ ? new Date(line.timestamp as string).getTime()
290
+ : Date.now();
291
+
292
+ messages.push({ text, timestamp: ts });
293
+ }
294
+
295
+ if (messages.length === 0) continue;
296
+
297
+ // Build chunks respecting maxTokens boundaries
298
+ let currentTexts: string[] = [];
299
+ let currentTokens = 0;
300
+ let chunkStart = messages[0].timestamp;
301
+ let chunkIndex = 0;
302
+
303
+ const flushChunk = () => {
304
+ if (currentTexts.length === 0) return;
305
+ chunkIndex++;
306
+ const text = currentTexts.join("\n\n");
307
+ chunks.push({
308
+ label: `Claude Code ${new Date(chunkStart).toISOString().slice(0, 10)} (${chunkIndex})`,
309
+ text,
310
+ estimatedTokens: estimateTokens(text),
311
+ timestamp: chunkStart,
312
+ });
313
+ currentTexts = [];
314
+ currentTokens = 0;
315
+ };
316
+
317
+ for (const msg of messages) {
318
+ const msgTokens = estimateTokens(msg.text);
319
+
320
+ // If adding this message would exceed the limit, flush first
321
+ if (currentTokens > 0 && currentTokens + msgTokens > maxTokens) {
322
+ flushChunk();
323
+ chunkStart = msg.timestamp;
324
+ }
325
+
326
+ currentTexts.push(msg.text);
327
+ currentTokens += msgTokens;
328
+ }
329
+
330
+ // Flush remaining
331
+ flushChunk();
332
+ }
333
+
334
+ return chunks;
335
+ },
336
+ };
337
+
338
+ // Auto-register on import
339
+ registerProvider(claudeCodeProvider);