mstro-app 0.4.3 → 0.4.4

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 (306) hide show
  1. package/dist/server/cli/headless/claude-invoker-process.d.ts +11 -0
  2. package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -0
  3. package/dist/server/cli/headless/claude-invoker-process.js +140 -0
  4. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -0
  5. package/dist/server/cli/headless/claude-invoker-stall.d.ts +40 -0
  6. package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -0
  7. package/dist/server/cli/headless/claude-invoker-stall.js +98 -0
  8. package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -0
  9. package/dist/server/cli/headless/claude-invoker-stream.d.ts +44 -0
  10. package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -0
  11. package/dist/server/cli/headless/claude-invoker-stream.js +276 -0
  12. package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -0
  13. package/dist/server/cli/headless/claude-invoker-tools.d.ts +21 -0
  14. package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -0
  15. package/dist/server/cli/headless/claude-invoker-tools.js +137 -0
  16. package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -0
  17. package/dist/server/cli/headless/claude-invoker.d.ts +6 -4
  18. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  19. package/dist/server/cli/headless/claude-invoker.js +10 -807
  20. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  21. package/dist/server/cli/headless/haiku-assessments.d.ts +62 -0
  22. package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -0
  23. package/dist/server/cli/headless/haiku-assessments.js +281 -0
  24. package/dist/server/cli/headless/haiku-assessments.js.map +1 -0
  25. package/dist/server/cli/headless/headless-logger.d.ts +3 -2
  26. package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
  27. package/dist/server/cli/headless/headless-logger.js +28 -5
  28. package/dist/server/cli/headless/headless-logger.js.map +1 -1
  29. package/dist/server/cli/headless/native-timeout-detector.d.ts +44 -0
  30. package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -0
  31. package/dist/server/cli/headless/native-timeout-detector.js +99 -0
  32. package/dist/server/cli/headless/native-timeout-detector.js.map +1 -0
  33. package/dist/server/cli/headless/stall-assessor.d.ts +2 -110
  34. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  35. package/dist/server/cli/headless/stall-assessor.js +65 -457
  36. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  37. package/dist/server/cli/improvisation-attachments.d.ts +21 -0
  38. package/dist/server/cli/improvisation-attachments.d.ts.map +1 -0
  39. package/dist/server/cli/improvisation-attachments.js +116 -0
  40. package/dist/server/cli/improvisation-attachments.js.map +1 -0
  41. package/dist/server/cli/improvisation-retry.d.ts +52 -0
  42. package/dist/server/cli/improvisation-retry.d.ts.map +1 -0
  43. package/dist/server/cli/improvisation-retry.js +434 -0
  44. package/dist/server/cli/improvisation-retry.js.map +1 -0
  45. package/dist/server/cli/improvisation-session-manager.d.ts +10 -266
  46. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  47. package/dist/server/cli/improvisation-session-manager.js +117 -1079
  48. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  49. package/dist/server/cli/improvisation-types.d.ts +86 -0
  50. package/dist/server/cli/improvisation-types.d.ts.map +1 -0
  51. package/dist/server/cli/improvisation-types.js +10 -0
  52. package/dist/server/cli/improvisation-types.js.map +1 -0
  53. package/dist/server/cli/prompt-builders.d.ts +68 -0
  54. package/dist/server/cli/prompt-builders.d.ts.map +1 -0
  55. package/dist/server/cli/prompt-builders.js +312 -0
  56. package/dist/server/cli/prompt-builders.js.map +1 -0
  57. package/dist/server/index.js +33 -212
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/mcp/bouncer-haiku.d.ts +10 -0
  60. package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -0
  61. package/dist/server/mcp/bouncer-haiku.js +152 -0
  62. package/dist/server/mcp/bouncer-haiku.js.map +1 -0
  63. package/dist/server/mcp/bouncer-integration.d.ts +3 -4
  64. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  65. package/dist/server/mcp/bouncer-integration.js +50 -196
  66. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  67. package/dist/server/mcp/security-analysis.d.ts +38 -0
  68. package/dist/server/mcp/security-analysis.d.ts.map +1 -0
  69. package/dist/server/mcp/security-analysis.js +183 -0
  70. package/dist/server/mcp/security-analysis.js.map +1 -0
  71. package/dist/server/mcp/security-audit.d.ts +1 -1
  72. package/dist/server/mcp/security-audit.d.ts.map +1 -1
  73. package/dist/server/mcp/security-patterns.d.ts +1 -25
  74. package/dist/server/mcp/security-patterns.d.ts.map +1 -1
  75. package/dist/server/mcp/security-patterns.js +55 -260
  76. package/dist/server/mcp/security-patterns.js.map +1 -1
  77. package/dist/server/server-setup.d.ts +22 -0
  78. package/dist/server/server-setup.d.ts.map +1 -0
  79. package/dist/server/server-setup.js +101 -0
  80. package/dist/server/server-setup.js.map +1 -0
  81. package/dist/server/services/file-explorer-ops.d.ts +24 -0
  82. package/dist/server/services/file-explorer-ops.d.ts.map +1 -0
  83. package/dist/server/services/file-explorer-ops.js +211 -0
  84. package/dist/server/services/file-explorer-ops.js.map +1 -0
  85. package/dist/server/services/files.d.ts +2 -85
  86. package/dist/server/services/files.d.ts.map +1 -1
  87. package/dist/server/services/files.js +7 -427
  88. package/dist/server/services/files.js.map +1 -1
  89. package/dist/server/services/plan/composer.d.ts.map +1 -1
  90. package/dist/server/services/plan/composer.js +2 -1
  91. package/dist/server/services/plan/composer.js.map +1 -1
  92. package/dist/server/services/plan/executor.d.ts.map +1 -1
  93. package/dist/server/services/plan/executor.js +3 -1
  94. package/dist/server/services/plan/executor.js.map +1 -1
  95. package/dist/server/services/plan/parser-core.d.ts +20 -0
  96. package/dist/server/services/plan/parser-core.d.ts.map +1 -0
  97. package/dist/server/services/plan/parser-core.js +350 -0
  98. package/dist/server/services/plan/parser-core.js.map +1 -0
  99. package/dist/server/services/plan/parser-migration.d.ts +5 -0
  100. package/dist/server/services/plan/parser-migration.d.ts.map +1 -0
  101. package/dist/server/services/plan/parser-migration.js +124 -0
  102. package/dist/server/services/plan/parser-migration.js.map +1 -0
  103. package/dist/server/services/plan/parser.d.ts +0 -8
  104. package/dist/server/services/plan/parser.d.ts.map +1 -1
  105. package/dist/server/services/plan/parser.js +50 -569
  106. package/dist/server/services/plan/parser.js.map +1 -1
  107. package/dist/server/services/plan/review-gate.d.ts +2 -0
  108. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  109. package/dist/server/services/plan/review-gate.js +2 -2
  110. package/dist/server/services/plan/review-gate.js.map +1 -1
  111. package/dist/server/services/plan/types.d.ts +2 -0
  112. package/dist/server/services/plan/types.d.ts.map +1 -1
  113. package/dist/server/services/platform-credentials.d.ts +24 -0
  114. package/dist/server/services/platform-credentials.d.ts.map +1 -0
  115. package/dist/server/services/platform-credentials.js +68 -0
  116. package/dist/server/services/platform-credentials.js.map +1 -0
  117. package/dist/server/services/platform.d.ts +1 -31
  118. package/dist/server/services/platform.d.ts.map +1 -1
  119. package/dist/server/services/platform.js +10 -119
  120. package/dist/server/services/platform.js.map +1 -1
  121. package/dist/server/services/terminal/pty-manager.d.ts +7 -97
  122. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  123. package/dist/server/services/terminal/pty-manager.js +53 -266
  124. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  125. package/dist/server/services/terminal/pty-utils.d.ts +57 -0
  126. package/dist/server/services/terminal/pty-utils.d.ts.map +1 -0
  127. package/dist/server/services/terminal/pty-utils.js +141 -0
  128. package/dist/server/services/terminal/pty-utils.js.map +1 -0
  129. package/dist/server/services/websocket/file-definition-handlers.d.ts +4 -0
  130. package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -0
  131. package/dist/server/services/websocket/file-definition-handlers.js +153 -0
  132. package/dist/server/services/websocket/file-definition-handlers.js.map +1 -0
  133. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
  134. package/dist/server/services/websocket/file-explorer-handlers.js +52 -391
  135. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  136. package/dist/server/services/websocket/file-search-handlers.d.ts +5 -0
  137. package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -0
  138. package/dist/server/services/websocket/file-search-handlers.js +238 -0
  139. package/dist/server/services/websocket/file-search-handlers.js.map +1 -0
  140. package/dist/server/services/websocket/file-utils.js +3 -3
  141. package/dist/server/services/websocket/file-utils.js.map +1 -1
  142. package/dist/server/services/websocket/git-branch-handlers.d.ts +7 -0
  143. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -0
  144. package/dist/server/services/websocket/git-branch-handlers.js +110 -0
  145. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -0
  146. package/dist/server/services/websocket/git-diff-handlers.d.ts +6 -0
  147. package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -0
  148. package/dist/server/services/websocket/git-diff-handlers.js +123 -0
  149. package/dist/server/services/websocket/git-diff-handlers.js.map +1 -0
  150. package/dist/server/services/websocket/git-handlers.d.ts +2 -31
  151. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
  152. package/dist/server/services/websocket/git-handlers.js +35 -541
  153. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  154. package/dist/server/services/websocket/git-log-handlers.d.ts +6 -0
  155. package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -0
  156. package/dist/server/services/websocket/git-log-handlers.js +128 -0
  157. package/dist/server/services/websocket/git-log-handlers.js.map +1 -0
  158. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
  159. package/dist/server/services/websocket/git-pr-handlers.js +13 -53
  160. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
  161. package/dist/server/services/websocket/git-tag-handlers.d.ts +6 -0
  162. package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -0
  163. package/dist/server/services/websocket/git-tag-handlers.js +76 -0
  164. package/dist/server/services/websocket/git-tag-handlers.js.map +1 -0
  165. package/dist/server/services/websocket/git-utils.d.ts +43 -0
  166. package/dist/server/services/websocket/git-utils.d.ts.map +1 -0
  167. package/dist/server/services/websocket/git-utils.js +201 -0
  168. package/dist/server/services/websocket/git-utils.js.map +1 -0
  169. package/dist/server/services/websocket/handler.d.ts +2 -0
  170. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  171. package/dist/server/services/websocket/handler.js +37 -126
  172. package/dist/server/services/websocket/handler.js.map +1 -1
  173. package/dist/server/services/websocket/plan-board-handlers.d.ts +11 -0
  174. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -0
  175. package/dist/server/services/websocket/plan-board-handlers.js +218 -0
  176. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -0
  177. package/dist/server/services/websocket/plan-execution-handlers.d.ts +9 -0
  178. package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -0
  179. package/dist/server/services/websocket/plan-execution-handlers.js +142 -0
  180. package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -0
  181. package/dist/server/services/websocket/plan-handlers.d.ts +7 -2
  182. package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
  183. package/dist/server/services/websocket/plan-handlers.js +6 -925
  184. package/dist/server/services/websocket/plan-handlers.js.map +1 -1
  185. package/dist/server/services/websocket/plan-helpers.d.ts +19 -0
  186. package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -0
  187. package/dist/server/services/websocket/plan-helpers.js +199 -0
  188. package/dist/server/services/websocket/plan-helpers.js.map +1 -0
  189. package/dist/server/services/websocket/plan-issue-handlers.d.ts +12 -0
  190. package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -0
  191. package/dist/server/services/websocket/plan-issue-handlers.js +162 -0
  192. package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -0
  193. package/dist/server/services/websocket/plan-sprint-handlers.d.ts +7 -0
  194. package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -0
  195. package/dist/server/services/websocket/plan-sprint-handlers.js +206 -0
  196. package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -0
  197. package/dist/server/services/websocket/quality-complexity.d.ts +14 -0
  198. package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -0
  199. package/dist/server/services/websocket/quality-complexity.js +262 -0
  200. package/dist/server/services/websocket/quality-complexity.js.map +1 -0
  201. package/dist/server/services/websocket/quality-fix-agent.d.ts +16 -0
  202. package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -0
  203. package/dist/server/services/websocket/quality-fix-agent.js +140 -0
  204. package/dist/server/services/websocket/quality-fix-agent.js.map +1 -0
  205. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  206. package/dist/server/services/websocket/quality-handlers.js +34 -346
  207. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  208. package/dist/server/services/websocket/quality-linting.d.ts +9 -0
  209. package/dist/server/services/websocket/quality-linting.d.ts.map +1 -0
  210. package/dist/server/services/websocket/quality-linting.js +178 -0
  211. package/dist/server/services/websocket/quality-linting.js.map +1 -0
  212. package/dist/server/services/websocket/quality-review-agent.d.ts +19 -0
  213. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -0
  214. package/dist/server/services/websocket/quality-review-agent.js +206 -0
  215. package/dist/server/services/websocket/quality-review-agent.js.map +1 -0
  216. package/dist/server/services/websocket/quality-service.d.ts +3 -51
  217. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  218. package/dist/server/services/websocket/quality-service.js +9 -651
  219. package/dist/server/services/websocket/quality-service.js.map +1 -1
  220. package/dist/server/services/websocket/quality-tools.d.ts +23 -0
  221. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -0
  222. package/dist/server/services/websocket/quality-tools.js +208 -0
  223. package/dist/server/services/websocket/quality-tools.js.map +1 -0
  224. package/dist/server/services/websocket/quality-types.d.ts +59 -0
  225. package/dist/server/services/websocket/quality-types.d.ts.map +1 -0
  226. package/dist/server/services/websocket/quality-types.js +101 -0
  227. package/dist/server/services/websocket/quality-types.js.map +1 -0
  228. package/dist/server/services/websocket/session-handlers.d.ts +3 -4
  229. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  230. package/dist/server/services/websocket/session-handlers.js +3 -378
  231. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  232. package/dist/server/services/websocket/session-history.d.ts +4 -0
  233. package/dist/server/services/websocket/session-history.d.ts.map +1 -0
  234. package/dist/server/services/websocket/session-history.js +208 -0
  235. package/dist/server/services/websocket/session-history.js.map +1 -0
  236. package/dist/server/services/websocket/session-initialization.d.ts +5 -0
  237. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -0
  238. package/dist/server/services/websocket/session-initialization.js +163 -0
  239. package/dist/server/services/websocket/session-initialization.js.map +1 -0
  240. package/dist/server/services/websocket/types.d.ts +12 -2
  241. package/dist/server/services/websocket/types.d.ts.map +1 -1
  242. package/package.json +1 -1
  243. package/server/cli/headless/claude-invoker-process.ts +204 -0
  244. package/server/cli/headless/claude-invoker-stall.ts +164 -0
  245. package/server/cli/headless/claude-invoker-stream.ts +353 -0
  246. package/server/cli/headless/claude-invoker-tools.ts +187 -0
  247. package/server/cli/headless/claude-invoker.ts +15 -1096
  248. package/server/cli/headless/haiku-assessments.ts +365 -0
  249. package/server/cli/headless/headless-logger.ts +26 -5
  250. package/server/cli/headless/native-timeout-detector.ts +117 -0
  251. package/server/cli/headless/stall-assessor.ts +65 -618
  252. package/server/cli/improvisation-attachments.ts +148 -0
  253. package/server/cli/improvisation-retry.ts +602 -0
  254. package/server/cli/improvisation-session-manager.ts +140 -1349
  255. package/server/cli/improvisation-types.ts +98 -0
  256. package/server/cli/prompt-builders.ts +370 -0
  257. package/server/index.ts +35 -246
  258. package/server/mcp/bouncer-haiku.ts +182 -0
  259. package/server/mcp/bouncer-integration.ts +87 -248
  260. package/server/mcp/security-analysis.ts +217 -0
  261. package/server/mcp/security-audit.ts +1 -1
  262. package/server/mcp/security-patterns.ts +60 -283
  263. package/server/server-setup.ts +114 -0
  264. package/server/services/file-explorer-ops.ts +293 -0
  265. package/server/services/files.ts +20 -532
  266. package/server/services/plan/composer.ts +2 -1
  267. package/server/services/plan/executor.ts +3 -1
  268. package/server/services/plan/parser-core.ts +406 -0
  269. package/server/services/plan/parser-migration.ts +128 -0
  270. package/server/services/plan/parser.ts +52 -620
  271. package/server/services/plan/review-gate.ts +4 -2
  272. package/server/services/plan/types.ts +2 -0
  273. package/server/services/platform-credentials.ts +83 -0
  274. package/server/services/platform.ts +15 -141
  275. package/server/services/terminal/pty-manager.ts +66 -313
  276. package/server/services/terminal/pty-utils.ts +176 -0
  277. package/server/services/websocket/file-definition-handlers.ts +165 -0
  278. package/server/services/websocket/file-explorer-handlers.ts +37 -452
  279. package/server/services/websocket/file-search-handlers.ts +291 -0
  280. package/server/services/websocket/file-utils.ts +3 -3
  281. package/server/services/websocket/git-branch-handlers.ts +130 -0
  282. package/server/services/websocket/git-diff-handlers.ts +140 -0
  283. package/server/services/websocket/git-handlers.ts +40 -625
  284. package/server/services/websocket/git-log-handlers.ts +149 -0
  285. package/server/services/websocket/git-pr-handlers.ts +17 -62
  286. package/server/services/websocket/git-tag-handlers.ts +91 -0
  287. package/server/services/websocket/git-utils.ts +230 -0
  288. package/server/services/websocket/handler.ts +39 -126
  289. package/server/services/websocket/plan-board-handlers.ts +277 -0
  290. package/server/services/websocket/plan-execution-handlers.ts +184 -0
  291. package/server/services/websocket/plan-handlers.ts +8 -1114
  292. package/server/services/websocket/plan-helpers.ts +215 -0
  293. package/server/services/websocket/plan-issue-handlers.ts +204 -0
  294. package/server/services/websocket/plan-sprint-handlers.ts +252 -0
  295. package/server/services/websocket/quality-complexity.ts +294 -0
  296. package/server/services/websocket/quality-fix-agent.ts +181 -0
  297. package/server/services/websocket/quality-handlers.ts +36 -404
  298. package/server/services/websocket/quality-linting.ts +187 -0
  299. package/server/services/websocket/quality-review-agent.ts +246 -0
  300. package/server/services/websocket/quality-service.ts +11 -762
  301. package/server/services/websocket/quality-tools.ts +209 -0
  302. package/server/services/websocket/quality-types.ts +169 -0
  303. package/server/services/websocket/session-handlers.ts +5 -437
  304. package/server/services/websocket/session-history.ts +222 -0
  305. package/server/services/websocket/session-initialization.ts +209 -0
  306. package/server/services/websocket/types.ts +17 -0
