@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
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Codex (OpenAI) conversation history provider.
3
+ *
4
+ * Reads JSONL session files from ~/.codex/sessions/YYYY/MM/DD/<rollout>.jsonl
5
+ * and archived sessions from ~/.codex/archived_sessions/*.jsonl
6
+ *
7
+ * Each JSONL file starts with a session_meta line containing { id, cwd, timestamp, ... }
8
+ * followed by response_item, event_msg, compacted, and turn_context lines.
9
+ */
10
+ import { readdirSync, readFileSync, statSync, existsSync } from "fs";
11
+ import { join } from "path";
12
+ import { homedir } from "os";
13
+ import type { AgentHistoryProvider, ConversationChunk, DetectedSession } from "../types";
14
+ import { registerProvider } from "./index";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Constants
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const CODEX_DIR = join(homedir(), ".codex");
21
+ const SESSIONS_DIR = join(CODEX_DIR, "sessions");
22
+ const ARCHIVED_DIR = join(CODEX_DIR, "archived_sessions");
23
+ const MAX_TOOL_OUTPUT_CHARS = 500;
24
+ const DEFAULT_MAX_TOKENS = 12288;
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // JSONL types (only the fields we read)
28
+ // ---------------------------------------------------------------------------
29
+
30
+ type CodexLine =
31
+ | {
32
+ type: "session_meta";
33
+ payload: {
34
+ meta: {
35
+ id: string;
36
+ timestamp: string;
37
+ cwd: string;
38
+ source?: string;
39
+ model_provider?: string;
40
+ cli_version?: string;
41
+ };
42
+ };
43
+ }
44
+ | {
45
+ type: "response_item";
46
+ payload: ResponseItem;
47
+ }
48
+ | {
49
+ type: "event_msg";
50
+ payload: {
51
+ type?: string;
52
+ output?: string;
53
+ truncated?: boolean;
54
+ };
55
+ }
56
+ | {
57
+ type: "compacted";
58
+ payload: {
59
+ replacement_history?: ResponseItem[];
60
+ };
61
+ }
62
+ | { type: string; payload?: unknown };
63
+
64
+ type ResponseItem = {
65
+ type?: string;
66
+ role?: string;
67
+ content?: string | ContentPart[];
68
+ name?: string;
69
+ arguments?: string;
70
+ output?: string;
71
+ status?: string;
72
+ };
73
+
74
+ type ContentPart =
75
+ | { type: "input_text"; text: string }
76
+ | { type: "output_text"; text: string }
77
+ | { type: "text"; text: string }
78
+ | { type: string; [key: string]: unknown };
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Helpers
82
+ // ---------------------------------------------------------------------------
83
+
84
+ function estimateTokens(text: string): number {
85
+ return Math.ceil(text.length / 3);
86
+ }
87
+
88
+ function truncate(text: string, max: number): string {
89
+ if (text.length <= max) return text;
90
+ return text.slice(0, max) + "...";
91
+ }
92
+
93
+ /** Recursively find all .jsonl files under a directory. */
94
+ function findJsonlFiles(dir: string): string[] {
95
+ const results: string[] = [];
96
+ if (!existsSync(dir)) return results;
97
+
98
+ const walk = (d: string) => {
99
+ let entries: string[];
100
+ try {
101
+ entries = readdirSync(d);
102
+ } catch {
103
+ return;
104
+ }
105
+ for (const entry of entries) {
106
+ const full = join(d, entry);
107
+ try {
108
+ const stat = statSync(full);
109
+ if (stat.isDirectory()) walk(full);
110
+ else if (stat.isFile() && entry.endsWith(".jsonl")) results.push(full);
111
+ } catch {
112
+ // Skip inaccessible entries
113
+ }
114
+ }
115
+ };
116
+
117
+ walk(dir);
118
+ return results;
119
+ }
120
+
121
+ /** Extract text content from a ResponseItem. */
122
+ function responseItemToText(item: ResponseItem): string | null {
123
+ if (!item) return null;
124
+
125
+ // Message items (user/assistant text)
126
+ if (item.type === "message" && item.role && item.content) {
127
+ const text = extractContent(item.content);
128
+ if (text) return `[${item.role}] ${text}`;
129
+ }
130
+
131
+ // Function/tool call items
132
+ if (item.type === "function_call" && item.name) {
133
+ const args = item.arguments
134
+ ? truncate(item.arguments, MAX_TOOL_OUTPUT_CHARS)
135
+ : "";
136
+ return `[tool: ${item.name}] ${args}`;
137
+ }
138
+
139
+ // Function/tool output items
140
+ if (item.type === "function_call_output" && item.output) {
141
+ return `[tool_result] ${truncate(item.output, MAX_TOOL_OUTPUT_CHARS)}`;
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ /** Extract text from content (string or array of content parts). */
148
+ function extractContent(content: string | ContentPart[]): string | null {
149
+ if (typeof content === "string") return content;
150
+ if (!Array.isArray(content)) return null;
151
+
152
+ const parts: string[] = [];
153
+ for (const part of content) {
154
+ if ("text" in part && typeof part.text === "string") {
155
+ parts.push(part.text);
156
+ }
157
+ }
158
+ return parts.length > 0 ? parts.join("\n") : null;
159
+ }
160
+
161
+ /** Parse a JSONL file, returning typed lines. */
162
+ function parseJSONL(filePath: string): CodexLine[] {
163
+ let raw: string;
164
+ try {
165
+ raw = readFileSync(filePath, "utf-8");
166
+ } catch {
167
+ return [];
168
+ }
169
+
170
+ const lines: CodexLine[] = [];
171
+ for (const line of raw.split("\n")) {
172
+ if (!line.trim()) continue;
173
+ try {
174
+ lines.push(JSON.parse(line) as CodexLine);
175
+ } catch {
176
+ // Skip malformed
177
+ }
178
+ }
179
+ return lines;
180
+ }
181
+
182
+ /** Get session metadata from the first line of a JSONL file. */
183
+ function getSessionMeta(
184
+ filePath: string,
185
+ ): {
186
+ id: string;
187
+ cwd: string;
188
+ timestamp: string;
189
+ messageCount: number;
190
+ fileSize: number;
191
+ } | null {
192
+ let raw: string;
193
+ try {
194
+ raw = readFileSync(filePath, "utf-8");
195
+ } catch {
196
+ return null;
197
+ }
198
+
199
+ const lines = raw.split("\n").filter((l) => l.trim());
200
+ if (lines.length === 0) return null;
201
+
202
+ // First line should be session_meta
203
+ let meta: CodexLine;
204
+ try {
205
+ meta = JSON.parse(lines[0]) as CodexLine;
206
+ } catch {
207
+ return null;
208
+ }
209
+
210
+ if (meta.type !== "session_meta") return null;
211
+
212
+ const payload = meta.payload as {
213
+ meta: { id: string; cwd: string; timestamp: string };
214
+ };
215
+
216
+ // Count message-like lines
217
+ let messageCount = 0;
218
+ for (const line of lines) {
219
+ try {
220
+ const parsed = JSON.parse(line) as CodexLine;
221
+ if (parsed.type === "response_item" || parsed.type === "event_msg") {
222
+ messageCount++;
223
+ }
224
+ } catch {
225
+ // Skip
226
+ }
227
+ }
228
+
229
+ return {
230
+ id: payload.meta.id,
231
+ cwd: payload.meta.cwd,
232
+ timestamp: payload.meta.timestamp,
233
+ messageCount,
234
+ fileSize: raw.length,
235
+ };
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Provider implementation
240
+ // ---------------------------------------------------------------------------
241
+
242
+ const codexProvider: AgentHistoryProvider = {
243
+ name: "codex",
244
+ displayName: "Codex",
245
+
246
+ detect(projectPath: string): DetectedSession[] {
247
+ const sessions: DetectedSession[] = [];
248
+
249
+ // Scan both active and archived sessions
250
+ const allFiles = [
251
+ ...findJsonlFiles(SESSIONS_DIR),
252
+ ...findJsonlFiles(ARCHIVED_DIR),
253
+ ];
254
+
255
+ for (const filePath of allFiles) {
256
+ const meta = getSessionMeta(filePath);
257
+ if (!meta) continue;
258
+
259
+ // Match by CWD — the session must have been started in this project
260
+ if (meta.cwd !== projectPath) continue;
261
+
262
+ // Skip trivially small sessions
263
+ if (meta.messageCount < 3) continue;
264
+
265
+ const ts = new Date(meta.timestamp).getTime();
266
+ const estimatedTokens = Math.ceil(meta.fileSize / 5);
267
+ const dateStr = new Date(ts).toISOString().slice(0, 10);
268
+
269
+ sessions.push({
270
+ id: filePath,
271
+ label: `${dateStr} (${meta.messageCount} messages)`,
272
+ startedAt: ts,
273
+ lastActivityAt: ts, // Best approximation without reading all lines
274
+ estimatedTokens,
275
+ messageCount: meta.messageCount,
276
+ });
277
+ }
278
+
279
+ return sessions.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
280
+ },
281
+
282
+ readChunks(
283
+ _projectPath: string,
284
+ sessionIds: string[],
285
+ maxTokens: number = DEFAULT_MAX_TOKENS,
286
+ ): ConversationChunk[] {
287
+ const chunks: ConversationChunk[] = [];
288
+
289
+ for (const filePath of sessionIds) {
290
+ const lines = parseJSONL(filePath);
291
+ const messages: { text: string; timestamp: number }[] = [];
292
+
293
+ // Find session timestamp for labeling
294
+ let sessionTimestamp = Date.now();
295
+ const firstLine = lines[0];
296
+ if (firstLine?.type === "session_meta") {
297
+ const meta = firstLine as Extract<CodexLine, { type: "session_meta" }>;
298
+ const ts = new Date(meta.payload.meta.timestamp).getTime();
299
+ if (!Number.isNaN(ts)) sessionTimestamp = ts;
300
+ }
301
+
302
+ for (const line of lines) {
303
+ if (line.type === "response_item") {
304
+ const ri = line as Extract<CodexLine, { type: "response_item" }>;
305
+ const text = responseItemToText(ri.payload);
306
+ if (text) {
307
+ messages.push({ text, timestamp: sessionTimestamp });
308
+ }
309
+ } else if (line.type === "event_msg") {
310
+ const ev = line as Extract<CodexLine, { type: "event_msg" }>;
311
+ if (ev.payload.output) {
312
+ messages.push({
313
+ text: `[exec] ${truncate(ev.payload.output, MAX_TOOL_OUTPUT_CHARS)}`,
314
+ timestamp: sessionTimestamp,
315
+ });
316
+ }
317
+ } else if (line.type === "compacted") {
318
+ const comp = line as Extract<CodexLine, { type: "compacted" }>;
319
+ if (comp.payload.replacement_history) {
320
+ // After compaction, the replacement_history is the compressed conversation
321
+ for (const item of comp.payload.replacement_history) {
322
+ const text = responseItemToText(item);
323
+ if (text) {
324
+ messages.push({ text, timestamp: sessionTimestamp });
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ if (messages.length === 0) continue;
332
+
333
+ // Build chunks respecting maxTokens boundaries
334
+ let currentTexts: string[] = [];
335
+ let currentTokens = 0;
336
+ let chunkIndex = 0;
337
+
338
+ const flushChunk = () => {
339
+ if (currentTexts.length === 0) return;
340
+ chunkIndex++;
341
+ const text = currentTexts.join("\n\n");
342
+ chunks.push({
343
+ label: `Codex ${new Date(sessionTimestamp).toISOString().slice(0, 10)} (${chunkIndex})`,
344
+ text,
345
+ estimatedTokens: estimateTokens(text),
346
+ timestamp: sessionTimestamp,
347
+ });
348
+ currentTexts = [];
349
+ currentTokens = 0;
350
+ };
351
+
352
+ for (const msg of messages) {
353
+ const msgTokens = estimateTokens(msg.text);
354
+ if (currentTokens > 0 && currentTokens + msgTokens > maxTokens) {
355
+ flushChunk();
356
+ }
357
+ currentTexts.push(msg.text);
358
+ currentTokens += msgTokens;
359
+ }
360
+
361
+ flushChunk();
362
+ }
363
+
364
+ return chunks;
365
+ },
366
+ };
367
+
368
+ // Auto-register on import
369
+ registerProvider(codexProvider);
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Continue (VS Code/JetBrains extension) conversation history provider.
3
+ *
4
+ * Reads JSON session files from ~/.continue/sessions/<sessionId>.json
5
+ * with the sessions index at ~/.continue/sessions/sessions.json.
6
+ *
7
+ * Each session JSON contains:
8
+ * { sessionId, title, workspaceDirectory, history: ChatHistoryItem[] }
9
+ *
10
+ * The CONTINUE_GLOBAL_DIR env var overrides the default ~/.continue path.
11
+ */
12
+ import { readdirSync, readFileSync, existsSync } from "fs";
13
+ import { join } from "path";
14
+ import { homedir } from "os";
15
+ import type { AgentHistoryProvider, ConversationChunk, DetectedSession } from "../types";
16
+ import { registerProvider } from "./index";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Constants
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const MAX_TOOL_OUTPUT_CHARS = 500;
23
+ const DEFAULT_MAX_TOKENS = 12288;
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Types (Continue's format)
27
+ // ---------------------------------------------------------------------------
28
+
29
+ type SessionMetadata = {
30
+ sessionId: string;
31
+ title: string;
32
+ dateCreated: string;
33
+ workspaceDirectory?: string;
34
+ messageCount?: number;
35
+ };
36
+
37
+ type ChatMessage = {
38
+ role: "user" | "assistant" | "system" | "tool";
39
+ content: string | ContentPart[];
40
+ toolCalls?: ToolCall[];
41
+ };
42
+
43
+ type ContentPart =
44
+ | { type: "text"; text: string }
45
+ | { type: "imageUrl"; imageUrl?: { url: string } }
46
+ | { type: string; [key: string]: unknown };
47
+
48
+ type ToolCall = {
49
+ id: string;
50
+ type: "function";
51
+ function: { name: string; arguments: string };
52
+ };
53
+
54
+ type ChatHistoryItem = {
55
+ message: ChatMessage;
56
+ contextItems?: unknown[];
57
+ toolCallStates?: Array<{
58
+ toolCallId: string;
59
+ status: string;
60
+ output?: string;
61
+ }>;
62
+ };
63
+
64
+ type SessionFile = {
65
+ sessionId: string;
66
+ title: string;
67
+ workspaceDirectory?: string;
68
+ history: ChatHistoryItem[];
69
+ mode?: string;
70
+ };
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Helpers
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function estimateTokens(text: string): number {
77
+ return Math.ceil(text.length / 3);
78
+ }
79
+
80
+ function truncate(text: string, max: number): string {
81
+ if (text.length <= max) return text;
82
+ return text.slice(0, max) + "...";
83
+ }
84
+
85
+ /** Get the Continue global directory. */
86
+ function continueDir(): string {
87
+ return process.env.CONTINUE_GLOBAL_DIR || join(homedir(), ".continue");
88
+ }
89
+
90
+ /** Load the sessions index. */
91
+ function loadSessionIndex(): SessionMetadata[] {
92
+ const indexPath = join(continueDir(), "sessions", "sessions.json");
93
+ if (!existsSync(indexPath)) return [];
94
+
95
+ try {
96
+ const raw = readFileSync(indexPath, "utf-8");
97
+ const parsed = JSON.parse(raw);
98
+ return Array.isArray(parsed) ? parsed : [];
99
+ } catch {
100
+ return [];
101
+ }
102
+ }
103
+
104
+ /** Load a full session file. */
105
+ function loadSession(sessionId: string): SessionFile | null {
106
+ const filePath = join(continueDir(), "sessions", `${sessionId}.json`);
107
+ if (!existsSync(filePath)) return null;
108
+
109
+ try {
110
+ const raw = readFileSync(filePath, "utf-8");
111
+ return JSON.parse(raw) as SessionFile;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ /** Extract text from a chat message's content. */
118
+ function extractMessageContent(content: string | ContentPart[]): string {
119
+ if (typeof content === "string") return content;
120
+ if (!Array.isArray(content)) return "";
121
+
122
+ return content
123
+ .filter((part): part is { type: "text"; text: string } =>
124
+ part.type === "text" && typeof (part as { text?: string }).text === "string",
125
+ )
126
+ .map((part) => part.text)
127
+ .join("\n");
128
+ }
129
+
130
+ /** Convert a ChatHistoryItem to text. */
131
+ function historyItemToText(item: ChatHistoryItem): string | null {
132
+ const msg = item.message;
133
+ if (!msg) return null;
134
+
135
+ // Skip system messages
136
+ if (msg.role === "system") return null;
137
+
138
+ const parts: string[] = [];
139
+
140
+ // Message content
141
+ const content = extractMessageContent(msg.content);
142
+ if (content) parts.push(content);
143
+
144
+ // Tool calls (from assistant)
145
+ if (msg.toolCalls) {
146
+ for (const call of msg.toolCalls) {
147
+ if (call.function) {
148
+ const args = truncate(call.function.arguments || "{}", MAX_TOOL_OUTPUT_CHARS);
149
+ parts.push(`[tool: ${call.function.name}] ${args}`);
150
+ }
151
+ }
152
+ }
153
+
154
+ // Tool call results
155
+ if (item.toolCallStates) {
156
+ for (const state of item.toolCallStates) {
157
+ if (state.output && state.status === "done") {
158
+ parts.push(`[tool_result] ${truncate(state.output, MAX_TOOL_OUTPUT_CHARS)}`);
159
+ }
160
+ }
161
+ }
162
+
163
+ if (parts.length === 0) return null;
164
+
165
+ const role = msg.role === "tool" ? "tool_result" : msg.role;
166
+ return `[${role}] ${parts.join("\n")}`;
167
+ }
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Provider implementation
171
+ // ---------------------------------------------------------------------------
172
+
173
+ const continueProvider: AgentHistoryProvider = {
174
+ name: "continue",
175
+ displayName: "Continue",
176
+
177
+ detect(projectPath: string): DetectedSession[] {
178
+ const sessions: DetectedSession[] = [];
179
+ const index = loadSessionIndex();
180
+
181
+ for (const meta of index) {
182
+ // Filter by workspace directory
183
+ if (meta.workspaceDirectory !== projectPath) continue;
184
+
185
+ // Load the full session to count messages
186
+ const session = loadSession(meta.sessionId);
187
+ if (!session || !session.history || session.history.length < 3) continue;
188
+
189
+ const ts = new Date(meta.dateCreated).getTime();
190
+ const dateStr = new Date(ts).toISOString().slice(0, 10);
191
+ const messageCount = session.history.length;
192
+
193
+ const label = meta.title
194
+ ? `${dateStr} - ${truncate(meta.title, 60)} (${messageCount} messages)`
195
+ : `${dateStr} (${messageCount} messages)`;
196
+
197
+ // Estimate tokens
198
+ const estimatedTokens = messageCount * 500;
199
+
200
+ sessions.push({
201
+ id: meta.sessionId,
202
+ label,
203
+ startedAt: ts,
204
+ lastActivityAt: ts,
205
+ estimatedTokens,
206
+ messageCount,
207
+ });
208
+ }
209
+
210
+ // Also scan for session files not in the index (some versions don't maintain it)
211
+ const sessionsDir = join(continueDir(), "sessions");
212
+ if (existsSync(sessionsDir)) {
213
+ const existingIds = new Set(sessions.map((s) => s.id));
214
+ let entries: string[];
215
+ try {
216
+ entries = readdirSync(sessionsDir);
217
+ } catch {
218
+ entries = [];
219
+ }
220
+
221
+ for (const entry of entries) {
222
+ if (!entry.endsWith(".json") || entry === "sessions.json") continue;
223
+ const sessionId = entry.replace(".json", "");
224
+ if (existingIds.has(sessionId)) continue;
225
+
226
+ const session = loadSession(sessionId);
227
+ if (!session) continue;
228
+ if (session.workspaceDirectory !== projectPath) continue;
229
+ if (!session.history || session.history.length < 3) continue;
230
+
231
+ const dateStr = session.title ? truncate(session.title, 60) : sessionId.slice(0, 8);
232
+ sessions.push({
233
+ id: sessionId,
234
+ label: `${dateStr} (${session.history.length} messages)`,
235
+ startedAt: Date.now(),
236
+ lastActivityAt: Date.now(),
237
+ estimatedTokens: session.history.length * 500,
238
+ messageCount: session.history.length,
239
+ });
240
+ }
241
+ }
242
+
243
+ return sessions.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
244
+ },
245
+
246
+ readChunks(
247
+ _projectPath: string,
248
+ sessionIds: string[],
249
+ maxTokens: number = DEFAULT_MAX_TOKENS,
250
+ ): ConversationChunk[] {
251
+ const chunks: ConversationChunk[] = [];
252
+
253
+ for (const sessionId of sessionIds) {
254
+ const session = loadSession(sessionId);
255
+ if (!session || !session.history) continue;
256
+
257
+ const textMessages: { text: string }[] = [];
258
+ for (const item of session.history) {
259
+ const text = historyItemToText(item);
260
+ if (text) textMessages.push({ text });
261
+ }
262
+
263
+ if (textMessages.length === 0) continue;
264
+
265
+ // Session timestamp
266
+ const sessionTimestamp = Date.now();
267
+
268
+ // Build chunks respecting maxTokens boundaries
269
+ let currentTexts: string[] = [];
270
+ let currentTokens = 0;
271
+ let chunkIndex = 0;
272
+
273
+ const flushChunk = () => {
274
+ if (currentTexts.length === 0) return;
275
+ chunkIndex++;
276
+ const text = currentTexts.join("\n\n");
277
+ chunks.push({
278
+ label: `Continue ${session.title || sessionId.slice(0, 8)} (${chunkIndex})`,
279
+ text,
280
+ estimatedTokens: estimateTokens(text),
281
+ timestamp: sessionTimestamp,
282
+ });
283
+ currentTexts = [];
284
+ currentTokens = 0;
285
+ };
286
+
287
+ for (const msg of textMessages) {
288
+ const msgTokens = estimateTokens(msg.text);
289
+ if (currentTokens > 0 && currentTokens + msgTokens > maxTokens) {
290
+ flushChunk();
291
+ }
292
+ currentTexts.push(msg.text);
293
+ currentTokens += msgTokens;
294
+ }
295
+
296
+ flushChunk();
297
+ }
298
+
299
+ return chunks;
300
+ },
301
+ };
302
+
303
+ // Auto-register on import
304
+ registerProvider(continueProvider);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Provider registry — maintains a list of all known agent history providers.
3
+ *
4
+ * Providers register themselves at import time. The detection orchestrator
5
+ * iterates over all registered providers to scan for conversation history.
6
+ */
7
+ import type { AgentHistoryProvider } from "../types";
8
+
9
+ const providers: AgentHistoryProvider[] = [];
10
+
11
+ /** Register a provider. Called at module load time by each provider module. */
12
+ export function registerProvider(provider: AgentHistoryProvider): void {
13
+ providers.push(provider);
14
+ }
15
+
16
+ /** Get all registered providers. */
17
+ export function getProviders(): readonly AgentHistoryProvider[] {
18
+ return providers;
19
+ }
20
+
21
+ /** Get a provider by internal name. */
22
+ export function getProvider(name: string): AgentHistoryProvider | undefined {
23
+ return providers.find((p) => p.name === name);
24
+ }
25
+
26
+ /**
27
+ * Clear all registered providers.
28
+ * Test-only — allows resetting the registry between test runs.
29
+ */
30
+ export function clearProviders(): void {
31
+ providers.length = 0;
32
+ }