@otto-assistant/bridge 0.4.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,125 @@
1
+ // Tests for Anthropic OAuth multi-account persistence and rotation.
2
+ import { mkdtemp, readFile, rm, mkdir, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
6
+ import { authFilePath, loadAccountStore, rememberAnthropicOAuth, removeAccount, rotateAnthropicAccount, saveAccountStore, shouldRotateAuth, } from './anthropic-auth-state.js';
7
+ const firstAccount = {
8
+ type: 'oauth',
9
+ refresh: 'refresh-first',
10
+ access: 'access-first',
11
+ expires: 1,
12
+ };
13
+ const secondAccount = {
14
+ type: 'oauth',
15
+ refresh: 'refresh-second',
16
+ access: 'access-second',
17
+ expires: 2,
18
+ };
19
+ let originalXdgDataHome;
20
+ let tempDir = '';
21
+ beforeEach(async () => {
22
+ originalXdgDataHome = process.env.XDG_DATA_HOME;
23
+ tempDir = await mkdtemp(path.join(tmpdir(), 'anthropic-auth-plugin-'));
24
+ process.env.XDG_DATA_HOME = tempDir;
25
+ });
26
+ afterEach(async () => {
27
+ if (originalXdgDataHome === undefined) {
28
+ delete process.env.XDG_DATA_HOME;
29
+ }
30
+ else {
31
+ process.env.XDG_DATA_HOME = originalXdgDataHome;
32
+ }
33
+ await rm(tempDir, { force: true, recursive: true });
34
+ });
35
+ describe('rememberAnthropicOAuth', () => {
36
+ test('stores accounts and updates existing entries by refresh token', async () => {
37
+ await rememberAnthropicOAuth(firstAccount);
38
+ await rememberAnthropicOAuth({ ...firstAccount, access: 'access-first-new', expires: 3 });
39
+ const store = await loadAccountStore();
40
+ expect(store.activeIndex).toBe(0);
41
+ expect(store.accounts).toHaveLength(1);
42
+ expect(store.accounts[0]).toMatchObject({
43
+ refresh: 'refresh-first',
44
+ access: 'access-first-new',
45
+ expires: 3,
46
+ });
47
+ });
48
+ });
49
+ describe('rotateAnthropicAccount', () => {
50
+ test('rotates to the next stored account and syncs auth state', async () => {
51
+ await saveAccountStore({
52
+ version: 1,
53
+ activeIndex: 0,
54
+ accounts: [
55
+ { ...firstAccount, addedAt: 1, lastUsed: 1 },
56
+ { ...secondAccount, addedAt: 2, lastUsed: 2 },
57
+ ],
58
+ });
59
+ const authSetCalls = [];
60
+ const client = {
61
+ auth: {
62
+ set: async (input) => {
63
+ authSetCalls.push(input);
64
+ },
65
+ },
66
+ };
67
+ const rotated = await rotateAnthropicAccount(firstAccount, client);
68
+ const store = await loadAccountStore();
69
+ const authJson = JSON.parse(await readFile(authFilePath(), 'utf8'));
70
+ expect(rotated).toMatchObject({ refresh: 'refresh-second' });
71
+ expect(store.activeIndex).toBe(1);
72
+ expect(authJson.anthropic?.refresh).toBe('refresh-second');
73
+ expect(authSetCalls).toEqual([
74
+ {
75
+ path: { id: 'anthropic' },
76
+ body: {
77
+ type: 'oauth',
78
+ refresh: 'refresh-second',
79
+ access: 'access-second',
80
+ expires: 2,
81
+ },
82
+ },
83
+ ]);
84
+ });
85
+ });
86
+ describe('removeAccount', () => {
87
+ test('removing the active account promotes the next stored account', async () => {
88
+ await saveAccountStore({
89
+ version: 1,
90
+ activeIndex: 1,
91
+ accounts: [
92
+ { ...firstAccount, addedAt: 1, lastUsed: 1 },
93
+ { ...secondAccount, addedAt: 2, lastUsed: 2 },
94
+ ],
95
+ });
96
+ await removeAccount(1);
97
+ const store = await loadAccountStore();
98
+ const authJson = JSON.parse(await readFile(authFilePath(), 'utf8'));
99
+ expect(store.activeIndex).toBe(0);
100
+ expect(store.accounts).toHaveLength(1);
101
+ expect(store.accounts[0]?.refresh).toBe('refresh-first');
102
+ expect(authJson.anthropic?.refresh).toBe('refresh-first');
103
+ });
104
+ test('removing the last account clears active Anthropic auth', async () => {
105
+ await saveAccountStore({
106
+ version: 1,
107
+ activeIndex: 0,
108
+ accounts: [{ ...firstAccount, addedAt: 1, lastUsed: 1 }],
109
+ });
110
+ await mkdir(path.dirname(authFilePath()), { recursive: true });
111
+ await writeFile(authFilePath(), JSON.stringify({ anthropic: firstAccount }, null, 2));
112
+ await removeAccount(0);
113
+ const store = await loadAccountStore();
114
+ const authJson = JSON.parse(await readFile(authFilePath(), 'utf8'));
115
+ expect(store.accounts).toHaveLength(0);
116
+ expect(authJson.anthropic).toBeUndefined();
117
+ });
118
+ });
119
+ describe('shouldRotateAuth', () => {
120
+ test('only rotates on rate limit or auth failures', () => {
121
+ expect(shouldRotateAuth(429, '')).toBe(true);
122
+ expect(shouldRotateAuth(401, 'permission_error')).toBe(true);
123
+ expect(shouldRotateAuth(400, 'bad request')).toBe(false);
124
+ });
125
+ });
@@ -0,0 +1,231 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import path from 'node:path';
4
+ const AUTH_LOCK_STALE_MS = 30_000;
5
+ const AUTH_LOCK_RETRY_MS = 100;
6
+ async function readJson(filePath, fallback) {
7
+ try {
8
+ return JSON.parse(await fs.readFile(filePath, 'utf8'));
9
+ }
10
+ catch {
11
+ return fallback;
12
+ }
13
+ }
14
+ async function writeJson(filePath, value) {
15
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
16
+ await fs.writeFile(filePath, JSON.stringify(value, null, 2), 'utf8');
17
+ await fs.chmod(filePath, 0o600);
18
+ }
19
+ function getErrorCode(error) {
20
+ if (!(error instanceof Error))
21
+ return undefined;
22
+ return error.code;
23
+ }
24
+ async function sleep(ms) {
25
+ await new Promise((resolve) => {
26
+ setTimeout(resolve, ms);
27
+ });
28
+ }
29
+ export function authFilePath() {
30
+ if (process.env.XDG_DATA_HOME) {
31
+ return path.join(process.env.XDG_DATA_HOME, 'opencode', 'auth.json');
32
+ }
33
+ return path.join(homedir(), '.local', 'share', 'opencode', 'auth.json');
34
+ }
35
+ export function accountsFilePath() {
36
+ if (process.env.XDG_DATA_HOME) {
37
+ return path.join(process.env.XDG_DATA_HOME, 'opencode', 'anthropic-oauth-accounts.json');
38
+ }
39
+ return path.join(homedir(), '.local', 'share', 'opencode', 'anthropic-oauth-accounts.json');
40
+ }
41
+ export async function withAuthStateLock(fn) {
42
+ const file = authFilePath();
43
+ const lockDir = `${file}.lock`;
44
+ const deadline = Date.now() + AUTH_LOCK_STALE_MS;
45
+ await fs.mkdir(path.dirname(file), { recursive: true });
46
+ while (true) {
47
+ try {
48
+ await fs.mkdir(lockDir);
49
+ break;
50
+ }
51
+ catch (error) {
52
+ const code = getErrorCode(error);
53
+ if (code !== 'EEXIST') {
54
+ throw error;
55
+ }
56
+ const stats = await fs.stat(lockDir).catch(() => {
57
+ return null;
58
+ });
59
+ if (stats && Date.now() - stats.mtimeMs > AUTH_LOCK_STALE_MS) {
60
+ await fs.rm(lockDir, { force: true, recursive: true }).catch(() => { });
61
+ continue;
62
+ }
63
+ if (Date.now() >= deadline) {
64
+ throw new Error(`Timed out waiting for auth lock: ${lockDir}`);
65
+ }
66
+ await sleep(AUTH_LOCK_RETRY_MS);
67
+ }
68
+ }
69
+ try {
70
+ return await fn();
71
+ }
72
+ finally {
73
+ await fs.rm(lockDir, { force: true, recursive: true }).catch(() => { });
74
+ }
75
+ }
76
+ export function normalizeAccountStore(input) {
77
+ const accounts = Array.isArray(input?.accounts)
78
+ ? input.accounts.filter((account) => !!account &&
79
+ account.type === 'oauth' &&
80
+ typeof account.refresh === 'string' &&
81
+ typeof account.access === 'string' &&
82
+ typeof account.expires === 'number' &&
83
+ typeof account.addedAt === 'number' &&
84
+ typeof account.lastUsed === 'number')
85
+ : [];
86
+ const rawIndex = typeof input?.activeIndex === 'number' ? Math.floor(input.activeIndex) : 0;
87
+ const activeIndex = accounts.length === 0 ? 0 : ((rawIndex % accounts.length) + accounts.length) % accounts.length;
88
+ return { version: 1, activeIndex, accounts };
89
+ }
90
+ export async function loadAccountStore() {
91
+ const raw = await readJson(accountsFilePath(), null);
92
+ return normalizeAccountStore(raw);
93
+ }
94
+ export async function saveAccountStore(store) {
95
+ await writeJson(accountsFilePath(), normalizeAccountStore(store));
96
+ }
97
+ function findCurrentAccountIndex(store, auth) {
98
+ if (!store.accounts.length)
99
+ return 0;
100
+ const byRefresh = store.accounts.findIndex((account) => {
101
+ return account.refresh === auth.refresh;
102
+ });
103
+ if (byRefresh >= 0)
104
+ return byRefresh;
105
+ const byAccess = store.accounts.findIndex((account) => {
106
+ return account.access === auth.access;
107
+ });
108
+ if (byAccess >= 0)
109
+ return byAccess;
110
+ return store.activeIndex;
111
+ }
112
+ export function upsertAccount(store, auth, now = Date.now()) {
113
+ const index = store.accounts.findIndex((account) => {
114
+ return account.refresh === auth.refresh || account.access === auth.access;
115
+ });
116
+ const nextAccount = {
117
+ type: 'oauth',
118
+ refresh: auth.refresh,
119
+ access: auth.access,
120
+ expires: auth.expires,
121
+ addedAt: now,
122
+ lastUsed: now,
123
+ };
124
+ if (index < 0) {
125
+ store.accounts.push(nextAccount);
126
+ store.activeIndex = store.accounts.length - 1;
127
+ return store.activeIndex;
128
+ }
129
+ const existing = store.accounts[index];
130
+ if (!existing)
131
+ return index;
132
+ store.accounts[index] = {
133
+ ...existing,
134
+ ...nextAccount,
135
+ addedAt: existing.addedAt,
136
+ };
137
+ store.activeIndex = index;
138
+ return index;
139
+ }
140
+ export async function rememberAnthropicOAuth(auth) {
141
+ await withAuthStateLock(async () => {
142
+ const store = await loadAccountStore();
143
+ upsertAccount(store, auth);
144
+ await saveAccountStore(store);
145
+ });
146
+ }
147
+ async function writeAnthropicAuthFile(auth) {
148
+ const file = authFilePath();
149
+ const data = await readJson(file, {});
150
+ if (auth) {
151
+ data.anthropic = auth;
152
+ }
153
+ else {
154
+ delete data.anthropic;
155
+ }
156
+ await writeJson(file, data);
157
+ }
158
+ export async function setAnthropicAuth(auth, client) {
159
+ await writeAnthropicAuthFile(auth);
160
+ await client.auth.set({ path: { id: 'anthropic' }, body: auth });
161
+ }
162
+ export async function rotateAnthropicAccount(auth, client) {
163
+ return withAuthStateLock(async () => {
164
+ const store = await loadAccountStore();
165
+ if (store.accounts.length < 2)
166
+ return undefined;
167
+ const currentIndex = findCurrentAccountIndex(store, auth);
168
+ const nextIndex = (currentIndex + 1) % store.accounts.length;
169
+ const nextAccount = store.accounts[nextIndex];
170
+ if (!nextAccount)
171
+ return undefined;
172
+ nextAccount.lastUsed = Date.now();
173
+ store.activeIndex = nextIndex;
174
+ await saveAccountStore(store);
175
+ const nextAuth = {
176
+ type: 'oauth',
177
+ refresh: nextAccount.refresh,
178
+ access: nextAccount.access,
179
+ expires: nextAccount.expires,
180
+ };
181
+ await setAnthropicAuth(nextAuth, client);
182
+ return nextAuth;
183
+ });
184
+ }
185
+ export async function removeAccount(index) {
186
+ return withAuthStateLock(async () => {
187
+ const store = await loadAccountStore();
188
+ if (!Number.isInteger(index) || index < 0 || index >= store.accounts.length) {
189
+ throw new Error(`Account ${index + 1} does not exist`);
190
+ }
191
+ store.accounts.splice(index, 1);
192
+ if (store.accounts.length === 0) {
193
+ store.activeIndex = 0;
194
+ await saveAccountStore(store);
195
+ await writeAnthropicAuthFile(undefined);
196
+ return { store, active: undefined };
197
+ }
198
+ if (store.activeIndex > index) {
199
+ store.activeIndex -= 1;
200
+ }
201
+ else if (store.activeIndex >= store.accounts.length) {
202
+ store.activeIndex = 0;
203
+ }
204
+ const active = store.accounts[store.activeIndex];
205
+ if (!active)
206
+ throw new Error('Active Anthropic account disappeared during removal');
207
+ active.lastUsed = Date.now();
208
+ await saveAccountStore(store);
209
+ const nextAuth = {
210
+ type: 'oauth',
211
+ refresh: active.refresh,
212
+ access: active.access,
213
+ expires: active.expires,
214
+ };
215
+ await writeAnthropicAuthFile(nextAuth);
216
+ return { store, active: nextAuth };
217
+ });
218
+ }
219
+ export function shouldRotateAuth(status, bodyText) {
220
+ const haystack = bodyText.toLowerCase();
221
+ if (status === 429)
222
+ return true;
223
+ if (status === 401 || status === 403)
224
+ return true;
225
+ return (haystack.includes('rate_limit') ||
226
+ haystack.includes('rate limit') ||
227
+ haystack.includes('invalid api key') ||
228
+ haystack.includes('authentication_error') ||
229
+ haystack.includes('permission_error') ||
230
+ haystack.includes('oauth'));
231
+ }
package/dist/bin.js ADDED
@@ -0,0 +1,90 @@
1
+ // Respawn wrapper for the kimaki bot process.
2
+ // When running the default command (no subcommand) with --auto-restart,
3
+ // spawns cli.js as a child process and restarts it on non-zero exit codes
4
+ // (crash, OOM kill, etc). Intentional exits (code 0 or EXIT_NO_RESTART=64)
5
+ // are not restarted.
6
+ //
7
+ // Subcommands (send, tunnel, project, etc.) run directly without the wrapper
8
+ // since they are short-lived and don't need crash recovery.
9
+ //
10
+ // When __KIMAKI_CHILD is set, we're the child process -- just run cli.js directly.
11
+ //
12
+ // V8 heap snapshot flags:
13
+ // Injects --heapsnapshot-near-heap-limit=3 and --diagnostic-dir so V8 writes
14
+ // heap snapshots internally as it approaches the heap limit. This catches OOM
15
+ // situations where SIGKILL (exit 137) would kill the process before our
16
+ // heap-monitor.ts polling can react. The polling monitor is kept as an early
17
+ // warning system at 85% usage; the V8 flag is the last-resort safety net.
18
+ import { spawn } from 'node:child_process';
19
+ import fs from 'node:fs';
20
+ import os from 'node:os';
21
+ import path from 'node:path';
22
+ const HEAP_SNAPSHOT_DIR = path.join(os.homedir(), '.kimaki', 'heap-snapshots');
23
+ // First arg after node + script is either a subcommand or a flag.
24
+ // If it doesn't start with '-', it's a subcommand (e.g. "send", "tunnel", "project").
25
+ const firstArg = process.argv[2];
26
+ const isSubcommand = firstArg && !firstArg.startsWith('-');
27
+ const isHelpFlag = process.argv.includes('--help');
28
+ if (process.env.__KIMAKI_CHILD || isSubcommand || isHelpFlag) {
29
+ await import('./cli.js');
30
+ }
31
+ else {
32
+ console.error('no subcommand detected. kimaki will automatically restart on crash');
33
+ console.error();
34
+ const EXIT_NO_RESTART = 64;
35
+ const MAX_RAPID_RESTARTS = 5;
36
+ const RAPID_RESTART_WINDOW_MS = 60_000;
37
+ const RESTART_DELAY_MS = 2_000;
38
+ const restartTimestamps = [];
39
+ let child = null;
40
+ // Track when we forwarded a termination signal so we don't restart after graceful shutdown
41
+ let shutdownRequested = false;
42
+ function start() {
43
+ if (!fs.existsSync(HEAP_SNAPSHOT_DIR)) {
44
+ fs.mkdirSync(HEAP_SNAPSHOT_DIR, { recursive: true });
45
+ }
46
+ const heapArgs = [
47
+ `--heapsnapshot-near-heap-limit=3`,
48
+ `--diagnostic-dir=${HEAP_SNAPSHOT_DIR}`,
49
+ ];
50
+ const args = [...heapArgs, ...process.execArgv, ...process.argv.slice(1)];
51
+ child = spawn(process.argv[0], args, {
52
+ stdio: 'inherit',
53
+ env: { ...process.env, __KIMAKI_CHILD: '1' },
54
+ });
55
+ child.on('exit', (code, signal) => {
56
+ if (code === 0 || code === EXIT_NO_RESTART || shutdownRequested) {
57
+ process.exit(code ?? 0);
58
+ return;
59
+ }
60
+ const now = Date.now();
61
+ restartTimestamps.push(now);
62
+ while (restartTimestamps.length > 0 &&
63
+ restartTimestamps[0] < now - RAPID_RESTART_WINDOW_MS) {
64
+ restartTimestamps.shift();
65
+ }
66
+ if (restartTimestamps.length > MAX_RAPID_RESTARTS) {
67
+ console.error(`[kimaki] Crash loop detected (${MAX_RAPID_RESTARTS} crashes in ${RAPID_RESTART_WINDOW_MS / 1000}s), exiting`);
68
+ process.exit(1);
69
+ return;
70
+ }
71
+ const reason = signal ? `signal ${signal}` : `code ${code}`;
72
+ console.error(`[kimaki] Process exited with ${reason}, restarting in ${RESTART_DELAY_MS / 1000}s...`);
73
+ setTimeout(start, RESTART_DELAY_MS);
74
+ });
75
+ }
76
+ // Forward signals to child so graceful shutdown and heap snapshots work.
77
+ // SIGTERM/SIGINT mark shutdownRequested so we don't restart after graceful exit.
78
+ for (const sig of ['SIGTERM', 'SIGINT']) {
79
+ process.on(sig, () => {
80
+ shutdownRequested = true;
81
+ child?.kill(sig);
82
+ });
83
+ }
84
+ for (const sig of ['SIGUSR1', 'SIGUSR2']) {
85
+ process.on(sig, () => {
86
+ child?.kill(sig);
87
+ });
88
+ }
89
+ start();
90
+ }
@@ -0,0 +1,227 @@
1
+ // Discord channel and category management.
2
+ // Creates and manages Kimaki project channels (text + voice pairs),
3
+ // extracts channel metadata from topic tags, and ensures category structure.
4
+ import { ChannelType, } from 'discord.js';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { getChannelDirectory, setChannelDirectory, findChannelsByDirectory, } from './database.js';
8
+ import { getProjectsDir } from './config.js';
9
+ import { execAsync } from './worktrees.js';
10
+ import { createLogger, LogPrefix } from './logger.js';
11
+ const logger = createLogger(LogPrefix.CHANNEL);
12
+ export async function ensureKimakiCategory(guild, botName) {
13
+ // Skip appending bot name if it's already "kimaki" to avoid "Kimaki kimaki"
14
+ const isKimakiBot = botName?.toLowerCase() === 'kimaki';
15
+ const categoryName = botName && !isKimakiBot ? `Kimaki ${botName}` : 'Kimaki';
16
+ const existingCategory = guild.channels.cache.find((channel) => {
17
+ if (channel.type !== ChannelType.GuildCategory) {
18
+ return false;
19
+ }
20
+ return channel.name.toLowerCase() === categoryName.toLowerCase();
21
+ });
22
+ if (existingCategory) {
23
+ return existingCategory;
24
+ }
25
+ return guild.channels.create({
26
+ name: categoryName,
27
+ type: ChannelType.GuildCategory,
28
+ });
29
+ }
30
+ export async function ensureKimakiAudioCategory(guild, botName) {
31
+ // Skip appending bot name if it's already "kimaki" to avoid "Kimaki Audio kimaki"
32
+ const isKimakiBot = botName?.toLowerCase() === 'kimaki';
33
+ const categoryName = botName && !isKimakiBot ? `Kimaki Audio ${botName}` : 'Kimaki Audio';
34
+ const existingCategory = guild.channels.cache.find((channel) => {
35
+ if (channel.type !== ChannelType.GuildCategory) {
36
+ return false;
37
+ }
38
+ return channel.name.toLowerCase() === categoryName.toLowerCase();
39
+ });
40
+ if (existingCategory) {
41
+ return existingCategory;
42
+ }
43
+ return guild.channels.create({
44
+ name: categoryName,
45
+ type: ChannelType.GuildCategory,
46
+ });
47
+ }
48
+ export async function createProjectChannels({ guild, projectDirectory, botName, enableVoiceChannels = false, }) {
49
+ const baseName = path.basename(projectDirectory);
50
+ const channelName = `${baseName}`
51
+ .toLowerCase()
52
+ .replace(/[^a-z0-9-]/g, '-')
53
+ .slice(0, 100);
54
+ const kimakiCategory = await ensureKimakiCategory(guild, botName);
55
+ const textChannel = await guild.channels.create({
56
+ name: channelName,
57
+ type: ChannelType.GuildText,
58
+ parent: kimakiCategory,
59
+ // Channel configuration is stored in SQLite, not in the topic
60
+ });
61
+ await setChannelDirectory({
62
+ channelId: textChannel.id,
63
+ directory: projectDirectory,
64
+ channelType: 'text',
65
+ });
66
+ let voiceChannelId = null;
67
+ if (enableVoiceChannels) {
68
+ const kimakiAudioCategory = await ensureKimakiAudioCategory(guild, botName);
69
+ const voiceChannel = await guild.channels.create({
70
+ name: channelName,
71
+ type: ChannelType.GuildVoice,
72
+ parent: kimakiAudioCategory,
73
+ });
74
+ await setChannelDirectory({
75
+ channelId: voiceChannel.id,
76
+ directory: projectDirectory,
77
+ channelType: 'voice',
78
+ });
79
+ voiceChannelId = voiceChannel.id;
80
+ }
81
+ return {
82
+ textChannelId: textChannel.id,
83
+ voiceChannelId,
84
+ channelName,
85
+ };
86
+ }
87
+ export async function getChannelsWithDescriptions(guild) {
88
+ const channels = [];
89
+ const textChannels = guild.channels.cache.filter((channel) => channel.isTextBased());
90
+ for (const channel of textChannels.values()) {
91
+ const textChannel = channel;
92
+ const description = textChannel.topic || null;
93
+ // Get channel config from database instead of parsing XML from topic
94
+ const channelConfig = await getChannelDirectory(textChannel.id);
95
+ channels.push({
96
+ id: textChannel.id,
97
+ name: textChannel.name,
98
+ description,
99
+ kimakiDirectory: channelConfig?.directory,
100
+ });
101
+ }
102
+ return channels;
103
+ }
104
+ const DEFAULT_GITIGNORE = `node_modules/
105
+ dist/
106
+ .env
107
+ .env.*
108
+ !.env.example
109
+ .DS_Store
110
+ tmp/
111
+ *.log
112
+ __pycache__/
113
+ *.pyc
114
+ .venv/
115
+ *.egg-info/
116
+ `;
117
+ const DEFAULT_CHANNEL_TOPIC = 'General channel for misc tasks with Kimaki. Not connected to a specific OpenCode project or repository.';
118
+ /**
119
+ * Create (or find) the default "kimaki" channel for general-purpose tasks.
120
+ * Channel name is "kimaki-{botName}" for self-hosted bots, "kimaki" for gateway.
121
+ * Directory is ~/.kimaki/projects/kimaki, git-initialized with a .gitignore.
122
+ *
123
+ * Idempotency: checks the database for an existing channel mapped to the
124
+ * kimaki projects directory. Also scans guild channels by name+category
125
+ * as a fallback for channels created before DB mapping existed.
126
+ */
127
+ export async function createDefaultKimakiChannel({ guild, botName, appId, isGatewayMode, }) {
128
+ const projectDirectory = path.join(getProjectsDir(), 'kimaki');
129
+ // Ensure the default kimaki project directory exists before any DB mapping
130
+ // restoration or git setup. Custom data dirs may not have <dataDir>/projects
131
+ // created yet, and later writes assume the full path is present.
132
+ if (!fs.existsSync(projectDirectory)) {
133
+ fs.mkdirSync(projectDirectory, { recursive: true });
134
+ logger.log(`Created default kimaki directory: ${projectDirectory}`);
135
+ }
136
+ // Hydrate guild channels from API so the cache scan is complete
137
+ try {
138
+ await guild.channels.fetch();
139
+ }
140
+ catch (error) {
141
+ logger.warn(`Could not fetch guild channels for ${guild.name}: ${error instanceof Error ? error.stack : String(error)}`);
142
+ }
143
+ // 1. Check database for existing channel mapped to this directory.
144
+ // Check ALL mappings (not just the first) since the same directory could
145
+ // have stale rows from deleted channels or other guilds.
146
+ const existingMappings = await findChannelsByDirectory({
147
+ directory: projectDirectory,
148
+ channelType: 'text',
149
+ });
150
+ const mappedChannelInGuild = existingMappings
151
+ .map((row) => guild.channels.cache.get(row.channel_id))
152
+ .find((ch) => ch?.type === ChannelType.GuildText);
153
+ if (mappedChannelInGuild) {
154
+ logger.log(`Default kimaki channel already exists: ${mappedChannelInGuild.id}`);
155
+ return null;
156
+ }
157
+ // 2. Fallback: detect existing channel by name+category
158
+ const kimakiCategory = await ensureKimakiCategory(guild, botName);
159
+ const existingByName = guild.channels.cache.find((ch) => {
160
+ if (ch.type !== ChannelType.GuildText) {
161
+ return false;
162
+ }
163
+ if (ch.parentId !== kimakiCategory.id) {
164
+ return false;
165
+ }
166
+ return ch.name === 'kimaki' || ch.name.startsWith('kimaki-');
167
+ });
168
+ if (existingByName) {
169
+ logger.log(`Found existing default kimaki channel by name: ${existingByName.id}, restoring DB mapping`);
170
+ await setChannelDirectory({
171
+ channelId: existingByName.id,
172
+ directory: projectDirectory,
173
+ channelType: 'text',
174
+ skipIfExists: true,
175
+ });
176
+ return null;
177
+ }
178
+ // Git init — gracefully skip if git is not installed
179
+ const gitDir = path.join(projectDirectory, '.git');
180
+ if (!fs.existsSync(gitDir)) {
181
+ try {
182
+ await execAsync('git init', { cwd: projectDirectory, timeout: 10_000 });
183
+ logger.log(`Initialized git in: ${projectDirectory}`);
184
+ }
185
+ catch (error) {
186
+ logger.warn(`Could not initialize git in ${projectDirectory}: ${error instanceof Error ? error.stack : String(error)}`);
187
+ }
188
+ }
189
+ // Write .gitignore if it doesn't exist
190
+ const gitignorePath = path.join(projectDirectory, '.gitignore');
191
+ if (!fs.existsSync(gitignorePath)) {
192
+ fs.writeFileSync(gitignorePath, DEFAULT_GITIGNORE);
193
+ }
194
+ // Channel name: "kimaki-{botName}" for self-hosted, "kimaki" for gateway
195
+ const channelName = (() => {
196
+ if (isGatewayMode || !botName) {
197
+ return 'kimaki';
198
+ }
199
+ const sanitized = botName
200
+ .toLowerCase()
201
+ .replace(/[^a-z0-9-]/g, '-')
202
+ .replace(/-+/g, '-')
203
+ .replace(/^-|-$/g, '');
204
+ if (!sanitized || sanitized === 'kimaki') {
205
+ return 'kimaki';
206
+ }
207
+ return `kimaki-${sanitized}`.slice(0, 100);
208
+ })();
209
+ const textChannel = await guild.channels.create({
210
+ name: channelName,
211
+ type: ChannelType.GuildText,
212
+ parent: kimakiCategory,
213
+ topic: DEFAULT_CHANNEL_TOPIC,
214
+ });
215
+ await setChannelDirectory({
216
+ channelId: textChannel.id,
217
+ directory: projectDirectory,
218
+ channelType: 'text',
219
+ });
220
+ logger.log(`Created default kimaki channel: #${channelName} (${textChannel.id})`);
221
+ return {
222
+ textChannel,
223
+ textChannelId: textChannel.id,
224
+ channelName,
225
+ projectDirectory,
226
+ };
227
+ }