kimaki 0.4.82 → 0.4.83

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 (246) hide show
  1. package/LICENSE +21 -0
  2. package/dist/anthropic-auth-plugin.js +7 -0
  3. package/dist/cli.js +42 -5
  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/session.js +1 -1
  16. package/dist/commands/undo-redo.js +91 -7
  17. package/dist/commands/user-command.js +1 -1
  18. package/dist/database.js +53 -2
  19. package/dist/db.js +6 -0
  20. package/dist/discord-bot.js +104 -90
  21. package/dist/external-opencode-sync.js +538 -0
  22. package/dist/external-opencode-sync.test.js +151 -0
  23. package/dist/gateway-proxy.e2e.test.js +51 -0
  24. package/dist/genai.js +1 -1
  25. package/dist/generated/enums.js +4 -0
  26. package/dist/generated/internal/class.js +4 -4
  27. package/dist/generated/internal/prismaNamespace.js +1 -0
  28. package/dist/generated/internal/prismaNamespaceBrowser.js +1 -0
  29. package/dist/generated/models/external_session_pending_prompts.js +1 -0
  30. package/dist/hrana-server.js +14 -285
  31. package/dist/hrana-server.test.js +4 -2
  32. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +7 -0
  33. package/dist/kimaki-opencode-plugin.js +2 -0
  34. package/dist/kitty-graphics-parser.js +3 -0
  35. package/dist/kitty-graphics-parser.test.js +276 -0
  36. package/dist/kitty-graphics-plugin.js +3 -0
  37. package/dist/markdown.js +4 -4
  38. package/dist/markdown.test.js +1 -1
  39. package/dist/message-formatting.js +54 -15
  40. package/dist/openai-realtime.js +9 -13
  41. package/dist/opencode.js +28 -5
  42. package/dist/queue-advanced-e2e-setup.js +53 -0
  43. package/dist/queue-advanced-permissions-typing.e2e.test.js +5 -5
  44. package/dist/queue-advanced-typing.e2e.test.js +9 -22
  45. package/dist/session-handler/event-stream-state.js +101 -7
  46. package/dist/session-handler/event-stream-state.test.js +7 -3
  47. package/dist/session-handler/thread-session-runtime.js +77 -9
  48. package/dist/system-message.js +6 -0
  49. package/dist/system-message.test.js +19 -0
  50. package/dist/task-runner.js +1 -1
  51. package/dist/thread-message-queue.e2e.test.js +8 -14
  52. package/dist/tools.js +1 -1
  53. package/dist/undo-redo.e2e.test.js +20 -25
  54. package/package.json +9 -6
  55. package/schema.prisma +6 -0
  56. package/skills/npm-package/SKILL.md +1 -0
  57. package/skills/proxyman/SKILL.md +215 -0
  58. package/skills/usecomputer/SKILL.md +339 -0
  59. package/src/ai-tool-to-genai.ts +1 -0
  60. package/src/anthropic-auth-plugin.ts +7 -0
  61. package/src/cli.ts +44 -5
  62. package/src/commands/abort.ts +6 -16
  63. package/src/commands/action-buttons.ts +5 -1
  64. package/src/commands/add-project.ts +1 -1
  65. package/src/commands/ask-question.ts +5 -2
  66. package/src/commands/context-usage.ts +1 -1
  67. package/src/commands/create-new-project.ts +1 -1
  68. package/src/commands/fork.ts +12 -11
  69. package/src/commands/merge-worktree.ts +1 -1
  70. package/src/commands/new-worktree.ts +74 -55
  71. package/src/commands/remove-project.ts +1 -1
  72. package/src/commands/resume.ts +12 -10
  73. package/src/commands/session.ts +1 -1
  74. package/src/commands/undo-redo.ts +108 -10
  75. package/src/commands/user-command.ts +1 -1
  76. package/src/database.ts +72 -3
  77. package/src/db.ts +8 -0
  78. package/src/discord-bot.ts +125 -97
  79. package/src/external-opencode-sync.ts +760 -0
  80. package/src/gateway-proxy.e2e.test.ts +63 -0
  81. package/src/genai.ts +1 -1
  82. package/src/generated/commonInputTypes.ts +34 -0
  83. package/src/generated/enums.ts +8 -0
  84. package/src/generated/internal/class.ts +4 -4
  85. package/src/generated/internal/prismaNamespace.ts +8 -0
  86. package/src/generated/internal/prismaNamespaceBrowser.ts +1 -0
  87. package/src/generated/models/thread_sessions.ts +53 -1
  88. package/src/hrana-server.test.ts +8 -2
  89. package/src/hrana-server.ts +18 -390
  90. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +7 -0
  91. package/src/kimaki-opencode-plugin.ts +2 -0
  92. package/src/markdown.test.ts +1 -1
  93. package/src/markdown.ts +4 -4
  94. package/src/message-formatting.ts +66 -17
  95. package/src/openai-realtime.ts +6 -10
  96. package/src/opencode.ts +31 -7
  97. package/src/queue-advanced-e2e-setup.ts +55 -0
  98. package/src/queue-advanced-permissions-typing.e2e.test.ts +5 -5
  99. package/src/queue-advanced-typing.e2e.test.ts +9 -22
  100. package/src/schema.sql +1 -0
  101. package/src/session-handler/event-stream-state.test.ts +7 -2
  102. package/src/session-handler/event-stream-state.ts +128 -7
  103. package/src/session-handler/thread-runtime-state.ts +5 -0
  104. package/src/session-handler/thread-session-runtime.ts +93 -11
  105. package/src/system-message.ts +11 -0
  106. package/src/task-runner.ts +1 -1
  107. package/src/thread-message-queue.e2e.test.ts +8 -14
  108. package/src/tools.ts +1 -1
  109. package/src/undo-redo.e2e.test.ts +28 -26
  110. package/skills/jitter/node_modules/.bin/esbuild +0 -21
  111. package/skills/jitter/node_modules/.bin/tsc +0 -21
  112. package/skills/jitter/node_modules/.bin/tsserver +0 -21
  113. package/skills/jitter/node_modules/typescript/LICENSE.txt +0 -55
  114. package/skills/jitter/node_modules/typescript/README.md +0 -50
  115. package/skills/jitter/node_modules/typescript/SECURITY.md +0 -41
  116. package/skills/jitter/node_modules/typescript/ThirdPartyNoticeText.txt +0 -193
  117. package/skills/jitter/node_modules/typescript/bin/tsc +0 -2
  118. package/skills/jitter/node_modules/typescript/bin/tsserver +0 -2
  119. package/skills/jitter/node_modules/typescript/lib/_tsc.js +0 -133792
  120. package/skills/jitter/node_modules/typescript/lib/_tsserver.js +0 -659
  121. package/skills/jitter/node_modules/typescript/lib/_typingsInstaller.js +0 -222
  122. package/skills/jitter/node_modules/typescript/lib/cs/diagnosticMessages.generated.json +0 -2122
  123. package/skills/jitter/node_modules/typescript/lib/de/diagnosticMessages.generated.json +0 -2122
  124. package/skills/jitter/node_modules/typescript/lib/es/diagnosticMessages.generated.json +0 -2122
  125. package/skills/jitter/node_modules/typescript/lib/fr/diagnosticMessages.generated.json +0 -2122
  126. package/skills/jitter/node_modules/typescript/lib/it/diagnosticMessages.generated.json +0 -2122
  127. package/skills/jitter/node_modules/typescript/lib/ja/diagnosticMessages.generated.json +0 -2122
  128. package/skills/jitter/node_modules/typescript/lib/ko/diagnosticMessages.generated.json +0 -2122
  129. package/skills/jitter/node_modules/typescript/lib/lib.d.ts +0 -22
  130. package/skills/jitter/node_modules/typescript/lib/lib.decorators.d.ts +0 -384
  131. package/skills/jitter/node_modules/typescript/lib/lib.decorators.legacy.d.ts +0 -22
  132. package/skills/jitter/node_modules/typescript/lib/lib.dom.asynciterable.d.ts +0 -41
  133. package/skills/jitter/node_modules/typescript/lib/lib.dom.d.ts +0 -39429
  134. package/skills/jitter/node_modules/typescript/lib/lib.dom.iterable.d.ts +0 -571
  135. package/skills/jitter/node_modules/typescript/lib/lib.es2015.collection.d.ts +0 -147
  136. package/skills/jitter/node_modules/typescript/lib/lib.es2015.core.d.ts +0 -597
  137. package/skills/jitter/node_modules/typescript/lib/lib.es2015.d.ts +0 -28
  138. package/skills/jitter/node_modules/typescript/lib/lib.es2015.generator.d.ts +0 -77
  139. package/skills/jitter/node_modules/typescript/lib/lib.es2015.iterable.d.ts +0 -605
  140. package/skills/jitter/node_modules/typescript/lib/lib.es2015.promise.d.ts +0 -81
  141. package/skills/jitter/node_modules/typescript/lib/lib.es2015.proxy.d.ts +0 -128
  142. package/skills/jitter/node_modules/typescript/lib/lib.es2015.reflect.d.ts +0 -144
  143. package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.d.ts +0 -46
  144. package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts +0 -326
  145. package/skills/jitter/node_modules/typescript/lib/lib.es2016.array.include.d.ts +0 -116
  146. package/skills/jitter/node_modules/typescript/lib/lib.es2016.d.ts +0 -21
  147. package/skills/jitter/node_modules/typescript/lib/lib.es2016.full.d.ts +0 -23
  148. package/skills/jitter/node_modules/typescript/lib/lib.es2016.intl.d.ts +0 -31
  149. package/skills/jitter/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts +0 -21
  150. package/skills/jitter/node_modules/typescript/lib/lib.es2017.d.ts +0 -26
  151. package/skills/jitter/node_modules/typescript/lib/lib.es2017.date.d.ts +0 -31
  152. package/skills/jitter/node_modules/typescript/lib/lib.es2017.full.d.ts +0 -23
  153. package/skills/jitter/node_modules/typescript/lib/lib.es2017.intl.d.ts +0 -44
  154. package/skills/jitter/node_modules/typescript/lib/lib.es2017.object.d.ts +0 -49
  155. package/skills/jitter/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts +0 -135
  156. package/skills/jitter/node_modules/typescript/lib/lib.es2017.string.d.ts +0 -45
  157. package/skills/jitter/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts +0 -53
  158. package/skills/jitter/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts +0 -77
  159. package/skills/jitter/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts +0 -53
  160. package/skills/jitter/node_modules/typescript/lib/lib.es2018.d.ts +0 -24
  161. package/skills/jitter/node_modules/typescript/lib/lib.es2018.full.d.ts +0 -24
  162. package/skills/jitter/node_modules/typescript/lib/lib.es2018.intl.d.ts +0 -83
  163. package/skills/jitter/node_modules/typescript/lib/lib.es2018.promise.d.ts +0 -30
  164. package/skills/jitter/node_modules/typescript/lib/lib.es2018.regexp.d.ts +0 -37
  165. package/skills/jitter/node_modules/typescript/lib/lib.es2019.array.d.ts +0 -79
  166. package/skills/jitter/node_modules/typescript/lib/lib.es2019.d.ts +0 -24
  167. package/skills/jitter/node_modules/typescript/lib/lib.es2019.full.d.ts +0 -24
  168. package/skills/jitter/node_modules/typescript/lib/lib.es2019.intl.d.ts +0 -23
  169. package/skills/jitter/node_modules/typescript/lib/lib.es2019.object.d.ts +0 -33
  170. package/skills/jitter/node_modules/typescript/lib/lib.es2019.string.d.ts +0 -37
  171. package/skills/jitter/node_modules/typescript/lib/lib.es2019.symbol.d.ts +0 -24
  172. package/skills/jitter/node_modules/typescript/lib/lib.es2020.bigint.d.ts +0 -765
  173. package/skills/jitter/node_modules/typescript/lib/lib.es2020.d.ts +0 -27
  174. package/skills/jitter/node_modules/typescript/lib/lib.es2020.date.d.ts +0 -42
  175. package/skills/jitter/node_modules/typescript/lib/lib.es2020.full.d.ts +0 -24
  176. package/skills/jitter/node_modules/typescript/lib/lib.es2020.intl.d.ts +0 -474
  177. package/skills/jitter/node_modules/typescript/lib/lib.es2020.number.d.ts +0 -28
  178. package/skills/jitter/node_modules/typescript/lib/lib.es2020.promise.d.ts +0 -47
  179. package/skills/jitter/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts +0 -99
  180. package/skills/jitter/node_modules/typescript/lib/lib.es2020.string.d.ts +0 -44
  181. package/skills/jitter/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts +0 -41
  182. package/skills/jitter/node_modules/typescript/lib/lib.es2021.d.ts +0 -23
  183. package/skills/jitter/node_modules/typescript/lib/lib.es2021.full.d.ts +0 -24
  184. package/skills/jitter/node_modules/typescript/lib/lib.es2021.intl.d.ts +0 -166
  185. package/skills/jitter/node_modules/typescript/lib/lib.es2021.promise.d.ts +0 -48
  186. package/skills/jitter/node_modules/typescript/lib/lib.es2021.string.d.ts +0 -33
  187. package/skills/jitter/node_modules/typescript/lib/lib.es2021.weakref.d.ts +0 -78
  188. package/skills/jitter/node_modules/typescript/lib/lib.es2022.array.d.ts +0 -121
  189. package/skills/jitter/node_modules/typescript/lib/lib.es2022.d.ts +0 -25
  190. package/skills/jitter/node_modules/typescript/lib/lib.es2022.error.d.ts +0 -75
  191. package/skills/jitter/node_modules/typescript/lib/lib.es2022.full.d.ts +0 -24
  192. package/skills/jitter/node_modules/typescript/lib/lib.es2022.intl.d.ts +0 -145
  193. package/skills/jitter/node_modules/typescript/lib/lib.es2022.object.d.ts +0 -26
  194. package/skills/jitter/node_modules/typescript/lib/lib.es2022.regexp.d.ts +0 -39
  195. package/skills/jitter/node_modules/typescript/lib/lib.es2022.string.d.ts +0 -25
  196. package/skills/jitter/node_modules/typescript/lib/lib.es2023.array.d.ts +0 -924
  197. package/skills/jitter/node_modules/typescript/lib/lib.es2023.collection.d.ts +0 -21
  198. package/skills/jitter/node_modules/typescript/lib/lib.es2023.d.ts +0 -22
  199. package/skills/jitter/node_modules/typescript/lib/lib.es2023.full.d.ts +0 -24
  200. package/skills/jitter/node_modules/typescript/lib/lib.es2023.intl.d.ts +0 -56
  201. package/skills/jitter/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts +0 -65
  202. package/skills/jitter/node_modules/typescript/lib/lib.es2024.collection.d.ts +0 -29
  203. package/skills/jitter/node_modules/typescript/lib/lib.es2024.d.ts +0 -26
  204. package/skills/jitter/node_modules/typescript/lib/lib.es2024.full.d.ts +0 -24
  205. package/skills/jitter/node_modules/typescript/lib/lib.es2024.object.d.ts +0 -29
  206. package/skills/jitter/node_modules/typescript/lib/lib.es2024.promise.d.ts +0 -35
  207. package/skills/jitter/node_modules/typescript/lib/lib.es2024.regexp.d.ts +0 -25
  208. package/skills/jitter/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts +0 -68
  209. package/skills/jitter/node_modules/typescript/lib/lib.es2024.string.d.ts +0 -29
  210. package/skills/jitter/node_modules/typescript/lib/lib.es5.d.ts +0 -4601
  211. package/skills/jitter/node_modules/typescript/lib/lib.es6.d.ts +0 -23
  212. package/skills/jitter/node_modules/typescript/lib/lib.esnext.array.d.ts +0 -35
  213. package/skills/jitter/node_modules/typescript/lib/lib.esnext.collection.d.ts +0 -96
  214. package/skills/jitter/node_modules/typescript/lib/lib.esnext.d.ts +0 -29
  215. package/skills/jitter/node_modules/typescript/lib/lib.esnext.decorators.d.ts +0 -28
  216. package/skills/jitter/node_modules/typescript/lib/lib.esnext.disposable.d.ts +0 -193
  217. package/skills/jitter/node_modules/typescript/lib/lib.esnext.error.d.ts +0 -24
  218. package/skills/jitter/node_modules/typescript/lib/lib.esnext.float16.d.ts +0 -443
  219. package/skills/jitter/node_modules/typescript/lib/lib.esnext.full.d.ts +0 -24
  220. package/skills/jitter/node_modules/typescript/lib/lib.esnext.intl.d.ts +0 -21
  221. package/skills/jitter/node_modules/typescript/lib/lib.esnext.iterator.d.ts +0 -148
  222. package/skills/jitter/node_modules/typescript/lib/lib.esnext.promise.d.ts +0 -34
  223. package/skills/jitter/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts +0 -25
  224. package/skills/jitter/node_modules/typescript/lib/lib.scripthost.d.ts +0 -322
  225. package/skills/jitter/node_modules/typescript/lib/lib.webworker.asynciterable.d.ts +0 -41
  226. package/skills/jitter/node_modules/typescript/lib/lib.webworker.d.ts +0 -13150
  227. package/skills/jitter/node_modules/typescript/lib/lib.webworker.importscripts.d.ts +0 -23
  228. package/skills/jitter/node_modules/typescript/lib/lib.webworker.iterable.d.ts +0 -340
  229. package/skills/jitter/node_modules/typescript/lib/pl/diagnosticMessages.generated.json +0 -2122
  230. package/skills/jitter/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json +0 -2122
  231. package/skills/jitter/node_modules/typescript/lib/ru/diagnosticMessages.generated.json +0 -2122
  232. package/skills/jitter/node_modules/typescript/lib/tr/diagnosticMessages.generated.json +0 -2122
  233. package/skills/jitter/node_modules/typescript/lib/tsc.js +0 -8
  234. package/skills/jitter/node_modules/typescript/lib/tsserver.js +0 -8
  235. package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.d.ts +0 -17
  236. package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.js +0 -21
  237. package/skills/jitter/node_modules/typescript/lib/typesMap.json +0 -497
  238. package/skills/jitter/node_modules/typescript/lib/typescript.d.ts +0 -11438
  239. package/skills/jitter/node_modules/typescript/lib/typescript.js +0 -200253
  240. package/skills/jitter/node_modules/typescript/lib/typingsInstaller.js +0 -8
  241. package/skills/jitter/node_modules/typescript/lib/watchGuard.js +0 -53
  242. package/skills/jitter/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json +0 -2122
  243. package/skills/jitter/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json +0 -2122
  244. package/skills/jitter/node_modules/typescript/node_modules/.bin/tsc +0 -21
  245. package/skills/jitter/node_modules/typescript/node_modules/.bin/tsserver +0 -21
  246. 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';
