kimaki 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/dist/agent-model.e2e.test.js +13 -23
  2. package/dist/channel-reference-permissions.e2e.test.js +85 -0
  3. package/dist/cli-commands/bot.js +1 -1
  4. package/dist/cli-commands/maintenance.js +1 -1
  5. package/dist/cli-commands/misc.js +1 -1
  6. package/dist/cli-commands/project.js +5 -5
  7. package/dist/cli-commands/send.js +11 -8
  8. package/dist/cli-commands/session.js +55 -7
  9. package/dist/cli-commands/task.js +1 -1
  10. package/dist/cli-commands/user.js +1 -1
  11. package/dist/cli-runner.js +11 -10
  12. package/dist/cli-send-thread.e2e.test.js +2 -2
  13. package/dist/commands/last-sessions.js +9 -8
  14. package/dist/commands/unset-model.js +5 -5
  15. package/dist/commands/verbosity.js +3 -3
  16. package/dist/commands/worktrees.js +3 -3
  17. package/dist/database.js +434 -1110
  18. package/dist/db.js +66 -151
  19. package/dist/db.test.js +78 -26
  20. package/dist/discord-bot.js +16 -13
  21. package/dist/hrana-server.js +1 -1
  22. package/dist/hrana-server.test.js +37 -308
  23. package/dist/ipc-polling.js +2 -2
  24. package/dist/ipc-tools-plugin.js +9 -17
  25. package/dist/message-preprocessing.js +25 -2
  26. package/dist/opencode.js +28 -5
  27. package/dist/queue-advanced-e2e-setup.js +64 -1
  28. package/dist/schema.js +270 -0
  29. package/dist/session-handler/event-stream-state.js +18 -0
  30. package/dist/session-handler/event-stream-state.test.js +46 -1
  31. package/dist/session-handler/thread-session-runtime.js +82 -17
  32. package/dist/startup-time.e2e.test.js +1 -1
  33. package/dist/system-message.js +24 -11
  34. package/dist/system-message.test.js +24 -11
  35. package/dist/test-utils.js +25 -0
  36. package/dist/wait-session.js +103 -26
  37. package/dist/worktrees.js +34 -0
  38. package/dist/worktrees.test.js +119 -1
  39. package/package.json +11 -14
  40. package/skills/egaki/SKILL.md +10 -1
  41. package/skills/errore/SKILL.md +20 -8
  42. package/skills/goke/SKILL.md +302 -1
  43. package/skills/sigillo/SKILL.md +214 -36
  44. package/skills/spiceflow/SKILL.md +100 -0
  45. package/skills/tuistory/SKILL.md +1 -1
  46. package/src/agent-model.e2e.test.ts +13 -23
  47. package/src/channel-reference-permissions.e2e.test.ts +102 -0
  48. package/src/cli-commands/bot.ts +1 -1
  49. package/src/cli-commands/maintenance.ts +1 -1
  50. package/src/cli-commands/misc.ts +1 -1
  51. package/src/cli-commands/project.ts +5 -5
  52. package/src/cli-commands/send.ts +12 -8
  53. package/src/cli-commands/session.ts +79 -7
  54. package/src/cli-commands/task.ts +1 -1
  55. package/src/cli-commands/user.ts +1 -1
  56. package/src/cli-runner.ts +11 -10
  57. package/src/cli-send-thread.e2e.test.ts +2 -2
  58. package/src/commands/fork.ts +1 -1
  59. package/src/commands/last-sessions.ts +9 -8
  60. package/src/commands/unset-model.ts +7 -5
  61. package/src/commands/verbosity.ts +3 -3
  62. package/src/commands/worktrees.ts +3 -3
  63. package/src/database.ts +528 -1618
  64. package/src/db.test.ts +80 -26
  65. package/src/db.ts +78 -158
  66. package/src/discord-bot.ts +14 -13
  67. package/src/hrana-server.test.ts +39 -343
  68. package/src/hrana-server.ts +1 -1
  69. package/src/ipc-polling.ts +2 -2
  70. package/src/ipc-tools-plugin.ts +9 -17
  71. package/src/message-preprocessing.ts +37 -3
  72. package/src/opencode.ts +31 -5
  73. package/src/queue-advanced-e2e-setup.ts +72 -0
  74. package/src/schema.sql +191 -173
  75. package/src/schema.ts +303 -0
  76. package/src/session-handler/event-stream-state.test.ts +49 -0
  77. package/src/session-handler/event-stream-state.ts +29 -0
  78. package/src/session-handler/thread-runtime-state.ts +4 -0
  79. package/src/session-handler/thread-session-runtime.ts +109 -19
  80. package/src/startup-time.e2e.test.ts +1 -1
  81. package/src/store.ts +1 -1
  82. package/src/system-message.test.ts +24 -11
  83. package/src/system-message.ts +25 -12
  84. package/src/test-utils.ts +27 -0
  85. package/src/wait-session.ts +154 -33
  86. package/src/worktrees.test.ts +136 -0
  87. package/src/worktrees.ts +64 -0
  88. package/dist/acp-client.test.js +0 -149
  89. package/dist/adapter-rest-boundary.test.js +0 -34
  90. package/dist/add-directory.e2e.test.js +0 -101
  91. package/dist/bash-tool.js +0 -194
  92. package/dist/bash-tool.test.js +0 -82
  93. package/dist/bundled-skills.js +0 -37
  94. package/dist/cli-commands/core.js +0 -1909
  95. package/dist/commands/add-directory.js +0 -67
  96. package/dist/commands/channel-ref.js +0 -16
  97. package/dist/commands/discord-install-url.js +0 -36
  98. package/dist/commands/sqlitedb.js +0 -16
  99. package/dist/commands/stop-opencode-server.js +0 -80
  100. package/dist/commands/thinking.js +0 -128
  101. package/dist/commands/vscode.test.js +0 -44
  102. package/dist/commands/worktree.js +0 -279
  103. package/dist/config-lock-port.test.js +0 -17
  104. package/dist/diff-patch-plugin.js +0 -314
  105. package/dist/directVoiceStreaming.js +0 -102
  106. package/dist/directory-permissions.js +0 -38
  107. package/dist/directory-permissions.test.js +0 -37
  108. package/dist/discord-js-import-boundary.test.js +0 -62
  109. package/dist/discord-ws-proxy.js +0 -350
  110. package/dist/discord-ws-proxy.test.js +0 -500
  111. package/dist/discordBot.js +0 -2814
  112. package/dist/external-opencode-sync.test.js +0 -151
  113. package/dist/fork.js +0 -163
  114. package/dist/forum-sync.js +0 -953
  115. package/dist/gateway-session.js +0 -163
  116. package/dist/generated/browser.js +0 -17
  117. package/dist/generated/client.js +0 -37
  118. package/dist/generated/cloudflare/browser.js +0 -17
  119. package/dist/generated/cloudflare/client.js +0 -34
  120. package/dist/generated/cloudflare/commonInputTypes.js +0 -10
  121. package/dist/generated/cloudflare/enums.js +0 -48
  122. package/dist/generated/cloudflare/internal/class.js +0 -47
  123. package/dist/generated/cloudflare/internal/prismaNamespace.js +0 -252
  124. package/dist/generated/cloudflare/internal/prismaNamespaceBrowser.js +0 -222
  125. package/dist/generated/cloudflare/internal/query_compiler_fast_bg.js +0 -135
  126. package/dist/generated/cloudflare/models/bot_api_keys.js +0 -1
  127. package/dist/generated/cloudflare/models/bot_tokens.js +0 -1
  128. package/dist/generated/cloudflare/models/channel_agents.js +0 -1
  129. package/dist/generated/cloudflare/models/channel_directories.js +0 -1
  130. package/dist/generated/cloudflare/models/channel_mention_mode.js +0 -1
  131. package/dist/generated/cloudflare/models/channel_models.js +0 -1
  132. package/dist/generated/cloudflare/models/channel_verbosity.js +0 -1
  133. package/dist/generated/cloudflare/models/channel_worktrees.js +0 -1
  134. package/dist/generated/cloudflare/models/forum_sync_configs.js +0 -1
  135. package/dist/generated/cloudflare/models/global_models.js +0 -1
  136. package/dist/generated/cloudflare/models/ipc_requests.js +0 -1
  137. package/dist/generated/cloudflare/models/part_messages.js +0 -1
  138. package/dist/generated/cloudflare/models/scheduled_tasks.js +0 -1
  139. package/dist/generated/cloudflare/models/session_agents.js +0 -1
  140. package/dist/generated/cloudflare/models/session_events.js +0 -1
  141. package/dist/generated/cloudflare/models/session_models.js +0 -1
  142. package/dist/generated/cloudflare/models/session_start_sources.js +0 -1
  143. package/dist/generated/cloudflare/models/thread_sessions.js +0 -1
  144. package/dist/generated/cloudflare/models/thread_worktrees.js +0 -1
  145. package/dist/generated/cloudflare/models.js +0 -1
  146. package/dist/generated/commonInputTypes.js +0 -10
  147. package/dist/generated/enums.js +0 -52
  148. package/dist/generated/internal/class.js +0 -49
  149. package/dist/generated/internal/prismaNamespace.js +0 -254
  150. package/dist/generated/internal/prismaNamespaceBrowser.js +0 -224
  151. package/dist/generated/models/bot_api_keys.js +0 -1
  152. package/dist/generated/models/bot_tokens.js +0 -1
  153. package/dist/generated/models/channel_agents.js +0 -1
  154. package/dist/generated/models/channel_directories.js +0 -1
  155. package/dist/generated/models/channel_mention_mode.js +0 -1
  156. package/dist/generated/models/channel_models.js +0 -1
  157. package/dist/generated/models/channel_verbosity.js +0 -1
  158. package/dist/generated/models/channel_worktrees.js +0 -1
  159. package/dist/generated/models/external_session_pending_prompts.js +0 -1
  160. package/dist/generated/models/forum_sync_configs.js +0 -1
  161. package/dist/generated/models/global_models.js +0 -1
  162. package/dist/generated/models/ipc_requests.js +0 -1
  163. package/dist/generated/models/part_messages.js +0 -1
  164. package/dist/generated/models/pending_auto_start.js +0 -1
  165. package/dist/generated/models/scheduled_tasks.js +0 -1
  166. package/dist/generated/models/session_agents.js +0 -1
  167. package/dist/generated/models/session_events.js +0 -1
  168. package/dist/generated/models/session_models.js +0 -1
  169. package/dist/generated/models/session_start_sources.js +0 -1
  170. package/dist/generated/models/session_thinking.js +0 -1
  171. package/dist/generated/models/thread_allowed_directories.js +0 -1
  172. package/dist/generated/models/thread_sessions.js +0 -1
  173. package/dist/generated/models/thread_worktrees.js +0 -1
  174. package/dist/generated/models.js +0 -1
  175. package/dist/generated/node/browser.js +0 -17
  176. package/dist/generated/node/client.js +0 -37
  177. package/dist/generated/node/commonInputTypes.js +0 -10
  178. package/dist/generated/node/enums.js +0 -48
  179. package/dist/generated/node/internal/class.js +0 -49
  180. package/dist/generated/node/internal/prismaNamespace.js +0 -252
  181. package/dist/generated/node/internal/prismaNamespaceBrowser.js +0 -222
  182. package/dist/generated/node/models/bot_api_keys.js +0 -1
  183. package/dist/generated/node/models/bot_tokens.js +0 -1
  184. package/dist/generated/node/models/channel_agents.js +0 -1
  185. package/dist/generated/node/models/channel_directories.js +0 -1
  186. package/dist/generated/node/models/channel_mention_mode.js +0 -1
  187. package/dist/generated/node/models/channel_models.js +0 -1
  188. package/dist/generated/node/models/channel_verbosity.js +0 -1
  189. package/dist/generated/node/models/channel_worktrees.js +0 -1
  190. package/dist/generated/node/models/forum_sync_configs.js +0 -1
  191. package/dist/generated/node/models/global_models.js +0 -1
  192. package/dist/generated/node/models/ipc_requests.js +0 -1
  193. package/dist/generated/node/models/part_messages.js +0 -1
  194. package/dist/generated/node/models/scheduled_tasks.js +0 -1
  195. package/dist/generated/node/models/session_agents.js +0 -1
  196. package/dist/generated/node/models/session_events.js +0 -1
  197. package/dist/generated/node/models/session_models.js +0 -1
  198. package/dist/generated/node/models/session_start_sources.js +0 -1
  199. package/dist/generated/node/models/thread_sessions.js +0 -1
  200. package/dist/generated/node/models/thread_worktrees.js +0 -1
  201. package/dist/generated/node/models.js +0 -1
  202. package/dist/install-url.js +0 -27
  203. package/dist/kimaki-opencode-plugin-specs.js +0 -13
  204. package/dist/kimaki-real-discord.e2e.test.js +0 -294
  205. package/dist/kitty-graphics-parser.js +0 -3
  206. package/dist/kitty-graphics-parser.test.js +0 -276
  207. package/dist/kitty-graphics-plugin.js +0 -3
  208. package/dist/memory-overview-plugin.test.js +0 -262
  209. package/dist/message-flags-boundary.test.js +0 -54
  210. package/dist/message-preprocessing.test.js +0 -35
  211. package/dist/model-command.js +0 -293
  212. package/dist/onboarding-tutorial-plugin.js +0 -73
  213. package/dist/opencode-plugin-interrupt.test.js +0 -138
  214. package/dist/opencode-plugin-loading.e2e.test.js +0 -80
  215. package/dist/opencode-plugin.js +0 -13
  216. package/dist/opencode-plugin.test.js +0 -98
  217. package/dist/opencode.test.js +0 -85
  218. package/dist/orphan-opencode-sweep.test.js +0 -80
  219. package/dist/pkce.js +0 -23
  220. package/dist/platform/components-v2.js +0 -20
  221. package/dist/platform/discord-adapter.js +0 -1440
  222. package/dist/platform/discord-routes.js +0 -31
  223. package/dist/platform/message-flags.js +0 -8
  224. package/dist/platform/platform-value.js +0 -41
  225. package/dist/platform/slack-adapter.js +0 -872
  226. package/dist/platform/slack-markdown.js +0 -169
  227. package/dist/platform/types.js +0 -4
  228. package/dist/plugin.js +0 -1414
  229. package/dist/proxy-ws-preload.cjs +0 -85
  230. package/dist/session-handler/runtime-types.js +0 -3
  231. package/dist/session-handler/state.js +0 -53
  232. package/dist/session-handler/state.test.js +0 -52
  233. package/dist/session-handler/thread-runtime-state.test.js +0 -287
  234. package/dist/subagent-rate-limit-plugin.test.js +0 -120
  235. package/dist/system-prompt-drift-plugin.js +0 -248
  236. package/dist/system-prompt-drift-plugin.test.js +0 -158
  237. package/dist/thinking-utils.test.js +0 -48
  238. package/dist/thread-queue-advanced.e2e.test.js +0 -884
  239. package/dist/token-usage.js +0 -11
  240. package/dist/xai-realtime.js +0 -95
  241. package/schema.prisma +0 -296
  242. package/skills/event-sourcing-state/SKILL.md +0 -252
  243. package/skills/jitter/EDITOR.md +0 -219
  244. package/skills/jitter/EXPORT-INTERNALS.md +0 -309
  245. package/skills/jitter/SKILL.md +0 -158
  246. package/skills/jitter/jitter-clipboard.json +0 -1042
  247. package/skills/jitter/package.json +0 -14
  248. package/skills/jitter/tsconfig.json +0 -15
  249. package/skills/jitter/utils/actions.ts +0 -212
  250. package/skills/jitter/utils/export.ts +0 -114
  251. package/skills/jitter/utils/index.ts +0 -141
  252. package/skills/jitter/utils/snapshot.ts +0 -154
  253. package/skills/jitter/utils/traverse.ts +0 -246
  254. package/skills/jitter/utils/types.ts +0 -279
  255. package/skills/jitter/utils/wait.ts +0 -133
  256. package/skills/parallel-security-review/SKILL.md +0 -332
  257. package/skills/proxyman/SKILL.md +0 -215
  258. package/skills/x-articles/SKILL.md +0 -554
  259. package/skills/zustand-centralized-state/SKILL.md +0 -1004
  260. package/src/generated/browser.ts +0 -114
  261. package/src/generated/client.ts +0 -138
  262. package/src/generated/commonInputTypes.ts +0 -736
  263. package/src/generated/enums.ts +0 -88
  264. package/src/generated/internal/class.ts +0 -384
  265. package/src/generated/internal/prismaNamespace.ts +0 -2387
  266. package/src/generated/internal/prismaNamespaceBrowser.ts +0 -327
  267. package/src/generated/models/bot_api_keys.ts +0 -1288
  268. package/src/generated/models/bot_tokens.ts +0 -1652
  269. package/src/generated/models/channel_agents.ts +0 -1256
  270. package/src/generated/models/channel_directories.ts +0 -1859
  271. package/src/generated/models/channel_mention_mode.ts +0 -1300
  272. package/src/generated/models/channel_models.ts +0 -1288
  273. package/src/generated/models/channel_verbosity.ts +0 -1228
  274. package/src/generated/models/channel_worktrees.ts +0 -1300
  275. package/src/generated/models/forum_sync_configs.ts +0 -1452
  276. package/src/generated/models/global_models.ts +0 -1288
  277. package/src/generated/models/ipc_requests.ts +0 -1485
  278. package/src/generated/models/part_messages.ts +0 -1302
  279. package/src/generated/models/scheduled_tasks.ts +0 -2320
  280. package/src/generated/models/session_agents.ts +0 -1086
  281. package/src/generated/models/session_events.ts +0 -1439
  282. package/src/generated/models/session_models.ts +0 -1114
  283. package/src/generated/models/session_start_sources.ts +0 -1408
  284. package/src/generated/models/thread_sessions.ts +0 -1833
  285. package/src/generated/models/thread_worktrees.ts +0 -1356
  286. package/src/generated/models.ts +0 -30
