kimaki 0.9.0 → 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 (291) 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 +10 -10
  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 +30 -16
  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/onboarding-tutorial.js +1 -1
  27. package/dist/opencode.js +28 -5
  28. package/dist/queue-advanced-e2e-setup.js +64 -1
  29. package/dist/schema.js +270 -0
  30. package/dist/session-handler/event-stream-state.js +18 -0
  31. package/dist/session-handler/event-stream-state.test.js +46 -1
  32. package/dist/session-handler/thread-session-runtime.js +82 -17
  33. package/dist/startup-time.e2e.test.js +1 -1
  34. package/dist/system-message.js +31 -18
  35. package/dist/system-message.test.js +49 -33
  36. package/dist/test-utils.js +25 -0
  37. package/dist/voice-message.e2e.test.js +7 -0
  38. package/dist/wait-session.js +103 -26
  39. package/dist/worktree-lifecycle.e2e.test.js +80 -4
  40. package/dist/worktrees.js +41 -0
  41. package/dist/worktrees.test.js +119 -1
  42. package/package.json +10 -13
  43. package/skills/egaki/SKILL.md +10 -1
  44. package/skills/errore/SKILL.md +20 -8
  45. package/skills/goke/SKILL.md +302 -1
  46. package/skills/sigillo/SKILL.md +214 -36
  47. package/skills/spiceflow/SKILL.md +100 -0
  48. package/skills/tuistory/SKILL.md +1 -1
  49. package/src/agent-model.e2e.test.ts +13 -23
  50. package/src/channel-reference-permissions.e2e.test.ts +102 -0
  51. package/src/cli-commands/bot.ts +1 -1
  52. package/src/cli-commands/maintenance.ts +1 -1
  53. package/src/cli-commands/misc.ts +1 -1
  54. package/src/cli-commands/project.ts +5 -5
  55. package/src/cli-commands/send.ts +12 -8
  56. package/src/cli-commands/session.ts +79 -7
  57. package/src/cli-commands/task.ts +1 -1
  58. package/src/cli-commands/user.ts +10 -11
  59. package/src/cli-runner.ts +11 -10
  60. package/src/cli-send-thread.e2e.test.ts +2 -2
  61. package/src/commands/fork.ts +1 -1
  62. package/src/commands/last-sessions.ts +9 -8
  63. package/src/commands/unset-model.ts +7 -5
  64. package/src/commands/verbosity.ts +3 -3
  65. package/src/commands/worktrees.ts +3 -3
  66. package/src/database.ts +528 -1618
  67. package/src/db.test.ts +80 -26
  68. package/src/db.ts +78 -158
  69. package/src/discord-bot.ts +33 -16
  70. package/src/hrana-server.test.ts +39 -343
  71. package/src/hrana-server.ts +1 -1
  72. package/src/ipc-polling.ts +2 -2
  73. package/src/ipc-tools-plugin.ts +9 -17
  74. package/src/message-preprocessing.ts +37 -3
  75. package/src/onboarding-tutorial.ts +1 -1
  76. package/src/opencode.ts +31 -5
  77. package/src/queue-advanced-e2e-setup.ts +72 -0
  78. package/src/schema.sql +191 -173
  79. package/src/schema.ts +303 -0
  80. package/src/session-handler/event-stream-state.test.ts +49 -0
  81. package/src/session-handler/event-stream-state.ts +29 -0
  82. package/src/session-handler/thread-runtime-state.ts +4 -0
  83. package/src/session-handler/thread-session-runtime.ts +109 -19
  84. package/src/startup-time.e2e.test.ts +1 -1
  85. package/src/store.ts +1 -1
  86. package/src/system-message.test.ts +49 -33
  87. package/src/system-message.ts +32 -19
  88. package/src/test-utils.ts +27 -0
  89. package/src/voice-message.e2e.test.ts +8 -0
  90. package/src/wait-session.ts +154 -33
  91. package/src/worktree-lifecycle.e2e.test.ts +94 -3
  92. package/src/worktrees.test.ts +136 -0
  93. package/src/worktrees.ts +72 -0
  94. package/dist/acp-client.test.js +0 -149
  95. package/dist/adapter-rest-boundary.test.js +0 -34
  96. package/dist/add-directory.e2e.test.js +0 -101
  97. package/dist/bash-tool.js +0 -194
  98. package/dist/bash-tool.test.js +0 -82
  99. package/dist/bundled-skills.js +0 -37
  100. package/dist/cli-commands/core.js +0 -1909
  101. package/dist/commands/add-directory.js +0 -67
  102. package/dist/commands/channel-ref.js +0 -16
  103. package/dist/commands/discord-install-url.js +0 -36
  104. package/dist/commands/sqlitedb.js +0 -16
  105. package/dist/commands/stop-opencode-server.js +0 -80
  106. package/dist/commands/thinking.js +0 -128
  107. package/dist/commands/vscode.test.js +0 -44
  108. package/dist/commands/worktree.js +0 -279
  109. package/dist/config-lock-port.test.js +0 -17
  110. package/dist/diff-patch-plugin.js +0 -314
  111. package/dist/directVoiceStreaming.js +0 -102
  112. package/dist/directory-permissions.js +0 -38
  113. package/dist/directory-permissions.test.js +0 -37
  114. package/dist/discord-js-import-boundary.test.js +0 -62
  115. package/dist/discord-ws-proxy.js +0 -350
  116. package/dist/discord-ws-proxy.test.js +0 -500
  117. package/dist/discordBot.js +0 -2814
  118. package/dist/external-opencode-sync.test.js +0 -151
  119. package/dist/fork.js +0 -163
  120. package/dist/forum-sync.js +0 -953
  121. package/dist/gateway-session.js +0 -163
  122. package/dist/generated/browser.js +0 -17
  123. package/dist/generated/client.js +0 -37
  124. package/dist/generated/cloudflare/browser.js +0 -17
  125. package/dist/generated/cloudflare/client.js +0 -34
  126. package/dist/generated/cloudflare/commonInputTypes.js +0 -10
  127. package/dist/generated/cloudflare/enums.js +0 -48
  128. package/dist/generated/cloudflare/internal/class.js +0 -47
  129. package/dist/generated/cloudflare/internal/prismaNamespace.js +0 -252
  130. package/dist/generated/cloudflare/internal/prismaNamespaceBrowser.js +0 -222
  131. package/dist/generated/cloudflare/internal/query_compiler_fast_bg.js +0 -135
  132. package/dist/generated/cloudflare/models/bot_api_keys.js +0 -1
  133. package/dist/generated/cloudflare/models/bot_tokens.js +0 -1
  134. package/dist/generated/cloudflare/models/channel_agents.js +0 -1
  135. package/dist/generated/cloudflare/models/channel_directories.js +0 -1
  136. package/dist/generated/cloudflare/models/channel_mention_mode.js +0 -1
  137. package/dist/generated/cloudflare/models/channel_models.js +0 -1
  138. package/dist/generated/cloudflare/models/channel_verbosity.js +0 -1
  139. package/dist/generated/cloudflare/models/channel_worktrees.js +0 -1
  140. package/dist/generated/cloudflare/models/forum_sync_configs.js +0 -1
  141. package/dist/generated/cloudflare/models/global_models.js +0 -1
  142. package/dist/generated/cloudflare/models/ipc_requests.js +0 -1
  143. package/dist/generated/cloudflare/models/part_messages.js +0 -1
  144. package/dist/generated/cloudflare/models/scheduled_tasks.js +0 -1
  145. package/dist/generated/cloudflare/models/session_agents.js +0 -1
  146. package/dist/generated/cloudflare/models/session_events.js +0 -1
  147. package/dist/generated/cloudflare/models/session_models.js +0 -1
  148. package/dist/generated/cloudflare/models/session_start_sources.js +0 -1
  149. package/dist/generated/cloudflare/models/thread_sessions.js +0 -1
  150. package/dist/generated/cloudflare/models/thread_worktrees.js +0 -1
  151. package/dist/generated/cloudflare/models.js +0 -1
  152. package/dist/generated/commonInputTypes.js +0 -10
  153. package/dist/generated/enums.js +0 -52
  154. package/dist/generated/internal/class.js +0 -49
  155. package/dist/generated/internal/prismaNamespace.js +0 -254
  156. package/dist/generated/internal/prismaNamespaceBrowser.js +0 -224
  157. package/dist/generated/models/bot_api_keys.js +0 -1
  158. package/dist/generated/models/bot_tokens.js +0 -1
  159. package/dist/generated/models/channel_agents.js +0 -1
  160. package/dist/generated/models/channel_directories.js +0 -1
  161. package/dist/generated/models/channel_mention_mode.js +0 -1
  162. package/dist/generated/models/channel_models.js +0 -1
  163. package/dist/generated/models/channel_verbosity.js +0 -1
  164. package/dist/generated/models/channel_worktrees.js +0 -1
  165. package/dist/generated/models/external_session_pending_prompts.js +0 -1
  166. package/dist/generated/models/forum_sync_configs.js +0 -1
  167. package/dist/generated/models/global_models.js +0 -1
  168. package/dist/generated/models/ipc_requests.js +0 -1
  169. package/dist/generated/models/part_messages.js +0 -1
  170. package/dist/generated/models/pending_auto_start.js +0 -1
  171. package/dist/generated/models/scheduled_tasks.js +0 -1
  172. package/dist/generated/models/session_agents.js +0 -1
  173. package/dist/generated/models/session_events.js +0 -1
  174. package/dist/generated/models/session_models.js +0 -1
  175. package/dist/generated/models/session_start_sources.js +0 -1
  176. package/dist/generated/models/session_thinking.js +0 -1
  177. package/dist/generated/models/thread_allowed_directories.js +0 -1
  178. package/dist/generated/models/thread_sessions.js +0 -1
  179. package/dist/generated/models/thread_worktrees.js +0 -1
  180. package/dist/generated/models.js +0 -1
  181. package/dist/generated/node/browser.js +0 -17
  182. package/dist/generated/node/client.js +0 -37
  183. package/dist/generated/node/commonInputTypes.js +0 -10
  184. package/dist/generated/node/enums.js +0 -48
  185. package/dist/generated/node/internal/class.js +0 -49
  186. package/dist/generated/node/internal/prismaNamespace.js +0 -252
  187. package/dist/generated/node/internal/prismaNamespaceBrowser.js +0 -222
  188. package/dist/generated/node/models/bot_api_keys.js +0 -1
  189. package/dist/generated/node/models/bot_tokens.js +0 -1
  190. package/dist/generated/node/models/channel_agents.js +0 -1
  191. package/dist/generated/node/models/channel_directories.js +0 -1
  192. package/dist/generated/node/models/channel_mention_mode.js +0 -1
  193. package/dist/generated/node/models/channel_models.js +0 -1
  194. package/dist/generated/node/models/channel_verbosity.js +0 -1
  195. package/dist/generated/node/models/channel_worktrees.js +0 -1
  196. package/dist/generated/node/models/forum_sync_configs.js +0 -1
  197. package/dist/generated/node/models/global_models.js +0 -1
  198. package/dist/generated/node/models/ipc_requests.js +0 -1
  199. package/dist/generated/node/models/part_messages.js +0 -1
  200. package/dist/generated/node/models/scheduled_tasks.js +0 -1
  201. package/dist/generated/node/models/session_agents.js +0 -1
  202. package/dist/generated/node/models/session_events.js +0 -1
  203. package/dist/generated/node/models/session_models.js +0 -1
  204. package/dist/generated/node/models/session_start_sources.js +0 -1
  205. package/dist/generated/node/models/thread_sessions.js +0 -1
  206. package/dist/generated/node/models/thread_worktrees.js +0 -1
  207. package/dist/generated/node/models.js +0 -1
  208. package/dist/install-url.js +0 -27
  209. package/dist/kimaki-opencode-plugin-specs.js +0 -13
  210. package/dist/kimaki-real-discord.e2e.test.js +0 -294
  211. package/dist/kitty-graphics-parser.js +0 -3
  212. package/dist/kitty-graphics-parser.test.js +0 -276
  213. package/dist/kitty-graphics-plugin.js +0 -3
  214. package/dist/memory-overview-plugin.test.js +0 -262
  215. package/dist/message-flags-boundary.test.js +0 -54
  216. package/dist/message-preprocessing.test.js +0 -35
  217. package/dist/model-command.js +0 -293
  218. package/dist/onboarding-tutorial-plugin.js +0 -73
  219. package/dist/opencode-plugin-interrupt.test.js +0 -138
  220. package/dist/opencode-plugin-loading.e2e.test.js +0 -80
  221. package/dist/opencode-plugin.js +0 -13
  222. package/dist/opencode-plugin.test.js +0 -98
  223. package/dist/opencode.test.js +0 -85
  224. package/dist/orphan-opencode-sweep.test.js +0 -80
  225. package/dist/pkce.js +0 -23
  226. package/dist/platform/components-v2.js +0 -20
  227. package/dist/platform/discord-adapter.js +0 -1440
  228. package/dist/platform/discord-routes.js +0 -31
  229. package/dist/platform/message-flags.js +0 -8
  230. package/dist/platform/platform-value.js +0 -41
  231. package/dist/platform/slack-adapter.js +0 -872
  232. package/dist/platform/slack-markdown.js +0 -169
  233. package/dist/platform/types.js +0 -4
  234. package/dist/plugin.js +0 -1414
  235. package/dist/proxy-ws-preload.cjs +0 -85
  236. package/dist/session-handler/runtime-types.js +0 -3
  237. package/dist/session-handler/state.js +0 -53
  238. package/dist/session-handler/state.test.js +0 -52
  239. package/dist/session-handler/thread-runtime-state.test.js +0 -287
  240. package/dist/subagent-rate-limit-plugin.test.js +0 -120
  241. package/dist/system-prompt-drift-plugin.js +0 -248
  242. package/dist/system-prompt-drift-plugin.test.js +0 -158
  243. package/dist/thinking-utils.test.js +0 -48
  244. package/dist/thread-queue-advanced.e2e.test.js +0 -884
  245. package/dist/token-usage.js +0 -11
  246. package/dist/xai-realtime.js +0 -95
  247. package/schema.prisma +0 -296
  248. package/skills/event-sourcing-state/SKILL.md +0 -252
  249. package/skills/jitter/EDITOR.md +0 -219
  250. package/skills/jitter/EXPORT-INTERNALS.md +0 -309
  251. package/skills/jitter/SKILL.md +0 -158
  252. package/skills/jitter/jitter-clipboard.json +0 -1042
  253. package/skills/jitter/package.json +0 -14
  254. package/skills/jitter/tsconfig.json +0 -15
  255. package/skills/jitter/utils/actions.ts +0 -212
  256. package/skills/jitter/utils/export.ts +0 -114
  257. package/skills/jitter/utils/index.ts +0 -141
  258. package/skills/jitter/utils/snapshot.ts +0 -154
  259. package/skills/jitter/utils/traverse.ts +0 -246
  260. package/skills/jitter/utils/types.ts +0 -279
  261. package/skills/jitter/utils/wait.ts +0 -133
  262. package/skills/proxyman/SKILL.md +0 -215
  263. package/skills/x-articles/SKILL.md +0 -554
  264. package/skills/zustand-centralized-state/SKILL.md +0 -1004
  265. package/src/generated/browser.ts +0 -114
  266. package/src/generated/client.ts +0 -138
  267. package/src/generated/commonInputTypes.ts +0 -736
  268. package/src/generated/enums.ts +0 -88
  269. package/src/generated/internal/class.ts +0 -384
  270. package/src/generated/internal/prismaNamespace.ts +0 -2387
  271. package/src/generated/internal/prismaNamespaceBrowser.ts +0 -327
  272. package/src/generated/models/bot_api_keys.ts +0 -1288
  273. package/src/generated/models/bot_tokens.ts +0 -1652
  274. package/src/generated/models/channel_agents.ts +0 -1256
  275. package/src/generated/models/channel_directories.ts +0 -1859
  276. package/src/generated/models/channel_mention_mode.ts +0 -1300
  277. package/src/generated/models/channel_models.ts +0 -1288
  278. package/src/generated/models/channel_verbosity.ts +0 -1228
  279. package/src/generated/models/channel_worktrees.ts +0 -1300
  280. package/src/generated/models/forum_sync_configs.ts +0 -1452
  281. package/src/generated/models/global_models.ts +0 -1288
  282. package/src/generated/models/ipc_requests.ts +0 -1485
  283. package/src/generated/models/part_messages.ts +0 -1302
  284. package/src/generated/models/scheduled_tasks.ts +0 -2320
  285. package/src/generated/models/session_agents.ts +0 -1086
  286. package/src/generated/models/session_events.ts +0 -1439
  287. package/src/generated/models/session_models.ts +0 -1114
  288. package/src/generated/models/session_start_sources.ts +0 -1408
  289. package/src/generated/models/thread_sessions.ts +0 -1833
  290. package/src/generated/models/thread_worktrees.ts +0 -1356
  291. 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';
