kimaki 0.31.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1001) hide show
  1. package/dist/agent-ui.d.ts +33 -0
  2. package/dist/agent-ui.e2e.test.d.ts +1 -0
  3. package/dist/agent-ui.e2e.test.js +140 -0
  4. package/dist/agent-ui.e2e.test.js.map +1 -0
  5. package/dist/agent-ui.js +199 -0
  6. package/dist/agent-ui.js.map +1 -0
  7. package/dist/analytics.d.ts +42 -0
  8. package/dist/analytics.e2e.test.d.ts +1 -0
  9. package/dist/analytics.e2e.test.js +112 -0
  10. package/dist/analytics.e2e.test.js.map +1 -0
  11. package/dist/analytics.js +162 -223
  12. package/dist/analytics.js.map +1 -0
  13. package/dist/analytics.test.d.ts +1 -0
  14. package/dist/analytics.test.js +121 -157
  15. package/dist/analytics.test.js.map +1 -0
  16. package/dist/autostart.d.ts +7 -0
  17. package/dist/autostart.js +87 -0
  18. package/dist/autostart.js.map +1 -0
  19. package/dist/bot.d.ts +91 -0
  20. package/dist/bot.js +120 -0
  21. package/dist/bot.js.map +1 -0
  22. package/dist/btw.e2e.test.d.ts +1 -0
  23. package/dist/btw.e2e.test.js +102 -0
  24. package/dist/btw.e2e.test.js.map +1 -0
  25. package/dist/builtin-skills.d.ts +18 -0
  26. package/dist/builtin-skills.js +51 -0
  27. package/dist/builtin-skills.js.map +1 -0
  28. package/dist/builtin-skills.test.d.ts +1 -0
  29. package/dist/builtin-skills.test.js +28 -0
  30. package/dist/builtin-skills.test.js.map +1 -0
  31. package/dist/catalog-refresh.e2e.test.d.ts +1 -0
  32. package/dist/catalog-refresh.e2e.test.js +90 -0
  33. package/dist/catalog-refresh.e2e.test.js.map +1 -0
  34. package/dist/cli/bot.d.ts +5 -0
  35. package/dist/cli/bot.js +434 -0
  36. package/dist/cli/bot.js.map +1 -0
  37. package/dist/cli/discord.d.ts +3 -0
  38. package/dist/cli/discord.js +118 -0
  39. package/dist/cli/discord.js.map +1 -0
  40. package/dist/cli/project.d.ts +5 -0
  41. package/dist/cli/project.js +143 -0
  42. package/dist/cli/project.js.map +1 -0
  43. package/dist/cli/schedule.d.ts +3 -0
  44. package/dist/cli/schedule.js +156 -0
  45. package/dist/cli/schedule.js.map +1 -0
  46. package/dist/cli/session.d.ts +4 -0
  47. package/dist/cli/session.js +245 -0
  48. package/dist/cli/session.js.map +1 -0
  49. package/dist/cli/shared.d.ts +55 -0
  50. package/dist/cli/shared.js +138 -0
  51. package/dist/cli/shared.js.map +1 -0
  52. package/dist/cli-discord.e2e.test.d.ts +1 -0
  53. package/dist/cli-discord.e2e.test.js +82 -0
  54. package/dist/cli-discord.e2e.test.js.map +1 -0
  55. package/dist/cli-lock.e2e.test.d.ts +1 -0
  56. package/dist/cli-lock.e2e.test.js +520 -0
  57. package/dist/cli-lock.e2e.test.js.map +1 -0
  58. package/dist/cli-read.e2e.test.d.ts +1 -0
  59. package/dist/cli-read.e2e.test.js +86 -0
  60. package/dist/cli-read.e2e.test.js.map +1 -0
  61. package/dist/cli-sessions.e2e.test.d.ts +1 -0
  62. package/dist/cli-sessions.e2e.test.js +116 -0
  63. package/dist/cli-sessions.e2e.test.js.map +1 -0
  64. package/dist/cli.d.ts +2 -0
  65. package/dist/cli.js +34 -263
  66. package/dist/cli.js.map +1 -0
  67. package/dist/commands/login-commands.d.ts +59 -0
  68. package/dist/commands/login-commands.js +341 -0
  69. package/dist/commands/login-commands.js.map +1 -0
  70. package/dist/commands/preference-commands.d.ts +44 -0
  71. package/dist/commands/preference-commands.js +565 -0
  72. package/dist/commands/preference-commands.js.map +1 -0
  73. package/dist/commands/session-commands.d.ts +8 -0
  74. package/dist/commands/session-commands.js +267 -0
  75. package/dist/commands/session-commands.js.map +1 -0
  76. package/dist/commands/thread-commands.d.ts +16 -0
  77. package/dist/commands/thread-commands.js +285 -0
  78. package/dist/commands/thread-commands.js.map +1 -0
  79. package/dist/commands/worktree-commands.d.ts +41 -0
  80. package/dist/commands/worktree-commands.js +279 -0
  81. package/dist/commands/worktree-commands.js.map +1 -0
  82. package/dist/commands.e2e.test.d.ts +1 -0
  83. package/dist/commands.e2e.test.js +255 -0
  84. package/dist/commands.e2e.test.js.map +1 -0
  85. package/dist/credentials.d.ts +84 -0
  86. package/dist/credentials.js +320 -0
  87. package/dist/credentials.js.map +1 -0
  88. package/dist/db.d.ts +1532 -0
  89. package/dist/db.js +57 -245
  90. package/dist/db.js.map +1 -0
  91. package/dist/db.test.d.ts +1 -0
  92. package/dist/db.test.js +351 -682
  93. package/dist/db.test.js.map +1 -0
  94. package/dist/duration.d.ts +2 -0
  95. package/dist/duration.js +18 -0
  96. package/dist/duration.js.map +1 -0
  97. package/dist/effects.d.ts +15 -0
  98. package/dist/effects.js +315 -0
  99. package/dist/effects.js.map +1 -0
  100. package/dist/errors.d.ts +38 -0
  101. package/dist/errors.js +40 -175
  102. package/dist/errors.js.map +1 -0
  103. package/dist/event-loop.d.ts +41 -0
  104. package/dist/event-loop.js +418 -0
  105. package/dist/event-loop.js.map +1 -0
  106. package/dist/file-edit-log.d.ts +41 -0
  107. package/dist/file-edit-log.js +122 -164
  108. package/dist/file-edit-log.js.map +1 -0
  109. package/dist/file-edit-log.test.d.ts +1 -0
  110. package/dist/file-edit-log.test.js +68 -244
  111. package/dist/file-edit-log.test.js.map +1 -0
  112. package/dist/format-parts.d.ts +137 -0
  113. package/dist/format-parts.js +300 -0
  114. package/dist/format-parts.js.map +1 -0
  115. package/dist/format-parts.test.d.ts +1 -0
  116. package/dist/format-parts.test.js +144 -0
  117. package/dist/format-parts.test.js.map +1 -0
  118. package/dist/gateway-replay.e2e.test.d.ts +1 -0
  119. package/dist/gateway-replay.e2e.test.js +95 -0
  120. package/dist/gateway-replay.e2e.test.js.map +1 -0
  121. package/dist/harness.e2e.test.d.ts +1 -0
  122. package/dist/harness.e2e.test.js +107 -0
  123. package/dist/harness.e2e.test.js.map +1 -0
  124. package/dist/hello-thread.e2e.test.d.ts +1 -0
  125. package/dist/hello-thread.e2e.test.js +194 -0
  126. package/dist/hello-thread.e2e.test.js.map +1 -0
  127. package/dist/ingress.d.ts +7 -0
  128. package/dist/ingress.js +337 -0
  129. package/dist/ingress.js.map +1 -0
  130. package/dist/interaction-context.d.ts +45 -0
  131. package/dist/interaction-context.js +77 -0
  132. package/dist/interaction-context.js.map +1 -0
  133. package/dist/interrupt.e2e.test.d.ts +1 -0
  134. package/dist/interrupt.e2e.test.js +124 -0
  135. package/dist/interrupt.e2e.test.js.map +1 -0
  136. package/dist/legacy.e2e.test.d.ts +1 -0
  137. package/dist/legacy.e2e.test.js +85 -0
  138. package/dist/legacy.e2e.test.js.map +1 -0
  139. package/dist/live-call.d.ts +51 -0
  140. package/dist/live-call.js +207 -0
  141. package/dist/live-call.js.map +1 -0
  142. package/dist/lock-routes.d.ts +812 -0
  143. package/dist/lock-routes.js +635 -0
  144. package/dist/lock-routes.js.map +1 -0
  145. package/dist/lock-server.d.ts +36 -0
  146. package/dist/lock-server.js +223 -0
  147. package/dist/lock-server.js.map +1 -0
  148. package/dist/logger.d.ts +13 -0
  149. package/dist/logger.js +112 -162
  150. package/dist/logger.js.map +1 -0
  151. package/dist/login.e2e.test.d.ts +1 -0
  152. package/dist/login.e2e.test.js +83 -0
  153. package/dist/login.e2e.test.js.map +1 -0
  154. package/dist/main.d.ts +33 -0
  155. package/dist/main.js +261 -0
  156. package/dist/main.js.map +1 -0
  157. package/dist/markdown/components.d.ts +10 -0
  158. package/dist/markdown/components.js +77 -0
  159. package/dist/markdown/components.js.map +1 -0
  160. package/dist/markdown/markdown.test.d.ts +1 -0
  161. package/dist/markdown/markdown.test.js +393 -0
  162. package/dist/markdown/markdown.test.js.map +1 -0
  163. package/dist/markdown/render-markdown.d.ts +30 -0
  164. package/dist/markdown/render-markdown.js +265 -0
  165. package/dist/markdown/render-markdown.js.map +1 -0
  166. package/dist/message-tool.e2e.test.d.ts +1 -0
  167. package/dist/message-tool.e2e.test.js +39 -0
  168. package/dist/message-tool.e2e.test.js.map +1 -0
  169. package/dist/migrations.d.ts +8 -0
  170. package/dist/migrations.js +187 -0
  171. package/dist/migrations.js.map +1 -0
  172. package/dist/onboarding-cli.e2e.test.d.ts +1 -0
  173. package/dist/onboarding-cli.e2e.test.js +178 -0
  174. package/dist/onboarding-cli.e2e.test.js.map +1 -0
  175. package/dist/onboarding.d.ts +36 -0
  176. package/dist/onboarding.e2e.test.d.ts +1 -0
  177. package/dist/onboarding.e2e.test.js +148 -0
  178. package/dist/onboarding.e2e.test.js.map +1 -0
  179. package/dist/onboarding.js +261 -0
  180. package/dist/onboarding.js.map +1 -0
  181. package/dist/opencode-server.d.ts +54 -0
  182. package/dist/opencode-server.js +417 -0
  183. package/dist/opencode-server.js.map +1 -0
  184. package/dist/opencode-server.test.d.ts +1 -0
  185. package/dist/opencode-server.test.js +21 -0
  186. package/dist/opencode-server.test.js.map +1 -0
  187. package/dist/permissions.d.ts +31 -0
  188. package/dist/permissions.e2e.test.d.ts +1 -0
  189. package/dist/permissions.e2e.test.js +166 -0
  190. package/dist/permissions.e2e.test.js.map +1 -0
  191. package/dist/permissions.js +95 -0
  192. package/dist/permissions.js.map +1 -0
  193. package/dist/plugin/anthropic/index.d.ts +13 -0
  194. package/dist/plugin/anthropic/index.js +201 -0
  195. package/dist/plugin/anthropic/index.js.map +1 -0
  196. package/dist/plugin/anthropic/index.test.d.ts +1 -0
  197. package/dist/plugin/anthropic/index.test.js +39 -0
  198. package/dist/plugin/anthropic/index.test.js.map +1 -0
  199. package/dist/plugin/index.d.ts +3 -0
  200. package/dist/plugin/index.js +124 -0
  201. package/dist/plugin/index.js.map +1 -0
  202. package/dist/plugin/read-media.d.ts +23 -0
  203. package/dist/plugin/read-media.js +72 -0
  204. package/dist/plugin/read-media.js.map +1 -0
  205. package/dist/plugin/session-id/index.d.ts +3 -0
  206. package/dist/plugin/session-id/index.js +18 -0
  207. package/dist/plugin/session-id/index.js.map +1 -0
  208. package/dist/plugin.e2e.test.d.ts +1 -0
  209. package/dist/plugin.e2e.test.js +92 -0
  210. package/dist/plugin.e2e.test.js.map +1 -0
  211. package/dist/preference-commands.e2e.test.d.ts +1 -0
  212. package/dist/preference-commands.e2e.test.js +555 -0
  213. package/dist/preference-commands.e2e.test.js.map +1 -0
  214. package/dist/probe-live.tmp.d.ts +1 -0
  215. package/dist/probe-live.tmp.js +82 -0
  216. package/dist/probe-live.tmp.js.map +1 -0
  217. package/dist/project.d.ts +58 -0
  218. package/dist/project.js +169 -0
  219. package/dist/project.js.map +1 -0
  220. package/dist/prompt.d.ts +112 -0
  221. package/dist/prompt.js +577 -0
  222. package/dist/prompt.js.map +1 -0
  223. package/dist/questions.d.ts +59 -0
  224. package/dist/questions.e2e.test.d.ts +1 -0
  225. package/dist/questions.e2e.test.js +290 -0
  226. package/dist/questions.e2e.test.js.map +1 -0
  227. package/dist/questions.js +231 -0
  228. package/dist/questions.js.map +1 -0
  229. package/dist/queue.d.ts +45 -0
  230. package/dist/queue.e2e.test.d.ts +1 -0
  231. package/dist/queue.e2e.test.js +220 -0
  232. package/dist/queue.e2e.test.js.map +1 -0
  233. package/dist/queue.js +132 -0
  234. package/dist/queue.js.map +1 -0
  235. package/dist/remote-send.d.ts +32 -0
  236. package/dist/remote-send.e2e.test.d.ts +1 -0
  237. package/dist/remote-send.e2e.test.js +89 -0
  238. package/dist/remote-send.e2e.test.js.map +1 -0
  239. package/dist/remote-send.js +105 -0
  240. package/dist/remote-send.js.map +1 -0
  241. package/dist/renderer.e2e.test.d.ts +1 -0
  242. package/dist/renderer.e2e.test.js +357 -0
  243. package/dist/renderer.e2e.test.js.map +1 -0
  244. package/dist/restart.e2e.test.d.ts +1 -0
  245. package/dist/restart.e2e.test.js +68 -0
  246. package/dist/restart.e2e.test.js.map +1 -0
  247. package/dist/routes.d.ts +24 -0
  248. package/dist/routes.js +42 -0
  249. package/dist/routes.js.map +1 -0
  250. package/dist/routes.test.d.ts +1 -0
  251. package/dist/routes.test.js +79 -0
  252. package/dist/routes.test.js.map +1 -0
  253. package/dist/routines.e2e.test.d.ts +1 -0
  254. package/dist/routines.e2e.test.js +188 -0
  255. package/dist/routines.e2e.test.js.map +1 -0
  256. package/dist/scheduler.d.ts +116 -0
  257. package/dist/scheduler.e2e.test.d.ts +1 -0
  258. package/dist/scheduler.e2e.test.js +163 -0
  259. package/dist/scheduler.e2e.test.js.map +1 -0
  260. package/dist/scheduler.js +602 -0
  261. package/dist/scheduler.js.map +1 -0
  262. package/dist/scheduler.test.d.ts +1 -0
  263. package/dist/scheduler.test.js +180 -0
  264. package/dist/scheduler.test.js.map +1 -0
  265. package/dist/schema-sql.d.ts +1 -0
  266. package/dist/schema-sql.js +3 -0
  267. package/dist/schema-sql.js.map +1 -0
  268. package/dist/schema.d.ts +2968 -0
  269. package/dist/schema.js +95 -252
  270. package/dist/schema.js.map +1 -0
  271. package/dist/session-commands.e2e.test.d.ts +1 -0
  272. package/dist/session-commands.e2e.test.js +285 -0
  273. package/dist/session-commands.e2e.test.js.map +1 -0
  274. package/dist/session-events.d.ts +83 -0
  275. package/dist/session-events.js +260 -0
  276. package/dist/session-events.js.map +1 -0
  277. package/dist/session-ops.e2e.test.d.ts +1 -0
  278. package/dist/session-ops.e2e.test.js +114 -0
  279. package/dist/session-ops.e2e.test.js.map +1 -0
  280. package/dist/sessions.d.ts +121 -0
  281. package/dist/sessions.js +663 -0
  282. package/dist/sessions.js.map +1 -0
  283. package/dist/shell.e2e.test.d.ts +1 -0
  284. package/dist/shell.e2e.test.js +147 -0
  285. package/dist/shell.e2e.test.js.map +1 -0
  286. package/dist/slash-commands.d.ts +62 -0
  287. package/dist/slash-commands.js +476 -0
  288. package/dist/slash-commands.js.map +1 -0
  289. package/dist/slash-commands.test.d.ts +1 -0
  290. package/dist/slash-commands.test.js +87 -0
  291. package/dist/slash-commands.test.js.map +1 -0
  292. package/dist/sleep.e2e.test.d.ts +1 -0
  293. package/dist/sleep.e2e.test.js +115 -487
  294. package/dist/sleep.e2e.test.js.map +1 -0
  295. package/dist/sleeps.d.ts +37 -0
  296. package/dist/sleeps.js +220 -0
  297. package/dist/sleeps.js.map +1 -0
  298. package/dist/store.d.ts +10 -0
  299. package/dist/store.js +8 -28
  300. package/dist/store.js.map +1 -0
  301. package/dist/system-prompt.d.ts +76 -0
  302. package/dist/system-prompt.js +852 -0
  303. package/dist/system-prompt.js.map +1 -0
  304. package/dist/tasks.e2e.test.d.ts +1 -0
  305. package/dist/tasks.e2e.test.js +173 -0
  306. package/dist/tasks.e2e.test.js.map +1 -0
  307. package/dist/test/auth-plugin/index.d.ts +3 -0
  308. package/dist/test/auth-plugin/index.js +18 -0
  309. package/dist/test/auth-plugin/index.js.map +1 -0
  310. package/dist/test/fake-mcp-server.d.ts +1 -0
  311. package/dist/test/fake-mcp-server.js +45 -0
  312. package/dist/test/fake-mcp-server.js.map +1 -0
  313. package/dist/test/harness.d.ts +246 -0
  314. package/dist/test/harness.js +704 -0
  315. package/dist/test/harness.js.map +1 -0
  316. package/dist/test/replay.d.ts +14 -0
  317. package/dist/test/replay.js +75 -0
  318. package/dist/test/replay.js.map +1 -0
  319. package/dist/thread-reducer.d.ts +172 -0
  320. package/dist/thread-reducer.fixtures.test.d.ts +1 -0
  321. package/dist/thread-reducer.fixtures.test.js +360 -0
  322. package/dist/thread-reducer.fixtures.test.js.map +1 -0
  323. package/dist/thread-reducer.js +497 -0
  324. package/dist/thread-reducer.js.map +1 -0
  325. package/dist/thread-reducer.test.d.ts +1 -0
  326. package/dist/thread-reducer.test.js +369 -0
  327. package/dist/thread-reducer.test.js.map +1 -0
  328. package/dist/tts.test.d.ts +1 -0
  329. package/dist/tts.test.js +147 -0
  330. package/dist/tts.test.js.map +1 -0
  331. package/dist/voice-agent.d.ts +33 -0
  332. package/dist/voice-agent.js +159 -0
  333. package/dist/voice-agent.js.map +1 -0
  334. package/dist/voice-attachments.e2e.test.d.ts +1 -0
  335. package/dist/voice-attachments.e2e.test.js +256 -0
  336. package/dist/voice-attachments.e2e.test.js.map +1 -0
  337. package/dist/voice-call.d.ts +59 -0
  338. package/dist/voice-call.e2e.test.d.ts +1 -0
  339. package/dist/voice-call.e2e.test.js +259 -0
  340. package/dist/voice-call.e2e.test.js.map +1 -0
  341. package/dist/voice-call.js +760 -0
  342. package/dist/voice-call.js.map +1 -0
  343. package/dist/voice-openai.e2e.test.d.ts +1 -0
  344. package/dist/voice-openai.e2e.test.js +161 -0
  345. package/dist/voice-openai.e2e.test.js.map +1 -0
  346. package/dist/voice.d.ts +116 -0
  347. package/dist/voice.js +619 -717
  348. package/dist/voice.js.map +1 -0
  349. package/dist/working-directory.e2e.test.d.ts +1 -0
  350. package/dist/working-directory.e2e.test.js +231 -0
  351. package/dist/working-directory.e2e.test.js.map +1 -0
  352. package/dist/worktrees.d.ts +54 -0
  353. package/dist/worktrees.js +247 -709
  354. package/dist/worktrees.js.map +1 -0
  355. package/dist/worktrees.test.d.ts +1 -0
  356. package/dist/worktrees.test.js +97 -952
  357. package/dist/worktrees.test.js.map +1 -0
  358. package/package.json +38 -73
  359. package/skills/kimaki-debug/SKILL.md +44 -0
  360. package/skills/kimaki-tunnel/SKILL.md +34 -0
  361. package/src/agent-ui.e2e.test.ts +134 -0
  362. package/src/agent-ui.ts +216 -0
  363. package/src/analytics.e2e.test.ts +138 -0
  364. package/src/analytics.test.ts +122 -182
  365. package/src/analytics.ts +206 -258
  366. package/src/autostart.ts +90 -0
  367. package/src/bot.ts +196 -0
  368. package/src/btw.e2e.test.ts +130 -0
  369. package/src/builtin-skills.test.ts +29 -0
  370. package/src/builtin-skills.ts +59 -0
  371. package/src/catalog-refresh.e2e.test.ts +112 -0
  372. package/src/cli/bot.ts +425 -0
  373. package/src/cli/discord.ts +116 -0
  374. package/src/cli/project.ts +147 -0
  375. package/src/cli/schedule.ts +151 -0
  376. package/src/cli/session.ts +252 -0
  377. package/src/cli/shared.ts +139 -0
  378. package/src/cli-discord.e2e.test.ts +79 -0
  379. package/src/cli-lock.e2e.test.ts +530 -0
  380. package/src/cli-read.e2e.test.ts +83 -0
  381. package/src/cli-sessions.e2e.test.ts +120 -0
  382. package/src/cli.ts +35 -418
  383. package/src/commands/login-commands.ts +360 -0
  384. package/src/commands/preference-commands.ts +599 -0
  385. package/src/commands/session-commands.ts +289 -0
  386. package/src/commands/thread-commands.ts +324 -0
  387. package/src/commands/worktree-commands.ts +288 -0
  388. package/src/commands.e2e.test.ts +290 -0
  389. package/src/credentials.ts +425 -0
  390. package/src/db.test.ts +344 -759
  391. package/src/db.ts +66 -284
  392. package/src/duration.ts +18 -0
  393. package/src/effects.ts +337 -0
  394. package/src/errors.ts +39 -228
  395. package/src/event-loop.ts +471 -0
  396. package/src/file-edit-log.test.ts +68 -272
  397. package/src/file-edit-log.ts +108 -231
  398. package/src/fixtures/v1-schema.sql +241 -0
  399. package/src/fixtures/voice.ogg +0 -0
  400. package/src/format-parts.test.ts +150 -0
  401. package/src/format-parts.ts +405 -0
  402. package/src/gateway-replay.e2e.test.ts +116 -0
  403. package/src/harness.e2e.test.ts +124 -0
  404. package/src/hello-thread.e2e.test.ts +223 -0
  405. package/src/ingress.ts +334 -0
  406. package/src/interaction-context.ts +131 -0
  407. package/src/interrupt.e2e.test.ts +151 -0
  408. package/src/legacy.e2e.test.ts +105 -0
  409. package/src/live-call.ts +249 -0
  410. package/src/lock-routes.ts +625 -0
  411. package/src/lock-server.ts +233 -0
  412. package/src/logger.ts +120 -207
  413. package/src/login.e2e.test.ts +86 -0
  414. package/src/main.ts +238 -0
  415. package/src/markdown/components.ts +102 -0
  416. package/src/markdown/markdown.test.ts +414 -0
  417. package/src/markdown/render-markdown.ts +288 -0
  418. package/src/migrations.ts +198 -0
  419. package/src/onboarding-cli.e2e.test.ts +215 -0
  420. package/src/onboarding.e2e.test.ts +171 -0
  421. package/src/onboarding.ts +297 -0
  422. package/src/opencode-server.test.ts +24 -0
  423. package/src/opencode-server.ts +464 -0
  424. package/src/permissions.e2e.test.ts +191 -0
  425. package/src/permissions.ts +121 -0
  426. package/src/plugin/anthropic/index.test.ts +39 -0
  427. package/src/plugin/anthropic/index.ts +213 -0
  428. package/src/plugin/index.ts +112 -0
  429. package/src/plugin.e2e.test.ts +117 -0
  430. package/src/preference-commands.e2e.test.ts +597 -0
  431. package/src/project.ts +216 -0
  432. package/src/prompt.ts +664 -0
  433. package/src/questions.e2e.test.ts +328 -0
  434. package/src/questions.ts +322 -0
  435. package/src/queue.e2e.test.ts +253 -0
  436. package/src/queue.ts +158 -0
  437. package/src/remote-send.e2e.test.ts +91 -0
  438. package/src/remote-send.ts +110 -0
  439. package/src/renderer.e2e.test.ts +390 -0
  440. package/src/restart.e2e.test.ts +90 -0
  441. package/src/routes.test.ts +81 -0
  442. package/src/routes.ts +48 -0
  443. package/src/routines.e2e.test.ts +210 -0
  444. package/src/scheduler.e2e.test.ts +182 -0
  445. package/src/scheduler.test.ts +183 -0
  446. package/src/scheduler.ts +648 -0
  447. package/src/schema-sql.ts +3 -0
  448. package/src/schema.ts +95 -271
  449. package/src/session-commands.e2e.test.ts +326 -0
  450. package/src/session-events.ts +301 -0
  451. package/src/session-ops.e2e.test.ts +141 -0
  452. package/src/sessions.ts +804 -0
  453. package/src/shell.e2e.test.ts +173 -0
  454. package/src/slash-commands.test.ts +89 -0
  455. package/src/slash-commands.ts +540 -0
  456. package/src/sleep.e2e.test.ts +135 -552
  457. package/src/sleeps.ts +230 -0
  458. package/src/store.ts +19 -217
  459. package/src/system-prompt.ts +923 -0
  460. package/src/test/auth-plugin/index.ts +17 -0
  461. package/src/test/fake-mcp-server.ts +49 -0
  462. package/src/test/harness.ts +957 -0
  463. package/src/test/replay.ts +90 -0
  464. package/src/thread-reducer.fixtures.test.ts +374 -0
  465. package/src/thread-reducer.test.ts +380 -0
  466. package/src/thread-reducer.ts +671 -0
  467. package/src/tts.test.ts +150 -0
  468. package/src/voice-agent.ts +196 -0
  469. package/src/voice-attachments.e2e.test.ts +301 -0
  470. package/src/voice-call.e2e.test.ts +293 -0
  471. package/src/voice-call.ts +764 -0
  472. package/src/voice-openai.e2e.test.ts +191 -0
  473. package/src/voice.ts +668 -988
  474. package/src/working-directory.e2e.test.ts +226 -0
  475. package/src/worktrees.test.ts +89 -1115
  476. package/src/worktrees.ts +195 -957
  477. package/bin.js +0 -2
  478. package/dist/agent-model.e2e.test.js +0 -1223
  479. package/dist/ai-tool-to-genai.js +0 -233
  480. package/dist/ai-tool-to-genai.test.js +0 -267
  481. package/dist/ai-tool.js +0 -6
  482. package/dist/anthropic-account-identity.js +0 -70
  483. package/dist/anthropic-account-identity.test.js +0 -38
  484. package/dist/anthropic-auth-plugin.js +0 -958
  485. package/dist/anthropic-auth-plugin.test.js +0 -115
  486. package/dist/anthropic-auth-state.js +0 -186
  487. package/dist/anthropic-auth-state.test.js +0 -203
  488. package/dist/bash-tool-schema-plugin.js +0 -50
  489. package/dist/bash-tool-schema-plugin.test.js +0 -68
  490. package/dist/bin.js +0 -151
  491. package/dist/bot-lifecycle.test.js +0 -206
  492. package/dist/btw-prefix-detection.js +0 -24
  493. package/dist/btw-prefix-detection.test.js +0 -130
  494. package/dist/cache-drift-plugin.js +0 -183
  495. package/dist/cache-rewrite.js +0 -39
  496. package/dist/cache-rewrite.test.js +0 -41
  497. package/dist/channel-management.js +0 -347
  498. package/dist/cli-commands/bot.js +0 -254
  499. package/dist/cli-commands/maintenance.js +0 -171
  500. package/dist/cli-commands/misc.js +0 -153
  501. package/dist/cli-commands/project.js +0 -510
  502. package/dist/cli-commands/send.js +0 -659
  503. package/dist/cli-commands/session.js +0 -889
  504. package/dist/cli-commands/task.js +0 -277
  505. package/dist/cli-commands/thread.js +0 -63
  506. package/dist/cli-commands/user.js +0 -145
  507. package/dist/cli-parsing.test.js +0 -226
  508. package/dist/cli-runner.js +0 -1591
  509. package/dist/cli-runner.test.js +0 -119
  510. package/dist/cli-send-thread.e2e.test.js +0 -470
  511. package/dist/cloudflare-transcription.js +0 -56
  512. package/dist/commands/abort.js +0 -84
  513. package/dist/commands/action-buttons.js +0 -248
  514. package/dist/commands/add-project.js +0 -111
  515. package/dist/commands/agent.js +0 -658
  516. package/dist/commands/ask-question.js +0 -371
  517. package/dist/commands/ask-question.test.js +0 -150
  518. package/dist/commands/btw.js +0 -275
  519. package/dist/commands/compact.js +0 -117
  520. package/dist/commands/context-usage.js +0 -194
  521. package/dist/commands/context-usage.test.js +0 -49
  522. package/dist/commands/create-new-project.js +0 -138
  523. package/dist/commands/diff.js +0 -63
  524. package/dist/commands/file-upload.js +0 -280
  525. package/dist/commands/fork-subagent.js +0 -175
  526. package/dist/commands/fork.js +0 -291
  527. package/dist/commands/gemini-apikey.js +0 -123
  528. package/dist/commands/last-sessions.js +0 -129
  529. package/dist/commands/login.js +0 -893
  530. package/dist/commands/mcp.js +0 -239
  531. package/dist/commands/memory-snapshot.js +0 -24
  532. package/dist/commands/mention-mode.js +0 -42
  533. package/dist/commands/merge-worktree.js +0 -205
  534. package/dist/commands/model-variant.js +0 -390
  535. package/dist/commands/model.js +0 -1012
  536. package/dist/commands/multioauth.js +0 -425
  537. package/dist/commands/new-worktree.js +0 -625
  538. package/dist/commands/paginated-select.js +0 -57
  539. package/dist/commands/permissions.js +0 -327
  540. package/dist/commands/queue.js +0 -347
  541. package/dist/commands/queue.test.js +0 -15
  542. package/dist/commands/remove-project.js +0 -111
  543. package/dist/commands/restart-opencode-server.js +0 -133
  544. package/dist/commands/resume.js +0 -148
  545. package/dist/commands/run-command.js +0 -79
  546. package/dist/commands/screenshare.js +0 -307
  547. package/dist/commands/screenshare.test.js +0 -20
  548. package/dist/commands/session-id.js +0 -78
  549. package/dist/commands/session.js +0 -248
  550. package/dist/commands/share.js +0 -80
  551. package/dist/commands/tasks.js +0 -296
  552. package/dist/commands/types.js +0 -2
  553. package/dist/commands/undo-redo.js +0 -313
  554. package/dist/commands/unset-model.js +0 -116
  555. package/dist/commands/upgrade.js +0 -41
  556. package/dist/commands/user-command.js +0 -196
  557. package/dist/commands/verbosity.js +0 -124
  558. package/dist/commands/vscode.js +0 -295
  559. package/dist/commands/worktrees.js +0 -548
  560. package/dist/condense-memory.js +0 -33
  561. package/dist/config.js +0 -126
  562. package/dist/context-awareness-plugin.js +0 -357
  563. package/dist/context-awareness-plugin.test.js +0 -126
  564. package/dist/cpu-profiler.js +0 -211
  565. package/dist/cpu-profiler.test.js +0 -160
  566. package/dist/critique-utils.js +0 -95
  567. package/dist/database.js +0 -1074
  568. package/dist/debounce-timeout.js +0 -28
  569. package/dist/debounced-process-flush.js +0 -77
  570. package/dist/default-channel-provisioning.test.js +0 -149
  571. package/dist/discord-bot.js +0 -1432
  572. package/dist/discord-command-registration.js +0 -519
  573. package/dist/discord-command-registration.test.js +0 -17
  574. package/dist/discord-urls.js +0 -81
  575. package/dist/discord-utils.js +0 -789
  576. package/dist/discord-utils.test.js +0 -400
  577. package/dist/escape-backticks.test.js +0 -429
  578. package/dist/event-stream-real-capture.e2e.test.js +0 -533
  579. package/dist/eventsource-parser.test.js +0 -327
  580. package/dist/exec-async.js +0 -38
  581. package/dist/external-directory-permissions.e2e.test.js +0 -86
  582. package/dist/external-directory-project-deny.e2e.test.js +0 -70
  583. package/dist/external-opencode-sync.js +0 -620
  584. package/dist/external-opencode-sync.test.js +0 -206
  585. package/dist/extract-sdk-error-message.test.js +0 -13
  586. package/dist/format-tables.js +0 -728
  587. package/dist/format-tables.test.js +0 -915
  588. package/dist/forum-sync/config.js +0 -79
  589. package/dist/forum-sync/discord-operations.js +0 -157
  590. package/dist/forum-sync/index.js +0 -5
  591. package/dist/forum-sync/markdown.js +0 -110
  592. package/dist/forum-sync/sync-to-discord.js +0 -417
  593. package/dist/forum-sync/sync-to-files.js +0 -190
  594. package/dist/forum-sync/types.js +0 -53
  595. package/dist/forum-sync/watchers.js +0 -322
  596. package/dist/gateway-proxy-reconnect.e2e.test.js +0 -414
  597. package/dist/gateway-proxy.e2e.test.js +0 -488
  598. package/dist/genai-worker-wrapper.js +0 -111
  599. package/dist/genai-worker.js +0 -309
  600. package/dist/genai.js +0 -232
  601. package/dist/git-worktree-core.js +0 -461
  602. package/dist/guild-categories.test.js +0 -225
  603. package/dist/heap-monitor.js +0 -122
  604. package/dist/hrana-server.js +0 -322
  605. package/dist/hrana-server.test.js +0 -99
  606. package/dist/html-actions.js +0 -123
  607. package/dist/html-actions.test.js +0 -70
  608. package/dist/html-components.js +0 -115
  609. package/dist/html-components.test.js +0 -34
  610. package/dist/image-optimizer-plugin.js +0 -153
  611. package/dist/image-utils.js +0 -112
  612. package/dist/interaction-handler.js +0 -578
  613. package/dist/ipc-polling.js +0 -245
  614. package/dist/ipc-tools-plugin.js +0 -251
  615. package/dist/kimaki-digital-twin.e2e.test.js +0 -161
  616. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +0 -158
  617. package/dist/kimaki-opencode-plugin.js +0 -33
  618. package/dist/kimaki-opencode-plugin.test.js +0 -126
  619. package/dist/kimaki-workspace-adaptor.js +0 -118
  620. package/dist/limit-heading-depth.js +0 -25
  621. package/dist/limit-heading-depth.test.js +0 -105
  622. package/dist/list-channel-threads.js +0 -163
  623. package/dist/list-channel-threads.test.js +0 -80
  624. package/dist/markdown.js +0 -455
  625. package/dist/markdown.test.js +0 -411
  626. package/dist/memory-overview-plugin.js +0 -123
  627. package/dist/message-formatting.js +0 -788
  628. package/dist/message-formatting.test.js +0 -1132
  629. package/dist/message-preprocessing.js +0 -419
  630. package/dist/oauth-rotation-shared.js +0 -253
  631. package/dist/onboarding-tutorial.js +0 -163
  632. package/dist/onboarding-welcome.js +0 -37
  633. package/dist/openai-auth-plugin.js +0 -125
  634. package/dist/openai-auth-state.js +0 -237
  635. package/dist/openai-realtime.js +0 -224
  636. package/dist/opencode-command-detection.js +0 -65
  637. package/dist/opencode-command-detection.test.js +0 -240
  638. package/dist/opencode-command.js +0 -181
  639. package/dist/opencode-command.test.js +0 -218
  640. package/dist/opencode-interrupt-plugin.js +0 -253
  641. package/dist/opencode-interrupt-plugin.test.js +0 -293
  642. package/dist/opencode.js +0 -1217
  643. package/dist/parse-permission-rules.test.js +0 -117
  644. package/dist/patch-text-parser.js +0 -97
  645. package/dist/plugin-logger.js +0 -105
  646. package/dist/plugin-opencode-client.js +0 -54
  647. package/dist/privacy-sanitizer.js +0 -105
  648. package/dist/project-add.test.js +0 -62
  649. package/dist/question-abort-clears-dropdown.e2e.test.js +0 -89
  650. package/dist/question-abort-resume.e2e.test.js +0 -100
  651. package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +0 -150
  652. package/dist/question-text-before-dropdown.e2e.test.js +0 -52
  653. package/dist/queue-advanced-abort.e2e.test.js +0 -348
  654. package/dist/queue-advanced-action-buttons.e2e.test.js +0 -209
  655. package/dist/queue-advanced-e2e-setup.js +0 -1043
  656. package/dist/queue-advanced-footer.e2e.test.js +0 -286
  657. package/dist/queue-advanced-model-switch.e2e.test.js +0 -297
  658. package/dist/queue-advanced-permissions-typing.e2e.test.js +0 -189
  659. package/dist/queue-advanced-question.e2e.test.js +0 -266
  660. package/dist/queue-advanced-typing-interrupt.e2e.test.js +0 -113
  661. package/dist/queue-advanced-typing.e2e.test.js +0 -151
  662. package/dist/queue-delete-message.e2e.test.js +0 -216
  663. package/dist/queue-drain-after-interactive-ui.e2e.test.js +0 -120
  664. package/dist/queue-interrupt-drain.e2e.test.js +0 -256
  665. package/dist/queue-question-select-drain.e2e.test.js +0 -162
  666. package/dist/runtime-idle-sweeper.js +0 -52
  667. package/dist/runtime-lifecycle.e2e.test.js +0 -570
  668. package/dist/sentry.js +0 -23
  669. package/dist/session-handler/agent-utils.js +0 -65
  670. package/dist/session-handler/event-stream-state.js +0 -1225
  671. package/dist/session-handler/event-stream-state.test.js +0 -3160
  672. package/dist/session-handler/global-event-listener.js +0 -212
  673. package/dist/session-handler/model-utils.js +0 -311
  674. package/dist/session-handler/model-utils.test.js +0 -445
  675. package/dist/session-handler/opencode-session-event-log.js +0 -121
  676. package/dist/session-handler/thread-ingress.test.js +0 -41
  677. package/dist/session-handler/thread-runtime-state.js +0 -209
  678. package/dist/session-handler/thread-session-runtime-summary.test.js +0 -121
  679. package/dist/session-handler/thread-session-runtime.js +0 -4320
  680. package/dist/session-handler.js +0 -9
  681. package/dist/session-search.js +0 -219
  682. package/dist/session-search.test.js +0 -287
  683. package/dist/session-sleep.test.js +0 -224
  684. package/dist/session-title-rename.test.js +0 -128
  685. package/dist/skill-filter.js +0 -43
  686. package/dist/skill-filter.test.js +0 -86
  687. package/dist/startup-service.js +0 -153
  688. package/dist/startup-time.e2e.test.js +0 -297
  689. package/dist/system-message.js +0 -806
  690. package/dist/system-message.test.js +0 -917
  691. package/dist/task-id-plugin.js +0 -16
  692. package/dist/task-runner.js +0 -554
  693. package/dist/task-schedule.js +0 -298
  694. package/dist/task-schedule.test.js +0 -366
  695. package/dist/test-utils.js +0 -385
  696. package/dist/thinking-utils.js +0 -45
  697. package/dist/thinking-utils.test.js +0 -35
  698. package/dist/thread-message-queue.e2e.test.js +0 -1227
  699. package/dist/tools.js +0 -357
  700. package/dist/undo-redo.e2e.test.js +0 -171
  701. package/dist/unnest-code-blocks.js +0 -146
  702. package/dist/unnest-code-blocks.test.js +0 -673
  703. package/dist/upgrade.js +0 -114
  704. package/dist/utils.js +0 -148
  705. package/dist/voice-attachment.js +0 -56
  706. package/dist/voice-handler.js +0 -727
  707. package/dist/voice-message.e2e.test.js +0 -1254
  708. package/dist/voice-routing-input.e2e.test.js +0 -195
  709. package/dist/voice-routing-question.e2e.test.js +0 -182
  710. package/dist/voice.test.js +0 -500
  711. package/dist/wait-session.js +0 -193
  712. package/dist/websockify.js +0 -69
  713. package/dist/worker-types.js +0 -4
  714. package/dist/worktree-clone-isolation.e2e.test.js +0 -188
  715. package/dist/worktree-lifecycle.e2e.test.js +0 -735
  716. package/dist/worktree-utils.js +0 -3
  717. package/dist/xai-auth-plugin.js +0 -238
  718. package/dist/xai-auth-state.js +0 -234
  719. package/dist/xai-auth-state.test.js +0 -183
  720. package/dist/xml.js +0 -92
  721. package/dist/xml.test.js +0 -32
  722. package/skills/bump-holocron-consumers/SKILL.md +0 -137
  723. package/skills/critique/SKILL.md +0 -116
  724. package/skills/egaki/SKILL.md +0 -86
  725. package/skills/errore/SKILL.md +0 -501
  726. package/skills/goke/SKILL.md +0 -695
  727. package/skills/holocron/SKILL.md +0 -942
  728. package/skills/lintcn/SKILL.md +0 -873
  729. package/skills/new-skill/SKILL.md +0 -339
  730. package/skills/npm-package/SKILL.md +0 -692
  731. package/skills/opensrc/SKILL.md +0 -102
  732. package/skills/playwriter/SKILL.md +0 -35
  733. package/skills/profano/SKILL.md +0 -33
  734. package/skills/readme.md +0 -20
  735. package/skills/sigillo/SKILL.md +0 -370
  736. package/skills/spiceflow/SKILL.md +0 -80
  737. package/skills/strada/SKILL.md +0 -104
  738. package/skills/termcast/SKILL.md +0 -133
  739. package/skills/tuistory/SKILL.md +0 -237
  740. package/skills/usecomputer/SKILL.md +0 -264
  741. package/skills/zele/SKILL.md +0 -81
  742. package/src/agent-model.e2e.test.ts +0 -1557
  743. package/src/ai-tool-to-genai.test.ts +0 -296
  744. package/src/ai-tool-to-genai.ts +0 -283
  745. package/src/ai-tool.ts +0 -39
  746. package/src/anthropic-account-identity.test.ts +0 -52
  747. package/src/anthropic-account-identity.ts +0 -96
  748. package/src/anthropic-auth-plugin.test.ts +0 -135
  749. package/src/anthropic-auth-plugin.ts +0 -1180
  750. package/src/anthropic-auth-state.test.ts +0 -259
  751. package/src/anthropic-auth-state.ts +0 -272
  752. package/src/bash-tool-schema-plugin.test.ts +0 -84
  753. package/src/bash-tool-schema-plugin.ts +0 -77
  754. package/src/bin.ts +0 -173
  755. package/src/bot-lifecycle.test.ts +0 -252
  756. package/src/btw-prefix-detection.test.ts +0 -142
  757. package/src/btw-prefix-detection.ts +0 -30
  758. package/src/cache-drift-plugin.ts +0 -249
  759. package/src/cache-rewrite.test.ts +0 -49
  760. package/src/cache-rewrite.ts +0 -96
  761. package/src/channel-management.ts +0 -494
  762. package/src/cli-commands/bot.ts +0 -374
  763. package/src/cli-commands/maintenance.ts +0 -237
  764. package/src/cli-commands/misc.ts +0 -208
  765. package/src/cli-commands/project.ts +0 -680
  766. package/src/cli-commands/send.ts +0 -885
  767. package/src/cli-commands/session.ts +0 -1181
  768. package/src/cli-commands/task.ts +0 -358
  769. package/src/cli-commands/thread.ts +0 -86
  770. package/src/cli-commands/user.ts +0 -195
  771. package/src/cli-parsing.test.ts +0 -305
  772. package/src/cli-runner.test.ts +0 -166
  773. package/src/cli-runner.ts +0 -2157
  774. package/src/cli-send-thread.e2e.test.ts +0 -609
  775. package/src/cloudflare-transcription.ts +0 -76
  776. package/src/commands/abort.ts +0 -113
  777. package/src/commands/action-buttons.ts +0 -371
  778. package/src/commands/add-project.ts +0 -146
  779. package/src/commands/agent.ts +0 -911
  780. package/src/commands/ask-question.test.ts +0 -177
  781. package/src/commands/ask-question.ts +0 -532
  782. package/src/commands/btw.ts +0 -356
  783. package/src/commands/compact.ts +0 -154
  784. package/src/commands/context-usage.test.ts +0 -57
  785. package/src/commands/context-usage.ts +0 -267
  786. package/src/commands/create-new-project.ts +0 -203
  787. package/src/commands/diff.ts +0 -91
  788. package/src/commands/file-upload.ts +0 -398
  789. package/src/commands/fork-subagent.ts +0 -262
  790. package/src/commands/fork.ts +0 -421
  791. package/src/commands/gemini-apikey.ts +0 -205
  792. package/src/commands/last-sessions.ts +0 -176
  793. package/src/commands/login.ts +0 -1182
  794. package/src/commands/mcp.ts +0 -307
  795. package/src/commands/memory-snapshot.ts +0 -30
  796. package/src/commands/mention-mode.ts +0 -66
  797. package/src/commands/merge-worktree.ts +0 -282
  798. package/src/commands/model-variant.ts +0 -522
  799. package/src/commands/model.ts +0 -1377
  800. package/src/commands/multioauth.ts +0 -508
  801. package/src/commands/new-worktree.ts +0 -835
  802. package/src/commands/paginated-select.ts +0 -81
  803. package/src/commands/permissions.ts +0 -458
  804. package/src/commands/queue.test.ts +0 -19
  805. package/src/commands/queue.ts +0 -471
  806. package/src/commands/remove-project.ts +0 -152
  807. package/src/commands/restart-opencode-server.ts +0 -173
  808. package/src/commands/resume.ts +0 -229
  809. package/src/commands/run-command.ts +0 -123
  810. package/src/commands/screenshare.test.ts +0 -30
  811. package/src/commands/screenshare.ts +0 -388
  812. package/src/commands/session-id.ts +0 -109
  813. package/src/commands/session.ts +0 -319
  814. package/src/commands/share.ts +0 -106
  815. package/src/commands/tasks.ts +0 -399
  816. package/src/commands/types.ts +0 -25
  817. package/src/commands/undo-redo.ts +0 -394
  818. package/src/commands/unset-model.ts +0 -152
  819. package/src/commands/upgrade.ts +0 -51
  820. package/src/commands/user-command.ts +0 -260
  821. package/src/commands/verbosity.ts +0 -172
  822. package/src/commands/vscode.ts +0 -378
  823. package/src/commands/worktrees.ts +0 -762
  824. package/src/condense-memory.ts +0 -36
  825. package/src/config.ts +0 -147
  826. package/src/context-awareness-plugin.test.ts +0 -144
  827. package/src/context-awareness-plugin.ts +0 -508
  828. package/src/cpu-profiler.test.ts +0 -183
  829. package/src/cpu-profiler.ts +0 -293
  830. package/src/critique-utils.ts +0 -139
  831. package/src/database.ts +0 -1484
  832. package/src/debounce-timeout.ts +0 -43
  833. package/src/debounced-process-flush.ts +0 -104
  834. package/src/default-channel-provisioning.test.ts +0 -156
  835. package/src/discord-bot.ts +0 -1852
  836. package/src/discord-command-registration.test.ts +0 -20
  837. package/src/discord-command-registration.ts +0 -704
  838. package/src/discord-urls.ts +0 -88
  839. package/src/discord-utils.test.ts +0 -470
  840. package/src/discord-utils.ts +0 -1079
  841. package/src/escape-backticks.test.ts +0 -469
  842. package/src/event-stream-real-capture.e2e.test.ts +0 -692
  843. package/src/eventsource-parser.test.ts +0 -351
  844. package/src/exec-async.ts +0 -44
  845. package/src/external-directory-permissions.e2e.test.ts +0 -103
  846. package/src/external-directory-project-deny.e2e.test.ts +0 -84
  847. package/src/external-opencode-sync.test.ts +0 -248
  848. package/src/external-opencode-sync.ts +0 -867
  849. package/src/extract-sdk-error-message.test.ts +0 -18
  850. package/src/format-tables.test.ts +0 -988
  851. package/src/format-tables.ts +0 -1031
  852. package/src/forum-sync/config.ts +0 -92
  853. package/src/forum-sync/discord-operations.ts +0 -245
  854. package/src/forum-sync/index.ts +0 -9
  855. package/src/forum-sync/markdown.ts +0 -172
  856. package/src/forum-sync/sync-to-discord.ts +0 -595
  857. package/src/forum-sync/sync-to-files.ts +0 -294
  858. package/src/forum-sync/types.ts +0 -175
  859. package/src/forum-sync/watchers.ts +0 -474
  860. package/src/gateway-proxy-reconnect.e2e.test.ts +0 -553
  861. package/src/gateway-proxy.e2e.test.ts +0 -648
  862. package/src/genai-worker-wrapper.ts +0 -164
  863. package/src/genai-worker.ts +0 -385
  864. package/src/genai.ts +0 -318
  865. package/src/git-worktree-core.ts +0 -616
  866. package/src/guild-categories.test.ts +0 -260
  867. package/src/heap-monitor.ts +0 -152
  868. package/src/hrana-server.test.ts +0 -130
  869. package/src/hrana-server.ts +0 -399
  870. package/src/html-actions.test.ts +0 -87
  871. package/src/html-actions.ts +0 -174
  872. package/src/html-components.test.ts +0 -38
  873. package/src/html-components.ts +0 -177
  874. package/src/image-optimizer-plugin.ts +0 -194
  875. package/src/image-utils.ts +0 -150
  876. package/src/interaction-handler.ts +0 -775
  877. package/src/ipc-polling.ts +0 -325
  878. package/src/ipc-tools-plugin.ts +0 -306
  879. package/src/kimaki-digital-twin.e2e.test.ts +0 -199
  880. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +0 -208
  881. package/src/kimaki-opencode-plugin.test.ts +0 -142
  882. package/src/kimaki-opencode-plugin.ts +0 -34
  883. package/src/kimaki-workspace-adaptor.ts +0 -144
  884. package/src/limit-heading-depth.test.ts +0 -116
  885. package/src/limit-heading-depth.ts +0 -26
  886. package/src/list-channel-threads.test.ts +0 -90
  887. package/src/list-channel-threads.ts +0 -252
  888. package/src/markdown.test.ts +0 -498
  889. package/src/markdown.ts +0 -577
  890. package/src/memory-overview-plugin.ts +0 -159
  891. package/src/message-formatting.test.ts +0 -1401
  892. package/src/message-formatting.ts +0 -999
  893. package/src/message-preprocessing.ts +0 -557
  894. package/src/oauth-rotation-shared.ts +0 -332
  895. package/src/onboarding-tutorial.ts +0 -167
  896. package/src/onboarding-welcome.ts +0 -49
  897. package/src/openai-auth-plugin.ts +0 -158
  898. package/src/openai-auth-state.ts +0 -297
  899. package/src/openai-realtime.ts +0 -358
  900. package/src/opencode-command-detection.test.ts +0 -307
  901. package/src/opencode-command-detection.ts +0 -76
  902. package/src/opencode-command.test.ts +0 -290
  903. package/src/opencode-command.ts +0 -251
  904. package/src/opencode-interrupt-plugin.test.ts +0 -419
  905. package/src/opencode-interrupt-plugin.ts +0 -306
  906. package/src/opencode.ts +0 -1590
  907. package/src/parse-permission-rules.test.ts +0 -127
  908. package/src/patch-text-parser.ts +0 -107
  909. package/src/plugin-logger.ts +0 -139
  910. package/src/plugin-opencode-client.ts +0 -71
  911. package/src/privacy-sanitizer.ts +0 -142
  912. package/src/project-add.test.ts +0 -80
  913. package/src/question-abort-clears-dropdown.e2e.test.ts +0 -109
  914. package/src/question-abort-resume.e2e.test.ts +0 -121
  915. package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +0 -172
  916. package/src/question-text-before-dropdown.e2e.test.ts +0 -67
  917. package/src/queue-advanced-abort.e2e.test.ts +0 -449
  918. package/src/queue-advanced-action-buttons.e2e.test.ts +0 -271
  919. package/src/queue-advanced-e2e-setup.ts +0 -1164
  920. package/src/queue-advanced-footer.e2e.test.ts +0 -357
  921. package/src/queue-advanced-model-switch.e2e.test.ts +0 -382
  922. package/src/queue-advanced-permissions-typing.e2e.test.ts +0 -256
  923. package/src/queue-advanced-question.e2e.test.ts +0 -322
  924. package/src/queue-advanced-typing-interrupt.e2e.test.ts +0 -147
  925. package/src/queue-advanced-typing.e2e.test.ts +0 -199
  926. package/src/queue-delete-message.e2e.test.ts +0 -262
  927. package/src/queue-drain-after-interactive-ui.e2e.test.ts +0 -152
  928. package/src/queue-interrupt-drain.e2e.test.ts +0 -299
  929. package/src/queue-question-select-drain.e2e.test.ts +0 -209
  930. package/src/runtime-idle-sweeper.ts +0 -76
  931. package/src/runtime-lifecycle.e2e.test.ts +0 -714
  932. package/src/schema.sql +0 -251
  933. package/src/sentry.ts +0 -26
  934. package/src/session-handler/agent-utils.ts +0 -104
  935. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +0 -45
  936. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +0 -40
  937. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +0 -23
  938. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +0 -22
  939. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +0 -277
  940. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +0 -46
  941. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +0 -21
  942. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +0 -56
  943. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +0 -44
  944. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +0 -29
  945. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +0 -29
  946. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +0 -50
  947. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +0 -59
  948. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +0 -52
  949. package/src/session-handler/event-stream-state.test.ts +0 -3431
  950. package/src/session-handler/event-stream-state.ts +0 -1792
  951. package/src/session-handler/global-event-listener.ts +0 -258
  952. package/src/session-handler/model-utils.test.ts +0 -537
  953. package/src/session-handler/model-utils.ts +0 -489
  954. package/src/session-handler/opencode-session-event-log.ts +0 -159
  955. package/src/session-handler/thread-ingress.test.ts +0 -54
  956. package/src/session-handler/thread-runtime-state.ts +0 -358
  957. package/src/session-handler/thread-session-runtime-summary.test.ts +0 -169
  958. package/src/session-handler/thread-session-runtime.ts +0 -5696
  959. package/src/session-handler.ts +0 -15
  960. package/src/session-search.test.ts +0 -331
  961. package/src/session-search.ts +0 -382
  962. package/src/session-sleep.test.ts +0 -294
  963. package/src/session-title-rename.test.ts +0 -186
  964. package/src/skill-filter.test.ts +0 -110
  965. package/src/skill-filter.ts +0 -63
  966. package/src/startup-service.ts +0 -200
  967. package/src/startup-time.e2e.test.ts +0 -377
  968. package/src/system-message.test.ts +0 -1013
  969. package/src/system-message.ts +0 -1043
  970. package/src/task-id-plugin.ts +0 -17
  971. package/src/task-runner.ts +0 -768
  972. package/src/task-schedule.test.ts +0 -424
  973. package/src/task-schedule.ts +0 -470
  974. package/src/test-utils.ts +0 -549
  975. package/src/thinking-utils.test.ts +0 -46
  976. package/src/thinking-utils.ts +0 -82
  977. package/src/thread-message-queue.e2e.test.ts +0 -1468
  978. package/src/tools.ts +0 -430
  979. package/src/undici.d.ts +0 -12
  980. package/src/undo-redo.e2e.test.ts +0 -219
  981. package/src/unnest-code-blocks.test.ts +0 -713
  982. package/src/unnest-code-blocks.ts +0 -185
  983. package/src/upgrade.ts +0 -127
  984. package/src/utils.ts +0 -216
  985. package/src/voice-attachment.ts +0 -79
  986. package/src/voice-handler.ts +0 -1000
  987. package/src/voice-message.e2e.test.ts +0 -1513
  988. package/src/voice-routing-input.e2e.test.ts +0 -203
  989. package/src/voice-routing-question.e2e.test.ts +0 -188
  990. package/src/voice.test.ts +0 -565
  991. package/src/wait-session.ts +0 -291
  992. package/src/websockify.ts +0 -101
  993. package/src/worker-types.ts +0 -64
  994. package/src/worktree-clone-isolation.e2e.test.ts +0 -213
  995. package/src/worktree-lifecycle.e2e.test.ts +0 -900
  996. package/src/worktree-utils.ts +0 -4
  997. package/src/xai-auth-plugin.ts +0 -303
  998. package/src/xai-auth-state.test.ts +0 -224
  999. package/src/xai-auth-state.ts +0 -294
  1000. package/src/xml.test.ts +0 -38
  1001. package/src/xml.ts +0 -121
