@mrpatronz/nexusflow 0.2.17 → 0.2.19

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 (320) hide show
  1. package/.agents/skills/ccc/SKILL.md +71 -0
  2. package/.agents/skills/ccc/references/management.md +110 -0
  3. package/.agents/skills/ccc/references/settings.md +126 -0
  4. package/.github/dependabot.yml +52 -52
  5. package/.github/workflows/ci.yml +44 -0
  6. package/.github/workflows/dependabot-merge.yml +53 -53
  7. package/.github/workflows/release-desktop.yml +67 -0
  8. package/.github/workflows/release-npm.yml +65 -0
  9. package/.github/workflows/release-vscode.yml +61 -0
  10. package/.vscode/launch.json +17 -17
  11. package/.vscode/tasks.json +17 -17
  12. package/GETTING_STARTED.md +114 -114
  13. package/README.md +364 -336
  14. package/desktop/build-installer.js +228 -228
  15. package/desktop/build.js +118 -118
  16. package/desktop/installer/nexusflow.iss +35 -35
  17. package/desktop/neutralino.config.json +38 -38
  18. package/desktop/package-lock.json +1060 -1060
  19. package/desktop/package.json +14 -14
  20. package/dist/commands/adapter.d.ts +27 -0
  21. package/dist/commands/adapter.d.ts.map +1 -0
  22. package/dist/commands/adapter.js +318 -0
  23. package/dist/commands/adapter.js.map +1 -0
  24. package/dist/commands/add-repo.d.ts.map +1 -1
  25. package/dist/commands/add-repo.js +0 -1
  26. package/dist/commands/add-repo.js.map +1 -1
  27. package/dist/commands/commands.test.js +2 -2
  28. package/dist/commands/commands.test.js.map +1 -1
  29. package/dist/commands/config.d.ts +22 -0
  30. package/dist/commands/config.d.ts.map +1 -0
  31. package/dist/commands/config.js +50 -0
  32. package/dist/commands/config.js.map +1 -0
  33. package/dist/commands/create.d.ts.map +1 -1
  34. package/dist/commands/create.js +14 -17
  35. package/dist/commands/create.js.map +1 -1
  36. package/dist/commands/doctor.d.ts.map +1 -1
  37. package/dist/commands/doctor.js +11 -0
  38. package/dist/commands/doctor.js.map +1 -1
  39. package/dist/commands/refresh.d.ts +1 -0
  40. package/dist/commands/refresh.d.ts.map +1 -1
  41. package/dist/commands/refresh.js +2 -14
  42. package/dist/commands/refresh.js.map +1 -1
  43. package/dist/commands/sync.d.ts.map +1 -1
  44. package/dist/commands/sync.js +36 -54
  45. package/dist/commands/sync.js.map +1 -1
  46. package/dist/commands/tui.d.ts.map +1 -1
  47. package/dist/commands/tui.js +63 -10
  48. package/dist/commands/tui.js.map +1 -1
  49. package/dist/core/adapters/local-storage.d.ts +14 -0
  50. package/dist/core/adapters/local-storage.d.ts.map +1 -0
  51. package/dist/core/adapters/local-storage.js +58 -0
  52. package/dist/core/adapters/local-storage.js.map +1 -0
  53. package/dist/core/adapters/obsidian-storage.d.ts +28 -0
  54. package/dist/core/adapters/obsidian-storage.d.ts.map +1 -0
  55. package/dist/core/adapters/obsidian-storage.js +135 -0
  56. package/dist/core/adapters/obsidian-storage.js.map +1 -0
  57. package/dist/core/adapters/registry.d.ts +8 -0
  58. package/dist/core/adapters/registry.d.ts.map +1 -0
  59. package/dist/core/adapters/registry.js +37 -0
  60. package/dist/core/adapters/registry.js.map +1 -0
  61. package/dist/core/adapters/storage.test.d.ts +2 -0
  62. package/dist/core/adapters/storage.test.d.ts.map +1 -0
  63. package/dist/core/adapters/storage.test.js +106 -0
  64. package/dist/core/adapters/storage.test.js.map +1 -0
  65. package/dist/core/adapters/vault-storage.d.ts +16 -0
  66. package/dist/core/adapters/vault-storage.d.ts.map +1 -0
  67. package/dist/core/adapters/vault-storage.js +77 -0
  68. package/dist/core/adapters/vault-storage.js.map +1 -0
  69. package/dist/core/config.d.ts.map +1 -1
  70. package/dist/core/config.js +28 -4
  71. package/dist/core/config.js.map +1 -1
  72. package/dist/core/plugins/index.d.ts +14 -0
  73. package/dist/core/plugins/index.d.ts.map +1 -0
  74. package/dist/core/plugins/index.js +2 -0
  75. package/dist/core/plugins/index.js.map +1 -0
  76. package/dist/core/plugins/loader.d.ts +2 -0
  77. package/dist/core/plugins/loader.d.ts.map +1 -0
  78. package/dist/core/plugins/loader.js +34 -0
  79. package/dist/core/plugins/loader.js.map +1 -0
  80. package/dist/core/ports/storage.d.ts +59 -0
  81. package/dist/core/ports/storage.d.ts.map +1 -0
  82. package/dist/core/ports/storage.js +10 -0
  83. package/dist/core/ports/storage.js.map +1 -0
  84. package/dist/core/storage.d.ts +10 -0
  85. package/dist/core/storage.d.ts.map +1 -0
  86. package/dist/core/storage.js +29 -0
  87. package/dist/core/storage.js.map +1 -0
  88. package/dist/core/sync.d.ts +44 -0
  89. package/dist/core/sync.d.ts.map +1 -0
  90. package/dist/core/sync.js +82 -0
  91. package/dist/core/sync.js.map +1 -0
  92. package/dist/core/workspace-state.d.ts +51 -0
  93. package/dist/core/workspace-state.d.ts.map +1 -0
  94. package/dist/core/workspace-state.js +108 -0
  95. package/dist/core/workspace-state.js.map +1 -0
  96. package/dist/core/workspace-state.test.d.ts +2 -0
  97. package/dist/core/workspace-state.test.d.ts.map +1 -0
  98. package/dist/core/workspace-state.test.js +84 -0
  99. package/dist/core/workspace-state.test.js.map +1 -0
  100. package/dist/core/workspace.d.ts.map +1 -1
  101. package/dist/core/workspace.js +110 -16
  102. package/dist/core/workspace.js.map +1 -1
  103. package/dist/generators/base.d.ts.map +1 -1
  104. package/dist/generators/base.js +80 -79
  105. package/dist/generators/base.js.map +1 -1
  106. package/dist/generators/codex.js +11 -11
  107. package/dist/generators/copilot.js +17 -17
  108. package/dist/generators/cursor.js +5 -5
  109. package/dist/generators/diff-context.d.ts +6 -0
  110. package/dist/generators/diff-context.d.ts.map +1 -0
  111. package/dist/generators/diff-context.js +68 -0
  112. package/dist/generators/diff-context.js.map +1 -0
  113. package/dist/generators/index.d.ts +1 -1
  114. package/dist/generators/index.d.ts.map +1 -1
  115. package/dist/generators/index.js +125 -68
  116. package/dist/generators/index.js.map +1 -1
  117. package/dist/generators/map-generator.d.ts.map +1 -1
  118. package/dist/generators/map-generator.js +21 -25
  119. package/dist/generators/map-generator.js.map +1 -1
  120. package/dist/generators/plan-generator.d.ts.map +1 -1
  121. package/dist/generators/plan-generator.js +3 -5
  122. package/dist/generators/plan-generator.js.map +1 -1
  123. package/dist/generators/skills-generator.js +28 -28
  124. package/dist/gui/assets/index-Bdbld6yY.css +1 -0
  125. package/dist/gui/assets/index-D8iJPvnk.js +26 -0
  126. package/dist/gui/icons.svg +24 -24
  127. package/dist/gui/index.html +18 -18
  128. package/dist/index.js +131 -20
  129. package/dist/index.js.map +1 -1
  130. package/dist/mcp/server.d.ts.map +1 -1
  131. package/dist/mcp/server.js +42 -0
  132. package/dist/mcp/server.js.map +1 -1
  133. package/dist/server.d.ts.map +1 -1
  134. package/dist/server.js +140 -56
  135. package/dist/server.js.map +1 -1
  136. package/dist/server.test.js +105 -48
  137. package/dist/server.test.js.map +1 -1
  138. package/dist/types.d.ts +38 -2
  139. package/dist/types.d.ts.map +1 -1
  140. package/dist/utils/local-ai.d.ts.map +1 -1
  141. package/dist/utils/local-ai.js +7 -3
  142. package/dist/utils/local-ai.js.map +1 -1
  143. package/dist/utils/multi-git.d.ts +33 -8
  144. package/dist/utils/multi-git.d.ts.map +1 -1
  145. package/dist/utils/multi-git.js +91 -19
  146. package/dist/utils/multi-git.js.map +1 -1
  147. package/dist/utils/multi-git.test.d.ts +2 -0
  148. package/dist/utils/multi-git.test.d.ts.map +1 -0
  149. package/dist/utils/multi-git.test.js +125 -0
  150. package/dist/utils/multi-git.test.js.map +1 -0
  151. package/dist/utils/update-check.d.ts.map +1 -1
  152. package/dist/utils/update-check.js +0 -28
  153. package/dist/utils/update-check.js.map +1 -1
  154. package/dist/utils/workflow-advisor.d.ts +16 -0
  155. package/dist/utils/workflow-advisor.d.ts.map +1 -0
  156. package/dist/utils/workflow-advisor.js +100 -0
  157. package/dist/utils/workflow-advisor.js.map +1 -0
  158. package/extension/package-lock.json +6044 -2673
  159. package/extension/package.json +117 -108
  160. package/extension/src/extension.ts +653 -631
  161. package/extension/tsconfig.json +21 -21
  162. package/gui/README.md +73 -73
  163. package/gui/e2e/dashboard.spec.ts +61 -0
  164. package/gui/e2e/wizard.spec.ts +346 -346
  165. package/gui/eslint.config.js +26 -26
  166. package/gui/index.html +17 -17
  167. package/gui/package-lock.json +3137 -3079
  168. package/gui/package.json +37 -36
  169. package/gui/playwright.config.ts +41 -41
  170. package/gui/public/icons.svg +24 -24
  171. package/gui/src/App.css +1 -1
  172. package/gui/src/App.tsx +3573 -3407
  173. package/gui/src/app/AppSidebar.tsx +69 -0
  174. package/gui/src/assets/vite.svg +1 -1
  175. package/gui/src/components/AddRepoPicker.tsx +81 -0
  176. package/gui/src/components/ui/Button.tsx +40 -0
  177. package/gui/src/components/ui/Card.tsx +6 -0
  178. package/gui/src/components/ui/EmptyState.tsx +22 -0
  179. package/gui/src/components/ui/Input.tsx +21 -0
  180. package/gui/src/components/ui/Menu.tsx +71 -0
  181. package/gui/src/components/ui/Modal.tsx +39 -0
  182. package/gui/src/components/ui/PageHeader.tsx +21 -0
  183. package/gui/src/components/ui/RepoStatusStrip.tsx +33 -0
  184. package/gui/src/components/ui/Skeleton.tsx +5 -0
  185. package/gui/src/components/ui/StatusPill.tsx +36 -0
  186. package/gui/src/components/ui/Tabs.tsx +40 -0
  187. package/gui/src/components/ui/cn.ts +4 -0
  188. package/gui/src/components/ui/index.ts +16 -0
  189. package/gui/src/features/changes/ChangesViewer.tsx +388 -388
  190. package/gui/src/features/knowledge/KnowledgeBase.tsx +84 -84
  191. package/gui/src/features/onboarding/OnboardingWizard.tsx +230 -230
  192. package/gui/src/features/plan/ImplementationPlan.tsx +32 -32
  193. package/gui/src/features/services/ServiceConsole.tsx +215 -250
  194. package/gui/src/features/sessions/SessionHistory.tsx +195 -195
  195. package/gui/src/index.css +167 -152
  196. package/gui/src/lib/status.ts +24 -0
  197. package/gui/src/main.tsx +94 -94
  198. package/gui/src/pages/DashboardPage.tsx +129 -0
  199. package/gui/src/pages/WorkspacesPage.tsx +352 -0
  200. package/gui/src/types.ts +81 -62
  201. package/gui/tsconfig.app.json +25 -25
  202. package/gui/tsconfig.json +7 -7
  203. package/gui/tsconfig.node.json +24 -24
  204. package/gui/vite.config.ts +12 -12
  205. package/package.json +55 -56
  206. package/resources/workflows/plan-implement-review.md +8 -8
  207. package/resources/workflows/research-verify.md +6 -6
  208. package/resources/workflows/solo-developer.md +3 -3
  209. package/scripts/simulate-workspaces.ts +55 -55
  210. package/skills-lock.json +11 -0
  211. package/src/analyzers/detect-apis.ts +290 -290
  212. package/src/analyzers/detect-deps.ts +315 -315
  213. package/src/analyzers/detect-existing.ts +74 -74
  214. package/src/analyzers/detect-ports.ts +110 -110
  215. package/src/analyzers/index.ts +97 -97
  216. package/src/analyzers/messaging-analyzer.ts +254 -254
  217. package/src/analyzers/readme-summarizer.ts +102 -102
  218. package/src/analyzers/run-analyzer.ts +269 -269
  219. package/src/analyzers/tech-stack.ts +283 -283
  220. package/src/commands/adapter.ts +348 -0
  221. package/src/commands/add-repo.ts +156 -157
  222. package/src/commands/commands.test.ts +156 -155
  223. package/src/commands/commit.ts +131 -131
  224. package/src/commands/config.ts +52 -0
  225. package/src/commands/create.ts +204 -207
  226. package/src/commands/desktop.ts +128 -128
  227. package/src/commands/diff.ts +125 -125
  228. package/src/commands/doctor.ts +370 -357
  229. package/src/commands/handoff.ts +266 -266
  230. package/src/commands/init.ts +134 -134
  231. package/src/commands/list.ts +46 -46
  232. package/src/commands/logs.ts +63 -63
  233. package/src/commands/mcp.ts +94 -92
  234. package/src/commands/open.ts +129 -129
  235. package/src/commands/refresh.ts +137 -147
  236. package/src/commands/remove.ts +98 -98
  237. package/src/commands/start.ts +117 -117
  238. package/src/commands/status.ts +54 -54
  239. package/src/commands/stop.ts +55 -55
  240. package/src/commands/sync.ts +125 -151
  241. package/src/commands/tui.ts +484 -424
  242. package/src/commands/ui.ts +111 -111
  243. package/src/core/adapters/local-storage.ts +66 -0
  244. package/src/core/adapters/obsidian-storage.ts +151 -0
  245. package/src/core/adapters/registry.ts +44 -0
  246. package/src/core/adapters/storage.test.ts +174 -0
  247. package/src/core/adapters/vault-storage.ts +86 -0
  248. package/src/core/config.test.ts +96 -96
  249. package/src/core/config.ts +140 -115
  250. package/src/core/graph.ts +344 -344
  251. package/src/core/plugins/index.ts +17 -0
  252. package/src/core/plugins/loader.ts +36 -0
  253. package/src/core/ports/storage.ts +71 -0
  254. package/src/core/scanner.test.ts +61 -61
  255. package/src/core/scanner.ts +91 -91
  256. package/src/core/storage.ts +37 -0
  257. package/src/core/sync.ts +121 -0
  258. package/src/core/workspace-state.test.ts +108 -0
  259. package/src/core/workspace-state.ts +130 -0
  260. package/src/core/workspace.ts +487 -380
  261. package/src/core/worktree.ts +120 -120
  262. package/src/generators/antigravity.ts +32 -32
  263. package/src/generators/base.ts +243 -240
  264. package/src/generators/claude.ts +34 -34
  265. package/src/generators/codex.ts +48 -48
  266. package/src/generators/copilot.ts +56 -56
  267. package/src/generators/cursor.ts +44 -44
  268. package/src/generators/diff-context.ts +75 -0
  269. package/src/generators/index.ts +322 -250
  270. package/src/generators/map-generator.test.ts +159 -159
  271. package/src/generators/map-generator.ts +539 -542
  272. package/src/generators/plan-generator.ts +483 -481
  273. package/src/generators/skills-generator.ts +259 -259
  274. package/src/index.ts +539 -426
  275. package/src/mcp/server.ts +338 -294
  276. package/src/orchestration/detect.ts +313 -313
  277. package/src/orchestration/index.ts +7 -7
  278. package/src/orchestration/runner.ts +283 -283
  279. package/src/server.test.ts +618 -555
  280. package/src/server.ts +1402 -1308
  281. package/src/types.ts +451 -408
  282. package/src/utils/detect-ai.test.ts +44 -44
  283. package/src/utils/detect-ai.ts +84 -84
  284. package/src/utils/detect-editors.test.ts +48 -48
  285. package/src/utils/detect-editors.ts +57 -57
  286. package/src/utils/git.test.ts +74 -74
  287. package/src/utils/git.ts +117 -117
  288. package/src/utils/local-ai.test.ts +130 -130
  289. package/src/utils/local-ai.ts +116 -111
  290. package/src/utils/multi-git.test.ts +158 -0
  291. package/src/utils/multi-git.ts +405 -313
  292. package/src/utils/prompts.ts +209 -209
  293. package/src/utils/session-finder.ts +483 -483
  294. package/src/utils/system-scanner.test.ts +89 -89
  295. package/src/utils/system-scanner.ts +96 -96
  296. package/src/utils/update-check.ts +286 -309
  297. package/src/utils/workflow-advisor.ts +118 -0
  298. package/src/utils/workflows.test.ts +114 -114
  299. package/src/utils/workflows.ts +178 -178
  300. package/tsconfig.json +19 -19
  301. package/vitest.config.ts +14 -14
  302. package/.github/workflows/release.yml +0 -112
  303. package/dist/commands/pack.d.ts +0 -7
  304. package/dist/commands/pack.d.ts.map +0 -1
  305. package/dist/commands/pack.js +0 -64
  306. package/dist/commands/pack.js.map +0 -1
  307. package/dist/core/packer.d.ts +0 -14
  308. package/dist/core/packer.d.ts.map +0 -1
  309. package/dist/core/packer.js +0 -87
  310. package/dist/core/packer.js.map +0 -1
  311. package/dist/core/packer.test.d.ts +0 -2
  312. package/dist/core/packer.test.d.ts.map +0 -1
  313. package/dist/core/packer.test.js +0 -82
  314. package/dist/core/packer.test.js.map +0 -1
  315. package/dist/gui/assets/index-Ct3qq-X4.js +0 -25
  316. package/dist/gui/assets/index-P_ZrgHXg.css +0 -1
  317. package/gui/src/features/workspace/WorkspaceList.tsx +0 -666
  318. package/src/commands/pack.ts +0 -73
  319. package/src/core/packer.test.ts +0 -99
  320. package/src/core/packer.ts +0 -107
