@mrpatronz/nexusflow 0.2.19 → 1.3.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 (236) hide show
  1. package/README.md +27 -3
  2. package/dist/analyzers/index.d.ts +23 -0
  3. package/dist/analyzers/index.d.ts.map +1 -1
  4. package/dist/analyzers/index.js +62 -0
  5. package/dist/analyzers/index.js.map +1 -1
  6. package/dist/commands/commands.test.js +20 -6
  7. package/dist/commands/commands.test.js.map +1 -1
  8. package/dist/commands/commit.d.ts +2 -0
  9. package/dist/commands/commit.d.ts.map +1 -1
  10. package/dist/commands/commit.js +27 -5
  11. package/dist/commands/commit.js.map +1 -1
  12. package/dist/commands/diff.d.ts +9 -2
  13. package/dist/commands/diff.d.ts.map +1 -1
  14. package/dist/commands/diff.js +36 -12
  15. package/dist/commands/diff.js.map +1 -1
  16. package/dist/commands/doctor.js +2 -2
  17. package/dist/commands/doctor.js.map +1 -1
  18. package/dist/commands/handoff.js +2 -2
  19. package/dist/commands/handoff.js.map +1 -1
  20. package/dist/commands/refresh.d.ts +4 -2
  21. package/dist/commands/refresh.d.ts.map +1 -1
  22. package/dist/commands/refresh.js +20 -36
  23. package/dist/commands/refresh.js.map +1 -1
  24. package/dist/commands/schedule.d.ts +35 -0
  25. package/dist/commands/schedule.d.ts.map +1 -0
  26. package/dist/commands/schedule.js +151 -0
  27. package/dist/commands/schedule.js.map +1 -0
  28. package/dist/commands/sync.d.ts.map +1 -1
  29. package/dist/commands/sync.js +28 -7
  30. package/dist/commands/sync.js.map +1 -1
  31. package/dist/core/analysis-cache.d.ts +58 -0
  32. package/dist/core/analysis-cache.d.ts.map +1 -0
  33. package/dist/core/analysis-cache.js +116 -0
  34. package/dist/core/analysis-cache.js.map +1 -0
  35. package/dist/core/analysis-cache.test.d.ts +2 -0
  36. package/dist/core/analysis-cache.test.d.ts.map +1 -0
  37. package/dist/core/analysis-cache.test.js +108 -0
  38. package/dist/core/analysis-cache.test.js.map +1 -0
  39. package/dist/core/refresh.d.ts +41 -0
  40. package/dist/core/refresh.d.ts.map +1 -0
  41. package/dist/core/refresh.js +60 -0
  42. package/dist/core/refresh.js.map +1 -0
  43. package/dist/core/scheduler.d.ts +117 -0
  44. package/dist/core/scheduler.d.ts.map +1 -0
  45. package/dist/core/scheduler.js +247 -0
  46. package/dist/core/scheduler.js.map +1 -0
  47. package/dist/core/scheduler.test.d.ts +2 -0
  48. package/dist/core/scheduler.test.d.ts.map +1 -0
  49. package/dist/core/scheduler.test.js +111 -0
  50. package/dist/core/scheduler.test.js.map +1 -0
  51. package/dist/core/sync.d.ts +6 -0
  52. package/dist/core/sync.d.ts.map +1 -1
  53. package/dist/core/sync.js +15 -5
  54. package/dist/core/sync.js.map +1 -1
  55. package/dist/generators/index.d.ts +7 -1
  56. package/dist/generators/index.d.ts.map +1 -1
  57. package/dist/generators/index.js +14 -1
  58. package/dist/generators/index.js.map +1 -1
  59. package/dist/index.js +107 -4
  60. package/dist/index.js.map +1 -1
  61. package/dist/server.d.ts.map +1 -1
  62. package/dist/server.js +81 -0
  63. package/dist/server.js.map +1 -1
  64. package/dist/server.test.js +66 -0
  65. package/dist/server.test.js.map +1 -1
  66. package/dist/utils/multi-git.d.ts +21 -0
  67. package/dist/utils/multi-git.d.ts.map +1 -1
  68. package/dist/utils/multi-git.js +40 -4
  69. package/dist/utils/multi-git.js.map +1 -1
  70. package/package.json +13 -1
  71. package/.agents/skills/ccc/SKILL.md +0 -71
  72. package/.agents/skills/ccc/references/management.md +0 -110
  73. package/.agents/skills/ccc/references/settings.md +0 -126
  74. package/.github/dependabot.yml +0 -52
  75. package/.github/workflows/ci.yml +0 -44
  76. package/.github/workflows/dependabot-merge.yml +0 -53
  77. package/.github/workflows/release-desktop.yml +0 -67
  78. package/.github/workflows/release-npm.yml +0 -65
  79. package/.github/workflows/release-vscode.yml +0 -61
  80. package/.vscode/launch.json +0 -17
  81. package/.vscode/tasks.json +0 -17
  82. package/GETTING_STARTED.md +0 -114
  83. package/desktop/build-installer.js +0 -228
  84. package/desktop/build.js +0 -118
  85. package/desktop/installer/nexusflow.iss +0 -35
  86. package/desktop/neutralino.config.json +0 -39
  87. package/desktop/package-lock.json +0 -1060
  88. package/desktop/package.json +0 -14
  89. package/extension/media/icon.svg +0 -1
  90. package/extension/package-lock.json +0 -6044
  91. package/extension/package.json +0 -117
  92. package/extension/src/extension.ts +0 -653
  93. package/extension/tsconfig.json +0 -21
  94. package/gui/README.md +0 -73
  95. package/gui/e2e/dashboard.spec.ts +0 -61
  96. package/gui/e2e/wizard.spec.ts +0 -346
  97. package/gui/eslint.config.js +0 -26
  98. package/gui/index.html +0 -17
  99. package/gui/package-lock.json +0 -3137
  100. package/gui/package.json +0 -37
  101. package/gui/playwright.config.ts +0 -41
  102. package/gui/public/app_logo.png +0 -0
  103. package/gui/public/favicon.svg +0 -1
  104. package/gui/public/icons.svg +0 -24
  105. package/gui/src/App.css +0 -1
  106. package/gui/src/App.tsx +0 -3573
  107. package/gui/src/app/AppSidebar.tsx +0 -69
  108. package/gui/src/assets/hero.png +0 -0
  109. package/gui/src/assets/react.svg +0 -1
  110. package/gui/src/assets/vite.svg +0 -1
  111. package/gui/src/components/AddRepoPicker.tsx +0 -81
  112. package/gui/src/components/ui/Button.tsx +0 -40
  113. package/gui/src/components/ui/Card.tsx +0 -6
  114. package/gui/src/components/ui/EmptyState.tsx +0 -22
  115. package/gui/src/components/ui/Input.tsx +0 -21
  116. package/gui/src/components/ui/Menu.tsx +0 -71
  117. package/gui/src/components/ui/Modal.tsx +0 -39
  118. package/gui/src/components/ui/PageHeader.tsx +0 -21
  119. package/gui/src/components/ui/RepoStatusStrip.tsx +0 -33
  120. package/gui/src/components/ui/Skeleton.tsx +0 -5
  121. package/gui/src/components/ui/StatusPill.tsx +0 -36
  122. package/gui/src/components/ui/Tabs.tsx +0 -40
  123. package/gui/src/components/ui/cn.ts +0 -4
  124. package/gui/src/components/ui/index.ts +0 -16
  125. package/gui/src/features/changes/ChangesViewer.tsx +0 -388
  126. package/gui/src/features/knowledge/KnowledgeBase.tsx +0 -84
  127. package/gui/src/features/onboarding/OnboardingWizard.tsx +0 -230
  128. package/gui/src/features/plan/ImplementationPlan.tsx +0 -32
  129. package/gui/src/features/services/ServiceConsole.tsx +0 -215
  130. package/gui/src/features/sessions/SessionHistory.tsx +0 -195
  131. package/gui/src/index.css +0 -167
  132. package/gui/src/lib/status.ts +0 -24
  133. package/gui/src/main.tsx +0 -94
  134. package/gui/src/pages/DashboardPage.tsx +0 -129
  135. package/gui/src/pages/WorkspacesPage.tsx +0 -352
  136. package/gui/src/types.ts +0 -81
  137. package/gui/tsconfig.app.json +0 -25
  138. package/gui/tsconfig.json +0 -7
  139. package/gui/tsconfig.node.json +0 -24
  140. package/gui/vite.config.ts +0 -12
  141. package/resources/workflows/plan-implement-review.md +0 -8
  142. package/resources/workflows/research-verify.md +0 -6
  143. package/resources/workflows/solo-developer.md +0 -3
  144. package/scripts/simulate-workspaces.ts +0 -55
  145. package/skills-lock.json +0 -11
  146. package/src/analyzers/detect-apis.ts +0 -290
  147. package/src/analyzers/detect-deps.ts +0 -315
  148. package/src/analyzers/detect-existing.ts +0 -74
  149. package/src/analyzers/detect-ports.ts +0 -110
  150. package/src/analyzers/index.ts +0 -97
  151. package/src/analyzers/messaging-analyzer.ts +0 -254
  152. package/src/analyzers/readme-summarizer.ts +0 -102
  153. package/src/analyzers/run-analyzer.ts +0 -269
  154. package/src/analyzers/tech-stack.ts +0 -283
  155. package/src/commands/adapter.ts +0 -348
  156. package/src/commands/add-repo.ts +0 -156
  157. package/src/commands/commands.test.ts +0 -156
  158. package/src/commands/commit.ts +0 -131
  159. package/src/commands/config.ts +0 -52
  160. package/src/commands/create.ts +0 -204
  161. package/src/commands/desktop.ts +0 -128
  162. package/src/commands/diff.ts +0 -125
  163. package/src/commands/doctor.ts +0 -370
  164. package/src/commands/handoff.ts +0 -266
  165. package/src/commands/init.ts +0 -134
  166. package/src/commands/list.ts +0 -46
  167. package/src/commands/logs.ts +0 -63
  168. package/src/commands/mcp.ts +0 -94
  169. package/src/commands/open.ts +0 -129
  170. package/src/commands/refresh.ts +0 -137
  171. package/src/commands/remove.ts +0 -98
  172. package/src/commands/start.ts +0 -117
  173. package/src/commands/status.ts +0 -54
  174. package/src/commands/stop.ts +0 -55
  175. package/src/commands/sync.ts +0 -125
  176. package/src/commands/tui.ts +0 -484
  177. package/src/commands/ui.ts +0 -111
  178. package/src/core/adapters/local-storage.ts +0 -66
  179. package/src/core/adapters/obsidian-storage.ts +0 -151
  180. package/src/core/adapters/registry.ts +0 -44
  181. package/src/core/adapters/storage.test.ts +0 -174
  182. package/src/core/adapters/vault-storage.ts +0 -86
  183. package/src/core/config.test.ts +0 -96
  184. package/src/core/config.ts +0 -140
  185. package/src/core/graph.ts +0 -344
  186. package/src/core/plugins/index.ts +0 -17
  187. package/src/core/plugins/loader.ts +0 -36
  188. package/src/core/ports/storage.ts +0 -71
  189. package/src/core/scanner.test.ts +0 -61
  190. package/src/core/scanner.ts +0 -91
  191. package/src/core/storage.ts +0 -37
  192. package/src/core/sync.ts +0 -121
  193. package/src/core/workspace-state.test.ts +0 -108
  194. package/src/core/workspace-state.ts +0 -130
  195. package/src/core/workspace.ts +0 -487
  196. package/src/core/worktree.ts +0 -120
  197. package/src/generators/antigravity.ts +0 -32
  198. package/src/generators/base.ts +0 -243
  199. package/src/generators/claude.ts +0 -34
  200. package/src/generators/codex.ts +0 -48
  201. package/src/generators/copilot.ts +0 -56
  202. package/src/generators/cursor.ts +0 -44
  203. package/src/generators/diff-context.ts +0 -75
  204. package/src/generators/index.ts +0 -322
  205. package/src/generators/map-generator.test.ts +0 -159
  206. package/src/generators/map-generator.ts +0 -539
  207. package/src/generators/plan-generator.ts +0 -483
  208. package/src/generators/skills-generator.ts +0 -259
  209. package/src/index.ts +0 -539
  210. package/src/mcp/server.ts +0 -338
  211. package/src/orchestration/detect.ts +0 -313
  212. package/src/orchestration/index.ts +0 -7
  213. package/src/orchestration/runner.ts +0 -283
  214. package/src/server.test.ts +0 -618
  215. package/src/server.ts +0 -1402
  216. package/src/types.ts +0 -451
  217. package/src/utils/detect-ai.test.ts +0 -44
  218. package/src/utils/detect-ai.ts +0 -84
  219. package/src/utils/detect-editors.test.ts +0 -48
  220. package/src/utils/detect-editors.ts +0 -57
  221. package/src/utils/git.test.ts +0 -74
  222. package/src/utils/git.ts +0 -117
  223. package/src/utils/local-ai.test.ts +0 -130
  224. package/src/utils/local-ai.ts +0 -116
  225. package/src/utils/multi-git.test.ts +0 -158
  226. package/src/utils/multi-git.ts +0 -405
  227. package/src/utils/prompts.ts +0 -209
  228. package/src/utils/session-finder.ts +0 -483
  229. package/src/utils/system-scanner.test.ts +0 -89
  230. package/src/utils/system-scanner.ts +0 -96
  231. package/src/utils/update-check.ts +0 -286
  232. package/src/utils/workflow-advisor.ts +0 -118
  233. package/src/utils/workflows.test.ts +0 -114
  234. package/src/utils/workflows.ts +0 -178
  235. package/tsconfig.json +0 -19
  236. package/vitest.config.ts +0 -14
