dr-claw 1.1.2

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 (287) hide show
  1. package/LICENSE +1380 -0
  2. package/NOTICE +17 -0
  3. package/README.md +875 -0
  4. package/README.zh-CN.md +857 -0
  5. package/dist/api-docs.html +878 -0
  6. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  7. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  8. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  21. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  22. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  23. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  26. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  28. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  29. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  30. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  31. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  32. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  33. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  34. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  35. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  36. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  37. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  48. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  49. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  50. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  51. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  52. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  53. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  54. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  55. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  56. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  57. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  58. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  59. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  60. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  61. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  62. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  63. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  64. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  65. package/dist/assets/_basePickBy-C0n1uo4u.js +1 -0
  66. package/dist/assets/_baseUniq-BRNmhFkc.js +1 -0
  67. package/dist/assets/arc-DOMBmC_A.js +1 -0
  68. package/dist/assets/architectureDiagram-VXUJARFQ-CtMWglbD.js +36 -0
  69. package/dist/assets/blockDiagram-VD42YOAC-DK_gTkJy.js +122 -0
  70. package/dist/assets/c4Diagram-YG6GDRKO-DrDAvsn7.js +10 -0
  71. package/dist/assets/channel-BFOE7OyC.js +1 -0
  72. package/dist/assets/chunk-4BX2VUAB-CGbhdZcN.js +1 -0
  73. package/dist/assets/chunk-55IACEB6-sGeFSxen.js +1 -0
  74. package/dist/assets/chunk-B4BG7PRW-RxgHAU_P.js +165 -0
  75. package/dist/assets/chunk-DI55MBZ5-CvHUXZrx.js +220 -0
  76. package/dist/assets/chunk-FMBD7UC4-Sn7cXVNU.js +15 -0
  77. package/dist/assets/chunk-QN33PNHL-bDKOHppN.js +1 -0
  78. package/dist/assets/chunk-QZHKN3VN-B9kN5LGm.js +1 -0
  79. package/dist/assets/chunk-TZMSLE5B-CCBe6len.js +1 -0
  80. package/dist/assets/classDiagram-2ON5EDUG-DQoMNYlY.js +1 -0
  81. package/dist/assets/classDiagram-v2-WZHVMYZB-DQoMNYlY.js +1 -0
  82. package/dist/assets/clone-BulQSblH.js +1 -0
  83. package/dist/assets/cose-bilkent-S5V4N54A-r6sLiHBW.js +1 -0
  84. package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  85. package/dist/assets/dagre-6UL2VRFP-AbsaR1Oa.js +4 -0
  86. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  87. package/dist/assets/diagram-PSM6KHXK-DjCvdUyy.js +24 -0
  88. package/dist/assets/diagram-QEK2KX5R-CqiocXkH.js +43 -0
  89. package/dist/assets/diagram-S2PKOQOG-D5ALeoII.js +24 -0
  90. package/dist/assets/erDiagram-Q2GNP2WA-C1AeOsiz.js +60 -0
  91. package/dist/assets/flowDiagram-NV44I4VS-C98ve_DO.js +162 -0
  92. package/dist/assets/ganttDiagram-JELNMOA3-3zdLvcxD.js +267 -0
  93. package/dist/assets/gitGraphDiagram-V2S2FVAM-nDuDMsHh.js +65 -0
  94. package/dist/assets/graph-Blm-emkR.js +1 -0
  95. package/dist/assets/index-B8Yutsnm.css +1 -0
  96. package/dist/assets/index-Cf6gyhc3.js +654 -0
  97. package/dist/assets/infoDiagram-HS3SLOUP-CmzE3q0R.js +2 -0
  98. package/dist/assets/init-Gi6I4Gst.js +1 -0
  99. package/dist/assets/journeyDiagram-XKPGCS4Q-CYaW7LrP.js +139 -0
  100. package/dist/assets/kanban-definition-3W4ZIXB7-mkmW7p0j.js +89 -0
  101. package/dist/assets/layout-Ct2kFIId.js +1 -0
  102. package/dist/assets/linear-CKG9EwIJ.js +1 -0
  103. package/dist/assets/mermaid.core-CaKcUnnd.js +250 -0
  104. package/dist/assets/mindmap-definition-VGOIOE7T-Bct-pYin.js +68 -0
  105. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  106. package/dist/assets/pieDiagram-ADFJNKIX-C8wFD4Jr.js +30 -0
  107. package/dist/assets/quadrantDiagram-AYHSOK5B-CuiE_f46.js +7 -0
  108. package/dist/assets/requirementDiagram-UZGBJVZJ-C4v1CqU8.js +64 -0
  109. package/dist/assets/sankeyDiagram-TZEHDZUN-keAqqs-Y.js +10 -0
  110. package/dist/assets/sequenceDiagram-WL72ISMW-ClNbvKTB.js +145 -0
  111. package/dist/assets/stateDiagram-FKZM4ZOC-D8We-2vq.js +1 -0
  112. package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +1 -0
  113. package/dist/assets/timeline-definition-IT6M3QCI-DsvEMT5T.js +61 -0
  114. package/dist/assets/treemap-GDKQZRPO-TJrzN7Bj.js +162 -0
  115. package/dist/assets/vendor-codemirror-ansM-eig.js +33 -0
  116. package/dist/assets/vendor-react-BWF6YQQ3.js +8 -0
  117. package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
  118. package/dist/assets/xychartDiagram-PRI3JC2R-BdATmY5F.js +7 -0
  119. package/dist/clear-cache.html +85 -0
  120. package/dist/convert-icons.md +53 -0
  121. package/dist/dr-claw.png +0 -0
  122. package/dist/favicon.png +0 -0
  123. package/dist/generate-icons.js +49 -0
  124. package/dist/icons/claude-ai-icon.svg +1 -0
  125. package/dist/icons/codex-white.svg +3 -0
  126. package/dist/icons/codex.svg +3 -0
  127. package/dist/icons/cursor-white.svg +12 -0
  128. package/dist/icons/cursor.svg +1 -0
  129. package/dist/icons/file.svg +16997 -0
  130. package/dist/icons/generate-icons.md +19 -0
  131. package/dist/icons/icon-128x128.png +0 -0
  132. package/dist/icons/icon-144x144.png +0 -0
  133. package/dist/icons/icon-152x152.png +0 -0
  134. package/dist/icons/icon-192x192.png +0 -0
  135. package/dist/icons/icon-384x384.png +0 -0
  136. package/dist/icons/icon-512x512.png +0 -0
  137. package/dist/icons/icon-72x72.png +0 -0
  138. package/dist/icons/icon-96x96.png +0 -0
  139. package/dist/icons/news/arxiv.svg +1 -0
  140. package/dist/icons/news/huggingface.svg +8 -0
  141. package/dist/icons/news/x-black.png +0 -0
  142. package/dist/icons/news/x-white.png +0 -0
  143. package/dist/icons/news/xiaohongshu.png +0 -0
  144. package/dist/index.html +51 -0
  145. package/dist/leveraged-cognition.png +0 -0
  146. package/dist/manifest.json +61 -0
  147. package/dist/screenshots/chat.png +0 -0
  148. package/dist/screenshots/new_claw.png +0 -0
  149. package/dist/screenshots/news_dashboard.png +0 -0
  150. package/dist/screenshots/project_dashboard.png +0 -0
  151. package/dist/screenshots/research_lab.png +0 -0
  152. package/dist/screenshots/skill_library.png +0 -0
  153. package/dist/screenshots/task_list.png +0 -0
  154. package/dist/screenshots/terminal_example.png +0 -0
  155. package/dist/screenshots/terminal_example1.png +0 -0
  156. package/dist/sw.js +58 -0
  157. package/dist/wechat-group-qr.jpg +0 -0
  158. package/package.json +133 -0
  159. package/public/api-docs.html +878 -0
  160. package/public/clear-cache.html +85 -0
  161. package/public/convert-icons.md +53 -0
  162. package/public/dr-claw.png +0 -0
  163. package/public/favicon.png +0 -0
  164. package/public/generate-icons.js +49 -0
  165. package/public/icons/claude-ai-icon.svg +1 -0
  166. package/public/icons/codex-white.svg +3 -0
  167. package/public/icons/codex.svg +3 -0
  168. package/public/icons/cursor-white.svg +12 -0
  169. package/public/icons/cursor.svg +1 -0
  170. package/public/icons/file.svg +16997 -0
  171. package/public/icons/generate-icons.md +19 -0
  172. package/public/icons/icon-128x128.png +0 -0
  173. package/public/icons/icon-144x144.png +0 -0
  174. package/public/icons/icon-152x152.png +0 -0
  175. package/public/icons/icon-192x192.png +0 -0
  176. package/public/icons/icon-384x384.png +0 -0
  177. package/public/icons/icon-512x512.png +0 -0
  178. package/public/icons/icon-72x72.png +0 -0
  179. package/public/icons/icon-96x96.png +0 -0
  180. package/public/icons/news/arxiv.svg +1 -0
  181. package/public/icons/news/huggingface.svg +8 -0
  182. package/public/icons/news/x-black.png +0 -0
  183. package/public/icons/news/x-white.png +0 -0
  184. package/public/icons/news/xiaohongshu.png +0 -0
  185. package/public/leveraged-cognition.png +0 -0
  186. package/public/manifest.json +61 -0
  187. package/public/screenshots/chat.png +0 -0
  188. package/public/screenshots/new_claw.png +0 -0
  189. package/public/screenshots/news_dashboard.png +0 -0
  190. package/public/screenshots/project_dashboard.png +0 -0
  191. package/public/screenshots/research_lab.png +0 -0
  192. package/public/screenshots/skill_library.png +0 -0
  193. package/public/screenshots/task_list.png +0 -0
  194. package/public/screenshots/terminal_example.png +0 -0
  195. package/public/screenshots/terminal_example1.png +0 -0
  196. package/public/sw.js +58 -0
  197. package/public/wechat-group-qr.jpg +0 -0
  198. package/scripts/export-skills-catalog-v2.mjs +746 -0
  199. package/scripts/fix-node-pty.js +45 -0
  200. package/scripts/gemini_image_core.py +470 -0
  201. package/scripts/install-python-tools.js +64 -0
  202. package/server/AGENTS.md +73 -0
  203. package/server/__tests__/gemini-legacy-thought.test.mjs +88 -0
  204. package/server/__tests__/runtimePorts.test.mjs +115 -0
  205. package/server/__tests__/session-delete.test.mjs +166 -0
  206. package/server/bin/compute-node +70 -0
  207. package/server/claude-sdk.js +799 -0
  208. package/server/cli-chat.js +488 -0
  209. package/server/cli.js +354 -0
  210. package/server/compute-node.js +439 -0
  211. package/server/constants/config.js +5 -0
  212. package/server/cursor-cli.js +340 -0
  213. package/server/database/auth.db +0 -0
  214. package/server/database/db.js +1852 -0
  215. package/server/database/init.sql +194 -0
  216. package/server/gemini-cli.js +1352 -0
  217. package/server/index.js +3154 -0
  218. package/server/load-env.js +71 -0
  219. package/server/local-gpu.js +860 -0
  220. package/server/middleware/auth.js +100 -0
  221. package/server/openai-codex.js +724 -0
  222. package/server/openrouter.js +890 -0
  223. package/server/project-token-usage.js +407 -0
  224. package/server/projects.js +3973 -0
  225. package/server/routes/agent.js +1272 -0
  226. package/server/routes/auth.js +128 -0
  227. package/server/routes/auto-research.js +558 -0
  228. package/server/routes/cli-auth.js +958 -0
  229. package/server/routes/codex.js +344 -0
  230. package/server/routes/commands.js +601 -0
  231. package/server/routes/compute.js +358 -0
  232. package/server/routes/cursor.js +795 -0
  233. package/server/routes/git.js +1348 -0
  234. package/server/routes/mcp-utils.js +48 -0
  235. package/server/routes/mcp.js +618 -0
  236. package/server/routes/news.js +711 -0
  237. package/server/routes/projects.js +633 -0
  238. package/server/routes/references.js +338 -0
  239. package/server/routes/settings.js +285 -0
  240. package/server/routes/skills.js +914 -0
  241. package/server/routes/taskmaster.js +2566 -0
  242. package/server/routes/telemetry.js +89 -0
  243. package/server/routes/user.js +155 -0
  244. package/server/scripts/fix_session_names.js +57 -0
  245. package/server/scripts/research-news/common_words.py +19 -0
  246. package/server/scripts/research-news/link_keywords.py +298 -0
  247. package/server/scripts/research-news/scan_existing_notes.py +266 -0
  248. package/server/scripts/research-news/scoring_utils.py +227 -0
  249. package/server/scripts/research-news/search_arxiv.py +842 -0
  250. package/server/scripts/research-news/search_huggingface.py +438 -0
  251. package/server/scripts/research-news/search_x.py +490 -0
  252. package/server/scripts/research-news/search_xiaohongshu.py +610 -0
  253. package/server/taskmaster-templates/README.md +150 -0
  254. package/server/taskmaster-templates/README.zh-CN.md +150 -0
  255. package/server/taskmaster-templates/ai-research-dataset.json +230 -0
  256. package/server/taskmaster-templates/ai-research-method-model.json +230 -0
  257. package/server/taskmaster-templates/ai-research-position-paper.json +230 -0
  258. package/server/telemetry.js +121 -0
  259. package/server/templates/AGENTS.md +103 -0
  260. package/server/templates/CLAUDE.md +104 -0
  261. package/server/templates/GEMINI.md +103 -0
  262. package/server/templates/cursor-project.md +108 -0
  263. package/server/templates/index.js +37 -0
  264. package/server/utils/__tests__/sessionTokenUsage.test.js +82 -0
  265. package/server/utils/auto-research-mailer.js +57 -0
  266. package/server/utils/cliResolution.js +170 -0
  267. package/server/utils/commandParser.js +303 -0
  268. package/server/utils/cursorCommand.js +61 -0
  269. package/server/utils/geminiApiKey.js +28 -0
  270. package/server/utils/gitConfig.js +24 -0
  271. package/server/utils/imageAttachmentFiles.js +35 -0
  272. package/server/utils/mcp-detector.js +198 -0
  273. package/server/utils/parsers/bibtex-parser.js +207 -0
  274. package/server/utils/permissions.js +100 -0
  275. package/server/utils/runtimePorts.js +212 -0
  276. package/server/utils/sessionFormatting.js +68 -0
  277. package/server/utils/sessionIndex.js +100 -0
  278. package/server/utils/sessionMode.js +95 -0
  279. package/server/utils/sessionTokenUsage.js +105 -0
  280. package/server/utils/taskmaster-websocket.js +129 -0
  281. package/server/utils/zotero-client.js +136 -0
  282. package/shared/errorClassifier.js +102 -0
  283. package/shared/geminiThinkingSupport.d.ts +22 -0
  284. package/shared/geminiThinkingSupport.js +96 -0
  285. package/shared/geminiThoughtParser.d.ts +8 -0
  286. package/shared/geminiThoughtParser.js +30 -0
  287. package/shared/modelConstants.js +139 -0
