kimaki 0.4.82 → 0.4.84

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 (264) hide show
  1. package/LICENSE +21 -0
  2. package/dist/anthropic-auth-plugin.js +7 -0
  3. package/dist/cli.js +51 -7
  4. package/dist/commands/abort.js +5 -16
  5. package/dist/commands/action-buttons.js +3 -3
  6. package/dist/commands/add-project.js +1 -1
  7. package/dist/commands/ask-question.js +3 -3
  8. package/dist/commands/context-usage.js +1 -1
  9. package/dist/commands/create-new-project.js +1 -1
  10. package/dist/commands/fork.js +11 -8
  11. package/dist/commands/merge-worktree.js +1 -1
  12. package/dist/commands/new-worktree.js +63 -44
  13. package/dist/commands/remove-project.js +1 -1
  14. package/dist/commands/resume.js +11 -8
  15. package/dist/commands/screenshare.js +14 -6
  16. package/dist/commands/screenshare.test.js +20 -0
  17. package/dist/commands/session.js +1 -1
  18. package/dist/commands/undo-redo.js +91 -7
  19. package/dist/commands/user-command.js +1 -1
  20. package/dist/config.js +16 -1
  21. package/dist/database.js +53 -2
  22. package/dist/db.js +6 -0
  23. package/dist/discord-bot.js +48 -85
  24. package/dist/discord-command-registration.js +1 -1
  25. package/dist/external-opencode-sync.js +515 -0
  26. package/dist/external-opencode-sync.test.js +151 -0
  27. package/dist/gateway-proxy.e2e.test.js +8 -5
  28. package/dist/genai.js +1 -1
  29. package/dist/generated/enums.js +4 -0
  30. package/dist/generated/internal/class.js +4 -4
  31. package/dist/generated/internal/prismaNamespace.js +1 -0
  32. package/dist/generated/internal/prismaNamespaceBrowser.js +1 -0
  33. package/dist/generated/models/external_session_pending_prompts.js +1 -0
  34. package/dist/hrana-server.js +14 -285
  35. package/dist/hrana-server.test.js +4 -2
  36. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +7 -0
  37. package/dist/kimaki-opencode-plugin.js +2 -0
  38. package/dist/kitty-graphics-parser.js +3 -0
  39. package/dist/kitty-graphics-parser.test.js +276 -0
  40. package/dist/kitty-graphics-plugin.js +3 -0
  41. package/dist/markdown.js +4 -4
  42. package/dist/markdown.test.js +1 -1
  43. package/dist/message-formatting.js +54 -15
  44. package/dist/onboarding-tutorial.js +1 -1
  45. package/dist/openai-realtime.js +9 -13
  46. package/dist/opencode.js +28 -5
  47. package/dist/queue-advanced-e2e-setup.js +89 -0
  48. package/dist/queue-advanced-permissions-typing.e2e.test.js +5 -5
  49. package/dist/queue-advanced-typing.e2e.test.js +9 -22
  50. package/dist/queue-question-select-drain.e2e.test.js +117 -0
  51. package/dist/session-handler/event-stream-state.js +101 -7
  52. package/dist/session-handler/event-stream-state.test.js +7 -3
  53. package/dist/session-handler/thread-session-runtime.js +120 -9
  54. package/dist/store.js +1 -0
  55. package/dist/system-message.js +22 -4
  56. package/dist/system-message.test.js +19 -0
  57. package/dist/task-runner.js +1 -1
  58. package/dist/thread-message-queue.e2e.test.js +8 -14
  59. package/dist/tools.js +1 -1
  60. package/dist/undo-redo.e2e.test.js +20 -25
  61. package/package.json +10 -6
  62. package/schema.prisma +6 -0
  63. package/skills/errore/SKILL.md +40 -13
  64. package/skills/goke/SKILL.md +12 -0
  65. package/skills/lintcn/SKILL.md +868 -0
  66. package/skills/npm-package/SKILL.md +1 -0
  67. package/skills/proxyman/SKILL.md +215 -0
  68. package/skills/spiceflow/SKILL.md +1 -1
  69. package/skills/usecomputer/SKILL.md +339 -0
  70. package/src/ai-tool-to-genai.ts +1 -0
  71. package/src/anthropic-auth-plugin.ts +7 -0
  72. package/src/cli.ts +59 -6
  73. package/src/commands/abort.ts +6 -16
  74. package/src/commands/action-buttons.ts +5 -1
  75. package/src/commands/add-project.ts +1 -1
  76. package/src/commands/ask-question.ts +5 -2
  77. package/src/commands/context-usage.ts +1 -1
  78. package/src/commands/create-new-project.ts +1 -1
  79. package/src/commands/fork.ts +12 -11
  80. package/src/commands/merge-worktree.ts +1 -1
  81. package/src/commands/new-worktree.ts +74 -55
  82. package/src/commands/remove-project.ts +1 -1
  83. package/src/commands/resume.ts +12 -10
  84. package/src/commands/screenshare.test.ts +30 -0
  85. package/src/commands/screenshare.ts +18 -6
  86. package/src/commands/session.ts +1 -1
  87. package/src/commands/undo-redo.ts +108 -10
  88. package/src/commands/user-command.ts +1 -1
  89. package/src/config.ts +19 -1
  90. package/src/database.ts +72 -3
  91. package/src/db.ts +8 -0
  92. package/src/discord-bot.ts +58 -93
  93. package/src/discord-command-registration.ts +1 -1
  94. package/src/external-opencode-sync.ts +729 -0
  95. package/src/gateway-proxy.e2e.test.ts +9 -5
  96. package/src/genai.ts +3 -3
  97. package/src/generated/commonInputTypes.ts +34 -0
  98. package/src/generated/enums.ts +8 -0
  99. package/src/generated/internal/class.ts +4 -4
  100. package/src/generated/internal/prismaNamespace.ts +8 -0
  101. package/src/generated/internal/prismaNamespaceBrowser.ts +1 -0
  102. package/src/generated/models/thread_sessions.ts +53 -1
  103. package/src/hrana-server.test.ts +8 -2
  104. package/src/hrana-server.ts +18 -390
  105. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +7 -0
  106. package/src/kimaki-opencode-plugin.ts +2 -0
  107. package/src/markdown.test.ts +1 -1
  108. package/src/markdown.ts +4 -4
  109. package/src/message-formatting.ts +66 -17
  110. package/src/onboarding-tutorial.ts +1 -1
  111. package/src/openai-realtime.ts +6 -10
  112. package/src/opencode.ts +31 -7
  113. package/src/queue-advanced-e2e-setup.ts +92 -0
  114. package/src/queue-advanced-permissions-typing.e2e.test.ts +5 -5
  115. package/src/queue-advanced-typing.e2e.test.ts +9 -22
  116. package/src/queue-question-select-drain.e2e.test.ts +149 -0
  117. package/src/schema.sql +1 -0
  118. package/src/session-handler/event-stream-state.test.ts +7 -2
  119. package/src/session-handler/event-stream-state.ts +128 -7
  120. package/src/session-handler/thread-runtime-state.ts +5 -0
  121. package/src/session-handler/thread-session-runtime.ts +153 -11
  122. package/src/store.ts +8 -0
  123. package/src/system-message.ts +27 -4
  124. package/src/task-runner.ts +1 -1
  125. package/src/thread-message-queue.e2e.test.ts +8 -14
  126. package/src/tools.ts +1 -1
  127. package/src/undo-redo.e2e.test.ts +28 -26
  128. package/skills/jitter/node_modules/.bin/esbuild +0 -21
  129. package/skills/jitter/node_modules/.bin/tsc +0 -21
  130. package/skills/jitter/node_modules/.bin/tsserver +0 -21
  131. package/skills/jitter/node_modules/typescript/LICENSE.txt +0 -55
  132. package/skills/jitter/node_modules/typescript/README.md +0 -50
  133. package/skills/jitter/node_modules/typescript/SECURITY.md +0 -41
  134. package/skills/jitter/node_modules/typescript/ThirdPartyNoticeText.txt +0 -193
  135. package/skills/jitter/node_modules/typescript/bin/tsc +0 -2
  136. package/skills/jitter/node_modules/typescript/bin/tsserver +0 -2
  137. package/skills/jitter/node_modules/typescript/lib/_tsc.js +0 -133792
  138. package/skills/jitter/node_modules/typescript/lib/_tsserver.js +0 -659
  139. package/skills/jitter/node_modules/typescript/lib/_typingsInstaller.js +0 -222
  140. package/skills/jitter/node_modules/typescript/lib/cs/diagnosticMessages.generated.json +0 -2122
  141. package/skills/jitter/node_modules/typescript/lib/de/diagnosticMessages.generated.json +0 -2122
  142. package/skills/jitter/node_modules/typescript/lib/es/diagnosticMessages.generated.json +0 -2122
  143. package/skills/jitter/node_modules/typescript/lib/fr/diagnosticMessages.generated.json +0 -2122
  144. package/skills/jitter/node_modules/typescript/lib/it/diagnosticMessages.generated.json +0 -2122
  145. package/skills/jitter/node_modules/typescript/lib/ja/diagnosticMessages.generated.json +0 -2122
  146. package/skills/jitter/node_modules/typescript/lib/ko/diagnosticMessages.generated.json +0 -2122
  147. package/skills/jitter/node_modules/typescript/lib/lib.d.ts +0 -22
  148. package/skills/jitter/node_modules/typescript/lib/lib.decorators.d.ts +0 -384
  149. package/skills/jitter/node_modules/typescript/lib/lib.decorators.legacy.d.ts +0 -22
  150. package/skills/jitter/node_modules/typescript/lib/lib.dom.asynciterable.d.ts +0 -41
  151. package/skills/jitter/node_modules/typescript/lib/lib.dom.d.ts +0 -39429
  152. package/skills/jitter/node_modules/typescript/lib/lib.dom.iterable.d.ts +0 -571
  153. package/skills/jitter/node_modules/typescript/lib/lib.es2015.collection.d.ts +0 -147
  154. package/skills/jitter/node_modules/typescript/lib/lib.es2015.core.d.ts +0 -597
  155. package/skills/jitter/node_modules/typescript/lib/lib.es2015.d.ts +0 -28
  156. package/skills/jitter/node_modules/typescript/lib/lib.es2015.generator.d.ts +0 -77
  157. package/skills/jitter/node_modules/typescript/lib/lib.es2015.iterable.d.ts +0 -605
  158. package/skills/jitter/node_modules/typescript/lib/lib.es2015.promise.d.ts +0 -81
  159. package/skills/jitter/node_modules/typescript/lib/lib.es2015.proxy.d.ts +0 -128
  160. package/skills/jitter/node_modules/typescript/lib/lib.es2015.reflect.d.ts +0 -144
  161. package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.d.ts +0 -46
  162. package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts +0 -326
  163. package/skills/jitter/node_modules/typescript/lib/lib.es2016.array.include.d.ts +0 -116
  164. package/skills/jitter/node_modules/typescript/lib/lib.es2016.d.ts +0 -21
  165. package/skills/jitter/node_modules/typescript/lib/lib.es2016.full.d.ts +0 -23
  166. package/skills/jitter/node_modules/typescript/lib/lib.es2016.intl.d.ts +0 -31
  167. package/skills/jitter/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts +0 -21
  168. package/skills/jitter/node_modules/typescript/lib/lib.es2017.d.ts +0 -26
  169. package/skills/jitter/node_modules/typescript/lib/lib.es2017.date.d.ts +0 -31
  170. package/skills/jitter/node_modules/typescript/lib/lib.es2017.full.d.ts +0 -23
  171. package/skills/jitter/node_modules/typescript/lib/lib.es2017.intl.d.ts +0 -44
  172. package/skills/jitter/node_modules/typescript/lib/lib.es2017.object.d.ts +0 -49
  173. package/skills/jitter/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts +0 -135
  174. package/skills/jitter/node_modules/typescript/lib/lib.es2017.string.d.ts +0 -45
  175. package/skills/jitter/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts +0 -53
  176. package/skills/jitter/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts +0 -77
  177. package/skills/jitter/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts +0 -53
  178. package/skills/jitter/node_modules/typescript/lib/lib.es2018.d.ts +0 -24
  179. package/skills/jitter/node_modules/typescript/lib/lib.es2018.full.d.ts +0 -24
  180. package/skills/jitter/node_modules/typescript/lib/lib.es2018.intl.d.ts +0 -83
  181. package/skills/jitter/node_modules/typescript/lib/lib.es2018.promise.d.ts +0 -30
  182. package/skills/jitter/node_modules/typescript/lib/lib.es2018.regexp.d.ts +0 -37
  183. package/skills/jitter/node_modules/typescript/lib/lib.es2019.array.d.ts +0 -79
  184. package/skills/jitter/node_modules/typescript/lib/lib.es2019.d.ts +0 -24
  185. package/skills/jitter/node_modules/typescript/lib/lib.es2019.full.d.ts +0 -24
  186. package/skills/jitter/node_modules/typescript/lib/lib.es2019.intl.d.ts +0 -23
  187. package/skills/jitter/node_modules/typescript/lib/lib.es2019.object.d.ts +0 -33
  188. package/skills/jitter/node_modules/typescript/lib/lib.es2019.string.d.ts +0 -37
  189. package/skills/jitter/node_modules/typescript/lib/lib.es2019.symbol.d.ts +0 -24
  190. package/skills/jitter/node_modules/typescript/lib/lib.es2020.bigint.d.ts +0 -765
  191. package/skills/jitter/node_modules/typescript/lib/lib.es2020.d.ts +0 -27
  192. package/skills/jitter/node_modules/typescript/lib/lib.es2020.date.d.ts +0 -42
  193. package/skills/jitter/node_modules/typescript/lib/lib.es2020.full.d.ts +0 -24
  194. package/skills/jitter/node_modules/typescript/lib/lib.es2020.intl.d.ts +0 -474
  195. package/skills/jitter/node_modules/typescript/lib/lib.es2020.number.d.ts +0 -28
  196. package/skills/jitter/node_modules/typescript/lib/lib.es2020.promise.d.ts +0 -47
  197. package/skills/jitter/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts +0 -99
  198. package/skills/jitter/node_modules/typescript/lib/lib.es2020.string.d.ts +0 -44
  199. package/skills/jitter/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts +0 -41
  200. package/skills/jitter/node_modules/typescript/lib/lib.es2021.d.ts +0 -23
  201. package/skills/jitter/node_modules/typescript/lib/lib.es2021.full.d.ts +0 -24
  202. package/skills/jitter/node_modules/typescript/lib/lib.es2021.intl.d.ts +0 -166
  203. package/skills/jitter/node_modules/typescript/lib/lib.es2021.promise.d.ts +0 -48
  204. package/skills/jitter/node_modules/typescript/lib/lib.es2021.string.d.ts +0 -33
  205. package/skills/jitter/node_modules/typescript/lib/lib.es2021.weakref.d.ts +0 -78
  206. package/skills/jitter/node_modules/typescript/lib/lib.es2022.array.d.ts +0 -121
  207. package/skills/jitter/node_modules/typescript/lib/lib.es2022.d.ts +0 -25
  208. package/skills/jitter/node_modules/typescript/lib/lib.es2022.error.d.ts +0 -75
  209. package/skills/jitter/node_modules/typescript/lib/lib.es2022.full.d.ts +0 -24
  210. package/skills/jitter/node_modules/typescript/lib/lib.es2022.intl.d.ts +0 -145
  211. package/skills/jitter/node_modules/typescript/lib/lib.es2022.object.d.ts +0 -26
  212. package/skills/jitter/node_modules/typescript/lib/lib.es2022.regexp.d.ts +0 -39
  213. package/skills/jitter/node_modules/typescript/lib/lib.es2022.string.d.ts +0 -25
  214. package/skills/jitter/node_modules/typescript/lib/lib.es2023.array.d.ts +0 -924
  215. package/skills/jitter/node_modules/typescript/lib/lib.es2023.collection.d.ts +0 -21
  216. package/skills/jitter/node_modules/typescript/lib/lib.es2023.d.ts +0 -22
  217. package/skills/jitter/node_modules/typescript/lib/lib.es2023.full.d.ts +0 -24
  218. package/skills/jitter/node_modules/typescript/lib/lib.es2023.intl.d.ts +0 -56
  219. package/skills/jitter/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts +0 -65
  220. package/skills/jitter/node_modules/typescript/lib/lib.es2024.collection.d.ts +0 -29
  221. package/skills/jitter/node_modules/typescript/lib/lib.es2024.d.ts +0 -26
  222. package/skills/jitter/node_modules/typescript/lib/lib.es2024.full.d.ts +0 -24
  223. package/skills/jitter/node_modules/typescript/lib/lib.es2024.object.d.ts +0 -29
  224. package/skills/jitter/node_modules/typescript/lib/lib.es2024.promise.d.ts +0 -35
  225. package/skills/jitter/node_modules/typescript/lib/lib.es2024.regexp.d.ts +0 -25
  226. package/skills/jitter/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts +0 -68
  227. package/skills/jitter/node_modules/typescript/lib/lib.es2024.string.d.ts +0 -29
  228. package/skills/jitter/node_modules/typescript/lib/lib.es5.d.ts +0 -4601
  229. package/skills/jitter/node_modules/typescript/lib/lib.es6.d.ts +0 -23
  230. package/skills/jitter/node_modules/typescript/lib/lib.esnext.array.d.ts +0 -35
  231. package/skills/jitter/node_modules/typescript/lib/lib.esnext.collection.d.ts +0 -96
  232. package/skills/jitter/node_modules/typescript/lib/lib.esnext.d.ts +0 -29
  233. package/skills/jitter/node_modules/typescript/lib/lib.esnext.decorators.d.ts +0 -28
  234. package/skills/jitter/node_modules/typescript/lib/lib.esnext.disposable.d.ts +0 -193
  235. package/skills/jitter/node_modules/typescript/lib/lib.esnext.error.d.ts +0 -24
  236. package/skills/jitter/node_modules/typescript/lib/lib.esnext.float16.d.ts +0 -443
  237. package/skills/jitter/node_modules/typescript/lib/lib.esnext.full.d.ts +0 -24
  238. package/skills/jitter/node_modules/typescript/lib/lib.esnext.intl.d.ts +0 -21
  239. package/skills/jitter/node_modules/typescript/lib/lib.esnext.iterator.d.ts +0 -148
  240. package/skills/jitter/node_modules/typescript/lib/lib.esnext.promise.d.ts +0 -34
  241. package/skills/jitter/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts +0 -25
  242. package/skills/jitter/node_modules/typescript/lib/lib.scripthost.d.ts +0 -322
  243. package/skills/jitter/node_modules/typescript/lib/lib.webworker.asynciterable.d.ts +0 -41
  244. package/skills/jitter/node_modules/typescript/lib/lib.webworker.d.ts +0 -13150
  245. package/skills/jitter/node_modules/typescript/lib/lib.webworker.importscripts.d.ts +0 -23
  246. package/skills/jitter/node_modules/typescript/lib/lib.webworker.iterable.d.ts +0 -340
  247. package/skills/jitter/node_modules/typescript/lib/pl/diagnosticMessages.generated.json +0 -2122
  248. package/skills/jitter/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json +0 -2122
  249. package/skills/jitter/node_modules/typescript/lib/ru/diagnosticMessages.generated.json +0 -2122
  250. package/skills/jitter/node_modules/typescript/lib/tr/diagnosticMessages.generated.json +0 -2122
  251. package/skills/jitter/node_modules/typescript/lib/tsc.js +0 -8
  252. package/skills/jitter/node_modules/typescript/lib/tsserver.js +0 -8
  253. package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.d.ts +0 -17
  254. package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.js +0 -21
  255. package/skills/jitter/node_modules/typescript/lib/typesMap.json +0 -497
  256. package/skills/jitter/node_modules/typescript/lib/typescript.d.ts +0 -11438
  257. package/skills/jitter/node_modules/typescript/lib/typescript.js +0 -200253
  258. package/skills/jitter/node_modules/typescript/lib/typingsInstaller.js +0 -8
  259. package/skills/jitter/node_modules/typescript/lib/watchGuard.js +0 -53
  260. package/skills/jitter/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json +0 -2122
  261. package/skills/jitter/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json +0 -2122
  262. package/skills/jitter/node_modules/typescript/node_modules/.bin/tsc +0 -21
  263. package/skills/jitter/node_modules/typescript/node_modules/.bin/tsserver +0 -21
  264. package/skills/jitter/node_modules/typescript/package.json +0 -120
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kimaki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,6 +1,13 @@
1
1
  /**
2
2
  * Anthropic OAuth authentication plugin for OpenCode.
3
3
  *
4
+ * If you're copy-pasting this plugin into your OpenCode config folder,
5
+ * you need to install the runtime dependencies first:
6
+ *
7
+ * cd ~/.config/opencode
8
+ * bun init -y
9
+ * bun add @openauthjs/openauth proper-lockfile
10
+ *
4
11
  * Handles two concerns:
5
12
  * 1. OAuth login + token refresh (PKCE flow against claude.ai)
6
13
  * 2. Request/response rewriting (tool names, system prompt, beta headers)
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { z } from 'zod';
7
7
  import { intro, outro, text, password, note, cancel, isCancel, confirm, log, multiselect, select, spinner, } from '@clack/prompts';
8
8
  import { deduplicateByKey, generateBotInstallUrl, generateDiscordInstallUrlForBot, KIMAKI_GATEWAY_APP_ID, KIMAKI_WEBSITE_URL, abbreviatePath, } from './utils.js';
9
9
  import { getChannelsWithDescriptions, createDiscordClient, initDatabase, getChannelDirectory, startDiscordBot, initializeOpencodeForDirectory, ensureKimakiCategory, createProjectChannels, createDefaultKimakiChannel, } from './discord-bot.js';
10
- import { getBotTokenWithMode, ensureServiceAuthToken, setBotToken, setBotMode, setChannelDirectory, findChannelsByDirectory, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getPrisma, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, } from './database.js';
10
+ import { getBotTokenWithMode, ensureServiceAuthToken, setBotToken, setBotMode, setChannelDirectory, findChannelsByDirectory, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getPrisma, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, } from './database.js';
11
11
  import { ShareMarkdown } from './markdown.js';
12
12
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts, } from './session-search.js';
13
13
  import { formatWorktreeName } from './commands/new-worktree.js';
@@ -26,7 +26,7 @@ import { createLogger, formatErrorWithStack, initLogFile, LogPrefix } from './lo
26
26
  import { initSentry, notifyError } from './sentry.js';
27
27
  import { archiveThread, uploadFilesToDiscord, stripMentions, } from './discord-utils.js';
28
28
  import { spawn, execSync } from 'node:child_process';
29
- import { setDataDir, getDataDir, getProjectsDir, } from './config.js';
29
+ import { setDataDir, setProjectsDir, getDataDir, getProjectsDir, } from './config.js';
30
30
  import { execAsync } from './worktrees.js';
31
31
  import { backgroundUpgradeKimaki, upgrade, getCurrentVersion, } from './upgrade.js';
32
32
  import { startHranaServer } from './hrana-server.js';
@@ -1305,6 +1305,7 @@ cli
1305
1305
  .option('--restart-onboarding', 'Prompt for new credentials even if saved')
1306
1306
  .option('--add-channels', 'Select OpenCode projects to create Discord channels before starting')
1307
1307
  .option('--data-dir <path>', 'Data directory for config and database (default: ~/.kimaki)')
1308
+ .option('--projects-dir <path>', 'Directory where new projects are created (default: <data-dir>/projects)')
1308
1309
  .option('--install-url', 'Print the bot install URL and exit')
1309
1310
  .option('--use-worktrees', 'Create git worktrees for all new sessions started from channel messages')
1310
1311
  .option('--enable-voice-channels', 'Create voice channels for projects (disabled by default)')
@@ -1332,6 +1333,10 @@ cli
1332
1333
  setDataDir(options.dataDir);
1333
1334
  cliLogger.log(`Using data directory: ${getDataDir()}`);
1334
1335
  }
1336
+ if (options.projectsDir) {
1337
+ setProjectsDir(options.projectsDir);
1338
+ cliLogger.log(`Using projects directory: ${getProjectsDir()}`);
1339
+ }
1335
1340
  // Initialize file logging to <dataDir>/kimaki.log
1336
1341
  initLogFile(getDataDir());
1337
1342
  // Batch all CLI flag store updates into a single setState call.
@@ -2353,6 +2358,7 @@ cli
2353
2358
  cli
2354
2359
  .command('project list', 'List all registered projects with their Discord channels')
2355
2360
  .option('--json', 'Output as JSON')
2361
+ .option('--prune', 'Remove stale entries whose Discord channel no longer exists')
2356
2362
  .action(async (options) => {
2357
2363
  await initDatabase();
2358
2364
  const prisma = await getPrisma();
@@ -2369,31 +2375,67 @@ cli
2369
2375
  const rest = botRow ? createDiscordRest(botRow.token) : null;
2370
2376
  const enriched = await Promise.all(channels.map(async (ch) => {
2371
2377
  let channelName = '';
2378
+ let deleted = false;
2372
2379
  if (rest) {
2373
2380
  try {
2374
2381
  const data = (await rest.get(Routes.channel(ch.channel_id)));
2375
2382
  channelName = data.name || '';
2376
2383
  }
2377
- catch {
2378
- // Channel may have been deleted from Discord
2384
+ catch (error) {
2385
+ // Only mark as deleted for Unknown Channel (10003) or 404,
2386
+ // not transient errors like rate limits or 5xx
2387
+ const isUnknownChannel = error instanceof Error &&
2388
+ 'code' in error &&
2389
+ 'status' in error &&
2390
+ (error.code === 10003 ||
2391
+ error.status === 404);
2392
+ deleted = isUnknownChannel;
2379
2393
  }
2380
2394
  }
2381
- return { ...ch, channelName };
2395
+ return { ...ch, channelName, deleted };
2382
2396
  }));
2397
+ // Prune stale entries if requested
2398
+ if (options.prune) {
2399
+ const stale = enriched.filter((ch) => {
2400
+ return ch.deleted;
2401
+ });
2402
+ if (stale.length === 0) {
2403
+ cliLogger.log('No stale channels to prune');
2404
+ }
2405
+ else {
2406
+ for (const ch of stale) {
2407
+ await deleteChannelDirectoryById(ch.channel_id);
2408
+ cliLogger.log(`Pruned stale channel ${ch.channel_id} (${path.basename(ch.directory)})`);
2409
+ }
2410
+ cliLogger.log(`Pruned ${stale.length} stale channel(s)`);
2411
+ }
2412
+ // Re-filter to only show live entries after pruning
2413
+ const live = enriched.filter((ch) => {
2414
+ return !ch.deleted;
2415
+ });
2416
+ if (live.length === 0) {
2417
+ cliLogger.log('No projects registered');
2418
+ process.exit(0);
2419
+ }
2420
+ enriched.length = 0;
2421
+ enriched.push(...live);
2422
+ }
2383
2423
  if (options.json) {
2384
2424
  const output = enriched.map((ch) => ({
2385
2425
  channel_id: ch.channel_id,
2386
2426
  channel_name: ch.channelName,
2387
2427
  directory: ch.directory,
2388
2428
  folder_name: path.basename(ch.directory),
2429
+ deleted: ch.deleted,
2389
2430
  }));
2390
2431
  console.log(JSON.stringify(output, null, 2));
2391
2432
  process.exit(0);
2392
2433
  }
2393
2434
  for (const ch of enriched) {
2394
2435
  const folderName = path.basename(ch.directory);
2436
+ const deletedTag = ch.deleted ? ' (deleted from Discord)' : '';
2395
2437
  const channelLabel = ch.channelName ? `#${ch.channelName}` : ch.channel_id;
2396
- console.log(`\n${channelLabel}`);
2438
+ console.log(`\n${channelLabel}${deletedTag}`);
2397
2439
  console.log(` Folder: ${folderName}`);
2398
2440
  console.log(` Directory: ${ch.directory}`);
2399
2441
  console.log(` Channel ID: ${ch.channel_id}`);
@@ -2586,6 +2628,7 @@ cli
2586
2628
  .option('-t, --tunnel-id [id]', 'Custom tunnel ID (only for services safe to expose publicly; prefer random default)')
2587
2629
  .option('-h, --host [host]', 'Local host (default: localhost)')
2588
2630
  .option('-s, --server [url]', 'Tunnel server URL')
2631
+ .option('-k, --kill', 'Kill any existing process on the port before starting')
2589
2632
  .action(async (options) => {
2590
2633
  const { runTunnel, parseCommandFromArgv, CLI_NAME } = await import('traforo/run-tunnel');
2591
2634
  if (!options.port) {
@@ -2607,10 +2650,11 @@ cli
2607
2650
  baseDomain: 'kimaki.xyz',
2608
2651
  serverUrl: options.server,
2609
2652
  command: command.length > 0 ? command : undefined,
2653
+ kill: options.kill,
2610
2654
  });
2611
2655
  });
2612
2656
  cli
2613
- .command('screenshare', 'Share your screen via VNC tunnel. Auto-stops after 1 hour. Runs until Ctrl+C. Use tmux to run in background.')
2657
+ .command('screenshare', 'Share your screen via VNC tunnel. Auto-stops after 30 minutes. Runs until Ctrl+C. Use tmux to run in background.')
2614
2658
  .action(async () => {
2615
2659
  const { startScreenshare } = await import('./commands/screenshare.js');
2616
2660
  try {
@@ -27,23 +27,18 @@ export async function handleAbortCommand({ command, }) {
27
27
  });
28
28
  return;
29
29
  }
30
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
30
31
  const resolved = await resolveWorkingDirectory({
31
32
  channel: channel,
32
33
  });
33
34
  if (!resolved) {
34
- await command.reply({
35
- content: 'Could not determine project directory for this channel',
36
- flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
37
- });
35
+ await command.editReply('Could not determine project directory for this channel');
38
36
  return;
39
37
  }
40
38
  const { projectDirectory } = resolved;
41
39
  const sessionId = await getThreadSession(channel.id);
42
40
  if (!sessionId) {
43
- await command.reply({
44
- content: 'No active session in this thread',
45
- flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
46
- });
41
+ await command.editReply('No active session in this thread');
47
42
  return;
48
43
  }
49
44
  // abortActiveRun delegates to session.abort(), run settlement stays event-driven.
@@ -55,10 +50,7 @@ export async function handleAbortCommand({ command, }) {
55
50
  // No runtime but session exists — fall back to direct API abort
56
51
  const getClient = await initializeOpencodeForDirectory(projectDirectory);
57
52
  if (getClient instanceof Error) {
58
- await command.reply({
59
- content: `Failed to abort: ${getClient.message}`,
60
- flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
61
- });
53
+ await command.editReply(`Failed to abort: ${getClient.message}`);
62
54
  return;
63
55
  }
64
56
  try {
@@ -68,9 +60,6 @@ export async function handleAbortCommand({ command, }) {
68
60
  logger.error('[ABORT] API abort failed:', error);
69
61
  }
70
62
  }
71
- await command.reply({
72
- content: `Request **aborted**`,
73
- flags: SILENT_MESSAGE_FLAGS,
74
- });
63
+ await command.editReply('Request **aborted**');
75
64
  logger.log(`Session ${sessionId} aborted by user`);
76
65
  }
@@ -4,7 +4,7 @@
4
4
  import { ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags, } from 'discord.js';
5
5
  import crypto from 'node:crypto';
6
6
  import { getThreadSession } from '../database.js';
7
- import { NOTIFY_MESSAGE_FLAGS, resolveWorkingDirectory, sendThreadMessage, } from '../discord-utils.js';
7
+ import { NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS, resolveWorkingDirectory, sendThreadMessage, } from '../discord-utils.js';
8
8
  import { createLogger } from '../logger.js';
9
9
  import { notifyError } from '../sentry.js';
10
10
  import { getOrCreateRuntime, } from '../session-handler/thread-session-runtime.js';
@@ -101,7 +101,7 @@ async function sendClickedActionToModel({ interaction, thread, prompt, }) {
101
101
  mode: 'opencode',
102
102
  });
103
103
  }
104
- export async function showActionButtons({ thread, sessionId, directory, buttons, }) {
104
+ export async function showActionButtons({ thread, sessionId, directory, buttons, silent, }) {
105
105
  const safeButtons = buttons
106
106
  .slice(0, 3)
107
107
  .map((button) => {
@@ -145,7 +145,7 @@ export async function showActionButtons({ thread, sessionId, directory, buttons,
145
145
  const message = await thread.send({
146
146
  content: '**Action Required**',
147
147
  components: [row],
148
- flags: NOTIFY_MESSAGE_FLAGS,
148
+ flags: silent ? SILENT_MESSAGE_FLAGS : NOTIFY_MESSAGE_FLAGS,
149
149
  });
150
150
  context.messageId = message.id;
151
151
  logger.log(`Showed ${safeButtons.length} action button(s) for session ${sessionId}`);
@@ -9,7 +9,7 @@ import { abbreviatePath } from '../utils.js';
9
9
  import * as errore from 'errore';
10
10
  const logger = createLogger(LogPrefix.ADD_PROJECT);
11
11
  export async function handleAddProjectCommand({ command, }) {
12
- await command.deferReply({ ephemeral: false });
12
+ await command.deferReply();
13
13
  const projectId = command.options.getString('project', true);
14
14
  const guild = command.guild;
15
15
  if (!guild) {
@@ -3,7 +3,7 @@
3
3
  // for each question and collects user responses.
4
4
  import { StringSelectMenuBuilder, StringSelectMenuInteraction, ActionRowBuilder, MessageFlags, } from 'discord.js';
5
5
  import crypto from 'node:crypto';
6
- import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
6
+ import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
7
7
  import { getOpencodeClient } from '../opencode.js';
8
8
  import { createLogger, LogPrefix } from '../logger.js';
9
9
  const logger = createLogger(LogPrefix.ASK_QUESTION);
@@ -15,7 +15,7 @@ export const pendingQuestionContexts = new Map();
15
15
  * Show dropdown menus for question tool input.
16
16
  * Sends one message per question with the dropdown directly under the question text.
17
17
  */
