kimaki 1.3.0 → 1.5.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 (346) hide show
  1. package/assets/fonts/Geist-Bold.ttf +0 -0
  2. package/assets/fonts/Geist-Regular.ttf +0 -0
  3. package/assets/fonts/GeistMono-Regular.ttf +0 -0
  4. package/assets/fonts/OFL.txt +92 -0
  5. package/dist/agent-ui.e2e.test.js +27 -13
  6. package/dist/agent-ui.e2e.test.js.map +1 -1
  7. package/dist/agent-ui.js +2 -1
  8. package/dist/agent-ui.js.map +1 -1
  9. package/dist/analytics.e2e.test.js +1 -1
  10. package/dist/analytics.e2e.test.js.map +1 -1
  11. package/dist/autostart.d.ts +4 -0
  12. package/dist/autostart.js +55 -10
  13. package/dist/autostart.js.map +1 -1
  14. package/dist/bot.d.ts +13 -6
  15. package/dist/bot.js +17 -15
  16. package/dist/bot.js.map +1 -1
  17. package/dist/btw.e2e.test.js +3 -3
  18. package/dist/builtin-skills.d.ts +3 -0
  19. package/dist/builtin-skills.js +29 -0
  20. package/dist/builtin-skills.js.map +1 -1
  21. package/dist/builtin-skills.test.js +72 -1
  22. package/dist/builtin-skills.test.js.map +1 -1
  23. package/dist/catalog-refresh.e2e.test.js +2 -2
  24. package/dist/cli/bot.d.ts +1 -1
  25. package/dist/cli/bot.js +33 -18
  26. package/dist/cli/bot.js.map +1 -1
  27. package/dist/cli/discord.js +32 -6
  28. package/dist/cli/discord.js.map +1 -1
  29. package/dist/cli/schedule.js +23 -9
  30. package/dist/cli/schedule.js.map +1 -1
  31. package/dist/cli/session.js +37 -12
  32. package/dist/cli/session.js.map +1 -1
  33. package/dist/cli/shared.d.ts +5 -3
  34. package/dist/cli/shared.js +45 -10
  35. package/dist/cli/shared.js.map +1 -1
  36. package/dist/cli-discord.e2e.test.js +13 -8
  37. package/dist/cli-discord.e2e.test.js.map +1 -1
  38. package/dist/cli-lock.e2e.test.js +87 -20
  39. package/dist/cli-lock.e2e.test.js.map +1 -1
  40. package/dist/cli-read.e2e.test.js +18 -9
  41. package/dist/cli-read.e2e.test.js.map +1 -1
  42. package/dist/cli-sessions.e2e.test.js +4 -3
  43. package/dist/cli-sessions.e2e.test.js.map +1 -1
  44. package/dist/cli.js +2 -0
  45. package/dist/cli.js.map +1 -1
  46. package/dist/commands/preference-commands.d.ts +4 -0
  47. package/dist/commands/preference-commands.js +38 -4
  48. package/dist/commands/preference-commands.js.map +1 -1
  49. package/dist/commands/session-commands.js +11 -8
  50. package/dist/commands/session-commands.js.map +1 -1
  51. package/dist/commands/thread-commands.js +94 -24
  52. package/dist/commands/thread-commands.js.map +1 -1
  53. package/dist/commands/worktree-commands.d.ts +5 -6
  54. package/dist/commands/worktree-commands.js +8 -7
  55. package/dist/commands/worktree-commands.js.map +1 -1
  56. package/dist/commands.e2e.test.js +25 -21
  57. package/dist/commands.e2e.test.js.map +1 -1
  58. package/dist/coordinator-agents.e2e.test.d.ts +1 -0
  59. package/dist/coordinator-agents.e2e.test.js +113 -0
  60. package/dist/coordinator-agents.e2e.test.js.map +1 -0
  61. package/dist/coordinator-idle.e2e.test.d.ts +1 -0
  62. package/dist/coordinator-idle.e2e.test.js +100 -0
  63. package/dist/coordinator-idle.e2e.test.js.map +1 -0
  64. package/dist/coordinator-queue.e2e.test.d.ts +1 -0
  65. package/dist/coordinator-queue.e2e.test.js +244 -0
  66. package/dist/coordinator-queue.e2e.test.js.map +1 -0
  67. package/dist/coordinator-warmup.e2e.test.d.ts +1 -0
  68. package/dist/coordinator-warmup.e2e.test.js +131 -0
  69. package/dist/coordinator-warmup.e2e.test.js.map +1 -0
  70. package/dist/coordinator.d.ts +6 -0
  71. package/dist/coordinator.e2e.test.d.ts +1 -0
  72. package/dist/coordinator.e2e.test.js +505 -0
  73. package/dist/coordinator.e2e.test.js.map +1 -0
  74. package/dist/coordinator.js +164 -0
  75. package/dist/coordinator.js.map +1 -0
  76. package/dist/db.d.ts +78 -3
  77. package/dist/db.test.js +1 -0
  78. package/dist/db.test.js.map +1 -1
  79. package/dist/diagram.d.ts +11 -0
  80. package/dist/diagram.js +232 -0
  81. package/dist/diagram.js.map +1 -0
  82. package/dist/diagram.test.d.ts +1 -0
  83. package/dist/diagram.test.js +52 -0
  84. package/dist/diagram.test.js.map +1 -0
  85. package/dist/duration.js +13 -5
  86. package/dist/duration.js.map +1 -1
  87. package/dist/effects.d.ts +6 -1
  88. package/dist/effects.js +97 -26
  89. package/dist/effects.js.map +1 -1
  90. package/dist/event-loop.d.ts +2 -3
  91. package/dist/event-loop.js +74 -44
  92. package/dist/event-loop.js.map +1 -1
  93. package/dist/format-parts.d.ts +21 -4
  94. package/dist/format-parts.js +40 -23
  95. package/dist/format-parts.js.map +1 -1
  96. package/dist/gateway-replay.e2e.test.js +4 -4
  97. package/dist/harness.e2e.test.js +2 -2
  98. package/dist/harness.e2e.test.js.map +1 -1
  99. package/dist/hello-thread.e2e.test.js +56 -5
  100. package/dist/hello-thread.e2e.test.js.map +1 -1
  101. package/dist/ingress.js +47 -12
  102. package/dist/ingress.js.map +1 -1
  103. package/dist/interaction-context.d.ts +4 -4
  104. package/dist/interaction-context.js +9 -7
  105. package/dist/interaction-context.js.map +1 -1
  106. package/dist/interrupt.e2e.test.js +8 -5
  107. package/dist/interrupt.e2e.test.js.map +1 -1
  108. package/dist/legacy.e2e.test.js +1 -1
  109. package/dist/lock-routes.d.ts +83 -31
  110. package/dist/lock-routes.js +68 -8
  111. package/dist/lock-routes.js.map +1 -1
  112. package/dist/lock-server.d.ts +3 -2
  113. package/dist/lock-server.js +23 -7
  114. package/dist/lock-server.js.map +1 -1
  115. package/dist/main.d.ts +6 -3
  116. package/dist/main.js +14 -4
  117. package/dist/main.js.map +1 -1
  118. package/dist/mainmast-idle.e2e.test.d.ts +1 -0
  119. package/dist/mainmast-idle.e2e.test.js +99 -0
  120. package/dist/mainmast-idle.e2e.test.js.map +1 -0
  121. package/dist/mainmast-queue.e2e.test.d.ts +1 -0
  122. package/dist/mainmast-queue.e2e.test.js +244 -0
  123. package/dist/mainmast-queue.e2e.test.js.map +1 -0
  124. package/dist/mainmast-warmup.e2e.test.d.ts +1 -0
  125. package/dist/mainmast-warmup.e2e.test.js +130 -0
  126. package/dist/mainmast-warmup.e2e.test.js.map +1 -0
  127. package/dist/mainmast.d.ts +6 -0
  128. package/dist/mainmast.e2e.test.d.ts +1 -0
  129. package/dist/mainmast.e2e.test.js +428 -0
  130. package/dist/mainmast.e2e.test.js.map +1 -0
  131. package/dist/mainmast.js +156 -0
  132. package/dist/mainmast.js.map +1 -0
  133. package/dist/markdown/markdown.test.js +14 -4
  134. package/dist/markdown/markdown.test.js.map +1 -1
  135. package/dist/markdown/render-markdown.js +9 -6
  136. package/dist/markdown/render-markdown.js.map +1 -1
  137. package/dist/onboarding-cli.e2e.test.js +7 -6
  138. package/dist/onboarding-cli.e2e.test.js.map +1 -1
  139. package/dist/onboarding.d.ts +3 -10
  140. package/dist/onboarding.e2e.test.js +8 -10
  141. package/dist/onboarding.e2e.test.js.map +1 -1
  142. package/dist/onboarding.js +14 -24
  143. package/dist/onboarding.js.map +1 -1
  144. package/dist/permissions.e2e.test.js +3 -3
  145. package/dist/plugin/coordinator.d.ts +4 -0
  146. package/dist/plugin/coordinator.js +389 -0
  147. package/dist/plugin/coordinator.js.map +1 -0
  148. package/dist/plugin/index.js +93 -61
  149. package/dist/plugin/index.js.map +1 -1
  150. package/dist/plugin/mainmast.d.ts +4 -0
  151. package/dist/plugin/mainmast.js +379 -0
  152. package/dist/plugin/mainmast.js.map +1 -0
  153. package/dist/plugin.e2e.test.js +1 -1
  154. package/dist/preference-commands.e2e.test.js +103 -24
  155. package/dist/preference-commands.e2e.test.js.map +1 -1
  156. package/dist/project.d.ts +1 -1
  157. package/dist/prompt.d.ts +22 -9
  158. package/dist/prompt.js +64 -48
  159. package/dist/prompt.js.map +1 -1
  160. package/dist/questions.e2e.test.js +13 -13
  161. package/dist/queue-sources.e2e.test.d.ts +1 -0
  162. package/dist/queue-sources.e2e.test.js +168 -0
  163. package/dist/queue-sources.e2e.test.js.map +1 -0
  164. package/dist/queue.d.ts +9 -1
  165. package/dist/queue.e2e.test.js +48 -30
  166. package/dist/queue.e2e.test.js.map +1 -1
  167. package/dist/queue.js +97 -24
  168. package/dist/queue.js.map +1 -1
  169. package/dist/queue.test.d.ts +1 -0
  170. package/dist/queue.test.js +57 -0
  171. package/dist/queue.test.js.map +1 -0
  172. package/dist/remote-send.e2e.test.js +6 -6
  173. package/dist/remote-send.js +2 -1
  174. package/dist/remote-send.js.map +1 -1
  175. package/dist/renderer.e2e.test.js +7 -7
  176. package/dist/restart.e2e.test.js +2 -2
  177. package/dist/routines.e2e.test.js +2 -2
  178. package/dist/scheduler.d.ts +3 -3
  179. package/dist/scheduler.e2e.test.js +43 -20
  180. package/dist/scheduler.e2e.test.js.map +1 -1
  181. package/dist/scheduler.js +4 -2
  182. package/dist/scheduler.js.map +1 -1
  183. package/dist/schema-sql.d.ts +1 -1
  184. package/dist/schema-sql.js +1 -1
  185. package/dist/schema-sql.js.map +1 -1
  186. package/dist/schema.d.ts +152 -6
  187. package/dist/schema.js +12 -0
  188. package/dist/schema.js.map +1 -1
  189. package/dist/session-commands.e2e.test.js +36 -11
  190. package/dist/session-commands.e2e.test.js.map +1 -1
  191. package/dist/session-events.d.ts +18 -6
  192. package/dist/session-events.js +175 -27
  193. package/dist/session-events.js.map +1 -1
  194. package/dist/session-ops.e2e.test.js +63 -12
  195. package/dist/session-ops.e2e.test.js.map +1 -1
  196. package/dist/sessions.d.ts +44 -21
  197. package/dist/sessions.js +177 -65
  198. package/dist/sessions.js.map +1 -1
  199. package/dist/shell.e2e.test.js +4 -4
  200. package/dist/slash-commands.js +6 -4
  201. package/dist/slash-commands.js.map +1 -1
  202. package/dist/slash-commands.test.js +2 -0
  203. package/dist/slash-commands.test.js.map +1 -1
  204. package/dist/sleep.e2e.test.js +29 -14
  205. package/dist/sleep.e2e.test.js.map +1 -1
  206. package/dist/sleeps.d.ts +2 -1
  207. package/dist/sleeps.js +21 -3
  208. package/dist/sleeps.js.map +1 -1
  209. package/dist/store.d.ts +1 -0
  210. package/dist/store.js +3 -2
  211. package/dist/store.js.map +1 -1
  212. package/dist/system-prompt.d.ts +5 -15
  213. package/dist/system-prompt.js +180 -131
  214. package/dist/system-prompt.js.map +1 -1
  215. package/dist/test/generate-log-plugin/index.d.ts +3 -0
  216. package/dist/test/generate-log-plugin/index.js +15 -0
  217. package/dist/test/generate-log-plugin/index.js.map +1 -0
  218. package/dist/test/harness.d.ts +20 -2
  219. package/dist/test/harness.js +51 -11
  220. package/dist/test/harness.js.map +1 -1
  221. package/dist/test/replay.js +11 -8
  222. package/dist/test/replay.js.map +1 -1
  223. package/dist/thread-reducer.d.ts +29 -6
  224. package/dist/thread-reducer.fixtures.test.js +58 -51
  225. package/dist/thread-reducer.fixtures.test.js.map +1 -1
  226. package/dist/thread-reducer.js +109 -50
  227. package/dist/thread-reducer.js.map +1 -1
  228. package/dist/thread-reducer.test.js +190 -48
  229. package/dist/thread-reducer.test.js.map +1 -1
  230. package/dist/tunnel.d.ts +9 -0
  231. package/dist/tunnel.js +117 -28
  232. package/dist/tunnel.js.map +1 -1
  233. package/dist/tunnel.test.js +81 -5
  234. package/dist/tunnel.test.js.map +1 -1
  235. package/dist/voice-attachments.e2e.test.js +108 -18
  236. package/dist/voice-attachments.e2e.test.js.map +1 -1
  237. package/dist/voice-call.e2e.test.js +9 -9
  238. package/dist/voice-call.js +6 -6
  239. package/dist/voice-call.js.map +1 -1
  240. package/dist/voice-openai.e2e.test.js +59 -23
  241. package/dist/voice-openai.e2e.test.js.map +1 -1
  242. package/dist/voice.d.ts +1 -1
  243. package/dist/voice.js +72 -24
  244. package/dist/voice.js.map +1 -1
  245. package/dist/working-directory.e2e.test.js +11 -11
  246. package/dist/working-directory.e2e.test.js.map +1 -1
  247. package/package.json +11 -5
  248. package/skills/kimaki-tunnel/SKILL.md +3 -2
  249. package/src/agent-ui.e2e.test.ts +27 -13
  250. package/src/agent-ui.ts +2 -1
  251. package/src/analytics.e2e.test.ts +1 -1
  252. package/src/autostart.ts +55 -10
  253. package/src/bot.ts +36 -19
  254. package/src/btw.e2e.test.ts +3 -3
  255. package/src/builtin-skills.test.ts +73 -1
  256. package/src/builtin-skills.ts +33 -0
  257. package/src/catalog-refresh.e2e.test.ts +2 -2
  258. package/src/cli/bot.ts +30 -19
  259. package/src/cli/discord.ts +30 -6
  260. package/src/cli/schedule.ts +23 -9
  261. package/src/cli/session.ts +35 -12
  262. package/src/cli/shared.ts +43 -8
  263. package/src/cli-discord.e2e.test.ts +13 -8
  264. package/src/cli-lock.e2e.test.ts +88 -20
  265. package/src/cli-read.e2e.test.ts +17 -8
  266. package/src/cli-sessions.e2e.test.ts +4 -3
  267. package/src/cli.ts +2 -0
  268. package/src/commands/preference-commands.ts +38 -4
  269. package/src/commands/session-commands.ts +12 -7
  270. package/src/commands/thread-commands.ts +92 -24
  271. package/src/commands/worktree-commands.ts +10 -9
  272. package/src/commands.e2e.test.ts +25 -21
  273. package/src/coordinator-agents.e2e.test.ts +145 -0
  274. package/src/coordinator-idle.e2e.test.ts +122 -0
  275. package/src/coordinator-queue.e2e.test.ts +279 -0
  276. package/src/coordinator-warmup.e2e.test.ts +158 -0
  277. package/src/coordinator.e2e.test.ts +545 -0
  278. package/src/coordinator.ts +168 -0
  279. package/src/db.test.ts +1 -0
  280. package/src/diagram.test.ts +56 -0
  281. package/src/diagram.ts +243 -0
  282. package/src/duration.ts +13 -5
  283. package/src/effects.ts +99 -27
  284. package/src/event-loop.ts +71 -45
  285. package/src/format-parts.ts +53 -22
  286. package/src/gateway-replay.e2e.test.ts +4 -4
  287. package/src/harness.e2e.test.ts +2 -2
  288. package/src/hello-thread.e2e.test.ts +59 -5
  289. package/src/ingress.ts +44 -11
  290. package/src/interaction-context.ts +12 -11
  291. package/src/interrupt.e2e.test.ts +8 -5
  292. package/src/legacy.e2e.test.ts +1 -1
  293. package/src/lock-routes.ts +62 -8
  294. package/src/lock-server.ts +23 -7
  295. package/src/main.ts +26 -10
  296. package/src/markdown/markdown.test.ts +15 -4
  297. package/src/markdown/render-markdown.ts +9 -6
  298. package/src/onboarding-cli.e2e.test.ts +7 -6
  299. package/src/onboarding.e2e.test.ts +8 -10
  300. package/src/onboarding.ts +13 -29
  301. package/src/permissions.e2e.test.ts +3 -3
  302. package/src/plugin/coordinator.ts +401 -0
  303. package/src/plugin/index.ts +86 -59
  304. package/src/plugin.e2e.test.ts +1 -1
  305. package/src/preference-commands.e2e.test.ts +103 -23
  306. package/src/prompt.ts +87 -43
  307. package/src/questions.e2e.test.ts +13 -13
  308. package/src/queue-sources.e2e.test.ts +197 -0
  309. package/src/queue.e2e.test.ts +48 -30
  310. package/src/queue.test.ts +59 -0
  311. package/src/queue.ts +104 -24
  312. package/src/remote-send.e2e.test.ts +6 -6
  313. package/src/remote-send.ts +2 -1
  314. package/src/renderer.e2e.test.ts +7 -7
  315. package/src/restart.e2e.test.ts +2 -2
  316. package/src/routines.e2e.test.ts +2 -2
  317. package/src/scheduler.e2e.test.ts +45 -19
  318. package/src/scheduler.ts +4 -2
  319. package/src/schema-sql.ts +1 -1
  320. package/src/schema.ts +13 -0
  321. package/src/session-commands.e2e.test.ts +38 -11
  322. package/src/session-events.ts +168 -25
  323. package/src/session-ops.e2e.test.ts +64 -11
  324. package/src/sessions.ts +217 -67
  325. package/src/shell.e2e.test.ts +4 -4
  326. package/src/slash-commands.test.ts +2 -0
  327. package/src/slash-commands.ts +6 -4
  328. package/src/sleep.e2e.test.ts +30 -13
  329. package/src/sleeps.ts +20 -3
  330. package/src/store.ts +5 -2
  331. package/src/system-prompt.ts +190 -134
  332. package/src/test/generate-log-plugin/index.ts +16 -0
  333. package/src/test/harness.ts +77 -17
  334. package/src/test/replay.ts +12 -7
  335. package/src/thread-reducer.fixtures.test.ts +58 -51
  336. package/src/thread-reducer.test.ts +199 -49
  337. package/src/thread-reducer.ts +135 -66
  338. package/src/tunnel.test.ts +81 -5
  339. package/src/tunnel.ts +124 -25
  340. package/src/voice-attachments.e2e.test.ts +110 -18
  341. package/src/voice-call.e2e.test.ts +9 -9
  342. package/src/voice-call.ts +6 -6
  343. package/src/voice-openai.e2e.test.ts +64 -24
  344. package/src/voice.ts +93 -26
  345. package/src/working-directory.e2e.test.ts +11 -11
  346. package/src/message-tool.e2e.test.ts +0 -45