@@ -0,0 +1,291 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ import { spawn } from 'node:child_process';
5
+ import { relative } from 'node:path';
6
+ import type { HandlerContext } from './handler-context.js';
7
+ import type { WebSocketMessage, WSContext } from './types.js';
8
+
9
+ type SearchMatch = { filePath: string; line: number; column: number; lineContent: string; contextBefore: string[]; contextAfter: string[] };
10
+
11
+ function appendGlobArgs(args: string[], globStr: string, prefix: string): void {
12
+ for (const glob of globStr.split(',')) {
13
+ const trimmed = glob.trim();
14
+ if (trimmed) args.push('--glob', `${prefix}${trimmed}`);
15
+ }
16
+ }
17
+
18
+ function buildRgArgs(query: string, options: Record<string, unknown>): string[] {
19
+ const args: string[] = ['--json', '--no-heading'];
20
+ if (!options.caseSensitive) args.push('-i');
21
+ if (options.wholeWord) args.push('-w');
22
+ if (!options.regex) args.push('-F');
23
+ args.push('-C', options.contextLines !== undefined ? String(options.contextLines) : '1');
24
+ if (options.includeGlob) appendGlobArgs(args, options.includeGlob as string, '');
25
+ if (options.excludeGlob) appendGlobArgs(args, options.excludeGlob as string, '!');
26
+ args.push('--', query, '.');
27
+ return args;
28
+ }
29
+
30
+ /** Process a single JSON line from rg output. Returns true if search should stop (maxResults reached). */
31
+ function processRgSearchLine(
32
+ line: string,
33
+ workingDir: string,
34
+ batch: SearchMatch[],
35
+ seenFiles: Set<string>,
36
+ contextMap: Map<string, { before: string[]; after: string[] }>,
37
+ counters: { totalMatches: number; fileCount: number },
38
+ maxResults: number,
39
+ flushBatch: () => void,
40
+ ): boolean {
41
+ try {
42
+ const parsed = JSON.parse(line);
43
+ if (parsed.type === 'match') {
44
+ return processRgMatch(parsed, workingDir, batch, seenFiles, contextMap, counters, maxResults, flushBatch);
45
+ }
46
+ if (parsed.type === 'context') {
47
+ appendRgContext(parsed, workingDir, batch);
48
+ }
49
+ } catch {
50
+ // Skip malformed JSON lines
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function processRgMatch(
56
+ parsed: { data: { path: { text: string }; line_number: number; lines: { text: string }; submatches?: Array<{ start: number }> } },
57
+ workingDir: string,
58
+ batch: SearchMatch[],
59
+ seenFiles: Set<string>,
60
+ contextMap: Map<string, { before: string[]; after: string[] }>,
61
+ counters: { totalMatches: number; fileCount: number },
62
+ maxResults: number,
63
+ flushBatch: () => void,
64
+ ): boolean {
65
+ const filePath = relative(workingDir, parsed.data.path.text);
66
+ const lineNumber = parsed.data.line_number;
67
+ const lineContent = parsed.data.lines.text.replace(/\n$/, '');
68
+ const column = parsed.data.submatches?.[0]?.start ?? 0;
69
+
70
+ if (!seenFiles.has(filePath)) {
71
+ seenFiles.add(filePath);
72
+ counters.fileCount++;
73
+ }
74
+ counters.totalMatches++;
75
+
76
+ const key = `${filePath}:${lineNumber}`;
77
+ const ctxLines = contextMap.get(key) || { before: [], after: [] };
78
+ batch.push({ filePath, line: lineNumber, column: column + 1, lineContent, contextBefore: ctxLines.before, contextAfter: [] });
79
+
80
+ if (counters.totalMatches >= maxResults) {
81
+ flushBatch();
82
+ return true;
83
+ }
84
+ if (batch.length >= 50) flushBatch();
85
+ return false;
86
+ }
87
+
88
+ function appendRgContext(
89
+ parsed: { data: { path: { text: string }; line_number: number; lines: { text: string } } },
90
+ workingDir: string,
91
+ batch: SearchMatch[],
92
+ ): void {
93
+ const filePath = relative(workingDir, parsed.data.path.text);
94
+ const lineNumber = parsed.data.line_number;
95
+ const lineContent = parsed.data.lines.text.replace(/\n$/, '');
96
+
97
+ const lastMatch = batch[batch.length - 1];
98
+ if (!lastMatch || lastMatch.filePath !== filePath) return;
99
+ if (lineNumber < lastMatch.line) {
100
+ lastMatch.contextBefore.push(lineContent);
101
+ } else {
102
+ lastMatch.contextAfter.push(lineContent);
103
+ }
104
+ }
105
+
106
+ export function handleSearchFileContents(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): void {
107
+ const query = msg.data?.query;
108
+ if (!query) {
109
+ ctx.send(ws, { type: 'contentSearchError', tabId, data: { error: 'Search query is required' } });
110
+ return;
111
+ }
112
+
113
+ handleCancelSearch(ctx, tabId);
114
+
115
+ const options = msg.data.options || {};
116
+ const startTime = Date.now();
117
+ let totalMatches = 0;
118
+ let fileCount = 0;
119
+ const seenFiles = new Set<string>();
120
+ const maxResults = options.maxResults || 5000;
121
+ let batch: SearchMatch[] = [];
122
+
123
+ const args = buildRgArgs(query, options);
124
+
125
+ const rgProcess = spawn('rg', args, { cwd: workingDir, stdio: ['ignore', 'pipe', 'pipe'] });
126
+ ctx.activeSearches.set(tabId, rgProcess);
127
+
128
+ let buffer = '';
129
+ const contextMap = new Map<string, { before: string[]; after: string[] }>();
130
+
131
+ const flushBatch = () => {
132
+ if (batch.length > 0) {
133
+ ctx.send(ws, { type: 'contentSearchResults', tabId, data: { matches: batch, partial: true } });
134
+ batch = [];
135
+ }
136
+ };
137
+
138
+ const searchState = { totalMatches, fileCount };
139
+
140
+ rgProcess.stdout?.on('data', (chunk: Buffer) => {
141
+ buffer += chunk.toString();
142
+ const lines = buffer.split('\n');
143
+ buffer = lines.pop() || '';
144
+
145
+ for (const line of lines) {
146
+ if (!line.trim()) continue;
147
+ if (processRgSearchLine(line, workingDir, batch, seenFiles, contextMap, searchState, maxResults, flushBatch)) {
148
+ rgProcess.kill();
149
+ return;
150
+ }
151
+ }
152
+ totalMatches = searchState.totalMatches;
153
+ fileCount = searchState.fileCount;
154
+ });
155
+
156
+ rgProcess.stderr?.on('data', (chunk: Buffer) => {
157
+ const errText = chunk.toString().trim();
158
+ if (errText && !errText.includes('No files were searched')) {
159
+ console.error(`[Search] rg stderr: ${errText}`);
160
+ }
161
+ });
162
+
163
+ rgProcess.on('close', (_code) => {
164
+ ctx.activeSearches.delete(tabId);
165
+ flushBatch();
166
+
167
+ ctx.send(ws, {
168
+ type: 'contentSearchComplete',
169
+ tabId,
170
+ data: {
171
+ totalMatches,
172
+ fileCount,
173
+ truncated: totalMatches >= maxResults,
174
+ durationMs: Date.now() - startTime,
175
+ },
176
+ });
177
+ });
178
+
179
+ rgProcess.on('error', (err) => {
180
+ ctx.activeSearches.delete(tabId);
181
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
182
+ handleSearchFallback(ctx, ws, query, options, tabId, workingDir);
183
+ } else {
184
+ ctx.send(ws, { type: 'contentSearchError', tabId, data: { error: err.message } });
185
+ }
186
+ });
187
+ }
188
+
189
+ /** Process a single grep output line. Returns true if search should stop. */
190
+ function processGrepLine(
191
+ line: string,
192
+ batch: SearchMatch[],
193
+ seenFiles: Set<string>,
194
+ counters: { totalMatches: number; fileCount: number },
195
+ maxResults: number,
196
+ flushBatch: () => void,
197
+ ): boolean {
198
+ const match = line.match(/^\.\/(.+?):(\d+):(.*)$/);
199
+ if (!match) return false;
200
+
201
+ const filePath = match[1];
202
+ const lineNumber = parseInt(match[2], 10);
203
+ const lineContent = match[3];
204
+
205
+ if (!seenFiles.has(filePath)) {
206
+ seenFiles.add(filePath);
207
+ counters.fileCount++;
208
+ }
209
+ counters.totalMatches++;
210
+
211
+ batch.push({ filePath, line: lineNumber, column: 1, lineContent, contextBefore: [], contextAfter: [] });
212
+
213
+ if (counters.totalMatches >= maxResults) {
214
+ flushBatch();
215
+ return true;
216
+ }
217
+ if (batch.length >= 50) flushBatch();
218
+ return false;
219
+ }
220
+
221
+ function handleSearchFallback(ctx: HandlerContext, ws: WSContext, query: string, options: Record<string, unknown>, tabId: string, workingDir: string): void {
222
+ const startTime = Date.now();
223
+ const args: string[] = ['-rn'];
224
+ if (!options.caseSensitive) args.push('-i');
225
+ if (options.includeGlob) {
226
+ for (const glob of String(options.includeGlob).split(',')) {
227
+ const trimmed = glob.trim();
228
+ if (trimmed) args.push(`--include=${trimmed}`);
229
+ }
230
+ }
231
+ args.push('--', query, '.');
232
+
233
+ const grepProcess = spawn('grep', args, { cwd: workingDir, stdio: ['ignore', 'pipe', 'pipe'] });
234
+ ctx.activeSearches.set(tabId, grepProcess);
235
+
236
+ let buffer = '';
237
+ let totalMatches = 0;
238
+ let fileCount = 0;
239
+ const seenFiles = new Set<string>();
240
+ const maxResults = (options.maxResults as number) || 5000;
241
+ let batch: SearchMatch[] = [];
242
+ const grepState = { totalMatches, fileCount };
243
+
244
+ const flushGrepBatch = () => {
245
+ if (batch.length > 0) {
246
+ ctx.send(ws, { type: 'contentSearchResults', tabId, data: { matches: batch, partial: true } });
247
+ batch = [];
248
+ }
249
+ };
250
+
251
+ grepProcess.stdout?.on('data', (chunk: Buffer) => {
252
+ buffer += chunk.toString();
253
+ const lines = buffer.split('\n');
254
+ buffer = lines.pop() || '';
255
+
256
+ for (const line of lines) {
257
+ if (!line.trim()) continue;
258
+ if (processGrepLine(line, batch, seenFiles, grepState, maxResults, flushGrepBatch)) {
259
+ grepProcess.kill();
260
+ return;
261
+ }
262
+ }
263
+ totalMatches = grepState.totalMatches;
264
+ fileCount = grepState.fileCount;
265
+ });
266
+
267
+ grepProcess.on('close', () => {
268
+ ctx.activeSearches.delete(tabId);
269
+ if (batch.length > 0) {
270
+ ctx.send(ws, { type: 'contentSearchResults', tabId, data: { matches: batch, partial: true } });
271
+ }
272
+ ctx.send(ws, {
273
+ type: 'contentSearchComplete',
274
+ tabId,
275
+ data: { totalMatches, fileCount, truncated: totalMatches >= maxResults, durationMs: Date.now() - startTime },
276
+ });
277
+ });
278
+
279
+ grepProcess.on('error', (err) => {
280
+ ctx.activeSearches.delete(tabId);
281
+ ctx.send(ws, { type: 'contentSearchError', tabId, data: { error: `Search unavailable: ${err.message}` } });
282
+ });
283
+ }
284
+
285
+ export function handleCancelSearch(ctx: HandlerContext, tabId: string): void {
286
+ const process = ctx.activeSearches.get(tabId);
287
+ if (process) {
288
+ process.kill();
289
+ ctx.activeSearches.delete(tabId);
290
+ }
291
+ }
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { existsSync, readdirSync, readFileSync, type Stats, statSync } from 'node:fs';
11
- import { extname, join, relative, sep } from 'node:path';
11
+ import { extname, join, relative, resolve, sep } from 'node:path';
12
12
  import type { CacheEntry, } from './types.js';
13
13
 
14
14
  // Directories always excluded from autocomplete scanning
@@ -286,8 +286,8 @@ function readTextContent(fullPath: string, filePath: string, fileName: string, s
286
286
  }
287
287
 
288
288
  function validateFileAccess(fullPath: string, filePath: string, fileName: string, workingDir: string): FileContentResult | null {
289
- const normalizedPath = join(fullPath);
290
- if (!normalizedPath.startsWith(join(workingDir)) && !isPathInSafeLocation(normalizedPath)) {
289
+ const normalizedPath = resolve(fullPath);
290
+ if (!normalizedPath.startsWith(resolve(workingDir)) && !isPathInSafeLocation(normalizedPath)) {
291
291
  return { path: filePath, fileName, content: '', error: 'Access denied: path outside allowed locations' };
292
292
  }
293
293
  if (!existsSync(fullPath)) {
@@ -0,0 +1,130 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ import { executeGitCommand, sendGitError } from './git-utils.js';
5
+ import type { HandlerContext } from './handler-context.js';
6
+ import type { GitBranchEntry, WebSocketMessage, WSContext } from './types.js';
7
+
8
+ export async function handleGitListBranches(ctx: HandlerContext, ws: WSContext, tabId: string, workingDir: string): Promise<void> {
9
+ try {
10
+ const result = await executeGitCommand(
11
+ ['branch', '-a', '--format=%(refname:short)|%(objectname:short)|%(upstream:short)|%(HEAD)'],
12
+ workingDir
13
+ );
14
+ if (result.exitCode !== 0) {
15
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to list branches' } });
16
+ return;
17
+ }
18
+
19
+ const currentBranchResult = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], workingDir);
20
+ const currentBranch = currentBranchResult.stdout.trim() || 'HEAD';
21
+
22
+ const branches: GitBranchEntry[] = result.stdout.trim().split('\n')
23
+ .filter(line => line.trim())
24
+ .map(line => {
25
+ const [name, shortHash, upstream, head] = line.split('|');
26
+ const isRemote = name.includes('/') && (name.startsWith('origin/') || name.includes('remotes/'));
27
+ return {
28
+ name: name.trim(),
29
+ shortHash: shortHash?.trim() || '',
30
+ isRemote,
31
+ isCurrent: head?.trim() === '*',
32
+ upstream: upstream?.trim() || undefined,
33
+ };
34
+ })
35
+ .filter(b => b.name !== 'origin/HEAD');
36
+
37
+ ctx.send(ws, { type: 'gitBranchList', tabId, data: { branches, current: currentBranch } });
38
+ } catch (error: unknown) {
39
+ sendGitError(ctx, ws, tabId, error);
40
+ }
41
+ }
42
+
43
+ export async function handleGitCheckout(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
44
+ try {
45
+ const { branch, create, startPoint } = msg.data || {};
46
+ if (!branch) {
47
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Branch name is required' } });
48
+ return;
49
+ }
50
+
51
+ const statusResult = await executeGitCommand(['status', '--porcelain'], workingDir);
52
+ if (statusResult.stdout.trim()) {
53
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Commit or stash changes before switching branches' } });
54
+ return;
55
+ }
56
+
57
+ const prevResult = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], workingDir);
58
+ const previous = prevResult.stdout.trim();
59
+
60
+ const args = create
61
+ ? ['checkout', '-b', branch, ...(startPoint ? [startPoint] : [])]
62
+ : ['checkout', branch];
63
+
64
+ const result = await executeGitCommand(args, workingDir);
65
+ if (result.exitCode !== 0) {
66
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to checkout branch' } });
67
+ return;
68
+ }
69
+
70
+ ctx.send(ws, { type: 'gitCheckedOut', tabId, data: { branch, previous } });
71
+ // Re-fetch status after checkout - import handleGitStatus at call site
72
+ const { handleGitStatus } = await import('./git-handlers.js');
73
+ handleGitStatus(ctx, ws, tabId, workingDir);
74
+ } catch (error: unknown) {
75
+ sendGitError(ctx, ws, tabId, error);
76
+ }
77
+ }
78
+
79
+ export async function handleGitCreateBranch(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
80
+ try {
81
+ const { name, startPoint, checkout } = msg.data || {};
82
+ if (!name) {
83
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Branch name is required' } });
84
+ return;
85
+ }
86
+
87
+ const args = ['branch', name, ...(startPoint ? [startPoint] : [])];
88
+ const result = await executeGitCommand(args, workingDir);
89
+ if (result.exitCode !== 0) {
90
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to create branch' } });
91
+ return;
92
+ }
93
+
94
+ const hashResult = await executeGitCommand(['rev-parse', '--short', name], workingDir);
95
+
96
+ if (checkout) {
97
+ await executeGitCommand(['checkout', name], workingDir);
98
+ }
99
+
100
+ ctx.send(ws, { type: 'gitBranchCreated', tabId, data: { name, hash: hashResult.stdout.trim() } });
101
+ } catch (error: unknown) {
102
+ sendGitError(ctx, ws, tabId, error);
103
+ }
104
+ }
105
+
106
+ export async function handleGitDeleteBranch(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
107
+ try {
108
+ const { name, force } = msg.data || {};
109
+ if (!name) {
110
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Branch name is required' } });
111
+ return;
112
+ }
113
+
114
+ const currentResult = await executeGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], workingDir);
115
+ if (currentResult.stdout.trim() === name) {
116
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Cannot delete the currently checked out branch' } });
117
+ return;
118
+ }
119
+
120
+ const result = await executeGitCommand(['branch', force ? '-D' : '-d', name], workingDir);
121
+ if (result.exitCode !== 0) {
122
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: result.stderr || 'Failed to delete branch' } });
123
+ return;
124
+ }
125
+
126
+ ctx.send(ws, { type: 'gitBranchDeleted', tabId, data: { name } });
127
+ } catch (error: unknown) {
128
+ sendGitError(ctx, ws, tabId, error);
129
+ }
130
+ }
@@ -0,0 +1,140 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ import { readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { executeGitCommand, sendGitError, stripCoauthorLines } from './git-utils.js';
7
+ import type { HandlerContext } from './handler-context.js';
8
+ import type { GitCommitFile, WebSocketMessage, WSContext } from './types.js';
9
+
10
+ export async function handleGitDiff(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
11
+ try {
12
+ const { path, staged } = msg.data || {};
13
+ if (!path) {
14
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'File path is required' } });
15
+ return;
16
+ }
17
+
18
+ const originalResult = await executeGitCommand(['show', `HEAD:${path}`], workingDir);
19
+ const original = originalResult.exitCode === 0 ? originalResult.stdout : '';
20
+
21
+ let modified: string;
22
+ if (staged) {
23
+ const indexResult = await executeGitCommand(['show', `:${path}`], workingDir);
24
+ modified = indexResult.exitCode === 0 ? indexResult.stdout : '';
25
+ } else {
26
+ const fullPath = join(workingDir, path);
27
+ try {
28
+ modified = readFileSync(fullPath, 'utf-8');
29
+ } catch {
30
+ modified = '';
31
+ }
32
+ }
33
+
34
+ ctx.send(ws, {
35
+ type: 'gitDiffResult',
36
+ tabId,
37
+ data: { path, original, modified, staged: !!staged },
38
+ });
39
+ } catch (error: unknown) {
40
+ sendGitError(ctx, ws, tabId, error);
41
+ }
42
+ }
43
+
44
+ /** Parse a single name-status line into {status, oldPath?} */
45
+ function parseStatusLine(line: string | undefined): { status: string; oldPath?: string } {
46
+ if (!line) return { status: 'M' };
47
+ const parts = line.split('\t');
48
+ const status = (parts[0] || 'M').charAt(0); // R100 -> R, C100 -> C
49
+ const oldPath = (status === 'R' || status === 'C') && parts.length >= 3 ? parts[1] : undefined;
50
+ return { status, oldPath };
51
+ }
52
+
53
+ /** Parse numstat + name-status output into GitCommitFile[] */
54
+ function parseCommitFiles(numstatOutput: string, statusOutput: string): GitCommitFile[] {
55
+ const numstatLines = numstatOutput.trim().split('\n').filter(Boolean);
56
+ const statusLines = statusOutput.trim().split('\n').filter(Boolean);
57
+
58
+ return numstatLines.map((line, i) => {
59
+ const numParts = line.split('\t');
60
+ const additions = numParts[0] === '-' ? 0 : parseInt(numParts[0], 10) || 0;
61
+ const deletions = numParts[1] === '-' ? 0 : parseInt(numParts[1], 10) || 0;
62
+ const { status, oldPath } = parseStatusLine(statusLines[i]);
63
+ return { path: numParts[2] || '', status, additions, deletions, ...(oldPath ? { oldPath } : {}) };
64
+ });
65
+ }
66
+
67
+ /** Get the changed files for a commit via diff-tree */
68
+ async function getCommitFiles(hash: string, workingDir: string): Promise<GitCommitFile[]> {
69
+ const isRoot = (await executeGitCommand(['rev-parse', `${hash}^`], workingDir)).exitCode !== 0;
70
+ const extra = isRoot ? ['--root'] : [];
71
+ const numstatResult = await executeGitCommand(
72
+ ['diff-tree', '-r', '--numstat', '--no-commit-id', ...extra, hash], workingDir
73
+ );
74
+ const statusResult = await executeGitCommand(
75
+ ['diff-tree', '-r', '--name-status', '--no-commit-id', ...extra, hash], workingDir
76
+ );
77
+ return parseCommitFiles(numstatResult.stdout, statusResult.stdout);
78
+ }
79
+
80
+ export async function handleGitShowCommit(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
81
+ try {
82
+ const hash = msg.data?.hash as string | undefined;
83
+ if (!hash) {
84
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Commit hash is required' } });
85
+ return;
86
+ }
87
+
88
+ const metaResult = await executeGitCommand([
89
+ 'show', '-s', '--format=%H%x00%h%x00%s%x00%b%x00%an%x00%aI', hash
90
+ ], workingDir);
91
+
92
+ if (metaResult.exitCode !== 0) {
93
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: metaResult.stderr || 'Failed to get commit details' } });
94
+ return;
95
+ }
96
+
97
+ const parts = metaResult.stdout.trim().split('\x00');
98
+ const subject = stripCoauthorLines(parts[2] || '') || parts[2] || '';
99
+ const files = await getCommitFiles(hash, workingDir);
100
+
101
+ ctx.send(ws, {
102
+ type: 'gitCommitDetail',
103
+ tabId,
104
+ data: {
105
+ hash: parts[0], shortHash: parts[1], subject,
106
+ body: stripCoauthorLines(parts[3] || '').trim(),
107
+ author: parts[4], date: parts[5], files,
108
+ },
109
+ });
110
+ } catch (error: unknown) {
111
+ sendGitError(ctx, ws, tabId, error);
112
+ }
113
+ }
114
+
115
+ export async function handleGitCommitDiff(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
116
+ try {
117
+ const hash = msg.data?.hash as string | undefined;
118
+ const path = msg.data?.path as string | undefined;
119
+ if (!hash || !path) {
120
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Commit hash and file path are required' } });
121
+ return;
122
+ }
123
+
124
+ // Get the file content at this commit
125
+ const modifiedResult = await executeGitCommand(['show', `${hash}:${path}`], workingDir);
126
+ const modified = modifiedResult.exitCode === 0 ? modifiedResult.stdout : '';
127
+
128
+ // Get the file content at the parent commit
129
+ const originalResult = await executeGitCommand(['show', `${hash}^:${path}`], workingDir);
130
+ const original = originalResult.exitCode === 0 ? originalResult.stdout : '';
131
+
132
+ ctx.send(ws, {
133
+ type: 'gitCommitDiffResult',
134
+ tabId,
135
+ data: { hash, path, original, modified },
136
+ });
137
+ } catch (error: unknown) {
138
+ sendGitError(ctx, ws, tabId, error);
139
+ }
140
+ }