@@ -2353,6 +2353,7 @@ cli
2353
2353
  cli
2354
2354
  .command('project list', 'List all registered projects with their Discord channels')
2355
2355
  .option('--json', 'Output as JSON')
2356
+ .option('--prune', 'Remove stale entries whose Discord channel no longer exists')
2356
2357
  .action(async (options) => {
2357
2358
  await initDatabase();
2358
2359
  const prisma = await getPrisma();
@@ -2369,31 +2370,67 @@ cli
2369
2370
  const rest = botRow ? createDiscordRest(botRow.token) : null;
2370
2371
  const enriched = await Promise.all(channels.map(async (ch) => {
2371
2372
  let channelName = '';
2373
+ let deleted = false;
2372
2374
  if (rest) {
2373
2375
  try {
2374
2376
  const data = (await rest.get(Routes.channel(ch.channel_id)));
2375
2377
  channelName = data.name || '';
2376
2378
  }
2377
- catch {
2378
- // Channel may have been deleted from Discord
2379
+ catch (error) {
2380
+ // Only mark as deleted for Unknown Channel (10003) or 404,
2381
+ // not transient errors like rate limits or 5xx
2382
+ const isUnknownChannel = error instanceof Error &&
2383
+ 'code' in error &&
2384
+ 'status' in error &&
2385
+ (error.code === 10003 ||
2386
+ error.status === 404);
2387
+ deleted = isUnknownChannel;
2379
2388
  }
2380
2389
  }
2381
- return { ...ch, channelName };
2390
+ return { ...ch, channelName, deleted };
2382
2391
  }));
2392
+ // Prune stale entries if requested
2393
+ if (options.prune) {
2394
+ const stale = enriched.filter((ch) => {
2395
+ return ch.deleted;
2396
+ });
2397
+ if (stale.length === 0) {
2398
+ cliLogger.log('No stale channels to prune');
2399
+ }
2400
+ else {
2401
+ for (const ch of stale) {
2402
+ await deleteChannelDirectoryById(ch.channel_id);
2403
+ cliLogger.log(`Pruned stale channel ${ch.channel_id} (${path.basename(ch.directory)})`);
2404
+ }
2405
+ cliLogger.log(`Pruned ${stale.length} stale channel(s)`);
2406
+ }
2407
+ // Re-filter to only show live entries after pruning
2408
+ const live = enriched.filter((ch) => {
2409
+ return !ch.deleted;
2410
+ });
2411
+ if (live.length === 0) {
2412
+ cliLogger.log('No projects registered');
2413
+ process.exit(0);
2414
+ }
2415
+ enriched.length = 0;
2416
+ enriched.push(...live);
2417
+ }
2383
2418
  if (options.json) {
2384
2419
  const output = enriched.map((ch) => ({
2385
2420
  channel_id: ch.channel_id,
2386
2421
  channel_name: ch.channelName,
2387
2422
  directory: ch.directory,
2388
2423
  folder_name: path.basename(ch.directory),
2424
+ deleted: ch.deleted,
2389
2425
  }));
2390
2426
  console.log(JSON.stringify(output, null, 2));
2391
2427
  process.exit(0);
2392
2428
  }
2393
2429
  for (const ch of enriched) {
2394
2430
  const folderName = path.basename(ch.directory);
2431
+ const deletedTag = ch.deleted ? ' (deleted from Discord)' : '';
2395
2432
  const channelLabel = ch.channelName ? `#${ch.channelName}` : ch.channel_id;
2396
- console.log(`\n${channelLabel}`);
2433
+ console.log(`\n${channelLabel}${deletedTag}`);
2397
2434
  console.log(` Folder: ${folderName}`);
2398
2435
  console.log(` Directory: ${ch.directory}`);
2399
2436
  console.log(` Channel ID: ${ch.channel_id}`);
@@ -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,
@@ -10,7 +10,7 @@ import { createLogger, LogPrefix } from '../logger.js';
10
10
  import * as errore from 'errore';
11
11
  const logger = createLogger(LogPrefix.SESSION);
12
12
  export async function handleSessionCommand({ command, appId, }) {
13
- await command.deferReply({ ephemeral: false });
13
+ await command.deferReply();
14
14
  const prompt = command.options.getString('prompt', true);
15
15
  const filesString = command.options.getString('files') || '';
16
16
  const agent = command.options.getString('agent') || undefined;