@@ -20,7 +20,9 @@ import { setDataDir } from './config.js';
20
20
  import { store } from './store.js';
21
21
  import { startDiscordBot } from './discord-bot.js';
22
22
  import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, setChannelAgent, setChannelModel, } from './database.js';
23
- import { getPrisma } from './db.js';
23
+ import { getDb } from './db.js';
24
+ import * as orm from 'drizzle-orm';
25
+ import * as schema from './schema.js';
24
26
  import { startHranaServer, stopHranaServer } from './hrana-server.js';
25
27
  import { initializeOpencodeForDirectory, stopOpencodeServer } from './opencode.js';
26
28
  import { chooseLockPort, cleanupTestSessions, initTestGitRepo, waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
@@ -379,13 +381,9 @@ describe('agent model resolution', () => {
379
381
  `);
380
382
  }, 15_000);
381
383
  test('reply message injects replied-message context', async () => {
382
- const prisma = await getPrisma();
383
- await prisma.channel_agents.deleteMany({
384
- where: { channel_id: TEXT_CHANNEL_ID },
385
- });
386
- await prisma.channel_models.deleteMany({
387
- where: { channel_id: TEXT_CHANNEL_ID },
388
- });
384
+ const db = await getDb();
385
+ await db.delete(schema.channel_agents).where(orm.eq(schema.channel_agents.channel_id, TEXT_CHANNEL_ID));
386
+ await db.delete(schema.channel_models).where(orm.eq(schema.channel_models.channel_id, TEXT_CHANNEL_ID));
389
387
  const existingThreadIds = new Set((await discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
390
388
  return thread.id;
391
389
  }));
@@ -429,10 +427,8 @@ describe('agent model resolution', () => {
429
427
  }, 15_000);
430
428
  test('new thread uses channel model when channel model preference is set', async () => {
431
429
  // Clear channel agent so model resolution falls through to channel model
432
- const prisma = await getPrisma();
433
- await prisma.channel_agents.deleteMany({
434
- where: { channel_id: TEXT_CHANNEL_ID },
435
- });
430
+ const db = await getDb();
431
+ await db.delete(schema.channel_agents).where(orm.eq(schema.channel_agents.channel_id, TEXT_CHANNEL_ID));
436
432
  // Set channel model preference — simulates /model selecting a model at channel scope
437
433
  await setChannelModel({
438
434
  channelId: TEXT_CHANNEL_ID,
@@ -480,10 +476,8 @@ describe('agent model resolution', () => {
480
476
  }, 15_000);
481
477
  test('channel model with variant preference completes without error', async () => {
482
478
  // Clear channel agent so model resolution falls through to channel model
483
- const prisma = await getPrisma();
484
- await prisma.channel_agents.deleteMany({
485
- where: { channel_id: TEXT_CHANNEL_ID },
486
- });
479
+ const db = await getDb();
480
+ await db.delete(schema.channel_agents).where(orm.eq(schema.channel_agents.channel_id, TEXT_CHANNEL_ID));
487
481
  // Set channel model with a variant (thinking level)
488
482
  // The deterministic provider doesn't support thinking, so the variant
489
483
  // is resolved but silently dropped (no matching thinking values).
@@ -610,14 +604,10 @@ describe('agent model resolution', () => {
610
604
  }, 20_000);
611
605
  test('thread created with no agent keeps default model after channel agent is set', async () => {
612
606
  // Clear any channel agent — thread starts with default (no agent)
613
- const prisma = await getPrisma();
614
- await prisma.channel_agents.deleteMany({
615
- where: { channel_id: TEXT_CHANNEL_ID },
616
- });
607
+ const db = await getDb();
608
+ await db.delete(schema.channel_agents).where(orm.eq(schema.channel_agents.channel_id, TEXT_CHANNEL_ID));
617
609
  // Also clear channel model so we get the pure default
618
- await prisma.channel_models.deleteMany({
619
- where: { channel_id: TEXT_CHANNEL_ID },
620
- });
610
+ await db.delete(schema.channel_models).where(orm.eq(schema.channel_models.channel_id, TEXT_CHANNEL_ID));
621
611
  // 1. Send a message to create a thread (no channel agent set)
622
612
  await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
623
613
  content: 'Reply with exactly: default-thread-msg',
@@ -0,0 +1,85 @@
1
+ // E2e tests for granting external_directory permissions from #channel references.
2
+ import { describe, expect, test } from 'vitest';
3
+ import fs from 'node:fs';
4
+ import { CHANNEL_REFERENCE_EXTERNAL_DIR, CHANNEL_REFERENCE_EXTERNAL_FILE, setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
5
+ import { setChannelDirectory } from './database.js';
6
+ import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
7
+ const TEXT_CHANNEL_ID = '200000000000001021';
8
+ const EXTERNAL_CHANNEL_ID = '200000000000001022';
9
+ describe('channel reference permissions', () => {
10
+ const ctx = setupQueueAdvancedSuite({
11
+ channelId: TEXT_CHANNEL_ID,
12
+ channelName: 'qa-channel-reference-e2e',
13
+ extraChannels: [{ id: EXTERNAL_CHANNEL_ID, name: 'external-project' }],
14
+ dirName: 'qa-channel-reference-e2e',
15
+ username: 'channel-reference-tester',
16
+ });
17
+ test('allows referenced project channel directories on new and existing sessions', async () => {
18
+ fs.mkdirSync(CHANNEL_REFERENCE_EXTERNAL_DIR, { recursive: true });
19
+ fs.writeFileSync(CHANNEL_REFERENCE_EXTERNAL_FILE, 'referenced channel file');
20
+ await setChannelDirectory({
21
+ channelId: EXTERNAL_CHANNEL_ID,
22
+ directory: CHANNEL_REFERENCE_EXTERNAL_DIR,
23
+ channelType: 'text',
24
+ });
25
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
26
+ content: `Use <#${EXTERNAL_CHANNEL_ID}> CHANNEL_REFERENCE_PERMISSION_MARKER first`,
27
+ });
28
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
29
+ timeout: 4_000,
30
+ predicate: (t) => {
31
+ return t.name?.includes('CHANNEL_REFERENCE_PERMISSION_MARKER') ?? false;
32
+ },
33
+ });
34
+ const th = ctx.discord.thread(thread.id);
35
+ await waitForBotMessageContaining({
36
+ discord: ctx.discord,
37
+ threadId: thread.id,
38
+ userId: TEST_USER_ID,
39
+ text: 'channel-reference-read-done',
40
+ timeout: 8_000,
41
+ });
42
+ await waitForFooterMessage({
43
+ discord: ctx.discord,
44
+ threadId: thread.id,
45
+ timeout: 4_000,
46
+ afterMessageIncludes: 'channel-reference-read-done',
47
+ afterAuthorId: ctx.discord.botUserId,
48
+ });
49
+ await th.user(TEST_USER_ID).sendMessage({
50
+ content: `Use <#${EXTERNAL_CHANNEL_ID}> CHANNEL_REFERENCE_PERMISSION_MARKER followup`,
51
+ });
52
+ await waitForBotMessageContaining({
53
+ discord: ctx.discord,
54
+ threadId: thread.id,
55
+ userId: TEST_USER_ID,
56
+ text: 'channel-reference-read-done',
57
+ afterUserMessageIncludes: 'followup',
58
+ timeout: 8_000,
59
+ });
60
+ await waitForFooterMessage({
61
+ discord: ctx.discord,
62
+ threadId: thread.id,
63
+ timeout: 4_000,
64
+ afterMessageIncludes: 'channel-reference-read-done',
65
+ afterAuthorId: ctx.discord.botUserId,
66
+ });
67
+ const text = await th.text();
68
+ expect(text).toMatchInlineSnapshot(`
69
+ "--- from: user (channel-reference-tester)
70
+ Use <#200000000000001022> CHANNEL_REFERENCE_PERMISSION_MARKER first
71
+ --- from: assistant (TestBot)
72
+ *using deterministic-provider/deterministic-v2*
73
+ ⬥ reading referenced channel directory
74
+ ⬥ channel-reference-read-done
75
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
76
+ --- from: user (channel-reference-tester)
77
+ Use <#200000000000001022> CHANNEL_REFERENCE_PERMISSION_MARKER followup
78
+ --- from: assistant (TestBot)
79
+ ⬥ reading referenced channel directory
80
+ ⬥ channel-reference-read-done
81
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
82
+ `);
83
+ expect(text).not.toContain('Permission Required');
84
+ });
85
+ });
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  import { spawn, execSync } from 'node:child_process';
12
12
  import { createLogger, LogPrefix, initLogFile } from '../logger.js';