package/src/cli-runner.ts DELETED
@@ -1,2157 +0,0 @@
1
- // Runtime startup and shared helpers for the Kimaki goke CLI.
2
- // Keeps cli.ts focused on command composition while preserving the bot onboarding flow.
3
- import {
4
- intro,
5
- outro,
6
- text,
7
- password,
8
- note,
9
- cancel,
10
- isCancel,
11
- confirm,
12
- log,
13
- multiselect,
14
- select,
15
- spinner,
16
- } from '@clack/prompts'
17
- import {
18
- deduplicateByKey,
19
- generateBotInstallUrl,
20
- generateDiscordInstallUrlForBot,
21
- KIMAKI_GATEWAY_APP_ID,
22
- KIMAKI_WEBSITE_URL,
23
- abbreviatePath,
24
- } from './utils.js'
25
- import {
26
- getChannelsWithDescriptions,
27
- createDiscordClient,
28
- initDatabase,
29
- getChannelDirectory,
30
- startDiscordBot,
31
- registerBotLifecycleHandlers,
32
- initializeOpencodeForDirectory,
33
- assertCompatibleOpencodeVersion,
34
- createProjectChannels,
35
- createDefaultKimakiChannel,
36
- type ChannelWithTags,
37
- } from './discord-bot.js'
38
- import {
39
- getBotTokenWithMode,
40
- ensureServiceAuthToken,
41
- setBotToken,
42
- setBotMode,
43
- setChannelDirectory,
44
- findChannelsByDirectory,
45
- getDb,
46
- } from './database.js'
47
- import * as orm from 'drizzle-orm'
48
- import * as dbSchema from './schema.js'
49
- import { selectResolvedCommand } from './opencode-command.js'
50
- import {
51
- Events,
52
- ChannelType,
53
- type Guild,
54
- type REST,
55
- Routes,
56
- AttachmentBuilder,
57
- } from 'discord.js'
58
- import { discordApiUrl, getDiscordRestApiUrl, getGatewayProxyRestBaseUrl, getInternetReachableBaseUrl } from './discord-urls.js'
59
- import { extractBtwQueueSuffix } from './btw-prefix-detection.js'
60
- import crypto from 'node:crypto'
61
- import path from 'node:path'
62
- import fs from 'node:fs'
63
- import { spawn } from 'node:child_process'
64
- import { createLogger, LogPrefix } from './logger.js'
65
- import { notifyError } from './sentry.js'
66
- import {
67
- uploadFilesToDiscord,
68
- stripMentions,
69
- isThreadChannelType,
70
- } from './discord-utils.js'
71
- import { setDataDir, getDataDir } from './config.js'
72
- import { execAsync } from './worktrees.js'
73
- import { backgroundUpgradeKimaki } from './upgrade.js'
74
- import { initAnalytics, setAnalyticsBotMode } from './analytics.js'
75
- import { sendWelcomeMessage } from './onboarding-welcome.js'
76
- import { startHranaServer } from './hrana-server.js'
77
- import { startIpcPolling, stopIpcPolling } from './ipc-polling.js'
78
- import { type ParsedSendAt } from './task-schedule.js'
79
- import { store } from './store.js'
80
- import { registerCommands, SKIP_USER_COMMANDS } from './discord-command-registration.js'
81
-
82
- export const cliLogger = createLogger(LogPrefix.CLI)
83
-
84
- // Gateway bot mode constants.
85
- // KIMAKI_GATEWAY_APP_ID is the Discord Application ID of the gateway bot.
86
- // KIMAKI_WEBSITE_URL is the website that handles OAuth callback + onboarding status.
87
- // KIMAKI_GATEWAY_PROXY_URL is the gateway-proxy base URL.
88
- // We derive REST base from this URL by swapping ws/wss to http/https.
89
- // These are hardcoded because they're deploy-time constants for the gateway infrastructure.
90
- export const KIMAKI_GATEWAY_PROXY_URL =
91
- process.env.KIMAKI_GATEWAY_PROXY_URL ||
92
- 'wss://discord-gateway.kimaki.dev'
93
-
94
- export const KIMAKI_GATEWAY_PROXY_REST_BASE_URL = getGatewayProxyRestBaseUrl({
95
- gatewayUrl: KIMAKI_GATEWAY_PROXY_URL,
96
- })
97
-
98
- export type OpenUrlCommand = {
99
- command: string
100
- args: string[]
101
- }
102
-
103
- export function getOpenUrlCommand(
104
- url: string,
105
- platform: NodeJS.Platform = process.platform,
106
- ): OpenUrlCommand {
107
- if (platform === 'darwin') {
108
- return { command: 'open', args: [url] }
109
- }
110
- if (platform === 'win32') {
111
- return { command: 'rundll32.exe', args: ['url.dll,FileProtocolHandler', url] }
112
- }
113
- return { command: 'xdg-open', args: [url] }
114
- }
115
-
116
- function openUrlInDefaultBrowser(url: string): void {
117
- const { command, args } = getOpenUrlCommand(url)
118
- const child = spawn(command, args, {
119
- detached: true,
120
- stdio: 'ignore',
121
- windowsHide: true,
122
- })
123
- child.on('error', (error) => {
124
- cliLogger.warn(
125
- 'Failed to open install URL:',
126
- error instanceof Error ? error.message : String(error),
127
- )
128
- })
129
- child.unref()
130
- }
131
-
132
- // Strip bracketed paste escape sequences from terminal input.
133
- // iTerm2 and other terminals wrap pasted content with \x1b[200~ and \x1b[201~
134
- // which can cause validation to fail on macOS. See: https://github.com/remorses/kimaki/issues/18
135
- export function stripBracketedPaste(value: string | undefined): string {
136
- if (!value) {
137
- return ''
138
- }
139
- return value
140
- .replace(/\x1b\[200~/g, '')
141
- .replace(/\x1b\[201~/g, '')
142
- .trim()
143
- }
144
-
145
-
146
- // Derive the Discord Application ID from a bot token.
147
- // Discord bot tokens have the format: base64(userId).timestamp.hmac
148
- // The first segment is the bot's user ID (= Application ID) base64-encoded.
149
- // For gateway mode tokens (client_id:secret format), this function returns
150
- // undefined -- the caller should use KIMAKI_GATEWAY_APP_ID instead.
151
- export function appIdFromToken(token: string): string | undefined {
152
- // Gateway mode tokens use "client_id:secret" format, not base64.
153
- if (token.includes(':')) {
154
- return undefined
155
- }
156
- const segment = token.split('.')[0]
157
- if (!segment) {
158
- return undefined
159
- }
160
- try {
161
- const decoded = Buffer.from(segment, 'base64').toString('utf8')
162
- if (/^\d{17,20}$/.test(decoded)) {
163
- return decoded
164
- }
165
- return undefined
166
- } catch {
167
- return undefined
168
- }
169
- }
170
-
171
- // Resolve bot token and app ID from env var or database.
172
- // Used by CLI subcommands (send, project add) that need credentials
173
- // but don't run the interactive wizard.
174
- // In gateway mode, also sets store.discordBaseUrl so REST calls
175
- // are routed through the gateway-proxy REST endpoint.
176
- //
177
- // Priority: KIMAKI_BOT_TOKEN env var takes precedence over saved DB
178
- // credentials. This lets CI and cross-instance commands override the
179
- // local bot identity. If the env token looks like a gateway credential
180
- // (clientId:clientSecret), the gateway proxy REST URL is set automatically.
181
- export async function resolveBotCredentials({ appIdOverride }: { appIdOverride?: string } = {}): Promise<{
182
- token: string
183
- appId: string | undefined
184
- }> {
185
- const envToken = process.env.KIMAKI_BOT_TOKEN
186
- if (envToken) {
187
- const isGatewayToken = envToken.includes(':')
188
- if (isGatewayToken) {
189
- // Gateway tokens need REST calls routed through the proxy, not discord.com
190
- store.setState({ discordBaseUrl: KIMAKI_GATEWAY_PROXY_REST_BASE_URL })
191
- }
192
- const appId = appIdOverride || appIdFromToken(envToken)
193
- return { token: envToken, appId }
194
- }
195
-
196
- // Fall back to saved credentials in the database
197
- const botRow = await getBotTokenWithMode().catch((e) => {
198
- cliLogger.error('Database error:', e instanceof Error ? e.message : String(e))
199
- return null
200
- })
201
- if (botRow) {
202
- return { token: botRow.token, appId: appIdOverride || botRow.appId }
203
- }
204
-
205
- cliLogger.error('No bot token found. Set KIMAKI_BOT_TOKEN env var or run `kimaki` first to set up.')
206
- process.exit(EXIT_NO_RESTART)
207
- }
208
-
209
- export { isThreadChannelType }
210
-
211
- /** Wrap long lines so prompt.md is readable in Discord's attachment preview. */
212
- function wrapPromptAttachmentText(prompt: string): string {
213
- return prompt
214
- .split('\n')
215
- .flatMap((line) => {
216
- if (line.length <= 120) {
217
- return [line]
218
- }
219
- const wrapped: string[] = []
220
- let remaining = line
221
- const maxCol = 120
222
- // Only soft-break at a space if it's reasonably close to maxCol,
223
- // otherwise hard-break to avoid tiny fragments from early spaces
224
- const minSoftBreak = 90
225
- while (remaining.length > maxCol) {
226
- const lastSpace = remaining.lastIndexOf(' ', maxCol)
227
- const useSoftBreak = lastSpace >= minSoftBreak
228
- const breakAt = useSoftBreak ? lastSpace : maxCol
229
- wrapped.push(remaining.slice(0, breakAt))
230
- // Only consume the separator space on soft breaks
231
- remaining = useSoftBreak
232
- ? remaining.slice(breakAt + 1)
233
- : remaining.slice(breakAt)
234
- }
235
- if (remaining.length > 0) {
236
- wrapped.push(remaining)
237
- }
238
- return wrapped
239
- })
240
- .join('\n')
241
- }
242
-
243
- /**
244
- * Visible message content + prompt.md text for prompts over Discord's limit.
245
- * The bot reads queue/btw suffixes only from message content, so the suffix
246
- * moves from the file to the visible message and the send still queues or forks.
247
- */
248
- export function buildLongPromptMessage(prompt: string): {
249
- content: string
250
- fileText: string
251
- } {
252
- const { prompt: fileText, forceQueue, forceBtw } = extractBtwQueueSuffix(prompt)
253
- const preview = fileText.slice(0, 100).replace(/\n/g, ' ')
254
- const summary = `Prompt attached as file (${fileText.length} chars)\n\n> ${preview}…`
255
- const suffix = [forceBtw && 'btw', forceQueue && 'queue'].filter(Boolean).join(' ')
256
- return { content: suffix ? `${summary}\n\n${suffix}` : summary, fileText }
257
- }
258
-
259
- function promptAttachmentBlob(text: string) {
260
- return new Blob([new Uint8Array(Buffer.from(text, 'utf8'))], {
261
- type: 'text/markdown',
262
- })
263
- }
264
-
265
- export async function sendDiscordMessageWithOptionalAttachment({
266
- channelId,
267
- prompt,
268
- botToken,
269
- embeds,
270
- rest,
271
- splitInsteadOfAttach,
272
- files,
273
- }: {
274
- channelId: string
275
- prompt: string
276
- botToken: string
277
- embeds?: Array<{ color: number; footer: { text: string } }>
278
- /** Only `post` is used (short prompt / split paths). Accept a narrow shape so tests need no cast. */
279
- rest: Pick<REST, 'post'>
280
- /** When true, long messages are split into multiple Discord messages instead of
281
- * being attached as a file. Useful for notify-only messages where the content
282
- * should be directly visible in the channel. */
283
- splitInsteadOfAttach?: boolean
284
- /** Local file paths to attach to the message (images, text files, etc.). */
285
- files?: string[]
286
- }): Promise<{ id: string }> {
287
- const discordMaxLength = 2000
288
-
289
- // When files are provided, always use multipart FormData upload
290
- if (files?.length) {
291
- const { DISCORD_DEFAULT_MAX_FILE_SIZE } = await import('./discord-utils.js')
292
- const { default: mime } = await import('mime')
293
-
294
- for (const file of files) {
295
- const stat = fs.statSync(file)
296
- if (stat.size > DISCORD_DEFAULT_MAX_FILE_SIZE) {
297
- const fileMB = (stat.size / 1024 / 1024).toFixed(1)
298
- const limitMB = (DISCORD_DEFAULT_MAX_FILE_SIZE / 1024 / 1024).toFixed(0)
299
- throw new Error(
300
- `File "${path.basename(file)}" is ${fileMB} MB, which exceeds Discord's ${limitMB} MB upload limit`,
301
- )
302
- }
303
- }
304
-
305
- // When prompt exceeds Discord's limit, attach it as prompt.md alongside
306
- // user files so nothing is silently lost. Build prompt.md from memory so
307
- // parallel kimaki send processes never share or unlink a temp path.
308
- const longPrompt = prompt.length > discordMaxLength
309
- ? buildLongPromptMessage(prompt)
310
- : undefined
311
- const content = longPrompt ? longPrompt.content : prompt
312
-
313
- const allFiles: Array<{ data: Uint8Array; filename: string; mimeType: string }> =
314
- files.map((file) => ({
315
- data: new Uint8Array(fs.readFileSync(file)),
316
- filename: path.basename(file),
317
- mimeType: mime.getType(file) || 'application/octet-stream',
318
- }))
319
-
320
- if (longPrompt) {
321
- allFiles.push({
322
- data: new Uint8Array(Buffer.from(longPrompt.fileText, 'utf8')),
323
- filename: 'prompt.md',
324
- mimeType: 'text/markdown',
325
- })
326
- }
327
-
328
- const attachments = allFiles.map((f, index) => ({
329
- id: index,
330
- filename: f.filename,
331
- }))
332
-
333
- const formData = new FormData()
334
- formData.append(
335
- 'payload_json',
336
- JSON.stringify({
337
- content,
338
- attachments,
339
- embeds,
340
- allowed_mentions: { parse: store.getState().allowedMentions },
341
- }),
342
- )
343
-
344
- for (const [index, f] of allFiles.entries()) {
345
- formData.append(
346
- `files[${index}]`,
347
- new Blob([f.data], { type: f.mimeType }),
348
- f.filename,
349
- )
350
- }
351
-
352
- const response = await fetch(
353
- discordApiUrl(`/channels/${channelId}/messages`),
354
- {
355
- method: 'POST',
356
- headers: { Authorization: `Bot ${botToken}` },
357
- body: formData,
358
- },
359
- )
360
- if (!response.ok) {
361
- const error = await response.text()
362
- throw new Error(`Discord API error: ${response.status} - ${error}`)
363
- }
364
- return (await response.json()) as { id: string }
365
- }
366
-
367
- if (prompt.length <= discordMaxLength) {
368
- return (await rest.post(Routes.channelMessages(channelId), {
369
- body: {
370
- content: prompt,
371
- embeds,
372
- allowed_mentions: { parse: store.getState().allowedMentions },
373
- },
374
- })) as { id: string }
375
- }
376
-
377
- if (splitInsteadOfAttach) {
378
- const { splitMarkdownForDiscord } = await import('./discord-utils.js')
379
- const chunks = splitMarkdownForDiscord({
380
- content: prompt,
381
- maxLength: discordMaxLength,
382
- })
383
- let firstMessage: { id: string } | undefined
384
- for (let chunk of chunks) {
385
- if (!chunk?.trim()) continue
386
- // Safety net: hard-truncate if splitting still produced an oversized chunk
387
- if (chunk.length > discordMaxLength) {
388
- chunk = chunk.slice(0, discordMaxLength - 4) + '...'
389
- }
390
- const message = (await rest.post(Routes.channelMessages(channelId), {
391
- body: {
392
- content: chunk,
393
- // Only attach embeds to the first message
394
- ...(firstMessage ? {} : { embeds }),
395
- allowed_mentions: { parse: store.getState().allowedMentions },
396
- },
397
- })) as { id: string }
398
- if (!firstMessage) firstMessage = message
399
- }
400
- return firstMessage!
401
- }
402
-
403
- const longPrompt = buildLongPromptMessage(prompt)
404
- // In-memory Blob only — no temp file. Parallel send must never share a path.
405
- const formData = new FormData()
406
- formData.append(
407
- 'payload_json',
408
- JSON.stringify({
409
- content: longPrompt.content,
410
- attachments: [{ id: 0, filename: 'prompt.md' }],
411
- embeds,
412
- allowed_mentions: { parse: store.getState().allowedMentions },
413
- }),
414
- )
415
- formData.append(
416
- 'files[0]',
417
- promptAttachmentBlob(wrapPromptAttachmentText(longPrompt.fileText)),
418
- 'prompt.md',
419
- )
420
-
421
- const starterMessageResponse = await fetch(
422
- discordApiUrl(`/channels/${channelId}/messages`),
423
- {
424
- method: 'POST',
425
- headers: {
426
- Authorization: `Bot ${botToken}`,
427
- },
428
- body: formData,
429
- },
430
- )
431
-
432
- if (!starterMessageResponse.ok) {
433
- const error = await starterMessageResponse.text()
434
- throw new Error(
435
- `Discord API error: ${starterMessageResponse.status} - ${error}`,
436
- )
437
- }
438
-
439
- return (await starterMessageResponse.json()) as { id: string }
440
- }
441
-
442
- export function formatRelativeTime(target: Date): string {
443
- const diffMs = target.getTime() - Date.now()
444
- if (diffMs <= 0) {
445
- return 'due now'
446
- }
447
-
448
- const totalSeconds = Math.floor(diffMs / 1000)
449
- if (totalSeconds < 60) {
450
- return `${totalSeconds}s`
451
- }
452
-
453
- const totalMinutes = Math.floor(totalSeconds / 60)
454
- if (totalMinutes < 60) {
455
- return `${totalMinutes}m`
456
- }
457
-
458
- const hours = Math.floor(totalMinutes / 60)
459
- const minutes = totalMinutes % 60
460
- if (hours < 24) {
461
- return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`
462
- }
463
-
464
- const days = Math.floor(hours / 24)
465
- const remainingHours = hours % 24
466
- return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`
467
- }
468
-
469
- export function formatTaskScheduleLine(schedule: ParsedSendAt): string {
470
- if (schedule.scheduleKind === 'at') {
471
- return `one-time at ${schedule.runAt.toISOString()}`
472
- }
473
- return `cron "${schedule.cronExpr}" (${schedule.timezone}) next ${schedule.nextRunAt.toISOString()}`
474
- }
475
-
476
- export const EXIT_NO_RESTART = 64
477
- // Temporary failure (sysexits EX_TEMPFAIL). Wrapper retries forever with
478
- // backoff and does not count this toward the crash-loop detector. Used when
479
- // Discord login fails because the network is down.
480
- export const EXIT_TEMPFAIL = 75
481
-
482
- export type GuildMemberSearchResult = {
483
- user: { id: string; username: string; global_name?: string }
484
- nick?: string
485
- }
486
-
487
- export type DiscordUserTarget = {
488
- id: string
489
- username?: string
490
- }
491
-
492
- export function isGuildMemberSearchResult(value: object | null): value is GuildMemberSearchResult {
493
- if (!value) {
494
- return false
495
- }
496
- const user = Reflect.get(value, 'user')
497
- return (
498
- typeof user === 'object' &&
499
- user !== null &&
500
- typeof Reflect.get(user, 'id') === 'string' &&
501
- typeof Reflect.get(user, 'username') === 'string'
502
- )
503
- }
504
-
505
- export function getDiscordUserIdFromUserOption(user: string): string | null {
506
- const trimmed = user.trim()
507
- const mentionMatch = trimmed.match(/^<@!?(\d{15,25})>$/)
508
- if (mentionMatch?.[1]) {
509
- return mentionMatch[1]
510
- }
511
- if (/^\d{15,25}$/.test(trimmed)) {
512
- return trimmed
513
- }
514
- return null
515
- }
516
-
517
- function readErrorField(error: object | null, key: string): unknown {
518
- if (!error) {
519
- return undefined
520
- }
521
-
522
- const directValue = Reflect.get(error, key)
523
- if (directValue !== undefined) {
524
- return directValue
525
- }
526
-
527
- const rawError = Reflect.get(error, 'rawError')
528
- if (typeof rawError === 'object' && rawError !== null) {
529
- const rawValue = Reflect.get(rawError, key)
530
- if (rawValue !== undefined) {
531
- return rawValue
532
- }
533
- }
534
-
535
- const cause = Reflect.get(error, 'cause')
536
- if (typeof cause === 'object' && cause !== null) {
537
- return readErrorField(cause, key)
538
- }
539
-
540
- return undefined
541
- }
542
-
543
- /** Transient network errors that may resolve on retry (DNS down, gateway unreachable, TLS blips). */
544
- const TRANSIENT_ERROR_CODES = new Set([
545
- 'ENOTFOUND',
546
- 'ECONNREFUSED',
547
- 'ETIMEDOUT',
548
- 'ECONNRESET',
549
- 'EAI_AGAIN',
550
- 'EPIPE',
551
- 'EHOSTUNREACH',
552
- 'ENETUNREACH',
553
- // TLS/cert errors can be transient (intermediate CA blip, MITM proxy, cert rotation).
554
- // Without these, Discord login failure exits EXIT_NO_RESTART and the bin wrapper
555
- // never restarts — the bot dies permanently on "unable to verify the first certificate".
556
- 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
557
- 'UNABLE_TO_GET_ISSUER_CERT',
558
- 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
559
- 'UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY',
560
- 'CERT_SIGNATURE_FAILURE',
561
- 'CERT_NOT_YET_VALID',
562
- 'CERT_HAS_EXPIRED',
563
- 'CRL_HAS_EXPIRED',
564
- 'DEPTH_ZERO_SELF_SIGNED_CERT',
565
- 'SELF_SIGNED_CERT_IN_CHAIN',
566
- 'ERR_TLS_CERT_ALTNAME_INVALID',
567
- // undici fetch/WebSocket connect failures when the network is down.
568
- // Without these, Discord login exits EXIT_NO_RESTART and the wrapper never
569
- // comes back after a connect timeout to discord-gateway.kimaki.dev.
570
- 'UND_ERR_CONNECT_TIMEOUT',
571
- 'UND_ERR_HEADERS_TIMEOUT',
572
- 'UND_ERR_BODY_TIMEOUT',
573
- 'UND_ERR_SOCKET',
574
- ])
575
-
576
- const TRANSIENT_ERROR_NAMES = new Set([
577
- 'ConnectTimeoutError',
578
- 'HeadersTimeoutError',
579
- 'BodyTimeoutError',
580
- 'SocketError',
581
- ])
582
-
583
- const TRANSIENT_ERROR_MESSAGE_PATTERNS = [
584
- /unable to verify the first certificate/i,
585
- /certificate has expired/i,
586
- /self[- ]signed certificate/i,
587
- /unable to get local issuer certificate/i,
588
- /connect timeout error/i,
589
- /headers timeout error/i,
590
- /body timeout error/i,
591
- ]
592
-
593
- export function isTransientNetworkError(error: unknown): boolean {
594
- if (!(error instanceof Error)) return false
595
- const code = (error as NodeJS.ErrnoException).code
596
- if (code && TRANSIENT_ERROR_CODES.has(code)) return true
597
- if (TRANSIENT_ERROR_NAMES.has(error.name)) return true
598
- // Fallback when code is stripped by wrappers (discord.js sometimes rethrows by message only).
599
- if (
600
- TRANSIENT_ERROR_MESSAGE_PATTERNS.some((pattern) =>
601
- pattern.test(error.message),
602
- )
603
- ) {
604
- return true
605
- }
606
- // discord.js wraps errors in cause chains
607
- if (error.cause instanceof Error) return isTransientNetworkError(error.cause)
608
- return false
609
- }
610
-
611
- export function isDiscordMemberLookupUnavailable(error: Error): boolean {
612
- const status = readErrorField(error, 'status')
613
- if (status === 403) {
614
- return true
615
- }
616
-
617
- const code = readErrorField(error, 'code')
618
- if (code === 50001 || code === 50013) {
619
- return true
620
- }
621
-
622
- const message = String(readErrorField(error, 'message') || '').toLowerCase()
623
- return (
624
- (message.includes('missing access') ||
625
- message.includes('missing permissions') ||
626
- message.includes('intent')) &&
627
- message.includes('member')
628
- )
629
- }
630
-
631
- export function formatMemberLookupUnavailableMessage(): string {
632
- return [
633
- 'Discord member search is unavailable for this bot.',
634
- 'Most Kimaki features still work. Searching names with `--user` needs Server Members Intent.',
635
- 'Use a Discord user ID or raw mention with the same `--user` flag instead:',
636
- ` kimaki send --channel <channelId> --prompt '...' --user 535922349652836367`,
637
- ` kimaki send --channel <channelId> --prompt '...' --user '<@535922349652836367>'`,
638
- ].join('\n')
639
- }
640
-
641
- export async function resolveDiscordUserOption({
642
- user,
643
- guildId,
644
- rest,
645
- }: {
646
- user: string | undefined
647
- guildId: string
648
- rest: REST
649
- }): Promise<Error | DiscordUserTarget | undefined> {
650
- if (!user) {
651
- return undefined
652
- }
653
-
654
- const directUserId = getDiscordUserIdFromUserOption(user)
655
- if (directUserId) {
656
- cliLogger.log(`Using Discord user ID: ${directUserId}`)
657
- return { id: directUserId }
658
- }
659
-
660
- cliLogger.log(`Searching for user "${user}" in guild...`)
661
- const searchResult = await rest
662
- .get(Routes.guildMembersSearch(guildId), {
663
- query: new URLSearchParams({ query: user, limit: '10' }),
664
- })
665
- .catch((error) => new Error('Discord member search failed', { cause: error }))
666
-
667
- if (searchResult instanceof Error) {
668
- if (isDiscordMemberLookupUnavailable(searchResult)) {
669
- return new Error(formatMemberLookupUnavailableMessage())
670
- }
671
- return searchResult
672
- }
673
-
674
- const searchResults = Array.isArray(searchResult)
675
- ? searchResult.filter(isGuildMemberSearchResult)
676
- : []
677
- const exactMatch = searchResults.find((member) => {
678
- const displayName = member.nick || member.user.global_name || member.user.username
679
- return (
680
- displayName.toLowerCase() === user.toLowerCase() ||
681
- member.user.username.toLowerCase() === user.toLowerCase()
682
- )
683
- })
684
- const member = exactMatch || searchResults[0]
685
- if (!member) {
686
- return new Error(`User "${user}" not found in guild`)
687
- }
688
-
689
- const username = member.nick || member.user.global_name || member.user.username
690
- cliLogger.log(`Found user: ${username} (${member.user.id})`)
691
- return { id: member.user.id, username }
692
- }
693
-
694
- export function canUseInteractivePrompts(): boolean {
695
- return Boolean(process.stdin.isTTY && process.stdout.isTTY)
696
- }
697
-
698
- export function exitNonInteractiveSetup(): never {
699
- cliLogger.error(
700
- 'Setup requires an interactive terminal (TTY) for prompts. Run `kimaki` in an interactive shell to complete setup.',
701
- )
702
- process.exit(EXIT_NO_RESTART)
703
- }
704
-
705
- // All structured events emitted on stdout in non-TTY mode (cloud sandboxes, CI).
706
- // Consumers parse these with the eventsource-parser npm package.
707
- export type ProgrammaticEvent =
708
- | { type: 'install_url'; url: string }
709
- | { type: 'authorized'; guild_id: string }
710
- | { type: 'ready'; app_id: string; guild_ids: string[] }
711
- | { type: 'error'; message: string; install_url?: string }
712
-
713
- // Emit a structured JSON line on stdout for non-TTY consumers (cloud sandboxes, CI).
714
- // Each line is a self-contained JSON object with a "type" field for easy parsing.
715
- // Lines are prefixed with "data: " and terminated with "\n\n" (SSE format) so consumers
716
- // can use the eventsource-parser npm package to robustly extract JSON events from noisy
717
- // process output (other log lines, warnings, etc. are ignored by the parser).
718
- export function emitJsonEvent(event: ProgrammaticEvent): void {
719
- process.stdout.write(`data: ${JSON.stringify(event)}\n\n`)
720
- }
721
-
722
- export async function resolveGatewayInstallCredentials(): Promise<
723
- Error | { clientId: string; clientSecret: string; createdNow: boolean }
724
- > {
725
- if (!KIMAKI_GATEWAY_APP_ID) {
726
- return new Error(
727
- 'Gateway mode is not available yet. KIMAKI_GATEWAY_APP_ID is not configured.',
728
- )
729
- }
730
-
731
- const db = await getDb()
732
- const gatewayBot = await db.query.bot_tokens.findFirst({
733
- where: { app_id: KIMAKI_GATEWAY_APP_ID },
734
- })
735
-
736
- if (gatewayBot?.client_id && gatewayBot.client_secret) {
737
- return {
738
- clientId: gatewayBot.client_id,
739
- clientSecret: gatewayBot.client_secret,
740
- createdNow: false,
741
- }
742
- }
743
-
744
- const clientId = crypto.randomUUID()
745
- const clientSecret = crypto.randomBytes(32).toString('hex')
746
-
747
- await setBotMode({
748
- appId: KIMAKI_GATEWAY_APP_ID,
749
- mode: 'gateway',
750
- clientId,
751
- clientSecret,
752
- proxyUrl: KIMAKI_GATEWAY_PROXY_REST_BASE_URL,
753
- })
754
-
755
- return {
756
- clientId,
757
- clientSecret,
758
- createdNow: true,
759
- }
760
- }
761
-
762
- export async function printDiscordInstallUrlAndExit({
763
- gateway,
764
- gatewayCallbackUrl,
765
- }: {
766
- gateway?: boolean
767
- gatewayCallbackUrl?: string
768
- } = {}) {
769
- await initDatabase()
770
-
771
- if (gateway) {
772
- const gatewayCredentials = await resolveGatewayInstallCredentials()
773
- if (gatewayCredentials instanceof Error) {
774
- cliLogger.error(`Failed to resolve gateway install URL: ${gatewayCredentials.message}`)
775
- process.exit(EXIT_NO_RESTART)
776
- }
777
-
778
- const installUrl = generateDiscordInstallUrlForBot({
779
- appId: KIMAKI_GATEWAY_APP_ID,
780
- mode: 'gateway',
781
- clientId: gatewayCredentials.clientId,
782
- clientSecret: gatewayCredentials.clientSecret,
783
- gatewayCallbackUrl,
784
- })
785
- if (installUrl instanceof Error) {
786
- cliLogger.error(`Failed to build install URL: ${installUrl.message}`)
787
- process.exit(EXIT_NO_RESTART)
788
- }
789
-
790
- cliLogger.log(installUrl)
791
- if (gatewayCredentials.createdNow) {
792
- cliLogger.log('Generated and saved new local gateway client credentials.')
793
- }
794
- cliLogger.log(
795
- 'This gateway install URL contains your client credentials. Do not share it.',
796
- )
797
- process.exit(0)
798
- }
799
-
800
- const existingBot = await getBotTokenWithMode()
801
-
802
- if (!existingBot) {
803
- cliLogger.error('No bot configured yet. Run `kimaki` first to set up.')
804
- process.exit(EXIT_NO_RESTART)
805
- }
806
-
807
- const installUrl = generateDiscordInstallUrlForBot({
808
- appId: existingBot.appId,
809
- mode: existingBot.mode,
810
- clientId: existingBot.clientId,
811
- clientSecret: existingBot.clientSecret,
812
- })
813
- if (installUrl instanceof Error) {
814
- cliLogger.error(`Failed to build install URL: ${installUrl.message}`)
815
- process.exit(EXIT_NO_RESTART)
816
- }
817
-
818
- cliLogger.log(installUrl)
819
- if (existingBot.mode === 'gateway') {
820
- cliLogger.log(
821
- 'This gateway install URL contains your client credentials. Do not share it.',
822
- )
823
- }
824
-
825
- process.exit(0)
826
- }
827
-
828
- // Detect if a CLI tool is installed, prompt to install if missing.
829
- // Uses official install scripts with platform-specific commands for Unix vs Windows.
830
- // Sets process.env[envPathKey] to the found binary path for the current session.
831
- // After install, re-checks PATH first, then falls back to common install locations.
832
- export async function ensureCommandAvailable({
833
- name,
834
- envPathKey,
835
- installUnix,
836
- installWindows,
837
- possiblePathsUnix,
838
- possiblePathsWindows,
839
- }: {
840
- name: string
841
- envPathKey: string
842
- installUnix: string
843
- installWindows: string
844
- possiblePathsUnix: string[]
845
- possiblePathsWindows: string[]
846
- }): Promise<void> {
847
- if (process.env[envPathKey]) {
848
- return
849
- }
850
-
851
- const isWindows = process.platform === 'win32'
852
- const whichCmd = isWindows ? 'where' : 'which'
853
- const isInstalled = await execAsync(`${whichCmd} ${name}`, {
854
- env: process.env,
855
- }).then(
856
- () => {
857
- return true
858
- },
859
- () => {
860
- return false
861
- },
862
- )
863
-
864
- if (isInstalled) {
865
- return
866
- }
867
-
868
- note(`${name} is required but not found in your PATH.`, `${name} Not Found`)
869
-
870
- // In non-TTY (cloud sandbox, CI), auto-install without prompting.
871
- // In interactive mode, ask the user first.
872
- if (canUseInteractivePrompts()) {
873
- const shouldInstall = await confirm({
874
- message: `Would you like to install ${name} right now?`,
875
- })
876
- if (isCancel(shouldInstall) || !shouldInstall) {
877
- cancel(`${name} is required to run this bot`)
878
- process.exit(EXIT_NO_RESTART)
879
- }
880
- } else {
881
- cliLogger.log(`Auto-installing ${name} (non-interactive mode)...`)
882
- }
883
-
884
- cliLogger.log(`Installing ${name}...`)
885
-
886
- try {
887
- // Use explicit shell invocation to avoid Node shell-mode quirks on Windows.
888
- // PowerShell needs -NoProfile and -ExecutionPolicy Bypass for install scripts.
889
- // Unix uses login shell (-l) so install scripts can update PATH in shell config.
890
- const cmd = isWindows ? 'powershell.exe' : '/bin/bash'
891
- const args = isWindows
892
- ? ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', installWindows]
893
- : ['-lc', installUnix]
894
- await new Promise<void>((resolve, reject) => {
895
- const child = spawn(cmd, args, { stdio: 'inherit', env: process.env })
896
- child.on('close', (code) => {
897
- if (code === 0) {
898
- resolve()
899
- } else {
900
- reject(new Error(`${name} install exited with code ${code}`))
901
- }
902
- })
903
- child.on('error', reject)
904
- })
905
- cliLogger.log(`${name} installed successfully!`)
906
- } catch (error) {
907
- cliLogger.log(`Failed to install ${name}`)
908
- cliLogger.error(
909
- 'Installation error:',
910
- error instanceof Error ? error.stack : String(error),
911
- )
912
- process.exit(EXIT_NO_RESTART)
913
- }
914
-
915
- // After install, re-check PATH first (install script may have added it)
916
- const foundInPath = await execAsync(`${whichCmd} ${name}`, {
917
- env: process.env,
918
- }).then(
919
- (result) => {
920
- const resolved = selectResolvedCommand({
921
- output: result.stdout,
922
- isWindows,
923
- })
924
- return resolved || ''
925
- },
926
- () => {
927
- return ''
928
- },
929
- )
930
- if (foundInPath) {
931
- process.env[envPathKey] = foundInPath
932
- return
933
- }
934
-
935
- // Fall back to probing common install locations
936
- const home = process.env.HOME || process.env.USERPROFILE || ''
937
- const accessFlag = isWindows ? fs.constants.F_OK : fs.constants.X_OK
938
- const possiblePaths = (isWindows ? possiblePathsWindows : possiblePathsUnix)
939
- .filter((p) => {
940
- return !p.startsWith('~') || home
941
- })
942
- .map((p) => {
943
- return p.replace('~', home)
944
- })
945
-
946
- const installedPath = possiblePaths.find((p) => {
947
- try {
948
- fs.accessSync(p, accessFlag)
949
- return true
950
- } catch {
951
- return false
952
- }
953
- })
954
-
955
- if (!installedPath) {
956
- note(
957
- `${name} was installed but may not be available in this session.\n` +
958
- 'Please restart your terminal and run this command again.',
959
- 'Restart Required',
960
- )
961
- process.exit(EXIT_NO_RESTART)
962
- }
963
-
964
- process.env[envPathKey] = installedPath
965
- }
966
-
967
- // Run opencode upgrade in the background so the user always has the latest version.
968
-
969
- // Spawn caffeinate on macOS to prevent system sleep while bot is running.
970
- // Uses -s to also prevent sleep on lid close (AC power only, not battery).
971
- // Uses -w to watch the parent PID so caffeinate self-terminates if kimaki
972
- // exits for any reason (SIGTERM, crash, process.exit, supervisor stop).
973
- export function startCaffeinate() {
974
- if (process.platform !== 'darwin') {
975
- return
976
- }
977
- try {
978
- const proc = spawn('caffeinate', ['-s', '-w', String(process.pid)], {
979
- stdio: 'ignore',
980
- detached: false,
981
- })
982
- proc.unref()
983
- proc.on('error', (err) => {
984
- cliLogger.warn('Failed to start caffeinate:', err.message)
985
- })
986
- cliLogger.log('Started caffeinate to prevent system sleep')
987
- } catch (err) {
988
- cliLogger.warn(
989
- 'Failed to spawn caffeinate:',
990
- err instanceof Error ? err.message : String(err),
991
- )
992
- }
993
- }
994
-
995
- // Result of credential resolution. `credentialSource` indicates how the
996
- // credentials were obtained so downstream code can decide whether to show
997
- // channel setup prompts (wizard) or skip them (env/saved/headless).
998
- type CredentialResult = {
999
- appId: string
1000
- token: string
1001
- credentialSource: 'env' | 'saved' | 'wizard'
1002
- isGatewayMode: boolean
1003
- installerDiscordUserId?: string
1004
- }
1005
-
1006
- type CliOptions = {
1007
- restartOnboarding?: boolean
1008
- addChannels?: boolean
1009
- dataDir?: string
1010
- useWorktrees?: boolean
1011
- enableVoiceChannels?: boolean
1012
- gateway?: boolean
1013
- gatewayCallbackUrl?: string
1014
- }
1015
-
1016
-
1017
- export async function collectKimakiChannels({
1018
- guilds,
1019
- }: {
1020
- guilds: Guild[]
1021
- }): Promise<{ guild: Guild; channels: ChannelWithTags[] }[]> {
1022
- const guildResults = await Promise.all(
1023
- guilds.map(async (guild) => {
1024
- const channels = await getChannelsWithDescriptions(guild)
1025
- const kimakiChans = channels.filter((ch) => ch.kimakiDirectory)
1026
-
1027
- return { guild, channels: kimakiChans }
1028
- }),
1029
- )
1030
-
1031
- return guildResults.filter((result) => {
1032
- return result.channels.length > 0
1033
- })
1034
- }
1035
-
1036
- /**
1037
- * Store channel-directory mappings in the database.
1038
- * Called after Discord login to persist channel configurations.
1039
- */
1040
- export async function storeChannelDirectories({
1041
- kimakiChannels,
1042
- }: {
1043
- kimakiChannels: { guild: Guild; channels: ChannelWithTags[] }[]
1044
- }): Promise<void> {
1045
- for (const { guild, channels } of kimakiChannels) {
1046
- for (const channel of channels) {
1047
- if (channel.kimakiDirectory) {
1048
- await setChannelDirectory({
1049
- channelId: channel.id,
1050
- directory: channel.kimakiDirectory,
1051
- channelType: 'text',
1052
- skipIfExists: true,
1053
- })
1054
-
1055
- const voiceChannel = guild.channels.cache.find(
1056
- (ch) =>
1057
- ch.type === ChannelType.GuildVoice && ch.name === channel.name,
1058
- )
1059
-
1060
- if (voiceChannel) {
1061
- await setChannelDirectory({
1062
- channelId: voiceChannel.id,
1063
- directory: channel.kimakiDirectory,
1064
- channelType: 'voice',
1065
- skipIfExists: true,
1066
- })
1067
- }
1068
- }
1069
- }
1070
- }
1071
- }
1072
-
1073
- /**
1074
- * Show the ready message with channel links.
1075
- * Called at the end of startup to display available channels.
1076
- */
1077
- export function showReadyMessage({
1078
- kimakiChannels,
1079
- createdChannels,
1080
- }: {
1081
- kimakiChannels: { guild: Guild; channels: ChannelWithTags[] }[]
1082
- createdChannels: { name: string; id: string; guildId: string }[]
1083
- }): void {
1084
- const allChannels: {
1085
- name: string
1086
- id: string
1087
- guildId: string
1088
- directory?: string
1089
- }[] = []
1090
-
1091
- allChannels.push(...createdChannels)
1092
-
1093
- kimakiChannels.forEach(({ guild, channels }) => {
1094
- channels.forEach((ch) => {
1095
- allChannels.push({
1096
- name: ch.name,
1097
- id: ch.id,
1098
- guildId: guild.id,
1099
- directory: ch.kimakiDirectory,
1100
- })
1101
- })
1102
- })
1103
-
1104
- if (allChannels.length > 0) {
1105
- const channelLinks = allChannels
1106
- .map(
1107
- (ch) =>
1108
- `• #${ch.name}: https://discord.com/channels/${ch.guildId}/${ch.id}`,
1109
- )
1110
- .join('\n')
1111
-
1112
- note(
1113
- `Your kimaki channels are ready! Click any link below to open in Discord:\n\n${channelLinks}\n\nSend a message in any channel to start using OpenCode!`,
1114
- '🚀 Ready to Use',
1115
- )
1116
- }
1117
-
1118
- note(
1119
- 'Leave this process running to keep the bot active.\n\nIf you close this process or restart your machine, run `npx kimaki` again to start the bot.',
1120
- '⚠️ Keep Running',
1121
- )
1122
- }
1123
-
1124
- /**
1125
- * Create default channels in locally configured guilds, or proxy-authorized gateway guilds.
1126
- * Idempotent: skips guilds that already have the channel.
1127
- * Extracted so both the interactive and headless startup paths share the same logic.
1128
- */
1129
- export async function ensureDefaultChannelsWithWelcome({
1130
- guilds,
1131
- discordClient,
1132
- appId,
1133
- isGatewayMode,
1134
- installerDiscordUserId,
1135
- }: {
1136
- guilds: Guild[]
1137
- discordClient: import('discord.js').Client
1138
- appId: string
1139
- isGatewayMode: boolean
1140
- installerDiscordUserId?: string
1141
- }): Promise<{ name: string; id: string; guildId: string }[]> {
1142
- if (process.env['KIMAKI_NO_DEFAULT_CHANNEL'] === '1') return []
1143
-
1144
- const localMappings = isGatewayMode ? [] : await findChannelsByDirectory({})
1145
- const created: { name: string; id: string; guildId: string }[] = []
1146
- for (const guild of guilds) {
1147
- try {
1148
- if (!isGatewayMode) {
1149
- // Match live channel IDs: older local mappings have no guild_id.
1150
- const channels = await guild.channels.fetch()
1151
- if (!localMappings.some((row) => channels.has(row.channel_id))) continue
1152
- }
1153
- const result = await createDefaultKimakiChannel({
1154
- guild,
1155
- botName: discordClient.user?.username,
1156
- appId,
1157
- isGatewayMode,
1158
- })
1159
- if (result) {
1160
- created.push({
1161
- name: result.channelName,
1162
- id: result.textChannelId,
1163
- guildId: guild.id,
1164
- })
1165
-
1166
- // Send welcome message to the newly created default channel.
1167
- // Mention the installer so they get a notification.
1168
- const mentionUserId = installerDiscordUserId || guild.ownerId
1169
- await sendWelcomeMessage({
1170
- channel: result.textChannel,
1171
- mentionUserId,
1172
- })
1173
- }
1174
- } catch (error) {
1175
- cliLogger.warn(
1176
- `Failed to create default kimaki channel in ${guild.name}: ${error instanceof Error ? error.stack : String(error)}`,
1177
- )
1178
- }
1179
- }
1180
- return created
1181
- }
1182
-
1183
- /**
1184
- * Background initialization for quick start mode.
1185
- * Starts OpenCode server and registers slash commands without blocking bot startup.
1186
- */
1187
- export async function backgroundInit({
1188
- currentDir,
1189
- token,
1190
- appId,
1191
- guildIds,
1192
- }: {
1193
- currentDir: string
1194
- token: string
1195
- appId: string
1196
- guildIds: string[]
1197
- }): Promise<void> {
1198
- try {
1199
- const opencodeResult = await initializeOpencodeForDirectory(currentDir)
1200
- if (opencodeResult instanceof Error) {
1201
- cliLogger.warn('Background OpenCode init failed:', opencodeResult.message)
1202
- // Still try to register basic commands without user commands/agents
1203
- await registerCommands({
1204
- token,
1205
- appId,
1206
- guildIds,
1207
- userCommands: [],
1208
- agents: [],
1209
- })
1210
- return
1211
- }
1212
-
1213
- const getClient = opencodeResult
1214
-
1215
- const [userCommands, agents] = await Promise.all([
1216
- getClient()
1217
- .command.list({ directory: currentDir })
1218
- .then((r) => r.data || [])
1219
- .catch((error) => {
1220
- cliLogger.warn(
1221
- 'Failed to load user commands during background init:',
1222
- error instanceof Error ? error.stack : String(error),
1223
- )
1224
- return []
1225
- }),
1226
- getClient()
1227
- .app.agents({ directory: currentDir })
1228
- .then((r) => r.data || [])
1229
- .catch((error) => {
1230
- cliLogger.warn(
1231
- 'Failed to load agents during background init:',
1232
- error instanceof Error ? error.stack : String(error),
1233
- )
1234
- return []
1235
- }),
1236
- ])
1237
-
1238
- await registerCommands({ token, appId, guildIds, userCommands, agents })
1239
- cliLogger.log('Slash commands registered!')
1240
- } catch (error) {
1241
- cliLogger.error(
1242
- 'Background init failed:',
1243
- error instanceof Error ? error.stack : String(error),
1244
- )
1245
- void notifyError(error, 'Background init failed')
1246
- }
1247
- }
1248
-
1249
- // Resolve bot credentials from (in priority order):
1250
- // 1. KIMAKI_BOT_TOKEN env var (headless/CI deployments)
1251
- // 2. Saved credentials in the database (self-hosted or gateway mode)
1252
- // 3. Interactive wizard (gateway OAuth or self-hosted token entry)
1253
- //
1254
- // credentialSource tells the caller how creds were obtained:
1255
- // 'env' — KIMAKI_BOT_TOKEN env var
1256
- // 'saved' — reused from database
1257
- // 'wizard' — user just completed onboarding (gateway OAuth or self-hosted)
1258
- export async function resolveCredentials({
1259
- forceRestartOnboarding,
1260
- forceGateway,
1261
- gatewayCallbackUrl,
1262
- }: {
1263
- forceRestartOnboarding: boolean
1264
- forceGateway: boolean
1265
- gatewayCallbackUrl?: string
1266
- }): Promise<CredentialResult> {
1267
- const envToken = process.env.KIMAKI_BOT_TOKEN
1268
- const existingBot = await getBotTokenWithMode()
1269
- // When --gateway is requested and the resolved bot is still self-hosted,
1270
- // check if saved gateway credentials exist by looking up the gateway app_id
1271
- // directly. This lets users switch back and forth between modes without
1272
- // re-running the onboarding wizard each time.
1273
- const hasGatewayCreds = (forceGateway && existingBot?.mode !== 'gateway')
1274
- ? await (await getDb()).query.bot_tokens.findFirst({
1275
- where: { app_id: KIMAKI_GATEWAY_APP_ID },
1276
- })
1277
- : undefined
1278
-
1279
- // 1. Env var takes precedence (headless deployments)
1280
- if (envToken && !forceRestartOnboarding && !forceGateway) {
1281
- const derivedAppId = appIdFromToken(envToken)
1282
- if (!derivedAppId) {
1283
- cliLogger.error(
1284
- 'Could not derive Application ID from KIMAKI_BOT_TOKEN. The token appears malformed.',
1285
- )
1286
- process.exit(EXIT_NO_RESTART)
1287
- }
1288
- await setBotToken(derivedAppId, envToken)
1289
- cliLogger.log(`Using KIMAKI_BOT_TOKEN env var (App ID: ${derivedAppId})`)
1290
- return { appId: derivedAppId, token: envToken, credentialSource: 'env', isGatewayMode: false }
1291
- }
1292
-
1293
- // 2. Saved credentials in the database
1294
- // Reuse saved creds unless: --restart-onboarding forces re-setup, or --gateway
1295
- // overrides saved self-hosted creds (saved gateway creds are still used).
1296
- const canReuseSavedCreds = existingBot && !forceRestartOnboarding
1297
- && !(forceGateway && existingBot.mode !== 'gateway')
1298
- if (canReuseSavedCreds) {
1299
- const modeLabel =
1300
- existingBot.mode === 'gateway' ? ' (gateway mode)' : ''
1301
- note(
1302
- `Using saved bot credentials${modeLabel}:\nApp ID: ${existingBot.appId}\n\nTo use different credentials, run with --restart-onboarding`,
1303
- 'Existing Bot Found',
1304
- )
1305
- if (existingBot.mode !== 'gateway') {
1306
- note(
1307
- `Bot install URL (in case you need to add it to another server):\n${generateBotInstallUrl({ clientId: existingBot.appId })}`,
1308
- 'Install URL',
1309
- )
1310
- }
1311
- return { appId: existingBot.appId, token: existingBot.token, credentialSource: 'saved', isGatewayMode: existingBot.mode === 'gateway' }
1312
- }
1313
-
1314
- // 2b. Switching to gateway: saved gateway credentials exist from a previous
1315
- // gateway setup. Reuse them without re-running the onboarding wizard.
1316
- if (hasGatewayCreds && !forceRestartOnboarding) {
1317
- const gatewayToken = (hasGatewayCreds.client_id && hasGatewayCreds.client_secret)
1318
- ? `${hasGatewayCreds.client_id}:${hasGatewayCreds.client_secret}`
1319
- : hasGatewayCreds.token
1320
- note(
1321
- `Switching to saved gateway credentials:\nApp ID: ${hasGatewayCreds.app_id}`,
1322
- 'Mode Switch',
1323
- )
1324
- return {
1325
- appId: hasGatewayCreds.app_id,
1326
- token: gatewayToken,
1327
- credentialSource: 'saved',
1328
- isGatewayMode: true,
1329
- }
1330
- }
1331
-
1332
- // 3. Interactive setup wizard (first-time users, --restart-onboarding, or --gateway override).
1333
- // Non-TTY: gateway mode proceeds headlessly (JSON events on stdout),
1334
- // self-hosted mode requires interactive prompts so we exit.
1335
- if (!canUseInteractivePrompts() && !forceGateway) {
1336
- exitNonInteractiveSetup()
1337
- }
1338
-
1339
- if (existingBot && forceGateway && existingBot.mode !== 'gateway') {
1340
- note(
1341
- 'Ignoring saved self-hosted credentials due to --gateway flag.\nSwitching to gateway mode.',
1342
- 'Gateway Mode',
1343
- )
1344
- } else if (forceRestartOnboarding && existingBot) {
1345
- note('Ignoring saved credentials due to --restart-onboarding flag', 'Restart Onboarding')
1346
- }
1347
-
1348
- // When --gateway is passed or we're in non-TTY mode, skip the mode selector.
1349
- // Non-TTY without --gateway was already rejected above.
1350
- const modeChoice: 'gateway' | 'self_hosted' = forceGateway
1351
- ? 'gateway'
1352
- : await (async () => {
1353
- const choice = await select({
1354
- message:
1355
- 'How do you want to connect to Discord?\n\nGateway: uses Kimaki\'s pre-built bot — no setup, instant. Self-hosted: you create your own Discord bot at discord.com/developers.',
1356
- options: [
1357
- {
1358
- value: 'gateway' as const,
1359
- label: 'Gateway (pre-built Kimaki bot, no setup needed)',
1360
- },
1361
- {
1362
- value: 'self_hosted' as const,
1363
- label: 'Self-hosted (your own Discord bot, 5-10 min setup)',
1364
- },
1365
- ],
1366
- })
1367
- if (isCancel(choice)) {
1368
- cancel('Setup cancelled')
1369
- process.exit(0)
1370
- }
1371
- return choice
1372
- })()
1373
-
1374
- // ── Gateway mode flow ──
1375
- if (modeChoice === 'gateway') {
1376
- if (!KIMAKI_GATEWAY_APP_ID) {
1377
- cliLogger.error(
1378
- 'Gateway mode is not available yet. KIMAKI_GATEWAY_APP_ID is not configured.',
1379
- )
1380
- process.exit(EXIT_NO_RESTART)
1381
- }
1382
-
1383
- const gatewayCredentials = await resolveGatewayInstallCredentials()
1384
- if (gatewayCredentials instanceof Error) {
1385
- throw gatewayCredentials
1386
- }
1387
- const { clientId, clientSecret } = gatewayCredentials
1388
-
1389
- const oauthUrlResult = generateDiscordInstallUrlForBot({
1390
- appId: KIMAKI_GATEWAY_APP_ID,
1391
- mode: 'gateway',
1392
- clientId,
1393
- clientSecret,
1394
- gatewayCallbackUrl,
1395
- reachableUrl: getInternetReachableBaseUrl() || undefined,
1396
- })
1397
- if (oauthUrlResult instanceof Error) {
1398
- throw oauthUrlResult
1399
- }
1400
- const oauthUrl = oauthUrlResult
1401
- const isInteractive = canUseInteractivePrompts()
1402
-
1403
- if (isInteractive) {
1404
- note(
1405
- `Open this URL to install the Kimaki bot in your Discord server:\n\n${oauthUrl}\n\nDo not share this URL with anyone — it contains your credentials.\n\nIf you don't have a server, create one first (+ button in the Discord sidebar).`,
1406
- 'Install Bot',
1407
- )
1408
-
1409
- // Open URL in default browser
1410
- openUrlInDefaultBrowser(oauthUrl)
1411
- } else {
1412
- // Non-TTY: emit structured JSON so the host process can show the URL to the user.
1413
- emitJsonEvent({ type: 'install_url', url: oauthUrl })
1414
- }
1415
-
1416
- // Poll until the user installs the bot in a Discord server.
1417
- // 100 attempts x 3s = 5 minutes timeout.
1418
- const s = isInteractive ? spinner() : undefined
1419
- s?.start('Waiting for a Discord server with the bot installed...')
1420
-
1421
- const pollUrl = new URL('/api/onboarding/status', KIMAKI_WEBSITE_URL)
1422
- pollUrl.searchParams.set('client_id', clientId)
1423
- pollUrl.searchParams.set('secret', clientSecret)
1424
-
1425
- let guildId: string | undefined
1426
- let installerDiscordUserId: string | undefined
1427
- let onboardingError: string | undefined
1428
- for (let attempt = 0; attempt < 100; attempt++) {
1429
- await new Promise((resolve) => {
1430
- setTimeout(resolve, 3000)
1431
- })
1432
-
1433
- // Progressive hints for interactive users who may be stuck
1434
- if (isInteractive) {
1435
- if (attempt === 15) {
1436
- s?.message(
1437
- 'Still waiting... Select a server in the Discord authorization page and click "Authorize"',
1438
- )
1439
- } else if (attempt === 45) {
1440
- s?.message(
1441
- `Still waiting... If you don't see any servers, create one first (+ button in Discord sidebar), then reopen the URL above`,
1442
- )
1443
- } else if (attempt === 75) {
1444
- s?.message(
1445
- `Still waiting... Reopen the install URL if you closed it:\n${oauthUrl}`,
1446
- )
1447
- }
1448
- }
1449
-
1450
- try {
1451
- const resp = await fetch(pollUrl.toString())
1452
- if (resp.ok) {
1453
- const data = (await resp.json()) as {
1454
- guild_id?: string
1455
- discord_user_id?: string
1456
- } | null
1457
- if (data?.guild_id) {
1458
- guildId = data.guild_id
1459
- installerDiscordUserId = data.discord_user_id
1460
- break
1461
- }
1462
- } else if (resp.status === 404) {
1463
- // Check if the server returned a specific onboarding error
1464
- // (e.g. guild_id missing from Discord callback)
1465
- const data = (await resp.json().catch(() => null)) as {
1466
- error?: string
1467
- onboarding_error?: boolean
1468
- } | null
1469
- if (data?.onboarding_error && data.error) {
1470
- onboardingError = data.error
1471
- break
1472
- }
1473
- }
1474
- } catch {
1475
- // Network error, retry
1476
- }
1477
- }
1478
-
1479
- if (!guildId) {
1480
- const errorMsg = onboardingError
1481
- ? `Authorization failed: ${onboardingError}`
1482
- : 'Bot authorization timed out after 5 minutes. Please try again.'
1483
- if (isInteractive) {
1484
- s?.stop(onboardingError ? 'Authorization failed' : 'Authorization timed out')
1485
- } else {
1486
- emitJsonEvent({ type: 'error', message: errorMsg })
1487
- }
1488
- cliLogger.error(errorMsg)
1489
- process.exit(EXIT_NO_RESTART)
1490
- }
1491
-
1492
- if (isInteractive) {
1493
- s?.stop('Bot authorized successfully!')
1494
- const syncSpinner = spinner()
1495
- syncSpinner.start('Waiting for gateway sync...')
1496
- await new Promise((resolve) => {
1497
- setTimeout(resolve, 2000)
1498
- })
1499
- syncSpinner.stop('Gateway sync completed')
1500
- } else {
1501
- emitJsonEvent({ type: 'authorized', guild_id: guildId })
1502
- await new Promise((resolve) => {
1503
- setTimeout(resolve, 2000)
1504
- })
1505
- }
1506
-
1507
- return {
1508
- appId: KIMAKI_GATEWAY_APP_ID,
1509
- token: `${clientId}:${clientSecret}`,
1510
- credentialSource: 'wizard',
1511
- isGatewayMode: true,
1512
- installerDiscordUserId,
1513
- }
1514
- }
1515
-
1516
- // ── Self-hosted mode flow (existing wizard) ──
1517
- note(
1518
- '1. Go to https://discord.com/developers/applications\n' +
1519
- '2. Click "New Application"\n' +
1520
- '3. Give your application a name',
1521
- 'Step 1: Create Discord Application',
1522
- )
1523
-
1524
- note(
1525
- '1. Go to the "Bot" section in the left sidebar\n' +
1526
- '2. Scroll down to "Privileged Gateway Intents"\n' +
1527
- '3. Enable MESSAGE CONTENT INTENT by toggling it ON\n' +
1528
- '4. Optional: enable SERVER MEMBERS INTENT only if you want name lookup in `kimaki user list` and `kimaki send --user Tommy`\n' +
1529
- '5. Click "Save Changes" at the bottom',
1530
- 'Step 2: Enable Message Content Intent',
1531
- )
1532
-
1533
- const intentsConfirmed = await text({
1534
- message: 'Press Enter after enabling Message Content Intent:',
1535
- placeholder: 'Enter',
1536
- })
1537
- if (isCancel(intentsConfirmed)) {
1538
- cancel('Setup cancelled')
1539
- process.exit(0)
1540
- }
1541
-
1542
- note(
1543
- '1. Still in the "Bot" section\n' +
1544
- '2. Click "Reset Token" to generate a new bot token (in case of errors try again)\n' +
1545
- "3. Copy the token (you won't be able to see it again!)",
1546
- 'Step 3: Get Bot Token',
1547
- )
1548
- const tokenInput = await password({
1549
- message:
1550
- 'Enter your Discord Bot Token (from "Bot" section - click "Reset Token" if needed):',
1551
- validate(value) {
1552
- const cleaned = stripBracketedPaste(value)
1553
- if (!cleaned) {
1554
- return 'Bot token is required'
1555
- }
1556
- if (cleaned.length < 50) {
1557
- return 'Invalid token format (too short)'
1558
- }
1559
- },
1560
- })
1561
- if (isCancel(tokenInput)) {
1562
- cancel('Setup cancelled')
1563
- process.exit(0)
1564
- }
1565
-
1566
- const wizardToken = stripBracketedPaste(tokenInput)
1567
- const derivedAppId = appIdFromToken(wizardToken)
1568
- if (!derivedAppId) {
1569
- cliLogger.error(
1570
- 'Could not derive Application ID from the bot token. The token appears malformed.',
1571
- )
1572
- process.exit(EXIT_NO_RESTART)
1573
- }
1574
-
1575
- await setBotToken(derivedAppId, wizardToken)
1576
-
1577
- note(
1578
- `Bot install URL:\n${generateBotInstallUrl({ clientId: derivedAppId })}\n\nYou MUST install the bot in your Discord server before continuing.`,
1579
- 'Step 4: Install Bot to Server',
1580
- )
1581
- const installed = await text({
1582
- message: 'Press Enter AFTER you have installed the bot in your server:',
1583
- placeholder: 'Enter',
1584
- })
1585
- if (isCancel(installed)) {
1586
- cancel('Setup cancelled')
1587
- process.exit(0)
1588
- }
1589
-
1590
- return { appId: derivedAppId, token: wizardToken, credentialSource: 'wizard', isGatewayMode: false }
1591
- }
1592
-
1593
- export async function run({
1594
- restartOnboarding,
1595
- addChannels,
1596
- useWorktrees,
1597
- enableVoiceChannels,
1598
- gateway,
1599
- gatewayCallbackUrl,
1600
- }: CliOptions) {
1601
- startCaffeinate()
1602
-
1603
- const forceRestartOnboarding = Boolean(restartOnboarding)
1604
- const forceGateway = Boolean(gateway)
1605
-
1606
- // Step 0: Ensure opencode and bun are installed
1607
- await Promise.all([
1608
- ensureCommandAvailable({
1609
- name: 'opencode',
1610
- envPathKey: 'OPENCODE_PATH',
1611
- installUnix: 'curl -fsSL https://opencode.ai/install | bash',
1612
- installWindows: 'irm https://opencode.ai/install.ps1 | iex',
1613
- possiblePathsUnix: [
1614
- '~/.local/bin/opencode',
1615
- '~/.opencode/bin/opencode',
1616
- '/usr/local/bin/opencode',
1617
- '/opt/opencode/bin/opencode',
1618
- ],
1619
- possiblePathsWindows: [
1620
- '~\\.local\\bin\\opencode.exe',
1621
- '~\\AppData\\Local\\opencode\\opencode.exe',
1622
- '~\\.opencode\\bin\\opencode.exe',
1623
- ],
1624
- }),
1625
- ensureCommandAvailable({
1626
- name: 'bun',
1627
- envPathKey: 'BUN_PATH',
1628
- installUnix: 'curl -fsSL https://bun.sh/install | bash',
1629
- installWindows: 'irm bun.sh/install.ps1 | iex',
1630
- possiblePathsUnix: ['~/.bun/bin/bun', '/usr/local/bin/bun'],
1631
- possiblePathsWindows: ['~\\.bun\\bin\\bun.exe'],
1632
- }),
1633
- ])
1634
-
1635
- const opencodeVersionCheck = await assertCompatibleOpencodeVersion()
1636
- if (opencodeVersionCheck instanceof Error) {
1637
- cliLogger.error(opencodeVersionCheck.message)
1638
- process.exit(EXIT_NO_RESTART)
1639
- }
1640
-
1641
-
1642
- if (store.getState().autoUpgradeEnabled) {
1643
- void backgroundUpgradeKimaki()
1644
- }
1645
-
1646
- // Start in-process Hrana server before database init. Required for the bot
1647
- // process because it serves as both the DB server and the single-instance
1648
- // lock (binds the fixed lock port). Without it, IPC and lock enforcement
1649
- // don't work. CLI subcommands skip the server and use file: directly.
1650
- const hranaResult = await startHranaServer({
1651
- dbPath: path.join(getDataDir(), 'discord-sessions.db'),
1652
- bindAll: getInternetReachableBaseUrl() !== null,
1653
- })
1654
- if (hranaResult instanceof Error) {
1655
- cliLogger.error('Failed to start hrana server:', hranaResult.message)
1656
- process.exit(EXIT_NO_RESTART)
1657
- }
1658
- // We own the lock port from here on. Stop signals must release it in every
1659
- // startup phase (Discord login, channel reconcile), not only after ready.
1660
- registerBotLifecycleHandlers()
1661
-
1662
- // Initialize database (connects to hrana server via HTTP)
1663
- await initDatabase()
1664
-
1665
- const { appId, token, credentialSource, isGatewayMode, installerDiscordUserId } = await resolveCredentials({
1666
- forceRestartOnboarding,
1667
- forceGateway,
1668
- gatewayCallbackUrl,
1669
- })
1670
-
1671
- setAnalyticsBotMode(isGatewayMode ? 'gateway' : 'self_hosted')
1672
- initAnalytics()
1673
-
1674
- const gatewayToken = await ensureServiceAuthToken({
1675
- appId,
1676
- preferredGatewayToken: isGatewayMode ? token : undefined,
1677
- })
1678
- // Always set service auth token so local and internet control-plane paths
1679
- // share one auth model (/kimaki/wake and future service endpoints).
1680
- store.setState({ gatewayToken })
1681
-
1682
- // In gateway mode, ensure REST calls route through the gateway proxy.
1683
- // getBotTokenWithMode() sets this for saved-credential paths, but the fresh
1684
- // onboarding path returns directly without going through getBotTokenWithMode(),
1685
- // leaving store.discordBaseUrl at the default 'https://discord.com'.
1686
- // Without this, discord.js sends the clientId:clientSecret token to Discord
1687
- // directly, which rejects it with "An invalid token was provided".
1688
- if (isGatewayMode) {
1689
- store.setState({ discordBaseUrl: KIMAKI_GATEWAY_PROXY_REST_BASE_URL })
1690
- }
1691
-
1692
- // When KIMAKI_INTERNET_REACHABLE_URL is set, the hrana server exposes
1693
- // a /kimaki/wake endpoint for the gateway-proxy to wake this instance and
1694
- // wait until discord.js is connected. Keep Discord traffic on the normal
1695
- // configured base URL (gateway-proxy in gateway mode).
1696
- if (getInternetReachableBaseUrl()) {
1697
- cliLogger.log('Internet-reachable mode: enabling /kimaki/wake endpoint on hrana server')
1698
- }
1699
-
1700
- // Start OpenCode server as early as possible — non-blocking.
1701
- // All dependencies are met (dataDir, lockPort, gatewayToken, hranaUrl set).
1702
- // Runs in parallel with last_used_at update, skipChannelSetup check, and
1703
- // Discord Gateway login so cold start is not blocked by OpenCode spawn.
1704
- const currentDir = process.cwd()
1705
- cliLogger.log('Starting OpenCode server...')
1706
- const opencodePromise = initializeOpencodeForDirectory(currentDir).then(
1707
- (result) => {
1708
- if (result instanceof Error) {
1709
- throw new Error(result.message)
1710
- }
1711
- cliLogger.log('OpenCode server ready!')
1712
- return result
1713
- },
1714
- )
1715
- // Prevent unhandled rejection if OpenCode fails before backgroundInit
1716
- // or the channel setup path awaits it. Errors are handled by the
1717
- // respective consumers (backgroundInit catches, channel setup re-throws).
1718
- opencodePromise.catch(() => {})
1719
-
1720
- // Mark this bot as the most recently used so subcommands in separate
1721
- // processes (send, upload-to-discord, project list) pick the correct bot.
1722
- // getBotTokenWithMode() orders by last_used_at DESC as cross-process
1723
- // source of truth.
1724
- await (await getDb()).update(dbSchema.bot_tokens)
1725
- .set({ last_used_at: new Date() })
1726
- .where(orm.eq(dbSchema.bot_tokens.app_id, appId))
1727
-
1728
- // skipChannelSetup: when true, skip interactive project/channel selection
1729
- // and go straight to bot startup. Channel sync happens in the background.
1730
- //
1731
- // Skip when: creds came from env/saved (not first-time wizard), OR non-TTY
1732
- // gateway (headless), OR user didn't pass --add-channels/--restart-onboarding.
1733
- // Force channel setup when: first-time quick-start with no channels configured
1734
- // and TTY is available, or user explicitly passed --add-channels.
1735
- const isHeadlessGateway = isGatewayMode && !canUseInteractivePrompts()
1736
- const hasConfiguredTextChannels = Boolean(
1737
- await (await getDb()).query.channel_directories.findFirst({
1738
- where: { channel_type: 'text' },
1739
- columns: { channel_id: true },
1740
- }),
1741
- )
1742
- const skipChannelSetup = isHeadlessGateway || (() => {
1743
- // Wizard source always shows channel setup (user just completed onboarding)
1744
- if (credentialSource === 'wizard') {
1745
- return false
1746
- }
1747
- // Env/saved source: skip unless user explicitly asked for channels
1748
- if (forceRestartOnboarding || Boolean(addChannels)) {
1749
- return false
1750
- }
1751
- // First-time quick start with no channels: force setup if TTY is available
1752
- if (!hasConfiguredTextChannels && canUseInteractivePrompts()) {
1753
- return false
1754
- }
1755
- return true
1756
- })()
1757
-
1758
- cliLogger.log(`Connecting to ${getDiscordRestApiUrl()}...`)
1759
- const discordClient = await createDiscordClient()
1760
-
1761
- const guilds: Guild[] = []
1762
- const kimakiChannels: { guild: Guild; channels: ChannelWithTags[] }[] = []
1763
- const createdChannels: { name: string; id: string; guildId: string }[] = []
1764
-
1765
- try {
1766
- await new Promise((resolve, reject) => {
1767
- discordClient.once(Events.ClientReady, async (c) => {
1768
- // Guild discovery comes from the Gateway WebSocket READY payload, not
1769
- // from a separate REST fetch. discord.js consumes READY and hydrates
1770
- // client.guilds.cache from d.guilds. In gateway mode, gateway-proxy
1771
- // already filters this list to authorized guilds for client_id:secret.
1772
- // Example payload fragment received over WS:
1773
- // {
1774
- // "op": 0,
1775
- // "t": "READY",
1776
- // "d": {
1777
- // "guilds": [
1778
- // { "id": "123456789012345678", "unavailable": false }
1779
- // ]
1780
- // }
1781
- // }
1782
- guilds.push(...Array.from(c.guilds.cache.values()))
1783
-
1784
- if (skipChannelSetup) {
1785
- resolve(null)
1786
- return
1787
- }
1788
-
1789
- // Process guild metadata when setup flow needs channel prompts.
1790
- const guildResults = await collectKimakiChannels({ guilds })
1791
-
1792
- // Collect results
1793
- for (const result of guildResults) {
1794
- kimakiChannels.push(result)
1795
- }
1796
-
1797
- resolve(null)
1798
- })
1799
-
1800
- discordClient.once(Events.Error, reject)
1801
-
1802
- discordClient.login(token).catch(reject)
1803
- })
1804
-
1805
- cliLogger.log('Connected to Discord!')
1806
- // Start IPC polling now that Discord client is ready.
1807
- // Register cleanup on process exit since the shutdown handler lives in discord-bot.ts.
1808
- await startIpcPolling({ discordClient })
1809
- process.on('exit', stopIpcPolling)
1810
- } catch (error) {
1811
- cliLogger.log('Failed to connect to Discord', discordClient.ws.gateway)
1812
- cliLogger.error(
1813
- 'Error: ' + (error instanceof Error ? error.stack : String(error)),
1814
- )
1815
- // Transient network errors (DNS down, gateway unreachable, connect
1816
- // timeout) should allow the bin.ts wrapper to restart us after a delay.
1817
- // EXIT_TEMPFAIL skips the crash-loop detector so a long outage cannot
1818
- // permanently stop the bot. Only truly fatal errors (bad token, invalid
1819
- // intent, etc.) should use EXIT_NO_RESTART.
1820
- if (isTransientNetworkError(error)) {
1821
- cliLogger.error('Transient network error, exiting for wrapper restart...')
1822
- process.exit(EXIT_TEMPFAIL)
1823
- }
1824
- process.exit(EXIT_NO_RESTART)
1825
- }
1826
- // Stop signal during Discord login: shutdown owns the process now.
1827
- if (global.shuttingDown) {
1828
- return
1829
- }
1830
- await setBotToken(appId, token)
1831
-
1832
- // In gateway mode the bot only sees guilds the user has installed
1833
- // it in. Zero guilds means the install URL callback never completed or the
1834
- // user removed the bot from all servers — there is nothing the bot can do.
1835
- if (isGatewayMode && guilds.length === 0) {
1836
- // Rebuild the install URL from the current credentials so the user can
1837
- // add the bot to a server without going through the full --restart-onboarding flow.
1838
- const [clientId, clientSecret] = token.split(':')
1839
- if (!clientId || !clientSecret) {
1840
- throw new Error('Malformed gateway token: expected clientId:clientSecret format')
1841
- }
1842
- const installUrlResult = generateDiscordInstallUrlForBot({
1843
- appId: KIMAKI_GATEWAY_APP_ID,
1844
- mode: 'gateway',
1845
- clientId,
1846
- clientSecret,
1847
- })
1848
- if (installUrlResult instanceof Error) {
1849
- throw installUrlResult
1850
- }
1851
- const installUrl = installUrlResult
1852
- if (!canUseInteractivePrompts()) {
1853
- emitJsonEvent({ type: 'error', message: 'No Discord servers found', install_url: installUrl })
1854
- }
1855
- cliLogger.error(
1856
- 'No Discord servers found. The bot must be installed in at least one server.\n' +
1857
- `Install URL: ${installUrl}\n` +
1858
- 'Do not share this URL with anyone — it contains your credentials.\n' +
1859
- 'Open the URL above to add the bot to a server, then run kimaki again.',
1860
- )
1861
- void discordClient.destroy()
1862
- process.exit(EXIT_NO_RESTART)
1863
- }
1864
-
1865
- if (skipChannelSetup) {
1866
- // Start bot immediately — channel sync happens in the background.
1867
- cliLogger.log('Starting Discord bot...')
1868
- await startDiscordBot({ token, appId, discordClient, useWorktrees })
1869
- if (global.shuttingDown) {
1870
- return
1871
- }
1872
- cliLogger.log('Discord bot is running!')
1873
-
1874
- // Background channel sync + role reconciliation + default channel creation.
1875
- // Never blocks ready state.
1876
- void (async () => {
1877
- try {
1878
- const backgroundChannels = await collectKimakiChannels({ guilds })
1879
- await storeChannelDirectories({ kimakiChannels: backgroundChannels })
1880
- cliLogger.log(
1881
- `Background channel sync completed for ${backgroundChannels.length} guild(s)`,
1882
- )
1883
- } catch (error) {
1884
- cliLogger.warn(
1885
- 'Background channel sync failed:',
1886
- error instanceof Error ? error.stack : String(error),
1887
- )
1888
- }
1889
-
1890
- // Create default channels only in locally configured or gateway-authorized guilds.
1891
- // Runs after channel sync so existing channels are detected correctly.
1892
- try {
1893
- await ensureDefaultChannelsWithWelcome({
1894
- guilds,
1895
- discordClient,
1896
- appId,
1897
- isGatewayMode,
1898
- installerDiscordUserId,
1899
- })
1900
- } catch (error) {
1901
- cliLogger.warn(
1902
- 'Background default channel creation failed:',
1903
- error instanceof Error ? error.stack : String(error),
1904
- )
1905
- }
1906
- })()
1907
-
1908
- // Background: OpenCode init + slash command registration (non-blocking)
1909
- void backgroundInit({
1910
- currentDir,
1911
- token,
1912
- appId,
1913
- guildIds: guilds.map((guild) => {
1914
- return guild.id
1915
- }),
1916
- })
1917
- } else {
1918
- // ── Channel setup flow ──
1919
- // Store channel-directory mappings discovered during Discord login.
1920
- await storeChannelDirectories({ kimakiChannels })
1921
-
1922
- if (!hasConfiguredTextChannels) {
1923
- note(
1924
- 'No Kimaki project channels are configured yet. Opening project/channel setup.',
1925
- 'Channel Setup',
1926
- )
1927
- }
1928
-
1929
- if (kimakiChannels.length > 0) {
1930
- const channelList = kimakiChannels
1931
- .flatMap(({ guild, channels }) =>
1932
- channels.map((ch) => {
1933
- return `#${ch.name} in ${guild.name}: ${ch.kimakiDirectory}`
1934
- }),
1935
- )
1936
- .join('\n')
1937
-
1938
- note(channelList, 'Existing Kimaki Channels')
1939
- }
1940
-
1941
- // Wait for OpenCode, fetch projects, show prompts, create channels if needed
1942
- cliLogger.log('Waiting for OpenCode server...')
1943
- const getClient = await opencodePromise
1944
-
1945
- cliLogger.log('Fetching OpenCode data...')
1946
-
1947
- // Fetch projects, commands, and agents in parallel
1948
- const [projects, allUserCommands, allAgents] = await Promise.all([
1949
- getClient()
1950
- .project.list()
1951
- .then((r) => r.data || [])
1952
- .catch((error) => {
1953
- cliLogger.log('Failed to fetch projects')
1954
- cliLogger.error(
1955
- 'Error:',
1956
- error instanceof Error ? error.stack : String(error),
1957
- )
1958
- void discordClient.destroy()
1959
- process.exit(EXIT_NO_RESTART)
1960
- }),
1961
- getClient()
1962
- .command.list({ directory: currentDir })
1963
- .then((r) => r.data || [])
1964
- .catch((error) => {
1965
- cliLogger.warn(
1966
- 'Failed to load user commands during setup:',
1967
- error instanceof Error ? error.stack : String(error),
1968
- )
1969
- return []
1970
- }),
1971
- getClient()
1972
- .app.agents({ directory: currentDir })
1973
- .then((r) => r.data || [])
1974
- .catch((error) => {
1975
- cliLogger.warn(
1976
- 'Failed to load agents during setup:',
1977
- error instanceof Error ? error.stack : String(error),
1978
- )
1979
- return []
1980
- }),
1981
- ])
1982
-
1983
- cliLogger.log(`Found ${projects.length} OpenCode project(s)`)
1984
-
1985
- const existingDirs = kimakiChannels.flatMap(({ channels }) =>
1986
- channels
1987
- .filter((ch) => ch.kimakiDirectory)
1988
- .map((ch) => ch.kimakiDirectory)
1989
- .filter(Boolean),
1990
- )
1991
-
1992
- const availableProjects = deduplicateByKey(
1993
- projects.filter((project) => {
1994
- if (existingDirs.includes(project.worktree)) {
1995
- return false
1996
- }
1997
- if (path.basename(project.worktree).startsWith('opencode-test-')) {
1998
- return false
1999
- }
2000
- return true
2001
- }),
2002
- (x) => x.worktree,
2003
- )
2004
-
2005
- if (availableProjects.length === 0) {
2006
- note(
2007
- 'All OpenCode projects already have Discord channels',
2008
- 'No New Projects',
2009
- )
2010
- }
2011
-
2012
- if (availableProjects.length > 0) {
2013
- if (!canUseInteractivePrompts()) {
2014
- exitNonInteractiveSetup()
2015
- }
2016
-
2017
- const selectedProjects = await multiselect({
2018
- message: 'Select projects to create Discord channels for:',
2019
- options: availableProjects.map((project) => ({
2020
- value: project.id,
2021
- label: `${path.basename(project.worktree)} (${abbreviatePath(project.worktree)})`,
2022
- })),
2023
- required: false,
2024
- })
2025
-
2026
- if (!isCancel(selectedProjects) && selectedProjects.length > 0) {
2027
- let targetGuild: Guild
2028
- if (guilds.length === 0) {
2029
- cliLogger.error(
2030
- 'No Discord servers found! The bot must be installed in at least one server.',
2031
- )
2032
- process.exit(EXIT_NO_RESTART)
2033
- }
2034
-
2035
- if (guilds.length === 1) {
2036
- targetGuild = guilds[0]!
2037
- note(`Using server: ${targetGuild.name}`, 'Server Selected')
2038
- } else {
2039
- const guildSelection = await multiselect({
2040
- message: 'Select a Discord server to create channels in:',
2041
- options: guilds.map((guild) => ({
2042
- value: guild.id,
2043
- label: `${guild.name} (${guild.memberCount} members)`,
2044
- })),
2045
- required: true,
2046
- maxItems: 1,
2047
- })
2048
-
2049
- if (isCancel(guildSelection)) {
2050
- cancel('Setup cancelled')
2051
- process.exit(0)
2052
- }
2053
-
2054
- targetGuild = guilds.find((g) => g.id === guildSelection[0])!
2055
- }
2056
-
2057
- cliLogger.log('Creating Discord channels...')
2058
-
2059
- for (const projectId of selectedProjects) {
2060
- const project = projects.find((p) => p.id === projectId)
2061
- if (!project) continue
2062
-
2063
- try {
2064
- const { textChannelId, channelName } = await createProjectChannels({
2065
- guild: targetGuild,
2066
- projectDirectory: project.worktree,
2067
- botName: discordClient.user?.username,
2068
- enableVoiceChannels,
2069
- analyticsSource: 'onboarding',
2070
- })
2071
-
2072
- createdChannels.push({
2073
- name: channelName,
2074
- id: textChannelId,
2075
- guildId: targetGuild.id,
2076
- })
2077
- } catch (error) {
2078
- cliLogger.error(
2079
- `Failed to create channels for ${path.basename(project.worktree)}:`,
2080
- error,
2081
- )
2082
- }
2083
- }
2084
-
2085
- cliLogger.log(`Created ${createdChannels.length} channel(s)`)
2086
-
2087
- if (createdChannels.length > 0) {
2088
- note(
2089
- createdChannels.map((ch) => `#${ch.name}`).join('\n'),
2090
- 'Created Channels',
2091
- )
2092
- }
2093
- }
2094
- }
2095
-
2096
- // Create default kimaki channel for general-purpose tasks.
2097
- // Only locally configured or gateway-authorized guilds are eligible.
2098
- const defaultChannelResults = await ensureDefaultChannelsWithWelcome({
2099
- guilds,
2100
- discordClient,
2101
- appId,
2102
- isGatewayMode,
2103
- installerDiscordUserId,
2104
- })
2105
- createdChannels.push(...defaultChannelResults)
2106
-
2107
- // Log available user commands
2108
- const registrableCommands = allUserCommands.filter(
2109
- (cmd) => !SKIP_USER_COMMANDS.includes(cmd.name),
2110
- )
2111
-
2112
- if (registrableCommands.length > 0) {
2113
- note(
2114
- `Found ${registrableCommands.length} user-defined command(s)`,
2115
- 'OpenCode Commands/Skills',
2116
- )
2117
- }
2118
-
2119
- cliLogger.log('Registering slash commands asynchronously...')
2120
- void registerCommands({
2121
- token,
2122
- appId,
2123
- guildIds: guilds.map((guild) => {
2124
- return guild.id
2125
- }),
2126
- userCommands: allUserCommands,
2127
- agents: allAgents,
2128
- })
2129
- .then(() => {
2130
- cliLogger.log('Slash commands registered!')
2131
- })
2132
- .catch((error) => {
2133
- cliLogger.error(
2134
- 'Failed to register slash commands:',
2135
- error instanceof Error ? error.stack : String(error),
2136
- )
2137
- })
2138
-
2139
- // Start bot after channel setup is complete so it doesn't handle
2140
- // messages/interactions while the user is still going through prompts.
2141
- cliLogger.log('Starting Discord bot...')
2142
- await startDiscordBot({ token, appId, discordClient, useWorktrees })
2143
- cliLogger.log('Discord bot is running!')
2144
- }
2145
-
2146
- // ── Ready ──
2147
- if (!canUseInteractivePrompts()) {
2148
- emitJsonEvent({
2149
- type: 'ready',
2150
- app_id: appId,
2151
- guild_ids: guilds.map((g) => { return g.id }),
2152
- })
2153
- } else {
2154
- showReadyMessage({ kimakiChannels, createdChannels })
2155
- outro('✨ Bot ready! Listening for messages...')
2156
- }
2157
- }