18
- export async function showAskUserQuestionDropdowns({ thread, sessionId, directory, requestId, input, }) {
18
+ export async function showAskUserQuestionDropdowns({ thread, sessionId, directory, requestId, input, silent, }) {
19
19
  const contextHash = crypto.randomBytes(8).toString('hex');
20
20
  const context = {
21
21
  sessionId,
@@ -83,7 +83,7 @@ export async function showAskUserQuestionDropdowns({ thread, sessionId, director
83
83
  await thread.send({
84
84
  content: `**${(q.header || '').slice(0, 200)}**\n${q.question.slice(0, 1700)}`,
85
85
  components: [actionRow],
86
- flags: NOTIFY_MESSAGE_FLAGS,
86
+ flags: silent ? SILENT_MESSAGE_FLAGS : NOTIFY_MESSAGE_FLAGS,
87
87
  });
88
88
  }
89
89
  logger.log(`Showed ${input.questions.length} question dropdown(s) for session ${sessionId}`);
@@ -75,7 +75,7 @@ export async function handleContextUsageCommand({ command, }) {
75
75
  if (m.info.role !== 'assistant') {
76
76
  return false;
77
77
  }
78
- if (!('tokens' in m.info) || !m.info.tokens) {
78
+ if (!m.info.tokens) {
79
79
  return false;
80
80
  }
81
81
  return getTokenTotal(m.info.tokens) > 0;
@@ -58,7 +58,7 @@ export async function createNewProject({ guild, projectName, botName, }) {
58
58
  };
59
59
  }
60
60
  export async function handleCreateNewProjectCommand({ command, appId, }) {
61
- await command.deferReply({ ephemeral: false });
61
+ await command.deferReply();
62
62
  const projectName = command.options.getString('name', true);
63
63
  const guild = command.guild;
64
64
  const channel = command.channel;
@@ -3,7 +3,7 @@ import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectM
3
3
  import { getThreadSession, setThreadSession, setPartMessagesBatch, } from '../database.js';
4
4
  import { initializeOpencodeForDirectory } from '../opencode.js';
5
5
  import { resolveWorkingDirectory, resolveTextChannel, sendThreadMessage, } from '../discord-utils.js';
6
- import { collectLastAssistantParts } from '../message-formatting.js';
6
+ import { collectSessionChunks, batchChunksForDiscord } from '../message-formatting.js';
7
7
  import { createLogger, LogPrefix } from '../logger.js';
8
8
  import * as errore from 'errore';
9
9
  const sessionLogger = createLogger(LogPrefix.SESSION);
@@ -135,7 +135,7 @@ export async function handleForkSelectMenu(interaction) {
135
135
  });
136
136
  return;
137
137
  }
138
- await interaction.deferReply({ ephemeral: false });
138
+ await interaction.deferReply();
139
139
  const threadChannel = interaction.channel;
140
140
  if (!threadChannel) {
141
141
  await interaction.editReply('Could not access thread channel');
@@ -184,9 +184,11 @@ export async function handleForkSelectMenu(interaction) {
184
184
  autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
185
185
  reason: `Forked from session ${sessionId}`,
186
186
  });
187
+ // Claim the forked session immediately so external polling does not race
188
+ // and create a duplicate Sync thread before the rest of this setup runs.
189
+ await setThreadSession(thread.id, forkedSession.id);
187
190
  // Add user to thread so it appears in their sidebar
188
191
  await thread.members.add(interaction.user.id);
189
- await setThreadSession(thread.id, forkedSession.id);
190
192
  sessionLogger.log(`Created forked session ${forkedSession.id} in thread ${thread.id}`);
191
193
  await sendThreadMessage(thread, `**Forked session created!**\nFrom: \`${sessionId}\`\nNew session: \`${forkedSession.id}\``);
192
194
  // Fetch and display the last assistant messages from the forked session
@@ -194,13 +196,14 @@ export async function handleForkSelectMenu(interaction) {
194
196
  sessionID: forkedSession.id,
195
197
  });
196
198
  if (messagesResponse.data) {
197
- const { partIds, content } = collectLastAssistantParts({
199
+ const { chunks } = collectSessionChunks({
198
200
  messages: messagesResponse.data,
201
+ limit: 30,
199
202
  });
200
- if (content.trim()) {
201
- const discordMessage = await sendThreadMessage(thread, content);
202
- // Store part-message mappings atomically
203
- await setPartMessagesBatch(partIds.map((partId) => ({
203
+ const batched = batchChunksForDiscord(chunks);
204
+ for (const batch of batched) {
205
+ const discordMessage = await sendThreadMessage(thread, batch.content);
206
+ await setPartMessagesBatch(batch.partIds.map((partId) => ({
204
207
  partId,
205
208
  messageId: discordMessage.id,
206
209
  threadId: thread.id,
@@ -55,7 +55,7 @@ async function sendPromptToModel({ prompt, thread, projectDirectory, command, ap
55
55
  });
56
56
  }
57
57
  export async function handleMergeWorktreeCommand({ command, appId, }) {
58
- await command.deferReply({ ephemeral: false });
58
+ await command.deferReply();
59
59
  const channel = command.channel;
60
60
  if (!channel || !channel.isThread()) {
61
61
  await command.editReply('This command can only be used in a thread');
@@ -11,6 +11,10 @@ import { createWorktreeWithSubmodules, execAsync, listBranchesByLastCommit, vali
11
11
  import { WORKTREE_PREFIX } from './merge-worktree.js';
12
12
  import * as errore from 'errore';
13
13
  const logger = createLogger(LogPrefix.WORKTREE);
14
+ /** Status message shown while a worktree is being created. */
15
+ export function worktreeCreatingMessage(worktreeName) {
16
+ return `🌳 **Creating worktree: ${worktreeName}**\nā³ Setting up...`;
17
+ }
14
18
  class WorktreeError extends Error {
15
19
  constructor(message, options) {
16
20
  super(message, options);
@@ -65,37 +69,64 @@ async function getProjectDirectoryFromChannel(channel) {
65
69
  return channelConfig.directory;
66
70
  }
67
71
  /**
68
- * Create worktree in background and update starter message when done.
72
+ * Create worktree and update the status message when done.
73
+ * Handles the full lifecycle: pending DB entry, git creation, DB ready/error,
74
+ * tree emoji reaction, and editing the status message.
75
+ *
76
+ * starterMessage is optional — if omitted, status edits are skipped (creation
77
+ * still proceeds). This keeps worktree creation independent of Discord message
78
+ * delivery, so a transient send failure never silently skips the worktree.
79
+ *
80
+ * Returns the worktree directory on success, or an Error on failure.
81
+ * Never throws — all internal errors are caught and returned as Error values.
69
82
  */
70
- async function createWorktreeInBackground({ thread, starterMessage, worktreeName, projectDirectory, baseBranch, rest, }) {
71
- logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory}${baseBranch ? ` from ${baseBranch}` : ''}`);
72
- const worktreeResult = await createWorktreeWithSubmodules({
73
- directory: projectDirectory,
74
- name: worktreeName,
75
- baseBranch,
76
- });
77
- if (worktreeResult instanceof Error) {
78
- const errorMsg = worktreeResult.message;
79
- logger.error('[NEW-WORKTREE] Error:', worktreeResult);
80
- await setWorktreeError({ threadId: thread.id, errorMessage: errorMsg });
81
- await starterMessage.edit(`🌳 **Worktree: ${worktreeName}**\nāŒ ${errorMsg}`);
82
- return;
83
- }
84
- // Success - update database and edit starter message
85
- await setWorktreeReady({
86
- threadId: thread.id,
87
- worktreeDirectory: worktreeResult.directory,
88
- });
89
- // React with tree emoji to mark as worktree thread
90
- await reactToThread({
91
- rest,
92
- threadId: thread.id,
93
- channelId: thread.parentId || undefined,
94
- emoji: '🌳',
83
+ export async function createWorktreeInBackground({ thread, starterMessage, worktreeName, projectDirectory, baseBranch, rest, }) {
84
+ return errore.tryAsync({
85
+ try: async () => {
86
+ logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory}${baseBranch ? ` from ${baseBranch}` : ''}`);
87
+ await createPendingWorktree({
88
+ threadId: thread.id,
89
+ worktreeName,
90
+ projectDirectory,
91
+ });
92
+ const worktreeResult = await createWorktreeWithSubmodules({
93
+ directory: projectDirectory,
94
+ name: worktreeName,
95
+ baseBranch,
96
+ });
97
+ if (worktreeResult instanceof Error) {
98
+ const errorMsg = worktreeResult.message;
99
+ logger.error('[WORKTREE] Creation failed:', worktreeResult);
100
+ await setWorktreeError({ threadId: thread.id, errorMessage: errorMsg });
101
+ await starterMessage
102
+ ?.edit(`🌳 **Worktree: ${worktreeName}**\nāŒ ${errorMsg}`)
103
+ .catch(() => { });
104
+ return worktreeResult;
105
+ }
106
+ // Success - update database and edit starter message
107
+ await setWorktreeReady({
108
+ threadId: thread.id,
109
+ worktreeDirectory: worktreeResult.directory,
110
+ });
111
+ // React with tree emoji to mark as worktree thread
112
+ await reactToThread({
113
+ rest,
114
+ threadId: thread.id,
115
+ channelId: thread.parentId || undefined,
116
+ emoji: '🌳',
117
+ });
118
+ await starterMessage
119
+ ?.edit(`🌳 **Worktree: ${worktreeName}**\n` +
120
+ `šŸ“ \`${worktreeResult.directory}\`\n` +
121
+ `🌿 Branch: \`${worktreeResult.branch}\``)
122
+ .catch(() => { });
123
+ return worktreeResult.directory;
124
+ },
125
+ catch: (e) => {
126
+ logger.error('[WORKTREE] Unexpected error in createWorktreeInBackground:', e);
127
+ return new Error(`Worktree creation failed: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
128
+ },
95
129
  });
96
- await starterMessage.edit(`🌳 **Worktree: ${worktreeName}**\n` +
97
- `šŸ“ \`${worktreeResult.directory}\`\n` +
98
- `🌿 Branch: \`${worktreeResult.branch}\``);
99
130
  }
100
131
  async function findExistingWorktreePath({ projectDirectory, worktreeName, }) {
101
132
  const listResult = await errore.tryAsync({
@@ -121,7 +152,7 @@ async function findExistingWorktreePath({ projectDirectory, worktreeName, }) {
121
152
  return undefined;
122
153
  }
123
154
  export async function handleNewWorktreeCommand({ command, }) {
124
- await command.deferReply({ ephemeral: false });
155
+ await command.deferReply();
125
156
  const channel = command.channel;
126
157
  if (!channel) {
127
158
  await command.editReply('Cannot determine channel');
@@ -187,7 +218,7 @@ export async function handleNewWorktreeCommand({ command, }) {
187
218
  const result = await errore.tryAsync({
188
219
  try: async () => {
189
220
  const starterMessage = await textChannel.send({
190
- content: `🌳 **Creating worktree: ${worktreeName}**\nā³ Setting up...`,
221
+ content: worktreeCreatingMessage(worktreeName),
191
222
  flags: SILENT_MESSAGE_FLAGS,
192
223
  });
193
224
  const thread = await starterMessage.startThread({
@@ -207,12 +238,6 @@ export async function handleNewWorktreeCommand({ command, }) {
207
238
  return;
208
239
  }
209
240
  const { thread, starterMessage } = result;
210
- // Store pending worktree in database
211
- await createPendingWorktree({
212
- threadId: thread.id,
213
- worktreeName,
214
- projectDirectory,
215
- });
216
241
  await command.editReply(`Creating worktree in ${thread.toString()}`);
217
242
  // Create worktree in background (don't await)
218
243
  createWorktreeInBackground({
@@ -282,15 +307,9 @@ async function handleWorktreeInThread({ command, thread, }) {
282
307
  await command.editReply(`Worktree \`${worktreeName}\` already exists at \`${existingWorktreePath}\``);
283
308
  return;
284
309
  }
285
- // Store pending worktree in database for this existing thread
286
- await createPendingWorktree({
287
- threadId: thread.id,
288
- worktreeName,
289
- projectDirectory,
290
- });
291
310
  // Send status message in thread
292
311
  const statusMessage = await thread.send({
293
- content: `🌳 **Creating worktree: ${worktreeName}**\nā³ Setting up...`,
312
+ content: worktreeCreatingMessage(worktreeName),
294
313
  flags: SILENT_MESSAGE_FLAGS,
295
314
  });
296
315
  await command.editReply(`Creating worktree \`${worktreeName}\` for this thread...`);
@@ -6,7 +6,7 @@ import { createLogger, LogPrefix } from '../logger.js';
6
6
  import { abbreviatePath } from '../utils.js';
7
7
  const logger = createLogger(LogPrefix.REMOVE_PROJECT);
8
8
  export async function handleRemoveProjectCommand({ command, appId, }) {
9
- await command.deferReply({ ephemeral: false });
9
+ await command.deferReply();
10
10
  const directory = command.options.getString('project', true);
11
11
  const guild = command.guild;
12
12
  if (!guild) {
@@ -4,12 +4,12 @@ import fs from 'node:fs';
4
4
  import { getChannelDirectory, setThreadSession, setPartMessagesBatch, getAllThreadSessionIds, } from '../database.js';
5
5
  import { initializeOpencodeForDirectory } from '../opencode.js';
6
6
  import { sendThreadMessage, resolveProjectDirectoryFromAutocomplete, NOTIFY_MESSAGE_FLAGS, } from '../discord-utils.js';
7
- import { collectLastAssistantParts } from '../message-formatting.js';
7
+ import { collectSessionChunks, batchChunksForDiscord } from '../message-formatting.js';
8
8
  import { createLogger, LogPrefix } from '../logger.js';
9
9
  import * as errore from 'errore';
10
10
  const logger = createLogger(LogPrefix.RESUME);
11
11
  export async function handleResumeCommand({ command, }) {
12
- await command.deferReply({ ephemeral: false });
12
+ await command.deferReply();
13
13
  const sessionId = command.options.getString('session', true);
14
14
  const channel = command.channel;
15
15
  const isThread = channel &&
@@ -56,9 +56,11 @@ export async function handleResumeCommand({ command, }) {
56
56
  autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
57
57
  reason: `Resuming session ${sessionId}`,
58
58
  });
59
+ // Claim the resumed session immediately so external polling does not race
60
+ // and create a duplicate Sync thread before the rest of this setup runs.
61
+ await setThreadSession(thread.id, sessionId);
59
62
  // Add user to thread so it appears in their sidebar
60
63
  await thread.members.add(command.user.id);
61
- await setThreadSession(thread.id, sessionId);
62
64
  logger.log(`[RESUME] Created thread ${thread.id} for session ${sessionId}`);
63
65
  const messagesResponse = await getClient().session.messages({
64
66
  sessionID: sessionId,
@@ -70,16 +72,17 @@ export async function handleResumeCommand({ command, }) {
70
72
  await command.editReply(`Resumed session "${sessionTitle}" in ${thread.toString()}`);
71
73
  await sendThreadMessage(thread, `**Resumed session:** ${sessionTitle}\n**Created:** ${new Date(sessionResponse.data.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`);
72
74
  try {
73
- const { partIds, content, skippedCount } = collectLastAssistantParts({
75
+ const { chunks, skippedCount } = collectSessionChunks({
74
76
  messages,
77
+ limit: 30,
75
78
  });
76
79
  if (skippedCount > 0) {
77
80
  await sendThreadMessage(thread, `*Skipped ${skippedCount} older assistant parts...*`);
78
81
  }
79
- if (content.trim()) {
80
- const discordMessage = await sendThreadMessage(thread, content);
81
- // Store part-message mappings atomically
82
- await setPartMessagesBatch(partIds.map((partId) => ({
82
+ const batched = batchChunksForDiscord(chunks);
83
+ for (const batch of batched) {
84
+ const discordMessage = await sendThreadMessage(thread, batch.content);
85
+ await setPartMessagesBatch(batch.partIds.map((partId) => ({
83
86
  partId,
84
87
  messageId: discordMessage.id,
85
88
  threadId: thread.id,
@@ -15,11 +15,14 @@ import { startWebsockify } from '../websockify.js';
15
15
  import { createLogger } from '../logger.js';
16
16
  import { execAsync } from '../worktrees.js';
17
17
  const logger = createLogger('SCREEN');
18
+ const SECURE_REPLY_FLAGS = MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS;
18
19
  /** One active screenshare per guild (Discord) or per machine (CLI) */
19
20
  const activeSessions = new Map();
20
21
  const VNC_PORT = 5900;
21
- const MAX_SESSION_MS = 60 * 60 * 1000; // 1 hour
22
+ const MAX_SESSION_MINUTES = 30;
23
+ const MAX_SESSION_MS = MAX_SESSION_MINUTES * 60 * 1000;
22
24
  const TUNNEL_BASE_DOMAIN = 'kimaki.xyz';
25
+ const SCREENSHARE_TUNNEL_ID_BYTES = 16;
23
26
  // Public noVNC client — we point it at our tunnel URL
24
27
  export function buildNoVncUrl({ tunnelHost }) {
25
28
  const params = new URLSearchParams({
@@ -32,6 +35,9 @@ export function buildNoVncUrl({ tunnelHost }) {
32
35
  });
33
36
  return `https://novnc.com/noVNC/vnc.html?${params.toString()}`;
34
37
  }
38
+ export function createScreenshareTunnelId() {
39
+ return crypto.randomBytes(SCREENSHARE_TUNNEL_ID_BYTES).toString('hex');
40
+ }
35
41
  // macOS has two separate services:
36
42
  // - "Screen Sharing" = view-only VNC (com.apple.screensharing)
37
43
  // - "Remote Management" = full control VNC with mouse/keyboard (ARDAgent)
@@ -170,7 +176,7 @@ export async function startScreenshare({ sessionKey, startedBy, }) {
170
176
  throw err;
171
177
  }
172
178
  // Step 3: create tunnel
173
- const tunnelId = crypto.randomBytes(8).toString('hex');
179
+ const tunnelId = createScreenshareTunnelId();
174
180
  const tunnelClient = new TunnelClient({
175
181
  localPort: wsInstance.port,
176
182
  tunnelId,
@@ -197,9 +203,9 @@ export async function startScreenshare({ sessionKey, startedBy, }) {
197
203
  const tunnelHost = `${tunnelId}-tunnel.${TUNNEL_BASE_DOMAIN}`;
198
204
  const tunnelUrl = `https://${tunnelHost}`;
199
205
  const noVncUrl = buildNoVncUrl({ tunnelHost });
200
- // Auto-kill after 1 hour
206
+ // Auto-kill after a short session so a leaked URL does not stay usable all day.
201
207
  const timeoutTimer = setTimeout(() => {
202
- logger.log(`Screen share auto-stopped after 1 hour (key: ${sessionKey})`);
208
+ logger.log(`Screen share auto-stopped after ${MAX_SESSION_MINUTES} minutes (key: ${sessionKey})`);
203
209
  stopScreenshare({ sessionKey });
204
210
  }, MAX_SESSION_MS);
205
211
  // Don't keep the process alive just for this timer
@@ -240,14 +246,16 @@ export async function handleScreenshareCommand({ command, }) {
240
246
  });
241
247
  return;
242
248
  }
243
- await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
249
+ await command.deferReply({ flags: SECURE_REPLY_FLAGS });
244
250
  try {
245
251
  const session = await startScreenshare({
246
252
  sessionKey: guildId,
247
253
  startedBy: command.user.tag,
248
254
  });
249
255
  await command.editReply({
250
- content: `Screen sharing started\n${session.noVncUrl}`,
256
+ content: `Screen sharing started. This reply is private and the URL uses a high-entropy tunnel id. ` +
257
+ `It will auto-stop after ${MAX_SESSION_MINUTES} minutes. Use /screenshare-stop to stop sooner.\n` +
258
+ `${session.noVncUrl}`,
251
259
  });
252
260
  }
253
261
  catch (err) {