@@ -1,483 +1,483 @@
1
- /**
2
- * @module utils/session-finder
3
- * Scans local storage directories of various AI coding assistants to find conversation histories.
4
- */
5
-
6
- import * as fs from 'node:fs/promises';
7
- import * as path from 'node:path';
8
- import * as os from 'node:os';
9
- import type { AISession, ChatMessage } from '../types.js';
10
-
11
- /**
12
- * Derives the directory name that Claude Code uses inside `~/.claude/projects/`
13
- * for a given absolute project directory path.
14
- * It replaces special characters /, \, space, -, and _ with a hyphen.
15
- */
16
- export function getClaudeProjectFolderName(absolutePath: string): string {
17
- return absolutePath.replace(/[\\\/ _-]+/g, '-');
18
- }
19
-
20
- /**
21
- * Recursively retrieves all files in a directory that match a specific extension.
22
- */
23
- async function getFilesRecursively(dir: string, extension: string): Promise<string[]> {
24
- const result: string[] = [];
25
- try {
26
- const entries = await fs.readdir(dir, { withFileTypes: true });
27
- for (const entry of entries) {
28
- const fullPath = path.join(dir, entry.name);
29
- if (entry.isDirectory()) {
30
- const subFiles = await getFilesRecursively(fullPath, extension);
31
- result.push(...subFiles);
32
- } else if (entry.isFile() && entry.name.endsWith(extension)) {
33
- result.push(fullPath);
34
- }
35
- }
36
- } catch {}
37
- return result;
38
- }
39
-
40
- /**
41
- * Scans the local filesystem for conversation histories belonging to Claude, Antigravity,
42
- * Codex, and Copilot that relate to the specified workspace.
43
- *
44
- * @param workspacePath - Root directory of the active workspace.
45
- * @param repoPaths - Directories of sub-repositories included in the workspace.
46
- * @returns A promise that resolves to an array of {@link AISession} objects sorted by update time descending.
47
- */
48
- export async function findSessions(workspacePath: string, repoPaths: string[] = []): Promise<AISession[]> {
49
- const sessions: AISession[] = [];
50
- const wsFolderName = path.basename(workspacePath);
51
-
52
- // Normalization helper for accurate path comparison
53
- const normalizePath = (p: string) => path.normalize(p).toLowerCase();
54
- const normWorkspace = normalizePath(workspacePath);
55
- const normRepos = repoPaths.map(r => normalizePath(r));
56
-
57
- const isPathMatch = (sessPath?: string) => {
58
- if (!sessPath) return false;
59
- const normSess = normalizePath(sessPath);
60
- if (normSess === normWorkspace || normSess.startsWith(normWorkspace + path.sep)) return true;
61
- for (const r of normRepos) {
62
- if (normSess === r || normSess.startsWith(r + path.sep)) return true;
63
- }
64
- return false;
65
- };
66
-
67
- // ─── 1. Scan Antigravity Sessions ──────────────────────────────────────
68
- const agDir = path.join(os.homedir(), '.gemini', 'antigravity-cli');
69
- const agHistoryPath = path.join(agDir, 'history.jsonl');
70
-
71
- try {
72
- const historyContent = await fs.readFile(agHistoryPath, 'utf-8');
73
- const lines = historyContent.split('\n').filter(Boolean);
74
- const conversationsMap = new Map<string, { id: string; title: string; entries: any[] }>();
75
-
76
- for (const line of lines) {
77
- try {
78
- const entry = JSON.parse(line);
79
- if (entry.conversationId && isPathMatch(entry.workspace)) {
80
- if (!conversationsMap.has(entry.conversationId)) {
81
- conversationsMap.set(entry.conversationId, {
82
- id: entry.conversationId,
83
- title: entry.display || 'Untitled Conversation',
84
- entries: [],
85
- });
86
- }
87
- conversationsMap.get(entry.conversationId)!.entries.push(entry);
88
- }
89
- } catch {}
90
- }
91
-
92
- for (const [convId, convData] of conversationsMap.entries()) {
93
- const transcriptPath = path.join(agDir, 'brain', convId, '.system_generated', 'logs', 'transcript.jsonl');
94
- let messageCount = 0;
95
- let createdAt = new Date(convData.entries[0]?.timestamp || Date.now()).toISOString();
96
- let updatedAt = new Date(convData.entries[convData.entries.length - 1]?.timestamp || Date.now()).toISOString();
97
- let title = convData.title;
98
-
99
- try {
100
- const transcriptContent = await fs.readFile(transcriptPath, 'utf-8');
101
- const transcriptLines = transcriptContent.split('\n').filter(Boolean);
102
- let firstUserMessage = null;
103
-
104
- for (const line of transcriptLines) {
105
- const tObj = JSON.parse(line);
106
- if (tObj.type === 'USER_INPUT' || tObj.type === 'PLANNER_RESPONSE') {
107
- messageCount++;
108
- if (tObj.type === 'USER_INPUT' && !firstUserMessage) {
109
- firstUserMessage = tObj.content;
110
- }
111
- }
112
- }
113
-
114
- if (firstUserMessage) {
115
- const cleanMatch = firstUserMessage.match(/<USER_REQUEST>([\s\S]*?)<\/USER_REQUEST>/);
116
- title = cleanMatch ? cleanMatch[1].trim() : firstUserMessage.trim();
117
- }
118
- } catch {
119
- messageCount = convData.entries.length * 2;
120
- }
121
-
122
- if (title.length > 80) {
123
- title = title.substring(0, 80) + '...';
124
- }
125
-
126
- sessions.push({
127
- id: convId,
128
- assistant: 'antigravity',
129
- title,
130
- createdAt,
131
- updatedAt,
132
- messageCount,
133
- workspacePath,
134
- });
135
- }
136
- } catch {}
137
-
138
- // ─── 2. Scan Claude Code Sessions ──────────────────────────────────────
139
- const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
140
- const claudeProjectsDir = path.join(claudeConfigDir, 'projects');
141
-
142
- try {
143
- const candidateFolders = [
144
- getClaudeProjectFolderName(workspacePath),
145
- ...repoPaths.map(r => getClaudeProjectFolderName(r))
146
- ];
147
-
148
- for (const folder of candidateFolders) {
149
- const projectPath = path.join(claudeProjectsDir, folder);
150
- try {
151
- const files = await fs.readdir(projectPath);
152
- const jsonlFiles = files.filter(f => f.endsWith('.jsonl'));
153
-
154
- for (const file of jsonlFiles) {
155
- const filePath = path.join(projectPath, file);
156
- const sessionId = path.basename(file, '.jsonl');
157
-
158
- try {
159
- const content = await fs.readFile(filePath, 'utf-8');
160
- const lines = content.split('\n').filter(Boolean);
161
-
162
- let messageCount = 0;
163
- let createdAt = null;
164
- let updatedAt = null;
165
- let title = 'Claude Session';
166
-
167
- for (const line of lines) {
168
- const record = JSON.parse(line);
169
- if (record.timestamp) {
170
- if (!createdAt) createdAt = record.timestamp;
171
- updatedAt = record.timestamp;
172
- }
173
-
174
- if (record.type === 'user' || record.type === 'assistant') {
175
- messageCount++;
176
- if (record.type === 'user' && title === 'Claude Session') {
177
- const rawText = record.message?.text || record.message?.content || '';
178
- if (typeof rawText === 'string') {
179
- title = rawText.trim();
180
- } else if (Array.isArray(rawText)) {
181
- title = rawText.map((b: any) => b.text || b.content || '').join(' ').trim();
182
- }
183
- }
184
- }
185
- }
186
-
187
- if (title.length > 80) {
188
- title = title.substring(0, 80) + '...';
189
- }
190
-
191
- sessions.push({
192
- id: sessionId,
193
- assistant: 'claude',
194
- title,
195
- createdAt: createdAt || new Date().toISOString(),
196
- updatedAt: updatedAt || new Date().toISOString(),
197
- messageCount,
198
- workspacePath,
199
- });
200
- } catch {}
201
- }
202
- } catch {}
203
- }
204
- } catch {}
205
-
206
- // ─── 3. Scan OpenAI Codex Sessions ──────────────────────────────────────
207
- const codexDir = path.join(os.homedir(), '.codex');
208
- const codexSessionsDir = path.join(codexDir, 'sessions');
209
- try {
210
- const codexFiles = await getFilesRecursively(codexSessionsDir, '.jsonl');
211
- for (const file of codexFiles) {
212
- try {
213
- const content = await fs.readFile(file, 'utf-8');
214
- // Heuristic: check if the file references our workspace path or folder name
215
- if (!content.toLowerCase().includes(normWorkspace) && !content.includes(wsFolderName)) {
216
- continue;
217
- }
218
-
219
- const lines = content.split('\n').filter(Boolean);
220
- let messageCount = 0;
221
- let createdAt: string | null = null;
222
- let updatedAt: string | null = null;
223
- let title = 'Codex Session';
224
-
225
- for (const line of lines) {
226
- try {
227
- const record = JSON.parse(line);
228
- if (record.timestamp) {
229
- if (!createdAt) createdAt = new Date(record.timestamp).toISOString();
230
- updatedAt = new Date(record.timestamp).toISOString();
231
- }
232
- const role = record.role || record.message?.role || (record.prompt ? 'user' : record.completion ? 'assistant' : null);
233
- if (role === 'user' || role === 'assistant') {
234
- messageCount++;
235
- if (role === 'user' && title === 'Codex Session') {
236
- const text = record.content || record.prompt || record.message?.content || '';
237
- if (text) title = text.trim();
238
- }
239
- }
240
- } catch {}
241
- }
242
-
243
- const sessionId = path.basename(file, '.jsonl');
244
- sessions.push({
245
- id: sessionId,
246
- assistant: 'codex',
247
- title: title.length > 80 ? title.substring(0, 80) + '...' : title,
248
- createdAt: createdAt || new Date().toISOString(),
249
- updatedAt: updatedAt || new Date().toISOString(),
250
- messageCount,
251
- workspacePath,
252
- });
253
- } catch {}
254
- }
255
- } catch {}
256
-
257
- // ─── 4. Scan GitHub Copilot Sessions ────────────────────────────────────
258
- const copilotDir = path.join(os.homedir(), '.copilot');
259
- const copilotSessionsDirs = [
260
- path.join(copilotDir, 'session-state'),
261
- path.join(copilotDir, 'history-session-state')
262
- ];
263
- for (const dir of copilotSessionsDirs) {
264
- try {
265
- const copilotFiles = await getFilesRecursively(dir, '.json');
266
- for (const file of copilotFiles) {
267
- try {
268
- const content = await fs.readFile(file, 'utf-8');
269
- // Heuristic check
270
- if (!content.toLowerCase().includes(normWorkspace) && !content.includes(wsFolderName)) {
271
- continue;
272
- }
273
-
274
- const record = JSON.parse(content);
275
- let messageCount = 0;
276
- let title = 'Copilot Session';
277
- let messagesList: any[] = [];
278
-
279
- if (Array.isArray(record)) {
280
- messagesList = record;
281
- } else if (record && Array.isArray(record.messages)) {
282
- messagesList = record.messages;
283
- }
284
-
285
- for (const msg of messagesList) {
286
- const role = msg.role || (msg.type === 'user' ? 'user' : msg.type === 'assistant' ? 'assistant' : null);
287
- if (role === 'user' || role === 'assistant') {
288
- messageCount++;
289
- if (role === 'user' && title === 'Copilot Session') {
290
- const text = msg.content || msg.text || '';
291
- if (text) title = text.trim();
292
- }
293
- }
294
- }
295
-
296
- const stats = await fs.stat(file);
297
- const sessionId = path.basename(file, '.json');
298
-
299
- sessions.push({
300
- id: sessionId,
301
- assistant: 'copilot',
302
- title: title.length > 80 ? title.substring(0, 80) + '...' : title,
303
- createdAt: stats.birthtime.toISOString(),
304
- updatedAt: stats.mtime.toISOString(),
305
- messageCount,
306
- workspacePath,
307
- });
308
- } catch {}
309
- }
310
- } catch {}
311
- }
312
-
313
- // Sort by updatedAt descending
314
- sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
315
- return sessions;
316
- }
317
-
318
- /**
319
- * Reads and parses the full transcript for a specific assistant conversation session.
320
- *
321
- * @param assistant - The name of the AI assistant ('antigravity', 'claude', 'codex', 'copilot').
322
- * @param sessionId - Unique ID of the conversation session.
323
- * @returns A promise that resolves to an array of {@link ChatMessage} objects.
324
- */
325
- export async function getSessionTranscript(assistant: string, sessionId: string): Promise<ChatMessage[]> {
326
- const messages: ChatMessage[] = [];
327
-
328
- if (assistant === 'antigravity') {
329
- const agDir = path.join(os.homedir(), '.gemini', 'antigravity-cli');
330
- const transcriptPath = path.join(agDir, 'brain', sessionId, '.system_generated', 'logs', 'transcript.jsonl');
331
- const content = await fs.readFile(transcriptPath, 'utf-8');
332
- const lines = content.split('\n').filter(Boolean);
333
-
334
- for (const line of lines) {
335
- const obj = JSON.parse(line);
336
- if (obj.type === 'USER_INPUT') {
337
- const rawContent = obj.content || '';
338
- const cleanMatch = rawContent.match(/<USER_REQUEST>([\s\S]*?)<\/USER_REQUEST>/);
339
- const cleanContent = cleanMatch ? cleanMatch[1].trim() : rawContent.trim();
340
- messages.push({
341
- role: 'user',
342
- content: cleanContent,
343
- timestamp: obj.created_at || obj.timestamp,
344
- });
345
- } else if (obj.type === 'PLANNER_RESPONSE') {
346
- messages.push({
347
- role: 'assistant',
348
- content: (obj.content || '').trim(),
349
- timestamp: obj.created_at || obj.timestamp,
350
- });
351
- }
352
- }
353
- } else if (assistant === 'claude') {
354
- const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
355
- const claudeProjectsDir = path.join(claudeConfigDir, 'projects');
356
- const projectFolders = await fs.readdir(claudeProjectsDir).catch(() => [] as string[]);
357
- let transcriptPath: string | null = null;
358
-
359
- for (const folder of projectFolders) {
360
- const p = path.join(claudeProjectsDir, folder, `${sessionId}.jsonl`);
361
- try {
362
- await fs.access(p);
363
- transcriptPath = p;
364
- break;
365
- } catch {}
366
- }
367
-
368
- if (!transcriptPath) {
369
- throw new Error(`Claude session ${sessionId} not found`);
370
- }
371
-
372
- const content = await fs.readFile(transcriptPath, 'utf-8');
373
- const lines = content.split('\n').filter(Boolean);
374
-
375
- for (const line of lines) {
376
- const record = JSON.parse(line);
377
- if (record.type === 'user' || record.role === 'user') {
378
- const rawText = record.message?.text || record.message?.content || record.text || '';
379
- let cleanText = '';
380
- if (typeof rawText === 'string') {
381
- cleanText = rawText.trim();
382
- } else if (Array.isArray(rawText)) {
383
- cleanText = rawText.map((b: any) => b.text || b.content || '').join(' ').trim();
384
- }
385
- messages.push({
386
- role: 'user',
387
- content: cleanText,
388
- timestamp: record.timestamp || record.created_at,
389
- });
390
- } else if (record.type === 'assistant' || record.role === 'assistant') {
391
- const rawText = record.message?.text || record.message?.content || record.text || '';
392
- let cleanText = '';
393
- if (typeof rawText === 'string') {
394
- cleanText = rawText.trim();
395
- } else if (Array.isArray(rawText)) {
396
- cleanText = rawText.map((b: any) => b.text || b.content || '').join(' ').trim();
397
- }
398
- messages.push({
399
- role: 'assistant',
400
- content: cleanText,
401
- timestamp: record.timestamp || record.created_at,
402
- });
403
- }
404
- }
405
- } else if (assistant === 'codex') {
406
- const codexDir = path.join(os.homedir(), '.codex');
407
- const codexSessionsDir = path.join(codexDir, 'sessions');
408
- const codexFiles = await getFilesRecursively(codexSessionsDir, '.jsonl');
409
- let transcriptPath: string | null = null;
410
-
411
- for (const file of codexFiles) {
412
- if (path.basename(file, '.jsonl') === sessionId) {
413
- transcriptPath = file;
414
- break;
415
- }
416
- }
417
-
418
- if (!transcriptPath) {
419
- throw new Error(`Codex session ${sessionId} not found`);
420
- }
421
-
422
- const content = await fs.readFile(transcriptPath, 'utf-8');
423
- const lines = content.split('\n').filter(Boolean);
424
-
425
- for (const line of lines) {
426
- const record = JSON.parse(line);
427
- const role = record.role || record.message?.role || (record.prompt ? 'user' : record.completion ? 'assistant' : null);
428
- if (role === 'user' || role === 'assistant') {
429
- const text = record.content || record.prompt || record.completion || record.message?.content || '';
430
- messages.push({
431
- role: role as 'user' | 'assistant',
432
- content: text.trim(),
433
- timestamp: record.timestamp ? new Date(record.timestamp).toISOString() : undefined,
434
- });
435
- }
436
- }
437
- } else if (assistant === 'copilot') {
438
- const copilotDir = path.join(os.homedir(), '.copilot');
439
- const copilotSessionsDirs = [
440
- path.join(copilotDir, 'session-state'),
441
- path.join(copilotDir, 'history-session-state')
442
- ];
443
- let transcriptPath: string | null = null;
444
-
445
- for (const dir of copilotSessionsDirs) {
446
- const files = await getFilesRecursively(dir, '.json');
447
- for (const file of files) {
448
- if (path.basename(file, '.json') === sessionId) {
449
- transcriptPath = file;
450
- break;
451
- }
452
- }
453
- if (transcriptPath) break;
454
- }
455
-
456
- if (!transcriptPath) {
457
- throw new Error(`Copilot session ${sessionId} not found`);
458
- }
459
-
460
- const content = await fs.readFile(transcriptPath, 'utf-8');
461
- const record = JSON.parse(content);
462
- let messagesList: any[] = [];
463
-
464
- if (Array.isArray(record)) {
465
- messagesList = record;
466
- } else if (record && Array.isArray(record.messages)) {
467
- messagesList = record.messages;
468
- }
469
-
470
- for (const msg of messagesList) {
471
- const role = msg.role || (msg.type === 'user' ? 'user' : msg.type === 'assistant' ? 'assistant' : null);
472
- if (role === 'user' || role === 'assistant') {
473
- messages.push({
474
- role: role as 'user' | 'assistant',
475
- content: (msg.content || msg.text || '').trim(),
476
- timestamp: msg.timestamp || msg.created_at,
477
- });
478
- }
479
- }
480
- }
481
-
482
- return messages;
483
- }
1
+ /**
2
+ * @module utils/session-finder
3
+ * Scans local storage directories of various AI coding assistants to find conversation histories.
4
+ */
5
+
6
+ import * as fs from 'node:fs/promises';
7
+ import * as path from 'node:path';
8
+ import * as os from 'node:os';
9
+ import type { AISession, ChatMessage } from '../types.js';
10
+
11
+ /**
12
+ * Derives the directory name that Claude Code uses inside `~/.claude/projects/`
13
+ * for a given absolute project directory path.
14
+ * It replaces special characters /, \, space, -, and _ with a hyphen.
15
+ */
16
+ export function getClaudeProjectFolderName(absolutePath: string): string {
17
+ return absolutePath.replace(/[\\\/ _-]+/g, '-');
18
+ }
19
+
20
+ /**
21
+ * Recursively retrieves all files in a directory that match a specific extension.
22
+ */
23
+ async function getFilesRecursively(dir: string, extension: string): Promise<string[]> {
24
+ const result: string[] = [];
25
+ try {
26
+ const entries = await fs.readdir(dir, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ const fullPath = path.join(dir, entry.name);
29
+ if (entry.isDirectory()) {
30
+ const subFiles = await getFilesRecursively(fullPath, extension);
31
+ result.push(...subFiles);
32
+ } else if (entry.isFile() && entry.name.endsWith(extension)) {
33
+ result.push(fullPath);
34
+ }
35
+ }
36
+ } catch {}
37
+ return result;
38
+ }
39
+
40
+ /**
41
+ * Scans the local filesystem for conversation histories belonging to Claude, Antigravity,
42
+ * Codex, and Copilot that relate to the specified workspace.
43
+ *
44
+ * @param workspacePath - Root directory of the active workspace.
45
+ * @param repoPaths - Directories of sub-repositories included in the workspace.
46
+ * @returns A promise that resolves to an array of {@link AISession} objects sorted by update time descending.
47
+ */
48
+ export async function findSessions(workspacePath: string, repoPaths: string[] = []): Promise<AISession[]> {
49
+ const sessions: AISession[] = [];
50
+ const wsFolderName = path.basename(workspacePath);
51
+
52
+ // Normalization helper for accurate path comparison
53
+ const normalizePath = (p: string) => path.normalize(p).toLowerCase();
54
+ const normWorkspace = normalizePath(workspacePath);
55
+ const normRepos = repoPaths.map(r => normalizePath(r));
56
+
57
+ const isPathMatch = (sessPath?: string) => {
58
+ if (!sessPath) return false;
59
+ const normSess = normalizePath(sessPath);
60
+ if (normSess === normWorkspace || normSess.startsWith(normWorkspace + path.sep)) return true;
61
+ for (const r of normRepos) {
62
+ if (normSess === r || normSess.startsWith(r + path.sep)) return true;
63
+ }
64
+ return false;
65
+ };
66
+
67
+ // ─── 1. Scan Antigravity Sessions ──────────────────────────────────────
68
+ const agDir = path.join(os.homedir(), '.gemini', 'antigravity-cli');
69
+ const agHistoryPath = path.join(agDir, 'history.jsonl');
70
+
71
+ try {
72
+ const historyContent = await fs.readFile(agHistoryPath, 'utf-8');
73
+ const lines = historyContent.split('\n').filter(Boolean);
74
+ const conversationsMap = new Map<string, { id: string; title: string; entries: any[] }>();
75
+
76
+ for (const line of lines) {
77
+ try {
78
+ const entry = JSON.parse(line);
79
+ if (entry.conversationId && isPathMatch(entry.workspace)) {
80
+ if (!conversationsMap.has(entry.conversationId)) {
81
+ conversationsMap.set(entry.conversationId, {
82
+ id: entry.conversationId,
83
+ title: entry.display || 'Untitled Conversation',
84
+ entries: [],
85
+ });
86
+ }
87
+ conversationsMap.get(entry.conversationId)!.entries.push(entry);
88
+ }
89
+ } catch {}
90
+ }
91
+
92
+ for (const [convId, convData] of conversationsMap.entries()) {
93
+ const transcriptPath = path.join(agDir, 'brain', convId, '.system_generated', 'logs', 'transcript.jsonl');
94
+ let messageCount = 0;
95
+ let createdAt = new Date(convData.entries[0]?.timestamp || Date.now()).toISOString();
96
+ let updatedAt = new Date(convData.entries[convData.entries.length - 1]?.timestamp || Date.now()).toISOString();
97
+ let title = convData.title;
98
+
99
+ try {
100
+ const transcriptContent = await fs.readFile(transcriptPath, 'utf-8');
101
+ const transcriptLines = transcriptContent.split('\n').filter(Boolean);
102
+ let firstUserMessage = null;
103
+
104
+ for (const line of transcriptLines) {
105
+ const tObj = JSON.parse(line);
106
+ if (tObj.type === 'USER_INPUT' || tObj.type === 'PLANNER_RESPONSE') {
107
+ messageCount++;
108
+ if (tObj.type === 'USER_INPUT' && !firstUserMessage) {
109
+ firstUserMessage = tObj.content;
110
+ }
111
+ }
112
+ }
113
+
114
+ if (firstUserMessage) {
115
+ const cleanMatch = firstUserMessage.match(/<USER_REQUEST>([\s\S]*?)<\/USER_REQUEST>/);
116
+ title = cleanMatch ? cleanMatch[1].trim() : firstUserMessage.trim();
117
+ }
118
+ } catch {
119
+ messageCount = convData.entries.length * 2;
120
+ }
121
+
122
+ if (title.length > 80) {
123
+ title = title.substring(0, 80) + '...';
124
+ }
125
+
126
+ sessions.push({
127
+ id: convId,
128
+ assistant: 'antigravity',
129
+ title,
130
+ createdAt,
131
+ updatedAt,
132
+ messageCount,
133
+ workspacePath,
134
+ });
135
+ }
136
+ } catch {}
137
+
138
+ // ─── 2. Scan Claude Code Sessions ──────────────────────────────────────
139
+ const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
140
+ const claudeProjectsDir = path.join(claudeConfigDir, 'projects');
141
+
142
+ try {
143
+ const candidateFolders = [
144
+ getClaudeProjectFolderName(workspacePath),
145
+ ...repoPaths.map(r => getClaudeProjectFolderName(r))
146
+ ];
147
+
148
+ for (const folder of candidateFolders) {
149
+ const projectPath = path.join(claudeProjectsDir, folder);
150
+ try {
151
+ const files = await fs.readdir(projectPath);
152
+ const jsonlFiles = files.filter(f => f.endsWith('.jsonl'));
153
+
154
+ for (const file of jsonlFiles) {
155
+ const filePath = path.join(projectPath, file);
156
+ const sessionId = path.basename(file, '.jsonl');
157
+
158
+ try {
159
+ const content = await fs.readFile(filePath, 'utf-8');
160
+ const lines = content.split('\n').filter(Boolean);
161
+
162
+ let messageCount = 0;
163
+ let createdAt = null;
164
+ let updatedAt = null;
165
+ let title = 'Claude Session';
166
+
167
+ for (const line of lines) {
168
+ const record = JSON.parse(line);
169
+ if (record.timestamp) {
170
+ if (!createdAt) createdAt = record.timestamp;
171
+ updatedAt = record.timestamp;
172
+ }
173
+
174
+ if (record.type === 'user' || record.type === 'assistant') {
175
+ messageCount++;
176
+ if (record.type === 'user' && title === 'Claude Session') {
177
+ const rawText = record.message?.text || record.message?.content || '';
178
+ if (typeof rawText === 'string') {
179
+ title = rawText.trim();
180
+ } else if (Array.isArray(rawText)) {
181
+ title = rawText.map((b: any) => b.text || b.content || '').join(' ').trim();
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ if (title.length > 80) {
188
+ title = title.substring(0, 80) + '...';
189
+ }
190
+
191
+ sessions.push({
192
+ id: sessionId,
193
+ assistant: 'claude',
194
+ title,
195
+ createdAt: createdAt || new Date().toISOString(),
196
+ updatedAt: updatedAt || new Date().toISOString(),
197
+ messageCount,
198
+ workspacePath,
199
+ });
200
+ } catch {}
201
+ }
202
+ } catch {}
203
+ }
204
+ } catch {}
205
+
206
+ // ─── 3. Scan OpenAI Codex Sessions ──────────────────────────────────────
207
+ const codexDir = path.join(os.homedir(), '.codex');
208
+ const codexSessionsDir = path.join(codexDir, 'sessions');
209
+ try {
210
+ const codexFiles = await getFilesRecursively(codexSessionsDir, '.jsonl');
211
+ for (const file of codexFiles) {
212
+ try {
213
+ const content = await fs.readFile(file, 'utf-8');
214
+ // Heuristic: check if the file references our workspace path or folder name
215
+ if (!content.toLowerCase().includes(normWorkspace) && !content.includes(wsFolderName)) {
216
+ continue;
217
+ }
218
+
219
+ const lines = content.split('\n').filter(Boolean);
220
+ let messageCount = 0;
221
+ let createdAt: string | null = null;
222
+ let updatedAt: string | null = null;
223
+ let title = 'Codex Session';
224
+
225
+ for (const line of lines) {
226
+ try {
227
+ const record = JSON.parse(line);
228
+ if (record.timestamp) {
229
+ if (!createdAt) createdAt = new Date(record.timestamp).toISOString();
230
+ updatedAt = new Date(record.timestamp).toISOString();
231
+ }
232
+ const role = record.role || record.message?.role || (record.prompt ? 'user' : record.completion ? 'assistant' : null);
233
+ if (role === 'user' || role === 'assistant') {
234
+ messageCount++;
235
+ if (role === 'user' && title === 'Codex Session') {
236
+ const text = record.content || record.prompt || record.message?.content || '';
237
+ if (text) title = text.trim();
238
+ }
239
+ }
240
+ } catch {}
241
+ }
242
+
243
+ const sessionId = path.basename(file, '.jsonl');
244
+ sessions.push({
245
+ id: sessionId,
246
+ assistant: 'codex',
247
+ title: title.length > 80 ? title.substring(0, 80) + '...' : title,
248
+ createdAt: createdAt || new Date().toISOString(),
249
+ updatedAt: updatedAt || new Date().toISOString(),
250
+ messageCount,
251
+ workspacePath,
252
+ });
253
+ } catch {}
254
+ }
255
+ } catch {}
256
+
257
+ // ─── 4. Scan GitHub Copilot Sessions ────────────────────────────────────
258
+ const copilotDir = path.join(os.homedir(), '.copilot');
259
+ const copilotSessionsDirs = [
260
+ path.join(copilotDir, 'session-state'),
261
+ path.join(copilotDir, 'history-session-state')
262
+ ];
263
+ for (const dir of copilotSessionsDirs) {
264
+ try {
265
+ const copilotFiles = await getFilesRecursively(dir, '.json');
266
+ for (const file of copilotFiles) {
267
+ try {
268
+ const content = await fs.readFile(file, 'utf-8');
269
+ // Heuristic check
270
+ if (!content.toLowerCase().includes(normWorkspace) && !content.includes(wsFolderName)) {
271
+ continue;
272
+ }
273
+
274
+ const record = JSON.parse(content);
275
+ let messageCount = 0;
276
+ let title = 'Copilot Session';
277
+ let messagesList: any[] = [];
278
+
279
+ if (Array.isArray(record)) {
280
+ messagesList = record;
281
+ } else if (record && Array.isArray(record.messages)) {
282
+ messagesList = record.messages;
283
+ }
284
+
285
+ for (const msg of messagesList) {
286
+ const role = msg.role || (msg.type === 'user' ? 'user' : msg.type === 'assistant' ? 'assistant' : null);
287
+ if (role === 'user' || role === 'assistant') {
288
+ messageCount++;
289
+ if (role === 'user' && title === 'Copilot Session') {
290
+ const text = msg.content || msg.text || '';
291
+ if (text) title = text.trim();
292
+ }
293
+ }
294
+ }
295
+
296
+ const stats = await fs.stat(file);
297
+ const sessionId = path.basename(file, '.json');
298
+
299
+ sessions.push({
300
+ id: sessionId,
301
+ assistant: 'copilot',
302
+ title: title.length > 80 ? title.substring(0, 80) + '...' : title,
303
+ createdAt: stats.birthtime.toISOString(),
304
+ updatedAt: stats.mtime.toISOString(),
305
+ messageCount,
306
+ workspacePath,
307
+ });
308
+ } catch {}
309
+ }
310
+ } catch {}
311
+ }
312
+
313
+ // Sort by updatedAt descending
314
+ sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
315
+ return sessions;
316
+ }
317
+
318
+ /**
319
+ * Reads and parses the full transcript for a specific assistant conversation session.
320
+ *
321
+ * @param assistant - The name of the AI assistant ('antigravity', 'claude', 'codex', 'copilot').
322
+ * @param sessionId - Unique ID of the conversation session.
323
+ * @returns A promise that resolves to an array of {@link ChatMessage} objects.
324
+ */
325
+ export async function getSessionTranscript(assistant: string, sessionId: string): Promise<ChatMessage[]> {
326
+ const messages: ChatMessage[] = [];
327
+
328
+ if (assistant === 'antigravity') {
329
+ const agDir = path.join(os.homedir(), '.gemini', 'antigravity-cli');
330
+ const transcriptPath = path.join(agDir, 'brain', sessionId, '.system_generated', 'logs', 'transcript.jsonl');
331
+ const content = await fs.readFile(transcriptPath, 'utf-8');
332
+ const lines = content.split('\n').filter(Boolean);
333
+
334
+ for (const line of lines) {
335
+ const obj = JSON.parse(line);
336
+ if (obj.type === 'USER_INPUT') {
337
+ const rawContent = obj.content || '';
338
+ const cleanMatch = rawContent.match(/<USER_REQUEST>([\s\S]*?)<\/USER_REQUEST>/);
339
+ const cleanContent = cleanMatch ? cleanMatch[1].trim() : rawContent.trim();
340
+ messages.push({
341
+ role: 'user',
342
+ content: cleanContent,
343
+ timestamp: obj.created_at || obj.timestamp,
344
+ });
345
+ } else if (obj.type === 'PLANNER_RESPONSE') {
346
+ messages.push({
347
+ role: 'assistant',
348
+ content: (obj.content || '').trim(),
349
+ timestamp: obj.created_at || obj.timestamp,
350
+ });
351
+ }
352
+ }
353
+ } else if (assistant === 'claude') {
354
+ const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
355
+ const claudeProjectsDir = path.join(claudeConfigDir, 'projects');
356
+ const projectFolders = await fs.readdir(claudeProjectsDir).catch(() => [] as string[]);
357
+ let transcriptPath: string | null = null;
358
+
359
+ for (const folder of projectFolders) {
360
+ const p = path.join(claudeProjectsDir, folder, `${sessionId}.jsonl`);
361
+ try {
362
+ await fs.access(p);
363
+ transcriptPath = p;
364
+ break;
365
+ } catch {}
366
+ }
367
+
368
+ if (!transcriptPath) {
369
+ throw new Error(`Claude session ${sessionId} not found`);
370
+ }
371
+
372
+ const content = await fs.readFile(transcriptPath, 'utf-8');
373
+ const lines = content.split('\n').filter(Boolean);
374
+
375
+ for (const line of lines) {
376
+ const record = JSON.parse(line);
377
+ if (record.type === 'user' || record.role === 'user') {
378
+ const rawText = record.message?.text || record.message?.content || record.text || '';
379
+ let cleanText = '';
380
+ if (typeof rawText === 'string') {
381
+ cleanText = rawText.trim();
382
+ } else if (Array.isArray(rawText)) {
383
+ cleanText = rawText.map((b: any) => b.text || b.content || '').join(' ').trim();
384
+ }
385
+ messages.push({
386
+ role: 'user',
387
+ content: cleanText,
388
+ timestamp: record.timestamp || record.created_at,
389
+ });
390
+ } else if (record.type === 'assistant' || record.role === 'assistant') {
391
+ const rawText = record.message?.text || record.message?.content || record.text || '';
392
+ let cleanText = '';
393
+ if (typeof rawText === 'string') {
394
+ cleanText = rawText.trim();
395
+ } else if (Array.isArray(rawText)) {
396
+ cleanText = rawText.map((b: any) => b.text || b.content || '').join(' ').trim();
397
+ }
398
+ messages.push({
399
+ role: 'assistant',
400
+ content: cleanText,
401
+ timestamp: record.timestamp || record.created_at,
402
+ });
403
+ }
404
+ }
405
+ } else if (assistant === 'codex') {
406
+ const codexDir = path.join(os.homedir(), '.codex');
407
+ const codexSessionsDir = path.join(codexDir, 'sessions');
408
+ const codexFiles = await getFilesRecursively(codexSessionsDir, '.jsonl');
409
+ let transcriptPath: string | null = null;
410
+
411
+ for (const file of codexFiles) {
412
+ if (path.basename(file, '.jsonl') === sessionId) {
413
+ transcriptPath = file;
414
+ break;
415
+ }
416
+ }
417
+
418
+ if (!transcriptPath) {
419
+ throw new Error(`Codex session ${sessionId} not found`);
420
+ }
421
+
422
+ const content = await fs.readFile(transcriptPath, 'utf-8');
423
+ const lines = content.split('\n').filter(Boolean);
424
+
425
+ for (const line of lines) {
426
+ const record = JSON.parse(line);
427
+ const role = record.role || record.message?.role || (record.prompt ? 'user' : record.completion ? 'assistant' : null);
428
+ if (role === 'user' || role === 'assistant') {
429
+ const text = record.content || record.prompt || record.completion || record.message?.content || '';
430
+ messages.push({
431
+ role: role as 'user' | 'assistant',
432
+ content: text.trim(),
433
+ timestamp: record.timestamp ? new Date(record.timestamp).toISOString() : undefined,
434
+ });
435
+ }
436
+ }
437
+ } else if (assistant === 'copilot') {
438
+ const copilotDir = path.join(os.homedir(), '.copilot');
439
+ const copilotSessionsDirs = [
440
+ path.join(copilotDir, 'session-state'),
441
+ path.join(copilotDir, 'history-session-state')
442
+ ];
443
+ let transcriptPath: string | null = null;
444
+
445
+ for (const dir of copilotSessionsDirs) {
446
+ const files = await getFilesRecursively(dir, '.json');
447
+ for (const file of files) {
448
+ if (path.basename(file, '.json') === sessionId) {
449
+ transcriptPath = file;
450
+ break;
451
+ }
452
+ }
453
+ if (transcriptPath) break;
454
+ }
455
+
456
+ if (!transcriptPath) {
457
+ throw new Error(`Copilot session ${sessionId} not found`);
458
+ }
459
+
460
+ const content = await fs.readFile(transcriptPath, 'utf-8');
461
+ const record = JSON.parse(content);
462
+ let messagesList: any[] = [];
463
+
464
+ if (Array.isArray(record)) {
465
+ messagesList = record;
466
+ } else if (record && Array.isArray(record.messages)) {
467
+ messagesList = record.messages;
468
+ }
469
+
470
+ for (const msg of messagesList) {
471
+ const role = msg.role || (msg.type === 'user' ? 'user' : msg.type === 'assistant' ? 'assistant' : null);
472
+ if (role === 'user' || role === 'assistant') {
473
+ messages.push({
474
+ role: role as 'user' | 'assistant',
475
+ content: (msg.content || msg.text || '').trim(),
476
+ timestamp: msg.timestamp || msg.created_at,
477
+ });
478
+ }
479
+ }
480
+ }
481
+
482
+ return messages;
483
+ }