@@ -89,25 +89,25 @@ cli
89
89
  });
90
90
  cli
91
91
  .command('tunnel', 'Expose a local port via tunnel')
92
- .option('-p, --port <port>', 'Local port to expose (required)')
92
+ .option('-p, --port <port>', 'Local port to expose (optional when command output reveals one)')
93
93
  .option('-t, --tunnel-id [id]', 'Custom tunnel ID (only for services safe to expose publicly; prefer random default)')
94
94
  .option('-h, --host [host]', 'Local host (default: localhost)')
95
95
  .option('-s, --server [url]', 'Tunnel server URL')
96
96
  .option('-k, --kill', 'Kill any existing process on the port before starting')
97
97
  .action(async (options) => {
98
- const { runTunnel, parseCommandFromArgv, CLI_NAME } = await import('traforo/run-tunnel');
99
- if (!options.port) {
100
- cliLogger.error('Error: --port is required');
101
- cliLogger.error(`\nUsage: kimaki tunnel -p <port> [-- command]`);
98
+ const { runTunnel, parseCommandFromArgv } = await import('traforo/run-tunnel');
99
+ const { command } = parseCommandFromArgv(process.argv);
100
+ if (!options.port && command.length === 0) {
101
+ cliLogger.error('Error: --port is required unless a command is provided after --');
102
+ cliLogger.error(`\nUsage: kimaki tunnel [-- command]`);
103
+ cliLogger.error(` or: kimaki tunnel --port <port>`);
102
104
  process.exit(EXIT_NO_RESTART);
103
105
  }
104
- const port = parseInt(options.port, 10);
105
- if (isNaN(port) || port < 1 || port > 65535) {
106
+ const port = options.port ? parseInt(options.port, 10) : undefined;
107
+ if (options.port && (!port || port < 1 || port > 65535)) {
106
108
  cliLogger.error(`Error: Invalid port number: ${options.port}`);
107
109
  process.exit(EXIT_NO_RESTART);
108
110
  }
109
- // Parse command after -- from argv
110
- const { command } = parseCommandFromArgv(process.argv);
111
111
  await runTunnel({
112
112
  port,
113
113
  tunnelId: options.tunnelId || undefined,
@@ -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) {