package/src/main.ts CHANGED
@@ -8,20 +8,22 @@ import * as errore from 'errore'
8
8
 
9
9
  import { createAgentUi } from './agent-ui.ts'
10
10
  import type { Analytics } from './analytics.ts'
11
- import { oc, type Bot } from './bot.ts'
11
+ import { oc, type Bot, type WarmupTimers } from './bot.ts'
12
+ import type { PromptSection } from './builtin-skills.ts'
12
13
  import { openDb } from './db.ts'
13
14
  import { createEffectsRunner } from './effects.ts'
14
15
  import { DiscordError } from './errors.ts'
15
- import { createEventLoop } from './event-loop.ts'
16
+ import { createEventLoop, type RunEnded } from './event-loop.ts'
16
17
  import { registerIngress } from './ingress.ts'
17
18
  import { runLockRoute } from './lock-routes.ts'
18
19
  import { installShim, type LockServer } from './lock-server.ts'
19
20
  import { createLogger } from './logger.ts'
21
+ import { createCoordinatorNotes } from './coordinator.ts'
20
22
  import { installPluginShim, watchOpencode, type OpencodeEndpoint } from './opencode-server.ts'
21
23
  import { countUserProjects } from './project.ts'
22
24
  import { createScheduler, systemClock, type Clock } from './scheduler.ts'
