@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,303 @@
1
+ // /screenshare command - Start screen sharing via VNC + WebSocket bridge + kimaki tunnel.
2
+ // On macOS: uses built-in Screen Sharing (port 5900).
3
+ // On Linux: spawns x11vnc against the current $DISPLAY.
4
+ // Exposes the VNC stream via an in-process websockify bridge and a traforo tunnel,
5
+ // then sends the user a noVNC URL they can open in a browser.
6
+ //
7
+ // /screenshare-stop command - Stops the active screen share for this guild.
8
+ import { MessageFlags } from 'discord.js';
9
+ import crypto from 'node:crypto';
10
+ import { spawn } from 'node:child_process';
11
+ import net from 'node:net';
12
+ import { TunnelClient } from 'traforo/client';
13
+ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
14
+ import { startWebsockify } from '../websockify.js';
15
+ import { createLogger } from '../logger.js';
16
+ import { execAsync } from '../worktrees.js';
17
+ const logger = createLogger('SCREEN');
18
+ const SECURE_REPLY_FLAGS = MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS;
19
+ /** One active screenshare per guild (Discord) or per machine (CLI) */
20
+ const activeSessions = new Map();
21
+ const VNC_PORT = 5900;
22
+ const MAX_SESSION_MINUTES = 30;
23
+ const MAX_SESSION_MS = MAX_SESSION_MINUTES * 60 * 1000;
24
+ const TUNNEL_BASE_DOMAIN = 'kimaki.xyz';
25
+ const SCREENSHARE_TUNNEL_ID_BYTES = 16;
26
+ // Public noVNC client — we point it at our tunnel URL
27
+ export function buildNoVncUrl({ tunnelHost }) {
28
+ const params = new URLSearchParams({
29
+ autoconnect: 'true',
30
+ host: tunnelHost,
31
+ port: '443',
32
+ encrypt: '1',
33
+ resize: 'scale',
34
+ view_only: 'false',
35
+ });
36
+ return `https://novnc.com/noVNC/vnc.html?${params.toString()}`;
37
+ }
38
+ export function createScreenshareTunnelId() {
39
+ return crypto.randomBytes(SCREENSHARE_TUNNEL_ID_BYTES).toString('hex');
40
+ }
41
+ // macOS has two separate services:
42
+ // - "Screen Sharing" = view-only VNC (com.apple.screensharing)
43
+ // - "Remote Management" = full control VNC with mouse/keyboard (ARDAgent)
44
+ // We need Remote Management for interactive control, not just Screen Sharing.
45
+ export async function ensureMacRemoteManagement() {
46
+ // Check if port 5900 is listening via netstat (no sudo needed).
47
+ // lsof and launchctl list both require sudo for system daemons.
48
+ try {
49
+ const { stdout } = await execAsync('netstat -an | grep "\\.5900 " | grep LISTEN', { timeout: 5000 });
50
+ if (stdout.trim()) {
51
+ return;
52
+ }
53
+ }
54
+ catch {
55
+ // not listening
56
+ }
57
+ throw new Error('macOS Remote Management is not enabled.\n' +
58
+ 'Enable it: **System Settings > General > Sharing > Remote Management**\n' +
59
+ 'Make sure "VNC viewers may control screen with password" is enabled.\n' +
60
+ 'Or via terminal:\n' +
61
+ '```\nsudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \\\n' +
62
+ ' -activate -configure -allowAccessFor -allUsers -privs -all \\\n' +
63
+ ' -clientopts -setvnclegacy -vnclegacy yes \\\n' +
64
+ ' -restart -agent -console\n```');
65
+ }
66
+ export function spawnX11Vnc() {
67
+ const display = process.env['DISPLAY'] || ':0';
68
+ const child = spawn('x11vnc', [
69
+ '-display', display,
70
+ '-nopw',
71
+ '-localhost',
72
+ '-rfbport', String(VNC_PORT),
73
+ '-shared',
74
+ '-forever',
75
+ ], {
76
+ stdio: ['ignore', 'pipe', 'pipe'],
77
+ });
78
+ child.stdout?.on('data', (data) => {
79
+ logger.log(`x11vnc: ${data.toString().trim()}`);
80
+ });
81
+ child.stderr?.on('data', (data) => {
82
+ logger.error(`x11vnc: ${data.toString().trim()}`);
83
+ });
84
+ return child;
85
+ }
86
+ function waitForPort({ port, process: proc, timeoutMs, }) {
87
+ return new Promise((resolve, reject) => {
88
+ const maxAttempts = Math.ceil(timeoutMs / 100);
89
+ let attempts = 0;
90
+ const check = () => {
91
+ if (proc.exitCode !== null) {
92
+ reject(new Error(`x11vnc exited with code ${proc.exitCode} before becoming ready`));
93
+ return;
94
+ }
95
+ const sock = net.createConnection(port, 'localhost');
96
+ sock.on('connect', () => {
97
+ sock.destroy();
98
+ resolve();
99
+ });
100
+ sock.on('error', () => {
101
+ sock.destroy();
102
+ if (++attempts >= maxAttempts) {
103
+ reject(new Error(`Port ${port} not reachable after ${timeoutMs}ms`));
104
+ }
105
+ else {
106
+ setTimeout(check, 100);
107
+ }
108
+ });
109
+ };
110
+ check();
111
+ });
112
+ }
113
+ export function cleanupSession(session) {
114
+ clearTimeout(session.timeoutTimer);
115
+ try {
116
+ session.tunnelClient.close();
117
+ }
118
+ catch { }
119
+ try {
120
+ session.wss.close();
121
+ }
122
+ catch { }
123
+ if (session.vncProcess) {
124
+ try {
125
+ session.vncProcess.kill();
126
+ }
127
+ catch { }
128
+ }
129
+ }
130
+ /**
131
+ * Core screenshare start logic, reused by both Discord command and CLI.
132
+ * Returns the session or throws on failure.
133
+ */
134
+ export async function startScreenshare({ sessionKey, startedBy, }) {
135
+ const existing = activeSessions.get(sessionKey);
136
+ if (existing) {
137
+ throw new Error(`Screen sharing is already active: ${existing.noVncUrl}`);
138
+ }
139
+ const platform = process.platform;
140
+ let vncProcess;
141
+ // Step 1: ensure VNC server is running
142
+ if (platform === 'darwin') {
143
+ await ensureMacRemoteManagement();
144
+ }
145
+ else if (platform === 'linux') {
146
+ if (!process.env['DISPLAY']) {
147
+ throw new Error('No $DISPLAY found. Screen sharing requires a running X11 display.');
148
+ }
149
+ try {
150
+ await execAsync('which x11vnc', { timeout: 3000 });
151
+ }
152
+ catch {
153
+ throw new Error('x11vnc is not installed. Install it with: sudo apt install x11vnc');
154
+ }
155
+ vncProcess = spawnX11Vnc();
156
+ // Wait for x11vnc to actually be ready (port 5900 accepting connections)
157
+ // instead of a blind 1s sleep. Polls every 100ms, fails if process exits first.
158
+ await waitForPort({ port: VNC_PORT, process: vncProcess, timeoutMs: 3000 });
159
+ }
160
+ else {
161
+ throw new Error(`Screen sharing is not supported on ${platform}. Only macOS and Linux are supported.`);
162
+ }
163
+ // Step 2: start in-process websockify bridge
164
+ let wsInstance;
165
+ try {
166
+ wsInstance = await startWebsockify({
167
+ wsPort: 0,
168
+ tcpHost: 'localhost',
169
+ tcpPort: VNC_PORT,
170
+ });
171
+ }
172
+ catch (err) {
173
+ if (vncProcess) {
174
+ vncProcess.kill();
175
+ }
176
+ throw err;
177
+ }
178
+ // Step 3: create tunnel
179
+ const tunnelId = createScreenshareTunnelId();
180
+ const tunnelClient = new TunnelClient({
181
+ localPort: wsInstance.port,
182
+ tunnelId,
183
+ baseDomain: TUNNEL_BASE_DOMAIN,
184
+ });
185
+ try {
186
+ await Promise.race([
187
+ tunnelClient.connect(),
188
+ new Promise((_, reject) => {
189
+ setTimeout(() => {
190
+ reject(new Error('Tunnel connection timed out after 15s'));
191
+ }, 15000);
192
+ }),
193
+ ]);
194
+ }
195
+ catch (err) {
196
+ tunnelClient.close();
197
+ wsInstance.close();
198
+ if (vncProcess) {
199
+ vncProcess.kill();
200
+ }
201
+ throw err;
202
+ }
203
+ const tunnelHost = `${tunnelId}-tunnel.${TUNNEL_BASE_DOMAIN}`;
204
+ const tunnelUrl = `https://${tunnelHost}`;
205
+ const noVncUrl = buildNoVncUrl({ tunnelHost });
206
+ // Auto-kill after a short session so a leaked URL does not stay usable all day.
207
+ const timeoutTimer = setTimeout(() => {
208
+ logger.log(`Screen share auto-stopped after ${MAX_SESSION_MINUTES} minutes (key: ${sessionKey})`);
209
+ stopScreenshare({ sessionKey });
210
+ }, MAX_SESSION_MS);
211
+ // Don't keep the process alive just for this timer
212
+ timeoutTimer.unref();
213
+ const session = {
214
+ tunnelClient,
215
+ wss: wsInstance.wss,
216
+ vncProcess,
217
+ url: tunnelUrl,
218
+ noVncUrl,
219
+ startedBy,
220
+ startedAt: Date.now(),
221
+ timeoutTimer,
222
+ };
223
+ activeSessions.set(sessionKey, session);
224
+ logger.log(`Screen share started by ${startedBy}: ${tunnelUrl}`);
225
+ return session;
226
+ }
227
+ /**
228
+ * Core screenshare stop logic, reused by both Discord command and CLI.
229
+ */
230
+ export function stopScreenshare({ sessionKey }) {
231
+ const session = activeSessions.get(sessionKey);
232
+ if (!session) {
233
+ return false;
234
+ }
235
+ cleanupSession(session);
236
+ activeSessions.delete(sessionKey);
237
+ logger.log(`Screen share stopped (key: ${sessionKey})`);
238
+ return true;
239
+ }
240
+ export async function handleScreenshareCommand({ command, }) {
241
+ const guildId = command.guildId;
242
+ if (!guildId) {
243
+ await command.reply({
244
+ content: 'This command can only be used in a server',
245
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
246
+ });
247
+ return;
248
+ }
249
+ await command.deferReply({ flags: SECURE_REPLY_FLAGS });
250
+ try {
251
+ const session = await startScreenshare({
252
+ sessionKey: guildId,
253
+ startedBy: command.user.tag,
254
+ });
255
+ await command.editReply({
256
+ content: `Screen sharing started. This reply is private and the URL uses a high-entropy tunnel id. ` +
257
+ `It will auto-stop after ${MAX_SESSION_MINUTES} minutes. Use /screenshare-stop to stop sooner.\n` +
258
+ `${session.noVncUrl}`,
259
+ });
260
+ }
261
+ catch (err) {
262
+ logger.error('Failed to start screen share:', err);
263
+ await command.editReply({
264
+ content: `Failed to start screen share: ${err instanceof Error ? err.message : String(err)}`,
265
+ });
266
+ }
267
+ }
268
+ export async function handleScreenshareStopCommand({ command, }) {
269
+ const guildId = command.guildId;
270
+ if (!guildId) {
271
+ await command.reply({
272
+ content: 'This command can only be used in a server',
273
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
274
+ });
275
+ return;
276
+ }
277
+ const stopped = stopScreenshare({ sessionKey: guildId });
278
+ if (!stopped) {
279
+ await command.reply({
280
+ content: 'No active screen share to stop',
281
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
282
+ });
283
+ return;
284
+ }
285
+ await command.reply({
286
+ content: 'Screen sharing stopped',
287
+ flags: SILENT_MESSAGE_FLAGS,
288
+ });
289
+ }
290
+ /** Cleanup all sessions on bot shutdown */
291
+ export function cleanupAllScreenshares() {
292
+ for (const [guildId, session] of activeSessions) {
293
+ cleanupSession(session);
294
+ activeSessions.delete(guildId);
295
+ }
296
+ }
297
+ // Kill all screenshares when the process exits (Ctrl+C, SIGTERM, etc.)
298
+ function onProcessExit() {
299
+ cleanupAllScreenshares();
300
+ }
301
+ process.on('SIGINT', onProcessExit);
302
+ process.on('SIGTERM', onProcessExit);
303
+ process.on('exit', onProcessExit);
@@ -0,0 +1,20 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { buildNoVncUrl, createScreenshareTunnelId } from './screenshare.js';
3
+ describe('screenshare security defaults', () => {
4
+ test('generates a 128-bit tunnel id', () => {
5
+ const ids = new Set(Array.from({ length: 32 }, () => {
6
+ return createScreenshareTunnelId();
7
+ }));
8
+ expect(ids.size).toBe(32);
9
+ for (const id of ids) {
10
+ expect(id).toMatch(/^[0-9a-f]{32}$/);
11
+ }
12
+ });
13
+ test('builds a secure noVNC URL', () => {
14
+ const url = new URL(buildNoVncUrl({ tunnelHost: '0123456789abcdef-tunnel.kimaki.xyz' }));
15
+ expect(url.origin).toBe('https://novnc.com');
16
+ expect(url.searchParams.get('host')).toBe('0123456789abcdef-tunnel.kimaki.xyz');
17
+ expect(url.searchParams.get('port')).toBe('443');
18
+ expect(url.searchParams.get('encrypt')).toBe('1');
19
+ });
20
+ });
@@ -0,0 +1,78 @@
1
+ // /session-id command - Show current session ID and an opencode attach command.
2
+ import { ChannelType, MessageFlags, } from 'discord.js';
3
+ import { getThreadSession } from '../database.js';
4
+ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
5
+ import { getOpencodeServerPort, initializeOpencodeForDirectory, } from '../opencode.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ const logger = createLogger(LogPrefix.SESSION);
8
+ function shellQuote(value) {
9
+ if (!value) {
10
+ return "''";
11
+ }
12
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
13
+ }
14
+ export async function handleSessionIdCommand({ command, }) {
15
+ const channel = command.channel;
16
+ if (!channel) {
17
+ await command.reply({
18
+ content: 'This command can only be used in a channel',
19
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
20
+ });
21
+ return;
22
+ }
23
+ const isThread = [
24
+ ChannelType.PublicThread,
25
+ ChannelType.PrivateThread,
26
+ ChannelType.AnnouncementThread,
27
+ ].includes(channel.type);
28
+ if (!isThread) {
29
+ await command.reply({
30
+ content: 'This command can only be used in a thread with an active session',
31
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
32
+ });
33
+ return;
34
+ }
35
+ const resolved = await resolveWorkingDirectory({
36
+ channel: channel,
37
+ });
38
+ if (!resolved) {
39
+ await command.reply({
40
+ content: 'Could not determine project directory for this channel',
41
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
42
+ });
43
+ return;
44
+ }
45
+ const { projectDirectory, workingDirectory } = resolved;
46
+ const sessionId = await getThreadSession(channel.id);
47
+ if (!sessionId) {
48
+ await command.reply({
49
+ content: 'No active session in this thread',
50
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
51
+ });
52
+ return;
53
+ }
54
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
55
+ let port = getOpencodeServerPort(projectDirectory);
56
+ if (!port) {
57
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
58
+ if (getClient instanceof Error) {
59
+ await command.editReply({
60
+ content: `Session ID: \`${sessionId}\`\nFailed to resolve OpenCode server port: ${getClient.message}`,
61
+ });
62
+ return;
63
+ }
64
+ port = getOpencodeServerPort(projectDirectory);
65
+ }
66
+ if (!port) {
67
+ await command.editReply({
68
+ content: `Session ID: \`${sessionId}\`\nCould not determine OpenCode server port`,
69
+ });
70
+ return;
71
+ }
72
+ const attachUrl = `http://127.0.0.1:${port}`;
73
+ const attachCommand = `opencode attach ${attachUrl} --session ${sessionId} --dir ${shellQuote(workingDirectory)}`;
74
+ await command.editReply({
75
+ content: `**Session ID:** \`${sessionId}\`\n**Attach command:**\n\`\`\`bash\n${attachCommand}\n\`\`\``,
76
+ });
77
+ logger.log(`Session ID shown for thread ${channel.id}: ${sessionId}`);
78
+ }
@@ -0,0 +1,176 @@
1
+ // /new-session command - Start a new OpenCode session.
2
+ import { ChannelType } from 'discord.js';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { getChannelDirectory } from '../database.js';
6
+ import { initializeOpencodeForDirectory } from '../opencode.js';
7
+ import { SILENT_MESSAGE_FLAGS, resolveProjectDirectoryFromAutocomplete } from '../discord-utils.js';
8
+ import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js';
9
+ import { createLogger, LogPrefix } from '../logger.js';
10
+ import * as errore from 'errore';
11
+ const logger = createLogger(LogPrefix.SESSION);
12
+ export async function handleSessionCommand({ command, appId, }) {
13
+ await command.deferReply();
14
+ const prompt = command.options.getString('prompt', true);
15
+ const filesString = command.options.getString('files') || '';
16
+ const agent = command.options.getString('agent') || undefined;
17
+ const channel = command.channel;
18
+ if (!channel || channel.type !== ChannelType.GuildText) {
19
+ await command.editReply('This command can only be used in text channels');
20
+ return;
21
+ }
22
+ const textChannel = channel;
23
+ const channelConfig = await getChannelDirectory(textChannel.id);
24
+ const projectDirectory = channelConfig?.directory;
25
+ if (!projectDirectory) {
26
+ await command.editReply('This channel is not configured with a project directory');
27
+ return;
28
+ }
29
+ if (!fs.existsSync(projectDirectory)) {
30
+ await command.editReply(`Directory does not exist: ${projectDirectory}`);
31
+ return;
32
+ }
33
+ try {
34
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
35
+ if (getClient instanceof Error) {
36
+ await command.editReply(getClient.message);
37
+ return;
38
+ }
39
+ const files = filesString
40
+ .split(',')
41
+ .map((f) => f.trim())
42
+ .filter((f) => f);
43
+ let fullPrompt = prompt;
44
+ if (files.length > 0) {
45
+ fullPrompt = `${prompt}\n\n@${files.join(' @')}`;
46
+ }
47
+ const starterMessage = await textChannel.send({
48
+ content: `🚀 **Starting OpenCode session**\n📝 ${prompt}${files.length > 0 ? `\n📎 Files: ${files.join(', ')}` : ''}`,
49
+ flags: SILENT_MESSAGE_FLAGS,
50
+ });
51
+ const thread = await starterMessage.startThread({
52
+ name: prompt.slice(0, 100),
53
+ autoArchiveDuration: 1440,
54
+ reason: 'OpenCode session',
55
+ });
56
+ // Add user to thread so it appears in their sidebar
57
+ await thread.members.add(command.user.id);
58
+ await command.editReply(`Created new session in ${thread.toString()}`);
59
+ const runtime = getOrCreateRuntime({
60
+ threadId: thread.id,
61
+ thread,
62
+ projectDirectory,
63
+ sdkDirectory: projectDirectory,
64
+ channelId: textChannel.id,
65
+ appId,
66
+ });
67
+ await runtime.enqueueIncoming({
68
+ prompt: fullPrompt,
69
+ userId: command.user.id,
70
+ username: command.user.displayName,
71
+ agent,
72
+ appId,
73
+ mode: 'opencode',
74
+ });
75
+ }
76
+ catch (error) {
77
+ logger.error('[SESSION] Error:', error);
78
+ await command.editReply(`Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}`);
79
+ }
80
+ }
81
+ async function handleAgentAutocomplete({ interaction, }) {
82
+ const focusedValue = interaction.options.getFocused();
83
+ // interaction.channel can be null when the channel isn't cached
84
+ // (common with gateway-proxy). Use channelId which is always available
85
+ // from the raw interaction payload.
86
+ const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction);
87
+ if (!projectDirectory) {
88
+ await interaction.respond([]);
89
+ return;
90
+ }
91
+ try {
92
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
93
+ if (getClient instanceof Error) {
94
+ await interaction.respond([]);
95
+ return;
96
+ }
97
+ const agentsResponse = await getClient().app.agents({
98
+ directory: projectDirectory,
99
+ });
100
+ if (!agentsResponse.data || agentsResponse.data.length === 0) {
101
+ await interaction.respond([]);
102
+ return;
103
+ }
104
+ const agents = agentsResponse.data
105
+ .filter((a) => {
106
+ const hidden = a.hidden;
107
+ return (a.mode === 'primary' || a.mode === 'all') && !hidden;
108
+ })
109
+ .filter((a) => a.name.toLowerCase().includes(focusedValue.toLowerCase()))
110
+ .slice(0, 25);
111
+ const choices = agents.map((agent) => ({
112
+ name: agent.name.slice(0, 100),
113
+ value: agent.name,
114
+ }));
115
+ await interaction.respond(choices);
116
+ }
117
+ catch (error) {
118
+ logger.error('[AUTOCOMPLETE] Error fetching agents:', error);
119
+ await interaction.respond([]);
120
+ }
121
+ }
122
+ export async function handleSessionAutocomplete({ interaction, }) {
123
+ const focusedOption = interaction.options.getFocused(true);
124
+ if (focusedOption.name === 'agent') {
125
+ await handleAgentAutocomplete({ interaction });
126
+ return;
127
+ }
128
+ if (focusedOption.name !== 'files') {
129
+ return;
130
+ }
131
+ const focusedValue = focusedOption.value;
132
+ const parts = focusedValue.split(',');
133
+ const previousFiles = parts
134
+ .slice(0, -1)
135
+ .map((f) => f.trim())
136
+ .filter((f) => f);
137
+ const currentQuery = (parts[parts.length - 1] || '').trim();
138
+ const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction);
139
+ if (!projectDirectory) {
140
+ await interaction.respond([]);
141
+ return;
142
+ }
143
+ try {
144
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
145
+ if (getClient instanceof Error) {
146
+ await interaction.respond([]);
147
+ return;
148
+ }
149
+ const response = await getClient().find.files({
150
+ query: currentQuery || '',
151
+ });
152
+ const files = response.data || [];
153
+ const prefix = previousFiles.length > 0 ? previousFiles.join(', ') + ', ' : '';
154
+ const choices = files
155
+ .map((file) => {
156
+ const fullValue = prefix + file;
157
+ const allFiles = [...previousFiles, file];
158
+ const allBasenames = allFiles.map((f) => f.split('/').pop() || f);
159
+ let displayName = allBasenames.join(', ');
160
+ if (displayName.length > 100) {
161
+ displayName = '…' + displayName.slice(-97);
162
+ }
163
+ return {
164
+ name: displayName,
165
+ value: fullValue,
166
+ };
167
+ })
168
+ .filter((choice) => choice.value.length <= 100)
169
+ .slice(0, 25);
170
+ await interaction.respond(choices);
171
+ }
172
+ catch (error) {
173
+ logger.error('[AUTOCOMPLETE] Error fetching files:', error);
174
+ await interaction.respond([]);
175
+ }
176
+ }
@@ -0,0 +1,80 @@
1
+ // /share command - Share the current session as a public URL.
2
+ import { ChannelType, MessageFlags, } from 'discord.js';
3
+ import { getThreadSession } from '../database.js';
4
+ import { initializeOpencodeForDirectory } from '../opencode.js';
5
+ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ const logger = createLogger(LogPrefix.SHARE);
8
+ export async function handleShareCommand({ command, }) {
9
+ const channel = command.channel;
10
+ if (!channel) {
11
+ await command.reply({
12
+ content: 'This command can only be used in a channel',
13
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
14
+ });
15
+ return;
16
+ }
17
+ const isThread = [
18
+ ChannelType.PublicThread,
19
+ ChannelType.PrivateThread,
20
+ ChannelType.AnnouncementThread,
21
+ ].includes(channel.type);
22
+ if (!isThread) {
23
+ await command.reply({
24
+ content: 'This command can only be used in a thread with an active session',
25
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
26
+ });
27
+ return;
28
+ }
29
+ const resolved = await resolveWorkingDirectory({
30
+ channel: channel,
31
+ });
32
+ if (!resolved) {
33
+ await command.reply({
34
+ content: 'Could not determine project directory for this channel',
35
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
36
+ });
37
+ return;
38
+ }
39
+ const { projectDirectory } = resolved;
40
+ const sessionId = await getThreadSession(channel.id);
41
+ if (!sessionId) {
42
+ await command.reply({
43
+ content: 'No active session in this thread',
44
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
45
+ });
46
+ return;
47
+ }
48
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
49
+ if (getClient instanceof Error) {
50
+ await command.reply({
51
+ content: `Failed to share session: ${getClient.message}`,
52
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
53
+ });
54
+ return;
55
+ }
56
+ try {
57
+ const response = await getClient().session.share({
58
+ sessionID: sessionId,
59
+ });
60
+ if (!response.data?.share?.url) {
61
+ await command.reply({
62
+ content: 'Failed to generate share URL',
63
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
64
+ });
65
+ return;
66
+ }
67
+ await command.reply({
68
+ content: `🔗 **Session shared:** ${response.data.share.url}`,
69
+ flags: SILENT_MESSAGE_FLAGS,
70
+ });
71
+ logger.log(`Session ${sessionId} shared: ${response.data.share.url}`);
72
+ }
73
+ catch (error) {
74
+ logger.error('[SHARE] Error:', error);
75
+ await command.reply({
76
+ content: `Failed to share session: ${error instanceof Error ? error.message : 'Unknown error'}`,
77
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
78
+ });
79
+ }
80
+ }