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
@@ -0,0 +1,20 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { buildNoVncUrl, createScreenshareTunnelId } from './screenshare.js';
3
+ describe('screenshare security defaults', () => {
4
+ test('generates a 128-bit tunnel id', () => {
5
+ const ids = new Set(Array.from({ length: 32 }, () => {
6
+ return createScreenshareTunnelId();
7
+ }));
8
+ expect(ids.size).toBe(32);
9
+ for (const id of ids) {
10
+ expect(id).toMatch(/^[0-9a-f]{32}$/);
11
+ }
12
+ });
13
+ test('builds a secure noVNC URL', () => {
14
+ const url = new URL(buildNoVncUrl({ tunnelHost: '0123456789abcdef-tunnel.kimaki.xyz' }));
15
+ expect(url.origin).toBe('https://novnc.com');
16
+ expect(url.searchParams.get('host')).toBe('0123456789abcdef-tunnel.kimaki.xyz');
17
+ expect(url.searchParams.get('port')).toBe('443');
18
+ expect(url.searchParams.get('encrypt')).toBe('1');
19
+ });
20
+ });
@@ -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;
@@ -5,6 +5,19 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
5
5
  import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
6
6
  import { createLogger, LogPrefix } from '../logger.js';
7
7
  const logger = createLogger(LogPrefix.UNDO_REDO);
8
+ async function waitForSessionIdle({ client, sessionId, directory, timeoutMs = 2_000, }) {
9
+ const deadline = Date.now() + timeoutMs;
10
+ while (Date.now() < deadline) {
11
+ const statusResponse = await client.session.status({ directory });
12
+ const sessionStatus = statusResponse.data?.[sessionId];
13
+ if (!sessionStatus || sessionStatus.type === 'idle') {
14
+ return;
15
+ }
16
+ await new Promise((resolve) => {
17
+ setTimeout(resolve, 50);
18
+ });
19
+ }
20
+ }
8
21
  export async function handleUndoCommand({ command, }) {
9
22
  const channel = command.channel;
10
23
  if (!channel) {
@@ -36,7 +49,7 @@ export async function handleUndoCommand({ command, }) {
36
49
  });
37
50
  return;
38
51
  }
39
- const { projectDirectory } = resolved;
52
+ const { projectDirectory, workingDirectory } = resolved;
40
53
  const sessionId = await getThreadSession(channel.id);