13
13
  import { createDiscordClient, initDatabase, getChannelDirectory, initializeOpencodeForDirectory, createProjectChannels } from '../discord-bot.js';
14
- import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getPrisma, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
14
+ import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
15
15
  import { ShareMarkdown } from '../markdown.js';
16
16
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
17
17
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  import { spawn, execSync } from 'node:child_process';
12
12
  import { createLogger, LogPrefix, initLogFile } from '../logger.js';
13
13
  import { createDiscordClient, initDatabase, getChannelDirectory, initializeOpencodeForDirectory, createProjectChannels } from '../discord-bot.js';
14
- import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getPrisma, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
14
+ import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
15
15
  import { ShareMarkdown } from '../markdown.js';
16
16
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
17
17
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  import { spawn, execSync } from 'node:child_process';
12
12
  import { createLogger, LogPrefix, initLogFile } from '../logger.js';
13
13
  import { createDiscordClient, initDatabase, getChannelDirectory, initializeOpencodeForDirectory, createProjectChannels } from '../discord-bot.js';
14
- import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getPrisma, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
14
+ import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
15
15
  import { ShareMarkdown } from '../markdown.js';
16
16
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
17
17
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  import { spawn, execSync } from 'node:child_process';
12
12
  import { createLogger, LogPrefix, initLogFile } from '../logger.js';
