@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,224 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ /* istanbul ignore file */
3
+ // @ts-nocheck
4
+ import { RealtimeClient } from '@openai/realtime-api-beta';
5
+ import { writeFile } from 'fs';
6
+ import { createLogger, LogPrefix } from './logger.js';
7
+ const openaiLogger = createLogger(LogPrefix.OPENAI);
8
+ const audioParts = [];
9
+ function saveBinaryFile(fileName, content) {
10
+ writeFile(fileName, content, 'utf8', (err) => {
11
+ if (err) {
12
+ openaiLogger.error(`Error writing file ${fileName}:`, err);
13
+ return;
14
+ }
15
+ openaiLogger.log(`Appending stream content to file ${fileName}.`);
16
+ });
17
+ }
18
+ function convertToWav(rawData, mimeType) {
19
+ const options = parseMimeType(mimeType);
20
+ const dataLength = rawData.reduce((a, b) => a + b.length, 0);
21
+ const wavHeader = createWavHeader(dataLength, options);
22
+ const buffer = Buffer.concat(rawData);
23
+ return Buffer.concat([wavHeader, buffer]);
24
+ }
25
+ function parseMimeType(mimeType) {
26
+ const [fileType, ...params] = mimeType.split(';').map((s) => s.trim());
27
+ const [_, format] = fileType?.split('/') || [];
28
+ const options = {
29
+ numChannels: 1,
30
+ bitsPerSample: 16,
31
+ };
32
+ if (format && format.startsWith('L')) {
33
+ const bits = parseInt(format.slice(1), 10);
34
+ if (!isNaN(bits)) {
35
+ options.bitsPerSample = bits;
36
+ }
37
+ }
38
+ for (const param of params) {
39
+ const [key, value] = param.split('=').map((s) => s.trim());
40
+ if (key === 'rate') {
41
+ options.sampleRate = parseInt(value || '', 10);
42
+ }
43
+ }
44
+ return options;
45
+ }
46
+ function createWavHeader(dataLength, options) {
47
+ const { numChannels, sampleRate, bitsPerSample } = options;
48
+ // http://soundfile.sapp.org/doc/WaveFormat
49
+ const byteRate = (sampleRate * numChannels * bitsPerSample) / 8;
50
+ const blockAlign = (numChannels * bitsPerSample) / 8;
51
+ const buffer = Buffer.alloc(44);
52
+ buffer.write('RIFF', 0); // ChunkID
53
+ buffer.writeUInt32LE(36 + dataLength, 4); // ChunkSize
54
+ buffer.write('WAVE', 8); // Format
55
+ buffer.write('fmt ', 12); // Subchunk1ID
56
+ buffer.writeUInt32LE(16, 16); // Subchunk1Size (PCM)
57
+ buffer.writeUInt16LE(1, 20); // AudioFormat (1 = PCM)
58
+ buffer.writeUInt16LE(numChannels, 22); // NumChannels
59
+ buffer.writeUInt32LE(sampleRate, 24); // SampleRate
60
+ buffer.writeUInt32LE(byteRate, 28); // ByteRate
61
+ buffer.writeUInt16LE(blockAlign, 32); // BlockAlign
62
+ buffer.writeUInt16LE(bitsPerSample, 34); // BitsPerSample
63
+ buffer.write('data', 36); // Subchunk2ID
64
+ buffer.writeUInt32LE(dataLength, 40); // Subchunk2Size
65
+ return buffer;
66
+ }
67
+ function defaultAudioChunkHandler({ data, mimeType, }) {
68
+ audioParts.push(data);
69
+ const fileName = 'audio.wav';
70
+ const buffer = convertToWav(audioParts, mimeType);
71
+ saveBinaryFile(fileName, buffer);
72
+ }
73
+ export async function startGenAiSession({ onAssistantAudioChunk, onAssistantStartSpeaking, onAssistantStopSpeaking, onAssistantInterruptSpeaking, systemMessage, tools, } = {}) {
74
+ if (!process.env.OPENAI_API_KEY) {
75
+ throw new Error('OPENAI_API_KEY environment variable is required');
76
+ }
77
+ const client = new RealtimeClient({
78
+ apiKey: process.env.OPENAI_API_KEY,
79
+ });
80
+ const audioChunkHandler = onAssistantAudioChunk || defaultAudioChunkHandler;
81
+ let isAssistantSpeaking = false;
82
+ // Configure session with 24kHz sample rate
83
+ client.updateSession({
84
+ instructions: systemMessage || '',
85
+ voice: 'alloy',
86
+ input_audio_format: 'pcm16',
87
+ output_audio_format: 'pcm16',
88
+ input_audio_transcription: { model: 'whisper-1' },
89
+ turn_detection: { type: 'server_vad' },
90
+ modalities: ['text', 'audio'],
91
+ temperature: 0.8,
92
+ });
93
+ // Add tools if provided
94
+ if (tools) {
95
+ for (const [name, tool] of Object.entries(tools)) {
96
+ // Convert AI SDK tool to OpenAI Realtime format
97
+ // The tool.inputSchema is a Zod schema, we need to convert it to JSON Schema
98
+ let parameters = {
99
+ type: 'object',
100
+ properties: {},
101
+ required: [],
102
+ };
103
+ // If the tool has a Zod schema, we can try to extract basic structure
104
+ // For now, we'll use a simple placeholder
105
+ if (tool.description?.includes('session')) {
106
+ parameters = {
107
+ type: 'object',
108
+ properties: {
109
+ sessionId: { type: 'string', description: 'The session ID' },
110
+ message: { type: 'string', description: 'The message text' },
111
+ },
112
+ required: ['sessionId'],
113
+ };
114
+ }
115
+ client.addTool({
116
+ type: 'function',
117
+ name,
118
+ description: tool.description || '',
119
+ parameters,
120
+ }, async (params) => {
121
+ try {
122
+ if (!tool.execute || typeof tool.execute !== 'function') {
123
+ return { error: 'Tool execute function not found' };
124
+ }
125
+ // Call the execute function with params
126
+ // The Tool type from 'ai' expects (input, options) but we need to handle this safely
127
+ const result = await tool.execute(params, {
128
+ abortSignal: new AbortController().signal,
129
+ toolCallId: '',
130
+ messages: [],
131
+ });
132
+ return result;
133
+ }
134
+ catch (error) {
135
+ openaiLogger.error(`Tool ${name} execution error:`, error);
136
+ return { error: String(error) };
137
+ }
138
+ });
139
+ }
140
+ }
141
+ // Set up event handlers
142
+ client.on('conversation.item.created', ({ item }) => {
143
+ if (item.role === 'assistant' &&
144
+ item.type === 'message') {
145
+ // Check if this is the first audio content
146
+ const hasAudio = Array.isArray(item.content) &&
147
+ item.content.some((c) => c.type === 'audio');
148
+ if (hasAudio && !isAssistantSpeaking && onAssistantStartSpeaking) {
149
+ isAssistantSpeaking = true;
150
+ onAssistantStartSpeaking();
151
+ }
152
+ }
153
+ });
154
+ client.on('conversation.updated', ({ item, delta, }) => {
155
+ // Handle audio chunks
156
+ if (delta?.audio && item.role === 'assistant') {
157
+ if (!isAssistantSpeaking && onAssistantStartSpeaking) {
158
+ isAssistantSpeaking = true;
159
+ onAssistantStartSpeaking();
160
+ }
161
+ // OpenAI provides audio as Int16Array or base64
162
+ let audioBuffer;
163
+ if (delta.audio instanceof Int16Array) {
164
+ audioBuffer = Buffer.from(delta.audio.buffer);
165
+ }
166
+ else {
167
+ // Assume base64 string
168
+ audioBuffer = Buffer.from(delta.audio, 'base64');
169
+ }
170
+ // OpenAI uses 24kHz PCM16 format
171
+ audioChunkHandler({
172
+ data: audioBuffer,
173
+ mimeType: 'audio/pcm;rate=24000',
174
+ });
175
+ }
176
+ // Handle transcriptions
177
+ if (delta?.transcript) {
178
+ if (item.role === 'user') {
179
+ openaiLogger.log('User transcription:', delta.transcript);
180
+ }
181
+ else if (item.role === 'assistant') {
182
+ openaiLogger.log('Assistant transcription:', delta.transcript);
183
+ }
184
+ }
185
+ });
186
+ client.on('conversation.item.completed', ({ item }) => {
187
+ if ('role' in item &&
188
+ item.role === 'assistant' &&
189
+ isAssistantSpeaking &&
190
+ onAssistantStopSpeaking) {
191
+ isAssistantSpeaking = false;
192
+ onAssistantStopSpeaking();
193
+ }
194
+ });
195
+ client.on('conversation.interrupted', () => {
196
+ openaiLogger.log('Assistant was interrupted');
197
+ if (isAssistantSpeaking && onAssistantInterruptSpeaking) {
198
+ isAssistantSpeaking = false;
199
+ onAssistantInterruptSpeaking();
200
+ }
201
+ });
202
+ // Connect to the Realtime API
203
+ await client.connect();
204
+ const sessionResult = {
205
+ session: {
206
+ send: (audioData) => {
207
+ // Convert ArrayBuffer to Int16Array for OpenAI
208
+ const int16Data = new Int16Array(audioData);
209
+ client.appendInputAudio(int16Data);
210
+ },
211
+ sendText: (text) => {
212
+ // Send text message to OpenAI
213
+ client.sendUserMessageContent([{ type: 'input_text', text }]);
214
+ },
215
+ close: () => {
216
+ client.disconnect();
217
+ },
218
+ },
219
+ stop: () => {
220
+ client.disconnect();
221
+ },
222
+ };
223
+ return sessionResult;
224
+ }
@@ -0,0 +1,65 @@
1
+ // Detect a /commandname token on its own line in a user prompt and resolve it
2
+ // to a registered opencode command. Mirrors the Discord slash command flow
3
+ // (commands/user-command.ts) so users can type `/build foo` or `/build-cmd foo`
4
+ // in chat, via `/new-session`, through `kimaki send --prompt`, or scheduled
5
+ // tasks and have it routed to opencode's session.command API instead of going
6
+ // to the model as plain text.
7
+ //
8
+ // Detection is line-based: we scan each line and return the first one whose
9
+ // first non-whitespace token is `/<registered-command>`. This keeps the
10
+ // detector oblivious to prefix lines (`» **kimaki-cli:**`, `Context from
11
+ // thread:`, etc). Producers that add such prefixes must put them on their
12
+ // own line so the user's content starts on a fresh line.
13
+ import { store } from './store.js';
14
+ const DISCORD_SUFFIXES = ['-mcp-prompt', '-skill', '-cmd'];
15
+ function stripDiscordSuffix(token) {
16
+ for (const suffix of DISCORD_SUFFIXES) {
17
+ if (token.endsWith(suffix)) {
18
+ return token.slice(0, -suffix.length);
19
+ }
20
+ }
21
+ return token;
22
+ }
23
+ // Resolve a /token against registeredUserCommands. When the list is empty
24
+ // (gateway startup race), falls back to suffix-stripping so tokens like
25
+ // /build-cmd still route to session.command('build'). Tokens without a
26
+ // recognizable suffix return undefined to avoid false positives.
27
+ function resolveCommandName({ token, registered, }) {
28
+ const exact = registered.find((c) => {
29
+ return c.name === token || c.discordCommandName === token;
30
+ });
31
+ if (exact)
32
+ return exact.name;
33
+ const base = stripDiscordSuffix(token);
34
+ if (base === token)
35
+ return undefined;
36
+ const stripped = registered.find((c) => {
37
+ return c.name === base || c.discordCommandName === base;
38
+ });
39
+ if (stripped)
40
+ return stripped.name;
41
+ // Empty registry fallback: suffix was stripped, trust it
42
+ if (registered.length === 0)
43
+ return base;
44
+ return undefined;
45
+ }
46
+ export function extractLeadingOpencodeCommand(prompt, registered = store.getState().registeredUserCommands) {
47
+ if (!prompt)
48
+ return null;
49
+ for (const line of prompt.split('\n')) {
50
+ const trimmed = line.trimStart();
51
+ if (!trimmed.startsWith('/'))
52
+ continue;
53
+ const match = trimmed.match(/^\/([^\s]+)(?:\s+(.*))?$/);
54
+ if (!match)
55
+ continue;
56
+ const [, token, rest] = match;
57
+ if (!token)
58
+ continue;
59
+ const name = resolveCommandName({ token, registered });
60
+ if (!name)
61
+ continue;
62
+ return { command: { name, arguments: (rest ?? '').trim() } };
63
+ }
64
+ return null;
65
+ }
@@ -0,0 +1,240 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { extractLeadingOpencodeCommand } from './opencode-command-detection.js';
3
+ const fixtures = [
4
+ {
5
+ name: 'build',
6
+ discordCommandName: 'build-cmd',
7
+ description: 'build the project',
8
+ source: 'command',
9
+ },
10
+ {
11
+ name: 'namespace:foo',
12
+ discordCommandName: 'namespace-foo-cmd',
13
+ description: 'namespaced',
14
+ source: 'command',
15
+ },
16
+ {
17
+ name: 'review',
18
+ discordCommandName: 'review-skill',
19
+ description: 'review skill',
20
+ source: 'skill',
21
+ },
22
+ {
23
+ name: 'plan',
24
+ discordCommandName: 'plan-mcp-prompt',
25
+ description: 'plan via mcp',
26
+ source: 'mcp',
27
+ },
28
+ ];
29
+ describe('extractLeadingOpencodeCommand', () => {
30
+ test('plain /build with args', () => {
31
+ expect(extractLeadingOpencodeCommand('/build foo bar', fixtures)).toMatchInlineSnapshot(`
32
+ {
33
+ "command": {
34
+ "arguments": "foo bar",
35
+ "name": "build",
36
+ },
37
+ }
38
+ `);
39
+ });
40
+ test('plain /build no args', () => {
41
+ expect(extractLeadingOpencodeCommand('/build', fixtures))
42
+ .toMatchInlineSnapshot(`
43
+ {
44
+ "command": {
45
+ "arguments": "",
46
+ "name": "build",
47
+ },
48
+ }
49
+ `);
50
+ });
51
+ test('/build-cmd suffix resolves to build', () => {
52
+ expect(extractLeadingOpencodeCommand('/build-cmd hello world', fixtures)).toMatchInlineSnapshot(`
53
+ {
54
+ "command": {
55
+ "arguments": "hello world",
56
+ "name": "build",
57
+ },
58
+ }
59
+ `);
60
+ });
61
+ test('-skill suffix', () => {
62
+ expect(extractLeadingOpencodeCommand('/review-skill a b', fixtures)).toMatchInlineSnapshot(`
63
+ {
64
+ "command": {
65
+ "arguments": "a b",
66
+ "name": "review",
67
+ },
68
+ }
69
+ `);
70
+ });
71
+ test('-mcp-prompt suffix', () => {
72
+ expect(extractLeadingOpencodeCommand('/plan-mcp-prompt go', fixtures)).toMatchInlineSnapshot(`
73
+ {
74
+ "command": {
75
+ "arguments": "go",
76
+ "name": "plan",
77
+ },
78
+ }
79
+ `);
80
+ });
81
+ test('original namespaced name with colon', () => {
82
+ expect(extractLeadingOpencodeCommand('/namespace:foo arg', fixtures)).toMatchInlineSnapshot(`
83
+ {
84
+ "command": {
85
+ "arguments": "arg",
86
+ "name": "namespace:foo",
87
+ },
88
+ }
89
+ `);
90
+ });
91
+ test('discord-sanitized namespaced name', () => {
92
+ expect(extractLeadingOpencodeCommand('/namespace-foo-cmd arg', fixtures)).toMatchInlineSnapshot(`
93
+ {
94
+ "command": {
95
+ "arguments": "arg",
96
+ "name": "namespace:foo",
97
+ },
98
+ }
99
+ `);
100
+ });
101
+ test('kimaki-cli prefix on its own line', () => {
102
+ expect(extractLeadingOpencodeCommand('» **kimaki-cli:**\n/build foo bar', fixtures)).toMatchInlineSnapshot(`
103
+ {
104
+ "command": {
105
+ "arguments": "foo bar",
106
+ "name": "build",
107
+ },
108
+ }
109
+ `);
110
+ });
111
+ test('queue-style user prefix on its own line', () => {
112
+ expect(extractLeadingOpencodeCommand('» **Tommy:**\n/build hey', fixtures)).toMatchInlineSnapshot(`
113
+ {
114
+ "command": {
115
+ "arguments": "hey",
116
+ "name": "build",
117
+ },
118
+ }
119
+ `);
120
+ });
121
+ test('username containing asterisk on its own line', () => {
122
+ expect(extractLeadingOpencodeCommand('» **A*B:**\n/build hi', fixtures)).toMatchInlineSnapshot(`
123
+ {
124
+ "command": {
125
+ "arguments": "hi",
126
+ "name": "build",
127
+ },
128
+ }
129
+ `);
130
+ });
131
+ test('Context from thread wrapping still detects command', () => {
132
+ const wrapped = 'Context from thread:\nsome starter text\n\nUser request:\n/build foo';
133
+ expect(extractLeadingOpencodeCommand(wrapped, fixtures))
134
+ .toMatchInlineSnapshot(`
135
+ {
136
+ "command": {
137
+ "arguments": "foo",
138
+ "name": "build",
139
+ },
140
+ }
141
+ `);
142
+ });
143
+ test('unknown command returns null', () => {
144
+ expect(extractLeadingOpencodeCommand('/nothing here', fixtures)).toMatchInlineSnapshot(`null`);
145
+ });
146
+ test('no leading slash on any line returns null', () => {
147
+ expect(extractLeadingOpencodeCommand('hello /build\nmore text', fixtures)).toMatchInlineSnapshot(`null`);
148
+ });
149
+ test('just slash returns null', () => {
150
+ expect(extractLeadingOpencodeCommand('/', fixtures)).toMatchInlineSnapshot(`null`);
151
+ });
152
+ test('empty string returns null', () => {
153
+ expect(extractLeadingOpencodeCommand('', fixtures)).toMatchInlineSnapshot(`null`);
154
+ });
155
+ test('empty registry returns null for tokens without Discord suffix', () => {
156
+ expect(extractLeadingOpencodeCommand('/build foo', [])).toMatchInlineSnapshot(`null`);
157
+ });
158
+ test('empty registry fallback: -cmd suffix strips and returns base name', () => {
159
+ expect(extractLeadingOpencodeCommand('/hello-test-cmd', [])).toMatchInlineSnapshot(`
160
+ {
161
+ "command": {
162
+ "arguments": "",
163
+ "name": "hello-test",
164
+ },
165
+ }
166
+ `);
167
+ });
168
+ test('empty registry fallback: -skill suffix with args', () => {
169
+ expect(extractLeadingOpencodeCommand('/review-skill check auth', [])).toMatchInlineSnapshot(`
170
+ {
171
+ "command": {
172
+ "arguments": "check auth",
173
+ "name": "review",
174
+ },
175
+ }
176
+ `);
177
+ });
178
+ test('empty registry fallback skips non-suffixed, matches suffixed on next line', () => {
179
+ expect(extractLeadingOpencodeCommand('/unknown\n/deploy-cmd now', [])).toMatchInlineSnapshot(`
180
+ {
181
+ "command": {
182
+ "arguments": "now",
183
+ "name": "deploy",
184
+ },
185
+ }
186
+ `);
187
+ });
188
+ test('leading whitespace before slash still matches', () => {
189
+ expect(extractLeadingOpencodeCommand(' /build foo', fixtures)).toMatchInlineSnapshot(`
190
+ {
191
+ "command": {
192
+ "arguments": "foo",
193
+ "name": "build",
194
+ },
195
+ }
196
+ `);
197
+ });
198
+ test('first matching line wins', () => {
199
+ const prompt = 'noise line\n/build first args\n/review second args';
200
+ expect(extractLeadingOpencodeCommand(prompt, fixtures))
201
+ .toMatchInlineSnapshot(`
202
+ {
203
+ "command": {
204
+ "arguments": "first args",
205
+ "name": "build",
206
+ },
207
+ }
208
+ `);
209
+ });
210
+ test('unknown command on one line, known on next', () => {
211
+ const prompt = '/unknown foo\n/build bar';
212
+ expect(extractLeadingOpencodeCommand(prompt, fixtures))
213
+ .toMatchInlineSnapshot(`
214
+ {
215
+ "command": {
216
+ "arguments": "bar",
217
+ "name": "build",
218
+ },
219
+ }
220
+ `);
221
+ });
222
+ test('suffix strip does not clobber a command whose name happens to end in -cmd', () => {
223
+ const custom = [
224
+ {
225
+ name: 'deploy-cmd',
226
+ discordCommandName: 'deploy-cmd-cmd',
227
+ description: '',
228
+ source: 'command',
229
+ },
230
+ ];
231
+ expect(extractLeadingOpencodeCommand('/deploy-cmd now', custom)).toMatchInlineSnapshot(`
232
+ {
233
+ "command": {
234
+ "arguments": "now",
235
+ "name": "deploy-cmd",
236
+ },
237
+ }
238
+ `);
239
+ });
240
+ });
@@ -0,0 +1,129 @@
1
+ // Shared OpenCode and Kimaki command resolution helpers.
2
+ // Normalizes `which`/`where` output across platforms, builds safe spawn
3
+ // arguments for Windows npm `.cmd` shims without relying on `shell: true`,
4
+ // and creates a stable `kimaki` shim for OpenCode child processes.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ const WINDOWS_CMD_SHIM_REGEX = /\.(cmd|bat)$/i;
8
+ function quotePosixShellSegment(value) {
9
+ return `'${value.replaceAll("'", `'\\''`)}'`;
10
+ }
11
+ export function splitCommandLookupOutput(output) {
12
+ return output
13
+ .split(/\r?\n/g)
14
+ .map((line) => {
15
+ return line.trim();
16
+ })
17
+ .filter((line) => {
18
+ return line.length > 0;
19
+ });
20
+ }
21
+ export function selectResolvedCommand({ output, isWindows, }) {
22
+ const lines = splitCommandLookupOutput(output);
23
+ if (lines.length === 0) {
24
+ return null;
25
+ }
26
+ if (!isWindows) {
27
+ return lines[0] || null;
28
+ }
29
+ const cmdShim = lines.find((line) => {
30
+ return WINDOWS_CMD_SHIM_REGEX.test(line);
31
+ });
32
+ return cmdShim || lines[0] || null;
33
+ }
34
+ function quoteWindowsCommandSegment(value) {
35
+ if (!/[\s"]/u.test(value)) {
36
+ return value;
37
+ }
38
+ return `"${value.replaceAll('"', '\\"')}"`;
39
+ }
40
+ export function getSpawnCommandAndArgs({ resolvedCommand, baseArgs, platform, }) {
41
+ const effectivePlatform = platform || process.platform;
42
+ if (effectivePlatform !== 'win32') {
43
+ return { command: resolvedCommand, args: baseArgs };
44
+ }
45
+ if (!WINDOWS_CMD_SHIM_REGEX.test(resolvedCommand)) {
46
+ return { command: resolvedCommand, args: baseArgs };
47
+ }
48
+ return {
49
+ command: 'cmd.exe',
50
+ args: [
51
+ '/d',
52
+ '/s',
53
+ '/c',
54
+ quoteWindowsCommandSegment(resolvedCommand),
55
+ ...baseArgs.map((arg) => {
56
+ return quoteWindowsCommandSegment(arg);
57
+ }),
58
+ ],
59
+ // Let cmd.exe receive the command line exactly as constructed above.
60
+ // Without this, Node re-quotes the executable segment and npm shim paths
61
+ // like `C:\Program Files\nodejs\opencode.cmd` break again.
62
+ windowsVerbatimArguments: true,
63
+ };
64
+ }
65
+ export function ensureKimakiCommandShim({ dataDir, execPath, execArgv, entryScript, platform, }) {
66
+ const effectivePlatform = platform || process.platform;
67
+ const shimDirectory = path.join(dataDir, 'bin');
68
+ try {
69
+ fs.mkdirSync(shimDirectory, { recursive: true });
70
+ const launcherArgs = [...execArgv, entryScript];
71
+ if (effectivePlatform === 'win32') {
72
+ const shimPath = path.join(shimDirectory, 'kimaki.cmd');
73
+ const shimContent = [
74
+ '@echo off',
75
+ [execPath, ...launcherArgs].map((segment) => {
76
+ return `"${segment.replaceAll('"', '""')}"`;
77
+ }).join(' ') + ' %*',
78
+ '',
79
+ ].join('\r\n');
80
+ writeShimIfNeeded({
81
+ shimPath,
82
+ shimContent,
83
+ });
84
+ return shimDirectory;
85
+ }
86
+ const shimPath = path.join(shimDirectory, 'kimaki');
87
+ const shimContent = [
88
+ '#!/bin/sh',
89
+ `exec ${[execPath, ...launcherArgs].map((segment) => {
90
+ return quotePosixShellSegment(segment);
91
+ }).join(' ')} "$@"`,
92
+ '',
93
+ ].join('\n');
94
+ writeShimIfNeeded({
95
+ shimPath,
96
+ shimContent,
97
+ mode: 0o755,
98
+ });
99
+ return shimDirectory;
100
+ }
101
+ catch (cause) {
102
+ return new Error('Failed to create kimaki command shim', { cause });
103
+ }
104
+ }
105
+ export function prependPathEntry({ entry, existingPath, }) {
106
+ const pathEntries = (existingPath || '').split(path.delimiter).filter((segment) => {
107
+ return segment.length > 0;
108
+ });
109
+ if (pathEntries.includes(entry)) {
110
+ return existingPath || entry;
111
+ }
112
+ return [entry, ...pathEntries].join(path.delimiter);
113
+ }
114
+ export function getPathEnvKey(env) {
115
+ return Object.keys(env).find((key) => {
116
+ return key.toLowerCase() === 'path';
117
+ }) || 'PATH';
118
+ }
119
+ function writeShimIfNeeded({ shimPath, shimContent, mode, }) {
120
+ const existingContent = fs.existsSync(shimPath)
121
+ ? fs.readFileSync(shimPath, 'utf8')
122
+ : null;
123
+ if (existingContent !== shimContent) {
124
+ fs.writeFileSync(shimPath, shimContent, 'utf8');
125
+ }
126
+ if (mode !== undefined) {
127
+ fs.chmodSync(shimPath, mode);
128
+ }
129
+ }