@jait/gateway 0.1.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 (474) hide show
  1. package/bin/jait.mjs +144 -0
  2. package/dist/config.d.ts +24 -0
  3. package/dist/config.d.ts.map +1 -0
  4. package/dist/config.js +73 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/db/connection.d.ts +37 -0
  7. package/dist/db/connection.d.ts.map +1 -0
  8. package/dist/db/connection.js +85 -0
  9. package/dist/db/connection.js.map +1 -0
  10. package/dist/db/index.d.ts +4 -0
  11. package/dist/db/index.d.ts.map +1 -0
  12. package/dist/db/index.js +4 -0
  13. package/dist/db/index.js.map +1 -0
  14. package/dist/db/migrations.d.ts +24 -0
  15. package/dist/db/migrations.d.ts.map +1 -0
  16. package/dist/db/migrations.js +312 -0
  17. package/dist/db/migrations.js.map +1 -0
  18. package/dist/db/schema.d.ts +2253 -0
  19. package/dist/db/schema.d.ts.map +1 -0
  20. package/dist/db/schema.js +195 -0
  21. package/dist/db/schema.js.map +1 -0
  22. package/dist/foundation.d.ts +26 -0
  23. package/dist/foundation.d.ts.map +1 -0
  24. package/dist/foundation.js +15 -0
  25. package/dist/foundation.js.map +1 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +413 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/lib/uuidv7.d.ts +10 -0
  31. package/dist/lib/uuidv7.d.ts.map +1 -0
  32. package/dist/lib/uuidv7.js +33 -0
  33. package/dist/lib/uuidv7.js.map +1 -0
  34. package/dist/memory/contracts.d.ts +42 -0
  35. package/dist/memory/contracts.d.ts.map +1 -0
  36. package/dist/memory/contracts.js +2 -0
  37. package/dist/memory/contracts.js.map +1 -0
  38. package/dist/memory/embeddings.d.ts +4 -0
  39. package/dist/memory/embeddings.d.ts.map +1 -0
  40. package/dist/memory/embeddings.js +26 -0
  41. package/dist/memory/embeddings.js.map +1 -0
  42. package/dist/memory/service.d.ts +17 -0
  43. package/dist/memory/service.d.ts.map +1 -0
  44. package/dist/memory/service.js +82 -0
  45. package/dist/memory/service.js.map +1 -0
  46. package/dist/memory/sqlite-backend.d.ts +11 -0
  47. package/dist/memory/sqlite-backend.d.ts.map +1 -0
  48. package/dist/memory/sqlite-backend.js +68 -0
  49. package/dist/memory/sqlite-backend.js.map +1 -0
  50. package/dist/plugins/contracts.d.ts +11 -0
  51. package/dist/plugins/contracts.d.ts.map +1 -0
  52. package/dist/plugins/contracts.js +2 -0
  53. package/dist/plugins/contracts.js.map +1 -0
  54. package/dist/providers/claude-code-provider.d.ts +39 -0
  55. package/dist/providers/claude-code-provider.d.ts.map +1 -0
  56. package/dist/providers/claude-code-provider.js +322 -0
  57. package/dist/providers/claude-code-provider.js.map +1 -0
  58. package/dist/providers/codex-provider.d.ts +51 -0
  59. package/dist/providers/codex-provider.d.ts.map +1 -0
  60. package/dist/providers/codex-provider.js +826 -0
  61. package/dist/providers/codex-provider.js.map +1 -0
  62. package/dist/providers/contracts.d.ts +167 -0
  63. package/dist/providers/contracts.d.ts.map +1 -0
  64. package/dist/providers/contracts.js +13 -0
  65. package/dist/providers/contracts.js.map +1 -0
  66. package/dist/providers/index.d.ts +6 -0
  67. package/dist/providers/index.d.ts.map +1 -0
  68. package/dist/providers/index.js +5 -0
  69. package/dist/providers/index.js.map +1 -0
  70. package/dist/providers/jait-provider.d.ts +23 -0
  71. package/dist/providers/jait-provider.d.ts.map +1 -0
  72. package/dist/providers/jait-provider.js +67 -0
  73. package/dist/providers/jait-provider.js.map +1 -0
  74. package/dist/providers/registry.d.ts +39 -0
  75. package/dist/providers/registry.d.ts.map +1 -0
  76. package/dist/providers/registry.js +64 -0
  77. package/dist/providers/registry.js.map +1 -0
  78. package/dist/pty-broker-client.d.ts +46 -0
  79. package/dist/pty-broker-client.d.ts.map +1 -0
  80. package/dist/pty-broker-client.js +142 -0
  81. package/dist/pty-broker-client.js.map +1 -0
  82. package/dist/routes/auth.d.ts +6 -0
  83. package/dist/routes/auth.d.ts.map +1 -0
  84. package/dist/routes/auth.js +236 -0
  85. package/dist/routes/auth.js.map +1 -0
  86. package/dist/routes/chat.d.ts +32 -0
  87. package/dist/routes/chat.d.ts.map +1 -0
  88. package/dist/routes/chat.js +1503 -0
  89. package/dist/routes/chat.js.map +1 -0
  90. package/dist/routes/consent.d.ts +10 -0
  91. package/dist/routes/consent.d.ts.map +1 -0
  92. package/dist/routes/consent.js +127 -0
  93. package/dist/routes/consent.js.map +1 -0
  94. package/dist/routes/filesystem.d.ts +14 -0
  95. package/dist/routes/filesystem.d.ts.map +1 -0
  96. package/dist/routes/filesystem.js +152 -0
  97. package/dist/routes/filesystem.js.map +1 -0
  98. package/dist/routes/git.d.ts +17 -0
  99. package/dist/routes/git.d.ts.map +1 -0
  100. package/dist/routes/git.js +213 -0
  101. package/dist/routes/git.js.map +1 -0
  102. package/dist/routes/health.d.ts +7 -0
  103. package/dist/routes/health.d.ts.map +1 -0
  104. package/dist/routes/health.js +21 -0
  105. package/dist/routes/health.js.map +1 -0
  106. package/dist/routes/hooks.d.ts +9 -0
  107. package/dist/routes/hooks.d.ts.map +1 -0
  108. package/dist/routes/hooks.js +22 -0
  109. package/dist/routes/hooks.js.map +1 -0
  110. package/dist/routes/jobs.d.ts +5 -0
  111. package/dist/routes/jobs.d.ts.map +1 -0
  112. package/dist/routes/jobs.js +333 -0
  113. package/dist/routes/jobs.js.map +1 -0
  114. package/dist/routes/mcp-server.d.ts +23 -0
  115. package/dist/routes/mcp-server.d.ts.map +1 -0
  116. package/dist/routes/mcp-server.js +177 -0
  117. package/dist/routes/mcp-server.js.map +1 -0
  118. package/dist/routes/mobile.d.ts +12 -0
  119. package/dist/routes/mobile.d.ts.map +1 -0
  120. package/dist/routes/mobile.js +64 -0
  121. package/dist/routes/mobile.js.map +1 -0
  122. package/dist/routes/network.d.ts +3 -0
  123. package/dist/routes/network.d.ts.map +1 -0
  124. package/dist/routes/network.js +367 -0
  125. package/dist/routes/network.js.map +1 -0
  126. package/dist/routes/repositories.d.ts +18 -0
  127. package/dist/routes/repositories.d.ts.map +1 -0
  128. package/dist/routes/repositories.js +90 -0
  129. package/dist/routes/repositories.js.map +1 -0
  130. package/dist/routes/screen-share.d.ts +17 -0
  131. package/dist/routes/screen-share.d.ts.map +1 -0
  132. package/dist/routes/screen-share.js +92 -0
  133. package/dist/routes/screen-share.js.map +1 -0
  134. package/dist/routes/sessions.d.ts +18 -0
  135. package/dist/routes/sessions.d.ts.map +1 -0
  136. package/dist/routes/sessions.js +169 -0
  137. package/dist/routes/sessions.js.map +1 -0
  138. package/dist/routes/terminals.d.ts +15 -0
  139. package/dist/routes/terminals.d.ts.map +1 -0
  140. package/dist/routes/terminals.js +326 -0
  141. package/dist/routes/terminals.js.map +1 -0
  142. package/dist/routes/threads.d.ts +38 -0
  143. package/dist/routes/threads.d.ts.map +1 -0
  144. package/dist/routes/threads.js +488 -0
  145. package/dist/routes/threads.js.map +1 -0
  146. package/dist/routes/trust.d.ts +9 -0
  147. package/dist/routes/trust.d.ts.map +1 -0
  148. package/dist/routes/trust.js +25 -0
  149. package/dist/routes/trust.js.map +1 -0
  150. package/dist/routes/voice.d.ts +5 -0
  151. package/dist/routes/voice.d.ts.map +1 -0
  152. package/dist/routes/voice.js +37 -0
  153. package/dist/routes/voice.js.map +1 -0
  154. package/dist/routes/workspace.d.ts +13 -0
  155. package/dist/routes/workspace.d.ts.map +1 -0
  156. package/dist/routes/workspace.js +275 -0
  157. package/dist/routes/workspace.js.map +1 -0
  158. package/dist/scheduler/contracts.d.ts +15 -0
  159. package/dist/scheduler/contracts.d.ts.map +1 -0
  160. package/dist/scheduler/contracts.js +2 -0
  161. package/dist/scheduler/contracts.js.map +1 -0
  162. package/dist/scheduler/hooks.d.ts +20 -0
  163. package/dist/scheduler/hooks.d.ts.map +1 -0
  164. package/dist/scheduler/hooks.js +78 -0
  165. package/dist/scheduler/hooks.js.map +1 -0
  166. package/dist/scheduler/service.d.ts +65 -0
  167. package/dist/scheduler/service.d.ts.map +1 -0
  168. package/dist/scheduler/service.js +188 -0
  169. package/dist/scheduler/service.js.map +1 -0
  170. package/dist/security/consent-executor.d.ts +48 -0
  171. package/dist/security/consent-executor.d.ts.map +1 -0
  172. package/dist/security/consent-executor.js +158 -0
  173. package/dist/security/consent-executor.js.map +1 -0
  174. package/dist/security/consent-manager.d.ts +105 -0
  175. package/dist/security/consent-manager.d.ts.map +1 -0
  176. package/dist/security/consent-manager.js +227 -0
  177. package/dist/security/consent-manager.js.map +1 -0
  178. package/dist/security/contracts.d.ts +31 -0
  179. package/dist/security/contracts.d.ts.map +1 -0
  180. package/dist/security/contracts.js +2 -0
  181. package/dist/security/contracts.js.map +1 -0
  182. package/dist/security/http-auth.d.ts +10 -0
  183. package/dist/security/http-auth.d.ts.map +1 -0
  184. package/dist/security/http-auth.js +48 -0
  185. package/dist/security/http-auth.js.map +1 -0
  186. package/dist/security/index.d.ts +10 -0
  187. package/dist/security/index.d.ts.map +1 -0
  188. package/dist/security/index.js +9 -0
  189. package/dist/security/index.js.map +1 -0
  190. package/dist/security/path-guard.d.ts +40 -0
  191. package/dist/security/path-guard.d.ts.map +1 -0
  192. package/dist/security/path-guard.js +125 -0
  193. package/dist/security/path-guard.js.map +1 -0
  194. package/dist/security/sandbox-manager.d.ts +43 -0
  195. package/dist/security/sandbox-manager.d.ts.map +1 -0
  196. package/dist/security/sandbox-manager.js +110 -0
  197. package/dist/security/sandbox-manager.js.map +1 -0
  198. package/dist/security/ssrf-guard.d.ts +11 -0
  199. package/dist/security/ssrf-guard.d.ts.map +1 -0
  200. package/dist/security/ssrf-guard.js +59 -0
  201. package/dist/security/ssrf-guard.js.map +1 -0
  202. package/dist/security/tool-permissions.d.ts +61 -0
  203. package/dist/security/tool-permissions.d.ts.map +1 -0
  204. package/dist/security/tool-permissions.js +105 -0
  205. package/dist/security/tool-permissions.js.map +1 -0
  206. package/dist/security/tool-profiles.d.ts +23 -0
  207. package/dist/security/tool-profiles.d.ts.map +1 -0
  208. package/dist/security/tool-profiles.js +106 -0
  209. package/dist/security/tool-profiles.js.map +1 -0
  210. package/dist/security/trust-engine.d.ts +61 -0
  211. package/dist/security/trust-engine.d.ts.map +1 -0
  212. package/dist/security/trust-engine.js +192 -0
  213. package/dist/security/trust-engine.js.map +1 -0
  214. package/dist/server.d.ts +54 -0
  215. package/dist/server.d.ts.map +1 -0
  216. package/dist/server.js +188 -0
  217. package/dist/server.js.map +1 -0
  218. package/dist/services/audit.d.ts +60 -0
  219. package/dist/services/audit.d.ts.map +1 -0
  220. package/dist/services/audit.js +58 -0
  221. package/dist/services/audit.js.map +1 -0
  222. package/dist/services/device-registry.d.ts +15 -0
  223. package/dist/services/device-registry.d.ts.map +1 -0
  224. package/dist/services/device-registry.js +32 -0
  225. package/dist/services/device-registry.js.map +1 -0
  226. package/dist/services/git.d.ts +168 -0
  227. package/dist/services/git.d.ts.map +1 -0
  228. package/dist/services/git.js +957 -0
  229. package/dist/services/git.js.map +1 -0
  230. package/dist/services/repositories.d.ts +32 -0
  231. package/dist/services/repositories.d.ts.map +1 -0
  232. package/dist/services/repositories.js +70 -0
  233. package/dist/services/repositories.js.map +1 -0
  234. package/dist/services/session-state.d.ts +20 -0
  235. package/dist/services/session-state.d.ts.map +1 -0
  236. package/dist/services/session-state.js +89 -0
  237. package/dist/services/session-state.js.map +1 -0
  238. package/dist/services/sessions.d.ts +68 -0
  239. package/dist/services/sessions.d.ts.map +1 -0
  240. package/dist/services/sessions.js +136 -0
  241. package/dist/services/sessions.js.map +1 -0
  242. package/dist/services/thread-title.d.ts +23 -0
  243. package/dist/services/thread-title.d.ts.map +1 -0
  244. package/dist/services/thread-title.js +141 -0
  245. package/dist/services/thread-title.js.map +1 -0
  246. package/dist/services/threads.d.ts +64 -0
  247. package/dist/services/threads.d.ts.map +1 -0
  248. package/dist/services/threads.js +202 -0
  249. package/dist/services/threads.js.map +1 -0
  250. package/dist/services/users.d.ts +39 -0
  251. package/dist/services/users.d.ts.map +1 -0
  252. package/dist/services/users.js +203 -0
  253. package/dist/services/users.js.map +1 -0
  254. package/dist/sessions/contracts.d.ts +14 -0
  255. package/dist/sessions/contracts.d.ts.map +1 -0
  256. package/dist/sessions/contracts.js +2 -0
  257. package/dist/sessions/contracts.js.map +1 -0
  258. package/dist/surfaces/browser.d.ts +65 -0
  259. package/dist/surfaces/browser.d.ts.map +1 -0
  260. package/dist/surfaces/browser.js +615 -0
  261. package/dist/surfaces/browser.js.map +1 -0
  262. package/dist/surfaces/contracts.d.ts +34 -0
  263. package/dist/surfaces/contracts.d.ts.map +1 -0
  264. package/dist/surfaces/contracts.js +2 -0
  265. package/dist/surfaces/contracts.js.map +1 -0
  266. package/dist/surfaces/filesystem.d.ts +76 -0
  267. package/dist/surfaces/filesystem.d.ts.map +1 -0
  268. package/dist/surfaces/filesystem.js +245 -0
  269. package/dist/surfaces/filesystem.js.map +1 -0
  270. package/dist/surfaces/index.d.ts +6 -0
  271. package/dist/surfaces/index.d.ts.map +1 -0
  272. package/dist/surfaces/index.js +5 -0
  273. package/dist/surfaces/index.js.map +1 -0
  274. package/dist/surfaces/registry.d.ts +24 -0
  275. package/dist/surfaces/registry.d.ts.map +1 -0
  276. package/dist/surfaces/registry.js +59 -0
  277. package/dist/surfaces/registry.js.map +1 -0
  278. package/dist/surfaces/terminal.d.ts +76 -0
  279. package/dist/surfaces/terminal.d.ts.map +1 -0
  280. package/dist/surfaces/terminal.js +271 -0
  281. package/dist/surfaces/terminal.js.map +1 -0
  282. package/dist/tools/agent-loop.d.ts +302 -0
  283. package/dist/tools/agent-loop.d.ts.map +1 -0
  284. package/dist/tools/agent-loop.js +918 -0
  285. package/dist/tools/agent-loop.js.map +1 -0
  286. package/dist/tools/agent-tools.d.ts +39 -0
  287. package/dist/tools/agent-tools.d.ts.map +1 -0
  288. package/dist/tools/agent-tools.js +263 -0
  289. package/dist/tools/agent-tools.js.map +1 -0
  290. package/dist/tools/browser-tools.d.ts +38 -0
  291. package/dist/tools/browser-tools.d.ts.map +1 -0
  292. package/dist/tools/browser-tools.js +725 -0
  293. package/dist/tools/browser-tools.js.map +1 -0
  294. package/dist/tools/chat-modes.d.ts +75 -0
  295. package/dist/tools/chat-modes.d.ts.map +1 -0
  296. package/dist/tools/chat-modes.js +228 -0
  297. package/dist/tools/chat-modes.js.map +1 -0
  298. package/dist/tools/contracts.d.ts +69 -0
  299. package/dist/tools/contracts.d.ts.map +1 -0
  300. package/dist/tools/contracts.js +2 -0
  301. package/dist/tools/contracts.js.map +1 -0
  302. package/dist/tools/core/agent.d.ts +31 -0
  303. package/dist/tools/core/agent.d.ts.map +1 -0
  304. package/dist/tools/core/agent.js +65 -0
  305. package/dist/tools/core/agent.js.map +1 -0
  306. package/dist/tools/core/edit.d.ts +30 -0
  307. package/dist/tools/core/edit.d.ts.map +1 -0
  308. package/dist/tools/core/edit.js +109 -0
  309. package/dist/tools/core/edit.js.map +1 -0
  310. package/dist/tools/core/execute.d.ts +36 -0
  311. package/dist/tools/core/execute.d.ts.map +1 -0
  312. package/dist/tools/core/execute.js +81 -0
  313. package/dist/tools/core/execute.js.map +1 -0
  314. package/dist/tools/core/get-fs.d.ts +32 -0
  315. package/dist/tools/core/get-fs.d.ts.map +1 -0
  316. package/dist/tools/core/get-fs.js +143 -0
  317. package/dist/tools/core/get-fs.js.map +1 -0
  318. package/dist/tools/core/index.d.ts +26 -0
  319. package/dist/tools/core/index.d.ts.map +1 -0
  320. package/dist/tools/core/index.js +26 -0
  321. package/dist/tools/core/index.js.map +1 -0
  322. package/dist/tools/core/jait.d.ts +60 -0
  323. package/dist/tools/core/jait.d.ts.map +1 -0
  324. package/dist/tools/core/jait.js +256 -0
  325. package/dist/tools/core/jait.js.map +1 -0
  326. package/dist/tools/core/read.d.ts +26 -0
  327. package/dist/tools/core/read.d.ts.map +1 -0
  328. package/dist/tools/core/read.js +118 -0
  329. package/dist/tools/core/read.js.map +1 -0
  330. package/dist/tools/core/search.d.ts +34 -0
  331. package/dist/tools/core/search.d.ts.map +1 -0
  332. package/dist/tools/core/search.js +187 -0
  333. package/dist/tools/core/search.js.map +1 -0
  334. package/dist/tools/core/todo.d.ts +38 -0
  335. package/dist/tools/core/todo.d.ts.map +1 -0
  336. package/dist/tools/core/todo.js +116 -0
  337. package/dist/tools/core/todo.js.map +1 -0
  338. package/dist/tools/core/web.d.ts +34 -0
  339. package/dist/tools/core/web.d.ts.map +1 -0
  340. package/dist/tools/core/web.js +120 -0
  341. package/dist/tools/core/web.js.map +1 -0
  342. package/dist/tools/cron-tools.d.ts +7 -0
  343. package/dist/tools/cron-tools.d.ts.map +1 -0
  344. package/dist/tools/cron-tools.js +116 -0
  345. package/dist/tools/cron-tools.js.map +1 -0
  346. package/dist/tools/file-tools.d.ts +32 -0
  347. package/dist/tools/file-tools.d.ts.map +1 -0
  348. package/dist/tools/file-tools.js +178 -0
  349. package/dist/tools/file-tools.js.map +1 -0
  350. package/dist/tools/gateway-tools.d.ts +15 -0
  351. package/dist/tools/gateway-tools.d.ts.map +1 -0
  352. package/dist/tools/gateway-tools.js +39 -0
  353. package/dist/tools/gateway-tools.js.map +1 -0
  354. package/dist/tools/index.d.ts +57 -0
  355. package/dist/tools/index.d.ts.map +1 -0
  356. package/dist/tools/index.js +170 -0
  357. package/dist/tools/index.js.map +1 -0
  358. package/dist/tools/mcp-bridge.d.ts +111 -0
  359. package/dist/tools/mcp-bridge.d.ts.map +1 -0
  360. package/dist/tools/mcp-bridge.js +166 -0
  361. package/dist/tools/mcp-bridge.js.map +1 -0
  362. package/dist/tools/memory-tools.d.ts +19 -0
  363. package/dist/tools/memory-tools.d.ts.map +1 -0
  364. package/dist/tools/memory-tools.js +78 -0
  365. package/dist/tools/memory-tools.js.map +1 -0
  366. package/dist/tools/meta-tools.d.ts +25 -0
  367. package/dist/tools/meta-tools.d.ts.map +1 -0
  368. package/dist/tools/meta-tools.js +125 -0
  369. package/dist/tools/meta-tools.js.map +1 -0
  370. package/dist/tools/network-tools.d.ts +21 -0
  371. package/dist/tools/network-tools.d.ts.map +1 -0
  372. package/dist/tools/network-tools.js +189 -0
  373. package/dist/tools/network-tools.js.map +1 -0
  374. package/dist/tools/os-tools.d.ts +18 -0
  375. package/dist/tools/os-tools.d.ts.map +1 -0
  376. package/dist/tools/os-tools.js +210 -0
  377. package/dist/tools/os-tools.js.map +1 -0
  378. package/dist/tools/prompts/claude-prompt.d.ts +8 -0
  379. package/dist/tools/prompts/claude-prompt.d.ts.map +1 -0
  380. package/dist/tools/prompts/claude-prompt.js +228 -0
  381. package/dist/tools/prompts/claude-prompt.js.map +1 -0
  382. package/dist/tools/prompts/default-openai-prompt.d.ts +8 -0
  383. package/dist/tools/prompts/default-openai-prompt.d.ts.map +1 -0
  384. package/dist/tools/prompts/default-openai-prompt.js +67 -0
  385. package/dist/tools/prompts/default-openai-prompt.js.map +1 -0
  386. package/dist/tools/prompts/default-prompt.d.ts +7 -0
  387. package/dist/tools/prompts/default-prompt.d.ts.map +1 -0
  388. package/dist/tools/prompts/default-prompt.js +50 -0
  389. package/dist/tools/prompts/default-prompt.js.map +1 -0
  390. package/dist/tools/prompts/gemini-prompt.d.ts +8 -0
  391. package/dist/tools/prompts/gemini-prompt.d.ts.map +1 -0
  392. package/dist/tools/prompts/gemini-prompt.js +118 -0
  393. package/dist/tools/prompts/gemini-prompt.js.map +1 -0
  394. package/dist/tools/prompts/gpt5-codex-prompt.d.ts +8 -0
  395. package/dist/tools/prompts/gpt5-codex-prompt.d.ts.map +1 -0
  396. package/dist/tools/prompts/gpt5-codex-prompt.js +72 -0
  397. package/dist/tools/prompts/gpt5-codex-prompt.js.map +1 -0
  398. package/dist/tools/prompts/gpt5-prompt.d.ts +8 -0
  399. package/dist/tools/prompts/gpt5-prompt.d.ts.map +1 -0
  400. package/dist/tools/prompts/gpt5-prompt.js +177 -0
  401. package/dist/tools/prompts/gpt5-prompt.js.map +1 -0
  402. package/dist/tools/prompts/gpt51-prompt.d.ts +8 -0
  403. package/dist/tools/prompts/gpt51-prompt.d.ts.map +1 -0
  404. package/dist/tools/prompts/gpt51-prompt.js +178 -0
  405. package/dist/tools/prompts/gpt51-prompt.js.map +1 -0
  406. package/dist/tools/prompts/gpt52-prompt.d.ts +8 -0
  407. package/dist/tools/prompts/gpt52-prompt.d.ts.map +1 -0
  408. package/dist/tools/prompts/gpt52-prompt.js +198 -0
  409. package/dist/tools/prompts/gpt52-prompt.js.map +1 -0
  410. package/dist/tools/prompts/index.d.ts +22 -0
  411. package/dist/tools/prompts/index.d.ts.map +1 -0
  412. package/dist/tools/prompts/index.js +23 -0
  413. package/dist/tools/prompts/index.js.map +1 -0
  414. package/dist/tools/prompts/prompt-registry.d.ts +44 -0
  415. package/dist/tools/prompts/prompt-registry.d.ts.map +1 -0
  416. package/dist/tools/prompts/prompt-registry.js +60 -0
  417. package/dist/tools/prompts/prompt-registry.js.map +1 -0
  418. package/dist/tools/prompts/shared-sections.d.ts +28 -0
  419. package/dist/tools/prompts/shared-sections.d.ts.map +1 -0
  420. package/dist/tools/prompts/shared-sections.js +111 -0
  421. package/dist/tools/prompts/shared-sections.js.map +1 -0
  422. package/dist/tools/prompts/xai-prompt.d.ts +8 -0
  423. package/dist/tools/prompts/xai-prompt.d.ts.map +1 -0
  424. package/dist/tools/prompts/xai-prompt.js +68 -0
  425. package/dist/tools/prompts/xai-prompt.js.map +1 -0
  426. package/dist/tools/redeploy-tools.d.ts +30 -0
  427. package/dist/tools/redeploy-tools.d.ts.map +1 -0
  428. package/dist/tools/redeploy-tools.js +191 -0
  429. package/dist/tools/redeploy-tools.js.map +1 -0
  430. package/dist/tools/registry.d.ts +51 -0
  431. package/dist/tools/registry.d.ts.map +1 -0
  432. package/dist/tools/registry.js +148 -0
  433. package/dist/tools/registry.js.map +1 -0
  434. package/dist/tools/screen-share-tools.d.ts +31 -0
  435. package/dist/tools/screen-share-tools.d.ts.map +1 -0
  436. package/dist/tools/screen-share-tools.js +183 -0
  437. package/dist/tools/screen-share-tools.js.map +1 -0
  438. package/dist/tools/surface-tools.d.ts +23 -0
  439. package/dist/tools/surface-tools.d.ts.map +1 -0
  440. package/dist/tools/surface-tools.js +99 -0
  441. package/dist/tools/surface-tools.js.map +1 -0
  442. package/dist/tools/terminal-tools.d.ts +37 -0
  443. package/dist/tools/terminal-tools.d.ts.map +1 -0
  444. package/dist/tools/terminal-tools.js +448 -0
  445. package/dist/tools/terminal-tools.js.map +1 -0
  446. package/dist/tools/thread-tools.d.ts +61 -0
  447. package/dist/tools/thread-tools.d.ts.map +1 -0
  448. package/dist/tools/thread-tools.js +484 -0
  449. package/dist/tools/thread-tools.js.map +1 -0
  450. package/dist/tools/token-estimator.d.ts +55 -0
  451. package/dist/tools/token-estimator.d.ts.map +1 -0
  452. package/dist/tools/token-estimator.js +82 -0
  453. package/dist/tools/token-estimator.js.map +1 -0
  454. package/dist/tools/tool-names.d.ts +64 -0
  455. package/dist/tools/tool-names.d.ts.map +1 -0
  456. package/dist/tools/tool-names.js +76 -0
  457. package/dist/tools/tool-names.js.map +1 -0
  458. package/dist/tools/validate.d.ts +27 -0
  459. package/dist/tools/validate.d.ts.map +1 -0
  460. package/dist/tools/validate.js +99 -0
  461. package/dist/tools/validate.js.map +1 -0
  462. package/dist/tools/voice-tools.d.ts +8 -0
  463. package/dist/tools/voice-tools.d.ts.map +1 -0
  464. package/dist/tools/voice-tools.js +32 -0
  465. package/dist/tools/voice-tools.js.map +1 -0
  466. package/dist/voice/service.d.ts +42 -0
  467. package/dist/voice/service.d.ts.map +1 -0
  468. package/dist/voice/service.js +75 -0
  469. package/dist/voice/service.js.map +1 -0
  470. package/dist/ws.d.ts +90 -0
  471. package/dist/ws.d.ts.map +1 -0
  472. package/dist/ws.js +562 -0
  473. package/dist/ws.js.map +1 -0
  474. package/package.json +61 -0