13
13
  import { createDiscordClient, initDatabase, getChannelDirectory, initializeOpencodeForDirectory, createProjectChannels } from '../discord-bot.js';
14
- import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getPrisma, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
14
+ import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getDb, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
15
15
  import { ShareMarkdown } from '../markdown.js';
16
16
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
17
17
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
@@ -70,10 +70,10 @@ cli
70
70
  guild = foundGuild;
71
71
  }
72
72
  else {
73
- const existingChannelId = await (await getPrisma()).channel_directories.findFirst({
73
+ const existingChannelId = await (await getDb()).query.channel_directories.findFirst({
74
74
  where: { channel_type: 'text' },
75
75
  orderBy: { created_at: 'desc' },
76
- select: { channel_id: true },
76
+ columns: { channel_id: true },
77
77
  }).then((row) => row?.channel_id);
78
78
  if (existingChannelId) {
79
79
  try {
@@ -168,8 +168,8 @@ cli
168
168
  .option('--prune', 'Remove stale entries whose Discord channel no longer exists')
169
169
  .action(async (options) => {
170
170
  await initDatabase();
171
- const prisma = await getPrisma();
172
- const channels = await prisma.channel_directories.findMany({
171
+ const db = await getDb();
172
+ const channels = await db.query.channel_directories.findMany({
173
173
  where: { channel_type: 'text' },
174
174
  orderBy: { created_at: 'desc' },
175
175
  });
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  import { spawn, execSync } from 'node:child_process';
12
12
  import { createLogger, LogPrefix, initLogFile } from '../logger.js';
13
13
  import { createDiscordClient, initDatabase, getChannelDirectory, initializeOpencodeForDirectory, createProjectChannels } from '../discord-bot.js';
14
- import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getPrisma, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
14
+ import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getDb, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
15
15
  import { ShareMarkdown } from '../markdown.js';
16
16
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
17
17
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
@@ -20,7 +20,7 @@ import { buildOpencodeEventLogLine } from '../session-handler/opencode-session-e
20
20
  import { createDiscordRest } from '../discord-urls.js';
21
21
  import { archiveThread, uploadFilesToDiscord, stripMentions } from '../discord-utils.js';
22
22
  import { setDataDir, setProjectsDir, getDataDir, getProjectsDir } from '../config.js';
23
- import { execAsync, validateWorktreeDirectory } from '../worktrees.js';
23
+ import { execAsync, resolveSessionWorkingDirectory } from '../worktrees.js';
24
24
  import { upgrade, getCurrentVersion } from '../upgrade.js';
25
25
  import { getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializeScheduledTaskPayload } from '../task-schedule.js';
26
26
  import { EXIT_NO_RESTART, formatMemberLookupUnavailableMessage, formatRelativeTime, formatTaskScheduleLine, isDiscordMemberLookupUnavailable, isGuildMemberSearchResult, isThreadChannelType, printDiscordInstallUrlAndExit, resolveBotCredentials, resolveDiscordUserOption, sendDiscordMessageWithOptionalAttachment, } from '../cli-runner.js';
@@ -36,7 +36,7 @@ cli
36
36
  .option('-a, --app-id [appId]', 'Bot application ID (required if no local database)')
37
37
  .option('--notify-only', 'Create notification thread without starting AI session')
38
38
  .option('--worktree [name]', 'Create git worktree for session (name optional, derives from thread name)')
39
- .option('--cwd <path>', 'Start session in an existing git worktree directory instead of the main project directory')
39
+ .option('--cwd <path>', 'Start session in an existing project subfolder or git worktree directory')
40
40
  .option('-u, --user <user>', 'Discord user ID, mention, or username to add to thread')
41
41
  .option('--agent <agent>', 'Agent to use for the session')
42
42
  .option('--model <model>', 'Model to use (format: provider/model)')
@@ -104,6 +104,7 @@ cli
104
104
  }
105
105
  process.exit(EXIT_NO_RESTART);
106
106
  }
107
+ const waitStartedAtMs = options.wait ? Date.now() : undefined;
107
108
  if (!existingThreadMode && options.worktree && notifyOnly) {
108
109
  cliLogger.error('Cannot use --worktree with --notify-only');
109
110
  process.exit(EXIT_NO_RESTART);
@@ -208,10 +209,10 @@ cli
208
209
  });
209
210
  // Get guild from existing channels or first available
210
211
  const guild = await (async () => {
211
- const existingChannelId = await (await getPrisma()).channel_directories.findFirst({
212
+ const existingChannelId = await (await getDb()).query.channel_directories.findFirst({
212
213
  where: { channel_type: 'text' },
213
214
  orderBy: { created_at: 'desc' },
214
- select: { channel_id: true },
215
+ columns: { channel_id: true },
215
216
  }).then((row) => row?.channel_id);
216
217
  if (existingChannelId) {
217
218
  try {
@@ -340,6 +341,7 @@ cli
340
341
  await waitAndOutputSession({
341
342
  threadId: targetThreadId,
342
343
  projectDirectory: channelConfig.directory,
344
+ waitStartedAtMs,
343
345
  });
344
346
  }
345
347
  process.exit(0);
@@ -356,10 +358,10 @@ cli
356
358
  throw new Error(`Channel #${channelData.name} is not configured with a project directory. Run the bot first to sync channel data.`);
357
359
  }
358
360
  const projectDirectory = channelConfig.directory;
359
- // Validate --cwd is an existing git worktree of the project
361
+ // Validate --cwd is inside the project or an existing git worktree.
360
362
  let resolvedCwd;
361
363
  if (options.cwd) {
362
- const cwdResult = await validateWorktreeDirectory({
364
+ const cwdResult = await resolveSessionWorkingDirectory({
363
365
  projectDirectory,
364
366
  candidatePath: options.cwd,
365
367
  });
@@ -367,7 +369,7 @@ cli
367
369
  cliLogger.error(cwdResult.message);
368
370
  process.exit(EXIT_NO_RESTART);
369
371
  }
370
- resolvedCwd = cwdResult;
372
+ resolvedCwd = cwdResult.directory;
371
373
  }
372
374
  const resolvedUser = await resolveDiscordUserOption({
373
375
  user: options.user,
@@ -483,6 +485,7 @@ cli
483
485
  await waitAndOutputSession({
484
486
  threadId: threadData.id,
485
487
  projectDirectory,
488
+ waitStartedAtMs,
486
489
  });
487
490
  }
488
491
  process.exit(0);
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  import { spawn, execSync } from 'node:child_process';
12
12
  import { createLogger, LogPrefix, initLogFile } from '../logger.js';
13
13
  import { createDiscordClient, initDatabase, getChannelDirectory, initializeOpencodeForDirectory, createProjectChannels } from '../discord-bot.js';
14
- import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getPrisma, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
14
+ import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getDb, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory, getThreadWorktree } from '../database.js';
15
15
  import { ShareMarkdown } from '../markdown.js';
16
16
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
17
17
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
@@ -26,6 +26,30 @@ import { getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializ
26
26
  import { EXIT_NO_RESTART, formatMemberLookupUnavailableMessage, formatRelativeTime, formatTaskScheduleLine, isDiscordMemberLookupUnavailable, isGuildMemberSearchResult, isThreadChannelType, printDiscordInstallUrlAndExit, resolveBotCredentials, resolveDiscordUserOption, sendDiscordMessageWithOptionalAttachment, } from '../cli-runner.js';
27
27
  const cliLogger = createLogger(LogPrefix.CLI);
28
28
  const cli = goke();
29
+ async function resolveSessionDirectoryFromDatabase({ sessionId, }) {
30
+ const threadId = await getThreadIdBySessionId(sessionId);
31
+ if (threadId) {
32
+ const worktree = await getThreadWorktree(threadId);
33
+ if (worktree?.status === 'ready' && worktree.worktree_directory) {
34
+ return worktree.worktree_directory;
35
+ }
36
+ const { token: botToken } = await resolveBotCredentials({});
37
+ const rest = createDiscordRest(botToken);
38
+ const threadData = (await rest.get(Routes.channel(threadId)));
39
+ if (!isThreadChannelType(threadData.type)) {
40
+ return new Error(`Channel is not a thread: ${threadId}`);
41
+ }
42
+ if (!threadData.parent_id) {
43
+ return new Error(`Thread has no parent channel: ${threadId}`);
44
+ }
45
+ const channelConfig = await getChannelDirectory(threadData.parent_id);
46
+ if (!channelConfig) {
47
+ return new Error(`Thread parent channel is not configured with a project directory: ${threadData.parent_id}`);
48
+ }
49
+ return channelConfig.directory;
50
+ }
51
+ return new Error(`Session is not linked to a Kimaki thread in the local database: ${sessionId}`);
52
+ }
29
53
  cli
30
54
  .command('session list', 'List all OpenCode sessions, marking which were started via Kimaki')
31
55
  .option('--project <path>', 'Project directory to list sessions for (defaults to cwd)')
@@ -47,9 +71,9 @@ cli
47
71
  process.exit(0);
48
72
  }
49
73
  // Look up which sessions were started via kimaki (have a thread mapping)
50
- const prisma = await getPrisma();
51
- const threadSessions = await prisma.thread_sessions.findMany({
52
- select: { thread_id: true, session_id: true },
74
+ const db = await getDb();
75
+ const threadSessions = await db.query.thread_sessions.findMany({
76
+ columns: { thread_id: true, session_id: true },
53
77
  });
54
78
  const sessionToThread = new Map(threadSessions
55
79
  .filter((row) => row.session_id !== '')
@@ -162,6 +186,30 @@ cli
162
186
  process.exit(EXIT_NO_RESTART);
163
187
  }
164
188
  });
189
+ cli
190
+ .command('session wait <sessionId>', 'Wait for a session to finish, then print its conversation as markdown')
191
+ .action(async (sessionId) => {
192
+ try {
193
+ await initDatabase();
194
+ const projectDirectory = await resolveSessionDirectoryFromDatabase({
195
+ sessionId,
196
+ });
197
+ if (projectDirectory instanceof Error) {
198
+ cliLogger.error(projectDirectory.message);
199
+ process.exit(EXIT_NO_RESTART);
200
+ }
201
+ const { waitAndOutputExistingSession } = await import('../wait-session.js');
202
+ await waitAndOutputExistingSession({
203
+ sessionId,
204
+ projectDirectory,
205
+ });
206
+ process.exit(0);
207
+ }
208
+ catch (error) {
209
+ cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
210
+ process.exit(EXIT_NO_RESTART);
211
+ }
212
+ });
165
213
  cli
166
214
  .command('session search <query>', 'Search past sessions for text or /regex/flags in the selected project')
167
215
  .option('--project <path>', 'Project directory (defaults to cwd)')
@@ -223,9 +271,9 @@ cli
223
271
  cliLogger.log('No sessions found');
224
272
  process.exit(0);
225
273
  }
226
- const prisma = await getPrisma();
227
- const threadSessions = await prisma.thread_sessions.findMany({
228
- select: { thread_id: true, session_id: true },
274
+ const db = await getDb();
275
+ const threadSessions = await db.query.thread_sessions.findMany({
276
+ columns: { thread_id: true, session_id: true },
229
277
  });
230
278
  const sessionToThread = new Map(threadSessions
231
279
  .filter((row) => row.session_id !== '')
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  import { spawn, execSync } from 'node:child_process';
12
12
  import { createLogger, LogPrefix, initLogFile } from '../logger.js';
13
13
  import { createDiscordClient, initDatabase, getChannelDirectory, initializeOpencodeForDirectory, createProjectChannels } from '../discord-bot.js';
14
- import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getPrisma, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
14
+ import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
15
15
  import { ShareMarkdown } from '../markdown.js';
16
16
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
17
17
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  import { spawn, execSync } from 'node:child_process';
12
12
  import { createLogger, LogPrefix, initLogFile } from '../logger.js';
13
13
  import { createDiscordClient, initDatabase, getChannelDirectory, initializeOpencodeForDirectory, createProjectChannels } from '../discord-bot.js';
14
- import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getPrisma, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
14
+ import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
15
15
  import { ShareMarkdown } from '../markdown.js';
16
16
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
17
17
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
@@ -3,7 +3,9 @@
3
3
  import { intro, outro, text, password, note, cancel, isCancel, confirm, log, multiselect, select, spinner, } from '@clack/prompts';
4
4
  import { deduplicateByKey, generateBotInstallUrl, generateDiscordInstallUrlForBot, KIMAKI_GATEWAY_APP_ID, KIMAKI_WEBSITE_URL, abbreviatePath, } from './utils.js';
5
5
  import { getChannelsWithDescriptions, createDiscordClient, initDatabase, getChannelDirectory, startDiscordBot, initializeOpencodeForDirectory, createProjectChannels, createDefaultKimakiChannel, } from './discord-bot.js';
6
- import { getBotTokenWithMode, ensureServiceAuthToken, setBotToken, setBotMode, setChannelDirectory, findChannelsByDirectory, getPrisma, } from './database.js';
6
+ import { getBotTokenWithMode, ensureServiceAuthToken, setBotToken, setBotMode, setChannelDirectory, findChannelsByDirectory, getDb, } from './database.js';
7
+ import * as orm from 'drizzle-orm';
8
+ import * as dbSchema from './schema.js';
7
9
  import { selectResolvedCommand } from './opencode-command.js';
8
10
  import { Events, ChannelType, Routes, AttachmentBuilder, } from 'discord.js';
9
11
  import { discordApiUrl, getDiscordRestApiUrl, getGatewayProxyRestBaseUrl, getInternetReachableBaseUrl } from './discord-urls.js';
@@ -327,8 +329,8 @@ export async function resolveGatewayInstallCredentials() {
327
329
  if (!KIMAKI_GATEWAY_APP_ID) {
328
330
  return new Error('Gateway mode is not available yet. KIMAKI_GATEWAY_APP_ID is not configured.');
329
331
  }
330
- const prisma = await getPrisma();
331
- const gatewayBot = await prisma.bot_tokens.findUnique({
332
+ const db = await getDb();
333
+ const gatewayBot = await db.query.bot_tokens.findFirst({
332
334
  where: { app_id: KIMAKI_GATEWAY_APP_ID },
333
335
  });
334
336
  if (gatewayBot?.client_id && gatewayBot.client_secret) {
@@ -687,7 +689,7 @@ export async function resolveCredentials({ forceRestartOnboarding, forceGateway,
687
689
  // directly. This lets users switch back and forth between modes without
688
690
  // re-running the onboarding wizard each time.
689
691
  const hasGatewayCreds = (forceGateway && existingBot?.mode !== 'gateway')
690
- ? await (await getPrisma()).bot_tokens.findUnique({
692
+ ? await (await getDb()).query.bot_tokens.findFirst({
691
693
  where: { app_id: KIMAKI_GATEWAY_APP_ID },
692
694
  })
693
695
  : undefined;
@@ -1029,10 +1031,9 @@ export async function run({ restartOnboarding, addChannels, useWorktrees, enable
1029
1031
  // processes (send, upload-to-discord, project list) pick the correct bot.
1030
1032
  // getBotTokenWithMode() orders by last_used_at DESC as cross-process
1031
1033
  // source of truth.
1032
- await (await getPrisma()).bot_tokens.update({
1033
- where: { app_id: appId },
1034
- data: { last_used_at: new Date() },
1035
- });
1034
+ await (await getDb()).update(dbSchema.bot_tokens)
1035
+ .set({ last_used_at: new Date() })
1036
+ .where(orm.eq(dbSchema.bot_tokens.app_id, appId));
1036
1037
  // skipChannelSetup: when true, skip interactive project/channel selection
1037
1038
  // and go straight to bot startup. Channel sync happens in the background.
1038
1039
  //
@@ -1041,9 +1042,9 @@ export async function run({ restartOnboarding, addChannels, useWorktrees, enable
1041
1042
  // Force channel setup when: first-time quick-start with no channels configured
1042
1043
  // and TTY is available, or user explicitly passed --add-channels.
1043
1044
  const isHeadlessGateway = isGatewayMode && !canUseInteractivePrompts();
1044
- const hasConfiguredTextChannels = Boolean(await (await getPrisma()).channel_directories.findFirst({
1045
+ const hasConfiguredTextChannels = Boolean(await (await getDb()).query.channel_directories.findFirst({
1045
1046
  where: { channel_type: 'text' },
1046
- select: { channel_id: true },
1047
+ columns: { channel_id: true },
1047
1048
  }));
1048
1049
  const skipChannelSetup = isHeadlessGateway || (() => {
1049
1050
  // Wizard source always shows channel setup (user just completed onboarding)
@@ -23,7 +23,7 @@ import { startDiscordBot } from './discord-bot.js';
23
23
  import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelMentionMode, setChannelVerbosity, } from './database.js';
24
24
  import { startHranaServer, stopHranaServer } from './hrana-server.js';
25
25
  import { initializeOpencodeForDirectory, stopOpencodeServer, } from './opencode.js';
26
- import { chooseLockPort, cleanupTestSessions, initTestGitRepo, waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
26
+ import { chooseAvailableLockPort, cleanupTestSessions, initTestGitRepo, waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
27
27
  import YAML from 'yaml';
28
28
  const TEST_USER_ID = '200000000000000830';
29
29
  const TEXT_CHANNEL_ID = '200000000000000831';
@@ -113,7 +113,7 @@ describe('kimaki send --channel thread creation', () => {
113
113
  beforeAll(async () => {
114
114
  testStartTime = Date.now();
115
115
  directories = createRunDirectories();
116
- const lockPort = chooseLockPort({ key: 'cli-send-thread-e2e' });
116
+ const lockPort = await chooseAvailableLockPort({ key: 'cli-send-thread-e2e' });
117
117
  process.env['KIMAKI_LOCK_PORT'] = String(lockPort);
118
118
  setDataDir(directories.dataDir);
119
119
  previousDefaultVerbosity = store.getState().defaultVerbosity;
@@ -3,25 +3,26 @@
3
3
  // clickable thread links and project names via Discord CV2 components.
4
4
  import { ChatInputCommandInteraction, ComponentType, MessageFlags, } from 'discord.js';
5
5
  import path from 'node:path';
6
- import { getPrisma } from '../db.js';
6
+ import { getDb } from '../db.js';
7
7
  import { getChannelDirectory } from '../database.js';
8
8
  import { splitTablesFromMarkdown } from '../format-tables.js';
9
9
  import { formatTimeAgo } from './worktrees.js';
10
10
  const MAX_ROWS = 20;
11
11
  async function fetchRecentSessions({ client, }) {
12
- const prisma = await getPrisma();
12
+ const db = await getDb();
13
13
  // Fetch all thread sessions with their most recent event timestamp.
14
- // Prisma doesn't support ORDER BY aggregated subquery, so we fetch all
15
- // sessions with their latest event and sort in JS.
16
- const sessions = await prisma.thread_sessions.findMany({
17
- select: {
14
+ // Fetch all sessions with their latest event and sort in JS.
15
+ const sessions = await db.query.thread_sessions.findMany({
16
+ columns: {
18
17
  thread_id: true,
19
18
  session_id: true,
20
19
  created_at: true,
20
+ },
21
+ with: {
21
22
  session_events: {
22
23
  orderBy: { timestamp: 'desc' },
23
- take: 1,
24
- select: { timestamp: true },
24
+ limit: 1,
25
+ columns: { timestamp: true },
25
26
  },
26
27
  },
27
28
  });
@@ -1,7 +1,9 @@
1
1
  // /unset-model-override command - Remove model overrides and use default instead.
2
2
  import { ChatInputCommandInteraction, ChannelType, MessageFlags, } from 'discord.js';
3
3
  import { getChannelModel, getSessionModel, getThreadSession, clearSessionModel, } from '../database.js';
4
- import { getPrisma } from '../db.js';
4
+ import { getDb } from '../db.js';
5
+ import * as orm from 'drizzle-orm';
6
+ import * as schema from '../schema.js';
5
7
  import { initializeOpencodeForDirectory } from '../opencode.js';
6
8
  import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
7
9
  import { getRuntime } from '../session-handler/thread-session-runtime.js';
@@ -90,10 +92,8 @@ export async function handleUnsetModelCommand({ interaction, appId, }) {
90
92
  }
91
93
  else if (channelPref) {
92
94
  // Clear channel override
93
- const prisma = await getPrisma();
94
- await prisma.channel_models.deleteMany({
95
- where: { channel_id: targetChannelId },
96
- });
95
+ const db = await getDb();
96
+ await db.delete(schema.channel_models).where(orm.eq(schema.channel_models.channel_id, targetChannelId));
97
97
  clearedType = 'channel';
98
98
  clearedModel = channelPref.modelId;
99
99
  unsetModelLogger.log(`[UNSET-MODEL] Cleared channel model for ${targetChannelId}`);
@@ -5,7 +5,7 @@
5
5
  // 'text_only': only shows text responses
6
6
  import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, MessageFlags, ChannelType, } from 'discord.js';
7
7
  import { getChannelVerbosity, setChannelVerbosity, } from '../database.js';
8
- import { getPrisma } from '../db.js';
8
+ import { getDb } from '../db.js';
9
9
  import { store } from '../store.js';
10
10
  import { createLogger, LogPrefix } from '../logger.js';
11
11
  const verbosityLogger = createLogger(LogPrefix.VERBOSITY);
@@ -45,8 +45,8 @@ function resolveChannelId(channel) {
45
45
  * Returns the override value if it exists, null otherwise.
46
46
  */
47
47
  async function getChannelVerbosityOverride(channelId) {
48
- const prisma = await getPrisma();
49
- const row = await prisma.channel_verbosity.findUnique({
48
+ const db = await getDb();
49
+ const row = await db.query.channel_verbosity.findFirst({
50
50
  where: { channel_id: channelId },
51
51
  });
52
52
  if (row?.verbosity) {
@@ -6,7 +6,7 @@
6
6
  // including HTML-backed action buttons for deletable worktrees.
7
7
  import { ButtonInteraction, ChatInputCommandInteraction, ChannelType, ComponentType, MessageFlags, } from 'discord.js';
8
8
  import { deleteThreadWorktree, } from '../database.js';
9
- import { getPrisma } from '../db.js';
9
+ import { getDb } from '../db.js';
10
10
  import { splitTablesFromMarkdown } from '../format-tables.js';
11
11
  import { buildHtmlActionCustomId, cancelHtmlActionsForOwner, registerHtmlAction, } from '../html-actions.js';
12
12
  import * as errore from 'errore';
@@ -195,8 +195,8 @@ async function resolveGitStatuses({ rows, projectDirectory, timeout, }) {
195
195
  // Git is the source of truth for what exists on disk. DB rows that aren't
196
196
  // in the git list (pending/error) are appended at the end.
197
197
  async function buildWorktreeRows({ projectDirectory, gitWorktrees, }) {
198
- const prisma = await getPrisma();
199
- const dbWorktrees = await prisma.thread_worktrees.findMany({
198
+ const db = await getDb();
199
+ const dbWorktrees = await db.query.thread_worktrees.findMany({
200
200
  where: { project_directory: projectDirectory },
201
201
  });
202
202
  // Index DB worktrees by directory for fast lookup