@otto-assistant/bridge 0.4.92

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 (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,861 @@
1
+ // OpenCode single-server process manager.
2
+ //
3
+ // Architecture: ONE opencode serve process shared by all project directories.
4
+ // Each SDK client uses the x-opencode-directory header to scope requests to a
5
+ // specific project. The server lazily creates and caches an Instance per unique
6
+ // directory path internally.
7
+ //
8
+ // Per-directory permissions (external_directory rules for worktrees, tmpdir,
9
+ // etc.) are passed via session.create({ permission }) at session creation time,
10
+ // NOT via the server config. The server config has permissive defaults
11
+ // (edit: allow, bash: allow, external_directory: ask) and session-level rules
12
+ // override them via opencode's findLast() evaluation (last matching rule wins).
13
+ //
14
+ // Uses errore for type-safe error handling.
15
+ import { spawn, execFileSync } from 'node:child_process';
16
+ import fs from 'node:fs';
17
+ import net from 'node:net';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ import { createOpencodeClient, } from '@opencode-ai/sdk/v2';
23
+ import { getDataDir, getLockPort, } from './config.js';
24
+ import { store } from './store.js';
25
+ import { getHranaUrl } from './hrana-server.js';
26
+ import * as errore from 'errore';
27
+ import { createLogger, LogPrefix } from './logger.js';
28
+ import { notifyError } from './sentry.js';
29
+ import { DirectoryNotAccessibleError, ServerStartError, ServerNotReadyError, FetchError, } from './errors.js';
30
+ import { ensureKimakiCommandShim, getPathEnvKey, getSpawnCommandAndArgs, prependPathEntry, selectResolvedCommand, } from './opencode-command.js';
31
+ const opencodeLogger = createLogger(LogPrefix.OPENCODE);
32
+ // Tracks directories that have been initialized, to avoid repeated log spam
33
+ // from the external sync polling loop.
34
+ const initializedDirectories = new Set();
35
+ const STARTUP_STDERR_TAIL_LIMIT = 30;
36
+ const STARTUP_STDERR_LINE_MAX_LENGTH = 120;
37
+ const STARTUP_ERROR_REASON_MAX_LENGTH = 1500;
38
+ const ANSI_ESCAPE_REGEX = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
39
+ function truncateWithEllipsis({ value, maxLength, }) {
40
+ if (maxLength <= 3) {
41
+ return value.slice(0, maxLength);
42
+ }
43
+ if (value.length <= maxLength) {
44
+ return value;
45
+ }
46
+ return `${value.slice(0, maxLength - 3)}...`;
47
+ }
48
+ function stripAnsiCodes(value) {
49
+ return value.replaceAll(ANSI_ESCAPE_REGEX, '');
50
+ }
51
+ function splitOutputChunkLines(chunk) {
52
+ return chunk
53
+ .split(/\r?\n/g)
54
+ .map((line) => stripAnsiCodes(line).trim())
55
+ .filter((line) => line.length > 0);
56
+ }
57
+ function sanitizeForCodeFence(line) {
58
+ return line.replaceAll('```', '`\u200b``');
59
+ }
60
+ function pushStartupStderrTail({ stderrTail, chunk, }) {
61
+ const incomingLines = splitOutputChunkLines(chunk);
62
+ const truncatedLines = incomingLines.map((line) => {
63
+ const sanitizedLine = sanitizeForCodeFence(line);
64
+ return truncateWithEllipsis({
65
+ value: sanitizedLine,
66
+ maxLength: STARTUP_STDERR_LINE_MAX_LENGTH,
67
+ });
68
+ });
69
+ stderrTail.push(...truncatedLines);
70
+ if (stderrTail.length > STARTUP_STDERR_TAIL_LIMIT) {
71
+ stderrTail.splice(0, stderrTail.length - STARTUP_STDERR_TAIL_LIMIT);
72
+ }
73
+ }
74
+ function buildStartupTimeoutReason({ maxAttempts, stderrTail, }) {
75
+ const timeoutSeconds = Math.round((maxAttempts * 100) / 1000);
76
+ const baseReason = `Server did not start after ${timeoutSeconds} seconds`;
77
+ if (stderrTail.length === 0) {
78
+ return baseReason;
79
+ }
80
+ const formatReason = ({ lines, omitted, }) => {
81
+ const omittedLine = omitted > 0
82
+ ? `[... ${omitted} older stderr lines omitted to fit Discord ...]\n`
83
+ : '';
84
+ const stderrCodeBlock = `${omittedLine}${lines.join('\n')}`;
85
+ return `${baseReason}\nLast opencode stderr lines:\n\`\`\`text\n${stderrCodeBlock}\n\`\`\``;
86
+ };
87
+ let lines = [...stderrTail];
88
+ let omitted = 0;
89
+ let formattedReason = formatReason({ lines, omitted });
90
+ while (formattedReason.length > STARTUP_ERROR_REASON_MAX_LENGTH &&
91
+ lines.length > 0) {
92
+ lines = lines.slice(1);
93
+ omitted += 1;
94
+ formattedReason = formatReason({ lines, omitted });
95
+ }
96
+ return truncateWithEllipsis({
97
+ value: formattedReason,
98
+ maxLength: STARTUP_ERROR_REASON_MAX_LENGTH,
99
+ });
100
+ }
101
+ let singleServer = null;
102
+ let serverRetryCount = 0;
103
+ const serverLifecycleListeners = new Set();
104
+ let processCleanupHandlersRegistered = false;
105
+ let startingServerProcess = null;
106
+ // Cached SDK clients per directory. Each client has a fixed
107
+ // x-opencode-directory header pointing to its project directory.
108
+ const clientCache = new Map();
109
+ function notifyServerLifecycle(event) {
110
+ for (const listener of serverLifecycleListeners) {
111
+ listener(event);
112
+ }
113
+ }
114
+ export function subscribeOpencodeServerLifecycle(listener) {
115
+ serverLifecycleListeners.add(listener);
116
+ return () => {
117
+ serverLifecycleListeners.delete(listener);
118
+ };
119
+ }
120
+ function killSingleServerProcessNow({ reason, }) {
121
+ if (!singleServer) {
122
+ return;
123
+ }
124
+ const serverProcess = singleServer.process;
125
+ const pid = serverProcess.pid;
126
+ if (!pid || serverProcess.killed) {
127
+ return;
128
+ }
129
+ const killResult = errore.try({
130
+ try: () => {
131
+ serverProcess.kill('SIGTERM');
132
+ },
133
+ catch: (error) => {
134
+ return new Error('Failed to send SIGTERM to opencode server', {
135
+ cause: error,
136
+ });
137
+ },
138
+ });
139
+ if (killResult instanceof Error) {
140
+ opencodeLogger.warn(`[cleanup:${reason}] ${killResult.message} (pid: ${pid}, port: ${singleServer.port})`);
141
+ return;
142
+ }
143
+ opencodeLogger.log(`[cleanup:${reason}] Sent SIGTERM to opencode server (pid: ${pid}, port: ${singleServer.port})`);
144
+ }
145
+ function killStartingServerProcessNow({ reason, }) {
146
+ const serverProcess = startingServerProcess;
147
+ if (!serverProcess) {
148
+ return;
149
+ }
150
+ const pid = serverProcess.pid;
151
+ if (!pid || serverProcess.killed) {
152
+ return;
153
+ }
154
+ const killResult = errore.try({
155
+ try: () => {
156
+ serverProcess.kill('SIGTERM');
157
+ },
158
+ catch: (error) => {
159
+ return new Error('Failed to send SIGTERM to starting opencode server', {
160
+ cause: error,
161
+ });
162
+ },
163
+ });
164
+ if (killResult instanceof Error) {
165
+ opencodeLogger.warn(`[cleanup:${reason}] ${killResult.message} (pid: ${pid})`);
166
+ return;
167
+ }
168
+ opencodeLogger.log(`[cleanup:${reason}] Sent SIGTERM to starting opencode server (pid: ${pid})`);
169
+ }
170
+ function ensureProcessCleanupHandlersRegistered() {
171
+ if (processCleanupHandlersRegistered) {
172
+ return;
173
+ }
174
+ processCleanupHandlersRegistered = true;
175
+ opencodeLogger.log('Registering process cleanup handlers for opencode server');
176
+ process.on('exit', () => {
177
+ killSingleServerProcessNow({ reason: 'process-exit' });
178
+ killStartingServerProcessNow({ reason: 'process-exit' });
179
+ });
180
+ // Fallback for short-lived CLI subcommands that call process.exit without
181
+ // running discord-bot.ts shutdown handlers.
182
+ process.on('SIGINT', () => {
183
+ killSingleServerProcessNow({ reason: 'sigint' });
184
+ killStartingServerProcessNow({ reason: 'sigint' });
185
+ });
186
+ process.on('SIGTERM', () => {
187
+ killSingleServerProcessNow({ reason: 'sigterm' });
188
+ killStartingServerProcessNow({ reason: 'sigterm' });
189
+ });
190
+ }
191
+ // ── Resolve opencode binary ──────────────────────────────────────
192
+ // Resolve the full path to the opencode binary so we can spawn without
193
+ // shell: true. Using shell: true creates an intermediate sh process — when
194
+ // cleanup sends SIGTERM it only kills the shell, leaving the actual opencode
195
+ // process orphaned (reparented to PID 1). Resolving the path upfront lets
196
+ // us spawn the binary directly and SIGTERM reaches the right process.
197
+ let resolvedOpencodeCommand = null;
198
+ export function resolveOpencodeCommand() {
199
+ if (resolvedOpencodeCommand) {
200
+ return resolvedOpencodeCommand;
201
+ }
202
+ const envPath = process.env.OPENCODE_PATH;
203
+ if (envPath) {
204
+ const resolvedFromEnv = selectResolvedCommand({
205
+ output: envPath,
206
+ isWindows: process.platform === 'win32',
207
+ });
208
+ if (resolvedFromEnv) {
209
+ resolvedOpencodeCommand = resolvedFromEnv;
210
+ return resolvedFromEnv;
211
+ }
212
+ }
213
+ const isWindows = process.platform === 'win32';
214
+ const whichCmd = isWindows ? 'where' : 'which';
215
+ const result = errore.try({
216
+ try: () => {
217
+ const commandOutput = execFileSync(whichCmd, ['opencode'], {
218
+ encoding: 'utf8',
219
+ timeout: 5000,
220
+ });
221
+ const resolved = selectResolvedCommand({
222
+ output: commandOutput,
223
+ isWindows,
224
+ });
225
+ if (resolved) {
226
+ return resolved;
227
+ }
228
+ throw new Error('opencode not found in PATH');
229
+ },
230
+ catch: () => new Error('opencode not found in PATH'),
231
+ });
232
+ if (result instanceof Error) {
233
+ // Fall back to bare command name — spawn will fail with a clear error
234
+ // if it can't find the binary.
235
+ opencodeLogger.warn('Could not resolve opencode path via which, falling back to "opencode"');
236
+ return 'opencode';
237
+ }
238
+ resolvedOpencodeCommand = result;
239
+ opencodeLogger.log(`Resolved opencode binary: ${result}`);
240
+ return result;
241
+ }
242
+ async function getOpenPort() {
243
+ return new Promise((resolve, reject) => {
244
+ const server = net.createServer();
245
+ server.listen(0, () => {
246
+ const address = server.address();
247
+ if (address && typeof address === 'object') {
248
+ const port = address.port;
249
+ server.close(() => {
250
+ resolve(port);
251
+ });
252
+ }
253
+ else {
254
+ reject(new Error('Failed to get port'));
255
+ }
256
+ });
257
+ server.on('error', reject);
258
+ });
259
+ }
260
+ async function waitForServer({ port, maxAttempts = 300, startupStderrTail, }) {
261
+ const endpoint = `http://127.0.0.1:${port}/api/health`;
262
+ for (let i = 0; i < maxAttempts; i++) {
263
+ const response = await errore.tryAsync({
264
+ try: () => fetch(endpoint),
265
+ catch: (e) => new FetchError({ url: endpoint, cause: e }),
266
+ });
267
+ if (response instanceof Error) {
268
+ // Connection refused or other transient errors - continue polling.
269
+ // Use 100ms interval instead of 1s so we detect readiness faster.
270
+ // Critical for scale-to-zero cold starts where every ms matters.
271
+ await new Promise((resolve) => setTimeout(resolve, 100));
272
+ continue;
273
+ }
274
+ if (response.status < 500) {
275
+ return true;
276
+ }
277
+ const body = await response.text();
278
+ // Fatal errors that won't resolve with retrying
279
+ if (body.includes('BunInstallFailedError')) {
280
+ return new ServerStartError({ port, reason: body.slice(0, 200) });
281
+ }
282
+ await new Promise((resolve) => setTimeout(resolve, 100));
283
+ }
284
+ return new ServerStartError({
285
+ port,
286
+ reason: buildStartupTimeoutReason({
287
+ maxAttempts,
288
+ stderrTail: startupStderrTail,
289
+ }),
290
+ });
291
+ }
292
+ // ── Single server lifecycle ──────────────────────────────────────
293
+ // The server is started lazily on first initializeOpencodeForDirectory() call.
294
+ // It uses permissive defaults (edit: allow, bash: allow, external_directory: ask).
295
+ // Per-directory permissions are applied at session creation time instead.
296
+ // In-flight promise to prevent concurrent startups from racing
297
+ let startingServer = null;
298
+ async function ensureSingleServer() {
299
+ if (singleServer && !singleServer.process.killed) {
300
+ return singleServer;
301
+ }
302
+ // Deduplicate concurrent startup attempts
303
+ if (startingServer) {
304
+ return startingServer;
305
+ }
306
+ startingServer = startSingleServer();
307
+ try {
308
+ return await startingServer;
309
+ }
310
+ finally {
311
+ startingServer = null;
312
+ }
313
+ }
314
+ async function startSingleServer() {
315
+ ensureProcessCleanupHandlersRegistered();
316
+ const port = await getOpenPort();
317
+ const serveArgs = [
318
+ 'serve',
319
+ '--port',
320
+ port.toString(),
321
+ '--print-logs',
322
+ '--log-level',
323
+ 'WARN',
324
+ ];
325
+ const { command: spawnCommand, args: spawnArgs, windowsVerbatimArguments, } = getSpawnCommandAndArgs({
326
+ resolvedCommand: resolveOpencodeCommand(),
327
+ baseArgs: serveArgs,
328
+ });
329
+ // Server config uses permissive defaults. Per-directory external_directory
330
+ // permissions are set at session creation time via session.create({ permission }).
331
+ // Common directories (tmpdir, ~/.config/opencode, ~/.kimaki) are pre-allowed
332
+ // at the server level so they never trigger permission prompts regardless of
333
+ // whether session-level rules compose correctly.
334
+ const tmpdir = os.tmpdir().replaceAll('\\', '/');
335
+ const opencodeConfigDir = path
336
+ .join(os.homedir(), '.config', 'opencode')
337
+ .replaceAll('\\', '/');
338
+ const kimakiDataDir = path
339
+ .join(os.homedir(), '.kimaki')
340
+ .replaceAll('\\', '/');
341
+ // No catch-all '*': 'ask' here — the user's opencode.json default is respected.
342
+ // Only allowlist specific known-safe directories at the server level.
343
+ const externalDirectoryPermissions = {
344
+ '/tmp': 'allow',
345
+ '/tmp/*': 'allow',
346
+ '/private/tmp': 'allow',
347
+ '/private/tmp/*': 'allow',
348
+ [tmpdir]: 'allow',
349
+ [`${tmpdir}/*`]: 'allow',
350
+ [opencodeConfigDir]: 'allow',
351
+ [`${opencodeConfigDir}/*`]: 'allow',
352
+ [kimakiDataDir]: 'allow',
353
+ [`${kimakiDataDir}/*`]: 'allow',
354
+ };
355
+ const kimakiShimDirectory = ensureKimakiCommandShim({
356
+ dataDir: getDataDir(),
357
+ execPath: process.execPath,
358
+ execArgv: process.execArgv,
359
+ entryScript: process.argv[1] || fileURLToPath(new URL('../bin.js', import.meta.url)),
360
+ });
361
+ const pathEnvKey = getPathEnvKey(process.env);
362
+ const pathEnv = kimakiShimDirectory instanceof Error
363
+ ? process.env[pathEnvKey]
364
+ : prependPathEntry({
365
+ entry: kimakiShimDirectory,
366
+ existingPath: process.env[pathEnvKey],
367
+ });
368
+ if (kimakiShimDirectory instanceof Error) {
369
+ opencodeLogger.warn(kimakiShimDirectory.message);
370
+ }
371
+ const gatewayToken = store.getState().gatewayToken;
372
+ const vitestOpencodeEnv = (() => {
373
+ if (process.env.KIMAKI_VITEST !== '1') {
374
+ return {};
375
+ }
376
+ const root = path.join(getDataDir(), 'opencode-vitest-home');
377
+ return {
378
+ OPENCODE_TEST_HOME: root,
379
+ OPENCODE_CONFIG_DIR: path.join(root, '.opencode-kimaki'),
380
+ XDG_CONFIG_HOME: path.join(root, '.config'),
381
+ XDG_DATA_HOME: path.join(root, '.local', 'share'),
382
+ XDG_CACHE_HOME: path.join(root, '.cache'),
383
+ XDG_STATE_HOME: path.join(root, '.local', 'state'),
384
+ };
385
+ })();
386
+ // Write config to a file instead of passing via OPENCODE_CONFIG_CONTENT env var.
387
+ // OPENCODE_CONFIG (file path) is loaded before project config in opencode's
388
+ // priority chain, so project-level opencode.json can override kimaki defaults.
389
+ // OPENCODE_CONFIG_CONTENT was loaded last and overrode user project configs,
390
+ // causing issue #90 (project permissions not being respected).
391
+ const opencodeConfig = {
392
+ $schema: 'https://opencode.ai/config.json',
393
+ lsp: false,
394
+ formatter: false,
395
+ plugin: [new URL('../src/kimaki-opencode-plugin.ts', import.meta.url).href],
396
+ permission: {
397
+ edit: 'allow',
398
+ bash: 'allow',
399
+ external_directory: externalDirectoryPermissions,
400
+ webfetch: 'allow',
401
+ },
402
+ agent: {
403
+ explore: {
404
+ permission: {
405
+ '*': 'deny',
406
+ grep: 'allow',
407
+ glob: 'allow',
408
+ list: 'allow',
409
+ read: {
410
+ '*': 'allow',
411
+ '*.env': 'deny',
412
+ '*.env.*': 'deny',
413
+ '*.env.example': 'allow',
414
+ },
415
+ webfetch: 'allow',
416
+ websearch: 'allow',
417
+ codesearch: 'allow',
418
+ external_directory: externalDirectoryPermissions,
419
+ },
420
+ },
421
+ },
422
+ skills: {
423
+ paths: [path.resolve(__dirname, '..', 'skills')],
424
+ },
425
+ };
426
+ const opencodeConfigPath = path.join(getDataDir(), 'opencode-config.json');
427
+ const opencodeConfigJson = JSON.stringify(opencodeConfig, null, 2);
428
+ const existingContent = (() => {
429
+ try {
430
+ return fs.readFileSync(opencodeConfigPath, 'utf-8');
431
+ }
432
+ catch {
433
+ return '';
434
+ }
435
+ })();
436
+ if (existingContent !== opencodeConfigJson) {
437
+ fs.writeFileSync(opencodeConfigPath, opencodeConfigJson);
438
+ }
439
+ const serverProcess = spawn(spawnCommand, spawnArgs, {
440
+ stdio: 'pipe',
441
+ detached: false,
442
+ windowsVerbatimArguments,
443
+ // No project-specific cwd — the server handles all directories via
444
+ // x-opencode-directory header. Use home dir as a neutral working dir.
445
+ cwd: os.homedir(),
446
+ env: {
447
+ ...process.env,
448
+ OPENCODE_CONFIG: opencodeConfigPath,
449
+ OPENCODE_PORT: port.toString(),
450
+ KIMAKI: '1',
451
+ KIMAKI_DATA_DIR: getDataDir(),
452
+ KIMAKI_LOCK_PORT: getLockPort().toString(),
453
+ ...(gatewayToken && { KIMAKI_DB_AUTH_TOKEN: gatewayToken }),
454
+ // Guard: prevents agents from running `kimaki` root command inside
455
+ // an OpenCode session, which would steal the lock port and break the bot.
456
+ KIMAKI_OPENCODE_PROCESS: '1',
457
+ ...(getHranaUrl() && { KIMAKI_DB_URL: getHranaUrl() }),
458
+ ...(process.env.KIMAKI_SENTRY_DSN && {
459
+ KIMAKI_SENTRY_DSN: process.env.KIMAKI_SENTRY_DSN,
460
+ }),
461
+ ...vitestOpencodeEnv,
462
+ ...(pathEnv && { [pathEnvKey]: pathEnv }),
463
+ },
464
+ });
465
+ startingServerProcess = serverProcess;
466
+ // Buffer logs until we know if server started successfully.
467
+ const logBuffer = [];
468
+ const startupStderrTail = [];
469
+ let serverReady = false;
470
+ logBuffer.push(`Spawned opencode serve --port ${port} (pid: ${serverProcess.pid})`);
471
+ serverProcess.stdout?.on('data', (data) => {
472
+ try {
473
+ const chunk = data.toString();
474
+ const lines = splitOutputChunkLines(chunk);
475
+ if (!serverReady) {
476
+ logBuffer.push(...lines.map((line) => `[stdout] ${line}`));
477
+ return;
478
+ }
479
+ for (const line of lines) {
480
+ opencodeLogger.log(line);
481
+ }
482
+ }
483
+ catch (error) {
484
+ logBuffer.push(`Failed to process stdout startup logs: ${error}`);
485
+ }
486
+ });
487
+ serverProcess.stderr?.on('data', (data) => {
488
+ try {
489
+ const chunk = data.toString();
490
+ const lines = splitOutputChunkLines(chunk);
491
+ if (!serverReady) {
492
+ logBuffer.push(...lines.map((line) => `[stderr] ${line}`));
493
+ pushStartupStderrTail({ stderrTail: startupStderrTail, chunk });
494
+ return;
495
+ }
496
+ for (const line of lines) {
497
+ opencodeLogger.error(line);
498
+ }
499
+ }
500
+ catch (error) {
501
+ logBuffer.push(`Failed to process stderr startup logs: ${error}`);
502
+ }
503
+ });
504
+ serverProcess.on('error', (error) => {
505
+ logBuffer.push(`Failed to start server on port ${port}: ${error}`);
506
+ });
507
+ serverProcess.on('exit', (code, signal) => {
508
+ if (startingServerProcess === serverProcess) {
509
+ startingServerProcess = null;
510
+ }
511
+ opencodeLogger.log(`Opencode server exited with code: ${code}, signal: ${signal}`);
512
+ singleServer = null;
513
+ clientCache.clear();
514
+ notifyServerLifecycle({ type: 'stopped' });
515
+ // Intentional kills should not trigger auto-restart:
516
+ // - SIGTERM from our cleanup/restart code
517
+ // - SIGINT propagated from Ctrl+C (parent process group signal)
518
+ // - any exit during bot shutdown (shuttingDown flag)
519
+ // Only unexpected crashes (non-zero exit without signal) get retried.
520
+ if (signal === 'SIGTERM' || signal === 'SIGINT' || global.shuttingDown) {
521
+ serverRetryCount = 0;
522
+ return;
523
+ }
524
+ if (code !== 0) {
525
+ if (serverRetryCount < 5) {
526
+ serverRetryCount += 1;
527
+ opencodeLogger.log(`Restarting server (attempt ${serverRetryCount}/5)`);
528
+ ensureSingleServer().then((result) => {
529
+ if (result instanceof Error) {
530
+ opencodeLogger.error(`Failed to restart opencode server:`, result);
531
+ void notifyError(result, `OpenCode server restart failed`);
532
+ }
533
+ });
534
+ }
535
+ else {
536
+ const crashError = new Error(`Server crashed too many times (5), not restarting`);
537
+ opencodeLogger.error(crashError.message);
538
+ void notifyError(crashError, `OpenCode server crash loop exhausted`);
539
+ }
540
+ }
541
+ else {
542
+ serverRetryCount = 0;
543
+ }
544
+ });
545
+ const waitResult = await waitForServer({
546
+ port,
547
+ startupStderrTail,
548
+ });
549
+ if (waitResult instanceof Error) {
550
+ killStartingServerProcessNow({ reason: 'startup-failed' });
551
+ if (startingServerProcess === serverProcess) {
552
+ startingServerProcess = null;
553
+ }
554
+ // Dump buffered logs on failure
555
+ opencodeLogger.error(`Server failed to start:`);
556
+ for (const line of logBuffer) {
557
+ opencodeLogger.error(` ${line}`);
558
+ }
559
+ return waitResult;
560
+ }
561
+ serverReady = true;
562
+ opencodeLogger.log(`Server ready on port ${port}`);
563
+ // Always dump startup logs so plugin loading errors and other startup output
564
+ // are visible in kimaki.log.
565
+ for (const line of logBuffer) {
566
+ opencodeLogger.log(line);
567
+ }
568
+ const server = {
569
+ process: serverProcess,
570
+ port,
571
+ baseUrl: `http://127.0.0.1:${port}`,
572
+ };
573
+ if (startingServerProcess === serverProcess) {
574
+ startingServerProcess = null;
575
+ }
576
+ singleServer = server;
577
+ notifyServerLifecycle({ type: 'started', port });
578
+ return server;
579
+ }
580
+ // ── Client cache ─────────────────────────────────────────────────
581
+ // One SDK client per directory, each with a fixed x-opencode-directory header.
582
+ function getOrCreateClient({ baseUrl, directory, }) {
583
+ const cached = clientCache.get(directory);
584
+ if (cached) {
585
+ return cached;
586
+ }
587
+ const fetchWithTimeout = (request) => fetch(request, {
588
+ // @ts-ignore
589
+ timeout: false,
590
+ });
591
+ const client = createOpencodeClient({
592
+ baseUrl,
593
+ directory,
594
+ fetch: fetchWithTimeout,
595
+ });
596
+ clientCache.set(directory, client);
597
+ return client;
598
+ }
599
+ // ── Public API ───────────────────────────────────────────────────
600
+ // Same signatures as before so callers don't need to change.
601
+ /**
602
+ * Initialize OpenCode server for a directory.
603
+ * Starts the single shared server if not running, then returns a client
604
+ * factory scoped to the given directory via x-opencode-directory header.
605
+ *
606
+ * @param directory - The project directory to scope requests to
607
+ * @param options.originalRepoDirectory - For worktrees: the original repo directory
608
+ * (no longer used for server-level permissions — use buildSessionPermissions
609
+ * at session.create() time instead)
610
+ */
611
+ export async function initializeOpencodeForDirectory(directory, _options) {
612
+ // Verify directory exists and is accessible
613
+ const accessCheck = errore.tryFn({
614
+ try: () => {
615
+ fs.accessSync(directory, fs.constants.R_OK | fs.constants.X_OK);
616
+ },
617
+ catch: () => new DirectoryNotAccessibleError({ directory }),
618
+ });
619
+ if (accessCheck instanceof Error) {
620
+ return accessCheck;
621
+ }
622
+ const server = await ensureSingleServer();
623
+ if (server instanceof Error) {
624
+ return server;
625
+ }
626
+ if (!initializedDirectories.has(directory)) {
627
+ initializedDirectories.add(directory);
628
+ }
629
+ return () => {
630
+ if (!singleServer) {
631
+ throw new ServerNotReadyError({ directory });
632
+ }
633
+ return getOrCreateClient({
634
+ baseUrl: singleServer.baseUrl,
635
+ directory,
636
+ });
637
+ };
638
+ }
639
+ /**
640
+ * Build per-session permission rules for external_directory access.
641
+ * These rules are passed to session.create({ permission }) and override
642
+ * the server-level defaults via opencode's findLast() evaluation.
643
+ *
644
+ * This replaces the old per-server OPENCODE_CONFIG_CONTENT external_directory
645
+ * permissions — now each session carries its own directory-scoped rules.
646
+ */
647
+ export function buildSessionPermissions({ directory, originalRepoDirectory, }) {
648
+ // Normalize path separators for cross-platform compatibility (Windows uses backslashes)
649
+ const tmpdir = os.tmpdir().replaceAll('\\', '/');
650
+ const normalizedDirectory = directory.replaceAll('\\', '/');
651
+ const originalRepo = originalRepoDirectory?.replaceAll('\\', '/');
652
+ const rules = [
653
+ // Allow tmpdir access
654
+ { permission: 'external_directory', pattern: '/tmp', action: 'allow' },
655
+ { permission: 'external_directory', pattern: '/tmp/*', action: 'allow' },
656
+ { permission: 'external_directory', pattern: '/private/tmp', action: 'allow' },
657
+ { permission: 'external_directory', pattern: '/private/tmp/*', action: 'allow' },
658
+ { permission: 'external_directory', pattern: tmpdir, action: 'allow' },
659
+ { permission: 'external_directory', pattern: `${tmpdir}/*`, action: 'allow' },
660
+ // Allow the project directory itself
661
+ { permission: 'external_directory', pattern: normalizedDirectory, action: 'allow' },
662
+ { permission: 'external_directory', pattern: `${normalizedDirectory}/*`, action: 'allow' },
663
+ ];
664
+ // Allow ~/.config/opencode so the agent doesn't get permission prompts when
665
+ // it tries to read the global AGENTS.md or opencode config (the path is
666
+ // visible in the system prompt, so models sometimes try to read it).
667
+ const opencodeConfigDir = path
668
+ .join(os.homedir(), '.config', 'opencode')
669
+ .replaceAll('\\', '/');
670
+ rules.push({ permission: 'external_directory', pattern: opencodeConfigDir, action: 'allow' }, { permission: 'external_directory', pattern: `${opencodeConfigDir}/*`, action: 'allow' });
671
+ // Allow ~/.kimaki so the agent can access kimaki data dir (logs, db, etc.)
672
+ // without permission prompts.
673
+ const kimakiDataDir = path
674
+ .join(os.homedir(), '.kimaki')
675
+ .replaceAll('\\', '/');
676
+ rules.push({ permission: 'external_directory', pattern: kimakiDataDir, action: 'allow' }, { permission: 'external_directory', pattern: `${kimakiDataDir}/*`, action: 'allow' });
677
+ // Allow opencode tool output artifacts under XDG data so agents can inspect
678
+ // prior tool outputs without interactive permission prompts.
679
+ const opencodeToolOutputDir = path
680
+ .join(os.homedir(), '.local', 'share', 'opencode', 'tool-output')
681
+ .replaceAll('\\', '/');
682
+ rules.push({
683
+ permission: 'external_directory',
684
+ pattern: opencodeToolOutputDir,
685
+ action: 'allow',
686
+ }, {
687
+ permission: 'external_directory',
688
+ pattern: `${opencodeToolOutputDir}/*`,
689
+ action: 'allow',
690
+ });
691
+ // For worktrees: allow access to the original repository directory
692
+ if (originalRepo) {
693
+ rules.push({ permission: 'external_directory', pattern: originalRepo, action: 'allow' }, { permission: 'external_directory', pattern: `${originalRepo}/*`, action: 'allow' });
694
+ }
695
+ return rules;
696
+ }
697
+ /**
698
+ * Parse raw permission strings into PermissionRuleset entries.
699
+ *
700
+ * Accepted formats:
701
+ * "tool:action" → { permission: tool, pattern: "*", action }
702
+ * "tool:pattern:action" → { permission: tool, pattern, action }
703
+ *
704
+ * The action must be one of "allow", "deny", "ask" (case-insensitive).
705
+ * Parts are trimmed to tolerate whitespace from YAML deserialization.
706
+ * Invalid entries are silently skipped (bad user input shouldn't crash the bot).
707
+ * If `raw` is not an array, returns empty (defensive against malformed YAML markers).
708
+ */
709
+ export function parsePermissionRules(raw) {
710
+ if (!Array.isArray(raw)) {
711
+ return [];
712
+ }
713
+ const validActions = new Set(['allow', 'deny', 'ask']);
714
+ return raw.flatMap((entry) => {
715
+ if (typeof entry !== 'string') {
716
+ return [];
717
+ }
718
+ const parts = entry.split(':').map((s) => {
719
+ return s.trim();
720
+ });
721
+ if (parts.length === 2) {
722
+ const [permission, rawAction] = parts;
723
+ const action = rawAction.toLowerCase();
724
+ if (!permission || !validActions.has(action)) {
725
+ return [];
726
+ }
727
+ return [{ permission, pattern: '*', action: action }];
728
+ }
729
+ if (parts.length >= 3) {
730
+ // Last segment is the action, first segment is the permission,
731
+ // everything in between is the pattern (may contain colons in theory,
732
+ // but unlikely for tool patterns).
733
+ const permission = parts[0];
734
+ const rawAction = parts[parts.length - 1];
735
+ const action = rawAction.toLowerCase();
736
+ const pattern = parts.slice(1, -1).join(':');
737
+ if (!permission || !pattern || !validActions.has(action)) {
738
+ return [];
739
+ }
740
+ return [{ permission, pattern, action: action }];
741
+ }
742
+ return [];
743
+ });
744
+ }
745
+ // ── Injection guard per-session config ───────────────────────────
746
+ // Per-session injection guard patterns are written as JSON files to
747
+ // <dataDir>/injection-guard/<sessionId>.json. The injection guard plugin
748
+ // (running inside the opencode server process) reads KIMAKI_DATA_DIR env
749
+ // var to find these files in tool.execute.after.
750
+ // This avoids needing env vars (which are per-process, not per-session).
751
+ function getInjectionGuardDir() {
752
+ return path.join(getDataDir(), 'injection-guard');
753
+ }
754
+ /**
755
+ * Write per-session injection guard config so the plugin picks it up.
756
+ * Only call this if injectionGuardPatterns is non-empty.
757
+ */
758
+ export function writeInjectionGuardConfig({ sessionId, scanPatterns, }) {
759
+ if (scanPatterns.length === 0) {
760
+ return;
761
+ }
762
+ try {
763
+ const dir = getInjectionGuardDir();
764
+ fs.mkdirSync(dir, { recursive: true });
765
+ fs.writeFileSync(path.join(dir, `${sessionId}.json`), JSON.stringify({ scanPatterns }));
766
+ }
767
+ catch {
768
+ // Best effort -- don't crash the bot if data dir write fails
769
+ }
770
+ }
771
+ /**
772
+ * Remove per-session injection guard config file.
773
+ */
774
+ export function removeInjectionGuardConfig({ sessionId }) {
775
+ try {
776
+ fs.unlinkSync(path.join(getInjectionGuardDir(), `${sessionId}.json`));
777
+ }
778
+ catch {
779
+ // File may already be gone
780
+ }
781
+ }
782
+ /**
783
+ * Read per-session injection guard config. Used by the kimaki plugin
784
+ * inside the opencode server process.
785
+ */
786
+ export function readInjectionGuardConfig({ sessionId }) {
787
+ try {
788
+ const raw = fs.readFileSync(path.join(getInjectionGuardDir(), `${sessionId}.json`), 'utf-8');
789
+ return JSON.parse(raw);
790
+ }
791
+ catch {
792
+ return null;
793
+ }
794
+ }
795
+ // ── Public helpers ───────────────────────────────────────────────
796
+ // These helpers expose the single shared server and directory-scoped clients.
797
+ export function getOpencodeServerPort(_directory) {
798
+ return singleServer?.port ?? null;
799
+ }
800
+ export function getOpencodeClient(directory) {
801
+ if (!singleServer) {
802
+ return null;
803
+ }
804
+ return getOrCreateClient({
805
+ baseUrl: singleServer.baseUrl,
806
+ directory,
807
+ });
808
+ }
809
+ /**
810
+ * Stop the single opencode server.
811
+ * Used for process teardown, tests, and explicit restarts.
812
+ */
813
+ export async function stopOpencodeServer() {
814
+ if (!singleServer) {
815
+ return false;
816
+ }
817
+ const server = singleServer;
818
+ opencodeLogger.log(`Stopping opencode server (pid: ${server.process.pid}, port: ${server.port})`);
819
+ if (!server.process.killed) {
820
+ const killResult = errore.try({
821
+ try: () => {
822
+ server.process.kill('SIGTERM');
823
+ },
824
+ catch: (error) => {
825
+ return new Error('Failed to send SIGTERM to opencode server', {
826
+ cause: error,
827
+ });
828
+ },
829
+ });
830
+ if (killResult instanceof Error) {
831
+ opencodeLogger.warn(killResult.message);
832
+ }
833
+ }
834
+ killStartingServerProcessNow({ reason: 'stop-opencode-server' });
835
+ startingServerProcess = null;
836
+ singleServer = null;
837
+ clientCache.clear();
838
+ serverRetryCount = 0;
839
+ await new Promise((resolve) => {
840
+ setTimeout(resolve, 1000);
841
+ });
842
+ return true;
843
+ }
844
+ /**
845
+ * Restart the single opencode server.
846
+ * Kills the existing process and starts a new one.
847
+ * All directory clients are invalidated and recreated on next use.
848
+ * Used for resolving opencode state issues, refreshing auth, plugins, etc.
849
+ */
850
+ export async function restartOpencodeServer() {
851
+ if (singleServer) {
852
+ await stopOpencodeServer();
853
+ }
854
+ // Reset retry count for the fresh start
855
+ serverRetryCount = 0;
856
+ const result = await ensureSingleServer();
857
+ if (result instanceof Error) {
858
+ return result;
859
+ }
860
+ return true;
861
+ }