@pixelbyte-software/pixcode 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/LICENSE +718 -0
  2. package/README.de.md +248 -0
  3. package/README.ja.md +240 -0
  4. package/README.ko.md +240 -0
  5. package/README.md +285 -0
  6. package/README.ru.md +248 -0
  7. package/README.tr.md +250 -0
  8. package/README.zh-CN.md +240 -0
  9. package/dist/api-docs.html +879 -0
  10. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  11. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  12. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  15. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  21. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  28. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  30. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  31. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  32. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  33. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  34. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  35. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  37. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  39. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  40. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  41. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  42. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  52. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  53. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  54. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  55. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  56. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  57. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  58. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  59. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  60. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  61. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  62. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  63. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  64. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  65. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  66. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  67. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  68. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  69. package/dist/assets/index-C2c9QNwK.css +32 -0
  70. package/dist/assets/index-DyXDZED-.js +1277 -0
  71. package/dist/assets/vendor-codemirror-NA4v81it.js +41 -0
  72. package/dist/assets/vendor-react-D7WwDXvu.js +59 -0
  73. package/dist/assets/vendor-xterm-CJZjLICi.js +66 -0
  74. package/dist/clear-cache.html +85 -0
  75. package/dist/convert-icons.md +53 -0
  76. package/dist/favicon.png +0 -0
  77. package/dist/favicon.svg +9 -0
  78. package/dist/generate-icons.js +49 -0
  79. package/dist/icons/claude-ai-icon.svg +1 -0
  80. package/dist/icons/codex-white.svg +3 -0
  81. package/dist/icons/codex.svg +3 -0
  82. package/dist/icons/cursor-white.svg +12 -0
  83. package/dist/icons/cursor.svg +1 -0
  84. package/dist/icons/gemini-ai-icon.svg +1 -0
  85. package/dist/icons/icon-128x128.png +0 -0
  86. package/dist/icons/icon-128x128.svg +12 -0
  87. package/dist/icons/icon-144x144.png +0 -0
  88. package/dist/icons/icon-144x144.svg +12 -0
  89. package/dist/icons/icon-152x152.png +0 -0
  90. package/dist/icons/icon-152x152.svg +12 -0
  91. package/dist/icons/icon-192x192.png +0 -0
  92. package/dist/icons/icon-192x192.svg +12 -0
  93. package/dist/icons/icon-384x384.png +0 -0
  94. package/dist/icons/icon-384x384.svg +12 -0
  95. package/dist/icons/icon-512x512.png +0 -0
  96. package/dist/icons/icon-512x512.svg +12 -0
  97. package/dist/icons/icon-72x72.png +0 -0
  98. package/dist/icons/icon-72x72.svg +12 -0
  99. package/dist/icons/icon-96x96.png +0 -0
  100. package/dist/icons/icon-96x96.svg +12 -0
  101. package/dist/icons/icon-template.svg +12 -0
  102. package/dist/index.html +52 -0
  103. package/dist/logo-128.png +0 -0
  104. package/dist/logo-256.png +0 -0
  105. package/dist/logo-32.png +0 -0
  106. package/dist/logo-512.png +0 -0
  107. package/dist/logo-64.png +0 -0
  108. package/dist/logo.svg +17 -0
  109. package/dist/manifest.json +61 -0
  110. package/dist/screenshots/cli-selection.png +0 -0
  111. package/dist/screenshots/desktop-main.png +0 -0
  112. package/dist/screenshots/mobile-chat.png +0 -0
  113. package/dist/screenshots/tools-modal.png +0 -0
  114. package/dist/sw.js +124 -0
  115. package/dist-server/server/claude-sdk.js +709 -0
  116. package/dist-server/server/claude-sdk.js.map +1 -0
  117. package/dist-server/server/cli.js +854 -0
  118. package/dist-server/server/cli.js.map +1 -0
  119. package/dist-server/server/constants/config.js +6 -0
  120. package/dist-server/server/constants/config.js.map +1 -0
  121. package/dist-server/server/cursor-cli.js +277 -0
  122. package/dist-server/server/cursor-cli.js.map +1 -0
  123. package/dist-server/server/daemon/manager.js +486 -0
  124. package/dist-server/server/daemon/manager.js.map +1 -0
  125. package/dist-server/server/daemon-manager.js +823 -0
  126. package/dist-server/server/daemon-manager.js.map +1 -0
  127. package/dist-server/server/database/db.js +535 -0
  128. package/dist-server/server/database/db.js.map +1 -0
  129. package/dist-server/server/database/schema.js +97 -0
  130. package/dist-server/server/database/schema.js.map +1 -0
  131. package/dist-server/server/gemini-cli.js +408 -0
  132. package/dist-server/server/gemini-cli.js.map +1 -0
  133. package/dist-server/server/gemini-response-handler.js +72 -0
  134. package/dist-server/server/gemini-response-handler.js.map +1 -0
  135. package/dist-server/server/index.js +2325 -0
  136. package/dist-server/server/index.js.map +1 -0
  137. package/dist-server/server/load-env.js +32 -0
  138. package/dist-server/server/load-env.js.map +1 -0
  139. package/dist-server/server/middleware/auth.js +111 -0
  140. package/dist-server/server/middleware/auth.js.map +1 -0
  141. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +103 -0
  142. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -0
  143. package/dist-server/server/modules/providers/list/claude/claude-mcp.provider.js +103 -0
  144. package/dist-server/server/modules/providers/list/claude/claude-mcp.provider.js.map +1 -0
  145. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +266 -0
  146. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -0
  147. package/dist-server/server/modules/providers/list/claude/claude.provider.js +13 -0
  148. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -0
  149. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js +84 -0
  150. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js.map +1 -0
  151. package/dist-server/server/modules/providers/list/codex/codex-mcp.provider.js +107 -0
  152. package/dist-server/server/modules/providers/list/codex/codex-mcp.provider.js.map +1 -0
  153. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +282 -0
  154. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -0
  155. package/dist-server/server/modules/providers/list/codex/codex.provider.js +13 -0
  156. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -0
  157. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +118 -0
  158. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -0
  159. package/dist-server/server/modules/providers/list/cursor/cursor-mcp.provider.js +80 -0
  160. package/dist-server/server/modules/providers/list/cursor/cursor-mcp.provider.js.map +1 -0
  161. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +369 -0
  162. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -0
  163. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +13 -0
  164. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -0
  165. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +131 -0
  166. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -0
  167. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js +82 -0
  168. package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js.map +1 -0
  169. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +207 -0
  170. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -0
  171. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +13 -0
  172. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -0
  173. package/dist-server/server/modules/providers/provider.registry.js +31 -0
  174. package/dist-server/server/modules/providers/provider.registry.js.map +1 -0
  175. package/dist-server/server/modules/providers/provider.routes.js +159 -0
  176. package/dist-server/server/modules/providers/provider.routes.js.map +1 -0
  177. package/dist-server/server/modules/providers/services/mcp.service.js +69 -0
  178. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -0
  179. package/dist-server/server/modules/providers/services/provider-auth.service.js +25 -0
  180. package/dist-server/server/modules/providers/services/provider-auth.service.js.map +1 -0
  181. package/dist-server/server/modules/providers/services/sessions.service.js +29 -0
  182. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -0
  183. package/dist-server/server/modules/providers/shared/base/abstract.provider.js +14 -0
  184. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -0
  185. package/dist-server/server/modules/providers/shared/mcp/mcp.provider.js +102 -0
  186. package/dist-server/server/modules/providers/shared/mcp/mcp.provider.js.map +1 -0
  187. package/dist-server/server/modules/providers/tests/mcp.test.js +250 -0
  188. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -0
  189. package/dist-server/server/openai-codex.js +373 -0
  190. package/dist-server/server/openai-codex.js.map +1 -0
  191. package/dist-server/server/projects.js +2492 -0
  192. package/dist-server/server/projects.js.map +1 -0
  193. package/dist-server/server/routes/agent.js +1147 -0
  194. package/dist-server/server/routes/agent.js.map +1 -0
  195. package/dist-server/server/routes/auth.js +117 -0
  196. package/dist-server/server/routes/auth.js.map +1 -0
  197. package/dist-server/server/routes/cli-auth.js +25 -0
  198. package/dist-server/server/routes/cli-auth.js.map +1 -0
  199. package/dist-server/server/routes/codex.js +18 -0
  200. package/dist-server/server/routes/codex.js.map +1 -0
  201. package/dist-server/server/routes/commands.js +487 -0
  202. package/dist-server/server/routes/commands.js.map +1 -0
  203. package/dist-server/server/routes/cursor.js +49 -0
  204. package/dist-server/server/routes/cursor.js.map +1 -0
  205. package/dist-server/server/routes/gemini.js +21 -0
  206. package/dist-server/server/routes/gemini.js.map +1 -0
  207. package/dist-server/server/routes/git.js +1259 -0
  208. package/dist-server/server/routes/git.js.map +1 -0
  209. package/dist-server/server/routes/mcp-utils.js +29 -0
  210. package/dist-server/server/routes/mcp-utils.js.map +1 -0
  211. package/dist-server/server/routes/messages.js +56 -0
  212. package/dist-server/server/routes/messages.js.map +1 -0
  213. package/dist-server/server/routes/plugins.js +266 -0
  214. package/dist-server/server/routes/plugins.js.map +1 -0
  215. package/dist-server/server/routes/projects.js +566 -0
  216. package/dist-server/server/routes/projects.js.map +1 -0
  217. package/dist-server/server/routes/settings.js +259 -0
  218. package/dist-server/server/routes/settings.js.map +1 -0
  219. package/dist-server/server/routes/taskmaster.js +1373 -0
  220. package/dist-server/server/routes/taskmaster.js.map +1 -0
  221. package/dist-server/server/routes/user.js +115 -0
  222. package/dist-server/server/routes/user.js.map +1 -0
  223. package/dist-server/server/services/notification-orchestrator.js +177 -0
  224. package/dist-server/server/services/notification-orchestrator.js.map +1 -0
  225. package/dist-server/server/services/vapid-keys.js +26 -0
  226. package/dist-server/server/services/vapid-keys.js.map +1 -0
  227. package/dist-server/server/sessionManager.js +194 -0
  228. package/dist-server/server/sessionManager.js.map +1 -0
  229. package/dist-server/server/shared/interfaces.js +2 -0
  230. package/dist-server/server/shared/interfaces.js.map +1 -0
  231. package/dist-server/server/shared/types.js +3 -0
  232. package/dist-server/server/shared/types.js.map +1 -0
  233. package/dist-server/server/shared/utils.js +150 -0
  234. package/dist-server/server/shared/utils.js.map +1 -0
  235. package/dist-server/server/utils/colors.js +20 -0
  236. package/dist-server/server/utils/colors.js.map +1 -0
  237. package/dist-server/server/utils/commandParser.js +255 -0
  238. package/dist-server/server/utils/commandParser.js.map +1 -0
  239. package/dist-server/server/utils/frontmatter.js +16 -0
  240. package/dist-server/server/utils/frontmatter.js.map +1 -0
  241. package/dist-server/server/utils/gitConfig.js +36 -0
  242. package/dist-server/server/utils/gitConfig.js.map +1 -0
  243. package/dist-server/server/utils/mcp-detector.js +134 -0
  244. package/dist-server/server/utils/mcp-detector.js.map +1 -0
  245. package/dist-server/server/utils/plugin-loader.js +413 -0
  246. package/dist-server/server/utils/plugin-loader.js.map +1 -0
  247. package/dist-server/server/utils/plugin-process-manager.js +163 -0
  248. package/dist-server/server/utils/plugin-process-manager.js.map +1 -0
  249. package/dist-server/server/utils/runtime-paths.js +30 -0
  250. package/dist-server/server/utils/runtime-paths.js.map +1 -0
  251. package/dist-server/server/utils/taskmaster-websocket.js +118 -0
  252. package/dist-server/server/utils/taskmaster-websocket.js.map +1 -0
  253. package/dist-server/server/utils/url-detection.js +58 -0
  254. package/dist-server/server/utils/url-detection.js.map +1 -0
  255. package/dist-server/server/vite-daemon.js +75 -0
  256. package/dist-server/server/vite-daemon.js.map +1 -0
  257. package/dist-server/shared/modelConstants.js +90 -0
  258. package/dist-server/shared/modelConstants.js.map +1 -0
  259. package/dist-server/shared/networkHosts.js +20 -0
  260. package/dist-server/shared/networkHosts.js.map +1 -0
  261. package/package.json +168 -0
  262. package/scripts/fix-node-pty.js +67 -0
  263. package/server/claude-sdk.js +834 -0
  264. package/server/cli.js +937 -0
  265. package/server/constants/config.js +5 -0
  266. package/server/cursor-cli.js +342 -0
  267. package/server/daemon/manager.js +564 -0
  268. package/server/daemon-manager.js +920 -0
  269. package/server/database/db.js +593 -0
  270. package/server/database/schema.js +102 -0
  271. package/server/gemini-cli.js +469 -0
  272. package/server/gemini-response-handler.js +79 -0
  273. package/server/index.js +2557 -0
  274. package/server/load-env.js +34 -0
  275. package/server/middleware/auth.js +132 -0
  276. package/server/modules/providers/list/claude/claude-auth.provider.ts +123 -0
  277. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -0
  278. package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -0
  279. package/server/modules/providers/list/claude/claude.provider.ts +15 -0
  280. package/server/modules/providers/list/codex/codex-auth.provider.ts +100 -0
  281. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -0
  282. package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -0
  283. package/server/modules/providers/list/codex/codex.provider.ts +15 -0
  284. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -0
  285. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -0
  286. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -0
  287. package/server/modules/providers/list/cursor/cursor.provider.ts +15 -0
  288. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +151 -0
  289. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -0
  290. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -0
  291. package/server/modules/providers/list/gemini/gemini.provider.ts +15 -0
  292. package/server/modules/providers/provider.registry.ts +36 -0
  293. package/server/modules/providers/provider.routes.ts +217 -0
  294. package/server/modules/providers/services/mcp.service.ts +94 -0
  295. package/server/modules/providers/services/provider-auth.service.ts +26 -0
  296. package/server/modules/providers/services/sessions.service.ts +45 -0
  297. package/server/modules/providers/shared/base/abstract.provider.ts +20 -0
  298. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -0
  299. package/server/modules/providers/tests/mcp.test.ts +293 -0
  300. package/server/openai-codex.js +426 -0
  301. package/server/projects.js +2792 -0
  302. package/server/routes/agent.js +1245 -0
  303. package/server/routes/auth.js +135 -0
  304. package/server/routes/cli-auth.js +27 -0
  305. package/server/routes/codex.js +19 -0
  306. package/server/routes/commands.js +554 -0
  307. package/server/routes/cursor.js +52 -0
  308. package/server/routes/gemini.js +24 -0
  309. package/server/routes/git.js +1488 -0
  310. package/server/routes/mcp-utils.js +31 -0
  311. package/server/routes/messages.js +61 -0
  312. package/server/routes/plugins.js +307 -0
  313. package/server/routes/projects.js +627 -0
  314. package/server/routes/settings.js +286 -0
  315. package/server/routes/taskmaster.js +1471 -0
  316. package/server/routes/user.js +123 -0
  317. package/server/services/notification-orchestrator.js +227 -0
  318. package/server/services/vapid-keys.js +35 -0
  319. package/server/sessionManager.js +226 -0
  320. package/server/shared/interfaces.ts +54 -0
  321. package/server/shared/types.ts +172 -0
  322. package/server/shared/utils.ts +193 -0
  323. package/server/tsconfig.json +36 -0
  324. package/server/utils/colors.js +21 -0
  325. package/server/utils/commandParser.js +303 -0
  326. package/server/utils/frontmatter.js +18 -0
  327. package/server/utils/gitConfig.js +34 -0
  328. package/server/utils/mcp-detector.js +147 -0
  329. package/server/utils/plugin-loader.js +457 -0
  330. package/server/utils/plugin-process-manager.js +184 -0
  331. package/server/utils/runtime-paths.js +37 -0
  332. package/server/utils/taskmaster-websocket.js +129 -0
  333. package/server/utils/url-detection.js +71 -0
  334. package/server/vite-daemon.js +78 -0
  335. package/shared/modelConstants.js +97 -0
  336. package/shared/networkHosts.js +22 -0