41
54
  if (!sessionId) {
42
55
  await command.reply({
@@ -56,13 +69,36 @@ export async function handleUndoCommand({ command, }) {
56
69
  // Fetch session to check existing revert state
57
70
  const sessionResponse = await client.session.get({
58
71
  sessionID: sessionId,
72
+ directory: workingDirectory,
59
73
  });
60
74
  if (sessionResponse.error) {
61
75
  await command.editReply(`Failed to undo: ${JSON.stringify(sessionResponse.error)}`);
62
76
  return;
63
77
  }
78
+ // Abort if session is busy before reverting, matching TUI behavior
79
+ // (use-session-commands.tsx always aborts non-idle sessions before revert).
80
+ // session.status() returns a sparse map — only non-idle sessions have entries,
81
+ // so a missing key means idle.
82
+ const statusResponse = await client.session.status({
83
+ directory: workingDirectory,
84
+ });
85
+ const sessionStatus = statusResponse.data?.[sessionId];
86
+ if (sessionStatus && sessionStatus.type !== 'idle') {
87
+ await client.session.abort({
88
+ sessionID: sessionId,
89
+ directory: workingDirectory,
90
+ }).catch((error) => {
91
+ logger.warn(`[UNDO] abort failed for ${sessionId}`, error);
92
+ });
93
+ await waitForSessionIdle({
94
+ client,
95
+ sessionId,
96
+ directory: workingDirectory,
97
+ });
98
+ }
64
99
  const messagesResponse = await client.session.messages({
65
100
  sessionID: sessionId,
101
+ directory: workingDirectory,
66
102
  });
67
103
  if (messagesResponse.error) {
68
104
  await command.editReply(`Failed to undo: ${JSON.stringify(messagesResponse.error)}`);
@@ -87,24 +123,46 @@ export async function handleUndoCommand({ command, }) {
87
123
  await command.editReply('No messages to undo');
88
124
  return;
89
125
  }
126
+ const targetAssistantMessage = [...messagesResponse.data].reverse().find((m) => {
127
+ return m.info.role === 'assistant' && m.info.parentID === targetUserMessage.info.id;
128
+ });
129
+ const revertMessageId = targetAssistantMessage?.info.id || targetUserMessage.info.id;
90
130
  // session.revert() reverts filesystem patches (file edits, writes) and
91
131
  // marks the session with revert.messageID. Messages are NOT deleted — they
92
132
  // get cleaned up automatically on the next promptAsync() call via
93
133
  // SessionRevert.cleanup(). The model only sees messages before the revert
94
134
  // point when processing the next prompt.
95
- const response = await client.session.revert({
135
+ logger.log(`[UNDO] session.revert start messageId=${revertMessageId}`);
136
+ let response = await client.session.revert({
96
137
  sessionID: sessionId,
97
- messageID: targetUserMessage.info.id,
138
+ directory: workingDirectory,
139
+ messageID: revertMessageId,
98
140
  });
141
+ logger.log(`[UNDO] session.revert done error=${Boolean(response.error)}`);
99
142
  if (response.error) {
100
- await command.editReply(`Failed to undo: ${JSON.stringify(response.error)}`);
101
- return;
143
+ logger.log('[UNDO] retry wait idle before revert retry');
144
+ await waitForSessionIdle({
145
+ client,
146
+ sessionId,
147
+ directory: workingDirectory,
148
+ });
149
+ logger.log('[UNDO] retry revert start');
150
+ response = await client.session.revert({
151
+ sessionID: sessionId,
152
+ directory: workingDirectory,
153
+ messageID: revertMessageId,
154
+ });
155
+ logger.log(`[UNDO] retry revert done error=${Boolean(response.error)}`);
156
+ if (response.error) {
157
+ await command.editReply(`Failed to undo: ${JSON.stringify(response.error)}`);
158
+ return;
159
+ }
102
160
  }
103
161
  const diffInfo = response.data?.revert?.diff
104
162
  ? `\n\`\`\`diff\n${response.data.revert.diff.slice(0, 1500)}\n\`\`\``
105
163
  : '';
106
164
  await command.editReply(`Undone - reverted last assistant message${diffInfo}`);
107
- logger.log(`Session ${sessionId} reverted to before user message ${targetUserMessage.info.id}`);
165
+ logger.log(`Session ${sessionId} reverted at message ${revertMessageId}`);
108
166
  }
109
167
  catch (error) {
110
168
  logger.error('[UNDO] Error:', error);
@@ -142,7 +200,7 @@ export async function handleRedoCommand({ command, }) {
142
200
  });
143
201
  return;
144
202
  }
145
- const { projectDirectory } = resolved;
203
+ const { projectDirectory, workingDirectory } = resolved;
146
204
  const sessionId = await getThreadSession(channel.id);
147
205
  if (!sessionId) {
148
206
  await command.reply({
@@ -162,6 +220,7 @@ export async function handleRedoCommand({ command, }) {
162
220
  // Fetch session to check existing revert state
163
221
  const sessionResponse = await client.session.get({
164
222
  sessionID: sessionId,
223
+ directory: workingDirectory,
165
224
  });
166
225
  if (sessionResponse.error) {
167
226
  await command.editReply(`Failed to redo: ${JSON.stringify(sessionResponse.error)}`);
@@ -172,12 +231,35 @@ export async function handleRedoCommand({ command, }) {
172
231
  await command.editReply('Nothing to redo - no previous undo found');
173
232
  return;
174
233
  }
234
+ // Abort if session is busy before reverting/unreverting — both enforce
235
+ // assertNotBusy in OpenCode and would fail with "Session is busy"
236
+ const redoStatusResponse = await client.session.status({
237
+ directory: workingDirectory,
238
+ });
239
+ const redoSessionStatus = redoStatusResponse.data?.[sessionId];
240
+ if (redoSessionStatus && redoSessionStatus.type !== 'idle') {
241
+ await client.session.abort({
242
+ sessionID: sessionId,
243
+ directory: workingDirectory,
244
+ }).catch((error) => {
245
+ logger.warn(`[REDO] abort failed for ${sessionId}`, error);
246
+ });
247
+ await waitForSessionIdle({
248
+ client,
249
+ sessionId,
250
+ directory: workingDirectory,
251
+ });
252
+ }
253
+ await new Promise((resolve) => {
254
+ setTimeout(resolve, 500);
255
+ });
175
256
  // Follow the same approach as the OpenCode TUI (use-session-commands.tsx):
176
257
  // find the next user message after the current revert point. If one exists,
177
258
  // move the revert cursor forward to it (one step redo). If none exists,
178
259
  // fully unrevert — we're at the end of the message history.
179
260
  const messagesResponse = await client.session.messages({
180
261
  sessionID: sessionId,
262
+ directory: workingDirectory,
181
263
  });
182
264
  if (messagesResponse.error) {
183
265
  await command.editReply(`Failed to redo: ${JSON.stringify(messagesResponse.error)}`);
@@ -193,6 +275,7 @@ export async function handleRedoCommand({ command, }) {
193
275
  // No more messages after revert point — fully unrevert
194
276
  const response = await client.session.unrevert({
195
277
  sessionID: sessionId,
278
+ directory: workingDirectory,
196
279
  });
197
280
  if (response.error) {
198
281
  await command.editReply(`Failed to redo: ${JSON.stringify(response.error)}`);
@@ -205,6 +288,7 @@ export async function handleRedoCommand({ command, }) {
205
288
  // Move revert cursor forward one step to the next user message
206
289
  const response = await client.session.revert({
207
290
  sessionID: sessionId,
291
+ directory: workingDirectory,
208
292
  messageID: nextMessage.info.id,
209
293
  });
210
294
  if (response.error) {
@@ -75,7 +75,7 @@ export const handleUserCommand = async ({ command, appId, }) => {
75
75
  });
76
76
  return;
77
77
  }
78
- await command.deferReply({ ephemeral: false });
78
+ await command.deferReply();
79
79
  try {
80
80
  // Use the dedicated session.command API instead of formatting as text prompt
81
81
  const commandPayload = { name: commandName, arguments: args };
package/dist/config.js CHANGED
@@ -42,11 +42,26 @@ export function setDataDir(dir) {
42
42
  }
43
43
  /**
44
44
  * Get the projects directory path (for /create-new-project command).
45
- * Returns <dataDir>/projects
45
+ * Returns the custom --projects-dir if set, otherwise <dataDir>/projects.
46
46
  */
47
47
  export function getProjectsDir() {
48
+ const custom = store.getState().projectsDir;
49
+ if (custom) {
50
+ return custom;
51
+ }
48
52
  return path.join(getDataDir(), 'projects');
49
53
  }
54
+ /**
55
+ * Set a custom projects directory path (from --projects-dir CLI flag).
56
+ * Creates the directory if it doesn't exist.
57
+ */
58
+ export function setProjectsDir(dir) {
59
+ const resolvedDir = path.resolve(dir);
60
+ if (!fs.existsSync(resolvedDir)) {
61
+ fs.mkdirSync(resolvedDir, { recursive: true });
62
+ }
63
+ store.setState({ projectsDir: resolvedDir });
64
+ }
50
65
  const DEFAULT_LOCK_PORT = 29988;
51
66
  /**
52
67
  * Derive a lock port from the data directory path.
package/dist/database.js CHANGED
@@ -662,12 +662,34 @@ export async function getThreadSession(threadId) {
662
662
  * Set the session ID for a thread.
663
663
  */
664
664
  export async function setThreadSession(threadId, sessionId) {
665
+ await upsertThreadSession({
666
+ threadId,
667
+ sessionId,
668
+ source: 'kimaki',
669
+ });
670
+ }
671
+ export async function upsertThreadSession({ threadId, sessionId, source, }) {
665
672
  const prisma = await getPrisma();
666
673
  await prisma.thread_sessions.upsert({
667
674
  where: { thread_id: threadId },
668
- create: { thread_id: threadId, session_id: sessionId },
669
- update: { session_id: sessionId },
675
+ create: {
676
+ thread_id: threadId,
677
+ session_id: sessionId,
678
+ source,
679
+ },
680
+ update: {
681
+ session_id: sessionId,
682
+ source,
683
+ },
684
+ });
685
+ }
686
+ export async function getThreadSessionSource(threadId) {
687
+ const prisma = await getPrisma();
688
+ const row = await prisma.thread_sessions.findUnique({
689
+ where: { thread_id: threadId },
690
+ select: { source: true },
670
691
  });
692
+ return row?.source;
671
693
  }
672
694
  /**
673
695
  * Get the thread ID for a session.
@@ -1094,6 +1116,14 @@ export async function getAllTextChannelDirectories() {
1094
1116
  });
1095
1117
  return rows.map((row) => row.directory);
1096
1118
  }
1119
+ export async function listTrackedTextChannels() {
1120
+ const prisma = await getPrisma();
1121
+ return prisma.channel_directories.findMany({
1122
+ where: { channel_type: 'text' },
1123
+ orderBy: [{ created_at: 'asc' }, { channel_id: 'asc' }],
1124
+ select: { channel_id: true, directory: true, created_at: true },
1125
+ });
1126
+ }
1097
1127
  /**
1098
1128
  * Delete all channel directories for a specific directory.
1099
1129
  */
@@ -1103,6 +1133,27 @@ export async function deleteChannelDirectoriesByDirectory(directory) {
1103
1133
  where: { directory },
1104
1134
  });
1105
1135
  }
1136
+ /**
1137
+ * Delete a single channel_directories row and all its child rows
1138
+ * (channel_models, channel_agents, channel_worktrees, channel_verbosity,
1139
+ * channel_mention_mode) in a single transaction. scheduled_tasks has
1140
+ * onDelete:SetNull so Prisma handles it automatically.
1141
+ */
1142
+ export async function deleteChannelDirectoryById(channelId) {
1143
+ const prisma = await getPrisma();
1144
+ const deletedCount = await prisma.$transaction(async (tx) => {
1145
+ await tx.channel_models.deleteMany({ where: { channel_id: channelId } });
1146
+ await tx.channel_agents.deleteMany({ where: { channel_id: channelId } });
1147
+ await tx.channel_worktrees.deleteMany({ where: { channel_id: channelId } });
1148
+ await tx.channel_verbosity.deleteMany({ where: { channel_id: channelId } });
1149
+ await tx.channel_mention_mode.deleteMany({ where: { channel_id: channelId } });
1150
+ const result = await tx.channel_directories.deleteMany({
1151
+ where: { channel_id: channelId },
1152
+ });
1153
+ return result.count;
1154
+ });
1155
+ return deletedCount > 0;
1156
+ }
1106
1157
  /**
1107
1158
  * Get the directory for a voice channel.
1108
1159
  */
package/dist/db.js CHANGED
@@ -159,6 +159,12 @@ async function migrateSchema(prisma) {
159
159
  // Column already exists
160
160
  }
161
161
  }
162
+ try {
163
+ await prisma.$executeRawUnsafe("ALTER TABLE thread_sessions ADD COLUMN source TEXT DEFAULT 'kimaki'");
164
+ }
165
+ catch {
166
+ // Column already exists
167
+ }
162
168
  // Migration: move session_thinking data into session_models.variant.
163
169
  // session_thinking table is left in place (not dropped) so older kimaki versions
164
170
  // that still reference it won't crash on the same database.
@@ -1,13 +1,12 @@
1
1
  // Core Discord bot module that handles message events and bot lifecycle.
2
2
  // Bridges Discord messages to OpenCode sessions, manages voice connections,
3
3
  // and orchestrates the main event loop for the Kimaki bot.
4
- import { initDatabase, closeDatabase, getThreadWorktree, getThreadSession, createPendingWorktree, setWorktreeReady, setWorktreeError, getChannelWorktreesEnabled, getChannelMentionMode, getChannelDirectory, getPrisma, cancelAllPendingIpcRequests, } from './database.js';
4
+ import { initDatabase, closeDatabase, getThreadWorktree, getThreadSession, getChannelWorktreesEnabled, getChannelMentionMode, getChannelDirectory, getPrisma, cancelAllPendingIpcRequests, deleteChannelDirectoryById, } from './database.js';
5
5
  import { stopOpencodeServer, } from './opencode.js';
6
- import { formatWorktreeName } from './commands/new-worktree.js';
6
+ import { formatWorktreeName, createWorktreeInBackground, worktreeCreatingMessage } from './commands/new-worktree.js';
7
7
  import { WORKTREE_PREFIX } from './commands/merge-worktree.js';
8
- import { createWorktreeWithSubmodules } from './worktrees.js';
9
8
  import { escapeBackticksInCodeBlocks, splitMarkdownForDiscord, sendThreadMessage, SILENT_MESSAGE_FLAGS, NOTIFY_MESSAGE_FLAGS, reactToThread, stripMentions, hasKimakiBotPermission, hasNoKimakiRole, } from './discord-utils.js';
10
- import { getOpencodeSystemMessage, } from './system-message.js';
9
+ import { getOpencodeSystemMessage, isInjectedPromptMarker, } from './system-message.js';
11
10
  import yaml from 'js-yaml';
12
11
  import { getTextAttachments, resolveMentions, } from './message-formatting.js';
13
12
  import { isVoiceAttachment } from './voice-attachment.js';
@@ -28,6 +27,7 @@ import { markDiscordGatewayReady, stopHranaServer } from './hrana-server.js';
28
27
  import { notifyError } from './sentry.js';
29
28
  import { flushDebouncedProcessCallbacks } from './debounced-process-flush.js';
30
29
  import { startRuntimeIdleSweeper } from './runtime-idle-sweeper.js';
30
+ import { startExternalOpencodeSessionSync, stopExternalOpencodeSessionSync, } from './external-opencode-sync.js';
31
31
  export { initDatabase, closeDatabase, getChannelDirectory, getPrisma, } from './database.js';
32
32
  export { initializeOpencodeForDirectory } from './opencode.js';
33
33
  export { escapeBackticksInCodeBlocks, splitMarkdownForDiscord, } from './discord-utils.js';
@@ -162,6 +162,7 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
162
162
  markDiscordGatewayReady();
163
163
  registerInteractionHandler({ discordClient: c, appId: currentAppId });
164
164
  registerVoiceStateHandler({ discordClient: c, appId: currentAppId });
165
+ startExternalOpencodeSessionSync({ discordClient: c });
165
166
  // Channel logging is informational only; do it in background so startup stays responsive.
166
167
  void (async () => {
167
168
  for (const guild of c.guilds.cache.values()) {
@@ -244,7 +245,7 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
244
245
  const promptMarker = parseEmbedFooterMarker({
245
246
  footer: message.embeds[0]?.footer?.text,
246
247
  });
247
- const isCliInjectedPrompt = Boolean(isSelfBotMessage && promptMarker?.cliThreadPrompt);
248
+ const isCliInjectedPrompt = Boolean(isSelfBotMessage && isInjectedPromptMarker({ marker: promptMarker }));
248
249
  const sessionStartSource = isCliInjectedPrompt
249
250
  ? parseSessionStartSourceFromMarker(promptMarker)
250
251
  : undefined;
@@ -419,7 +420,6 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
419
420
  discordLogger.log(`Cannot process message: no project directory for thread ${thread.id}`);
420
421
  return;
421
422
  }
422
- // Capture narrowed non-undefined value for use in the preprocess closure
423
423
  const resolvedProjectDir = projectDirectory;
424
424
  const sdkDir = worktreeInfo?.status === 'ready' &&
425
425
  worktreeInfo.worktree_directory
@@ -442,7 +442,9 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
442
442
  cancelHtmlActionsForThread(thread.id);
443
443
  const dismissedPermission = await cancelPendingPermission(thread.id);
444
444
  if (dismissedPermission) {
445
- runtime.abortActiveRun('user sent a new message while permission was pending');
445
+ await runtime.abortActiveRunAndWait({
446
+ reason: 'user sent a new message while permission was pending',
447
+ });
446
448
  }
447
449
  const questionResult = await cancelPendingQuestion(thread.id, message.content);
448
450
  void cancelPendingFileUpload(thread.id);
@@ -460,6 +462,8 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
460
462
  username: cliInjectedUsername ||
461
463
  message.member?.displayName ||
462
464
  message.author.displayName,
465
+ sourceMessageId: message.id,
466
+ sourceThreadId: thread.id,
463
467
  appId: currentAppId,
464
468
  agent: cliInjectedAgent,
465
469
  model: cliInjectedModel,
@@ -560,42 +564,21 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
560
564
  if (shouldUseWorktrees) {
561
565
  const worktreeName = formatWorktreeName(hasVoice ? `voice-${Date.now()}` : threadName.slice(0, 50));
562
566
  discordLogger.log(`[WORKTREE] Creating worktree: ${worktreeName}`);
563
- // Store pending worktree immediately so bot knows about it
564
- await createPendingWorktree({
565
- threadId: thread.id,
567
+ const worktreeStatusMessage = await thread
568
+ .send({
569
+ content: worktreeCreatingMessage(worktreeName),
570
+ flags: SILENT_MESSAGE_FLAGS,
571
+ })
572
+ .catch(() => undefined);
573
+ const result = await createWorktreeInBackground({
574
+ thread,
575
+ starterMessage: worktreeStatusMessage,
566
576
  worktreeName,
567
577
  projectDirectory,
578
+ rest: discordClient.rest,
568
579
  });
569
- const worktreeResult = await createWorktreeWithSubmodules({
570
- directory: projectDirectory,
571
- name: worktreeName,
572
- });
573
- if (worktreeResult instanceof Error) {
574
- const errMsg = worktreeResult.message;
575
- discordLogger.error(`[WORKTREE] Creation failed: ${errMsg}`);
576
- await setWorktreeError({
577
- threadId: thread.id,
578
- errorMessage: errMsg,
579
- });
580
- await thread.send({
581
- content: `⚠️ Failed to create worktree: ${errMsg}\nUsing main project directory instead.`,
582
- flags: NOTIFY_MESSAGE_FLAGS,
583
- });
584
- }
585
- else {
586
- await setWorktreeReady({
587
- threadId: thread.id,
588
- worktreeDirectory: worktreeResult.directory,
589
- });
590
- sessionDirectory = worktreeResult.directory;
591
- discordLogger.log(`[WORKTREE] Created: ${worktreeResult.directory} (branch: ${worktreeResult.branch})`);
592
- // React with tree emoji to mark as worktree thread
593
- await reactToThread({
594
- rest: discordClient.rest,
595
- threadId: thread.id,
596
- channelId: thread.parentId || undefined,
597
- emoji: '🌳',
598
- });
580
+ if (!(result instanceof Error)) {
581
+ sessionDirectory = result;
599
582
  }
600
583
  }
601
584
  const channelRuntime = getOrCreateRuntime({
@@ -610,6 +593,8 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
610
593
  prompt: '',
611
594
  userId: message.author.id,
612
595
  username: message.member?.displayName || message.author.displayName,
596
+ sourceMessageId: message.id,
597
+ sourceThreadId: thread.id,
613
598
  appId: currentAppId,
614
599
  preprocess: () => {
615
600
  return preprocessNewThreadMessage({
@@ -711,58 +696,21 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
711
696
  discordLogger.log(`[BOT_SESSION] Creating worktree: ${marker.worktree}`);
712
697
  const worktreeStatusMessage = await thread
713
698
  .send({
714
- content: `🌳 Creating worktree: ${marker.worktree}\n⏳ Setting up (this can take a bit)...`,
699
+ content: worktreeCreatingMessage(marker.worktree),
715
700
  flags: SILENT_MESSAGE_FLAGS,
716
701
  })
717
- .catch(() => {
718
- return null;
719
- });
720
- await createPendingWorktree({
721
- threadId: thread.id,
702
+ .catch(() => undefined);
703
+ const result = await createWorktreeInBackground({
704
+ thread,
705
+ starterMessage: worktreeStatusMessage,
722
706
  worktreeName: marker.worktree,
723
707
  projectDirectory,
708
+ rest: discordClient.rest,
724
709
  });
725
- const worktreeResult = await createWorktreeWithSubmodules({
726
- directory: projectDirectory,
727
- name: marker.worktree,
728
- });
729
- if (errore.isError(worktreeResult)) {
730
- discordLogger.error(`[BOT_SESSION] Worktree creation failed: ${worktreeResult.message}`);
731
- await setWorktreeError({
732
- threadId: thread.id,
733
- errorMessage: worktreeResult.message,
734
- });
735
- await (worktreeStatusMessage?.edit({
736
- content: `⚠️ Failed to create worktree: ${worktreeResult.message}\nUsing main project directory instead.`,
737
- flags: NOTIFY_MESSAGE_FLAGS,
738
- }) ||
739
- thread.send({
740
- content: `⚠️ Failed to create worktree: ${worktreeResult.message}\nUsing main project directory instead.`,
741
- flags: NOTIFY_MESSAGE_FLAGS,
742
- }));
710
+ if (result instanceof Error) {
743
711
  return projectDirectory;
744
712
  }
745
- await setWorktreeReady({
746
- threadId: thread.id,
747
- worktreeDirectory: worktreeResult.directory,
748
- });
749
- discordLogger.log(`[BOT_SESSION] Worktree created: ${worktreeResult.directory}`);
750
- // React with tree emoji to mark as worktree thread
751
- await reactToThread({
752
- rest: discordClient.rest,
753
- threadId: thread.id,
754
- channelId: thread.parentId || undefined,
755
- emoji: '🌳',
756
- });
757
- await (worktreeStatusMessage?.edit({
758
- content: `🌳 **Worktree ready: ${marker.worktree}**\n📁 \`${worktreeResult.directory}\`\n🌿 Branch: \`${worktreeResult.branch}\``,
759
- flags: SILENT_MESSAGE_FLAGS,
760
- }) ||
761
- thread.send({
762
- content: `🌳 **Worktree ready: ${marker.worktree}**\n📁 \`${worktreeResult.directory}\`\n🌿 Branch: \`${worktreeResult.branch}\``,
763
- flags: SILENT_MESSAGE_FLAGS,
764
- }));
765
- return worktreeResult.directory;
713
+ return result;
766
714
  })();
767
715
  discordLogger.log(`[BOT_SESSION] Starting session for thread ${thread.id} with prompt: "${prompt.slice(0, 50)}..."`);
768
716
  const botThreadStartSource = parseSessionStartSourceFromMarker(marker);
@@ -811,6 +759,20 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
811
759
  discordClient.on(Events.ThreadDelete, (thread) => {
812
760
  disposeRuntime(thread.id);
813
761
  });
762
+ // Clean up SQLite when a Discord channel is deleted so project list
763
+ // doesn't show stale ghost entries. Thread runtimes inside the deleted
764
+ // channel are disposed by their own ThreadDelete events from Discord.
765
+ discordClient.on(Events.ChannelDelete, async (channel) => {
766
+ try {
767
+ const deleted = await deleteChannelDirectoryById(channel.id);
768
+ if (deleted) {
769
+ discordLogger.log(`Cleaned up channel_directories for deleted channel ${channel.id}`);
770
+ }
771
+ }
772
+ catch (error) {
773
+ notifyError(error instanceof Error ? error : new Error(String(error)), `Failed to clean up channel_directories for deleted channel ${channel.id}`);
774
+ }
775
+ });
814
776
  // Skip login if the caller already connected the client (e.g. cli.ts logs in
815
777
  // before calling startDiscordBot). Calling login() again destroys the existing
816
778
  // WebSocket (close code 1000) and triggers a spurious ShardReconnecting event.
@@ -849,6 +811,7 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
849
811
  discordLogger.log(`All voice connections cleaned up`);
850
812
  }
851
813
  voiceLogger.log('[SHUTDOWN] Stopping OpenCode server');
814
+ stopExternalOpencodeSessionSync();
852
815
  await stopOpencodeServer();
853
816
  discordLogger.log('Closing database...');
854
817
  await closeDatabase();
@@ -360,7 +360,7 @@ export async function registerCommands({ token, appId, guildIds, userCommands =
360
360
  .toJSON(),
361
361
  new SlashCommandBuilder()
362
362
  .setName('screenshare')
363
- .setDescription(truncateCommandDescription('Start screen sharing via VNC tunnel (auto-stops after 1 hour)'))
363
+ .setDescription(truncateCommandDescription('Start screen sharing via VNC tunnel (auto-stops after 30 minutes)'))
364
364
  .setDMPermission(false)
365
365
  .toJSON(),
366
366
  new SlashCommandBuilder()