@@ -0,0 +1,1503 @@
1
+ import { inferContextWindow } from "../config.js";
2
+ import { FileSystemSurface } from "../surfaces/filesystem.js";
3
+ import { resolveWorkspaceRoot } from "../tools/core/get-fs.js";
4
+ import { messages as messagesTable } from "../db/schema.js";
5
+ import { eq } from "drizzle-orm";
6
+ import { uuidv7 } from "../lib/uuidv7.js";
7
+ import { requireAuth } from "../security/http-auth.js";
8
+ import { runAgentLoop, retryToolCall, buildTieredToolSchemas, fromOpenAIName, SteeringController, } from "../tools/agent-loop.js";
9
+ import { isValidChatMode, } from "../tools/chat-modes.js";
10
+ import { buildSystemPrompt } from "../tools/prompts/index.js";
11
+ // ── In-memory state ──────────────────────────────────────────────────
12
+ const sessionHistory = new Map();
13
+ const activeStreams = new Set();
14
+ const sessionAbortControllers = new Map();
15
+ /** Persistent CLI provider sessions — kept alive across turns so the agent retains conversation context */
16
+ const activeCliSessions = new Map();
17
+ const sessionSubscribers = new Map();
18
+ const DEFAULT_UI_MESSAGE_LIMIT = 120;
19
+ const MAX_UI_MESSAGE_LIMIT = 500;
20
+ function parseToolArguments(raw) {
21
+ if (!raw)
22
+ return {};
23
+ try {
24
+ return JSON.parse(raw);
25
+ }
26
+ catch {
27
+ return {};
28
+ }
29
+ }
30
+ function mapPendingToolCallsForUI(toolCalls, resultStateByCallId) {
31
+ const now = Date.now();
32
+ return toolCalls.map((tc) => ({
33
+ callId: tc.id,
34
+ tool: fromOpenAIName(tc.function.name),
35
+ args: parseToolArguments(tc.function.arguments),
36
+ ...(resultStateByCallId?.has(tc.id)
37
+ ? {
38
+ status: resultStateByCallId.get(tc.id).ok ? "success" : "error",
39
+ ok: resultStateByCallId.get(tc.id).ok,
40
+ message: resultStateByCallId.get(tc.id).message,
41
+ data: resultStateByCallId.get(tc.id).data,
42
+ completedAt: now,
43
+ }
44
+ : {
45
+ status: "running",
46
+ startedAt: now,
47
+ }),
48
+ }));
49
+ }
50
+ function mapPersistedToolCallsForUI(toolCalls) {
51
+ return toolCalls.map((tc) => ({
52
+ callId: tc.callId,
53
+ tool: tc.tool,
54
+ args: (typeof tc.args === "object" && tc.args !== null ? tc.args : {}),
55
+ status: tc.ok ? "success" : "error",
56
+ ok: tc.ok,
57
+ message: tc.message,
58
+ output: tc.output,
59
+ data: tc.data,
60
+ startedAt: tc.startedAt,
61
+ completedAt: tc.completedAt,
62
+ }));
63
+ }
64
+ function buildToolResultStateMap(history) {
65
+ const out = new Map();
66
+ for (const msg of history) {
67
+ if (msg.role !== "tool" || !msg.tool_call_id)
68
+ continue;
69
+ let parsed;
70
+ try {
71
+ parsed = JSON.parse(msg.content);
72
+ }
73
+ catch {
74
+ // Keep best effort fallback below.
75
+ }
76
+ const ok = typeof parsed?.ok === "boolean" ? parsed.ok : false;
77
+ const message = typeof parsed?.message === "string"
78
+ ? parsed.message
79
+ : (msg.content?.trim() || (ok ? "Completed" : "Failed"));
80
+ out.set(msg.tool_call_id, {
81
+ ok,
82
+ message,
83
+ data: parsed?.data,
84
+ });
85
+ }
86
+ return out;
87
+ }
88
+ function emitToSubscribers(sessionId, event) {
89
+ const subs = sessionSubscribers.get(sessionId);
90
+ if (subs)
91
+ for (const fn of subs)
92
+ fn(event);
93
+ }
94
+ function subscribe(sessionId, fn) {
95
+ if (!sessionSubscribers.has(sessionId))
96
+ sessionSubscribers.set(sessionId, new Set());
97
+ sessionSubscribers.get(sessionId).add(fn);
98
+ return () => {
99
+ const subs = sessionSubscribers.get(sessionId);
100
+ if (subs) {
101
+ subs.delete(fn);
102
+ if (subs.size === 0)
103
+ sessionSubscribers.delete(sessionId);
104
+ }
105
+ };
106
+ }
107
+ function parseMessageLimit(raw) {
108
+ const parsed = typeof raw === "number"
109
+ ? raw
110
+ : typeof raw === "string"
111
+ ? Number.parseInt(raw, 10)
112
+ : Number.NaN;
113
+ if (!Number.isFinite(parsed) || parsed <= 0)
114
+ return DEFAULT_UI_MESSAGE_LIMIT;
115
+ return Math.min(Math.floor(parsed), MAX_UI_MESSAGE_LIMIT);
116
+ }
117
+ function windowMessages(messages, limit) {
118
+ const total = messages.length;
119
+ const start = Math.max(total - limit, 0);
120
+ return {
121
+ messages: messages.slice(start),
122
+ total,
123
+ hasMore: start > 0,
124
+ };
125
+ }
126
+ function sleep(ms) {
127
+ return new Promise((resolve) => setTimeout(resolve, ms));
128
+ }
129
+ function buildVisibleHistoryEntries(sessionId, history, options) {
130
+ const out = [];
131
+ let visibleIndex = 0;
132
+ const includePendingAssistantToolCalls = options?.includePendingAssistantToolCalls === true;
133
+ const toolResultStateByCallId = includePendingAssistantToolCalls
134
+ ? buildToolResultStateMap(history)
135
+ : undefined;
136
+ for (let i = 0; i < history.length; i++) {
137
+ const m = history[i];
138
+ if (m.role === "system" || m.role === "tool")
139
+ continue;
140
+ let uiToolCalls;
141
+ if (m.role === "assistant") {
142
+ if (Array.isArray(m.uiToolCalls) && m.uiToolCalls.length > 0) {
143
+ uiToolCalls = mapPersistedToolCallsForUI(m.uiToolCalls);
144
+ }
145
+ else if (m.tool_calls && includePendingAssistantToolCalls) {
146
+ uiToolCalls = mapPendingToolCallsForUI(m.tool_calls, toolResultStateByCallId);
147
+ }
148
+ }
149
+ if (m.role === "assistant" && m.tool_calls && !m.content && !includePendingAssistantToolCalls) {
150
+ continue;
151
+ }
152
+ out.push({
153
+ id: `${sessionId}-${visibleIndex}`,
154
+ role: m.role,
155
+ content: m.content,
156
+ toolCalls: uiToolCalls,
157
+ segments: m.segments,
158
+ historyIndex: i,
159
+ });
160
+ visibleIndex++;
161
+ }
162
+ return out;
163
+ }
164
+ function buildVisibleHistoryMessages(sessionId, history, options) {
165
+ return buildVisibleHistoryEntries(sessionId, history, options).map(({ id, role, content, toolCalls, segments }) => ({
166
+ id,
167
+ role,
168
+ content,
169
+ toolCalls,
170
+ segments,
171
+ }));
172
+ }
173
+ // ── System prompt ────────────────────────────────────────────────────
174
+ const SYSTEM_PROMPT = `You are Jait — Just Another Intelligent Tool. You are a capable AI assistant that can run shell commands, read/write files, and manage system surfaces.
175
+
176
+ When the user asks you to do something that requires action (run a command, edit a file, check system info, etc.), use your tools. Don't just describe what you would do — actually do it.
177
+
178
+ Key capabilities:
179
+ - terminal.run: Execute shell commands (PowerShell on Windows). Always use this to run commands.
180
+ - file.read / file.write / file.patch: Read, create, and edit files.
181
+ - file.list / file.stat: Browse the filesystem.
182
+ - os.query: Get system info, running processes, disk usage.
183
+ - surfaces.list / surfaces.start / surfaces.stop: Manage terminal and filesystem surfaces.
184
+ - cron.add / cron.list / cron.update / cron.remove: Create and manage recurring Jait jobs.
185
+
186
+ Guidelines:
187
+ - Be direct and concise.
188
+ - When running commands, use the actual tools — don't just suggest commands.
189
+ - For multi-step tasks, execute them step by step, checking each result.
190
+ - If a command fails, analyze the error and try to fix it.
191
+ - When editing files, read them first to understand the context before patching.
192
+ - For recurring or scheduled automation requests, prefer cron tools and Jait jobs instead of OS-native schedulers.
193
+ - Do not create Windows Task Scheduler jobs unless the user explicitly asks for OS-native scheduling.`;
194
+ /** Max agentic loop iterations to prevent infinite loops */
195
+ const MAX_TOOL_ROUNDS = 15;
196
+ // ── Module-level DB ref for persistence from extracted functions ──────
197
+ let _dbRef;
198
+ let _appRef;
199
+ function persistMessageGlobal(sessionId, role, content, toolCalls, segments) {
200
+ if (!_dbRef)
201
+ return;
202
+ try {
203
+ _dbRef.insert(messagesTable)
204
+ .values({
205
+ id: crypto.randomUUID(),
206
+ sessionId,
207
+ role,
208
+ content,
209
+ toolCalls: toolCalls ?? null,
210
+ segments: segments ?? null,
211
+ createdAt: new Date().toISOString(),
212
+ })
213
+ .run();
214
+ }
215
+ catch (err) {
216
+ _appRef?.log.error(err, "Failed to persist message");
217
+ }
218
+ }
219
+ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
220
+ // Support both old signature (db, sessionService) and new deps object
221
+ let db;
222
+ let sessionService;
223
+ let userService;
224
+ let toolRegistry;
225
+ let surfaceRegistry;
226
+ let audit;
227
+ let toolExecutor;
228
+ let memoryService;
229
+ let ws;
230
+ let sessionStateService;
231
+ let providerRegistry;
232
+ if (depsOrDb && typeof depsOrDb === "object" && "sessionService" in depsOrDb) {
233
+ const deps = depsOrDb;
234
+ db = deps.db;
235
+ sessionService = deps.sessionService;
236
+ userService = deps.userService;
237
+ toolRegistry = deps.toolRegistry;
238
+ surfaceRegistry = deps.surfaceRegistry;
239
+ audit = deps.audit;
240
+ toolExecutor = deps.toolExecutor;
241
+ memoryService = deps.memoryService;
242
+ ws = deps.ws;
243
+ sessionStateService = deps.sessionState;
244
+ providerRegistry = deps.providerRegistry;
245
+ }
246
+ else {
247
+ db = depsOrDb;
248
+ sessionService = sessionServiceArg;
249
+ }
250
+ // Store refs for persistence from extracted functions
251
+ _dbRef = db;
252
+ _appRef = app;
253
+ const hasTools = !!toolRegistry && toolRegistry.list().length > 0;
254
+ // ── Per-session steering controllers and executed tool call tracking ──
255
+ const sessionSteeringControllers = new Map();
256
+ const sessionExecutedToolCalls = new Map();
257
+ /** Plans produced by plan mode — keyed by session ID */
258
+ const sessionPlans = new Map();
259
+ app.log.info(`Chat route: ${hasTools ? toolRegistry.list().length + " tools available for agent (tiered)" : "no tools (text-only mode)"}`);
260
+ // Hydrate in-memory cache from DB if session not yet loaded
261
+ function hydrateSession(sessionId) {
262
+ if (sessionHistory.has(sessionId))
263
+ return;
264
+ if (!db)
265
+ return;
266
+ const rows = db
267
+ .select()
268
+ .from(messagesTable)
269
+ .where(eq(messagesTable.sessionId, sessionId))
270
+ .orderBy(messagesTable.createdAt)
271
+ .all();
272
+ if (rows.length > 0) {
273
+ sessionHistory.set(sessionId, [
274
+ { role: "system", content: SYSTEM_PROMPT },
275
+ ...rows.map((r) => {
276
+ let uiToolCalls;
277
+ let segments;
278
+ if (r.toolCalls) {
279
+ try {
280
+ const parsed = JSON.parse(r.toolCalls);
281
+ if (Array.isArray(parsed)) {
282
+ uiToolCalls = parsed;
283
+ }
284
+ }
285
+ catch {
286
+ // Ignore malformed historical toolCalls payloads.
287
+ }
288
+ }
289
+ if (r.segments) {
290
+ try {
291
+ const parsed = JSON.parse(r.segments);
292
+ if (Array.isArray(parsed)) {
293
+ segments = parsed;
294
+ }
295
+ }
296
+ catch { /* ignore */ }
297
+ }
298
+ return {
299
+ role: r.role,
300
+ content: r.content,
301
+ uiToolCalls,
302
+ segments,
303
+ };
304
+ }),
305
+ ]);
306
+ }
307
+ }
308
+ function persistMessage(sessionId, role, content, toolCalls, segments) {
309
+ if (!db)
310
+ return;
311
+ try {
312
+ db.insert(messagesTable)
313
+ .values({
314
+ id: crypto.randomUUID(),
315
+ sessionId,
316
+ role,
317
+ content,
318
+ toolCalls: toolCalls ?? null,
319
+ segments: segments ?? null,
320
+ createdAt: new Date().toISOString(),
321
+ })
322
+ .run();
323
+ }
324
+ catch (err) {
325
+ app.log.error(err, "Failed to persist message");
326
+ }
327
+ }
328
+ // ── Tool execution helper ──────────────────────────────────────────
329
+ async function executeTool(toolName, args, sessionId, auth, onOutputChunk, signal) {
330
+ if (!toolRegistry) {
331
+ return { ok: false, message: "Tool registry not available" };
332
+ }
333
+ if (signal?.aborted) {
334
+ return { ok: false, message: "Cancelled" };
335
+ }
336
+ const context = {
337
+ sessionId,
338
+ actionId: uuidv7(),
339
+ workspaceRoot: surfaceRegistry
340
+ ? resolveWorkspaceRoot(surfaceRegistry, sessionId)
341
+ : process.cwd(),
342
+ requestedBy: "agent",
343
+ userId: auth?.userId,
344
+ apiKeys: auth?.apiKeys,
345
+ onOutputChunk,
346
+ signal,
347
+ };
348
+ try {
349
+ const toolPromise = toolExecutor
350
+ ? toolExecutor(toolName, args, context)
351
+ : toolRegistry.execute(toolName, args, context, audit);
352
+ // Race the tool execution against the abort signal so a stuck tool
353
+ // (e.g. browser launch hanging) doesn't block the cancel flow forever.
354
+ if (signal && !signal.aborted) {
355
+ const abortPromise = new Promise((resolve) => {
356
+ const onAbort = () => resolve({ ok: false, message: "Cancelled" });
357
+ signal.addEventListener("abort", onAbort, { once: true });
358
+ // Clean up if the tool finishes first
359
+ toolPromise.finally(() => signal.removeEventListener("abort", onAbort));
360
+ });
361
+ return await Promise.race([toolPromise, abortPromise]);
362
+ }
363
+ return await toolPromise;
364
+ }
365
+ catch (err) {
366
+ if (signal?.aborted)
367
+ return { ok: false, message: "Cancelled" };
368
+ return { ok: false, message: err instanceof Error ? err.message : String(err) };
369
+ }
370
+ }
371
+ // ══ POST /api/chat — Main chat endpoint with agentic tool loop ═════
372
+ app.post("/api/chat", async (request, reply) => {
373
+ const authUser = await requireAuth(request, reply, config.jwtSecret);
374
+ if (!authUser)
375
+ return;
376
+ const body = request.body;
377
+ const content = typeof body["content"] === "string"
378
+ ? body["content"]
379
+ : typeof body["message"] === "string"
380
+ ? body["message"]
381
+ : "";
382
+ const sessionId = typeof body["sessionId"] === "string"
383
+ ? body["sessionId"]
384
+ : typeof body["session_id"] === "string"
385
+ ? body["session_id"]
386
+ : crypto.randomUUID();
387
+ const chatMode = isValidChatMode(body["mode"]) ? body["mode"] : "agent";
388
+ const requestProvider = typeof body["provider"] === "string"
389
+ ? body["provider"]
390
+ : undefined;
391
+ if (!content.trim()) {
392
+ return reply
393
+ .status(400)
394
+ .send({ error: "VALIDATION_ERROR", details: "content is required" });
395
+ }
396
+ if (sessionService) {
397
+ const session = sessionService.getById(sessionId, authUser.id);
398
+ if (!session) {
399
+ return reply.status(404).send({ error: "NOT_FOUND", details: "Session not found" });
400
+ }
401
+ }
402
+ const userApiKeys = userService?.getSettings(authUser.id).apiKeys ?? {};
403
+ const effectiveModel = userApiKeys["OPENAI_MODEL"]?.trim() || config.openaiModel;
404
+ const llmRuntime = {
405
+ openaiApiKey: userApiKeys["OPENAI_API_KEY"]?.trim() || config.openaiApiKey,
406
+ openaiBaseUrl: userApiKeys["OPENAI_BASE_URL"]?.trim() || config.openaiBaseUrl,
407
+ openaiModel: effectiveModel,
408
+ contextWindow: userApiKeys["OPENAI_MODEL"]?.trim()
409
+ ? inferContextWindow(effectiveModel)
410
+ : config.contextWindow,
411
+ };
412
+ // Set SSE headers
413
+ reply.raw.writeHead(200, {
414
+ "Content-Type": "text/event-stream",
415
+ "Cache-Control": "no-cache",
416
+ Connection: "keep-alive",
417
+ });
418
+ // Build model endpoint for prompt resolution
419
+ const modelEndpoint = {
420
+ model: llmRuntime.openaiModel,
421
+ baseUrl: llmRuntime.openaiBaseUrl,
422
+ };
423
+ // Build conversation history (hydrate from DB if needed)
424
+ hydrateSession(sessionId);
425
+ // Resolve workspace root so the system prompt includes it
426
+ const sessionRecord = sessionService?.getById(sessionId);
427
+ const wsRoot = surfaceRegistry
428
+ ? resolveWorkspaceRoot(surfaceRegistry, sessionId, sessionRecord?.workspacePath)
429
+ : (sessionRecord?.workspacePath?.trim() || process.cwd());
430
+ const promptCtx = { workspaceRoot: wsRoot };
431
+ if (!sessionHistory.has(sessionId)) {
432
+ sessionHistory.set(sessionId, [
433
+ { role: "system", content: buildSystemPrompt(chatMode, modelEndpoint, promptCtx) },
434
+ ]);
435
+ }
436
+ else {
437
+ // Update system prompt if mode/model/workspace changed mid-session
438
+ const h = sessionHistory.get(sessionId);
439
+ const modePrompt = buildSystemPrompt(chatMode, modelEndpoint, promptCtx);
440
+ if (h[0]?.role === "system" && h[0].content !== modePrompt) {
441
+ h[0] = { role: "system", content: modePrompt };
442
+ }
443
+ }
444
+ const history = sessionHistory.get(sessionId);
445
+ history.push({ role: "user", content });
446
+ persistMessage(sessionId, "user", content);
447
+ try {
448
+ sessionService?.touch(sessionId);
449
+ }
450
+ catch { /* session may not exist */ }
451
+ const streamAbort = new AbortController();
452
+ sessionAbortControllers.set(sessionId, streamAbort);
453
+ let fullContent = "";
454
+ let partialToolCalls = [];
455
+ let resultSegmentsJson;
456
+ let hitMaxRounds = false;
457
+ activeStreams.add(sessionId);
458
+ let clientDisconnected = false;
459
+ reply.raw.on("close", () => { clientDisconnected = true; });
460
+ const safeWrite = (data) => {
461
+ if (!clientDisconnected) {
462
+ try {
463
+ reply.raw.write(data);
464
+ }
465
+ catch {
466
+ clientDisconnected = true;
467
+ }
468
+ }
469
+ };
470
+ const providerLabel = requestProvider === "codex"
471
+ ? "Codex"
472
+ : requestProvider === "claude-code"
473
+ ? "Claude Code"
474
+ : config.llmProvider === "openai" ? "OpenAI" : "Ollama";
475
+ // Create steering controller for this session
476
+ const steering = new SteeringController();
477
+ sessionSteeringControllers.set(sessionId, steering);
478
+ try {
479
+ // ══ CLI Provider path (codex / claude-code via MCP) ══════════
480
+ if (requestProvider && requestProvider !== "jait" && providerRegistry) {
481
+ const cliProvider = providerRegistry.get(requestProvider);
482
+ if (!cliProvider) {
483
+ safeWrite(`data: ${JSON.stringify({ type: "error", message: `Unknown provider: ${requestProvider}` })}\n\n`);
484
+ reply.raw.end();
485
+ return;
486
+ }
487
+ const available = await cliProvider.checkAvailability();
488
+ if (!available) {
489
+ safeWrite(`data: ${JSON.stringify({ type: "error", message: `Provider ${requestProvider} is not available: ${cliProvider.info.unavailableReason}` })}\n\n`);
490
+ reply.raw.end();
491
+ return;
492
+ }
493
+ const cliWsRoot = surfaceRegistry
494
+ ? resolveWorkspaceRoot(surfaceRegistry, sessionId, sessionRecord?.workspacePath)
495
+ : (sessionRecord?.workspacePath?.trim() || process.cwd());
496
+ console.log(`[chat/cli] session=${sessionId} wsRoot="${cliWsRoot}" session.workspacePath="${sessionRecord?.workspacePath}" surfaces=${surfaceRegistry?.getBySession(sessionId)?.length ?? 0}`);
497
+ // Ensure a FileSystemSurface exists for this session so we can
498
+ // back up files before CLI providers (Codex/Claude) write them,
499
+ // enabling the keep/discard (undo) flow.
500
+ let cliFsSurface = null;
501
+ if (surfaceRegistry) {
502
+ const fsId = `fs-${sessionId}`;
503
+ const existing = surfaceRegistry.getSurface(fsId);
504
+ if (existing instanceof FileSystemSurface && existing.state === "running") {
505
+ cliFsSurface = existing;
506
+ }
507
+ else {
508
+ try {
509
+ const started = await surfaceRegistry.startSurface("filesystem", fsId, {
510
+ sessionId,
511
+ workspaceRoot: cliWsRoot,
512
+ });
513
+ cliFsSurface = started;
514
+ }
515
+ catch { /* best effort */ }
516
+ }
517
+ }
518
+ const mcpServers = [providerRegistry.buildJaitMcpServerRef(config)];
519
+ // ── Reuse an existing CLI session if one is alive for this Jait session ──
520
+ const cachedCliSession = activeCliSessions.get(sessionId);
521
+ let providerSessionId;
522
+ if (cachedCliSession && cachedCliSession.providerId === requestProvider) {
523
+ // Existing session with the same provider — try to reuse it
524
+ providerSessionId = cachedCliSession.providerSessionId;
525
+ console.log(`[chat/cli] Reusing ${requestProvider} session ${providerSessionId} for ${sessionId}`);
526
+ }
527
+ else {
528
+ // If the user switched providers, stop the old session first
529
+ if (cachedCliSession) {
530
+ const oldProvider = providerRegistry.get(cachedCliSession.providerId);
531
+ if (oldProvider) {
532
+ try {
533
+ await oldProvider.stopSession(cachedCliSession.providerSessionId);
534
+ }
535
+ catch { /* best effort */ }
536
+ }
537
+ activeCliSessions.delete(sessionId);
538
+ }
539
+ const session = await cliProvider.startSession({
540
+ threadId: sessionId,
541
+ workingDirectory: cliWsRoot,
542
+ mode: "full-access",
543
+ model: typeof body["model"] === "string" ? body["model"] : undefined,
544
+ mcpServers,
545
+ });
546
+ providerSessionId = session.id;
547
+ activeCliSessions.set(sessionId, { providerId: requestProvider, providerSessionId });
548
+ console.log(`[chat/cli] Started new ${requestProvider} session ${providerSessionId} for ${sessionId}`);
549
+ }
550
+ // Collect full content from CLI provider events
551
+ const contentChunks = [];
552
+ // ── Accumulate tool calls + segments for persistence ──
553
+ const cliToolCalls = [];
554
+ const cliSegments = [];
555
+ /** Track the current pending tool-group callIds (batched between text tokens) */
556
+ let pendingToolGroup = [];
557
+ let lastSegmentWasText = false;
558
+ /** Flush any buffered text into a text segment */
559
+ const flushTextSegment = () => {
560
+ if (lastSegmentWasText)
561
+ return; // already flushed
562
+ const text = contentChunks.join("");
563
+ // Only create a segment if there's new text since the last tool group
564
+ const prevTextLen = cliSegments
565
+ .filter((s) => s.type === "text")
566
+ .reduce((n, s) => n + s.content.length, 0);
567
+ const newText = text.slice(prevTextLen);
568
+ if (newText) {
569
+ cliSegments.push({ type: "text", content: newText });
570
+ }
571
+ lastSegmentWasText = true;
572
+ };
573
+ /** Flush any pending tool group into a segment */
574
+ const flushToolGroup = () => {
575
+ if (pendingToolGroup.length > 0) {
576
+ // Before adding a tool group, flush any preceding text
577
+ flushTextSegment();
578
+ cliSegments.push({ type: "toolGroup", callIds: [...pendingToolGroup] });
579
+ pendingToolGroup = [];
580
+ lastSegmentWasText = false;
581
+ }
582
+ };
583
+ const unsubscribe = cliProvider.onEvent((event) => {
584
+ if (event.sessionId !== providerSessionId) {
585
+ return;
586
+ }
587
+ // Map provider events to SSE events the frontend understands
588
+ switch (event.type) {
589
+ case "token":
590
+ // If there's a pending tool group, flush it first
591
+ flushToolGroup();
592
+ contentChunks.push(event.content);
593
+ lastSegmentWasText = false; // new text arrived
594
+ safeWrite(`data: ${JSON.stringify({ type: "token", content: event.content })}\n\n`);
595
+ emitToSubscribers(sessionId, { type: "token", content: event.content });
596
+ break;
597
+ case "tool.start": {
598
+ const callId = event.callId ?? `cli-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
599
+ // Accumulate for persistence
600
+ cliToolCalls.push({
601
+ callId,
602
+ tool: event.tool,
603
+ args: event.args ?? {},
604
+ ok: true,
605
+ message: "",
606
+ startedAt: Date.now(),
607
+ });
608
+ pendingToolGroup.push(callId);
609
+ // Save backup of original file *before* CLI provider writes it
610
+ if (event.tool === "edit" && cliFsSurface) {
611
+ const editPath = String(event.args?.path ?? "");
612
+ if (editPath) {
613
+ cliFsSurface.saveExternalBackup(editPath).catch(() => { });
614
+ }
615
+ }
616
+ safeWrite(`data: ${JSON.stringify({ type: "tool_start", call_id: callId, tool: event.tool, args: event.args })}\n\n`);
617
+ emitToSubscribers(sessionId, { type: "tool_start", call_id: callId, tool: event.tool, args: event.args });
618
+ break;
619
+ }
620
+ case "tool.output": {
621
+ // Accumulate streaming output on the matching tool call
622
+ const tc = cliToolCalls.find(t => t.callId === event.callId);
623
+ if (tc) {
624
+ tc.message = (tc.message || "") + event.content;
625
+ }
626
+ safeWrite(`data: ${JSON.stringify({ type: "tool_output", call_id: event.callId, content: event.content })}\n\n`);
627
+ break;
628
+ }
629
+ case "tool.result": {
630
+ const resultCallId = event.callId ?? `cli-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
631
+ // Update the matching tool call record
632
+ const tc = cliToolCalls.find(t => t.callId === resultCallId);
633
+ if (tc) {
634
+ tc.ok = event.ok;
635
+ tc.message = event.message || tc.message;
636
+ tc.data = event.data;
637
+ tc.completedAt = Date.now();
638
+ }
639
+ safeWrite(`data: ${JSON.stringify({ type: "tool_result", call_id: resultCallId, tool: event.tool, ok: event.ok, message: event.message, data: event.data })}\n\n`);
640
+ emitToSubscribers(sessionId, { type: "tool_result", call_id: resultCallId, tool: event.tool, ok: event.ok, message: event.message });
641
+ // Emit file_changed for successful edits → drives the keep/discard UI
642
+ if (event.ok && event.tool === "edit") {
643
+ const editPath = String(tc?.args ? tc.args.path ?? "" : "");
644
+ if (editPath) {
645
+ const editName = editPath.split(/[\/\\]/).pop() ?? editPath;
646
+ safeWrite(`data: ${JSON.stringify({ type: "file_changed", path: editPath, name: editName })}\n\n`);
647
+ // Broadcast to other session clients
648
+ if (ws) {
649
+ ws.broadcast(sessionId, {
650
+ type: "ui.state-sync",
651
+ sessionId,
652
+ timestamp: new Date().toISOString(),
653
+ payload: { key: "file_changed", value: { path: editPath, name: editName } },
654
+ });
655
+ }
656
+ // Persist cumulative changed files list
657
+ if (sessionStateService) {
658
+ try {
659
+ const existing = sessionStateService.get(sessionId, ["changed_files"]);
660
+ const files = Array.isArray(existing["changed_files"]) ? existing["changed_files"] : [];
661
+ if (!files.some((f) => f.path === editPath)) {
662
+ files.push({ path: editPath, name: editName });
663
+ sessionStateService.set(sessionId, { changed_files: files });
664
+ }
665
+ }
666
+ catch { /* ignore */ }
667
+ }
668
+ }
669
+ }
670
+ break;
671
+ }
672
+ case "tool.approval-required":
673
+ safeWrite(`data: ${JSON.stringify({ type: "approval_required", tool: event.tool, args: event.args, requestId: event.requestId })}\n\n`);
674
+ break;
675
+ case "message":
676
+ if (event.role === "assistant") {
677
+ flushToolGroup();
678
+ contentChunks.push(event.content);
679
+ lastSegmentWasText = false;
680
+ }
681
+ break;
682
+ case "session.error":
683
+ safeWrite(`data: ${JSON.stringify({ type: "error", message: event.error })}\n\n`);
684
+ break;
685
+ }
686
+ });
687
+ // Send the turn — with recovery if the cached session died between messages
688
+ try {
689
+ await cliProvider.sendTurn(providerSessionId, content);
690
+ }
691
+ catch (sendErr) {
692
+ // Session likely died (process exited) — start a fresh one
693
+ console.warn(`[chat/cli] sendTurn failed on cached session, recovering:`, sendErr);
694
+ activeCliSessions.delete(sessionId);
695
+ const freshSession = await cliProvider.startSession({
696
+ threadId: sessionId,
697
+ workingDirectory: cliWsRoot,
698
+ mode: "full-access",
699
+ model: typeof body["model"] === "string" ? body["model"] : undefined,
700
+ mcpServers,
701
+ });
702
+ providerSessionId = freshSession.id;
703
+ activeCliSessions.set(sessionId, { providerId: requestProvider, providerSessionId });
704
+ console.log(`[chat/cli] Recovered with new session ${providerSessionId}`);
705
+ await cliProvider.sendTurn(providerSessionId, content);
706
+ }
707
+ // Wait for turn completion or error
708
+ await new Promise((resolve) => {
709
+ const checkDone = cliProvider.onEvent((event) => {
710
+ if (event.sessionId !== providerSessionId) {
711
+ return;
712
+ }
713
+ if (event.type === "session.completed" || event.type === "session.error") {
714
+ // If the session errored, invalidate the cache so the next message creates a fresh one
715
+ if (event.type === "session.error") {
716
+ activeCliSessions.delete(sessionId);
717
+ }
718
+ checkDone();
719
+ resolve();
720
+ }
721
+ });
722
+ // Also abort if client disconnects
723
+ streamAbort.signal.addEventListener("abort", () => {
724
+ cliProvider.interruptTurn(providerSessionId).catch(() => { });
725
+ resolve();
726
+ });
727
+ });
728
+ unsubscribe();
729
+ fullContent = contentChunks.join("");
730
+ // Flush any remaining tool group / trailing text into segments
731
+ flushToolGroup();
732
+ flushTextSegment();
733
+ // Build persistence JSON
734
+ const cliTcJson = cliToolCalls.length > 0 ? JSON.stringify(cliToolCalls) : undefined;
735
+ const cliSegJson = cliSegments.length > 0 ? JSON.stringify(cliSegments) : undefined;
736
+ // Also stash on the outer scope so the done handler can emit them
737
+ partialToolCalls = cliToolCalls;
738
+ resultSegmentsJson = cliSegJson;
739
+ // Persist assistant message with tool calls and segments
740
+ history.push({ role: "assistant", content: fullContent, uiToolCalls: cliToolCalls.length > 0 ? cliToolCalls : undefined });
741
+ persistMessage(sessionId, "assistant", fullContent, cliTcJson, cliSegJson);
742
+ // Session stays alive for the next turn — do NOT stop it.
743
+ // It will be cleaned up on session error, provider switch, or server shutdown.
744
+ }
745
+ else if (config.llmProvider === "openai") {
746
+ // ══ OpenAI agentic loop (using extracted runAgentLoop) ═════
747
+ // Build tiered schemas per request — respects user-disabled tools
748
+ const userSettings = userService?.getSettings(authUser.id);
749
+ const disabledTools = userSettings?.disabledTools?.length
750
+ ? new Set(userSettings.disabledTools)
751
+ : undefined;
752
+ const toolSchemas = toolRegistry
753
+ ? buildTieredToolSchemas(toolRegistry, disabledTools)
754
+ : [];
755
+ const onEvent = (event) => {
756
+ emitToSubscribers(sessionId, event);
757
+ safeWrite(`data: ${JSON.stringify(event)}\n\n`);
758
+ // ── Cross-client sync: persist & broadcast state changes ──
759
+ const ev = event;
760
+ // Broadcast todo list updates to all session clients and persist to DB
761
+ if (ev.type === "todo_list" && Array.isArray(ev.items)) {
762
+ if (sessionStateService) {
763
+ try {
764
+ sessionStateService.set(sessionId, { "todo_list": ev.items });
765
+ }
766
+ catch { /* ignore */ }
767
+ }
768
+ if (ws) {
769
+ ws.broadcast(sessionId, {
770
+ type: "ui.state-sync",
771
+ sessionId,
772
+ timestamp: new Date().toISOString(),
773
+ payload: { key: "todo_list", value: ev.items },
774
+ });
775
+ }
776
+ }
777
+ // Broadcast file change events and persist cumulative list
778
+ if (ev.type === "file_changed" && typeof ev.path === "string") {
779
+ if (ws) {
780
+ ws.broadcast(sessionId, {
781
+ type: "ui.state-sync",
782
+ sessionId,
783
+ timestamp: new Date().toISOString(),
784
+ payload: { key: "file_changed", value: { path: ev.path, name: ev.name } },
785
+ });
786
+ }
787
+ // Persist cumulative changed files list
788
+ if (sessionStateService) {
789
+ try {
790
+ const existing = sessionStateService.get(sessionId, ["changed_files"]);
791
+ const files = Array.isArray(existing["changed_files"]) ? existing["changed_files"] : [];
792
+ if (!files.some((f) => f.path === ev.path)) {
793
+ files.push({ path: ev.path, name: ev.name ?? "" });
794
+ sessionStateService.set(sessionId, { "changed_files": files });
795
+ }
796
+ }
797
+ catch { /* ignore */ }
798
+ }
799
+ }
800
+ };
801
+ const result = await runAgentLoop({
802
+ llm: llmRuntime,
803
+ history,
804
+ toolSchemas,
805
+ hasTools,
806
+ sessionId,
807
+ auth: { userId: authUser.id, apiKeys: userApiKeys },
808
+ abort: streamAbort,
809
+ maxRounds: MAX_TOOL_ROUNDS,
810
+ parallel: true,
811
+ toolRegistry,
812
+ disabledTools,
813
+ mode: chatMode,
814
+ onEvent,
815
+ onPersist: (sid, role, content, tc, seg) => persistMessage(sid, role, content, tc, seg),
816
+ log: app.log,
817
+ }, executeTool, steering);
818
+ fullContent = result.content;
819
+ partialToolCalls = result.executedToolCalls;
820
+ resultSegmentsJson = result.segments.length > 0 ? JSON.stringify(result.segments) : undefined;
821
+ hitMaxRounds = result.hitMaxRounds;
822
+ // Track executed tool calls for retry API
823
+ sessionExecutedToolCalls.set(sessionId, result.executedToolCalls);
824
+ // Store plan if plan mode produced one
825
+ if (result.plan) {
826
+ sessionPlans.set(sessionId, result.plan);
827
+ }
828
+ }
829
+ else {
830
+ // ══ Ollama (text only — no tool support) ═══════════════════
831
+ fullContent = await runOllamaStream(config, history, sessionId, streamAbort, safeWrite, app);
832
+ }
833
+ }
834
+ catch (err) {
835
+ // The OpenAI agentic loop now handles AbortError internally and returns
836
+ // partial results. This catch only fires for non-abort errors (OpenAI)
837
+ // or for Ollama stream errors (including abort).
838
+ const wasCancelled = err instanceof Error && err.name === "AbortError";
839
+ if (!wasCancelled)
840
+ app.log.error(err, `${providerLabel} streaming error`);
841
+ // Save partial content for real (non-cancel) errors
842
+ if (!wasCancelled && (fullContent || partialToolCalls.length > 0)) {
843
+ const tcJson = partialToolCalls.length > 0 ? JSON.stringify(partialToolCalls) : undefined;
844
+ persistMessage(sessionId, "assistant", fullContent || "", tcJson, resultSegmentsJson);
845
+ }
846
+ const errMsg = wasCancelled
847
+ ? "cancelled"
848
+ : err instanceof Error ? err.message : `Failed to reach ${providerLabel}`;
849
+ emitToSubscribers(sessionId, wasCancelled
850
+ ? { type: "done", session_id: sessionId, prompt_count: history.filter(m => m.role === "user").length, remaining_prompts: null }
851
+ : { type: "error", message: errMsg });
852
+ try {
853
+ safeWrite(`data: ${JSON.stringify(wasCancelled ? { type: "done", session_id: sessionId } : { type: "error", message: errMsg })}\n\n`);
854
+ }
855
+ catch { /* client gone */ }
856
+ }
857
+ // Persist partial results BEFORE clearing stream state so that a reload
858
+ // between these two steps loads the cancelled tool calls from the DB.
859
+ if (streamAbort.signal.aborted && partialToolCalls.length > 0) {
860
+ const tcJson = JSON.stringify(partialToolCalls);
861
+ persistMessage(sessionId, "assistant", fullContent || "", tcJson, resultSegmentsJson);
862
+ }
863
+ activeStreams.delete(sessionId);
864
+ sessionAbortControllers.delete(sessionId);
865
+ sessionSteeringControllers.delete(sessionId);
866
+ // Clean up in-memory history: remove any dangling assistant tool_calls
867
+ // messages that never got a text response (e.g. cancelled mid-tool-call).
868
+ // This prevents them from showing as "running" on reload.
869
+ const currentHistory = sessionHistory.get(sessionId);
870
+ if (currentHistory) {
871
+ // Walk backwards: if the last messages are assistant+tool_calls with no
872
+ // following text response, and the corresponding tool results are missing,
873
+ // remove them so the history is clean for the next session load.
874
+ while (currentHistory.length > 0) {
875
+ const last = currentHistory[currentHistory.length - 1];
876
+ // Remove orphaned tool result messages at the tail
877
+ if (last.role === "tool") {
878
+ currentHistory.pop();
879
+ continue;
880
+ }
881
+ // Remove assistant messages that only contain tool_calls with no text
882
+ if (last.role === "assistant" && last.tool_calls && !last.content) {
883
+ currentHistory.pop();
884
+ continue;
885
+ }
886
+ break;
887
+ }
888
+ }
889
+ // Final done event
890
+ const doneEvent = {
891
+ type: "done",
892
+ session_id: sessionId,
893
+ prompt_count: history.filter(m => m.role === "user").length,
894
+ remaining_prompts: null,
895
+ hit_max_rounds: hitMaxRounds,
896
+ };
897
+ emitToSubscribers(sessionId, doneEvent);
898
+ safeWrite(`data: ${JSON.stringify(doneEvent)}\n\n`);
899
+ try {
900
+ reply.raw.end();
901
+ }
902
+ catch { /* already closed */ }
903
+ // Notify all WS-subscribed clients that the chat is done so they can refresh.
904
+ if (ws) {
905
+ ws.broadcast(sessionId, {
906
+ type: "message.complete",
907
+ sessionId,
908
+ timestamp: new Date().toISOString(),
909
+ payload: {},
910
+ });
911
+ }
912
+ });
913
+ // Cancel an active stream for a session
914
+ app.post("/api/sessions/:sessionId/cancel", async (request, reply) => {
915
+ const authUser = await requireAuth(request, reply, config.jwtSecret);
916
+ if (!authUser)
917
+ return;
918
+ const { sessionId } = request.params;
919
+ if (sessionService) {
920
+ const session = sessionService.getById(sessionId, authUser.id);
921
+ if (!session) {
922
+ return reply.status(404).send({ error: "NOT_FOUND", details: "Session not found" });
923
+ }
924
+ }
925
+ const controller = sessionAbortControllers.get(sessionId);
926
+ if (controller) {
927
+ controller.abort();
928
+ return { ok: true, cancelled: true };
929
+ }
930
+ return { ok: true, cancelled: false };
931
+ });
932
+ // Truncate a session from a specific user message onward (used for edit + replay).
933
+ app.post("/api/sessions/:sessionId/restart-from", async (request, reply) => {
934
+ const authUser = await requireAuth(request, reply, config.jwtSecret);
935
+ if (!authUser)
936
+ return;
937
+ const { sessionId } = request.params;
938
+ if (sessionService) {
939
+ const session = sessionService.getById(sessionId, authUser.id);
940
+ if (!session) {
941
+ return reply.status(404).send({ error: "NOT_FOUND", details: "Session not found" });
942
+ }
943
+ }
944
+ const body = request.body ?? {};
945
+ const messageId = typeof body["messageId"] === "string" ? body["messageId"] : "";
946
+ const messageIndex = typeof body["messageIndex"] === "number" ? body["messageIndex"] : -1;
947
+ const messageFromEnd = typeof body["messageFromEnd"] === "number" ? body["messageFromEnd"] : -1;
948
+ if (!messageId && messageIndex < 0 && messageFromEnd < 0) {
949
+ return reply.status(400).send({ error: "VALIDATION_ERROR", details: "messageId, messageFromEnd, or messageIndex is required" });
950
+ }
951
+ if (activeStreams.has(sessionId)) {
952
+ const controller = sessionAbortControllers.get(sessionId);
953
+ if (controller)
954
+ controller.abort();
955
+ const deadline = Date.now() + 5000;
956
+ while (activeStreams.has(sessionId) && Date.now() < deadline) {
957
+ await sleep(50);
958
+ }
959
+ if (activeStreams.has(sessionId)) {
960
+ return reply.status(409).send({ error: "CONFLICT", details: "Cannot restart while session is streaming" });
961
+ }
962
+ }
963
+ hydrateSession(sessionId);
964
+ const history = sessionHistory.get(sessionId) ?? [];
965
+ const visibleEntries = buildVisibleHistoryEntries(sessionId, history);
966
+ let targetVisibleIndex = visibleEntries.findIndex((m) => m.id === messageId);
967
+ if (targetVisibleIndex === -1 &&
968
+ Number.isFinite(messageFromEnd) &&
969
+ messageFromEnd >= 0 &&
970
+ messageFromEnd < visibleEntries.length) {
971
+ targetVisibleIndex = visibleEntries.length - 1 - Math.floor(messageFromEnd);
972
+ }
973
+ if (targetVisibleIndex === -1 &&
974
+ Number.isFinite(messageIndex) &&
975
+ messageIndex >= 0 &&
976
+ messageIndex < visibleEntries.length) {
977
+ targetVisibleIndex = Math.floor(messageIndex);
978
+ }
979
+ if (targetVisibleIndex === -1) {
980
+ return reply.status(404).send({ error: "NOT_FOUND", details: "Message not found" });
981
+ }
982
+ const target = visibleEntries[targetVisibleIndex];
983
+ if (target.role !== "user") {
984
+ return reply.status(400).send({ error: "VALIDATION_ERROR", details: "Only user messages can be edited/restarted" });
985
+ }
986
+ if (memoryService) {
987
+ const toFlush = visibleEntries
988
+ .slice(targetVisibleIndex)
989
+ .filter((entry) => entry.content.trim().length > 0)
990
+ .map((entry) => `[${entry.role}] ${entry.content}`);
991
+ await memoryService.flushPreCompaction(sessionId, toFlush);
992
+ }
993
+ const truncatedHistory = history.slice(0, target.historyIndex);
994
+ sessionHistory.set(sessionId, truncatedHistory);
995
+ if (db) {
996
+ const rows = db
997
+ .select()
998
+ .from(messagesTable)
999
+ .where(eq(messagesTable.sessionId, sessionId))
1000
+ .orderBy(messagesTable.createdAt)
1001
+ .all();
1002
+ const rowsToDelete = rows.slice(targetVisibleIndex);
1003
+ for (const row of rowsToDelete) {
1004
+ db.delete(messagesTable).where(eq(messagesTable.id, row.id)).run();
1005
+ }
1006
+ }
1007
+ try {
1008
+ sessionService?.touch(sessionId);
1009
+ }
1010
+ catch { /* ignore */ }
1011
+ const updatedMessages = buildVisibleHistoryMessages(sessionId, truncatedHistory);
1012
+ const windowed = windowMessages(updatedMessages, DEFAULT_UI_MESSAGE_LIMIT);
1013
+ return {
1014
+ ok: true,
1015
+ sessionId,
1016
+ streaming: false,
1017
+ total: windowed.total,
1018
+ hasMore: windowed.hasMore,
1019
+ limit: DEFAULT_UI_MESSAGE_LIMIT,
1020
+ messages: windowed.messages,
1021
+ };
1022
+ });
1023
+ // List messages in a session
1024
+ app.get("/api/sessions/:sessionId/messages", async (request, reply) => {
1025
+ const authUser = await requireAuth(request, reply, config.jwtSecret);
1026
+ if (!authUser)
1027
+ return;
1028
+ const { sessionId } = request.params;
1029
+ if (sessionService) {
1030
+ const session = sessionService.getById(sessionId, authUser.id);
1031
+ if (!session) {
1032
+ return reply.status(404).send({ error: "NOT_FOUND", details: "Session not found" });
1033
+ }
1034
+ }
1035
+ const query = request.query;
1036
+ const limit = parseMessageLimit(query?.limit);
1037
+ hydrateSession(sessionId);
1038
+ const history = sessionHistory.get(sessionId) ?? [];
1039
+ const visible = buildVisibleHistoryMessages(sessionId, history);
1040
+ const windowed = windowMessages(visible, limit);
1041
+ return {
1042
+ sessionId,
1043
+ streaming: activeStreams.has(sessionId),
1044
+ total: windowed.total,
1045
+ hasMore: windowed.hasMore,
1046
+ limit,
1047
+ messages: windowed.messages,
1048
+ };
1049
+ });
1050
+ // SSE stream-resume: join an in-progress session's token stream
1051
+ // Client receives a snapshot of current content, then live tokens until done.
1052
+ app.get("/api/sessions/:sessionId/stream", async (request, reply) => {
1053
+ const authUser = await requireAuth(request, reply, config.jwtSecret);
1054
+ if (!authUser)
1055
+ return;
1056
+ const { sessionId } = request.params;
1057
+ if (sessionService) {
1058
+ const session = sessionService.getById(sessionId, authUser.id);
1059
+ if (!session) {
1060
+ return reply.status(404).send({ error: "NOT_FOUND", details: "Session not found" });
1061
+ }
1062
+ }
1063
+ const query = request.query;
1064
+ const limit = parseMessageLimit(query?.limit);
1065
+ reply.raw.writeHead(200, {
1066
+ "Content-Type": "text/event-stream",
1067
+ "Cache-Control": "no-cache",
1068
+ Connection: "keep-alive",
1069
+ });
1070
+ hydrateSession(sessionId);
1071
+ const history = sessionHistory.get(sessionId) ?? [];
1072
+ const isStreaming = activeStreams.has(sessionId);
1073
+ // Build snapshot. While streaming, prefer in-memory history so partial assistant
1074
+ // content is visible immediately (DB persistence may lag until stream completion).
1075
+ let snapshotMessages;
1076
+ let total = 0;
1077
+ let hasMore = false;
1078
+ if (db && !isStreaming) {
1079
+ const rows = db
1080
+ .select()
1081
+ .from(messagesTable)
1082
+ .where(eq(messagesTable.sessionId, sessionId))
1083
+ .orderBy(messagesTable.createdAt)
1084
+ .all();
1085
+ const allMessages = rows
1086
+ .filter((r) => r.role === "user" || r.role === "assistant")
1087
+ .map((r, i) => {
1088
+ const msg = {
1089
+ id: `${sessionId}-${i}`,
1090
+ role: r.role,
1091
+ content: r.content,
1092
+ };
1093
+ if (r.toolCalls) {
1094
+ try {
1095
+ msg.toolCalls = JSON.parse(r.toolCalls);
1096
+ }
1097
+ catch { /* ignore */ }
1098
+ }
1099
+ if (r.segments) {
1100
+ try {
1101
+ msg.segments = JSON.parse(r.segments);
1102
+ }
1103
+ catch { /* ignore */ }
1104
+ }
1105
+ return msg;
1106
+ });
1107
+ const windowed = windowMessages(allMessages, limit);
1108
+ snapshotMessages = windowed.messages;
1109
+ total = windowed.total;
1110
+ hasMore = windowed.hasMore;
1111
+ }
1112
+ else {
1113
+ const allMessages = buildVisibleHistoryMessages(sessionId, history, { includePendingAssistantToolCalls: isStreaming });
1114
+ const windowed = windowMessages(allMessages, limit);
1115
+ snapshotMessages = windowed.messages;
1116
+ total = windowed.total;
1117
+ hasMore = windowed.hasMore;
1118
+ }
1119
+ reply.raw.write(`data: ${JSON.stringify({
1120
+ type: "snapshot",
1121
+ messages: snapshotMessages,
1122
+ streaming: isStreaming,
1123
+ total,
1124
+ hasMore,
1125
+ limit,
1126
+ })}\n\n`);
1127
+ if (!isStreaming) {
1128
+ // Not streaming — send done immediately
1129
+ reply.raw.write(`data: ${JSON.stringify({ type: "done", session_id: sessionId, prompt_count: history.filter(m => m.role === "user").length, remaining_prompts: null })}\n\n`);
1130
+ reply.raw.end();
1131
+ return;
1132
+ }
1133
+ // Subscribe to live events
1134
+ let closed = false;
1135
+ let unsubscribe = () => { };
1136
+ const closeStream = () => {
1137
+ if (closed)
1138
+ return;
1139
+ closed = true;
1140
+ unsubscribe();
1141
+ try {
1142
+ reply.raw.end();
1143
+ }
1144
+ catch { /* already closed */ }
1145
+ };
1146
+ reply.raw.on("close", () => {
1147
+ closeStream();
1148
+ });
1149
+ unsubscribe = subscribe(sessionId, (event) => {
1150
+ if (closed)
1151
+ return;
1152
+ try {
1153
+ reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
1154
+ if (event.type === "done" || event.type === "error") {
1155
+ closeStream();
1156
+ }
1157
+ }
1158
+ catch {
1159
+ closeStream();
1160
+ }
1161
+ });
1162
+ // Clean up subscription if client disconnects before stream finishes
1163
+ request.raw.on("close", () => {
1164
+ closeStream();
1165
+ });
1166
+ });
1167
+ // ── POST /api/sessions/:sessionId/retry-tool ────────────────────────
1168
+ // Retry a specific failed tool call by its callId.
1169
+ app.post("/api/sessions/:sessionId/retry-tool", async (request, reply) => {
1170
+ const authUser = await requireAuth(request, reply, config.jwtSecret);
1171
+ if (!authUser)
1172
+ return;
1173
+ const { sessionId } = request.params;
1174
+ if (sessionService) {
1175
+ const session = sessionService.getById(sessionId, authUser.id);
1176
+ if (!session) {
1177
+ return reply.status(404).send({ error: "NOT_FOUND", details: "Session not found" });
1178
+ }
1179
+ }
1180
+ const body = request.body ?? {};
1181
+ const callId = typeof body["callId"] === "string" ? body["callId"] : "";
1182
+ if (!callId) {
1183
+ return reply.status(400).send({ error: "VALIDATION_ERROR", details: "callId is required" });
1184
+ }
1185
+ // Cannot retry while a stream is active
1186
+ if (activeStreams.has(sessionId)) {
1187
+ return reply.status(409).send({ error: "CONFLICT", details: "Cannot retry while session is streaming" });
1188
+ }
1189
+ const executed = sessionExecutedToolCalls.get(sessionId);
1190
+ if (!executed) {
1191
+ return reply.status(404).send({ error: "NOT_FOUND", details: "No tool calls recorded for this session" });
1192
+ }
1193
+ const original = executed.find((tc) => tc.callId === callId);
1194
+ if (!original) {
1195
+ return reply.status(404).send({ error: "NOT_FOUND", details: `Tool call ${callId} not found` });
1196
+ }
1197
+ hydrateSession(sessionId);
1198
+ const history = sessionHistory.get(sessionId);
1199
+ if (!history) {
1200
+ return reply.status(404).send({ error: "NOT_FOUND", details: "Session history not found" });
1201
+ }
1202
+ const userApiKeys = userService?.getSettings(authUser.id).apiKeys ?? {};
1203
+ const result = await retryToolCall(callId, history, executed, executeTool, sessionId, { userId: authUser.id, apiKeys: userApiKeys }, (event) => emitToSubscribers(sessionId, event));
1204
+ // Persist updated history entry
1205
+ if (db) {
1206
+ const tcJson = JSON.stringify(executed);
1207
+ // Find the last assistant message and update its tool calls
1208
+ const rows = db
1209
+ .select()
1210
+ .from(messagesTable)
1211
+ .where(eq(messagesTable.sessionId, sessionId))
1212
+ .orderBy(messagesTable.createdAt)
1213
+ .all();
1214
+ const lastAssistant = [...rows].reverse().find((r) => r.role === "assistant");
1215
+ if (lastAssistant) {
1216
+ db.update(messagesTable)
1217
+ .set({ toolCalls: tcJson })
1218
+ .where(eq(messagesTable.id, lastAssistant.id))
1219
+ .run();
1220
+ }
1221
+ }
1222
+ return {
1223
+ ok: result.ok,
1224
+ callId,
1225
+ tool: original.tool,
1226
+ message: result.message,
1227
+ data: result.data,
1228
+ retryCount: original.retryCount,
1229
+ };
1230
+ });
1231
+ // ── POST /api/sessions/:sessionId/steer ─────────────────────────────
1232
+ // Inject a steering message into an active agent loop.
1233
+ app.post("/api/sessions/:sessionId/steer", async (request, reply) => {
1234
+ const authUser = await requireAuth(request, reply, config.jwtSecret);
1235
+ if (!authUser)
1236
+ return;
1237
+ const { sessionId } = request.params;
1238
+ if (sessionService) {
1239
+ const session = sessionService.getById(sessionId, authUser.id);
1240
+ if (!session) {
1241
+ return reply.status(404).send({ error: "NOT_FOUND", details: "Session not found" });
1242
+ }
1243
+ }
1244
+ const body = request.body ?? {};
1245
+ const message = typeof body["message"] === "string" ? body["message"] : "";
1246
+ if (!message.trim()) {
1247
+ return reply.status(400).send({ error: "VALIDATION_ERROR", details: "message is required" });
1248
+ }
1249
+ if (!activeStreams.has(sessionId)) {
1250
+ return reply.status(409).send({ error: "CONFLICT", details: "No active stream for this session — steering only works during streaming" });
1251
+ }
1252
+ const controller = sessionSteeringControllers.get(sessionId);
1253
+ if (!controller) {
1254
+ return reply.status(404).send({ error: "NOT_FOUND", details: "No steering controller for this session" });
1255
+ }
1256
+ controller.steer(message);
1257
+ return { ok: true, steered: true };
1258
+ });
1259
+ // ══ GET /api/sessions/:sessionId/plan — Get pending plan ═══════════
1260
+ app.get("/api/sessions/:sessionId/plan", async (request, reply) => {
1261
+ const authUser = await requireAuth(request, reply, config.jwtSecret);
1262
+ if (!authUser)
1263
+ return;
1264
+ const { sessionId } = request.params;
1265
+ if (sessionService) {
1266
+ const session = sessionService.getById(sessionId, authUser.id);
1267
+ if (!session) {
1268
+ return reply.status(404).send({ error: "NOT_FOUND", details: "Session not found" });
1269
+ }
1270
+ }
1271
+ const plan = sessionPlans.get(sessionId);
1272
+ if (!plan) {
1273
+ return reply.status(404).send({ error: "NOT_FOUND", details: "No pending plan for this session" });
1274
+ }
1275
+ return {
1276
+ plan_id: plan.id,
1277
+ summary: plan.summary,
1278
+ actions: plan.actions.map((a) => ({
1279
+ id: a.id,
1280
+ tool: a.tool,
1281
+ args: a.args,
1282
+ description: a.description,
1283
+ order: a.order,
1284
+ status: a.status,
1285
+ })),
1286
+ };
1287
+ });
1288
+ // ══ POST /api/sessions/:sessionId/plan/execute — Execute approved plan ═
1289
+ app.post("/api/sessions/:sessionId/plan/execute", async (request, reply) => {
1290
+ const authUser = await requireAuth(request, reply, config.jwtSecret);
1291
+ if (!authUser)
1292
+ return;
1293
+ const { sessionId } = request.params;
1294
+ if (sessionService) {
1295
+ const session = sessionService.getById(sessionId, authUser.id);
1296
+ if (!session) {
1297
+ return reply.status(404).send({ error: "NOT_FOUND", details: "Session not found" });
1298
+ }
1299
+ }
1300
+ const plan = sessionPlans.get(sessionId);
1301
+ if (!plan) {
1302
+ return reply.status(404).send({ error: "NOT_FOUND", details: "No pending plan for this session" });
1303
+ }
1304
+ const body = request.body ?? {};
1305
+ // Optional: allow partial approval by specifying action IDs to execute
1306
+ const approvedActionIds = Array.isArray(body["action_ids"])
1307
+ ? new Set(body["action_ids"].filter((id) => typeof id === "string"))
1308
+ : null;
1309
+ const userApiKeys = userService?.getSettings(authUser.id).apiKeys ?? {};
1310
+ // SSE headers for streaming plan execution
1311
+ reply.raw.writeHead(200, {
1312
+ "Content-Type": "text/event-stream",
1313
+ "Cache-Control": "no-cache",
1314
+ Connection: "keep-alive",
1315
+ });
1316
+ let clientDisconnected = false;
1317
+ reply.raw.on("close", () => { clientDisconnected = true; });
1318
+ const safeWrite = (data) => {
1319
+ if (!clientDisconnected) {
1320
+ try {
1321
+ reply.raw.write(data);
1322
+ }
1323
+ catch {
1324
+ clientDisconnected = true;
1325
+ }
1326
+ }
1327
+ };
1328
+ const executionResults = [];
1329
+ for (const action of plan.actions) {
1330
+ // Skip rejected or already-executed actions
1331
+ if (action.status === "rejected" || action.status === "executed")
1332
+ continue;
1333
+ // If partial approval, skip non-approved
1334
+ if (approvedActionIds && !approvedActionIds.has(action.id)) {
1335
+ action.status = "rejected";
1336
+ continue;
1337
+ }
1338
+ action.status = "approved";
1339
+ safeWrite(`data: ${JSON.stringify({ type: "plan_action_start", id: action.id, tool: action.tool, order: action.order })}\n\n`);
1340
+ emitToSubscribers(sessionId, { type: "tool_start", tool: action.tool, args: action.args, call_id: action.id });
1341
+ try {
1342
+ const result = await executeTool(action.tool, action.args, sessionId, { userId: authUser.id, apiKeys: userApiKeys }, (chunk) => {
1343
+ safeWrite(`data: ${JSON.stringify({ type: "plan_action_output", id: action.id, content: chunk })}\n\n`);
1344
+ });
1345
+ action.status = result.ok ? "executed" : "failed";
1346
+ action.result = { ok: result.ok, message: result.message, data: result.data };
1347
+ executionResults.push({ id: action.id, tool: action.tool, ok: result.ok, message: result.message, data: result.data });
1348
+ safeWrite(`data: ${JSON.stringify({
1349
+ type: "plan_action_result",
1350
+ id: action.id,
1351
+ tool: action.tool,
1352
+ ok: result.ok,
1353
+ message: result.message,
1354
+ data: result.data,
1355
+ })}\n\n`);
1356
+ emitToSubscribers(sessionId, {
1357
+ type: "tool_result",
1358
+ call_id: action.id,
1359
+ tool: action.tool,
1360
+ ok: result.ok,
1361
+ message: result.message,
1362
+ data: result.data,
1363
+ });
1364
+ // Add to conversation history so the agent has context
1365
+ const history = sessionHistory.get(sessionId);
1366
+ if (history) {
1367
+ history.push({
1368
+ role: "tool",
1369
+ content: JSON.stringify({ ok: result.ok, message: result.message, data: result.data }),
1370
+ tool_call_id: action.id,
1371
+ name: action.tool,
1372
+ });
1373
+ }
1374
+ }
1375
+ catch (err) {
1376
+ const message = err instanceof Error ? err.message : String(err);
1377
+ action.status = "failed";
1378
+ action.result = { ok: false, message };
1379
+ executionResults.push({ id: action.id, tool: action.tool, ok: false, message });
1380
+ safeWrite(`data: ${JSON.stringify({ type: "plan_action_result", id: action.id, tool: action.tool, ok: false, message })}\n\n`);
1381
+ }
1382
+ }
1383
+ // Plan fully executed — clean up
1384
+ const allDone = plan.actions.every((a) => a.status === "executed" || a.status === "rejected" || a.status === "failed");
1385
+ if (allDone) {
1386
+ sessionPlans.delete(sessionId);
1387
+ }
1388
+ const succeeded = executionResults.filter((r) => r.ok).length;
1389
+ const failed = executionResults.filter((r) => !r.ok).length;
1390
+ safeWrite(`data: ${JSON.stringify({
1391
+ type: "plan_execution_complete",
1392
+ plan_id: plan.id,
1393
+ total: executionResults.length,
1394
+ succeeded,
1395
+ failed,
1396
+ results: executionResults,
1397
+ })}\n\n`);
1398
+ reply.raw.end();
1399
+ });
1400
+ // ══ POST /api/sessions/:sessionId/plan/reject — Reject/discard plan ═
1401
+ app.post("/api/sessions/:sessionId/plan/reject", async (request, reply) => {
1402
+ const authUser = await requireAuth(request, reply, config.jwtSecret);
1403
+ if (!authUser)
1404
+ return;
1405
+ const { sessionId } = request.params;
1406
+ if (sessionService) {
1407
+ const session = sessionService.getById(sessionId, authUser.id);
1408
+ if (!session) {
1409
+ return reply.status(404).send({ error: "NOT_FOUND", details: "Session not found" });
1410
+ }
1411
+ }
1412
+ const plan = sessionPlans.get(sessionId);
1413
+ if (!plan) {
1414
+ return reply.status(404).send({ error: "NOT_FOUND", details: "No pending plan for this session" });
1415
+ }
1416
+ for (const action of plan.actions) {
1417
+ if (action.status === "pending")
1418
+ action.status = "rejected";
1419
+ }
1420
+ sessionPlans.delete(sessionId);
1421
+ // Add a system message so the agent knows the plan was rejected
1422
+ const history = sessionHistory.get(sessionId);
1423
+ if (history) {
1424
+ history.push({
1425
+ role: "system",
1426
+ content: "[PLAN REJECTED] The user rejected the proposed plan. Ask if they want to revise it or try a different approach.",
1427
+ });
1428
+ }
1429
+ return { ok: true, plan_id: plan.id, message: "Plan rejected and discarded." };
1430
+ });
1431
+ }
1432
+ // ══════════════════════════════════════════════════════════════════════
1433
+ // Agent loop extracted to ../tools/agent-loop.ts
1434
+ // (runAgentLoop, parseOpenAIStream, serializeMessages, etc.)
1435
+ // ══════════════════════════════════════════════════════════════════════
1436
+ // ══════════════════════════════════════════════════════════════════════
1437
+ // Ollama streaming (text-only — no tool support)
1438
+ // ══════════════════════════════════════════════════════════════════════
1439
+ async function runOllamaStream(config, history, sessionId, streamAbort, safeWrite, app) {
1440
+ let fullContent = "";
1441
+ const ollamaResponse = await fetch(`${config.ollamaUrl}/api/chat`, {
1442
+ method: "POST",
1443
+ headers: { "Content-Type": "application/json" },
1444
+ body: JSON.stringify({
1445
+ model: config.ollamaModel,
1446
+ messages: history
1447
+ .filter(m => m.role !== "tool")
1448
+ .map(m => ({ role: m.role, content: m.content })),
1449
+ stream: true,
1450
+ }),
1451
+ signal: streamAbort.signal,
1452
+ });
1453
+ if (!ollamaResponse.ok) {
1454
+ const errText = await ollamaResponse.text();
1455
+ app.log.error(`Ollama error ${ollamaResponse.status}: ${errText}`);
1456
+ safeWrite(`data: ${JSON.stringify({ type: "error", message: `Ollama error: ${ollamaResponse.status}` })}\n\n`);
1457
+ return fullContent;
1458
+ }
1459
+ const reader = ollamaResponse.body?.getReader();
1460
+ if (!reader) {
1461
+ safeWrite(`data: ${JSON.stringify({ type: "error", message: "No response body from Ollama" })}\n\n`);
1462
+ return fullContent;
1463
+ }
1464
+ const decoder = new TextDecoder();
1465
+ let buffer = "";
1466
+ let streamingAssistantIndex = null;
1467
+ while (true) {
1468
+ const { done, value } = await reader.read();
1469
+ if (done)
1470
+ break;
1471
+ buffer += decoder.decode(value, { stream: true });
1472
+ const lines = buffer.split("\n");
1473
+ buffer = lines.pop() || "";
1474
+ for (const line of lines) {
1475
+ if (!line.trim())
1476
+ continue;
1477
+ try {
1478
+ const chunk = JSON.parse(line);
1479
+ if (chunk.message?.content) {
1480
+ const token = chunk.message.content;
1481
+ fullContent += token;
1482
+ // Keep in-memory history updated during streaming so endpoints can
1483
+ // return a partial assistant response mid-stream.
1484
+ if (streamingAssistantIndex === null) {
1485
+ history.push({ role: "assistant", content: "" });
1486
+ streamingAssistantIndex = history.length - 1;
1487
+ }
1488
+ history[streamingAssistantIndex].content += token;
1489
+ emitToSubscribers(sessionId, { type: "token", content: token });
1490
+ safeWrite(`data: ${JSON.stringify({ type: "token", content: token })}\n\n`);
1491
+ }
1492
+ }
1493
+ catch {
1494
+ // partial JSON
1495
+ }
1496
+ }
1497
+ }
1498
+ if (fullContent) {
1499
+ persistMessageGlobal(sessionId, "assistant", fullContent);
1500
+ }
1501
+ return fullContent;
1502
+ }
1503
+ //# sourceMappingURL=chat.js.map