@loreai/core 0.17.0 → 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
@@ -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);
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Cline (VS Code extension) conversation history provider.
3
+ *
4
+ * Reads JSON task files from VS Code's globalStorage for the Cline extension:
5
+ * ~/.vscode/data/User/globalStorage/saoudrizwan.claude-dev/tasks/<taskId>/
6
+ *
7
+ * Each task directory contains:
8
+ * - api_conversation_history.json — Anthropic MessageParam[] format
9
+ * - task_metadata.json — optional metadata
10
+ *
11
+ * The task history index at:
12
+ * globalStorage/saoudrizwan.claude-dev/state/taskHistory.json
13
+ * maps tasks to their CWD (cwdOnTaskInitialization).
14
+ */
15
+ import { readdirSync, readFileSync, existsSync, statSync } from "fs";
16
+ import { join } from "path";
17
+ import { homedir } from "os";
18
+ import type { AgentHistoryProvider, ConversationChunk, DetectedSession } from "../types";
19
+ import { registerProvider } from "./index";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Constants
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const MAX_TOOL_OUTPUT_CHARS = 500;
26
+ const DEFAULT_MAX_TOKENS = 12288;
27
+
28
+ // Extension IDs — Cline has been published under multiple IDs
29
+ const EXTENSION_IDS = [
30
+ "saoudrizwan.claude-dev",
31
+ "cline.cline",
32
+ ];
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Types (Cline's Anthropic-compatible format)
36
+ // ---------------------------------------------------------------------------
37
+
38
+ type ContentBlock =
39
+ | { type: "text"; text: string }
40
+ | { type: "tool_use"; id: string; name: string; input: Record<string, unknown> }
41
+ | { type: "tool_result"; tool_use_id: string; content: string | ContentBlock[] }
42
+ | { type: "image"; source?: unknown }
43
+ | { type: string };
44
+
45
+ type ClineMessage = {
46
+ role: "user" | "assistant";
47
+ content: string | ContentBlock[];
48
+ };
49
+
50
+ type TaskHistoryItem = {
51
+ id: string;
52
+ ts: number;
53
+ task: string;
54
+ tokensIn?: number;
55
+ tokensOut?: number;
56
+ totalCost?: number;
57
+ cwdOnTaskInitialization?: string;
58
+ modelId?: string;
59
+ };
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Helpers
63
+ // ---------------------------------------------------------------------------
64
+
65
+ function estimateTokens(text: string): number {
66
+ return Math.ceil(text.length / 3);
67
+ }
68
+
69
+ function truncate(text: string, max: number): string {
70
+ if (text.length <= max) return text;
71
+ return text.slice(0, max) + "...";
72
+ }
73
+
74
+ /**
75
+ * Find the VS Code globalStorage directories to search.
76
+ * Checks multiple VS Code variants (stable, insiders, OSS) and extension IDs.
77
+ */
78
+ function findGlobalStorageDirs(): string[] {
79
+ const home = homedir();
80
+ const dirs: string[] = [];
81
+
82
+ // VS Code storage paths by platform
83
+ const basePaths: string[] = [];
84
+ const platform = process.platform;
85
+
86
+ if (platform === "darwin") {
87
+ basePaths.push(
88
+ join(home, "Library", "Application Support", "Code", "User", "globalStorage"),
89
+ join(home, "Library", "Application Support", "Code - Insiders", "User", "globalStorage"),
90
+ join(home, "Library", "Application Support", "VSCodium", "User", "globalStorage"),
91
+ );
92
+ } else if (platform === "win32") {
93
+ const appdata = process.env.APPDATA || join(home, "AppData", "Roaming");
94
+ basePaths.push(
95
+ join(appdata, "Code", "User", "globalStorage"),
96
+ join(appdata, "Code - Insiders", "User", "globalStorage"),
97
+ join(appdata, "VSCodium", "User", "globalStorage"),
98
+ );
99
+ } else {
100
+ // Linux
101
+ const configHome = process.env.XDG_CONFIG_HOME || join(home, ".config");
102
+ basePaths.push(
103
+ join(configHome, "Code", "User", "globalStorage"),
104
+ join(configHome, "Code - Insiders", "User", "globalStorage"),
105
+ join(configHome, "VSCodium", "User", "globalStorage"),
106
+ );
107
+ // Also check the older data path
108
+ basePaths.push(
109
+ join(home, ".vscode", "data", "User", "globalStorage"),
110
+ join(home, ".vscode-insiders", "data", "User", "globalStorage"),
111
+ );
112
+ }
113
+
114
+ for (const base of basePaths) {
115
+ for (const extId of EXTENSION_IDS) {
116
+ const dir = join(base, extId);
117
+ if (existsSync(dir)) dirs.push(dir);
118
+ }
119
+ }
120
+
121
+ return dirs;
122
+ }
123
+
124
+ /** Load the task history index and filter by project CWD. */
125
+ function loadTaskHistory(
126
+ storageDir: string,
127
+ projectPath: string,
128
+ ): TaskHistoryItem[] {
129
+ // Try both known locations for the history file
130
+ const paths = [
131
+ join(storageDir, "state", "taskHistory.json"),
132
+ join(storageDir, "taskHistory.json"),
133
+ ];
134
+
135
+ for (const historyPath of paths) {
136
+ if (!existsSync(historyPath)) continue;
137
+
138
+ try {
139
+ const raw = readFileSync(historyPath, "utf-8");
140
+ const items = JSON.parse(raw) as TaskHistoryItem[];
141
+ if (!Array.isArray(items)) continue;
142
+
143
+ return items.filter(
144
+ (item) => item.cwdOnTaskInitialization === projectPath,
145
+ );
146
+ } catch {
147
+ continue;
148
+ }
149
+ }
150
+
151
+ return [];
152
+ }
153
+
154
+ /** Read the API conversation history for a task. */
155
+ function readConversation(taskDir: string): ClineMessage[] {
156
+ const filePath = join(taskDir, "api_conversation_history.json");
157
+ if (!existsSync(filePath)) return [];
158
+
159
+ try {
160
+ const raw = readFileSync(filePath, "utf-8");
161
+ const messages = JSON.parse(raw) as ClineMessage[];
162
+ return Array.isArray(messages) ? messages : [];
163
+ } catch {
164
+ return [];
165
+ }
166
+ }
167
+
168
+ /** Convert a content block to text. */
169
+ function blockToText(block: ContentBlock): string | null {
170
+ switch (block.type) {
171
+ case "text":
172
+ return (block as { type: "text"; text: string }).text;
173
+ case "tool_use": {
174
+ const tu = block as { type: "tool_use"; name: string; input: Record<string, unknown> };
175
+ return `[tool: ${tu.name}] ${truncate(JSON.stringify(tu.input), MAX_TOOL_OUTPUT_CHARS)}`;
176
+ }
177
+ case "tool_result": {
178
+ const tr = block as { type: "tool_result"; content: string | ContentBlock[] };
179
+ let content: string;
180
+ if (typeof tr.content === "string") {
181
+ content = tr.content;
182
+ } else if (Array.isArray(tr.content)) {
183
+ content = tr.content
184
+ .filter((b): b is { type: "text"; text: string } => b.type === "text")
185
+ .map((b) => b.text)
186
+ .join("\n");
187
+ } else {
188
+ content = "";
189
+ }
190
+ return content ? `[tool_result] ${truncate(content, MAX_TOOL_OUTPUT_CHARS)}` : null;
191
+ }
192
+ default:
193
+ return null;
194
+ }
195
+ }
196
+
197
+ /** Convert a ClineMessage to text. */
198
+ function messageToText(msg: ClineMessage): string | null {
199
+ if (typeof msg.content === "string") {
200
+ return msg.content ? `[${msg.role}] ${msg.content}` : null;
201
+ }
202
+
203
+ const parts = (msg.content as ContentBlock[])
204
+ .map(blockToText)
205
+ .filter(Boolean) as string[];
206
+ return parts.length > 0 ? `[${msg.role}] ${parts.join("\n")}` : null;
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Provider implementation
211
+ // ---------------------------------------------------------------------------
212
+
213
+ const clineProvider: AgentHistoryProvider = {
214
+ name: "cline",
215
+ displayName: "Cline",
216
+
217
+ detect(projectPath: string): DetectedSession[] {
218
+ const sessions: DetectedSession[] = [];
219
+ const storageDirs = findGlobalStorageDirs();
220
+
221
+ for (const storageDir of storageDirs) {
222
+ const tasks = loadTaskHistory(storageDir, projectPath);
223
+
224
+ for (const task of tasks) {
225
+ const taskDir = join(storageDir, "tasks", task.id);
226
+ if (!existsSync(taskDir)) continue;
227
+
228
+ // Quick count of messages
229
+ const messages = readConversation(taskDir);
230
+ if (messages.length < 3) continue;
231
+
232
+ const dateStr = new Date(task.ts).toISOString().slice(0, 10);
233
+ const label = task.task
234
+ ? `${dateStr} - ${truncate(task.task, 60)} (${messages.length} messages)`
235
+ : `${dateStr} (${messages.length} messages)`;
236
+
237
+ // Estimate tokens from file size
238
+ const historyFile = join(taskDir, "api_conversation_history.json");
239
+ let estimatedTokens = messages.length * 500;
240
+ try {
241
+ const stat = statSync(historyFile);
242
+ estimatedTokens = Math.ceil(stat.size / 5);
243
+ } catch {
244
+ // Use the message-count-based estimate
245
+ }
246
+
247
+ sessions.push({
248
+ id: taskDir,
249
+ label,
250
+ startedAt: task.ts,
251
+ lastActivityAt: task.ts,
252
+ estimatedTokens,
253
+ messageCount: messages.length,
254
+ });
255
+ }
256
+ }
257
+
258
+ return sessions.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
259
+ },
260
+
261
+ readChunks(
262
+ _projectPath: string,
263
+ sessionIds: string[],
264
+ maxTokens: number = DEFAULT_MAX_TOKENS,
265
+ ): ConversationChunk[] {
266
+ const chunks: ConversationChunk[] = [];
267
+
268
+ for (const taskDir of sessionIds) {
269
+ const messages = readConversation(taskDir);
270
+ if (messages.length === 0) continue;
271
+
272
+ // Get timestamp from directory stat
273
+ let sessionTimestamp: number;
274
+ try {
275
+ sessionTimestamp = statSync(taskDir).mtimeMs;
276
+ } catch {
277
+ sessionTimestamp = Date.now();
278
+ }
279
+
280
+ const textMessages: { text: string }[] = [];
281
+ for (const msg of messages) {
282
+ const text = messageToText(msg);
283
+ if (text) textMessages.push({ text });
284
+ }
285
+
286
+ if (textMessages.length === 0) continue;
287
+
288
+ // Build chunks respecting maxTokens boundaries
289
+ let currentTexts: string[] = [];
290
+ let currentTokens = 0;
291
+ let chunkIndex = 0;
292
+
293
+ const flushChunk = () => {
294
+ if (currentTexts.length === 0) return;
295
+ chunkIndex++;
296
+ const text = currentTexts.join("\n\n");
297
+ chunks.push({
298
+ label: `Cline ${new Date(sessionTimestamp).toISOString().slice(0, 10)} (${chunkIndex})`,
299
+ text,
300
+ estimatedTokens: estimateTokens(text),
301
+ timestamp: sessionTimestamp,
302
+ });
303
+ currentTexts = [];
304
+ currentTokens = 0;
305
+ };
306
+
307
+ for (const msg of textMessages) {
308
+ const msgTokens = estimateTokens(msg.text);
309
+ if (currentTokens > 0 && currentTokens + msgTokens > maxTokens) {
310
+ flushChunk();
311
+ }
312
+ currentTexts.push(msg.text);
313
+ currentTokens += msgTokens;
314
+ }
315
+
316
+ flushChunk();
317
+ }
318
+
319
+ return chunks;
320
+ },
321
+ };
322
+
323
+ // Auto-register on import
324
+ registerProvider(clineProvider);