@@ -1,483 +0,0 @@
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,89 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { execa } from 'execa';
3
- import * as os from 'node:os';
4
- import { scanSystemSpecs } from './system-scanner.js';
5
-
6
- vi.mock('execa');
7
- vi.mock('node:os', async () => {
8
- const actual = await vi.importActual<typeof import('node:os')>('node:os');
9
- return {
10
- ...actual,
11
- totalmem: vi.fn(),
12
- platform: vi.fn(),
13
- };
14
- });
15
-
16
- describe('scanSystemSpecs', () => {
17
- beforeEach(() => {
18
- vi.clearAllMocks();
19
- });
20
-
21
- it('should recommend 7b on windows with 16GB RAM even if no GPU detected', async () => {
22
- vi.mocked(os.totalmem).mockReturnValue(16 * 1024 * 1024 * 1024);
23
- vi.mocked(os.platform).mockReturnValue('win32');
24
- vi.mocked(execa).mockResolvedValue({ stdout: '' } as any);
25
-
26
- const specs = await scanSystemSpecs();
27
-
28
- expect(specs.totalRamGb).toBe(16);
29
- expect(specs.gpuName).toBe('Unknown/Integrated');
30
- expect(specs.hasHardwareAcceleration).toBe(false);
31
- expect(specs.recommendedModel).toBe('qwen2.5-coder:7b');
32
- });
33
-
34
- it('should recommend 1.5b on windows with 8GB RAM and no GPU', async () => {
35
- vi.mocked(os.totalmem).mockReturnValue(8 * 1024 * 1024 * 1024);
36
- vi.mocked(os.platform).mockReturnValue('win32');
37
- vi.mocked(execa).mockResolvedValue({ stdout: '' } as any);
38
-
39
- const specs = await scanSystemSpecs();
40
-
41
- expect(specs.totalRamGb).toBe(8);
42
- expect(specs.recommendedModel).toBe('qwen2.5-coder:1.5b');
43
- });
44
-
45
- it('should detect Nvidia GPU on Windows and recommend 7b with 12GB RAM', async () => {
46
- vi.mocked(os.totalmem).mockReturnValue(12 * 1024 * 1024 * 1024);
47
- vi.mocked(os.platform).mockReturnValue('win32');
48
- vi.mocked(execa).mockResolvedValue({
49
- stdout: 'NVIDIA GeForce RTX 4070 Laptop GPU\r\n'
50
- } as any);
51
-
52
- const specs = await scanSystemSpecs();
53
-
54
- expect(specs.totalRamGb).toBe(12);
55
- expect(specs.gpuName).toBe('NVIDIA GeForce RTX 4070 Laptop GPU');
56
- expect(specs.hasHardwareAcceleration).toBe(true);
57
- expect(specs.recommendedModel).toBe('qwen2.5-coder:7b');
58
- });
59
-
60
- it('should detect Apple Silicon on Darwin and recommend 7b with 12GB RAM', async () => {
61
- vi.mocked(os.totalmem).mockReturnValue(12 * 1024 * 1024 * 1024);
62
- vi.mocked(os.platform).mockReturnValue('darwin');
63
- vi.mocked(execa).mockResolvedValue({
64
- stdout: 'Apple M3 Max\n'
65
- } as any);
66
-
67
- const specs = await scanSystemSpecs();
68
-
69
- expect(specs.totalRamGb).toBe(12);
70
- expect(specs.gpuName).toBe('Apple Silicon (Integrated)');
71
- expect(specs.hasHardwareAcceleration).toBe(true);
72
- expect(specs.recommendedModel).toBe('qwen2.5-coder:7b');
73
- });
74
-
75
- it('should detect Linux GPU and recommend 7b with 12GB RAM', async () => {
76
- vi.mocked(os.totalmem).mockReturnValue(12 * 1024 * 1024 * 1024);
77
- vi.mocked(os.platform).mockReturnValue('linux');
78
- vi.mocked(execa).mockResolvedValue({
79
- stdout: '01:00.0 VGA compatible controller: NVIDIA Corporation AD104 [GeForce RTX 4070 Ti] (rev a1)\n'
80
- } as any);
81
-
82
- const specs = await scanSystemSpecs();
83
-
84
- expect(specs.totalRamGb).toBe(12);
85
- expect(specs.gpuName).toBe('NVIDIA Corporation AD104 [GeForce RTX 4070 Ti] (rev a1)');
86
- expect(specs.hasHardwareAcceleration).toBe(true);
87
- expect(specs.recommendedModel).toBe('qwen2.5-coder:7b');
88
- });
89
- });
@@ -1,96 +0,0 @@
1
- /**
2
- * @module utils/system-scanner
3
- * Detects local system specifications (RAM, GPU) and recommends the optimal local LLM size.
4
- */
5
-
6
- import { execa } from 'execa';
7
- import * as os from 'node:os';
8
-
9
- export interface SystemSpecs {
10
- totalRamGb: number;
11
- gpuName: string;
12
- hasHardwareAcceleration: boolean;
13
- recommendedModel: string;
14
- }
15
-
16
- /**
17
- * Probes the local operating system to detect RAM and GPU specifications,
18
- * returning a recommended local LLM model name.
19
- */
20
- export async function scanSystemSpecs(): Promise<SystemSpecs> {
21
- const totalRamGb = Math.round(os.totalmem() / (1024 * 1024 * 1024));
22
- let gpuName = 'Unknown/Integrated';
23
- let hasHardwareAcceleration = false;
24
-
25
- const platform = os.platform();
26
-
27
- try {
28
- if (platform === 'win32') {
29
- const { stdout } = await execa('powershell', ['-NoProfile', '-Command', 'Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name'], { reject: false });
30
- if (stdout) {
31
- const lines = stdout.split('\n')
32
- .map(l => l.trim())
33
- .filter(l => l && l.toLowerCase() !== 'name');
34
-
35
- if (lines.length > 0) {
36
- gpuName = lines.join(', ');
37
- const lowerGpu = gpuName.toLowerCase();
38
- if (
39
- lowerGpu.includes('nvidia') ||
40
- lowerGpu.includes('radeon') ||
41
- lowerGpu.includes('geforce') ||
42
- lowerGpu.includes('rtx') ||
43
- lowerGpu.includes('gtx')
44
- ) {
45
- hasHardwareAcceleration = true;
46
- }
47
- }
48
- }
49
- } else if (platform === 'darwin') {
50
- const { stdout } = await execa('sysctl', ['-n', 'machdep.cpu.brand_string'], { reject: false });
51
- if (stdout && stdout.toLowerCase().includes('apple')) {
52
- gpuName = 'Apple Silicon (Integrated)';
53
- hasHardwareAcceleration = true;
54
- } else {
55
- const { stdout: spOut } = await execa('system_profiler', ['SPDisplaysDataType'], { reject: false });
56
- if (spOut) {
57
- gpuName = 'Intel Mac GPU';
58
- const lowerGpu = spOut.toLowerCase();
59
- if (lowerGpu.includes('amd') || lowerGpu.includes('radeon') || lowerGpu.includes('nvidia')) {
60
- hasHardwareAcceleration = true;
61
- }
62
- }
63
- }
64
- } else if (platform === 'linux') {
65
- const { stdout } = await execa('lspci', [], { reject: false });
66
- if (stdout) {
67
- const gpuLines = stdout.split('\n').filter(line => line.toLowerCase().includes('vga') || line.toLowerCase().includes('3d'));
68
- if (gpuLines.length > 0) {
69
- gpuName = gpuLines.map(line => line.split(':').pop()?.trim()).filter(Boolean).join(', ');
70
- const lowerGpu = gpuName.toLowerCase();
71
- if (lowerGpu.includes('nvidia') || lowerGpu.includes('amd') || lowerGpu.includes('radeon')) {
72
- hasHardwareAcceleration = true;
73
- }
74
- }
75
- }
76
- }
77
- } catch {
78
- // Ignore execution failures, fall back to default values
79
- }
80
-
81
- // Recommendation logic:
82
- // - RAM >= 12GB AND has dedicated GPU/Apple Silicon -> Recommend 7B
83
- // - RAM >= 16GB regardless of GPU -> Recommend 7B (can run fine on CPU)
84
- // - Otherwise -> Recommend 1.5B (fast and safe)
85
- let recommendedModel = 'qwen2.5-coder:1.5b';
86
- if ((totalRamGb >= 12 && hasHardwareAcceleration) || totalRamGb >= 16) {
87
- recommendedModel = 'qwen2.5-coder:7b';
88
- }
89
-
90
- return {
91
- totalRamGb,
92
- gpuName,
93
- hasHardwareAcceleration,
94
- recommendedModel,
95
- };
96
- }