@@ -0,0 +1,2792 @@
1
+ /**
2
+ * PROJECT DISCOVERY AND MANAGEMENT SYSTEM
3
+ * ========================================
4
+ *
5
+ * This module manages project discovery for both Claude CLI and Cursor CLI sessions.
6
+ *
7
+ * ## Architecture Overview
8
+ *
9
+ * 1. **Claude Projects** (stored in ~/.claude/projects/)
10
+ * - Each project is a directory named with the project path encoded (/ replaced with -)
11
+ * - Contains .jsonl files with conversation history including 'cwd' field
12
+ * - Project metadata stored in ~/.claude/project-config.json
13
+ *
14
+ * 2. **Cursor Projects** (stored in ~/.cursor/chats/)
15
+ * - Each project directory is named with MD5 hash of the absolute project path
16
+ * - Example: /Users/john/myproject -> MD5 -> a1b2c3d4e5f6...
17
+ * - Contains session directories with SQLite databases (store.db)
18
+ * - Project path is NOT stored in the database - only in the MD5 hash
19
+ *
20
+ * ## Project Discovery Strategy
21
+ *
22
+ * 1. **Claude Projects Discovery**:
23
+ * - Scan ~/.claude/projects/ directory for Claude project folders
24
+ * - Extract actual project path from .jsonl files (cwd field)
25
+ * - Fall back to decoded directory name if no sessions exist
26
+ *
27
+ * 2. **Cursor Sessions Discovery**:
28
+ * - For each KNOWN project (from Claude or manually added)
29
+ * - Compute MD5 hash of the project's absolute path
30
+ * - Check if ~/.cursor/chats/{md5_hash}/ directory exists
31
+ * - Read session metadata from SQLite store.db files
32
+ *
33
+ * 3. **Manual Project Addition**:
34
+ * - Users can manually add project paths via UI
35
+ * - Stored in ~/.claude/project-config.json with 'manuallyAdded' flag
36
+ * - Allows discovering Cursor sessions for projects without Claude sessions
37
+ *
38
+ * ## Critical Limitations
39
+ *
40
+ * - **CANNOT discover Cursor-only projects**: From a quick check, there was no mention of
41
+ * the cwd of each project. if someone has the time, you can try to reverse engineer it.
42
+ *
43
+ * - **Project relocation breaks history**: If a project directory is moved or renamed,
44
+ * the MD5 hash changes, making old Cursor sessions inaccessible unless the old
45
+ * path is known and manually added.
46
+ *
47
+ * ## Error Handling
48
+ *
49
+ * - Missing ~/.claude directory is handled gracefully with automatic creation
50
+ * - ENOENT errors are caught and handled without crashing
51
+ * - Empty arrays returned when no projects/sessions exist
52
+ *
53
+ * ## Caching Strategy
54
+ *
55
+ * - Project directory extraction is cached to minimize file I/O
56
+ * - Cache is cleared when project configuration changes
57
+ * - Session data is fetched on-demand, not cached
58
+ */
59
+
60
+ import { promises as fs } from 'fs';
61
+ import fsSync from 'fs';
62
+ import path from 'path';
63
+ import readline from 'readline';
64
+ import crypto from 'crypto';
65
+ import Database from 'better-sqlite3';
66
+ import os from 'os';
67
+ import sessionManager from './sessionManager.js';
68
+ import { applyCustomSessionNames } from './database/db.js';
69
+
70
+ // Import TaskMaster detection functions
71
+ async function detectTaskMasterFolder(projectPath) {
72
+ try {
73
+ const taskMasterPath = path.join(projectPath, '.taskmaster');
74
+
75
+ // Check if .taskmaster directory exists
76
+ try {
77
+ const stats = await fs.stat(taskMasterPath);
78
+ if (!stats.isDirectory()) {
79
+ return {
80
+ hasTaskmaster: false,
81
+ reason: '.taskmaster exists but is not a directory'
82
+ };
83
+ }
84
+ } catch (error) {
85
+ if (error.code === 'ENOENT') {
86
+ return {
87
+ hasTaskmaster: false,
88
+ reason: '.taskmaster directory not found'
89
+ };
90
+ }
91
+ throw error;
92
+ }
93
+
94
+ // Check for key TaskMaster files
95
+ const keyFiles = [
96
+ 'tasks/tasks.json',
97
+ 'config.json'
98
+ ];
99
+
100
+ const fileStatus = {};
101
+ let hasEssentialFiles = true;
102
+
103
+ for (const file of keyFiles) {
104
+ const filePath = path.join(taskMasterPath, file);
105
+ try {
106
+ await fs.access(filePath);
107
+ fileStatus[file] = true;
108
+ } catch (error) {
109
+ fileStatus[file] = false;
110
+ if (file === 'tasks/tasks.json') {
111
+ hasEssentialFiles = false;
112
+ }
113
+ }
114
+ }
115
+
116
+ // Parse tasks.json if it exists for metadata
117
+ let taskMetadata = null;
118
+ if (fileStatus['tasks/tasks.json']) {
119
+ try {
120
+ const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
121
+ const tasksContent = await fs.readFile(tasksPath, 'utf8');
122
+ const tasksData = JSON.parse(tasksContent);
123
+
124
+ // Handle both tagged and legacy formats
125
+ let tasks = [];
126
+ if (tasksData.tasks) {
127
+ // Legacy format
128
+ tasks = tasksData.tasks;
129
+ } else {
130
+ // Tagged format - get tasks from all tags
131
+ Object.values(tasksData).forEach(tagData => {
132
+ if (tagData.tasks) {
133
+ tasks = tasks.concat(tagData.tasks);
134
+ }
135
+ });
136
+ }
137
+
138
+ // Calculate task statistics
139
+ const stats = tasks.reduce((acc, task) => {
140
+ acc.total++;
141
+ acc[task.status] = (acc[task.status] || 0) + 1;
142
+
143
+ // Count subtasks
144
+ if (task.subtasks) {
145
+ task.subtasks.forEach(subtask => {
146
+ acc.subtotalTasks++;
147
+ acc.subtasks = acc.subtasks || {};
148
+ acc.subtasks[subtask.status] = (acc.subtasks[subtask.status] || 0) + 1;
149
+ });
150
+ }
151
+
152
+ return acc;
153
+ }, {
154
+ total: 0,
155
+ subtotalTasks: 0,
156
+ pending: 0,
157
+ 'in-progress': 0,
158
+ done: 0,
159
+ review: 0,
160
+ deferred: 0,
161
+ cancelled: 0,
162
+ subtasks: {}
163
+ });
164
+
165
+ taskMetadata = {
166
+ taskCount: stats.total,
167
+ subtaskCount: stats.subtotalTasks,
168
+ completed: stats.done || 0,
169
+ pending: stats.pending || 0,
170
+ inProgress: stats['in-progress'] || 0,
171
+ review: stats.review || 0,
172
+ completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
173
+ lastModified: (await fs.stat(tasksPath)).mtime.toISOString()
174
+ };
175
+ } catch (parseError) {
176
+ console.warn('Failed to parse tasks.json:', parseError.message);
177
+ taskMetadata = { error: 'Failed to parse tasks.json' };
178
+ }
179
+ }
180
+
181
+ return {
182
+ hasTaskmaster: true,
183
+ hasEssentialFiles,
184
+ files: fileStatus,
185
+ metadata: taskMetadata,
186
+ path: taskMasterPath
187
+ };
188
+
189
+ } catch (error) {
190
+ console.error('Error detecting TaskMaster folder:', error);
191
+ return {
192
+ hasTaskmaster: false,
193
+ reason: `Error checking directory: ${error.message}`
194
+ };
195
+ }
196
+ }
197
+
198
+ // Cache for extracted project directories
199
+ const projectDirectoryCache = new Map();
200
+ let hasWarnedDiscoveredPersistenceFailure = false;
201
+
202
+ // Clear cache when needed (called when project files change)
203
+ function clearProjectDirectoryCache() {
204
+ projectDirectoryCache.clear();
205
+ }
206
+
207
+ // Load project configuration file
208
+ async function loadProjectConfig() {
209
+ const configPath = path.join(os.homedir(), '.claude', 'project-config.json');
210
+ try {
211
+ const configData = await fs.readFile(configPath, 'utf8');
212
+ return JSON.parse(configData);
213
+ } catch (error) {
214
+ // Return empty config if file doesn't exist
215
+ return {};
216
+ }
217
+ }
218
+
219
+ // Save project configuration file
220
+ async function saveProjectConfig(config) {
221
+ const claudeDir = path.join(os.homedir(), '.claude');
222
+ const configPath = path.join(claudeDir, 'project-config.json');
223
+
224
+ // Ensure the .claude directory exists
225
+ try {
226
+ await fs.mkdir(claudeDir, { recursive: true });
227
+ } catch (error) {
228
+ if (error.code !== 'EEXIST') {
229
+ throw error;
230
+ }
231
+ }
232
+
233
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
234
+ }
235
+
236
+ function encodeProjectName(projectPath) {
237
+ return path.resolve(projectPath).replace(/[\\/:\s~_]/g, '-');
238
+ }
239
+
240
+ function isTrackedConfigProject(projectConfig) {
241
+ return Boolean(projectConfig?.manuallyAdded || projectConfig?.autoDiscovered);
242
+ }
243
+
244
+ function mergeUniqueProviders(existingProviders = [], incomingProviders = []) {
245
+ return Array.from(new Set([
246
+ ...(Array.isArray(existingProviders) ? existingProviders : []),
247
+ ...(Array.isArray(incomingProviders) ? incomingProviders : []),
248
+ ])).sort();
249
+ }
250
+
251
+ function areStringArraysEqual(first, second) {
252
+ if (!Array.isArray(first) || !Array.isArray(second)) {
253
+ return false;
254
+ }
255
+
256
+ if (first.length !== second.length) {
257
+ return false;
258
+ }
259
+
260
+ return first.every((value, index) => value === second[index]);
261
+ }
262
+
263
+ async function listGeminiCliProjectEntries() {
264
+ const geminiTmpDir = path.join(os.homedir(), '.gemini', 'tmp');
265
+ try {
266
+ await fs.access(geminiTmpDir);
267
+ } catch {
268
+ return [];
269
+ }
270
+
271
+ let projectDirs;
272
+ try {
273
+ projectDirs = await fs.readdir(geminiTmpDir);
274
+ } catch {
275
+ return [];
276
+ }
277
+
278
+ const entries = [];
279
+
280
+ for (const projectDir of projectDirs) {
281
+ const projectRootFile = path.join(geminiTmpDir, projectDir, '.project_root');
282
+ let projectRoot;
283
+ try {
284
+ projectRoot = (await fs.readFile(projectRootFile, 'utf8')).trim();
285
+ } catch {
286
+ continue;
287
+ }
288
+
289
+ const normalizedProjectRoot = normalizeComparablePath(projectRoot);
290
+ if (!normalizedProjectRoot) {
291
+ continue;
292
+ }
293
+
294
+ entries.push({
295
+ projectDir,
296
+ projectRoot: path.resolve(projectRoot),
297
+ normalizedProjectRoot,
298
+ });
299
+ }
300
+
301
+ return entries;
302
+ }
303
+
304
+ function addDiscoveredProject(discoveredProjectsByPath, projectPath, provider) {
305
+ if (typeof projectPath !== 'string' || projectPath.trim().length === 0) {
306
+ return;
307
+ }
308
+
309
+ const normalizedProjectPath = normalizeComparablePath(projectPath);
310
+ if (!normalizedProjectPath) {
311
+ return;
312
+ }
313
+
314
+ const absoluteProjectPath = path.resolve(projectPath);
315
+ if (!discoveredProjectsByPath.has(normalizedProjectPath)) {
316
+ discoveredProjectsByPath.set(normalizedProjectPath, {
317
+ path: absoluteProjectPath,
318
+ providers: new Set(),
319
+ });
320
+ }
321
+
322
+ discoveredProjectsByPath.get(normalizedProjectPath).providers.add(provider);
323
+ }
324
+
325
+ async function discoverProjectsFromHistory(codexSessionsIndexRef = null, geminiCliProjectsRef = null) {
326
+ const discoveredProjectsByPath = new Map();
327
+
328
+ if (codexSessionsIndexRef && !codexSessionsIndexRef.sessionsByProject) {
329
+ codexSessionsIndexRef.sessionsByProject = await buildCodexSessionsIndex();
330
+ }
331
+ const codexSessionsByProject = codexSessionsIndexRef?.sessionsByProject || await buildCodexSessionsIndex();
332
+
333
+ for (const sessions of codexSessionsByProject.values()) {
334
+ const sampleSession = sessions.find(session => typeof session.cwd === 'string' && session.cwd.trim().length > 0);
335
+ if (!sampleSession?.cwd) {
336
+ continue;
337
+ }
338
+ addDiscoveredProject(discoveredProjectsByPath, sampleSession.cwd, 'codex');
339
+ }
340
+
341
+ if (geminiCliProjectsRef && !geminiCliProjectsRef.entries) {
342
+ geminiCliProjectsRef.entries = await listGeminiCliProjectEntries();
343
+ }
344
+ const geminiCliEntries = geminiCliProjectsRef?.entries || await listGeminiCliProjectEntries();
345
+ for (const entry of geminiCliEntries) {
346
+ addDiscoveredProject(discoveredProjectsByPath, entry.projectRoot, 'gemini');
347
+ }
348
+
349
+ for (const session of sessionManager.sessions.values()) {
350
+ if (session?.projectPath) {
351
+ addDiscoveredProject(discoveredProjectsByPath, session.projectPath, 'gemini');
352
+ }
353
+ }
354
+
355
+ return Array.from(discoveredProjectsByPath.values()).map((entry) => ({
356
+ path: entry.path,
357
+ providers: Array.from(entry.providers).sort(),
358
+ }));
359
+ }
360
+
361
+ function mergeDiscoveredProjectsIntoConfig(config, discoveredProjects) {
362
+ let changed = false;
363
+ const nextConfig = { ...config };
364
+ const nowIso = new Date().toISOString();
365
+
366
+ for (const discovered of discoveredProjects) {
367
+ const projectName = encodeProjectName(discovered.path);
368
+ const currentConfig = nextConfig[projectName] || {};
369
+
370
+ const mergedProviders = mergeUniqueProviders(
371
+ currentConfig.detectedProviders,
372
+ discovered.providers
373
+ );
374
+
375
+ const nextProjectConfig = {
376
+ ...currentConfig,
377
+ originalPath: currentConfig.originalPath || discovered.path,
378
+ detectedProviders: mergedProviders,
379
+ detectedAt: currentConfig.detectedAt || nowIso,
380
+ };
381
+
382
+ if (currentConfig.manuallyAdded) {
383
+ delete nextProjectConfig.autoDiscovered;
384
+ } else {
385
+ nextProjectConfig.autoDiscovered = true;
386
+ }
387
+
388
+ const providersChanged = !areStringArraysEqual(
389
+ Array.isArray(currentConfig.detectedProviders) ? currentConfig.detectedProviders : [],
390
+ mergedProviders
391
+ );
392
+ const autoDiscoveredChanged = Boolean(currentConfig.autoDiscovered) !== Boolean(nextProjectConfig.autoDiscovered);
393
+ const originalPathChanged = currentConfig.originalPath !== nextProjectConfig.originalPath;
394
+ const detectedAtChanged = !currentConfig.detectedAt && Boolean(nextProjectConfig.detectedAt);
395
+
396
+ if (providersChanged || autoDiscoveredChanged || originalPathChanged || detectedAtChanged || !nextConfig[projectName]) {
397
+ changed = true;
398
+ nextConfig[projectName] = nextProjectConfig;
399
+ }
400
+ }
401
+
402
+ return { config: nextConfig, changed };
403
+ }
404
+
405
+ async function syncAutoDiscoveredProjects(config, codexSessionsIndexRef = null, geminiCliProjectsRef = null) {
406
+ const discoveredProjects = await discoverProjectsFromHistory(codexSessionsIndexRef, geminiCliProjectsRef);
407
+ if (discoveredProjects.length === 0) {
408
+ return config;
409
+ }
410
+
411
+ const mergedConfigResult = mergeDiscoveredProjectsIntoConfig(config, discoveredProjects);
412
+ if (!mergedConfigResult.changed) {
413
+ return config;
414
+ }
415
+
416
+ try {
417
+ await saveProjectConfig(mergedConfigResult.config);
418
+ clearProjectDirectoryCache();
419
+ } catch (error) {
420
+ // Keep discovered projects available in-memory even if persistence fails.
421
+ if (!hasWarnedDiscoveredPersistenceFailure) {
422
+ console.warn('Failed to persist discovered projects to config:', error.message);
423
+ hasWarnedDiscoveredPersistenceFailure = true;
424
+ }
425
+ }
426
+ return mergedConfigResult.config;
427
+ }
428
+
429
+ // Generate better display name from path
430
+ async function generateDisplayName(projectName, actualProjectDir = null) {
431
+ // Use actual project directory if provided, otherwise decode from project name
432
+ let projectPath = actualProjectDir || projectName.replace(/-/g, '/');
433
+
434
+ // Try to read package.json from the project path
435
+ try {
436
+ const packageJsonPath = path.join(projectPath, 'package.json');
437
+ const packageData = await fs.readFile(packageJsonPath, 'utf8');
438
+ const packageJson = JSON.parse(packageData);
439
+
440
+ // Return the name from package.json if it exists
441
+ if (packageJson.name) {
442
+ return packageJson.name;
443
+ }
444
+ } catch (error) {
445
+ // Fall back to path-based naming if package.json doesn't exist or can't be read
446
+ }
447
+
448
+ // If it starts with /, it's an absolute path
449
+ if (projectPath.startsWith('/')) {
450
+ const parts = projectPath.split('/').filter(Boolean);
451
+ // Return only the last folder name
452
+ return parts[parts.length - 1] || projectPath;
453
+ }
454
+
455
+ return projectPath;
456
+ }
457
+
458
+ // Extract the actual project directory from JSONL sessions (with caching)
459
+ async function extractProjectDirectory(projectName) {
460
+ // Check cache first
461
+ if (projectDirectoryCache.has(projectName)) {
462
+ return projectDirectoryCache.get(projectName);
463
+ }
464
+
465
+ // Check project config for originalPath (manually added projects via UI or platform)
466
+ // This handles projects with dashes in their directory names correctly
467
+ const config = await loadProjectConfig();
468
+ if (config[projectName]?.originalPath) {
469
+ const originalPath = config[projectName].originalPath;
470
+ projectDirectoryCache.set(projectName, originalPath);
471
+ return originalPath;
472
+ }
473
+
474
+ const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
475
+ const cwdCounts = new Map();
476
+ let latestTimestamp = 0;
477
+ let latestCwd = null;
478
+ let extractedPath;
479
+
480
+ try {
481
+ // Check if the project directory exists
482
+ await fs.access(projectDir);
483
+
484
+ const files = await fs.readdir(projectDir);
485
+ const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
486
+
487
+ if (jsonlFiles.length === 0) {
488
+ // Fall back to decoded project name if no sessions
489
+ extractedPath = projectName.replace(/-/g, '/');
490
+ } else {
491
+ // Process all JSONL files to collect cwd values
492
+ for (const file of jsonlFiles) {
493
+ const jsonlFile = path.join(projectDir, file);
494
+ const fileStream = fsSync.createReadStream(jsonlFile);
495
+ const rl = readline.createInterface({
496
+ input: fileStream,
497
+ crlfDelay: Infinity
498
+ });
499
+
500
+ for await (const line of rl) {
501
+ if (line.trim()) {
502
+ try {
503
+ const entry = JSON.parse(line);
504
+
505
+ if (entry.cwd) {
506
+ // Count occurrences of each cwd
507
+ cwdCounts.set(entry.cwd, (cwdCounts.get(entry.cwd) || 0) + 1);
508
+
509
+ // Track the most recent cwd
510
+ const timestamp = new Date(entry.timestamp || 0).getTime();
511
+ if (timestamp > latestTimestamp) {
512
+ latestTimestamp = timestamp;
513
+ latestCwd = entry.cwd;
514
+ }
515
+ }
516
+ } catch (parseError) {
517
+ // Skip malformed lines
518
+ }
519
+ }
520
+ }
521
+ }
522
+
523
+ // Determine the best cwd to use
524
+ if (cwdCounts.size === 0) {
525
+ // No cwd found, fall back to decoded project name
526
+ extractedPath = projectName.replace(/-/g, '/');
527
+ } else if (cwdCounts.size === 1) {
528
+ // Only one cwd, use it
529
+ extractedPath = Array.from(cwdCounts.keys())[0];
530
+ } else {
531
+ // Multiple cwd values - prefer the most recent one if it has reasonable usage
532
+ const mostRecentCount = cwdCounts.get(latestCwd) || 0;
533
+ const maxCount = Math.max(...cwdCounts.values());
534
+
535
+ // Use most recent if it has at least 25% of the max count
536
+ if (mostRecentCount >= maxCount * 0.25) {
537
+ extractedPath = latestCwd;
538
+ } else {
539
+ // Otherwise use the most frequently used cwd
540
+ for (const [cwd, count] of cwdCounts.entries()) {
541
+ if (count === maxCount) {
542
+ extractedPath = cwd;
543
+ break;
544
+ }
545
+ }
546
+ }
547
+
548
+ // Fallback (shouldn't reach here)
549
+ if (!extractedPath) {
550
+ extractedPath = latestCwd || projectName.replace(/-/g, '/');
551
+ }
552
+ }
553
+ }
554
+
555
+ // Cache the result
556
+ projectDirectoryCache.set(projectName, extractedPath);
557
+
558
+ return extractedPath;
559
+
560
+ } catch (error) {
561
+ // If the directory doesn't exist, just use the decoded project name
562
+ if (error.code === 'ENOENT') {
563
+ extractedPath = projectName.replace(/-/g, '/');
564
+ } else {
565
+ console.error(`Error extracting project directory for ${projectName}:`, error);
566
+ // Fall back to decoded project name for other errors
567
+ extractedPath = projectName.replace(/-/g, '/');
568
+ }
569
+
570
+ // Cache the fallback result too
571
+ projectDirectoryCache.set(projectName, extractedPath);
572
+
573
+ return extractedPath;
574
+ }
575
+ }
576
+
577
+ async function getProjects(progressCallback = null) {
578
+ const claudeDir = path.join(os.homedir(), '.claude', 'projects');
579
+ let config = await loadProjectConfig();
580
+ const projects = [];
581
+ const existingProjects = new Set();
582
+ const codexSessionsIndexRef = { sessionsByProject: null };
583
+ const geminiCliProjectsRef = { entries: null };
584
+ let totalProjects = 0;
585
+ let processedProjects = 0;
586
+ let directories = [];
587
+
588
+ try {
589
+ config = await syncAutoDiscoveredProjects(config, codexSessionsIndexRef, geminiCliProjectsRef);
590
+ } catch (error) {
591
+ console.warn('Failed to sync auto-discovered projects:', error.message);
592
+ }
593
+
594
+ try {
595
+ // Check if the .claude/projects directory exists
596
+ await fs.access(claudeDir);
597
+
598
+ // First, get existing Claude projects from the file system
599
+ const entries = await fs.readdir(claudeDir, { withFileTypes: true });
600
+ directories = entries.filter(e => e.isDirectory());
601
+
602
+ // Build set of existing project names for later
603
+ directories.forEach(e => existingProjects.add(e.name));
604
+ } catch (error) {
605
+ // If the directory doesn't exist (ENOENT), that's okay - just continue with empty projects
606
+ if (error.code !== 'ENOENT') {
607
+ console.error('Error reading projects directory:', error);
608
+ }
609
+ }
610
+
611
+ // Count tracked config projects (manual + auto discovered) not already represented by Claude folders
612
+ const trackedProjectsCount = Object.entries(config)
613
+ .filter(([name, cfg]) => isTrackedConfigProject(cfg) && !existingProjects.has(name))
614
+ .length;
615
+ totalProjects = directories.length + trackedProjectsCount;
616
+
617
+ for (const entry of directories) {
618
+ processedProjects++;
619
+
620
+ if (progressCallback) {
621
+ progressCallback({
622
+ phase: 'loading',
623
+ current: processedProjects,
624
+ total: totalProjects,
625
+ currentProject: entry.name
626
+ });
627
+ }
628
+
629
+ const actualProjectDir = await extractProjectDirectory(entry.name);
630
+ const projectConfig = config[entry.name] || {};
631
+ const isManuallyAdded = Boolean(projectConfig.manuallyAdded);
632
+ const isAutoDiscovered = Boolean(projectConfig.autoDiscovered) && !isManuallyAdded;
633
+ const customName = projectConfig.displayName;
634
+ const autoDisplayName = await generateDisplayName(entry.name, actualProjectDir);
635
+
636
+ const project = {
637
+ name: entry.name,
638
+ path: actualProjectDir,
639
+ displayName: customName || autoDisplayName,
640
+ fullPath: actualProjectDir,
641
+ isCustomName: !!customName,
642
+ isManuallyAdded,
643
+ autoDiscovered: isAutoDiscovered,
644
+ source: isManuallyAdded ? 'manual' : 'claude',
645
+ detectedProviders: projectConfig.detectedProviders || [],
646
+ detectedAt: projectConfig.detectedAt || null,
647
+ sessions: [],
648
+ geminiSessions: [],
649
+ sessionMeta: {
650
+ hasMore: false,
651
+ total: 0
652
+ }
653
+ };
654
+
655
+ // Try to get sessions for this project (just first 5 for performance)
656
+ try {
657
+ const sessionResult = await getSessions(entry.name, 5, 0);
658
+ project.sessions = sessionResult.sessions || [];
659
+ project.sessionMeta = {
660
+ hasMore: sessionResult.hasMore,
661
+ total: sessionResult.total
662
+ };
663
+ } catch (e) {
664
+ console.warn(`Could not load sessions for project ${entry.name}:`, e.message);
665
+ project.sessionMeta = {
666
+ hasMore: false,
667
+ total: 0
668
+ };
669
+ }
670
+ applyCustomSessionNames(project.sessions, 'claude');
671
+
672
+ try {
673
+ project.cursorSessions = await getCursorSessions(actualProjectDir);
674
+ } catch (e) {
675
+ console.warn(`Could not load Cursor sessions for project ${entry.name}:`, e.message);
676
+ project.cursorSessions = [];
677
+ }
678
+ applyCustomSessionNames(project.cursorSessions, 'cursor');
679
+
680
+ try {
681
+ project.codexSessions = await getCodexSessions(actualProjectDir, {
682
+ indexRef: codexSessionsIndexRef,
683
+ });
684
+ } catch (e) {
685
+ console.warn(`Could not load Codex sessions for project ${entry.name}:`, e.message);
686
+ project.codexSessions = [];
687
+ }
688
+ applyCustomSessionNames(project.codexSessions, 'codex');
689
+
690
+ try {
691
+ const uiSessions = sessionManager.getProjectSessions(actualProjectDir) || [];
692
+ const cliSessions = await getGeminiCliSessions(actualProjectDir, {
693
+ geminiCliProjectsRef,
694
+ });
695
+ const uiIds = new Set(uiSessions.map(s => s.id));
696
+ project.geminiSessions = [...uiSessions, ...cliSessions.filter(s => !uiIds.has(s.id))];
697
+ } catch (e) {
698
+ console.warn(`Could not load Gemini sessions for project ${entry.name}:`, e.message);
699
+ project.geminiSessions = [];
700
+ }
701
+ applyCustomSessionNames(project.geminiSessions, 'gemini');
702
+
703
+ try {
704
+ const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
705
+ project.taskmaster = {
706
+ hasTaskmaster: taskMasterResult.hasTaskmaster,
707
+ hasEssentialFiles: taskMasterResult.hasEssentialFiles,
708
+ metadata: taskMasterResult.metadata,
709
+ status: taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles ? 'configured' : 'not-configured'
710
+ };
711
+ } catch (e) {
712
+ console.warn(`Could not detect TaskMaster for project ${entry.name}:`, e.message);
713
+ project.taskmaster = {
714
+ hasTaskmaster: false,
715
+ hasEssentialFiles: false,
716
+ metadata: null,
717
+ status: 'error'
718
+ };
719
+ }
720
+
721
+ projects.push(project);
722
+ }
723
+
724
+ // Add tracked projects from config that do not currently have Claude folders.
725
+ for (const [projectName, projectConfig] of Object.entries(config)) {
726
+ if (!existingProjects.has(projectName) && isTrackedConfigProject(projectConfig)) {
727
+ processedProjects++;
728
+
729
+ if (progressCallback) {
730
+ progressCallback({
731
+ phase: 'loading',
732
+ current: processedProjects,
733
+ total: totalProjects,
734
+ currentProject: projectName
735
+ });
736
+ }
737
+
738
+ let actualProjectDir = projectConfig.originalPath;
739
+ if (!actualProjectDir) {
740
+ try {
741
+ actualProjectDir = await extractProjectDirectory(projectName);
742
+ } catch (error) {
743
+ // Fall back to decoded project name
744
+ actualProjectDir = projectName.replace(/-/g, '/');
745
+ }
746
+ }
747
+
748
+ const isManuallyAdded = Boolean(projectConfig.manuallyAdded);
749
+ const isAutoDiscovered = Boolean(projectConfig.autoDiscovered) && !isManuallyAdded;
750
+ const source = isManuallyAdded ? 'manual' : 'history';
751
+
752
+ const project = {
753
+ name: projectName,
754
+ path: actualProjectDir,
755
+ displayName: projectConfig.displayName || await generateDisplayName(projectName, actualProjectDir),
756
+ fullPath: actualProjectDir,
757
+ isCustomName: !!projectConfig.displayName,
758
+ isManuallyAdded,
759
+ autoDiscovered: isAutoDiscovered,
760
+ source,
761
+ detectedProviders: projectConfig.detectedProviders || [],
762
+ detectedAt: projectConfig.detectedAt || null,
763
+ sessions: [],
764
+ geminiSessions: [],
765
+ sessionMeta: {
766
+ hasMore: false,
767
+ total: 0
768
+ },
769
+ cursorSessions: [],
770
+ codexSessions: []
771
+ };
772
+
773
+ try {
774
+ project.cursorSessions = await getCursorSessions(actualProjectDir);
775
+ } catch (e) {
776
+ console.warn(`Could not load Cursor sessions for tracked project ${projectName}:`, e.message);
777
+ }
778
+ applyCustomSessionNames(project.cursorSessions, 'cursor');
779
+
780
+ try {
781
+ project.codexSessions = await getCodexSessions(actualProjectDir, {
782
+ indexRef: codexSessionsIndexRef,
783
+ });
784
+ } catch (e) {
785
+ console.warn(`Could not load Codex sessions for tracked project ${projectName}:`, e.message);
786
+ }
787
+ applyCustomSessionNames(project.codexSessions, 'codex');
788
+
789
+ try {
790
+ const uiSessions = sessionManager.getProjectSessions(actualProjectDir) || [];
791
+ const cliSessions = await getGeminiCliSessions(actualProjectDir, {
792
+ geminiCliProjectsRef,
793
+ });
794
+ const uiIds = new Set(uiSessions.map(s => s.id));
795
+ project.geminiSessions = [...uiSessions, ...cliSessions.filter(s => !uiIds.has(s.id))];
796
+ } catch (e) {
797
+ console.warn(`Could not load Gemini sessions for tracked project ${projectName}:`, e.message);
798
+ }
799
+ applyCustomSessionNames(project.geminiSessions, 'gemini');
800
+
801
+ try {
802
+ const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
803
+
804
+ let taskMasterStatus = 'not-configured';
805
+ if (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles) {
806
+ taskMasterStatus = 'taskmaster-only';
807
+ }
808
+
809
+ project.taskmaster = {
810
+ status: taskMasterStatus,
811
+ hasTaskmaster: taskMasterResult.hasTaskmaster,
812
+ hasEssentialFiles: taskMasterResult.hasEssentialFiles,
813
+ metadata: taskMasterResult.metadata
814
+ };
815
+ } catch (error) {
816
+ console.warn(`TaskMaster detection failed for tracked project ${projectName}:`, error.message);
817
+ project.taskmaster = {
818
+ status: 'error',
819
+ hasTaskmaster: false,
820
+ hasEssentialFiles: false,
821
+ error: error.message
822
+ };
823
+ }
824
+
825
+ projects.push(project);
826
+ }
827
+ }
828
+
829
+ if (progressCallback) {
830
+ progressCallback({
831
+ phase: 'complete',
832
+ current: totalProjects,
833
+ total: totalProjects
834
+ });
835
+ }
836
+
837
+ return projects;
838
+ }
839
+
840
+ async function getSessions(projectName, limit = 5, offset = 0) {
841
+ const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
842
+
843
+ try {
844
+ const files = await fs.readdir(projectDir);
845
+ // agent-*.jsonl files contain session start data at this point. This needs to be revisited
846
+ // periodically to make sure only accurate data is there and no new functionality is added there
847
+ const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
848
+
849
+ if (jsonlFiles.length === 0) {
850
+ return { sessions: [], hasMore: false, total: 0 };
851
+ }
852
+
853
+ // Sort files by modification time (newest first)
854
+ const filesWithStats = await Promise.all(
855
+ jsonlFiles.map(async (file) => {
856
+ const filePath = path.join(projectDir, file);
857
+ const stats = await fs.stat(filePath);
858
+ return { file, mtime: stats.mtime };
859
+ })
860
+ );
861
+ filesWithStats.sort((a, b) => b.mtime - a.mtime);
862
+
863
+ const allSessions = new Map();
864
+ const allEntries = [];
865
+ const uuidToSessionMap = new Map();
866
+
867
+ // Collect all sessions and entries from all files
868
+ for (const { file } of filesWithStats) {
869
+ const jsonlFile = path.join(projectDir, file);
870
+ const result = await parseJsonlSessions(jsonlFile);
871
+
872
+ result.sessions.forEach(session => {
873
+ if (!allSessions.has(session.id)) {
874
+ allSessions.set(session.id, session);
875
+ }
876
+ });
877
+
878
+ allEntries.push(...result.entries);
879
+
880
+ // Early exit optimization for large projects
881
+ if (allSessions.size >= (limit + offset) * 2 && allEntries.length >= Math.min(3, filesWithStats.length)) {
882
+ break;
883
+ }
884
+ }
885
+
886
+ // Build UUID-to-session mapping for timeline detection
887
+ allEntries.forEach(entry => {
888
+ if (entry.uuid && entry.sessionId) {
889
+ uuidToSessionMap.set(entry.uuid, entry.sessionId);
890
+ }
891
+ });
892
+
893
+ // Group sessions by first user message ID
894
+ const sessionGroups = new Map(); // firstUserMsgId -> { latestSession, allSessions[] }
895
+ const sessionToFirstUserMsgId = new Map(); // sessionId -> firstUserMsgId
896
+
897
+ // Find the first user message for each session
898
+ allEntries.forEach(entry => {
899
+ if (entry.sessionId && entry.type === 'user' && entry.parentUuid === null && entry.uuid) {
900
+ // This is a first user message in a session (parentUuid is null)
901
+ const firstUserMsgId = entry.uuid;
902
+
903
+ if (!sessionToFirstUserMsgId.has(entry.sessionId)) {
904
+ sessionToFirstUserMsgId.set(entry.sessionId, firstUserMsgId);
905
+
906
+ const session = allSessions.get(entry.sessionId);
907
+ if (session) {
908
+ if (!sessionGroups.has(firstUserMsgId)) {
909
+ sessionGroups.set(firstUserMsgId, {
910
+ latestSession: session,
911
+ allSessions: [session]
912
+ });
913
+ } else {
914
+ const group = sessionGroups.get(firstUserMsgId);
915
+ group.allSessions.push(session);
916
+
917
+ // Update latest session if this one is more recent
918
+ if (new Date(session.lastActivity) > new Date(group.latestSession.lastActivity)) {
919
+ group.latestSession = session;
920
+ }
921
+ }
922
+ }
923
+ }
924
+ }
925
+ });
926
+
927
+ // Collect all sessions that don't belong to any group (standalone sessions)
928
+ const groupedSessionIds = new Set();
929
+ sessionGroups.forEach(group => {
930
+ group.allSessions.forEach(session => groupedSessionIds.add(session.id));
931
+ });
932
+
933
+ const standaloneSessionsArray = Array.from(allSessions.values())
934
+ .filter(session => !groupedSessionIds.has(session.id));
935
+
936
+ // Combine grouped sessions (only show latest from each group) + standalone sessions
937
+ const latestFromGroups = Array.from(sessionGroups.values()).map(group => {
938
+ const session = { ...group.latestSession };
939
+ // Add metadata about grouping
940
+ if (group.allSessions.length > 1) {
941
+ session.isGrouped = true;
942
+ session.groupSize = group.allSessions.length;
943
+ session.groupSessions = group.allSessions.map(s => s.id);
944
+ }
945
+ return session;
946
+ });
947
+ const visibleSessions = [...latestFromGroups, ...standaloneSessionsArray]
948
+ .filter(session => !session.summary.startsWith('{ "'))
949
+ .sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
950
+
951
+ const total = visibleSessions.length;
952
+ const paginatedSessions = visibleSessions.slice(offset, offset + limit);
953
+ const hasMore = offset + limit < total;
954
+
955
+ return {
956
+ sessions: paginatedSessions,
957
+ hasMore,
958
+ total,
959
+ offset,
960
+ limit
961
+ };
962
+ } catch (error) {
963
+ console.error(`Error reading sessions for project ${projectName}:`, error);
964
+ return { sessions: [], hasMore: false, total: 0 };
965
+ }
966
+ }
967
+
968
+ async function parseJsonlSessions(filePath) {
969
+ const sessions = new Map();
970
+ const entries = [];
971
+ const pendingSummaries = new Map(); // leafUuid -> summary for entries without sessionId
972
+
973
+ try {
974
+ const fileStream = fsSync.createReadStream(filePath);
975
+ const rl = readline.createInterface({
976
+ input: fileStream,
977
+ crlfDelay: Infinity
978
+ });
979
+
980
+ for await (const line of rl) {
981
+ if (line.trim()) {
982
+ try {
983
+ const entry = JSON.parse(line);
984
+ entries.push(entry);
985
+
986
+ // Handle summary entries that don't have sessionId yet
987
+ if (entry.type === 'summary' && entry.summary && !entry.sessionId && entry.leafUuid) {
988
+ pendingSummaries.set(entry.leafUuid, entry.summary);
989
+ }
990
+
991
+ if (entry.sessionId) {
992
+ if (!sessions.has(entry.sessionId)) {
993
+ sessions.set(entry.sessionId, {
994
+ id: entry.sessionId,
995
+ summary: 'New Session',
996
+ messageCount: 0,
997
+ lastActivity: new Date(),
998
+ cwd: entry.cwd || '',
999
+ lastUserMessage: null,
1000
+ lastAssistantMessage: null
1001
+ });
1002
+ }
1003
+
1004
+ const session = sessions.get(entry.sessionId);
1005
+
1006
+ // Apply pending summary if this entry has a parentUuid that matches a pending summary
1007
+ if (session.summary === 'New Session' && entry.parentUuid && pendingSummaries.has(entry.parentUuid)) {
1008
+ session.summary = pendingSummaries.get(entry.parentUuid);
1009
+ }
1010
+
1011
+ // Update summary from summary entries with sessionId
1012
+ if (entry.type === 'summary' && entry.summary) {
1013
+ session.summary = entry.summary;
1014
+ }
1015
+
1016
+ // Track last user and assistant messages (skip system messages)
1017
+ if (entry.message?.role === 'user' && entry.message?.content) {
1018
+ const content = entry.message.content;
1019
+
1020
+ // Extract text from array format if needed
1021
+ let textContent = content;
1022
+ if (Array.isArray(content) && content.length > 0 && content[0].type === 'text') {
1023
+ textContent = content[0].text;
1024
+ }
1025
+
1026
+ const isSystemMessage = typeof textContent === 'string' && (
1027
+ textContent.startsWith('<command-name>') ||
1028
+ textContent.startsWith('<command-message>') ||
1029
+ textContent.startsWith('<command-args>') ||
1030
+ textContent.startsWith('<local-command-stdout>') ||
1031
+ textContent.startsWith('<system-reminder>') ||
1032
+ textContent.startsWith('Caveat:') ||
1033
+ textContent.startsWith('This session is being continued from a previous') ||
1034
+ textContent.startsWith('Invalid API key') ||
1035
+ textContent.includes('{"subtasks":') || // Filter Task Master prompts
1036
+ textContent.includes('CRITICAL: You MUST respond with ONLY a JSON') || // Filter Task Master system prompts
1037
+ textContent === 'Warmup' // Explicitly filter out "Warmup"
1038
+ );
1039
+
1040
+ if (typeof textContent === 'string' && textContent.length > 0 && !isSystemMessage) {
1041
+ session.lastUserMessage = textContent;
1042
+ }
1043
+ } else if (entry.message?.role === 'assistant' && entry.message?.content) {
1044
+ // Skip API error messages using the isApiErrorMessage flag
1045
+ if (entry.isApiErrorMessage === true) {
1046
+ // Skip this message entirely
1047
+ } else {
1048
+ // Track last assistant text message
1049
+ let assistantText = null;
1050
+
1051
+ if (Array.isArray(entry.message.content)) {
1052
+ for (const part of entry.message.content) {
1053
+ if (part.type === 'text' && part.text) {
1054
+ assistantText = part.text;
1055
+ }
1056
+ }
1057
+ } else if (typeof entry.message.content === 'string') {
1058
+ assistantText = entry.message.content;
1059
+ }
1060
+
1061
+ // Additional filter for assistant messages with system content
1062
+ const isSystemAssistantMessage = typeof assistantText === 'string' && (
1063
+ assistantText.startsWith('Invalid API key') ||
1064
+ assistantText.includes('{"subtasks":') ||
1065
+ assistantText.includes('CRITICAL: You MUST respond with ONLY a JSON')
1066
+ );
1067
+
1068
+ if (assistantText && !isSystemAssistantMessage) {
1069
+ session.lastAssistantMessage = assistantText;
1070
+ }
1071
+ }
1072
+ }
1073
+
1074
+ session.messageCount++;
1075
+
1076
+ if (entry.timestamp) {
1077
+ session.lastActivity = new Date(entry.timestamp);
1078
+ }
1079
+ }
1080
+ } catch (parseError) {
1081
+ // Skip malformed lines silently
1082
+ }
1083
+ }
1084
+ }
1085
+
1086
+ // After processing all entries, set final summary based on last message if no summary exists
1087
+ for (const session of sessions.values()) {
1088
+ if (session.summary === 'New Session') {
1089
+ // Prefer last user message, fall back to last assistant message
1090
+ const lastMessage = session.lastUserMessage || session.lastAssistantMessage;
1091
+ if (lastMessage) {
1092
+ session.summary = lastMessage.length > 50 ? lastMessage.substring(0, 50) + '...' : lastMessage;
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ // Filter out sessions that contain JSON responses (Task Master errors)
1098
+ const allSessions = Array.from(sessions.values());
1099
+ const filteredSessions = allSessions.filter(session => {
1100
+ const shouldFilter = session.summary.startsWith('{ "');
1101
+ if (shouldFilter) {
1102
+ }
1103
+ // Log a sample of summaries to debug
1104
+ if (Math.random() < 0.01) { // Log 1% of sessions
1105
+ }
1106
+ return !shouldFilter;
1107
+ });
1108
+
1109
+
1110
+ return {
1111
+ sessions: filteredSessions,
1112
+ entries: entries
1113
+ };
1114
+
1115
+ } catch (error) {
1116
+ console.error('Error reading JSONL file:', error);
1117
+ return { sessions: [], entries: [] };
1118
+ }
1119
+ }
1120
+
1121
+ // Parse an agent JSONL file and extract tool uses
1122
+ async function parseAgentTools(filePath) {
1123
+ const tools = [];
1124
+
1125
+ try {
1126
+ const fileStream = fsSync.createReadStream(filePath);
1127
+ const rl = readline.createInterface({
1128
+ input: fileStream,
1129
+ crlfDelay: Infinity
1130
+ });
1131
+
1132
+ for await (const line of rl) {
1133
+ if (line.trim()) {
1134
+ try {
1135
+ const entry = JSON.parse(line);
1136
+ // Look for assistant messages with tool_use
1137
+ if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
1138
+ for (const part of entry.message.content) {
1139
+ if (part.type === 'tool_use') {
1140
+ tools.push({
1141
+ toolId: part.id,
1142
+ toolName: part.name,
1143
+ toolInput: part.input,
1144
+ timestamp: entry.timestamp
1145
+ });
1146
+ }
1147
+ }
1148
+ }
1149
+ // Look for tool results
1150
+ if (entry.message?.role === 'user' && Array.isArray(entry.message?.content)) {
1151
+ for (const part of entry.message.content) {
1152
+ if (part.type === 'tool_result') {
1153
+ // Find the matching tool and add result
1154
+ const tool = tools.find(t => t.toolId === part.tool_use_id);
1155
+ if (tool) {
1156
+ tool.toolResult = {
1157
+ content: typeof part.content === 'string' ? part.content :
1158
+ Array.isArray(part.content) ? part.content.map(c => c.text || '').join('\n') :
1159
+ JSON.stringify(part.content),
1160
+ isError: Boolean(part.is_error)
1161
+ };
1162
+ }
1163
+ }
1164
+ }
1165
+ }
1166
+ } catch (parseError) {
1167
+ // Skip malformed lines
1168
+ }
1169
+ }
1170
+ }
1171
+ } catch (error) {
1172
+ console.warn(`Error parsing agent file ${filePath}:`, error.message);
1173
+ }
1174
+
1175
+ return tools;
1176
+ }
1177
+
1178
+ // Get messages for a specific session with pagination support
1179
+ async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
1180
+ const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1181
+
1182
+ try {
1183
+ const files = await fs.readdir(projectDir);
1184
+ // agent-*.jsonl files contain subagent tool history - we'll process them separately
1185
+ const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
1186
+ const agentFiles = files.filter(file => file.endsWith('.jsonl') && file.startsWith('agent-'));
1187
+
1188
+ if (jsonlFiles.length === 0) {
1189
+ return { messages: [], total: 0, hasMore: false };
1190
+ }
1191
+
1192
+ const messages = [];
1193
+ // Map of agentId -> tools for subagent tool grouping
1194
+ const agentToolsCache = new Map();
1195
+
1196
+ // Process all JSONL files to find messages for this session
1197
+ for (const file of jsonlFiles) {
1198
+ const jsonlFile = path.join(projectDir, file);
1199
+ const fileStream = fsSync.createReadStream(jsonlFile);
1200
+ const rl = readline.createInterface({
1201
+ input: fileStream,
1202
+ crlfDelay: Infinity
1203
+ });
1204
+
1205
+ for await (const line of rl) {
1206
+ if (line.trim()) {
1207
+ try {
1208
+ const entry = JSON.parse(line);
1209
+ if (entry.sessionId === sessionId) {
1210
+ messages.push(entry);
1211
+ }
1212
+ } catch (parseError) {
1213
+ // Silently skip malformed JSONL lines (common with concurrent writes)
1214
+ }
1215
+ }
1216
+ }
1217
+ }
1218
+
1219
+ // Collect agentIds from Task tool results
1220
+ const agentIds = new Set();
1221
+ for (const message of messages) {
1222
+ if (message.toolUseResult?.agentId) {
1223
+ agentIds.add(message.toolUseResult.agentId);
1224
+ }
1225
+ }
1226
+
1227
+ // Load agent tools for each agentId found
1228
+ for (const agentId of agentIds) {
1229
+ const agentFileName = `agent-${agentId}.jsonl`;
1230
+ if (agentFiles.includes(agentFileName)) {
1231
+ const agentFilePath = path.join(projectDir, agentFileName);
1232
+ const tools = await parseAgentTools(agentFilePath);
1233
+ agentToolsCache.set(agentId, tools);
1234
+ }
1235
+ }
1236
+
1237
+ // Attach agent tools to their parent Task messages
1238
+ for (const message of messages) {
1239
+ if (message.toolUseResult?.agentId) {
1240
+ const agentId = message.toolUseResult.agentId;
1241
+ const agentTools = agentToolsCache.get(agentId);
1242
+ if (agentTools && agentTools.length > 0) {
1243
+ message.subagentTools = agentTools;
1244
+ }
1245
+ }
1246
+ }
1247
+ // Sort messages by timestamp
1248
+ const sortedMessages = messages.sort((a, b) =>
1249
+ new Date(a.timestamp || 0) - new Date(b.timestamp || 0)
1250
+ );
1251
+
1252
+ const total = sortedMessages.length;
1253
+
1254
+ // If no limit is specified, return all messages (backward compatibility)
1255
+ if (limit === null) {
1256
+ return sortedMessages;
1257
+ }
1258
+
1259
+ // Apply pagination - for recent messages, we need to slice from the end
1260
+ // offset 0 should give us the most recent messages
1261
+ const startIndex = Math.max(0, total - offset - limit);
1262
+ const endIndex = total - offset;
1263
+ const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
1264
+ const hasMore = startIndex > 0;
1265
+
1266
+ return {
1267
+ messages: paginatedMessages,
1268
+ total,
1269
+ hasMore,
1270
+ offset,
1271
+ limit
1272
+ };
1273
+ } catch (error) {
1274
+ console.error(`Error reading messages for session ${sessionId}:`, error);
1275
+ return limit === null ? [] : { messages: [], total: 0, hasMore: false };
1276
+ }
1277
+ }
1278
+
1279
+ // Rename a project's display name
1280
+ async function renameProject(projectName, newDisplayName) {
1281
+ const config = await loadProjectConfig();
1282
+
1283
+ if (!newDisplayName || newDisplayName.trim() === '') {
1284
+ // Remove custom name if empty, will fall back to auto-generated
1285
+ if (config[projectName]) {
1286
+ delete config[projectName].displayName;
1287
+ }
1288
+ } else {
1289
+ // Set custom display name, preserving other properties (manuallyAdded, originalPath)
1290
+ config[projectName] = {
1291
+ ...config[projectName],
1292
+ displayName: newDisplayName.trim()
1293
+ };
1294
+ }
1295
+
1296
+ await saveProjectConfig(config);
1297
+ return true;
1298
+ }
1299
+
1300
+ // Delete a session from a project
1301
+ async function deleteSession(projectName, sessionId) {
1302
+ const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1303
+
1304
+ try {
1305
+ const files = await fs.readdir(projectDir);
1306
+ const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
1307
+
1308
+ if (jsonlFiles.length === 0) {
1309
+ throw new Error('No session files found for this project');
1310
+ }
1311
+
1312
+ // Check all JSONL files to find which one contains the session
1313
+ for (const file of jsonlFiles) {
1314
+ const jsonlFile = path.join(projectDir, file);
1315
+ const content = await fs.readFile(jsonlFile, 'utf8');
1316
+ const lines = content.split('\n').filter(line => line.trim());
1317
+
1318
+ // Check if this file contains the session
1319
+ const hasSession = lines.some(line => {
1320
+ try {
1321
+ const data = JSON.parse(line);
1322
+ return data.sessionId === sessionId;
1323
+ } catch {
1324
+ return false;
1325
+ }
1326
+ });
1327
+
1328
+ if (hasSession) {
1329
+ // Filter out all entries for this session
1330
+ const filteredLines = lines.filter(line => {
1331
+ try {
1332
+ const data = JSON.parse(line);
1333
+ return data.sessionId !== sessionId;
1334
+ } catch {
1335
+ return true; // Keep malformed lines
1336
+ }
1337
+ });
1338
+
1339
+ // Write back the filtered content
1340
+ await fs.writeFile(jsonlFile, filteredLines.join('\n') + (filteredLines.length > 0 ? '\n' : ''));
1341
+ return true;
1342
+ }
1343
+ }
1344
+
1345
+ throw new Error(`Session ${sessionId} not found in any files`);
1346
+ } catch (error) {
1347
+ console.error(`Error deleting session ${sessionId} from project ${projectName}:`, error);
1348
+ throw error;
1349
+ }
1350
+ }
1351
+
1352
+ // Check if a project is empty (has no sessions)
1353
+ async function isProjectEmpty(projectName) {
1354
+ try {
1355
+ const sessionsResult = await getSessions(projectName, 1, 0);
1356
+ return sessionsResult.total === 0;
1357
+ } catch (error) {
1358
+ console.error(`Error checking if project ${projectName} is empty:`, error);
1359
+ return false;
1360
+ }
1361
+ }
1362
+
1363
+ // Remove a project from the UI.
1364
+ // When deleteData=true, also delete session/memory files on disk (destructive).
1365
+ async function deleteProject(projectName, force = false, deleteData = false) {
1366
+ const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1367
+
1368
+ try {
1369
+ const isEmpty = await isProjectEmpty(projectName);
1370
+ if (!isEmpty && !force) {
1371
+ throw new Error('Cannot delete project with existing sessions');
1372
+ }
1373
+
1374
+ const config = await loadProjectConfig();
1375
+
1376
+ // Destructive path: delete underlying data when explicitly requested
1377
+ if (deleteData) {
1378
+ let projectPath = config[projectName]?.path || config[projectName]?.originalPath;
1379
+ if (!projectPath) {
1380
+ projectPath = await extractProjectDirectory(projectName);
1381
+ }
1382
+
1383
+ // Remove the Claude project directory (session logs, memory, subagent data)
1384
+ await fs.rm(projectDir, { recursive: true, force: true });
1385
+
1386
+ // Delete Codex sessions associated with this project
1387
+ if (projectPath) {
1388
+ try {
1389
+ const codexSessions = await getCodexSessions(projectPath, { limit: 0 });
1390
+ for (const session of codexSessions) {
1391
+ try {
1392
+ await deleteCodexSession(session.id);
1393
+ } catch (err) {
1394
+ console.warn(`Failed to delete Codex session ${session.id}:`, err.message);
1395
+ }
1396
+ }
1397
+ } catch (err) {
1398
+ console.warn('Failed to delete Codex sessions:', err.message);
1399
+ }
1400
+
1401
+ // Delete Cursor sessions directory if it exists
1402
+ try {
1403
+ const hash = crypto.createHash('md5').update(projectPath).digest('hex');
1404
+ const cursorProjectDir = path.join(os.homedir(), '.cursor', 'chats', hash);
1405
+ await fs.rm(cursorProjectDir, { recursive: true, force: true });
1406
+ } catch (err) {
1407
+ // Cursor dir may not exist, ignore
1408
+ }
1409
+ }
1410
+ }
1411
+
1412
+ // Always remove from project config
1413
+ delete config[projectName];
1414
+ await saveProjectConfig(config);
1415
+
1416
+ return true;
1417
+ } catch (error) {
1418
+ console.error(`Error removing project ${projectName}:`, error);
1419
+ throw error;
1420
+ }
1421
+ }
1422
+
1423
+ // Add a project manually to the config (without creating folders)
1424
+ async function addProjectManually(projectPath, displayName = null) {
1425
+ const absolutePath = path.resolve(projectPath);
1426
+
1427
+ try {
1428
+ // Check if the path exists
1429
+ await fs.access(absolutePath);
1430
+ } catch (error) {
1431
+ throw new Error(`Path does not exist: ${absolutePath}`);
1432
+ }
1433
+
1434
+ // Generate project name (encode path for use as directory name)
1435
+ const projectName = encodeProjectName(absolutePath);
1436
+
1437
+ // Check if project already exists in config
1438
+ const config = await loadProjectConfig();
1439
+
1440
+ if (config[projectName]) {
1441
+ if (config[projectName].manuallyAdded) {
1442
+ throw new Error(`Project already configured for path: ${absolutePath}`);
1443
+ }
1444
+
1445
+ // Upgrade discovered project to manual tracking.
1446
+ config[projectName] = {
1447
+ ...config[projectName],
1448
+ manuallyAdded: true,
1449
+ originalPath: absolutePath
1450
+ };
1451
+ delete config[projectName].autoDiscovered;
1452
+ if (displayName) {
1453
+ config[projectName].displayName = displayName;
1454
+ }
1455
+
1456
+ await saveProjectConfig(config);
1457
+ clearProjectDirectoryCache();
1458
+
1459
+ return {
1460
+ name: projectName,
1461
+ path: absolutePath,
1462
+ fullPath: absolutePath,
1463
+ displayName: displayName || await generateDisplayName(projectName, absolutePath),
1464
+ isManuallyAdded: true,
1465
+ sessions: [],
1466
+ cursorSessions: []
1467
+ };
1468
+ }
1469
+
1470
+ // Allow adding projects even if the directory exists - this enables tracking
1471
+ // existing Claude Code or Cursor projects in the UI
1472
+
1473
+ // Add to config as manually added project
1474
+ config[projectName] = {
1475
+ manuallyAdded: true,
1476
+ originalPath: absolutePath
1477
+ };
1478
+
1479
+ if (displayName) {
1480
+ config[projectName].displayName = displayName;
1481
+ }
1482
+
1483
+ await saveProjectConfig(config);
1484
+
1485
+
1486
+ return {
1487
+ name: projectName,
1488
+ path: absolutePath,
1489
+ fullPath: absolutePath,
1490
+ displayName: displayName || await generateDisplayName(projectName, absolutePath),
1491
+ isManuallyAdded: true,
1492
+ sessions: [],
1493
+ cursorSessions: []
1494
+ };
1495
+ }
1496
+
1497
+ // Fetch Cursor sessions for a given project path
1498
+ async function getCursorSessions(projectPath) {
1499
+ try {
1500
+ // Calculate cwdID hash for the project path (Cursor uses MD5 hash)
1501
+ const cwdId = crypto.createHash('md5').update(projectPath).digest('hex');
1502
+ const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId);
1503
+
1504
+ // Check if the directory exists
1505
+ try {
1506
+ await fs.access(cursorChatsPath);
1507
+ } catch (error) {
1508
+ // No sessions for this project
1509
+ return [];
1510
+ }
1511
+
1512
+ // List all session directories
1513
+ const sessionDirs = await fs.readdir(cursorChatsPath);
1514
+ const sessions = [];
1515
+
1516
+ for (const sessionId of sessionDirs) {
1517
+ const sessionPath = path.join(cursorChatsPath, sessionId);
1518
+ const storeDbPath = path.join(sessionPath, 'store.db');
1519
+
1520
+ try {
1521
+ // Check if store.db exists
1522
+ await fs.access(storeDbPath);
1523
+
1524
+ // Capture store.db mtime as a reliable fallback timestamp
1525
+ let dbStatMtimeMs = null;
1526
+ try {
1527
+ const stat = await fs.stat(storeDbPath);
1528
+ dbStatMtimeMs = stat.mtimeMs;
1529
+ } catch (_) { }
1530
+
1531
+ // Open SQLite database
1532
+ const db = new Database(storeDbPath, { readonly: true, fileMustExist: true });
1533
+
1534
+ // Get metadata from meta table
1535
+ const metaRows = db.prepare('SELECT key, value FROM meta').all();
1536
+
1537
+ // Parse metadata
1538
+ let metadata = {};
1539
+ for (const row of metaRows) {
1540
+ if (row.value) {
1541
+ try {
1542
+ // Try to decode as hex-encoded JSON
1543
+ const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
1544
+ if (hexMatch) {
1545
+ const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
1546
+ metadata[row.key] = JSON.parse(jsonStr);
1547
+ } else {
1548
+ metadata[row.key] = row.value.toString();
1549
+ }
1550
+ } catch (e) {
1551
+ metadata[row.key] = row.value.toString();
1552
+ }
1553
+ }
1554
+ }
1555
+
1556
+ // Get message count
1557
+ const messageCountResult = db.prepare('SELECT COUNT(*) as count FROM blobs').get();
1558
+
1559
+ db.close();
1560
+
1561
+ // Extract session info
1562
+ const sessionName = metadata.title || metadata.sessionTitle || 'Untitled Session';
1563
+
1564
+ // Determine timestamp - prefer createdAt from metadata, fall back to db file mtime
1565
+ let createdAt = null;
1566
+ if (metadata.createdAt) {
1567
+ createdAt = new Date(metadata.createdAt).toISOString();
1568
+ } else if (dbStatMtimeMs) {
1569
+ createdAt = new Date(dbStatMtimeMs).toISOString();
1570
+ } else {
1571
+ createdAt = new Date().toISOString();
1572
+ }
1573
+
1574
+ sessions.push({
1575
+ id: sessionId,
1576
+ name: sessionName,
1577
+ createdAt: createdAt,
1578
+ lastActivity: createdAt, // For compatibility with Claude sessions
1579
+ messageCount: messageCountResult.count || 0,
1580
+ projectPath: projectPath
1581
+ });
1582
+
1583
+ } catch (error) {
1584
+ console.warn(`Could not read Cursor session ${sessionId}:`, error.message);
1585
+ }
1586
+ }
1587
+
1588
+ // Sort sessions by creation time (newest first)
1589
+ sessions.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
1590
+
1591
+ // Return only the first 5 sessions for performance
1592
+ return sessions.slice(0, 5);
1593
+
1594
+ } catch (error) {
1595
+ console.error('Error fetching Cursor sessions:', error);
1596
+ return [];
1597
+ }
1598
+ }
1599
+
1600
+
1601
+ function normalizeComparablePath(inputPath) {
1602
+ if (!inputPath || typeof inputPath !== 'string') {
1603
+ return '';
1604
+ }
1605
+
1606
+ const withoutLongPathPrefix = inputPath.startsWith('\\\\?\\')
1607
+ ? inputPath.slice(4)
1608
+ : inputPath;
1609
+ const normalized = path.normalize(withoutLongPathPrefix.trim());
1610
+
1611
+ if (!normalized) {
1612
+ return '';
1613
+ }
1614
+
1615
+ const resolved = path.resolve(normalized);
1616
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
1617
+ }
1618
+
1619
+ async function findCodexJsonlFiles(dir) {
1620
+ const files = [];
1621
+
1622
+ try {
1623
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1624
+ for (const entry of entries) {
1625
+ const fullPath = path.join(dir, entry.name);
1626
+ if (entry.isDirectory()) {
1627
+ files.push(...await findCodexJsonlFiles(fullPath));
1628
+ } else if (entry.name.endsWith('.jsonl')) {
1629
+ files.push(fullPath);
1630
+ }
1631
+ }
1632
+ } catch (error) {
1633
+ // Skip directories we can't read
1634
+ }
1635
+
1636
+ return files;
1637
+ }
1638
+
1639
+ async function buildCodexSessionsIndex() {
1640
+ const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1641
+ const sessionsByProject = new Map();
1642
+
1643
+ try {
1644
+ await fs.access(codexSessionsDir);
1645
+ } catch (error) {
1646
+ return sessionsByProject;
1647
+ }
1648
+
1649
+ const jsonlFiles = await findCodexJsonlFiles(codexSessionsDir);
1650
+
1651
+ for (const filePath of jsonlFiles) {
1652
+ try {
1653
+ const sessionData = await parseCodexSessionFile(filePath);
1654
+ if (!sessionData || !sessionData.id) {
1655
+ continue;
1656
+ }
1657
+
1658
+ const normalizedProjectPath = normalizeComparablePath(sessionData.cwd);
1659
+ if (!normalizedProjectPath) {
1660
+ continue;
1661
+ }
1662
+
1663
+ const session = {
1664
+ id: sessionData.id,
1665
+ summary: sessionData.summary || 'Codex Session',
1666
+ messageCount: sessionData.messageCount || 0,
1667
+ lastActivity: sessionData.timestamp ? new Date(sessionData.timestamp) : new Date(),
1668
+ cwd: sessionData.cwd,
1669
+ model: sessionData.model,
1670
+ filePath,
1671
+ provider: 'codex',
1672
+ };
1673
+
1674
+ if (!sessionsByProject.has(normalizedProjectPath)) {
1675
+ sessionsByProject.set(normalizedProjectPath, []);
1676
+ }
1677
+
1678
+ sessionsByProject.get(normalizedProjectPath).push(session);
1679
+ } catch (error) {
1680
+ console.warn(`Could not parse Codex session file ${filePath}:`, error.message);
1681
+ }
1682
+ }
1683
+
1684
+ for (const sessions of sessionsByProject.values()) {
1685
+ sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
1686
+ }
1687
+
1688
+ return sessionsByProject;
1689
+ }
1690
+
1691
+ // Fetch Codex sessions for a given project path
1692
+ async function getCodexSessions(projectPath, options = {}) {
1693
+ const { limit = 5, indexRef = null } = options;
1694
+ try {
1695
+ const normalizedProjectPath = normalizeComparablePath(projectPath);
1696
+ if (!normalizedProjectPath) {
1697
+ return [];
1698
+ }
1699
+
1700
+ if (indexRef && !indexRef.sessionsByProject) {
1701
+ indexRef.sessionsByProject = await buildCodexSessionsIndex();
1702
+ }
1703
+
1704
+ const sessionsByProject = indexRef?.sessionsByProject || await buildCodexSessionsIndex();
1705
+ const sessions = sessionsByProject.get(normalizedProjectPath) || [];
1706
+
1707
+ // Return limited sessions for performance (0 = unlimited for deletion)
1708
+ return limit > 0 ? sessions.slice(0, limit) : [...sessions];
1709
+
1710
+ } catch (error) {
1711
+ console.error('Error fetching Codex sessions:', error);
1712
+ return [];
1713
+ }
1714
+ }
1715
+
1716
+ function isVisibleCodexUserMessage(payload) {
1717
+ if (!payload || payload.type !== 'user_message') {
1718
+ return false;
1719
+ }
1720
+
1721
+ // Codex logs internal context (environment, instructions) as non-plain user_message kinds.
1722
+ if (payload.kind && payload.kind !== 'plain') {
1723
+ return false;
1724
+ }
1725
+
1726
+ if (typeof payload.message !== 'string' || payload.message.trim().length === 0) {
1727
+ return false;
1728
+ }
1729
+
1730
+ return true;
1731
+ }
1732
+
1733
+ // Parse a Codex session JSONL file to extract metadata
1734
+ async function parseCodexSessionFile(filePath) {
1735
+ try {
1736
+ const fileStream = fsSync.createReadStream(filePath);
1737
+ const rl = readline.createInterface({
1738
+ input: fileStream,
1739
+ crlfDelay: Infinity
1740
+ });
1741
+
1742
+ let sessionMeta = null;
1743
+ let lastTimestamp = null;
1744
+ let lastUserMessage = null;
1745
+ let messageCount = 0;
1746
+
1747
+ for await (const line of rl) {
1748
+ if (line.trim()) {
1749
+ try {
1750
+ const entry = JSON.parse(line);
1751
+
1752
+ // Track timestamp
1753
+ if (entry.timestamp) {
1754
+ lastTimestamp = entry.timestamp;
1755
+ }
1756
+
1757
+ // Extract session metadata
1758
+ if (entry.type === 'session_meta' && entry.payload) {
1759
+ sessionMeta = {
1760
+ id: entry.payload.id,
1761
+ cwd: entry.payload.cwd,
1762
+ model: entry.payload.model || entry.payload.model_provider,
1763
+ timestamp: entry.timestamp,
1764
+ git: entry.payload.git
1765
+ };
1766
+ }
1767
+
1768
+ // Count visible user messages and extract summary from the latest plain user input.
1769
+ if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload)) {
1770
+ messageCount++;
1771
+ if (entry.payload.message) {
1772
+ lastUserMessage = entry.payload.message;
1773
+ }
1774
+ }
1775
+
1776
+ if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'assistant') {
1777
+ messageCount++;
1778
+ }
1779
+
1780
+ } catch (parseError) {
1781
+ // Skip malformed lines
1782
+ }
1783
+ }
1784
+ }
1785
+
1786
+ if (sessionMeta) {
1787
+ return {
1788
+ ...sessionMeta,
1789
+ timestamp: lastTimestamp || sessionMeta.timestamp,
1790
+ summary: lastUserMessage ?
1791
+ (lastUserMessage.length > 50 ? lastUserMessage.substring(0, 50) + '...' : lastUserMessage) :
1792
+ 'Codex Session',
1793
+ messageCount
1794
+ };
1795
+ }
1796
+
1797
+ return null;
1798
+
1799
+ } catch (error) {
1800
+ console.error('Error parsing Codex session file:', error);
1801
+ return null;
1802
+ }
1803
+ }
1804
+
1805
+ // Get messages for a specific Codex session
1806
+ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
1807
+ try {
1808
+ const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1809
+
1810
+ // Find the session file by searching for the session ID
1811
+ const findSessionFile = async (dir) => {
1812
+ try {
1813
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1814
+ for (const entry of entries) {
1815
+ const fullPath = path.join(dir, entry.name);
1816
+ if (entry.isDirectory()) {
1817
+ const found = await findSessionFile(fullPath);
1818
+ if (found) return found;
1819
+ } else if (entry.name.includes(sessionId) && entry.name.endsWith('.jsonl')) {
1820
+ return fullPath;
1821
+ }
1822
+ }
1823
+ } catch (error) {
1824
+ // Skip directories we can't read
1825
+ }
1826
+ return null;
1827
+ };
1828
+
1829
+ const sessionFilePath = await findSessionFile(codexSessionsDir);
1830
+
1831
+ if (!sessionFilePath) {
1832
+ console.warn(`Codex session file not found for session ${sessionId}`);
1833
+ return { messages: [], total: 0, hasMore: false };
1834
+ }
1835
+
1836
+ const messages = [];
1837
+ let tokenUsage = null;
1838
+ const fileStream = fsSync.createReadStream(sessionFilePath);
1839
+ const rl = readline.createInterface({
1840
+ input: fileStream,
1841
+ crlfDelay: Infinity
1842
+ });
1843
+
1844
+ // Helper to extract text from Codex content array
1845
+ const extractText = (content) => {
1846
+ if (!Array.isArray(content)) return content;
1847
+ return content
1848
+ .map(item => {
1849
+ if (item.type === 'input_text' || item.type === 'output_text') {
1850
+ return item.text;
1851
+ }
1852
+ if (item.type === 'text') {
1853
+ return item.text;
1854
+ }
1855
+ return '';
1856
+ })
1857
+ .filter(Boolean)
1858
+ .join('\n');
1859
+ };
1860
+
1861
+ for await (const line of rl) {
1862
+ if (line.trim()) {
1863
+ try {
1864
+ const entry = JSON.parse(line);
1865
+
1866
+ // Extract token usage from token_count events (keep latest)
1867
+ if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
1868
+ const info = entry.payload.info;
1869
+ if (info.total_token_usage) {
1870
+ tokenUsage = {
1871
+ used: info.total_token_usage.total_tokens || 0,
1872
+ total: info.model_context_window || 200000
1873
+ };
1874
+ }
1875
+ }
1876
+
1877
+ // Use event_msg.user_message for user-visible inputs.
1878
+ if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload)) {
1879
+ messages.push({
1880
+ type: 'user',
1881
+ timestamp: entry.timestamp,
1882
+ message: {
1883
+ role: 'user',
1884
+ content: entry.payload.message
1885
+ }
1886
+ });
1887
+ }
1888
+
1889
+ // response_item.message may include internal prompts for non-assistant roles.
1890
+ // Keep only assistant output from response_item.
1891
+ if (
1892
+ entry.type === 'response_item' &&
1893
+ entry.payload?.type === 'message' &&
1894
+ entry.payload.role === 'assistant'
1895
+ ) {
1896
+ const content = entry.payload.content;
1897
+ const textContent = extractText(content);
1898
+
1899
+ // Only add if there's actual content
1900
+ if (textContent?.trim()) {
1901
+ messages.push({
1902
+ type: 'assistant',
1903
+ timestamp: entry.timestamp,
1904
+ message: {
1905
+ role: 'assistant',
1906
+ content: textContent
1907
+ }
1908
+ });
1909
+ }
1910
+ }
1911
+
1912
+ if (entry.type === 'response_item' && entry.payload?.type === 'reasoning') {
1913
+ const summaryText = entry.payload.summary
1914
+ ?.map(s => s.text)
1915
+ .filter(Boolean)
1916
+ .join('\n');
1917
+ if (summaryText?.trim()) {
1918
+ messages.push({
1919
+ type: 'thinking',
1920
+ timestamp: entry.timestamp,
1921
+ message: {
1922
+ role: 'assistant',
1923
+ content: summaryText
1924
+ }
1925
+ });
1926
+ }
1927
+ }
1928
+
1929
+ if (entry.type === 'response_item' && entry.payload?.type === 'function_call') {
1930
+ let toolName = entry.payload.name;
1931
+ let toolInput = entry.payload.arguments;
1932
+
1933
+ // Map Codex tool names to Claude equivalents
1934
+ if (toolName === 'shell_command') {
1935
+ toolName = 'Bash';
1936
+ try {
1937
+ const args = JSON.parse(entry.payload.arguments);
1938
+ toolInput = JSON.stringify({ command: args.command });
1939
+ } catch (e) {
1940
+ // Keep original if parsing fails
1941
+ }
1942
+ }
1943
+
1944
+ messages.push({
1945
+ type: 'tool_use',
1946
+ timestamp: entry.timestamp,
1947
+ toolName: toolName,
1948
+ toolInput: toolInput,
1949
+ toolCallId: entry.payload.call_id
1950
+ });
1951
+ }
1952
+
1953
+ if (entry.type === 'response_item' && entry.payload?.type === 'function_call_output') {
1954
+ messages.push({
1955
+ type: 'tool_result',
1956
+ timestamp: entry.timestamp,
1957
+ toolCallId: entry.payload.call_id,
1958
+ output: entry.payload.output
1959
+ });
1960
+ }
1961
+
1962
+ if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call') {
1963
+ const toolName = entry.payload.name || 'custom_tool';
1964
+ const input = entry.payload.input || '';
1965
+
1966
+ if (toolName === 'apply_patch') {
1967
+ // Parse Codex patch format and convert to Claude Edit format
1968
+ const fileMatch = input.match(/\*\*\* Update File: (.+)/);
1969
+ const filePath = fileMatch ? fileMatch[1].trim() : 'unknown';
1970
+
1971
+ // Extract old and new content from patch
1972
+ const lines = input.split('\n');
1973
+ const oldLines = [];
1974
+ const newLines = [];
1975
+
1976
+ for (const line of lines) {
1977
+ if (line.startsWith('-') && !line.startsWith('---')) {
1978
+ oldLines.push(line.substring(1));
1979
+ } else if (line.startsWith('+') && !line.startsWith('+++')) {
1980
+ newLines.push(line.substring(1));
1981
+ }
1982
+ }
1983
+
1984
+ messages.push({
1985
+ type: 'tool_use',
1986
+ timestamp: entry.timestamp,
1987
+ toolName: 'Edit',
1988
+ toolInput: JSON.stringify({
1989
+ file_path: filePath,
1990
+ old_string: oldLines.join('\n'),
1991
+ new_string: newLines.join('\n')
1992
+ }),
1993
+ toolCallId: entry.payload.call_id
1994
+ });
1995
+ } else {
1996
+ messages.push({
1997
+ type: 'tool_use',
1998
+ timestamp: entry.timestamp,
1999
+ toolName: toolName,
2000
+ toolInput: input,
2001
+ toolCallId: entry.payload.call_id
2002
+ });
2003
+ }
2004
+ }
2005
+
2006
+ if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call_output') {
2007
+ messages.push({
2008
+ type: 'tool_result',
2009
+ timestamp: entry.timestamp,
2010
+ toolCallId: entry.payload.call_id,
2011
+ output: entry.payload.output || ''
2012
+ });
2013
+ }
2014
+
2015
+ } catch (parseError) {
2016
+ // Skip malformed lines
2017
+ }
2018
+ }
2019
+ }
2020
+
2021
+ // Sort by timestamp
2022
+ messages.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
2023
+
2024
+ const total = messages.length;
2025
+
2026
+ // Apply pagination if limit is specified
2027
+ if (limit !== null) {
2028
+ const startIndex = Math.max(0, total - offset - limit);
2029
+ const endIndex = total - offset;
2030
+ const paginatedMessages = messages.slice(startIndex, endIndex);
2031
+ const hasMore = startIndex > 0;
2032
+
2033
+ return {
2034
+ messages: paginatedMessages,
2035
+ total,
2036
+ hasMore,
2037
+ offset,
2038
+ limit,
2039
+ tokenUsage
2040
+ };
2041
+ }
2042
+
2043
+ return { messages, tokenUsage };
2044
+
2045
+ } catch (error) {
2046
+ console.error(`Error reading Codex session messages for ${sessionId}:`, error);
2047
+ return { messages: [], total: 0, hasMore: false };
2048
+ }
2049
+ }
2050
+
2051
+ async function deleteCodexSession(sessionId) {
2052
+ try {
2053
+ const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
2054
+
2055
+ const findJsonlFiles = async (dir) => {
2056
+ const files = [];
2057
+ try {
2058
+ const entries = await fs.readdir(dir, { withFileTypes: true });
2059
+ for (const entry of entries) {
2060
+ const fullPath = path.join(dir, entry.name);
2061
+ if (entry.isDirectory()) {
2062
+ files.push(...await findJsonlFiles(fullPath));
2063
+ } else if (entry.name.endsWith('.jsonl')) {
2064
+ files.push(fullPath);
2065
+ }
2066
+ }
2067
+ } catch (error) { }
2068
+ return files;
2069
+ };
2070
+
2071
+ const jsonlFiles = await findJsonlFiles(codexSessionsDir);
2072
+
2073
+ for (const filePath of jsonlFiles) {
2074
+ const sessionData = await parseCodexSessionFile(filePath);
2075
+ if (sessionData && sessionData.id === sessionId) {
2076
+ await fs.unlink(filePath);
2077
+ return true;
2078
+ }
2079
+ }
2080
+
2081
+ throw new Error(`Codex session file not found for session ${sessionId}`);
2082
+ } catch (error) {
2083
+ console.error(`Error deleting Codex session ${sessionId}:`, error);
2084
+ throw error;
2085
+ }
2086
+ }
2087
+
2088
+ async function searchConversations(query, limit = 50, onProjectResult = null, signal = null) {
2089
+ const safeQuery = typeof query === 'string' ? query.trim() : '';
2090
+ const safeLimit = Math.max(1, Math.min(Number.isFinite(limit) ? limit : 50, 200));
2091
+ const claudeDir = path.join(os.homedir(), '.claude', 'projects');
2092
+ let config = await loadProjectConfig();
2093
+ const codexSessionsIndexRef = { sessionsByProject: null };
2094
+ const geminiCliProjectsRef = { entries: null };
2095
+ const results = [];
2096
+ let totalMatches = 0;
2097
+ const words = safeQuery.toLowerCase().split(/\s+/).filter(w => w.length > 0);
2098
+ if (words.length === 0) return { results: [], totalMatches: 0, query: safeQuery };
2099
+
2100
+ const isAborted = () => signal?.aborted === true;
2101
+
2102
+ const isSystemMessage = (textContent) => {
2103
+ return typeof textContent === 'string' && (
2104
+ textContent.startsWith('<command-name>') ||
2105
+ textContent.startsWith('<command-message>') ||
2106
+ textContent.startsWith('<command-args>') ||
2107
+ textContent.startsWith('<local-command-stdout>') ||
2108
+ textContent.startsWith('<system-reminder>') ||
2109
+ textContent.startsWith('Caveat:') ||
2110
+ textContent.startsWith('This session is being continued from a previous') ||
2111
+ textContent.startsWith('Invalid API key') ||
2112
+ textContent.includes('{"subtasks":') ||
2113
+ textContent.includes('CRITICAL: You MUST respond with ONLY a JSON') ||
2114
+ textContent === 'Warmup'
2115
+ );
2116
+ };
2117
+
2118
+ const extractText = (content) => {
2119
+ if (typeof content === 'string') return content;
2120
+ if (Array.isArray(content)) {
2121
+ return content
2122
+ .filter(part => part.type === 'text' && part.text)
2123
+ .map(part => part.text)
2124
+ .join(' ');
2125
+ }
2126
+ return '';
2127
+ };
2128
+
2129
+ const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2130
+ const wordPatterns = words.map(w => new RegExp(`(?<!\\p{L})${escapeRegex(w)}(?!\\p{L})`, 'u'));
2131
+ const allWordsMatch = (textLower) => {
2132
+ return wordPatterns.every(p => p.test(textLower));
2133
+ };
2134
+
2135
+ const buildSnippet = (text, textLower, snippetLen = 150) => {
2136
+ let firstIndex = -1;
2137
+ let firstWordLen = 0;
2138
+ for (const w of words) {
2139
+ const re = new RegExp(`(?<!\\p{L})${escapeRegex(w)}(?!\\p{L})`, 'u');
2140
+ const m = re.exec(textLower);
2141
+ if (m && (firstIndex === -1 || m.index < firstIndex)) {
2142
+ firstIndex = m.index;
2143
+ firstWordLen = w.length;
2144
+ }
2145
+ }
2146
+ if (firstIndex === -1) firstIndex = 0;
2147
+ const halfLen = Math.floor(snippetLen / 2);
2148
+ let start = Math.max(0, firstIndex - halfLen);
2149
+ let end = Math.min(text.length, firstIndex + halfLen + firstWordLen);
2150
+ let snippet = text.slice(start, end).replace(/\n/g, ' ');
2151
+ const prefix = start > 0 ? '...' : '';
2152
+ const suffix = end < text.length ? '...' : '';
2153
+ snippet = prefix + snippet + suffix;
2154
+ const snippetLower = snippet.toLowerCase();
2155
+ const highlights = [];
2156
+ for (const word of words) {
2157
+ const re = new RegExp(`(?<!\\p{L})${escapeRegex(word)}(?!\\p{L})`, 'gu');
2158
+ let match;
2159
+ while ((match = re.exec(snippetLower)) !== null) {
2160
+ highlights.push({ start: match.index, end: match.index + word.length });
2161
+ }
2162
+ }
2163
+ highlights.sort((a, b) => a.start - b.start);
2164
+ const merged = [];
2165
+ for (const h of highlights) {
2166
+ const last = merged[merged.length - 1];
2167
+ if (last && h.start <= last.end) {
2168
+ last.end = Math.max(last.end, h.end);
2169
+ } else {
2170
+ merged.push({ ...h });
2171
+ }
2172
+ }
2173
+ return { snippet, highlights: merged };
2174
+ };
2175
+
2176
+ try {
2177
+ config = await syncAutoDiscoveredProjects(config, codexSessionsIndexRef, geminiCliProjectsRef);
2178
+ } catch (error) {
2179
+ console.warn('Failed to sync auto-discovered projects for search:', error.message);
2180
+ }
2181
+
2182
+ const projectTargetsByName = new Map();
2183
+
2184
+ try {
2185
+ await fs.access(claudeDir);
2186
+ const entries = await fs.readdir(claudeDir, { withFileTypes: true });
2187
+ const projectDirs = entries.filter(e => e.isDirectory());
2188
+ for (const projectDir of projectDirs) {
2189
+ projectTargetsByName.set(projectDir.name, {
2190
+ projectName: projectDir.name,
2191
+ claudeProjectDir: path.join(claudeDir, projectDir.name),
2192
+ projectConfig: config[projectDir.name] || {},
2193
+ });
2194
+ }
2195
+ } catch {
2196
+ // No Claude sessions directory; continue with tracked projects from config.
2197
+ }
2198
+
2199
+ for (const [projectName, projectConfig] of Object.entries(config)) {
2200
+ if (!isTrackedConfigProject(projectConfig)) {
2201
+ continue;
2202
+ }
2203
+
2204
+ const existingTarget = projectTargetsByName.get(projectName);
2205
+ if (existingTarget) {
2206
+ existingTarget.projectConfig = {
2207
+ ...existingTarget.projectConfig,
2208
+ ...projectConfig,
2209
+ };
2210
+ continue;
2211
+ }
2212
+
2213
+ projectTargetsByName.set(projectName, {
2214
+ projectName,
2215
+ claudeProjectDir: null,
2216
+ projectConfig,
2217
+ });
2218
+ }
2219
+
2220
+ const projectTargets = Array.from(projectTargetsByName.values());
2221
+ const totalProjects = projectTargets.length;
2222
+ let scannedProjects = 0;
2223
+
2224
+ for (const projectTarget of projectTargets) {
2225
+ if (totalMatches >= safeLimit || isAborted()) break;
2226
+
2227
+ const projectName = projectTarget.projectName;
2228
+ const projectConfig = projectTarget.projectConfig || {};
2229
+ let actualProjectDir = projectConfig.originalPath;
2230
+ if (!actualProjectDir) {
2231
+ try {
2232
+ actualProjectDir = await extractProjectDirectory(projectName);
2233
+ } catch {
2234
+ actualProjectDir = projectName.replace(/-/g, '/');
2235
+ }
2236
+ }
2237
+
2238
+ const displayName = projectConfig.displayName
2239
+ || await generateDisplayName(projectName, actualProjectDir);
2240
+
2241
+ const projectResult = {
2242
+ projectName,
2243
+ projectDisplayName: displayName,
2244
+ sessions: []
2245
+ };
2246
+
2247
+ if (projectTarget.claudeProjectDir) {
2248
+ let files;
2249
+ try {
2250
+ files = await fs.readdir(projectTarget.claudeProjectDir);
2251
+ } catch {
2252
+ files = [];
2253
+ }
2254
+
2255
+ const jsonlFiles = files.filter(
2256
+ file => file.endsWith('.jsonl') && !file.startsWith('agent-')
2257
+ );
2258
+
2259
+ for (const file of jsonlFiles) {
2260
+ if (totalMatches >= safeLimit || isAborted()) break;
2261
+
2262
+ const filePath = path.join(projectTarget.claudeProjectDir, file);
2263
+ const sessionMatches = new Map();
2264
+ const sessionSummaries = new Map();
2265
+ const pendingSummaries = new Map();
2266
+ const sessionLastMessages = new Map();
2267
+ let currentSessionId = null;
2268
+
2269
+ try {
2270
+ const fileStream = fsSync.createReadStream(filePath);
2271
+ const rl = readline.createInterface({
2272
+ input: fileStream,
2273
+ crlfDelay: Infinity
2274
+ });
2275
+
2276
+ for await (const line of rl) {
2277
+ if (totalMatches >= safeLimit || isAborted()) break;
2278
+ if (!line.trim()) continue;
2279
+
2280
+ let entry;
2281
+ try {
2282
+ entry = JSON.parse(line);
2283
+ } catch {
2284
+ continue;
2285
+ }
2286
+
2287
+ if (entry.sessionId) {
2288
+ currentSessionId = entry.sessionId;
2289
+ }
2290
+ if (entry.type === 'summary' && entry.summary) {
2291
+ const sid = entry.sessionId || currentSessionId;
2292
+ if (sid) {
2293
+ sessionSummaries.set(sid, entry.summary);
2294
+ } else if (entry.leafUuid) {
2295
+ pendingSummaries.set(entry.leafUuid, entry.summary);
2296
+ }
2297
+ }
2298
+
2299
+ if (entry.parentUuid && currentSessionId && !sessionSummaries.has(currentSessionId)) {
2300
+ const pending = pendingSummaries.get(entry.parentUuid);
2301
+ if (pending) sessionSummaries.set(currentSessionId, pending);
2302
+ }
2303
+
2304
+ if (entry.message?.content && currentSessionId && !entry.isApiErrorMessage) {
2305
+ const role = entry.message.role;
2306
+ if (role === 'user' || role === 'assistant') {
2307
+ const text = extractText(entry.message.content);
2308
+ if (text && !isSystemMessage(text)) {
2309
+ if (!sessionLastMessages.has(currentSessionId)) {
2310
+ sessionLastMessages.set(currentSessionId, {});
2311
+ }
2312
+ const messages = sessionLastMessages.get(currentSessionId);
2313
+ if (role === 'user') messages.user = text;
2314
+ else messages.assistant = text;
2315
+ }
2316
+ }
2317
+ }
2318
+
2319
+ if (!entry.message?.content) continue;
2320
+ if (entry.message.role !== 'user' && entry.message.role !== 'assistant') continue;
2321
+ if (entry.isApiErrorMessage) continue;
2322
+
2323
+ const text = extractText(entry.message.content);
2324
+ if (!text || isSystemMessage(text)) continue;
2325
+
2326
+ const textLower = text.toLowerCase();
2327
+ if (!allWordsMatch(textLower)) continue;
2328
+
2329
+ const sessionId = entry.sessionId || currentSessionId || file.replace('.jsonl', '');
2330
+ if (!sessionMatches.has(sessionId)) {
2331
+ sessionMatches.set(sessionId, []);
2332
+ }
2333
+
2334
+ const matches = sessionMatches.get(sessionId);
2335
+ if (matches.length < 2) {
2336
+ const { snippet, highlights } = buildSnippet(text, textLower);
2337
+ matches.push({
2338
+ role: entry.message.role,
2339
+ snippet,
2340
+ highlights,
2341
+ timestamp: entry.timestamp || null,
2342
+ provider: 'claude',
2343
+ messageUuid: entry.uuid || null
2344
+ });
2345
+ totalMatches++;
2346
+ }
2347
+ }
2348
+ } catch {
2349
+ continue;
2350
+ }
2351
+
2352
+ for (const [sessionId, matches] of sessionMatches) {
2353
+ projectResult.sessions.push({
2354
+ sessionId,
2355
+ provider: 'claude',
2356
+ sessionSummary: sessionSummaries.get(sessionId) || (() => {
2357
+ const messages = sessionLastMessages.get(sessionId);
2358
+ const lastMessage = messages?.user || messages?.assistant;
2359
+ return lastMessage
2360
+ ? (lastMessage.length > 50 ? lastMessage.substring(0, 50) + '...' : lastMessage)
2361
+ : 'New Session';
2362
+ })(),
2363
+ matches
2364
+ });
2365
+ }
2366
+ }
2367
+ }
2368
+
2369
+ if (actualProjectDir && !isAborted() && totalMatches < safeLimit) {
2370
+ await searchCodexSessionsForProject(
2371
+ actualProjectDir,
2372
+ projectResult,
2373
+ allWordsMatch,
2374
+ buildSnippet,
2375
+ safeLimit,
2376
+ () => totalMatches,
2377
+ (n) => { totalMatches += n; },
2378
+ isAborted,
2379
+ codexSessionsIndexRef
2380
+ );
2381
+ }
2382
+
2383
+ if (actualProjectDir && !isAborted() && totalMatches < safeLimit) {
2384
+ await searchGeminiSessionsForProject(
2385
+ actualProjectDir,
2386
+ projectResult,
2387
+ allWordsMatch,
2388
+ buildSnippet,
2389
+ safeLimit,
2390
+ () => totalMatches,
2391
+ (n) => { totalMatches += n; },
2392
+ isAborted,
2393
+ geminiCliProjectsRef
2394
+ );
2395
+ }
2396
+
2397
+ scannedProjects++;
2398
+ if (projectResult.sessions.length > 0) {
2399
+ results.push(projectResult);
2400
+ if (onProjectResult) {
2401
+ onProjectResult({ projectResult, totalMatches, scannedProjects, totalProjects });
2402
+ }
2403
+ } else if (onProjectResult && scannedProjects % 10 === 0) {
2404
+ onProjectResult({ projectResult: null, totalMatches, scannedProjects, totalProjects });
2405
+ }
2406
+ }
2407
+
2408
+ return { results, totalMatches, query: safeQuery };
2409
+ }
2410
+
2411
+ async function searchCodexSessionsForProject(
2412
+ projectPath,
2413
+ projectResult,
2414
+ allWordsMatch,
2415
+ buildSnippet,
2416
+ limit,
2417
+ getTotalMatches,
2418
+ addMatches,
2419
+ isAborted,
2420
+ codexSessionsIndexRef = null
2421
+ ) {
2422
+ const normalizedProjectPath = normalizeComparablePath(projectPath);
2423
+ if (!normalizedProjectPath) return;
2424
+
2425
+ if (codexSessionsIndexRef && !codexSessionsIndexRef.sessionsByProject) {
2426
+ codexSessionsIndexRef.sessionsByProject = await buildCodexSessionsIndex();
2427
+ }
2428
+ const sessionsByProject = codexSessionsIndexRef?.sessionsByProject || await buildCodexSessionsIndex();
2429
+ const indexedSessions = sessionsByProject.get(normalizedProjectPath) || [];
2430
+ if (indexedSessions.length === 0) return;
2431
+
2432
+ const sessionsByFile = new Map();
2433
+ for (const session of indexedSessions) {
2434
+ if (session?.filePath && !sessionsByFile.has(session.filePath)) {
2435
+ sessionsByFile.set(session.filePath, session);
2436
+ }
2437
+ }
2438
+
2439
+ for (const [filePath, indexedSession] of sessionsByFile.entries()) {
2440
+ if (getTotalMatches() >= limit || isAborted()) break;
2441
+
2442
+ try {
2443
+ const fileStream = fsSync.createReadStream(filePath);
2444
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
2445
+ let lastUserMessage = null;
2446
+ const matches = [];
2447
+
2448
+ for await (const line of rl) {
2449
+ if (getTotalMatches() >= limit || isAborted()) break;
2450
+ if (!line.trim()) continue;
2451
+
2452
+ let entry;
2453
+ try { entry = JSON.parse(line); } catch { continue; }
2454
+
2455
+ let text = null;
2456
+ let role = null;
2457
+
2458
+ if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload) && entry.payload.message) {
2459
+ text = entry.payload.message;
2460
+ role = 'user';
2461
+ lastUserMessage = text;
2462
+ } else if (entry.type === 'response_item' && entry.payload?.type === 'message') {
2463
+ const contentParts = entry.payload.content || [];
2464
+ if (entry.payload.role === 'user') {
2465
+ text = contentParts
2466
+ .filter(p => p.type === 'input_text' && p.text)
2467
+ .map(p => p.text)
2468
+ .join(' ');
2469
+ role = 'user';
2470
+ if (text) lastUserMessage = text;
2471
+ } else if (entry.payload.role === 'assistant') {
2472
+ text = contentParts
2473
+ .filter(p => p.type === 'output_text' && p.text)
2474
+ .map(p => p.text)
2475
+ .join(' ');
2476
+ role = 'assistant';
2477
+ }
2478
+ }
2479
+
2480
+ if (!text || !role) continue;
2481
+ const textLower = text.toLowerCase();
2482
+ if (!allWordsMatch(textLower)) continue;
2483
+
2484
+ if (matches.length < 2) {
2485
+ const { snippet, highlights } = buildSnippet(text, textLower);
2486
+ matches.push({ role, snippet, highlights, timestamp: entry.timestamp || null, provider: 'codex' });
2487
+ addMatches(1);
2488
+ }
2489
+ }
2490
+
2491
+ if (matches.length > 0) {
2492
+ projectResult.sessions.push({
2493
+ sessionId: indexedSession.id || path.basename(filePath, '.jsonl'),
2494
+ provider: 'codex',
2495
+ sessionSummary: lastUserMessage
2496
+ ? (lastUserMessage.length > 50 ? lastUserMessage.substring(0, 50) + '...' : lastUserMessage)
2497
+ : (indexedSession.summary || 'Codex Session'),
2498
+ matches
2499
+ });
2500
+ }
2501
+ } catch {
2502
+ continue;
2503
+ }
2504
+ }
2505
+ }
2506
+
2507
+ async function searchGeminiSessionsForProject(
2508
+ projectPath, projectResult, allWordsMatch,
2509
+ buildSnippet, limit, getTotalMatches, addMatches, isAborted = () => false, geminiCliProjectsRef = null
2510
+ ) {
2511
+ // 1) Search in-memory sessions (created via UI)
2512
+ for (const [sessionId, session] of sessionManager.sessions) {
2513
+ if (getTotalMatches() >= limit || isAborted()) break;
2514
+ if (session.projectPath !== projectPath) continue;
2515
+
2516
+ const matches = [];
2517
+ for (const msg of session.messages) {
2518
+ if (getTotalMatches() >= limit || isAborted()) break;
2519
+ if (msg.role !== 'user' && msg.role !== 'assistant') continue;
2520
+
2521
+ const text = typeof msg.content === 'string' ? msg.content
2522
+ : Array.isArray(msg.content) ? msg.content.filter(p => p.type === 'text').map(p => p.text).join(' ')
2523
+ : '';
2524
+ if (!text) continue;
2525
+
2526
+ const textLower = text.toLowerCase();
2527
+ if (!allWordsMatch(textLower)) continue;
2528
+
2529
+ if (matches.length < 2) {
2530
+ const { snippet, highlights } = buildSnippet(text, textLower);
2531
+ matches.push({
2532
+ role: msg.role, snippet, highlights,
2533
+ timestamp: msg.timestamp ? msg.timestamp.toISOString() : null,
2534
+ provider: 'gemini'
2535
+ });
2536
+ addMatches(1);
2537
+ }
2538
+ }
2539
+
2540
+ if (matches.length > 0) {
2541
+ const firstUserMsg = session.messages.find(m => m.role === 'user');
2542
+ const summary = firstUserMsg?.content
2543
+ ? (typeof firstUserMsg.content === 'string'
2544
+ ? (firstUserMsg.content.length > 50 ? firstUserMsg.content.substring(0, 50) + '...' : firstUserMsg.content)
2545
+ : 'Gemini Session')
2546
+ : 'Gemini Session';
2547
+
2548
+ projectResult.sessions.push({
2549
+ sessionId,
2550
+ provider: 'gemini',
2551
+ sessionSummary: summary,
2552
+ matches
2553
+ });
2554
+ }
2555
+ }
2556
+
2557
+ // 2) Search Gemini CLI sessions on disk (~/.gemini/tmp/<project>/chats/*.json)
2558
+ const normalizedProjectPath = normalizeComparablePath(projectPath);
2559
+ if (!normalizedProjectPath) return;
2560
+
2561
+ const geminiTmpDir = path.join(os.homedir(), '.gemini', 'tmp');
2562
+
2563
+ const trackedSessionIds = new Set();
2564
+ for (const [sid] of sessionManager.sessions) {
2565
+ trackedSessionIds.add(sid);
2566
+ }
2567
+
2568
+ if (geminiCliProjectsRef && !geminiCliProjectsRef.entries) {
2569
+ geminiCliProjectsRef.entries = await listGeminiCliProjectEntries();
2570
+ }
2571
+ const geminiCliEntries = geminiCliProjectsRef?.entries || await listGeminiCliProjectEntries();
2572
+
2573
+ for (const entry of geminiCliEntries) {
2574
+ if (getTotalMatches() >= limit || isAborted()) break;
2575
+ if (entry.normalizedProjectRoot !== normalizedProjectPath) continue;
2576
+
2577
+ const projectDir = entry.projectDir;
2578
+ const chatsDir = path.join(geminiTmpDir, projectDir, 'chats');
2579
+ let chatFiles;
2580
+ try {
2581
+ chatFiles = await fs.readdir(chatsDir);
2582
+ } catch {
2583
+ continue;
2584
+ }
2585
+
2586
+ for (const chatFile of chatFiles) {
2587
+ if (getTotalMatches() >= limit || isAborted()) break;
2588
+ if (!chatFile.endsWith('.json')) continue;
2589
+
2590
+ try {
2591
+ const filePath = path.join(chatsDir, chatFile);
2592
+ const data = await fs.readFile(filePath, 'utf8');
2593
+ const session = JSON.parse(data);
2594
+ if (!session.messages || !Array.isArray(session.messages)) continue;
2595
+
2596
+ const cliSessionId = session.sessionId || chatFile.replace('.json', '');
2597
+ if (trackedSessionIds.has(cliSessionId)) continue;
2598
+
2599
+ const matches = [];
2600
+ let firstUserText = null;
2601
+
2602
+ for (const msg of session.messages) {
2603
+ if (getTotalMatches() >= limit || isAborted()) break;
2604
+
2605
+ const role = msg.type === 'user' ? 'user'
2606
+ : (msg.type === 'gemini' || msg.type === 'assistant') ? 'assistant'
2607
+ : null;
2608
+ if (!role) continue;
2609
+
2610
+ let text = '';
2611
+ if (typeof msg.content === 'string') {
2612
+ text = msg.content;
2613
+ } else if (Array.isArray(msg.content)) {
2614
+ text = msg.content
2615
+ .filter(p => p.text)
2616
+ .map(p => p.text)
2617
+ .join(' ');
2618
+ }
2619
+ if (!text) continue;
2620
+
2621
+ if (role === 'user' && !firstUserText) firstUserText = text;
2622
+
2623
+ const textLower = text.toLowerCase();
2624
+ if (!allWordsMatch(textLower)) continue;
2625
+
2626
+ if (matches.length < 2) {
2627
+ const { snippet, highlights } = buildSnippet(text, textLower);
2628
+ matches.push({
2629
+ role, snippet, highlights,
2630
+ timestamp: msg.timestamp || null,
2631
+ provider: 'gemini'
2632
+ });
2633
+ addMatches(1);
2634
+ }
2635
+ }
2636
+
2637
+ if (matches.length > 0) {
2638
+ const summary = firstUserText
2639
+ ? (firstUserText.length > 50 ? firstUserText.substring(0, 50) + '...' : firstUserText)
2640
+ : 'Gemini CLI Session';
2641
+
2642
+ projectResult.sessions.push({
2643
+ sessionId: cliSessionId,
2644
+ provider: 'gemini',
2645
+ sessionSummary: summary,
2646
+ matches
2647
+ });
2648
+ }
2649
+ } catch {
2650
+ continue;
2651
+ }
2652
+ }
2653
+ }
2654
+ }
2655
+
2656
+ async function getGeminiCliSessions(projectPath, options = {}) {
2657
+ const { geminiCliProjectsRef = null } = options;
2658
+ const normalizedProjectPath = normalizeComparablePath(projectPath);
2659
+ if (!normalizedProjectPath) return [];
2660
+
2661
+ const sessions = [];
2662
+ const geminiTmpDir = path.join(os.homedir(), '.gemini', 'tmp');
2663
+
2664
+ if (geminiCliProjectsRef && !geminiCliProjectsRef.entries) {
2665
+ geminiCliProjectsRef.entries = await listGeminiCliProjectEntries();
2666
+ }
2667
+ const geminiCliEntries = geminiCliProjectsRef?.entries || await listGeminiCliProjectEntries();
2668
+
2669
+ for (const entry of geminiCliEntries) {
2670
+ if (entry.normalizedProjectRoot !== normalizedProjectPath) continue;
2671
+ const projectDir = entry.projectDir;
2672
+ const chatsDir = path.join(geminiTmpDir, projectDir, 'chats');
2673
+ let chatFiles;
2674
+ try {
2675
+ chatFiles = await fs.readdir(chatsDir);
2676
+ } catch {
2677
+ continue;
2678
+ }
2679
+
2680
+ for (const chatFile of chatFiles) {
2681
+ if (!chatFile.endsWith('.json')) continue;
2682
+ try {
2683
+ const filePath = path.join(chatsDir, chatFile);
2684
+ const data = await fs.readFile(filePath, 'utf8');
2685
+ const session = JSON.parse(data);
2686
+ if (!session.messages || !Array.isArray(session.messages)) continue;
2687
+
2688
+ const sessionId = session.sessionId || chatFile.replace('.json', '');
2689
+ const firstUserMsg = session.messages.find(m => m.type === 'user');
2690
+ let summary = 'Gemini CLI Session';
2691
+ if (firstUserMsg) {
2692
+ const text = Array.isArray(firstUserMsg.content)
2693
+ ? firstUserMsg.content.filter(p => p.text).map(p => p.text).join(' ')
2694
+ : (typeof firstUserMsg.content === 'string' ? firstUserMsg.content : '');
2695
+ if (text) {
2696
+ summary = text.length > 50 ? text.substring(0, 50) + '...' : text;
2697
+ }
2698
+ }
2699
+
2700
+ sessions.push({
2701
+ id: sessionId,
2702
+ summary,
2703
+ messageCount: session.messages.length,
2704
+ lastActivity: session.lastUpdated || session.startTime || null,
2705
+ provider: 'gemini'
2706
+ });
2707
+ } catch {
2708
+ continue;
2709
+ }
2710
+ }
2711
+ }
2712
+
2713
+ return sessions.sort((a, b) =>
2714
+ new Date(b.lastActivity || 0) - new Date(a.lastActivity || 0)
2715
+ );
2716
+ }
2717
+
2718
+ async function getGeminiCliSessionMessages(sessionId) {
2719
+ const geminiTmpDir = path.join(os.homedir(), '.gemini', 'tmp');
2720
+ let projectDirs;
2721
+ try {
2722
+ projectDirs = await fs.readdir(geminiTmpDir);
2723
+ } catch {
2724
+ return [];
2725
+ }
2726
+
2727
+ for (const projectDir of projectDirs) {
2728
+ const chatsDir = path.join(geminiTmpDir, projectDir, 'chats');
2729
+ let chatFiles;
2730
+ try {
2731
+ chatFiles = await fs.readdir(chatsDir);
2732
+ } catch {
2733
+ continue;
2734
+ }
2735
+
2736
+ for (const chatFile of chatFiles) {
2737
+ if (!chatFile.endsWith('.json')) continue;
2738
+ try {
2739
+ const filePath = path.join(chatsDir, chatFile);
2740
+ const data = await fs.readFile(filePath, 'utf8');
2741
+ const session = JSON.parse(data);
2742
+ const fileSessionId = session.sessionId || chatFile.replace('.json', '');
2743
+ if (fileSessionId !== sessionId) continue;
2744
+
2745
+ return (session.messages || []).map(msg => {
2746
+ const role = msg.type === 'user' ? 'user'
2747
+ : (msg.type === 'gemini' || msg.type === 'assistant') ? 'assistant'
2748
+ : msg.type;
2749
+
2750
+ let content = '';
2751
+ if (typeof msg.content === 'string') {
2752
+ content = msg.content;
2753
+ } else if (Array.isArray(msg.content)) {
2754
+ content = msg.content.filter(p => p.text).map(p => p.text).join('\n');
2755
+ }
2756
+
2757
+ return {
2758
+ type: 'message',
2759
+ message: { role, content },
2760
+ timestamp: msg.timestamp || null
2761
+ };
2762
+ });
2763
+ } catch {
2764
+ continue;
2765
+ }
2766
+ }
2767
+ }
2768
+
2769
+ return [];
2770
+ }
2771
+
2772
+ export {
2773
+ getProjects,
2774
+ getSessions,
2775
+ getSessionMessages,
2776
+ parseJsonlSessions,
2777
+ renameProject,
2778
+ deleteSession,
2779
+ isProjectEmpty,
2780
+ deleteProject,
2781
+ addProjectManually,
2782
+ loadProjectConfig,
2783
+ saveProjectConfig,
2784
+ extractProjectDirectory,
2785
+ clearProjectDirectoryCache,
2786
+ getCodexSessions,
2787
+ getCodexSessionMessages,
2788
+ deleteCodexSession,
2789
+ getGeminiCliSessions,
2790
+ getGeminiCliSessionMessages,
2791
+ searchConversations
2792
+ };