23
25
  import { createEventRecorder, syncSessionTranscript } from './session-events.ts'
24
- import { createPluginWait, refreshCliContext } from './sessions.ts'
26
+ import { channelDefaults, createPluginWait, refreshCliContext } from './sessions.ts'
25
27
  import { createInteractionRegistry, registerSlashCommands } from './slash-commands.ts'
26
28
  import { createSleepLock } from './sleeps.ts'
27
29
  import { createBotStore } from './store.ts'
@@ -52,8 +54,9 @@ export type StartBotOptions = {
52
54
  // OpenCode's SQLite file (opencodeDbFile()): voice transcription reads the ChatGPT credential from it.
53
55
  // Required, so tests never read the developer's real credentials.
54
56
  opencodeDbFile: string
55
- // Shell command that runs this Kimaki install (given to agents), from kimakiShellCommand().
56
- kimakiCommand: string
57
+ // Command that runs this Kimaki install, for the <dataDir>/bin/kimaki shim agents call.
58
+ // No --data-dir: the shim sets KIMAKI_DATA_DIR, and not every command takes the flag.
59
+ kimakiArgv: readonly string[]
57
60
  // Product analytics sink (createAnalytics or disabledAnalytics).
58
61
  analytics: Analytics
59
62
  // Voice transcription API base URLs; tests point Gemini at a local fake.
@@ -68,8 +71,12 @@ export type StartBotOptions = {
68
71
  allowAllUsers?: boolean
69
72
  // Built-in skill ids denied in new sessions (builtin-skills.ts).
70
73
  disabledSkills?: readonly string[]
71
- // `kimaki --message-tool`: agents also reply through send_message; Discord hides their plain text.
72
- messageTool?: boolean
74
+ // Static instruction parts left out of new sessions (system-prompt.ts).
75
+ disabledPrompt?: readonly PromptSection[]
76
+ // `--coordinator`: coordinator mode for channels without a /toggle-coordinator choice (ingress.ts). Default: on.
77
+ coordinator?: boolean
78
+ // Default: the plugin's prompt cache warmup windows (plugin/coordinator.ts). Tests pass short ones.
79
+ warmupTimers?: WarmupTimers
73
80
  }
74
81
 
75
82
  export type BotHandle = Bot & {
@@ -140,6 +147,7 @@ export async function startBot(options: StartBotOptions): Promise<Error | BotHan
140
147
  const result = await oc({ opencode }, 'mark session viewed', (client) => client.session.view({ sessionID: sessionId, idle }))
141
148
  if (result instanceof Error) return result
142
149
  },
150
+ footerDefaults: (channelId) => channelDefaults({ db, opencode }, channelId),
143
151
  })