@@ -0,0 +1,1348 @@
1
+ import express from 'express';
2
+ import { exec, spawn } from 'child_process';
3
+ import { promisify } from 'util';
4
+ import path from 'path';
5
+ import { promises as fs } from 'fs';
6
+ import { extractProjectDirectory } from '../projects.js';
7
+ import { queryClaudeSDK } from '../claude-sdk.js';
8
+ import { spawnCursor } from '../cursor-cli.js';
9
+
10
+ const router = express.Router();
11
+ const execAsync = promisify(exec);
12
+
13
+ function spawnAsync(command, args, options = {}) {
14
+ return new Promise((resolve, reject) => {
15
+ const child = spawn(command, args, {
16
+ ...options,
17
+ shell: false,
18
+ });
19
+
20
+ let stdout = '';
21
+ let stderr = '';
22
+
23
+ child.stdout.on('data', (data) => {
24
+ stdout += data.toString();
25
+ });
26
+
27
+ child.stderr.on('data', (data) => {
28
+ stderr += data.toString();
29
+ });
30
+
31
+ child.on('error', (error) => {
32
+ reject(error);
33
+ });
34
+
35
+ child.on('close', (code) => {
36
+ if (code === 0) {
37
+ resolve({ stdout, stderr });
38
+ return;
39
+ }
40
+
41
+ const error = new Error(`Command failed: ${command} ${args.join(' ')}`);
42
+ error.code = code;
43
+ error.stdout = stdout;
44
+ error.stderr = stderr;
45
+ reject(error);
46
+ });
47
+ });
48
+ }
49
+
50
+ // Helper function to get the actual project path from the encoded project name
51
+ async function getActualProjectPath(projectName) {
52
+ try {
53
+ return await extractProjectDirectory(projectName);
54
+ } catch (error) {
55
+ console.error(`Error extracting project directory for ${projectName}:`, error);
56
+ // Fallback to the old method
57
+ return projectName.replace(/-/g, '/');
58
+ }
59
+ }
60
+
61
+ // Helper function to strip git diff headers
62
+ function stripDiffHeaders(diff) {
63
+ if (!diff) return '';
64
+
65
+ const lines = diff.split('\n');
66
+ const filteredLines = [];
67
+ let startIncluding = false;
68
+
69
+ for (const line of lines) {
70
+ // Skip all header lines including diff --git, index, file mode, and --- / +++ file paths
71
+ if (line.startsWith('diff --git') ||
72
+ line.startsWith('index ') ||
73
+ line.startsWith('new file mode') ||
74
+ line.startsWith('deleted file mode') ||
75
+ line.startsWith('---') ||
76
+ line.startsWith('+++')) {
77
+ continue;
78
+ }
79
+
80
+ // Start including lines from @@ hunk headers onwards
81
+ if (line.startsWith('@@') || startIncluding) {
82
+ startIncluding = true;
83
+ filteredLines.push(line);
84
+ }
85
+ }
86
+
87
+ return filteredLines.join('\n');
88
+ }
89
+
90
+ // Helper function to validate git repository
91
+ async function validateGitRepository(projectPath) {
92
+ try {
93
+ // Check if directory exists
94
+ await fs.access(projectPath);
95
+ } catch {
96
+ throw new Error(`Project path not found: ${projectPath}`);
97
+ }
98
+
99
+ try {
100
+ // Allow any directory that is inside a work tree (repo root or nested folder).
101
+ const { stdout: insideWorkTreeOutput } = await execAsync('git rev-parse --is-inside-work-tree', { cwd: projectPath });
102
+ const isInsideWorkTree = insideWorkTreeOutput.trim() === 'true';
103
+ if (!isInsideWorkTree) {
104
+ throw new Error('Not inside a git work tree');
105
+ }
106
+
107
+ // Ensure git can resolve the repository root for this directory.
108
+ await execAsync('git rev-parse --show-toplevel', { cwd: projectPath });
109
+ } catch {
110
+ throw new Error('Not a git repository. This directory does not contain a .git folder. Initialize a git repository with "git init" to use source control features.');
111
+ }
112
+ }
113
+
114
+ async function initializeGitRepository(projectPath) {
115
+ try {
116
+ await spawnAsync('git', ['init', '-b', 'main'], { cwd: projectPath });
117
+ } catch (error) {
118
+ const stderr = String(error.stderr || '').toLowerCase();
119
+ const stdout = String(error.stdout || '').toLowerCase();
120
+ const output = `${stdout}\n${stderr}`;
121
+
122
+ // Older git versions do not support `git init -b`.
123
+ if (output.includes('unknown switch') || output.includes('invalid option') || output.includes('usage: git init')) {
124
+ await spawnAsync('git', ['init'], { cwd: projectPath });
125
+
126
+ try {
127
+ await spawnAsync('git', ['symbolic-ref', 'HEAD', 'refs/heads/main'], { cwd: projectPath });
128
+ } catch (symbolicRefError) {
129
+ console.warn('Failed to rename default branch to main after git init:', symbolicRefError);
130
+ }
131
+
132
+ return;
133
+ }
134
+
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ function isGitRepositoryMissingError(message = '') {
140
+ const msg = String(message || '').toLowerCase();
141
+ return msg.includes('not a git repository') ||
142
+ msg.includes('not a git work tree') ||
143
+ msg.includes('project directory is not a git repository');
144
+ }
145
+
146
+ function getGitErrorText(error) {
147
+ return [
148
+ String(error?.message || ''),
149
+ String(error?.stdout || ''),
150
+ String(error?.stderr || ''),
151
+ ].join('\n');
152
+ }
153
+
154
+ async function getCurrentBranchName(projectPath, fallback = 'main') {
155
+ try {
156
+ const { stdout } = await spawnAsync('git', ['symbolic-ref', '--short', 'HEAD'], { cwd: projectPath });
157
+ return stdout.trim() || fallback;
158
+ } catch {
159
+ return fallback;
160
+ }
161
+ }
162
+
163
+ async function repositoryHasCommits(projectPath) {
164
+ try {
165
+ await spawnAsync('git', ['rev-parse', '--verify', 'HEAD'], { cwd: projectPath });
166
+ return true;
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+
172
+ async function getFileStatusOutput(projectPath, file) {
173
+ const { stdout } = await spawnAsync('git', ['status', '--porcelain', '--', file], { cwd: projectPath });
174
+ return stdout;
175
+ }
176
+
177
+ async function getHeadFileContent(projectPath, file) {
178
+ const { stdout } = await spawnAsync('git', ['show', `HEAD:${file}`], { cwd: projectPath });
179
+ return stdout;
180
+ }
181
+
182
+ async function getUnstagedDiff(projectPath, file) {
183
+ const { stdout } = await spawnAsync('git', ['diff', '--', file], { cwd: projectPath });
184
+ return stdout;
185
+ }
186
+
187
+ async function getStagedDiff(projectPath, file) {
188
+ const { stdout } = await spawnAsync('git', ['diff', '--cached', '--', file], { cwd: projectPath });
189
+ return stdout;
190
+ }
191
+
192
+ async function getDiffAgainstHead(projectPath, file) {
193
+ const { stdout } = await spawnAsync('git', ['diff', 'HEAD', '--', file], { cwd: projectPath });
194
+ return stdout;
195
+ }
196
+
197
+ async function checkoutBranch(projectPath, branch) {
198
+ const { stdout } = await spawnAsync('git', ['checkout', branch], { cwd: projectPath });
199
+ return stdout;
200
+ }
201
+
202
+ async function createBranchAndCheckout(projectPath, branch) {
203
+ const { stdout } = await spawnAsync('git', ['checkout', '-b', branch], { cwd: projectPath });
204
+ return stdout;
205
+ }
206
+
207
+ async function commitFiles(projectPath, message) {
208
+ const { stdout } = await spawnAsync('git', ['commit', '-m', message], { cwd: projectPath });
209
+ return stdout;
210
+ }
211
+
212
+ async function getUpstreamBranch(projectPath, branch) {
213
+ const { stdout } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', `${branch}@{upstream}`], { cwd: projectPath });
214
+ return stdout.trim();
215
+ }
216
+
217
+ async function pushWithUpstream(projectPath, remoteName, branch) {
218
+ const { stdout } = await spawnAsync('git', ['push', '--set-upstream', remoteName, branch], { cwd: projectPath });
219
+ return stdout;
220
+ }
221
+
222
+ async function fetchRemote(projectPath, remoteName) {
223
+ const { stdout } = await spawnAsync('git', ['fetch', remoteName], { cwd: projectPath });
224
+ return stdout;
225
+ }
226
+
227
+ async function pullRemoteBranch(projectPath, remoteName, remoteBranch) {
228
+ const { stdout } = await spawnAsync('git', ['pull', remoteName, remoteBranch], { cwd: projectPath });
229
+ return stdout;
230
+ }
231
+
232
+ async function pushRemoteBranch(projectPath, remoteName, remoteBranch) {
233
+ const { stdout } = await spawnAsync('git', ['push', remoteName, remoteBranch], { cwd: projectPath });
234
+ return stdout;
235
+ }
236
+
237
+ async function getRemoteTrackingCounts(projectPath, trackingBranch) {
238
+ const { stdout } = await spawnAsync('git', ['rev-list', '--count', '--left-right', `${trackingBranch}...HEAD`], { cwd: projectPath });
239
+ return stdout;
240
+ }
241
+
242
+ async function getCurrentHeadBranch(projectPath) {
243
+ const { stdout } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
244
+ return stdout.trim();
245
+ }
246
+
247
+ async function listGitRemotes(projectPath) {
248
+ const { stdout } = await spawnAsync('git', ['remote'], { cwd: projectPath });
249
+ return stdout.trim().split('\n').filter(r => r.trim());
250
+ }
251
+
252
+ // Get git status for a project
253
+ router.get('/status', async (req, res) => {
254
+ const { project } = req.query;
255
+
256
+ if (!project) {
257
+ return res.status(400).json({ error: 'Project name is required' });
258
+ }
259
+
260
+ try {
261
+ const projectPath = await getActualProjectPath(project);
262
+
263
+ // Validate git repository
264
+ await validateGitRepository(projectPath);
265
+
266
+ // Get current branch - handle case where there are no commits yet
267
+ let branch = 'main';
268
+ let hasCommits = true;
269
+ try {
270
+ branch = await getCurrentBranchName(projectPath, 'main');
271
+ hasCommits = await repositoryHasCommits(projectPath);
272
+ } catch (error) {
273
+ throw error;
274
+ }
275
+
276
+ // Get git status
277
+ const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: projectPath });
278
+
279
+ const modified = [];
280
+ const added = [];
281
+ const deleted = [];
282
+ const untracked = [];
283
+
284
+ statusOutput.split('\n').forEach(line => {
285
+ if (!line.trim()) return;
286
+
287
+ const status = line.substring(0, 2);
288
+ const file = line.substring(3);
289
+
290
+ if (status === 'M ' || status === ' M' || status === 'MM') {
291
+ modified.push(file);
292
+ } else if (status === 'A ' || status === 'AM') {
293
+ added.push(file);
294
+ } else if (status === 'D ' || status === ' D') {
295
+ deleted.push(file);
296
+ } else if (status === '??') {
297
+ untracked.push(file);
298
+ }
299
+ });
300
+
301
+ res.json({
302
+ branch,
303
+ hasCommits,
304
+ modified,
305
+ added,
306
+ deleted,
307
+ untracked
308
+ });
309
+ } catch (error) {
310
+ const isRepositoryMissing = isGitRepositoryMissingError(error.message);
311
+ if (!isRepositoryMissing) {
312
+ console.error('Git status error:', error);
313
+ }
314
+ res.json({
315
+ error: isRepositoryMissing ? error.message : 'Git operation failed',
316
+ details: isRepositoryMissing ? error.message : `Failed to get git status: ${error.message}`
317
+ });
318
+ }
319
+ });
320
+
321
+ // Get diff for a specific file
322
+ router.get('/diff', async (req, res) => {
323
+ const { project, file } = req.query;
324
+
325
+ if (!project || !file) {
326
+ return res.status(400).json({ error: 'Project name and file path are required' });
327
+ }
328
+
329
+ try {
330
+ const projectPath = await getActualProjectPath(project);
331
+
332
+ // Validate git repository
333
+ await validateGitRepository(projectPath);
334
+
335
+ // Check if file is untracked or deleted
336
+ const statusOutput = await getFileStatusOutput(projectPath, file);
337
+ const isUntracked = statusOutput.startsWith('??');
338
+ const isDeleted = statusOutput.trim().startsWith('D ') || statusOutput.trim().startsWith(' D');
339
+
340
+ let diff;
341
+ if (isUntracked) {
342
+ // For untracked files, show the entire file content as additions
343
+ const filePath = path.join(projectPath, file);
344
+ const stats = await fs.stat(filePath);
345
+
346
+ if (stats.isDirectory()) {
347
+ // For directories, show a simple message
348
+ diff = `Directory: ${file}\n(Cannot show diff for directories)`;
349
+ } else {
350
+ const fileContent = await fs.readFile(filePath, 'utf-8');
351
+ const lines = fileContent.split('\n');
352
+ diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` +
353
+ lines.map(line => `+${line}`).join('\n');
354
+ }
355
+ } else if (isDeleted) {
356
+ // For deleted files, show the entire file content from HEAD as deletions
357
+ const fileContent = await getHeadFileContent(projectPath, file);
358
+ const lines = fileContent.split('\n');
359
+ diff = `--- a/${file}\n+++ /dev/null\n@@ -1,${lines.length} +0,0 @@\n` +
360
+ lines.map(line => `-${line}`).join('\n');
361
+ } else {
362
+ // Get diff for tracked files
363
+ // First check for unstaged changes (working tree vs index)
364
+ const unstagedDiff = await getUnstagedDiff(projectPath, file);
365
+
366
+ if (unstagedDiff) {
367
+ // Show unstaged changes if they exist
368
+ diff = stripDiffHeaders(unstagedDiff);
369
+ } else {
370
+ // If no unstaged changes, check for staged changes (index vs HEAD)
371
+ const stagedDiff = await getStagedDiff(projectPath, file);
372
+ diff = stripDiffHeaders(stagedDiff) || '';
373
+ }
374
+ }
375
+
376
+ res.json({ diff });
377
+ } catch (error) {
378
+ console.error('Git diff error:', error);
379
+ res.json({ error: error.message });
380
+ }
381
+ });
382
+
383
+ // Get file content with diff information for CodeEditor
384
+ router.get('/file-with-diff', async (req, res) => {
385
+ const { project, file } = req.query;
386
+
387
+ if (!project || !file) {
388
+ return res.status(400).json({ error: 'Project name and file path are required' });
389
+ }
390
+
391
+ try {
392
+ const projectPath = await getActualProjectPath(project);
393
+
394
+ // Validate git repository
395
+ await validateGitRepository(projectPath);
396
+
397
+ // Check file status
398
+ const statusOutput = await getFileStatusOutput(projectPath, file);
399
+ const isUntracked = statusOutput.startsWith('??');
400
+ const isDeleted = statusOutput.trim().startsWith('D ') || statusOutput.trim().startsWith(' D');
401
+
402
+ let currentContent = '';
403
+ let oldContent = '';
404
+
405
+ if (isDeleted) {
406
+ // For deleted files, get content from HEAD
407
+ const headContent = await getHeadFileContent(projectPath, file);
408
+ oldContent = headContent;
409
+ currentContent = headContent; // Show the deleted content in editor
410
+ } else {
411
+ // Get current file content
412
+ const filePath = path.join(projectPath, file);
413
+ const stats = await fs.stat(filePath);
414
+
415
+ if (stats.isDirectory()) {
416
+ // Cannot show content for directories
417
+ return res.status(400).json({ error: 'Cannot show diff for directories' });
418
+ }
419
+
420
+ currentContent = await fs.readFile(filePath, 'utf-8');
421
+
422
+ if (!isUntracked) {
423
+ // Get the old content from HEAD for tracked files
424
+ try {
425
+ const headContent = await getHeadFileContent(projectPath, file);
426
+ oldContent = headContent;
427
+ } catch (error) {
428
+ // File might be newly added to git (staged but not committed)
429
+ oldContent = '';
430
+ }
431
+ }
432
+ }
433
+
434
+ res.json({
435
+ currentContent,
436
+ oldContent,
437
+ isDeleted,
438
+ isUntracked
439
+ });
440
+ } catch (error) {
441
+ console.error('Git file-with-diff error:', error);
442
+ res.json({ error: error.message });
443
+ }
444
+ });
445
+
446
+ // Initialize git repository for a project
447
+ router.post('/init', async (req, res) => {
448
+ const { project } = req.body;
449
+
450
+ if (!project) {
451
+ return res.status(400).json({ error: 'Project name is required' });
452
+ }
453
+
454
+ try {
455
+ const projectPath = await getActualProjectPath(project);
456
+
457
+ try {
458
+ await validateGitRepository(projectPath);
459
+ return res.status(400).json({ error: 'Git is already enabled for this project.' });
460
+ } catch (error) {
461
+ if (!isGitRepositoryMissingError(error.message)) {
462
+ throw error;
463
+ }
464
+ }
465
+
466
+ await initializeGitRepository(projectPath);
467
+
468
+ res.json({
469
+ success: true,
470
+ message: 'Git has been enabled for this project.'
471
+ });
472
+ } catch (error) {
473
+ console.error('Git init error:', error);
474
+ res.status(500).json({ error: error.message || 'Failed to initialize git repository' });
475
+ }
476
+ });
477
+
478
+ // Create initial commit
479
+ router.post('/initial-commit', async (req, res) => {
480
+ const { project } = req.body;
481
+
482
+ if (!project) {
483
+ return res.status(400).json({ error: 'Project name is required' });
484
+ }
485
+
486
+ try {
487
+ const projectPath = await getActualProjectPath(project);
488
+
489
+ // Validate git repository
490
+ await validateGitRepository(projectPath);
491
+
492
+ // Check if there are already commits
493
+ if (await repositoryHasCommits(projectPath)) {
494
+ return res.status(400).json({ error: 'Repository already has commits. Use regular commit instead.' });
495
+ }
496
+
497
+ // Add all files
498
+ await spawnAsync('git', ['add', '.'], { cwd: projectPath });
499
+
500
+ // Create initial commit
501
+ const stdout = await commitFiles(projectPath, 'Initial commit');
502
+
503
+ res.json({ success: true, output: stdout, message: 'Initial commit created successfully' });
504
+ } catch (error) {
505
+ console.error('Git initial commit error:', error);
506
+
507
+ // Handle the case where there's nothing to commit
508
+ const output = String(error.stdout || '') + String(error.stderr || '');
509
+ if (output.toLowerCase().includes('nothing to commit')) {
510
+ return res.status(400).json({
511
+ error: 'Nothing to commit',
512
+ details: 'No files found in the repository. Add some files first.'
513
+ });
514
+ }
515
+
516
+ res.status(500).json({ error: error.message });
517
+ }
518
+ });
519
+
520
+ // Commit changes
521
+ router.post('/commit', async (req, res) => {
522
+ const { project, message, files } = req.body;
523
+
524
+ if (!project || !message || !files || files.length === 0) {
525
+ return res.status(400).json({ error: 'Project name, commit message, and files are required' });
526
+ }
527
+
528
+ try {
529
+ const projectPath = await getActualProjectPath(project);
530
+
531
+ // Validate git repository
532
+ await validateGitRepository(projectPath);
533
+
534
+ // Stage selected files
535
+ for (const file of files) {
536
+ await spawnAsync('git', ['add', '--', file], { cwd: projectPath });
537
+ }
538
+
539
+ // Commit with message
540
+ const stdout = await commitFiles(projectPath, message);
541
+
542
+ res.json({ success: true, output: stdout });
543
+ } catch (error) {
544
+ console.error('Git commit error:', error);
545
+ res.status(500).json({ error: error.message });
546
+ }
547
+ });
548
+
549
+ // Get list of branches
550
+ router.get('/branches', async (req, res) => {
551
+ const { project } = req.query;
552
+
553
+ if (!project) {
554
+ return res.status(400).json({ error: 'Project name is required' });
555
+ }
556
+
557
+ try {
558
+ const projectPath = await getActualProjectPath(project);
559
+
560
+ // Validate git repository
561
+ await validateGitRepository(projectPath);
562
+
563
+ if (!(await repositoryHasCommits(projectPath))) {
564
+ return res.json({ branches: [await getCurrentBranchName(projectPath, 'main')] });
565
+ }
566
+
567
+ // Get all branches
568
+ const { stdout } = await execAsync('git branch -a', { cwd: projectPath });
569
+
570
+ // Parse branches
571
+ const branches = stdout
572
+ .split('\n')
573
+ .map(branch => branch.trim())
574
+ .filter(branch => branch && !branch.includes('->')) // Remove empty lines and HEAD pointer
575
+ .map(branch => {
576
+ // Remove asterisk from current branch
577
+ if (branch.startsWith('* ')) {
578
+ return branch.substring(2);
579
+ }
580
+ // Remove remotes/ prefix
581
+ if (branch.startsWith('remotes/origin/')) {
582
+ return branch.substring(15);
583
+ }
584
+ return branch;
585
+ })
586
+ .filter((branch, index, self) => self.indexOf(branch) === index); // Remove duplicates
587
+
588
+ res.json({ branches });
589
+ } catch (error) {
590
+ if (!isGitRepositoryMissingError(error.message)) {
591
+ console.error('Git branches error:', error);
592
+ }
593
+ res.json({ error: error.message });
594
+ }
595
+ });
596
+
597
+ // Checkout branch
598
+ router.post('/checkout', async (req, res) => {
599
+ const { project, branch } = req.body;
600
+
601
+ if (!project || !branch) {
602
+ return res.status(400).json({ error: 'Project name and branch are required' });
603
+ }
604
+
605
+ try {
606
+ const projectPath = await getActualProjectPath(project);
607
+
608
+ // Checkout the branch
609
+ const stdout = await checkoutBranch(projectPath, branch);
610
+
611
+ res.json({ success: true, output: stdout });
612
+ } catch (error) {
613
+ console.error('Git checkout error:', error);
614
+ res.status(500).json({ error: error.message });
615
+ }
616
+ });
617
+
618
+ // Create new branch
619
+ router.post('/create-branch', async (req, res) => {
620
+ const { project, branch } = req.body;
621
+
622
+ if (!project || !branch) {
623
+ return res.status(400).json({ error: 'Project name and branch name are required' });
624
+ }
625
+
626
+ try {
627
+ const projectPath = await getActualProjectPath(project);
628
+
629
+ // Create and checkout new branch
630
+ const stdout = await createBranchAndCheckout(projectPath, branch);
631
+
632
+ res.json({ success: true, output: stdout });
633
+ } catch (error) {
634
+ console.error('Git create branch error:', error);
635
+ res.status(500).json({ error: error.message });
636
+ }
637
+ });
638
+
639
+ // Get recent commits
640
+ router.get('/commits', async (req, res) => {
641
+ const { project, limit = 10 } = req.query;
642
+
643
+ if (!project) {
644
+ return res.status(400).json({ error: 'Project name is required' });
645
+ }
646
+
647
+ try {
648
+ const projectPath = await getActualProjectPath(project);
649
+ await validateGitRepository(projectPath);
650
+ const parsedLimit = Number.parseInt(String(limit), 10);
651
+ const safeLimit = Number.isFinite(parsedLimit) && parsedLimit > 0
652
+ ? Math.min(parsedLimit, 100)
653
+ : 10;
654
+
655
+ // Get commit log with stats
656
+ const { stdout } = await spawnAsync(
657
+ 'git',
658
+ ['log', '--pretty=format:%H|%an|%ae|%ad|%s', '--date=relative', '-n', String(safeLimit)],
659
+ { cwd: projectPath },
660
+ );
661
+
662
+ const commits = stdout
663
+ .split('\n')
664
+ .filter(line => line.trim())
665
+ .map(line => {
666
+ const [hash, author, email, date, ...messageParts] = line.split('|');
667
+ return {
668
+ hash,
669
+ author,
670
+ email,
671
+ date,
672
+ message: messageParts.join('|')
673
+ };
674
+ });
675
+
676
+ // Get stats for each commit
677
+ for (const commit of commits) {
678
+ try {
679
+ const { stdout: stats } = await execAsync(
680
+ `git show --stat --format='' ${commit.hash}`,
681
+ { cwd: projectPath }
682
+ );
683
+ commit.stats = stats.trim().split('\n').pop(); // Get the summary line
684
+ } catch (error) {
685
+ commit.stats = '';
686
+ }
687
+ }
688
+
689
+ res.json({ commits });
690
+ } catch (error) {
691
+ console.error('Git commits error:', error);
692
+ res.json({ error: error.message });
693
+ }
694
+ });
695
+
696
+ // Get diff for a specific commit
697
+ router.get('/commit-diff', async (req, res) => {
698
+ const { project, commit } = req.query;
699
+
700
+ if (!project || !commit) {
701
+ return res.status(400).json({ error: 'Project name and commit hash are required' });
702
+ }
703
+
704
+ try {
705
+ const projectPath = await getActualProjectPath(project);
706
+
707
+ // Get diff for the commit
708
+ const { stdout } = await execAsync(
709
+ `git show ${commit}`,
710
+ { cwd: projectPath }
711
+ );
712
+
713
+ res.json({ diff: stdout });
714
+ } catch (error) {
715
+ console.error('Git commit diff error:', error);
716
+ res.json({ error: error.message });
717
+ }
718
+ });
719
+
720
+ // Generate commit message based on staged changes using AI
721
+ router.post('/generate-commit-message', async (req, res) => {
722
+ const { project, files, provider = 'claude' } = req.body;
723
+
724
+ if (!project || !files || files.length === 0) {
725
+ return res.status(400).json({ error: 'Project name and files are required' });
726
+ }
727
+
728
+ // Validate provider
729
+ if (!['claude', 'cursor'].includes(provider)) {
730
+ return res.status(400).json({ error: 'provider must be "claude" or "cursor"' });
731
+ }
732
+
733
+ try {
734
+ const projectPath = await getActualProjectPath(project);
735
+
736
+ // Get diff for selected files
737
+ let diffContext = '';
738
+ for (const file of files) {
739
+ try {
740
+ const diffOutput = await getDiffAgainstHead(projectPath, file);
741
+ if (diffOutput) {
742
+ diffContext += `\n--- ${file} ---\n${diffOutput}`;
743
+ }
744
+ } catch (error) {
745
+ console.error(`Error getting diff for ${file}:`, error);
746
+ }
747
+ }
748
+
749
+ // If no diff found, might be untracked files
750
+ if (!diffContext.trim()) {
751
+ // Try to get content of untracked files
752
+ for (const file of files) {
753
+ try {
754
+ const filePath = path.join(projectPath, file);
755
+ const stats = await fs.stat(filePath);
756
+
757
+ if (!stats.isDirectory()) {
758
+ const content = await fs.readFile(filePath, 'utf-8');
759
+ diffContext += `\n--- ${file} (new file) ---\n${content.substring(0, 1000)}\n`;
760
+ } else {
761
+ diffContext += `\n--- ${file} (new directory) ---\n`;
762
+ }
763
+ } catch (error) {
764
+ console.error(`Error reading file ${file}:`, error);
765
+ }
766
+ }
767
+ }
768
+
769
+ // Generate commit message using AI
770
+ const message = await generateCommitMessageWithAI(files, diffContext, provider, projectPath);
771
+
772
+ res.json({ message });
773
+ } catch (error) {
774
+ console.error('Generate commit message error:', error);
775
+ res.status(500).json({ error: error.message });
776
+ }
777
+ });
778
+
779
+ /**
780
+ * Generates a commit message using AI (Claude SDK or Cursor CLI)
781
+ * @param {Array<string>} files - List of changed files
782
+ * @param {string} diffContext - Git diff content
783
+ * @param {string} provider - 'claude' or 'cursor'
784
+ * @param {string} projectPath - Project directory path
785
+ * @returns {Promise<string>} Generated commit message
786
+ */
787
+ async function generateCommitMessageWithAI(files, diffContext, provider, projectPath) {
788
+ // Create the prompt
789
+ const prompt = `Generate a conventional commit message for these changes.
790
+
791
+ REQUIREMENTS:
792
+ - Format: type(scope): subject
793
+ - Include body explaining what changed and why
794
+ - Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
795
+ - Subject under 50 chars, body wrapped at 72 chars
796
+ - Focus on user-facing changes, not implementation details
797
+ - Consider what's being added AND removed
798
+ - Return ONLY the commit message (no markdown, explanations, or code blocks)
799
+
800
+ FILES CHANGED:
801
+ ${files.map(f => `- ${f}`).join('\n')}
802
+
803
+ DIFFS:
804
+ ${diffContext.substring(0, 4000)}
805
+
806
+ Generate the commit message:`;
807
+
808
+ try {
809
+ // Create a simple writer that collects the response
810
+ let responseText = '';
811
+ const writer = {
812
+ send: (data) => {
813
+ try {
814
+ const parsed = typeof data === 'string' ? JSON.parse(data) : data;
815
+ console.log('🔍 Writer received message type:', parsed.type);
816
+
817
+ // Handle different message formats from Claude SDK and Cursor CLI
818
+ // Claude SDK sends: {type: 'claude-response', data: {message: {content: [...]}}}
819
+ if (parsed.type === 'claude-response' && parsed.data) {
820
+ const message = parsed.data.message || parsed.data;
821
+ console.log('📦 Claude response message:', JSON.stringify(message, null, 2).substring(0, 500));
822
+ if (message.content && Array.isArray(message.content)) {
823
+ // Extract text from content array
824
+ for (const item of message.content) {
825
+ if (item.type === 'text' && item.text) {
826
+ console.log('✅ Extracted text chunk:', item.text.substring(0, 100));
827
+ responseText += item.text;
828
+ }
829
+ }
830
+ }
831
+ }
832
+ // Cursor CLI sends: {type: 'cursor-output', output: '...'}
833
+ else if (parsed.type === 'cursor-output' && parsed.output) {
834
+ console.log('✅ Cursor output:', parsed.output.substring(0, 100));
835
+ responseText += parsed.output;
836
+ }
837
+ // Also handle direct text messages
838
+ else if (parsed.type === 'text' && parsed.text) {
839
+ console.log('✅ Direct text:', parsed.text.substring(0, 100));
840
+ responseText += parsed.text;
841
+ }
842
+ } catch (e) {
843
+ // Ignore parse errors
844
+ console.error('Error parsing writer data:', e);
845
+ }
846
+ },
847
+ setSessionId: () => {}, // No-op for this use case
848
+ };
849
+
850
+ console.log('🚀 Calling AI agent with provider:', provider);
851
+ console.log('📝 Prompt length:', prompt.length);
852
+
853
+ // Call the appropriate agent
854
+ if (provider === 'claude') {
855
+ await queryClaudeSDK(prompt, {
856
+ cwd: projectPath,
857
+ permissionMode: 'bypassPermissions',
858
+ model: 'sonnet'
859
+ }, writer);
860
+ } else if (provider === 'cursor') {
861
+ await spawnCursor(prompt, {
862
+ cwd: projectPath,
863
+ skipPermissions: true
864
+ }, writer);
865
+ }
866
+
867
+ console.log('📊 Total response text collected:', responseText.length, 'characters');
868
+ console.log('📄 Response preview:', responseText.substring(0, 200));
869
+
870
+ // Clean up the response
871
+ const cleanedMessage = cleanCommitMessage(responseText);
872
+ console.log('🧹 Cleaned message:', cleanedMessage.substring(0, 200));
873
+
874
+ return cleanedMessage || 'chore: update files';
875
+ } catch (error) {
876
+ console.error('Error generating commit message with AI:', error);
877
+ // Fallback to simple message
878
+ return `chore: update ${files.length} file${files.length !== 1 ? 's' : ''}`;
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Cleans the AI-generated commit message by removing markdown, code blocks, and extra formatting
884
+ * @param {string} text - Raw AI response
885
+ * @returns {string} Clean commit message
886
+ */
887
+ function cleanCommitMessage(text) {
888
+ if (!text || !text.trim()) {
889
+ return '';
890
+ }
891
+
892
+ let cleaned = text.trim();
893
+
894
+ // Remove markdown code blocks
895
+ cleaned = cleaned.replace(/```[a-z]*\n/g, '');
896
+ cleaned = cleaned.replace(/```/g, '');
897
+
898
+ // Remove markdown headers
899
+ cleaned = cleaned.replace(/^#+\s*/gm, '');
900
+
901
+ // Remove leading/trailing quotes
902
+ cleaned = cleaned.replace(/^["']|["']$/g, '');
903
+
904
+ // If there are multiple lines, take everything (subject + body)
905
+ // Just clean up extra blank lines
906
+ cleaned = cleaned.replace(/\n{3,}/g, '\n\n');
907
+
908
+ // Remove any explanatory text before the actual commit message
909
+ // Look for conventional commit pattern and start from there
910
+ const conventionalCommitMatch = cleaned.match(/(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\(.+?\))?:.+/s);
911
+ if (conventionalCommitMatch) {
912
+ cleaned = cleaned.substring(cleaned.indexOf(conventionalCommitMatch[0]));
913
+ }
914
+
915
+ return cleaned.trim();
916
+ }
917
+
918
+ // Get remote status (ahead/behind commits with smart remote detection)
919
+ router.get('/remote-status', async (req, res) => {
920
+ const { project } = req.query;
921
+
922
+ if (!project) {
923
+ return res.status(400).json({ error: 'Project name is required' });
924
+ }
925
+
926
+ try {
927
+ const projectPath = await getActualProjectPath(project);
928
+ await validateGitRepository(projectPath);
929
+
930
+ const branch = await getCurrentBranchName(projectPath, 'main');
931
+
932
+ if (!(await repositoryHasCommits(projectPath))) {
933
+ let remoteName = null;
934
+ try {
935
+ const remotes = await listGitRemotes(projectPath);
936
+ remoteName = remotes.includes('origin') ? 'origin' : (remotes[0] || null);
937
+ } catch {
938
+ remoteName = null;
939
+ }
940
+
941
+ return res.json({
942
+ hasRemote: Boolean(remoteName),
943
+ hasUpstream: false,
944
+ branch,
945
+ remoteName,
946
+ hasCommits: false,
947
+ message: 'No commits yet'
948
+ });
949
+ }
950
+
951
+ // Check if there's a remote tracking branch (smart detection)
952
+ let trackingBranch;
953
+ let remoteName;
954
+ try {
955
+ trackingBranch = await getUpstreamBranch(projectPath, branch);
956
+ remoteName = trackingBranch.split('/')[0]; // Extract remote name (e.g., "origin/main" -> "origin")
957
+ } catch (error) {
958
+ // No upstream branch configured - but check if we have remotes
959
+ let hasRemote = false;
960
+ let remoteName = null;
961
+ try {
962
+ const remotes = await listGitRemotes(projectPath);
963
+ if (remotes.length > 0) {
964
+ hasRemote = true;
965
+ remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
966
+ }
967
+ } catch (remoteError) {
968
+ // No remotes configured
969
+ }
970
+
971
+ return res.json({
972
+ hasRemote,
973
+ hasUpstream: false,
974
+ branch,
975
+ remoteName,
976
+ message: 'No remote tracking branch configured'
977
+ });
978
+ }
979
+
980
+ // Get ahead/behind counts
981
+ const countOutput = await getRemoteTrackingCounts(projectPath, trackingBranch);
982
+
983
+ const [behind, ahead] = countOutput.trim().split('\t').map(Number);
984
+
985
+ res.json({
986
+ hasRemote: true,
987
+ hasUpstream: true,
988
+ branch,
989
+ remoteBranch: trackingBranch,
990
+ remoteName,
991
+ ahead: ahead || 0,
992
+ behind: behind || 0,
993
+ isUpToDate: ahead === 0 && behind === 0
994
+ });
995
+ } catch (error) {
996
+ if (!isGitRepositoryMissingError(error.message)) {
997
+ console.error('Git remote status error:', error);
998
+ }
999
+ res.json({ error: error.message });
1000
+ }
1001
+ });
1002
+
1003
+ // Fetch from remote (using smart remote detection)
1004
+ router.post('/fetch', async (req, res) => {
1005
+ const { project } = req.body;
1006
+
1007
+ if (!project) {
1008
+ return res.status(400).json({ error: 'Project name is required' });
1009
+ }
1010
+
1011
+ try {
1012
+ const projectPath = await getActualProjectPath(project);
1013
+ await validateGitRepository(projectPath);
1014
+
1015
+ // Get current branch and its upstream remote
1016
+ const branch = await getCurrentHeadBranch(projectPath);
1017
+
1018
+ let remoteName = 'origin'; // fallback
1019
+ try {
1020
+ remoteName = (await getUpstreamBranch(projectPath, branch)).split('/')[0]; // Extract remote name
1021
+ } catch (error) {
1022
+ // No upstream, try to fetch from origin anyway
1023
+ console.log('No upstream configured, using origin as fallback');
1024
+ }
1025
+
1026
+ const stdout = await fetchRemote(projectPath, remoteName);
1027
+
1028
+ res.json({ success: true, output: stdout || 'Fetch completed successfully', remoteName });
1029
+ } catch (error) {
1030
+ console.error('Git fetch error:', error);
1031
+ const errorText = getGitErrorText(error);
1032
+ res.status(500).json({
1033
+ error: 'Fetch failed',
1034
+ details: errorText.includes('Could not resolve hostname')
1035
+ ? 'Unable to connect to remote repository. Check your internet connection.'
1036
+ : errorText.includes('fatal: \'origin\' does not appear to be a git repository')
1037
+ ? 'No remote repository configured. Add a remote with: git remote add origin <url>'
1038
+ : errorText
1039
+ });
1040
+ }
1041
+ });
1042
+
1043
+ // Pull from remote (fetch + merge using smart remote detection)
1044
+ router.post('/pull', async (req, res) => {
1045
+ const { project } = req.body;
1046
+
1047
+ if (!project) {
1048
+ return res.status(400).json({ error: 'Project name is required' });
1049
+ }
1050
+
1051
+ try {
1052
+ const projectPath = await getActualProjectPath(project);
1053
+ await validateGitRepository(projectPath);
1054
+
1055
+ // Get current branch and its upstream remote
1056
+ const branch = await getCurrentHeadBranch(projectPath);
1057
+
1058
+ let remoteName = 'origin'; // fallback
1059
+ let remoteBranch = branch; // fallback
1060
+ try {
1061
+ const tracking = await getUpstreamBranch(projectPath, branch);
1062
+ remoteName = tracking.split('/')[0]; // Extract remote name
1063
+ remoteBranch = tracking.split('/').slice(1).join('/'); // Extract branch name
1064
+ } catch (error) {
1065
+ // No upstream, use fallback
1066
+ console.log('No upstream configured, using origin/branch as fallback');
1067
+ }
1068
+
1069
+ const stdout = await pullRemoteBranch(projectPath, remoteName, remoteBranch);
1070
+
1071
+ res.json({
1072
+ success: true,
1073
+ output: stdout || 'Pull completed successfully',
1074
+ remoteName,
1075
+ remoteBranch
1076
+ });
1077
+ } catch (error) {
1078
+ console.error('Git pull error:', error);
1079
+ const errorText = getGitErrorText(error);
1080
+
1081
+ // Enhanced error handling for common pull scenarios
1082
+ let errorMessage = 'Pull failed';
1083
+ let details = errorText;
1084
+
1085
+ if (errorText.includes('CONFLICT')) {
1086
+ errorMessage = 'Merge conflicts detected';
1087
+ details = 'Pull created merge conflicts. Please resolve conflicts manually in the editor, then commit the changes.';
1088
+ } else if (errorText.includes('Please commit your changes or stash them')) {
1089
+ errorMessage = 'Uncommitted changes detected';
1090
+ details = 'Please commit or stash your local changes before pulling.';
1091
+ } else if (errorText.includes('Could not resolve hostname')) {
1092
+ errorMessage = 'Network error';
1093
+ details = 'Unable to connect to remote repository. Check your internet connection.';
1094
+ } else if (errorText.includes('fatal: \'origin\' does not appear to be a git repository')) {
1095
+ errorMessage = 'Remote not configured';
1096
+ details = 'No remote repository configured. Add a remote with: git remote add origin <url>';
1097
+ } else if (errorText.includes('diverged')) {
1098
+ errorMessage = 'Branches have diverged';
1099
+ details = 'Your local branch and remote branch have diverged. Consider fetching first to review changes.';
1100
+ }
1101
+
1102
+ res.status(500).json({
1103
+ error: errorMessage,
1104
+ details: details
1105
+ });
1106
+ }
1107
+ });
1108
+
1109
+ // Push commits to remote repository
1110
+ router.post('/push', async (req, res) => {
1111
+ const { project } = req.body;
1112
+
1113
+ if (!project) {
1114
+ return res.status(400).json({ error: 'Project name is required' });
1115
+ }
1116
+
1117
+ try {
1118
+ const projectPath = await getActualProjectPath(project);
1119
+ await validateGitRepository(projectPath);
1120
+
1121
+ // Get current branch and its upstream remote
1122
+ const branch = await getCurrentHeadBranch(projectPath);
1123
+
1124
+ let remoteName = 'origin'; // fallback
1125
+ let remoteBranch = branch; // fallback
1126
+ try {
1127
+ const tracking = await getUpstreamBranch(projectPath, branch);
1128
+ remoteName = tracking.split('/')[0]; // Extract remote name
1129
+ remoteBranch = tracking.split('/').slice(1).join('/'); // Extract branch name
1130
+ } catch (error) {
1131
+ // No upstream, use fallback
1132
+ console.log('No upstream configured, using origin/branch as fallback');
1133
+ }
1134
+
1135
+ const stdout = await pushRemoteBranch(projectPath, remoteName, remoteBranch);
1136
+
1137
+ res.json({
1138
+ success: true,
1139
+ output: stdout || 'Push completed successfully',
1140
+ remoteName,
1141
+ remoteBranch
1142
+ });
1143
+ } catch (error) {
1144
+ console.error('Git push error:', error);
1145
+ const errorText = getGitErrorText(error);
1146
+
1147
+ // Enhanced error handling for common push scenarios
1148
+ let errorMessage = 'Push failed';
1149
+ let details = errorText;
1150
+
1151
+ if (errorText.includes('rejected')) {
1152
+ errorMessage = 'Push rejected';
1153
+ details = 'The remote has newer commits. Pull first to merge changes before pushing.';
1154
+ } else if (errorText.includes('non-fast-forward')) {
1155
+ errorMessage = 'Non-fast-forward push';
1156
+ details = 'Your branch is behind the remote. Pull the latest changes first.';
1157
+ } else if (errorText.includes('Could not resolve hostname')) {
1158
+ errorMessage = 'Network error';
1159
+ details = 'Unable to connect to remote repository. Check your internet connection.';
1160
+ } else if (errorText.includes('fatal: \'origin\' does not appear to be a git repository')) {
1161
+ errorMessage = 'Remote not configured';
1162
+ details = 'No remote repository configured. Add a remote with: git remote add origin <url>';
1163
+ } else if (errorText.includes('Permission denied')) {
1164
+ errorMessage = 'Authentication failed';
1165
+ details = 'Permission denied. Check your credentials or SSH keys.';
1166
+ } else if (errorText.includes('no upstream branch')) {
1167
+ errorMessage = 'No upstream branch';
1168
+ details = 'No upstream branch configured. Use: git push --set-upstream origin <branch>';
1169
+ } else if (errorText.includes('src refspec') && errorText.includes('does not match any')) {
1170
+ errorMessage = 'No commits to push';
1171
+ details = 'Create an initial commit before publishing or pushing this branch.';
1172
+ }
1173
+
1174
+ res.status(500).json({
1175
+ error: errorMessage,
1176
+ details: details
1177
+ });
1178
+ }
1179
+ });
1180
+
1181
+ // Publish branch to remote (set upstream and push)
1182
+ router.post('/publish', async (req, res) => {
1183
+ const { project, branch } = req.body;
1184
+
1185
+ if (!project || !branch) {
1186
+ return res.status(400).json({ error: 'Project name and branch are required' });
1187
+ }
1188
+
1189
+ try {
1190
+ const projectPath = await getActualProjectPath(project);
1191
+ await validateGitRepository(projectPath);
1192
+
1193
+ // Get current branch to verify it matches the requested branch
1194
+ const currentBranchName = await getCurrentHeadBranch(projectPath);
1195
+
1196
+ if (currentBranchName !== branch) {
1197
+ return res.status(400).json({
1198
+ error: `Branch mismatch. Current branch is ${currentBranchName}, but trying to publish ${branch}`
1199
+ });
1200
+ }
1201
+
1202
+ // Check if remote exists
1203
+ let remoteName = 'origin';
1204
+ try {
1205
+ const remotes = await listGitRemotes(projectPath);
1206
+ if (remotes.length === 0) {
1207
+ return res.status(400).json({
1208
+ error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
1209
+ });
1210
+ }
1211
+ remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
1212
+ } catch (error) {
1213
+ return res.status(400).json({
1214
+ error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
1215
+ });
1216
+ }
1217
+
1218
+ // Publish the branch (set upstream and push)
1219
+ const stdout = await pushWithUpstream(projectPath, remoteName, branch);
1220
+
1221
+ res.json({
1222
+ success: true,
1223
+ output: stdout || 'Branch published successfully',
1224
+ remoteName,
1225
+ branch
1226
+ });
1227
+ } catch (error) {
1228
+ console.error('Git publish error:', error);
1229
+ const errorText = getGitErrorText(error);
1230
+
1231
+ // Enhanced error handling for common publish scenarios
1232
+ let errorMessage = 'Publish failed';
1233
+ let details = errorText;
1234
+
1235
+ if (errorText.includes('rejected')) {
1236
+ errorMessage = 'Publish rejected';
1237
+ details = 'The remote branch already exists and has different commits. Use push instead.';
1238
+ } else if (errorText.includes('Could not resolve hostname')) {
1239
+ errorMessage = 'Network error';
1240
+ details = 'Unable to connect to remote repository. Check your internet connection.';
1241
+ } else if (errorText.includes('Permission denied')) {
1242
+ errorMessage = 'Authentication failed';
1243
+ details = 'Permission denied. Check your credentials or SSH keys.';
1244
+ } else if (errorText.includes('fatal:') && errorText.includes('does not appear to be a git repository')) {
1245
+ errorMessage = 'Remote not configured';
1246
+ details = 'Remote repository not properly configured. Check your remote URL.';
1247
+ } else if (errorText.includes('src refspec') && errorText.includes('does not match any')) {
1248
+ errorMessage = 'No commits to publish';
1249
+ details = 'Create an initial commit before publishing this branch.';
1250
+ }
1251
+
1252
+ res.status(500).json({
1253
+ error: errorMessage,
1254
+ details: details
1255
+ });
1256
+ }
1257
+ });
1258
+
1259
+ // Discard changes for a specific file
1260
+ router.post('/discard', async (req, res) => {
1261
+ const { project, file } = req.body;
1262
+
1263
+ if (!project || !file) {
1264
+ return res.status(400).json({ error: 'Project name and file path are required' });
1265
+ }
1266
+
1267
+ try {
1268
+ const projectPath = await getActualProjectPath(project);
1269
+ await validateGitRepository(projectPath);
1270
+
1271
+ // Check file status to determine correct discard command
1272
+ const statusOutput = await getFileStatusOutput(projectPath, file);
1273
+
1274
+ if (!statusOutput.trim()) {
1275
+ return res.status(400).json({ error: 'No changes to discard for this file' });
1276
+ }
1277
+
1278
+ const status = statusOutput.substring(0, 2);
1279
+
1280
+ if (status === '??') {
1281
+ // Untracked file or directory - delete it
1282
+ const filePath = path.join(projectPath, file);
1283
+ const stats = await fs.stat(filePath);
1284
+
1285
+ if (stats.isDirectory()) {
1286
+ await fs.rm(filePath, { recursive: true, force: true });
1287
+ } else {
1288
+ await fs.unlink(filePath);
1289
+ }
1290
+ } else if (status.includes('M') || status.includes('D')) {
1291
+ // Modified or deleted file - restore from HEAD
1292
+ await spawnAsync('git', ['restore', '--', file], { cwd: projectPath });
1293
+ } else if (status.includes('A')) {
1294
+ // Added file - unstage it
1295
+ await spawnAsync('git', ['reset', 'HEAD', '--', file], { cwd: projectPath });
1296
+ }
1297
+
1298
+ res.json({ success: true, message: `Changes discarded for ${file}` });
1299
+ } catch (error) {
1300
+ console.error('Git discard error:', error);
1301
+ res.status(500).json({ error: error.message });
1302
+ }
1303
+ });
1304
+
1305
+ // Delete untracked file
1306
+ router.post('/delete-untracked', async (req, res) => {
1307
+ const { project, file } = req.body;
1308
+
1309
+ if (!project || !file) {
1310
+ return res.status(400).json({ error: 'Project name and file path are required' });
1311
+ }
1312
+
1313
+ try {
1314
+ const projectPath = await getActualProjectPath(project);
1315
+ await validateGitRepository(projectPath);
1316
+
1317
+ // Check if file is actually untracked
1318
+ const statusOutput = await getFileStatusOutput(projectPath, file);
1319
+
1320
+ if (!statusOutput.trim()) {
1321
+ return res.status(400).json({ error: 'File is not untracked or does not exist' });
1322
+ }
1323
+
1324
+ const status = statusOutput.substring(0, 2);
1325
+
1326
+ if (status !== '??') {
1327
+ return res.status(400).json({ error: 'File is not untracked. Use discard for tracked files.' });
1328
+ }
1329
+
1330
+ // Delete the untracked file or directory
1331
+ const filePath = path.join(projectPath, file);
1332
+ const stats = await fs.stat(filePath);
1333
+
1334
+ if (stats.isDirectory()) {
1335
+ // Use rm with recursive option for directories
1336
+ await fs.rm(filePath, { recursive: true, force: true });
1337
+ res.json({ success: true, message: `Untracked directory ${file} deleted successfully` });
1338
+ } else {
1339
+ await fs.unlink(filePath);
1340
+ res.json({ success: true, message: `Untracked file ${file} deleted successfully` });
1341
+ }
1342
+ } catch (error) {
1343
+ console.error('Git delete untracked error:', error);
1344
+ res.status(500).json({ error: error.message });
1345
+ }
1346
+ });
1347
+
1348
+ export default router;