144
152
  cleanup.defer(() => effects.stop())
145
153
  const voiceCalls = createVoiceCalls({ opencodeConfigDir: options.opencodeConfigDir })
@@ -148,7 +156,12 @@ export async function startBot(options: StartBotOptions): Promise<Error | BotHan
148
156
  if (result instanceof Error) logger.warn(`transcript of ${sessionId} not synced`, result)
149
157
  })
150
158
  }
151
- const eventLoop = createEventLoop({ store, db, discord, effects, recorder, analytics: options.analytics, messageTool: options.messageTool ?? false, onRunEnded: voiceCalls.runEnded, onTranscriptDue })
159
+ const coordinatorNotes = createCoordinatorNotes()
160
+ const onRunEnded = (run: RunEnded) => {
161
+ voiceCalls.runEnded(run)
162
+ coordinatorNotes.runEnded(run)
163
+ }
164
+ const eventLoop = createEventLoop({ store, db, discord, effects, recorder, analytics: options.analytics, onRunEnded, onTranscriptDue })
152
165
  const loaded = await eventLoop.load()
153
166
  if (loaded instanceof Error) return loaded
154
167
  // Before the service is used, so a service started by ensure() loads it at once.
@@ -162,7 +175,7 @@ export async function startBot(options: StartBotOptions): Promise<Error | BotHan
162
175
  onDisconnect: eventLoop.onDisconnect,
163
176
  })
164
177
  cleanup.defer(() => opencode.stop())
165
- const shim = await installShim({ dataDir: options.dataDir, command: options.kimakiCommand })
178
+ const shim = await installShim({ dataDir: options.dataDir, argv: options.kimakiArgv })
166
179
  if (shim instanceof Error) return shim
167
180
  // The features that non-interaction code reaches through bot.features.
168
181
  const agentUi = createAgentUi({ store, eventLoop, opencode })
@@ -187,7 +200,9 @@ export async function startBot(options: StartBotOptions): Promise<Error | BotHan
187
200
  autoWorktrees: options.autoWorktrees ?? false,
188
201
  allowAllUsers: options.allowAllUsers ?? false,
189
202
  disabledSkills: options.disabledSkills ?? [],
190
- messageTool: options.messageTool ?? false,
203
+ disabledPrompt: options.disabledPrompt ?? [],
204
+ coordinatorDefault: options.coordinator ?? true,
205
+ warmupTimers: options.warmupTimers ?? null,
191
206
  features: { withSleepLock: createSleepLock(), waitForPlugin: createPluginWait({ opencode }), agentUi, voiceCalls },
192
207
  }
193
208
  const scheduler = createScheduler({
@@ -200,6 +215,7 @@ export async function startBot(options: StartBotOptions): Promise<Error | BotHan
200
215
  // Before login: gateway-proxy replays missed messages right after READY.
201
216
  registerIngress(bot)
202
217
  voiceCalls.register(bot)
218
+ coordinatorNotes.register(bot)
203
219
  // Before discord.destroy() (registered earlier, so disposed later): leaving voice needs the gateway.
204
220
  cleanup.defer(() => voiceCalls.stop(bot))
205
221
 
@@ -344,10 +344,10 @@ test('long paragraph splits by sentences, tight paragraph-list spacing is kept',
344
344
  const text = `Steps:\n- one\n- two\n\n${sentence.repeat(30)}`
345
345
  expect(renderMarkdown(text, { limit: 500 }).map((segment) => segment.kind === 'text' && segment.markdown.length)).toMatchInlineSnapshot(`
346
346
  [
347
- 499,
348
- 479,
349
- 479,
350
- 479,
347
+ 500,
348
+ 480,
349
+ 480,
350
+ 480,
351
351
  480,
352
352
  ]
353
353
  `)
@@ -412,3 +412,14 @@ test('edge cases: 2500-char code line, fence info line inside a fence, oversized
412
412
 
413
413
  expect(markdownPayloads('<callout accent="#ef4444"></callout>')).toMatchInlineSnapshot(`[]`)
414
414
  })
415
+
416
+ test('empty blockquote lines get a trailing space so Discord keeps the quote', () => {
417
+ const text = '**1. To a@b.com**\n\n> Hi Mohammad,\n>\n> Your subscription is cancelled.\n>\n> Tommy\n\nDone.'
418
+ const segments = renderMarkdown(text)
419
+ expect(segments.map((segment) => ('markdown' in segment ? JSON.stringify(segment.markdown.split('\n')) : segment.kind)))
420
+ .toMatchInlineSnapshot(`
421
+ [
422
+ "["**1. To a@b.com**","","> Hi Mohammad,","> ","> Your subscription is cancelled.","> ","> Tommy","","Done."]",
423
+ ]
424
+ `)
425
+ })
@@ -5,12 +5,13 @@
5
5
  // ├─ table ──▶ table segment (Components V2 later)
6
6
  // ├─ heading depth > 3 ──▶ clamped to ###
7
7
  // ├─ list with code items ──▶ code lifted after its item
8
+ // ├─ blockquote ──▶ bare ">" lines become "> "
8
9
  // └─ other nodes ──▶ original source slices, packed into ≤ limit
9
10
  //
10
11
  // Text is sliced from the original source by node offsets, never
11
12
  // re-serialized, so the model's markdown reaches Discord unchanged. Only
12
13
  // oversized nodes are split: code by lines (each piece a valid fence), lists
13
- // by items, paragraphs by sentences, then words.
14
+ // by items, paragraphs by lines, sentences, then words.
14
15
 
15
16
  import type { Code, List, ListItem, Root, RootContent, Table } from 'mdast'
16
17
  import { fromMarkdown } from 'mdast-util-from-markdown'
@@ -162,16 +163,17 @@ function codePieces(source: string, code: Code, limit: number): string[] {
162
163
  return codeBlocks({ fence, lang: code.lang ?? '', value: code.value, limit })
163
164
  }
164
165
 
165
- // Sentences first, then words, then a hard cut.
166
+ // Lines first, then sentences, then words, then a hard cut.
166
167
  function splitText(text: string, limit: number): string[] {
167
168
  if (text.length <= limit) return [text]
168
- const units = text.split(/(?<=[.!?])\s+/)
169
- const byWords = units.flatMap((unit) => (unit.length <= limit ? [unit] : unit.split(/(?<=\s)/)))
169
+ // Every unit keeps its trailing whitespace, so packing joins them as they were.
170
+ const units = text.split(/(?<=\n)/).flatMap((line) => (line.length <= limit ? [line] : line.split(/(?<=[.!?]\s+)(?=\S)/)))
171
+ const byWords = units.flatMap((unit) => (unit.length <= limit ? [unit] : unit.split(/(?<=\s)(?=\S)/)))
170
172
  const parts = byWords.flatMap((unit) => {
171
173
  if (unit.length <= limit) return [unit]
172
174
  return Array.from({ length: Math.ceil(unit.length / limit) }, (_, index) => unit.slice(index * limit, (index + 1) * limit))
173
175
  })
174
- return pack(parts, limit, ' ')
176
+ return pack(parts, limit, '')
175
177
  }
176
178
 
177
179
  function pack(parts: string[], limit: number, separator: string): string[] {
@@ -217,7 +219,8 @@ function nodePieces(source: string, node: RootContent, limit: number): string[]
217
219
  return unnestList(source, node, limit).flatMap((piece) => (piece.length <= limit ? [piece] : splitText(piece, limit)))
218
220
  }
219
221
  if (node.type === 'code') return codePieces(source, node, limit)
220
- const text = slice(source, node)
222
+ // Discord quotes need "> " plus a space; a bare ">" line shows as a literal ">".
223
+ const text = node.type === 'blockquote' ? slice(source, node).replace(/^( *>)$/gm, '$1 ') : slice(source, node)
221
224
  if (text.length <= limit) return [text]
222
225
  if (node.type === 'list') {
223
226
  const items = node.children.map((item) => slice(source, item))
@@ -161,15 +161,16 @@ test('kimaki --gateway without a TTY installs, onboards and reports ready on std
161
161
  `)
162
162
 
163
163
  const kimakiChannel = channels.find((channel) => channel.name === 'kimaki')!
164
- const thread = await twin.discord.channel(kimakiChannel.id).waitForThread({ timeout: 8_000 })
165
- await waitForFooter({ discord: twin.discord, threadId: thread.id })
166
- expect(await twin.discord.thread(thread.id).text()).toMatchInlineSnapshot(`
164
+ // Coordinator mode is on by default: onboarding answers in the channel.
165
+ await waitForFooter({ discord: twin.discord, threadId: kimakiChannel.id })
166
+ expect(await twin.discord.channel(kimakiChannel.id).text()).toMatchInlineSnapshot(`
167
167
  "--- from: assistant (TestBot)
168
- **Kimaki** lets you code from Discord. Each project channel is linked to a folder on your computer. A message there starts an AI coding session in that folder.
169
- Reply in the thread below to add channels for your projects. <@200000000000000001>
168
+ **Kimaki** lets you code from Discord. Each project channel is linked to a folder on your computer. Write in the channel: one long session answers there and starts threads for the work.
169
+ Reply here to add channels for your projects. <@200000000000000001>
170
170
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
171
+ -# Coordinator mode is on: this channel is one session that starts and manages project threads for you. Use /new-session for a thread without it. Turn it off with /toggle-coordinator or start kimaki with \`--coordinator false\`.
171
172
  Want channels for your projects?
172
- -# *kimaki ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
173
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
173
174
  `)
174
175
 
175
176
  // `kimaki restart`: the supervisor starts a new bot process, which reports ready again.
@@ -13,7 +13,8 @@ import { ChannelType } from 'discord.js'
13
13
  import { afterAll, beforeAll, expect, test } from 'vitest'
14
14
 
15
15
  import type { BotHandle } from './main.ts'
16
- import { kimakiShellCommand, runOnboarding } from './onboarding.ts'
16
+ import { shellQuote } from './lock-server.ts'
17
+ import { runOnboarding } from './onboarding.ts'
17
18
  import {
18
19
  TEST_USER_ID,
19
20
  scriptedTurn,
@@ -40,9 +41,7 @@ beforeAll(async () => {
40
41
  dataDir = tempDataDir()
41
42
  twin = await startTwin({ gateway: true })
42
43
  const require = createRequire(import.meta.url)
43
- kimaki = kimakiShellCommand({
44
- command: [process.execPath, require.resolve('tsx/cli'), path.join(import.meta.dirname, 'cli.ts')],
45
- })
44
+ kimaki = [process.execPath, require.resolve('tsx/cli'), path.join(import.meta.dirname, 'cli.ts')].map(shellQuote).join(' ')
46
45
  otherProject = path.join(dataDir, 'other-project')
47
46
  fs.mkdirSync(otherProject)
48
47
  execFileSync('git', ['init', '-q'], { cwd: otherProject })
@@ -90,7 +89,7 @@ async function guildChannels() {
90
89
  test('onboarding thread adds a project channel through the kimaki CLI, which answers at once', async () => {
91
90
  const { discord } = twin
92
91
  const guild = bot.discord.guilds.cache.get(discord.guildId)!
93
- const onboarded = await runOnboarding({ bot, dataDir, guild, kimaki, gateway: true, installerId: TEST_USER_ID, machine: 'test-machine' })
92
+ const onboarded = await runOnboarding({ bot, dataDir, guild, gateway: true, installerId: TEST_USER_ID, machine: 'test-machine' })
94
93
  if (!onboarded || onboarded instanceof Error) throw new Error(`onboarding failed: ${onboarded?.message}`, { cause: onboarded })
95
94
  await waitForFooter({ discord, threadId: onboarded.threadId })
96
95
 
@@ -108,14 +107,14 @@ test('onboarding thread adds a project channel through the kimaki CLI, which ans
108
107
  Reply in the thread below to add channels for your projects. <@200000000000000001>
109
108
  -# *using deterministic-provider/deterministic-v2 ⋅ build*
110
109
  Want channels for your projects? I can search for git repositories.
111
- -# *kimaki ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
110
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
112
111
  --- from: user (tommy)
113
112
  yes, add-other-project
114
113
  --- from: assistant (TestBot)
115
114
  -# ┣ shell _add project channel_
116
115
 
117
116
  Added the channel for other-project.
118
- -# *kimaki ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
117
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
119
118
  `)
120
119
  expect(await guildChannels()).toMatchInlineSnapshot(`
121
120
  [
@@ -143,7 +142,7 @@ test('onboarding thread adds a project channel through the kimaki CLI, which ans
143
142
  await waitForFooter({ discord, threadId: thread.id })
144
143
 
145
144
  // Second start: nothing is created again.
146
- expect(await runOnboarding({ bot, dataDir, guild, kimaki, gateway: true, machine: 'test-machine' })).toBe(null)
145
+ expect(await runOnboarding({ bot, dataDir, guild, gateway: true, machine: 'test-machine' })).toBe(null)
147
146
  await waitFor({ label: 'no extra channels', check: async () => (await guildChannels()).length === 6 })
148
147
 
149
148
  // An empty default channel means an earlier onboarding failed: retry in the same channel.
@@ -151,7 +150,7 @@ test('onboarding thread adds a project channel through the kimaki CLI, which ans
151
150
  const channel = await bot.discord.channels.fetch(defaultChannel.channel_id)
152
151
  if (channel?.type !== ChannelType.GuildText) throw new Error('default channel is not a text channel')
153
152
  for (const message of (await channel.messages.fetch()).values()) await message.delete()
154
- const retried = await runOnboarding({ bot, dataDir, guild, kimaki, gateway: true, installerId: TEST_USER_ID, machine: 'test-machine' })
153
+ const retried = await runOnboarding({ bot, dataDir, guild, gateway: true, installerId: TEST_USER_ID, machine: 'test-machine' })
155
154
  if (retried instanceof Error || !retried) throw new Error(`expected a retried onboarding, got ${retried}`)
156
155
  expect(retried.channelId).toBe(defaultChannel.channel_id)
157
156
  await waitForFooter({ discord, threadId: retried.threadId })
@@ -163,7 +162,6 @@ test('an upgraded setup with project mappings skips onboarding without selecting
163
162
  expect(await runOnboarding({
164
163
  bot,
165
164
  dataDir: path.join(dataDir, 'upgraded-layout'),
166
- kimaki,
167
165
  gateway: true,
168
166
  machine: 'test-machine',
169
167
  })).toBe(null)
package/src/onboarding.ts CHANGED
@@ -25,8 +25,8 @@ import { ConfigError, DbError, DiscordError, OpenCodeMissingError, type OpenCode
25
25
  import { NOTIFY_MESSAGE_FLAGS } from './format-parts.ts'
26
26
  import { createLogger } from './logger.ts'
27
27
  import { checkOpencode, findOpencodeBinary, installedOpencodeBinary, OPENCODE_INSTALL_COMMAND } from './opencode-server.ts'
28
- import type { Bot } from './bot.ts'
29
- import { startSession } from './sessions.ts'
28
+ import { isCoordinatorChannel, type Bot } from './bot.ts'
29
+ import { startCoordinator, startSession } from './sessions.ts'
30
30
  import { addProjectChannel, defaultChannelName, defaultProjectDirectory } from './project.ts'
31
31
 
32
32
  const logger = createLogger('ONBOARD')
@@ -130,11 +130,7 @@ export function startCaffeinate(): void {
130
130
  child.unref()
131
131
  }
132
132
 
133
- export function shellQuote(arg: string): string {
134
- return /^[\w@%+=:,./-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`
135
- }
136
-
137
- export function onboardingPrompt({ kimaki }: { kimaki: string }): string {
133
+ export function onboardingPrompt(): string {
138
134
  return dedent`
139
135
  This is the Kimaki onboarding thread. Kimaki maps Discord channels to project folders on this computer:
140
136
  a message in a project channel starts a coding session in that folder.
@@ -145,21 +141,15 @@ export function onboardingPrompt({ kimaki }: { kimaki: string }): string {
145
141
  searching for git repositories on this computer.
146
142
  2. If they agree, search. Only list folders that exist, most recently changed first, at most 15:
147
143
  \`find ~/Documents ~/Projects ~/projects ~/code ~/dev ~/src ~/repos ~/GitHub ~/github -maxdepth 3 -name .git -prune 2>/dev/null\`
148
- Skip folders that already have a channel: \`${kimaki} project list\`
144
+ Skip folders that already have a channel: \`kimaki project list\`
149
145
  3. Show the folders as a numbered list and ask the user to reply with the numbers to add.
150
- 4. Add each chosen folder with its absolute path: \`${kimaki} project add <absolute-directory>\`
146
+ 4. Add each chosen folder with its absolute path: \`kimaki project add <absolute-directory>\`
151
147
  The command prints the new channel as a mention like <#123>. Reply with the list of new channels.
152
148
 
153
149
  Never add a channel the user did not choose. Keep every message short.
154
150
  `
155
151
  }
156
152
 
157
- // The shell command the agent runs to call this Kimaki install.
158
- // No --data-dir: agent shells get KIMAKI_DATA_DIR from the plugin, and not every command takes the flag.
159
- export function kimakiShellCommand({ command }: { command: readonly string[] }): string {
160
- return command.map(shellQuote).join(' ')
161
- }
162
-
163
153
  class GitInitError extends errore.createTaggedError({
164
154
  name: 'GitInitError',
165
155
  message: 'Could not create $directory',
@@ -191,7 +181,6 @@ export async function runOnboarding({
191
181
  guild,
192
182
  guildId,
193
183
  installUrl,
194
- kimaki,
195
184
  gateway,
196
185
  installerId,
197
186
  machine,
@@ -201,8 +190,6 @@ export async function runOnboarding({
201
190
  guild?: Guild
202
191
  guildId?: string
203
192
  installUrl?: string
204
- // Shell command for `kimaki`, from kimakiShellCommand().
205
- kimaki: string
206
193
  gateway: boolean
207
194
  // --machine-name, else defaultMachineName().
208
195
  machine: string
@@ -266,11 +253,12 @@ export async function runOnboarding({
266
253
  .fetch(installerId ?? selectedGuild.ownerId)
267
254
  .catch((e) => new DiscordError({ operation: 'fetch installer', cause: e }))
268
255
  if (owner instanceof Error) return owner
256
+ const coordinator = isCoordinatorChannel(bot, channel.channelId)
269
257
  const welcome = await textChannel
270
258
  .send({
271
259
  content: dedent`
272
- **Kimaki** lets you code from Discord. Each project channel is linked to a folder on your computer. A message there starts an AI coding session in that folder.
273
- Reply in the thread below to add channels for your projects. <@${owner.id}>
260
+ **Kimaki** lets you code from Discord. Each project channel is linked to a folder on your computer. ${coordinator ? 'Write in the channel: one long session answers there and starts threads for the work.' : 'A message there starts an AI coding session in that folder.'}
261
+ Reply ${coordinator ? 'here' : 'in the thread below'} to add channels for your projects. <@${owner.id}>
274
262
  `,
275
263
  allowedMentions: { users: [owner.id] },
276
264
  flags: NOTIFY_MESSAGE_FLAGS,
@@ -278,15 +266,11 @@ export async function runOnboarding({
278
266
  .catch((e) => new DiscordError({ operation: 'send welcome message', cause: e }))
279
267
  if (welcome instanceof Error) return welcome
280
268
 
281
- const session = await startSession(bot, {
282
- channelId: channel.channelId,
283
- directory,
284
- route: { kind: 'steer', text: onboardingPrompt({ kimaki }) },
285
- author: { id: owner.id, username: owner.user.username },
286
- messageId: welcome.id,
287
- threadName: 'Kimaki onboarding',
288
- worktree: false,
289
- })
269
+ const author = { id: owner.id, username: owner.user.username }
270
+ const route = { kind: 'steer' as const, text: onboardingPrompt() }
271
+ const session = coordinator
272
+ ? await startCoordinator(bot, { channel: textChannel, route, author, messageId: welcome.id })
273
+ : await startSession(bot, { channelId: channel.channelId, directory, route, author, messageId: welcome.id, threadName: 'Kimaki onboarding', worktree: false })
290
274
  if (session instanceof Error) {
291
275
  // Leaves the channel empty, so the next start onboards again.
292
276
  await welcome.delete().catch((e) => logger.warn('delete welcome message', e))
@@ -132,7 +132,7 @@ test('Accept runs the command', async () => {
132
132
  ✓ _Accepted_
133
133
 
134
134
  accept turn done
135
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
135
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
136
136
  `)
137
137
  })
138
138
 
@@ -163,7 +163,7 @@ test('Deny stops the run, the next message is answered', async () => {
163
163
  Go on after-deny
164
164
  --- from: assistant (TestBot)
165
165
  continued after deny
166
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
166
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
167
167
  `)
168
168
  })
169
169
 
@@ -186,6 +186,6 @@ test('a subagent permission shows in the parent thread', async () => {
186
186
  ✓ _Accepted always_
187
187
 
188
188
  parent done
189
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
189
+ -# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
190
190
  `)
191
191
  })