kimaki 0.26.0 → 0.28.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 (302) hide show
  1. package/dist/agent-model.e2e.test.js +250 -164
  2. package/dist/anthropic-account-identity.js +9 -1
  3. package/dist/anthropic-auth-plugin.js +10 -13
  4. package/dist/anthropic-auth-plugin.test.js +17 -0
  5. package/dist/anthropic-auth-state.js +1 -0
  6. package/dist/bash-tool-schema-plugin.js +32 -0
  7. package/dist/bash-tool-schema-plugin.test.js +49 -0
  8. package/dist/bin.js +73 -19
  9. package/dist/cli-commands/maintenance.js +46 -7
  10. package/dist/cli-commands/project.js +11 -27
  11. package/dist/cli-commands/send.js +30 -1
  12. package/dist/cli-commands/session.js +322 -90
  13. package/dist/cli-commands/task.js +34 -15
  14. package/dist/cli-parsing.test.js +83 -4
  15. package/dist/cli-runner.js +44 -8
  16. package/dist/cli-runner.test.js +27 -0
  17. package/dist/cli-send-thread.e2e.test.js +9 -9
  18. package/dist/cli.js +32 -0
  19. package/dist/commands/abort.js +5 -1
  20. package/dist/commands/action-buttons.js +2 -1
  21. package/dist/commands/add-project.js +4 -7
  22. package/dist/commands/agent.js +164 -32
  23. package/dist/commands/ask-question.js +23 -33
  24. package/dist/commands/ask-question.test.js +43 -1
  25. package/dist/commands/btw.js +28 -6
  26. package/dist/commands/file-upload.js +2 -1
  27. package/dist/commands/fork-subagent.js +2 -4
  28. package/dist/commands/fork.js +4 -4
  29. package/dist/commands/last-sessions.js +17 -10
  30. package/dist/commands/login.js +6 -0
  31. package/dist/commands/merge-worktree.js +47 -14
  32. package/dist/commands/model-variant.js +8 -1
  33. package/dist/commands/model.js +16 -9
  34. package/dist/commands/multioauth.js +1 -0
  35. package/dist/commands/new-worktree.js +170 -102
  36. package/dist/commands/queue.js +3 -2
  37. package/dist/commands/resume.js +4 -4
  38. package/dist/commands/session.js +78 -63
  39. package/dist/commands/user-command.js +6 -8
  40. package/dist/commands/worktrees.js +5 -13
  41. package/dist/config.js +16 -1
  42. package/dist/cpu-profiler.js +211 -0
  43. package/dist/cpu-profiler.test.js +160 -0
  44. package/dist/database.js +152 -3
  45. package/dist/db.js +33 -0
  46. package/dist/db.test.js +267 -1
  47. package/dist/default-channel-provisioning.test.js +148 -0
  48. package/dist/discord-bot.js +517 -488
  49. package/dist/discord-command-registration.js +25 -10
  50. package/dist/discord-command-registration.test.js +17 -0
  51. package/dist/discord-utils.js +97 -5
  52. package/dist/discord-utils.test.js +151 -1
  53. package/dist/errors.js +10 -0
  54. package/dist/exec-async.js +17 -5
  55. package/dist/external-directory-permissions.e2e.test.js +11 -7
  56. package/dist/external-directory-project-deny.e2e.test.js +5 -3
  57. package/dist/external-opencode-sync.js +94 -17
  58. package/dist/external-opencode-sync.test.js +206 -0
  59. package/dist/extract-sdk-error-message.test.js +13 -0
  60. package/dist/file-edit-log.js +197 -0
  61. package/dist/file-edit-log.test.js +258 -0
  62. package/dist/format-tables.js +52 -0
  63. package/dist/format-tables.test.js +93 -0
  64. package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
  65. package/dist/gateway-proxy.e2e.test.js +18 -18
  66. package/dist/git-worktree-core.js +182 -17
  67. package/dist/interaction-handler.js +416 -398
  68. package/dist/ipc-tools-plugin.js +68 -3
  69. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
  70. package/dist/kimaki-opencode-plugin.js +9 -3
  71. package/dist/kimaki-workspace-adaptor.js +33 -10
  72. package/dist/logger.js +11 -1
  73. package/dist/markdown.js +3 -1
  74. package/dist/markdown.test.js +11 -3
  75. package/dist/message-formatting.js +181 -51
  76. package/dist/message-formatting.test.js +529 -22
  77. package/dist/message-preprocessing.js +105 -1
  78. package/dist/oauth-rotation-shared.js +10 -0
  79. package/dist/openai-auth-plugin.js +1 -0
  80. package/dist/openai-auth-state.js +1 -0
  81. package/dist/opencode-command.js +28 -0
  82. package/dist/opencode-command.test.js +109 -1
  83. package/dist/opencode-interrupt-plugin.js +4 -0
  84. package/dist/opencode-interrupt-plugin.test.js +24 -0
  85. package/dist/opencode.js +82 -15
  86. package/dist/project-add.test.js +62 -0
  87. package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
  88. package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
  89. package/dist/question-text-before-dropdown.e2e.test.js +52 -0
  90. package/dist/queue-advanced-abort.e2e.test.js +8 -10
  91. package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
  92. package/dist/queue-advanced-e2e-setup.js +78 -5
  93. package/dist/queue-advanced-footer.e2e.test.js +60 -269
  94. package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
  95. package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
  96. package/dist/queue-advanced-question.e2e.test.js +32 -27
  97. package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
  98. package/dist/queue-advanced-typing.e2e.test.js +7 -9
  99. package/dist/queue-delete-message.e2e.test.js +3 -3
  100. package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
  101. package/dist/queue-interrupt-drain.e2e.test.js +122 -12
  102. package/dist/queue-question-select-drain.e2e.test.js +34 -132
  103. package/dist/runtime-lifecycle.e2e.test.js +21 -51
  104. package/dist/schema.js +35 -1
  105. package/dist/session-handler/event-stream-state.js +391 -50
  106. package/dist/session-handler/event-stream-state.test.js +736 -8
  107. package/dist/session-handler/model-utils.js +172 -5
  108. package/dist/session-handler/model-utils.test.js +445 -0
  109. package/dist/session-handler/opencode-session-event-log.js +30 -1
  110. package/dist/session-handler/thread-ingress.test.js +41 -0
  111. package/dist/session-handler/thread-runtime-state.js +2 -0
  112. package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
  113. package/dist/session-handler/thread-session-runtime.js +574 -299
  114. package/dist/session-search.js +123 -4
  115. package/dist/session-search.test.js +248 -1
  116. package/dist/session-sleep.test.js +224 -0
  117. package/dist/session-title-rename.test.js +37 -39
  118. package/dist/sleep.e2e.test.js +494 -0
  119. package/dist/startup-time.e2e.test.js +2 -1
  120. package/dist/store.js +3 -0
  121. package/dist/system-message.js +102 -16
  122. package/dist/system-message.test.js +192 -15
  123. package/dist/task-runner.js +96 -5
  124. package/dist/task-schedule.js +87 -1
  125. package/dist/task-schedule.test.js +149 -1
  126. package/dist/test-utils.js +17 -9
  127. package/dist/thinking-utils.js +10 -0
  128. package/dist/thinking-utils.test.js +35 -0
  129. package/dist/thread-message-queue.e2e.test.js +65 -213
  130. package/dist/undo-redo.e2e.test.js +17 -9
  131. package/dist/voice-attachment.js +23 -1
  132. package/dist/voice-handler.js +21 -4
  133. package/dist/voice-message.e2e.test.js +144 -30
  134. package/dist/voice-routing-input.e2e.test.js +195 -0
  135. package/dist/voice-routing-question.e2e.test.js +125 -0
  136. package/dist/voice.js +28 -8
  137. package/dist/voice.test.js +96 -30
  138. package/dist/worktree-clone-isolation.e2e.test.js +188 -0
  139. package/dist/worktree-lifecycle.e2e.test.js +55 -36
  140. package/dist/worktrees.js +226 -443
  141. package/dist/worktrees.test.js +435 -58
  142. package/dist/xai-auth-plugin.js +1 -0
  143. package/dist/xai-auth-state.js +1 -0
  144. package/package.json +8 -6
  145. package/skills/bump-holocron-consumers/SKILL.md +137 -0
  146. package/skills/critique/SKILL.md +4 -4
  147. package/skills/errore/SKILL.md +60 -218
  148. package/skills/goke/SKILL.md +150 -10
  149. package/skills/holocron/SKILL.md +154 -17
  150. package/skills/npm-package/SKILL.md +55 -35
  151. package/skills/profano/SKILL.md +17 -2
  152. package/skills/spiceflow/SKILL.md +39 -3
  153. package/skills/strada/SKILL.md +39 -0
  154. package/skills/zele/SKILL.md +19 -0
  155. package/src/agent-model.e2e.test.ts +287 -186
  156. package/src/anthropic-account-identity.ts +20 -1
  157. package/src/anthropic-auth-plugin.test.ts +19 -0
  158. package/src/anthropic-auth-plugin.ts +12 -16
  159. package/src/anthropic-auth-state.ts +1 -0
  160. package/src/bash-tool-schema-plugin.test.ts +55 -0
  161. package/src/bash-tool-schema-plugin.ts +46 -0
  162. package/src/bin.ts +78 -26
  163. package/src/cli-commands/maintenance.ts +59 -13
  164. package/src/cli-commands/project.ts +14 -35
  165. package/src/cli-commands/send.ts +31 -1
  166. package/src/cli-commands/session.ts +374 -114
  167. package/src/cli-commands/task.ts +34 -15
  168. package/src/cli-parsing.test.ts +103 -4
  169. package/src/cli-runner.test.ts +49 -0
  170. package/src/cli-runner.ts +44 -7
  171. package/src/cli-send-thread.e2e.test.ts +9 -11
  172. package/src/cli.ts +53 -0
  173. package/src/commands/abort.ts +6 -1
  174. package/src/commands/action-buttons.ts +2 -1
  175. package/src/commands/add-project.ts +4 -8
  176. package/src/commands/agent.ts +205 -29
  177. package/src/commands/ask-question.test.ts +48 -1
  178. package/src/commands/ask-question.ts +23 -33
  179. package/src/commands/btw.ts +40 -5
  180. package/src/commands/file-upload.ts +2 -1
  181. package/src/commands/fork-subagent.ts +2 -3
  182. package/src/commands/fork.ts +4 -3
  183. package/src/commands/last-sessions.ts +10 -4
  184. package/src/commands/login.ts +6 -0
  185. package/src/commands/merge-worktree.ts +54 -16
  186. package/src/commands/model-variant.ts +9 -1
  187. package/src/commands/model.ts +20 -9
  188. package/src/commands/multioauth.ts +1 -0
  189. package/src/commands/new-worktree.ts +138 -55
  190. package/src/commands/queue.ts +3 -2
  191. package/src/commands/resume.ts +4 -3
  192. package/src/commands/session.ts +94 -70
  193. package/src/commands/user-command.ts +6 -6
  194. package/src/commands/worktrees.ts +5 -12
  195. package/src/config.ts +18 -1
  196. package/src/cpu-profiler.test.ts +183 -0
  197. package/src/cpu-profiler.ts +293 -0
  198. package/src/database.ts +235 -3
  199. package/src/db.test.ts +293 -0
  200. package/src/db.ts +38 -0
  201. package/src/default-channel-provisioning.test.ts +155 -0
  202. package/src/discord-bot.ts +78 -36
  203. package/src/discord-command-registration.test.ts +20 -0
  204. package/src/discord-command-registration.ts +42 -12
  205. package/src/discord-utils.test.ts +176 -0
  206. package/src/discord-utils.ts +152 -7
  207. package/src/errors.ts +12 -0
  208. package/src/exec-async.ts +20 -11
  209. package/src/external-directory-permissions.e2e.test.ts +11 -7
  210. package/src/external-directory-project-deny.e2e.test.ts +5 -3
  211. package/src/external-opencode-sync.test.ts +248 -0
  212. package/src/external-opencode-sync.ts +122 -19
  213. package/src/extract-sdk-error-message.test.ts +18 -0
  214. package/src/file-edit-log.test.ts +287 -0
  215. package/src/file-edit-log.ts +283 -0
  216. package/src/format-tables.test.ts +101 -0
  217. package/src/format-tables.ts +75 -0
  218. package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
  219. package/src/gateway-proxy.e2e.test.ts +18 -17
  220. package/src/git-worktree-core.ts +247 -20
  221. package/src/interaction-handler.ts +36 -13
  222. package/src/ipc-tools-plugin.ts +82 -5
  223. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
  224. package/src/kimaki-opencode-plugin.ts +9 -3
  225. package/src/kimaki-workspace-adaptor.ts +40 -11
  226. package/src/logger.ts +14 -4
  227. package/src/markdown.test.ts +13 -3
  228. package/src/markdown.ts +4 -1
  229. package/src/message-formatting.test.ts +672 -25
  230. package/src/message-formatting.ts +242 -53
  231. package/src/message-preprocessing.ts +116 -1
  232. package/src/oauth-rotation-shared.ts +10 -0
  233. package/src/openai-auth-plugin.ts +1 -0
  234. package/src/openai-auth-state.ts +1 -0
  235. package/src/opencode-command.test.ts +140 -0
  236. package/src/opencode-command.ts +38 -0
  237. package/src/opencode-interrupt-plugin.test.ts +32 -0
  238. package/src/opencode-interrupt-plugin.ts +3 -0
  239. package/src/opencode.ts +115 -15
  240. package/src/project-add.test.ts +80 -0
  241. package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
  242. package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
  243. package/src/question-text-before-dropdown.e2e.test.ts +67 -0
  244. package/src/queue-advanced-abort.e2e.test.ts +8 -9
  245. package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
  246. package/src/queue-advanced-e2e-setup.ts +83 -4
  247. package/src/queue-advanced-footer.e2e.test.ts +67 -315
  248. package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
  249. package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
  250. package/src/queue-advanced-question.e2e.test.ts +36 -30
  251. package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
  252. package/src/queue-advanced-typing.e2e.test.ts +8 -8
  253. package/src/queue-delete-message.e2e.test.ts +3 -3
  254. package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
  255. package/src/queue-interrupt-drain.e2e.test.ts +133 -12
  256. package/src/queue-question-select-drain.e2e.test.ts +36 -161
  257. package/src/runtime-lifecycle.e2e.test.ts +21 -54
  258. package/src/schema.sql +15 -2
  259. package/src/schema.ts +37 -1
  260. package/src/session-handler/event-stream-state.test.ts +790 -7
  261. package/src/session-handler/event-stream-state.ts +568 -53
  262. package/src/session-handler/model-utils.test.ts +537 -0
  263. package/src/session-handler/model-utils.ts +281 -5
  264. package/src/session-handler/opencode-session-event-log.ts +31 -1
  265. package/src/session-handler/thread-ingress.test.ts +54 -0
  266. package/src/session-handler/thread-runtime-state.ts +3 -0
  267. package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
  268. package/src/session-handler/thread-session-runtime.ts +712 -358
  269. package/src/session-search.test.ts +281 -0
  270. package/src/session-search.ts +241 -7
  271. package/src/session-sleep.test.ts +294 -0
  272. package/src/session-title-rename.test.ts +54 -48
  273. package/src/sleep.e2e.test.ts +565 -0
  274. package/src/startup-time.e2e.test.ts +5 -1
  275. package/src/store.ts +22 -0
  276. package/src/system-message.test.ts +226 -15
  277. package/src/system-message.ts +130 -16
  278. package/src/task-runner.ts +138 -3
  279. package/src/task-schedule.test.ts +170 -0
  280. package/src/task-schedule.ts +141 -1
  281. package/src/test-utils.ts +21 -10
  282. package/src/thinking-utils.test.ts +46 -0
  283. package/src/thinking-utils.ts +21 -0
  284. package/src/thread-message-queue.e2e.test.ts +65 -245
  285. package/src/undo-redo.e2e.test.ts +17 -9
  286. package/src/voice-attachment.ts +29 -1
  287. package/src/voice-handler.ts +23 -4
  288. package/src/voice-message.e2e.test.ts +149 -29
  289. package/src/voice-routing-input.e2e.test.ts +203 -0
  290. package/src/voice-routing-question.e2e.test.ts +131 -0
  291. package/src/voice.test.ts +107 -29
  292. package/src/voice.ts +34 -6
  293. package/src/worktree-clone-isolation.e2e.test.ts +213 -0
  294. package/src/worktree-lifecycle.e2e.test.ts +55 -35
  295. package/src/worktrees.test.ts +505 -64
  296. package/src/worktrees.ts +257 -603
  297. package/src/xai-auth-plugin.ts +1 -0
  298. package/src/xai-auth-state.ts +1 -0
  299. package/dist/message-finish-field.e2e.test.js +0 -169
  300. package/dist/subagent-rate-limit-plugin.js +0 -184
  301. package/src/message-finish-field.e2e.test.ts +0 -196
  302. package/src/subagent-rate-limit-plugin.ts +0 -228
@@ -16,8 +16,12 @@ import { closeDb } from './db.js'
16
16
  import { runTaskCommand } from './task-runner.js'
17
17
  import {
18
18
  appendTaskCommandOutput,
19
+ applyScheduledTaskUserEdit,
19
20
  parseScheduledTaskPayload,
21
+ formatSessionSleepToolOutput,
22
+ formatSessionSleepWakePrompt,
20
23
  parseSendAtValue,
24
+ parseSleepWakeAt,
21
25
  } from './task-schedule.js'
22
26
 
23
27
  describe('parseSendAtValue', () => {
@@ -100,6 +104,117 @@ describe('parseSendAtValue', () => {
100
104
  })
101
105
  })
102
106
 
107
+ describe('parseSleepWakeAt', () => {
108
+ const now = new Date('2026-08-19T12:00:00Z')
109
+
110
+ test('adds 2h to now', () => {
111
+ const result = parseSleepWakeAt({ duration: '2h', now })
112
+ expect(result).not.toBeInstanceOf(Error)
113
+ if (result instanceof Error) throw result
114
+ expect(result.toISOString()).toBe('2026-08-19T14:00:00.000Z')
115
+ })
116
+
117
+ test('adds 30s and 1d', () => {
118
+ const thirtySeconds = parseSleepWakeAt({ duration: '30s', now })
119
+ expect(thirtySeconds).not.toBeInstanceOf(Error)
120
+ if (thirtySeconds instanceof Error) throw thirtySeconds
121
+ expect(thirtySeconds.toISOString()).toBe('2026-08-19T12:00:30.000Z')
122
+
123
+ const oneDay = parseSleepWakeAt({ duration: '1d', now })
124
+ expect(oneDay).not.toBeInstanceOf(Error)
125
+ if (oneDay instanceof Error) throw oneDay
126
+ expect(oneDay.toISOString()).toBe('2026-08-20T12:00:00.000Z')
127
+ })
128
+
129
+ test('accepts until UTC ISO date ending with Z', () => {
130
+ const result = parseSleepWakeAt({
131
+ until: '2026-08-20T09:00:00Z',
132
+ now,
133
+ })
134
+ expect(result).not.toBeInstanceOf(Error)
135
+ if (result instanceof Error) throw result
136
+ expect(result.toISOString()).toBe('2026-08-20T09:00:00.000Z')
137
+ })
138
+
139
+ test('rejects past until', () => {
140
+ const result = parseSleepWakeAt({
141
+ until: '2026-08-19T11:59:59Z',
142
+ now,
143
+ })
144
+ expect(result).toBeInstanceOf(Error)
145
+ if (result instanceof Error) {
146
+ expect(result.message).toContain('until must be in the future (UTC)')
147
+ }
148
+ })
149
+
150
+ test('rejects both duration and until', () => {
151
+ const result = parseSleepWakeAt({
152
+ duration: '1h',
153
+ until: '2026-08-20T09:00:00Z',
154
+ now,
155
+ })
156
+ expect(result).toBeInstanceOf(Error)
157
+ if (result instanceof Error) {
158
+ expect(result.message).toContain('either duration or until')
159
+ }
160
+ })
161
+
162
+ test('rejects neither duration nor until', () => {
163
+ const result = parseSleepWakeAt({ now })
164
+ expect(result).toBeInstanceOf(Error)
165
+ if (result instanceof Error) {
166
+ expect(result.message).toContain('duration or until')
167
+ }
168
+ })
169
+
170
+ test('rejects until with a non-UTC offset', () => {
171
+ const result = parseSleepWakeAt({
172
+ until: '2026-08-20T09:00:00+01:00',
173
+ now,
174
+ })
175
+ expect(result).toBeInstanceOf(Error)
176
+ if (result instanceof Error) {
177
+ expect(result.message).toContain('UTC ISO format ending with Z')
178
+ }
179
+ })
180
+ })
181
+
182
+ describe('formatSessionSleepToolOutput', () => {
183
+ const wakeAt = new Date('2026-09-07T13:21:00Z')
184
+
185
+ test('starts with Sleeping until and tells the model to wait', () => {
186
+ const output = formatSessionSleepToolOutput({
187
+ wakeAt,
188
+ reason: 'Wait 1 day, then check mail',
189
+ })
190
+
191
+ expect(output).toBe(
192
+ 'Sleeping until 2026-09-07 13:21 UTC. Reason: Wait 1 day, then check mail. This tool result is not a wake. Do not continue the waited work. Do not call more tools. Reply with one short line that you are waiting until that time, then stop. The real wake is a later Discord message that starts with "Woke after sleeping until". Only then continue the wait reason. A new user message in this thread cancels the sleep. If you still need that later wake after answering, call kimaki_sleep again with until set to the same UTC time.',
193
+ )
194
+ expect(output.startsWith('Sleeping until ')).toBe(true)
195
+ expect(output).toContain('This tool result is not a wake')
196
+ expect(output).not.toContain('You will be woken')
197
+ expect(output).not.toContain('Stop now')
198
+ })
199
+
200
+ test('omits reason when none is passed', () => {
201
+ const output = formatSessionSleepToolOutput({ wakeAt })
202
+ expect(output.startsWith('Sleeping until 2026-09-07 13:21 UTC.')).toBe(true)
203
+ expect(output).not.toContain('Reason:')
204
+ })
205
+
206
+ test('wake prompt stays a separate later message', () => {
207
+ expect(
208
+ formatSessionSleepWakePrompt({
209
+ wakeAt,
210
+ reason: 'Wait 1 day, then check mail',
211
+ }),
212
+ ).toBe(
213
+ '⬦ Woke after sleeping until 2026-09-07 13:21 UTC\nReason: Wait 1 day, then check mail\nContinue the work you were waiting for.',
214
+ )
215
+ })
216
+ })
217
+
103
218
  describe('scheduled task execution options', () => {
104
219
  const tempDirectories: string[] = []
105
220
 
@@ -134,6 +249,61 @@ describe('scheduled task execution options', () => {
134
249
  `)
135
250
  })
136
251
 
252
+ test('clears the stored user when task edit passes an empty --user', () => {
253
+ const payload = parseScheduledTaskPayload(JSON.stringify({
254
+ kind: 'thread',
255
+ threadId: 'thread-1',
256
+ prompt: 'Handle support requests',
257
+ userId: '535922349652836367',
258
+ username: 'Tommy',
259
+ }))
260
+ expect(payload).not.toBeInstanceOf(Error)
261
+ if (payload instanceof Error) throw payload
262
+
263
+ expect(applyScheduledTaskUserEdit({
264
+ payload,
265
+ userOption: '',
266
+ })).toMatchInlineSnapshot(`
267
+ {
268
+ "agent": null,
269
+ "allowConcurrency": false,
270
+ "injectionGuardPatterns": null,
271
+ "kind": "thread",
272
+ "model": null,
273
+ "parentSessionId": null,
274
+ "permissions": null,
275
+ "preRunCommand": null,
276
+ "prompt": "Handle support requests",
277
+ "threadId": "thread-1",
278
+ "userId": null,
279
+ "username": null,
280
+ }
281
+ `)
282
+ expect(applyScheduledTaskUserEdit({
283
+ payload,
284
+ userOption: ' ',
285
+ }).userId).toBeNull()
286
+ })
287
+
288
+ test('sets the stored user when task edit passes a resolved --user', () => {
289
+ const payload = parseScheduledTaskPayload(JSON.stringify({
290
+ kind: 'channel',
291
+ channelId: 'chan-1',
292
+ prompt: 'Daily digest',
293
+ }))
294
+ expect(payload).not.toBeInstanceOf(Error)
295
+ if (payload instanceof Error) throw payload
296
+
297
+ expect(applyScheduledTaskUserEdit({
298
+ payload,
299
+ userOption: '535922349652836367',
300
+ resolvedUser: { id: '535922349652836367', username: 'Tommy' },
301
+ })).toMatchObject({
302
+ userId: '535922349652836367',
303
+ username: 'Tommy',
304
+ })
305
+ })
306
+
137
307
  test('adds command stdout to the prompt', () => {
138
308
  expect(appendTaskCommandOutput({
139
309
  prompt: 'Handle support requests',
@@ -1,7 +1,8 @@
1
- // Scheduled task parsing utilities for `send --send-at` and task runner execution.
1
+ // Scheduled task parsing for `send --send-at`, plus kimaki_sleep wake/result text.
2
2
 
3
3
  import { CronExpressionParser } from 'cron-parser'
4
4
  import * as errore from 'errore'
5
+ import { STATUS_PREFIX } from './message-formatting.js'
5
6
 
6
7
  export type ScheduledTaskPayload = {
7
8
  preRunCommand: string | null
@@ -101,6 +102,121 @@ function parseUtcSendAtDate({
101
102
  return runAt
102
103
  }
103
104
 
105
+ const SLEEP_DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i
106
+
107
+ const SLEEP_DURATION_MS = {
108
+ ms: 1,
109
+ s: 1000,
110
+ m: 60_000,
111
+ h: 3_600_000,
112
+ d: 86_400_000,
113
+ } as const
114
+
115
+ function parseUtcFutureDate({
116
+ value,
117
+ now,
118
+ field,
119
+ }: {
120
+ value: string
121
+ now: Date
122
+ field: string
123
+ }): Date | Error {
124
+ if (!UTC_SEND_AT_DATE_REGEX.test(value)) {
125
+ return new Error(
126
+ `${field} must be UTC ISO format ending with Z (example: 2026-08-20T09:00:00Z). Received: ${value}`,
127
+ )
128
+ }
129
+
130
+ const parsed = new Date(value)
131
+ if (Number.isNaN(parsed.getTime())) {
132
+ return new Error(`Invalid UTC date for ${field}: ${value}`)
133
+ }
134
+
135
+ if (parsed.getTime() <= now.getTime()) {
136
+ return new Error(`${field} must be in the future (UTC): ${value}`)
137
+ }
138
+
139
+ return parsed
140
+ }
141
+
142
+ export function parseSleepWakeAt({
143
+ duration,
144
+ until,
145
+ now,
146
+ }: {
147
+ duration?: string
148
+ until?: string
149
+ now: Date
150
+ }): Date | Error {
151
+ const trimmedDuration = duration?.trim() || ''
152
+ const trimmedUntil = until?.trim() || ''
153
+ if (trimmedDuration && trimmedUntil) {
154
+ return new Error('Pass either duration or until, not both')
155
+ }
156
+ if (!trimmedDuration && !trimmedUntil) {
157
+ return new Error('Pass duration or until')
158
+ }
159
+
160
+ if (trimmedUntil) {
161
+ return parseUtcFutureDate({
162
+ value: trimmedUntil,
163
+ now,
164
+ field: 'until',
165
+ })
166
+ }
167
+
168
+ const match = SLEEP_DURATION_REGEX.exec(trimmedDuration)
169
+ if (!match) {
170
+ return new Error(
171
+ `Invalid duration: "${trimmedDuration}". Use a number plus ms, s, m, h, or d (example: 2h).`,
172
+ )
173
+ }
174
+
175
+ const amount = Number(match[1])
176
+ const unit = match[2]!.toLowerCase() as keyof typeof SLEEP_DURATION_MS
177
+ if (!Number.isFinite(amount) || amount <= 0) {
178
+ return new Error('duration must be greater than 0')
179
+ }
180
+
181
+ return new Date(now.getTime() + amount * SLEEP_DURATION_MS[unit])
182
+ }
183
+
184
+ export function formatSessionSleepWakeAt(wakeAt: Date): string {
185
+ return `${wakeAt.toISOString().slice(0, 16).replace('T', ' ')} UTC`
186
+ }
187
+
188
+ export function formatSessionSleepWakePrompt({
189
+ wakeAt,
190
+ reason,
191
+ }: {
192
+ wakeAt: Date
193
+ reason: string | null
194
+ }): string {
195
+ const until = formatSessionSleepWakeAt(wakeAt)
196
+ const reasonLine = reason?.trim() ? `\nReason: ${reason.trim()}` : ''
197
+ return `${STATUS_PREFIX}Woke after sleeping until ${until}${reasonLine}\nContinue the work you were waiting for.`
198
+ }
199
+
200
+ // Tool result after persist. Keep "Sleeping until" first so e2e matchers still hit.
201
+ // Do not say "woken" or "Woke after": models treat that as the wake turn.
202
+ export function formatSessionSleepToolOutput({
203
+ wakeAt,
204
+ reason,
205
+ }: {
206
+ wakeAt: Date
207
+ reason?: string
208
+ }): string {
209
+ const until = formatSessionSleepWakeAt(wakeAt)
210
+ const reasonText = reason?.trim() ? ` Reason: ${reason.trim()}.` : ''
211
+ return [
212
+ `Sleeping until ${until}.${reasonText}`,
213
+ 'This tool result is not a wake. Do not continue the waited work. Do not call more tools.',
214
+ 'Reply with one short line that you are waiting until that time, then stop.',
215
+ 'The real wake is a later Discord message that starts with "Woke after sleeping until". Only then continue the wait reason.',
216
+ 'A new user message in this thread cancels the sleep. If you still need that later wake after answering, call kimaki_sleep again with until set to the same UTC time.',
217
+ ].join(' ')
218
+ }
219
+
104
220
  export function parseSendAtValue({
105
221
  value,
106
222
  now,
@@ -208,6 +324,30 @@ export function serializeScheduledTaskPayload(
208
324
  return JSON.stringify(payload)
209
325
  }
210
326
 
327
+ export function applyScheduledTaskUserEdit({
328
+ payload,
329
+ userOption,
330
+ resolvedUser,
331
+ }: {
332
+ payload: ScheduledTaskPayload
333
+ userOption: string
334
+ resolvedUser?: { id: string; username?: string } | null
335
+ }): ScheduledTaskPayload {
336
+ if (!userOption.trim()) {
337
+ return {
338
+ ...payload,
339
+ userId: null,
340
+ username: null,
341
+ }
342
+ }
343
+ if (!resolvedUser) return payload
344
+ return {
345
+ ...payload,
346
+ userId: resolvedUser.id,
347
+ username: resolvedUser.username || null,
348
+ }
349
+ }
350
+
211
351
  export function appendTaskCommandOutput({
212
352
  prompt,
213
353
  stdout,
package/src/test-utils.ts CHANGED
@@ -73,7 +73,12 @@ export function initTestGitRepo(directory: string): void {
73
73
  execSync('git commit --allow-empty -m "init"', { cwd: directory, stdio: 'pipe' })
74
74
  }
75
75
 
76
- import type { DigitalDiscord } from 'discord-digital-twin/src'
76
+ import {
77
+ getMessageVisibleText,
78
+ type DigitalDiscord,
79
+ } from 'discord-digital-twin/src'
80
+
81
+ export { getMessageVisibleText }
77
82
  import {
78
83
  getOpencodeClient,
79
84
  initializeOpencodeForDirectory,
@@ -210,7 +215,7 @@ export async function waitForBotReplyAfterUserMessage({
210
215
  const userMessageIndex = messages.findIndex((message) => {
211
216
  return (
212
217
  message.author.id === userId &&
213
- message.content.includes(userMessageIncludes)
218
+ getMessageVisibleText(message).includes(userMessageIncludes)
214
219
  )
215
220
  })
216
221
  const botReplyIndex = messages.findIndex((message, index) => {
@@ -265,7 +270,7 @@ export async function waitForBotMessageContaining({
265
270
  return messages.findLastIndex((message) => {
266
271
  return (
267
272
  message.author.id === userId &&
268
- message.content.includes(afterUserMessageIncludes)
273
+ getMessageVisibleText(message).includes(afterUserMessageIncludes)
269
274
  )
270
275
  })
271
276
  }
@@ -285,7 +290,7 @@ export async function waitForBotMessageContaining({
285
290
  }
286
291
  return (
287
292
  message.author.id === discord.botUserId &&
288
- message.content.includes(text)
293
+ getMessageVisibleText(message).includes(text)
289
294
  )
290
295
  })
291
296
  if (match) {
@@ -299,7 +304,7 @@ export async function waitForBotMessageContaining({
299
304
  .slice(-12)
300
305
  .map((message) => {
301
306
  const role = message.author.id === discord.botUserId ? 'bot' : 'user'
302
- return `${role}: ${message.content.slice(0, 120)}`
307
+ return `${role}: ${getMessageVisibleText(message).slice(0, 120)}`
303
308
  })
304
309
  .join('\n')
305
310
  throw new Error(
@@ -339,7 +344,7 @@ export async function waitForMessageById({
339
344
  )
340
345
  }
341
346
 
342
- function isFooterMessage({
347
+ export function isFooterMessage({
343
348
  message,
344
349
  botUserId,
345
350
  }: {
@@ -349,10 +354,16 @@ function isFooterMessage({
349
354
  if (message.author.id !== botUserId) {
350
355
  return false
351
356
  }
352
- if (!message.content.startsWith('*')) {
357
+ const content = message.content.startsWith('> ')
358
+ ? message.content.slice(2)
359
+ : message.content
360
+ if (!content.startsWith('*')) {
361
+ return false
362
+ }
363
+ if (content.startsWith('*using ')) {
353
364
  return false
354
365
  }
355
- return message.content.includes('⋅')
366
+ return content.includes('⋅')
356
367
  }
357
368
 
358
369
  /**
@@ -380,7 +391,7 @@ export async function waitForFooterMessage({
380
391
  lastMessages = messages
381
392
  const afterIndex = afterMessageIncludes
382
393
  ? messages.findLastIndex((message) => {
383
- if (!message.content.includes(afterMessageIncludes)) {
394
+ if (!getMessageVisibleText(message).includes(afterMessageIncludes)) {
384
395
  return false
385
396
  }
386
397
  if (!afterAuthorId) {
@@ -413,7 +424,7 @@ export async function waitForFooterMessage({
413
424
  .slice(-12)
414
425
  .map((message) => {
415
426
  const role = message.author.id === discord.botUserId ? 'bot' : 'user'
416
- return `${role}: ${message.content.slice(0, 120)}`
427
+ return `${role}: ${getMessageVisibleText(message).slice(0, 120)}`
417
428
  })
418
429
  .join('\n')
419
430
  const anchorText = afterMessageIncludes || 'start'
@@ -0,0 +1,46 @@
1
+ // Tests thinking-level variant matching used by /xxx-agent variant.
2
+
3
+ import { describe, expect, test } from 'vitest'
4
+ import {
5
+ getThinkingValuesForModel,
6
+ matchThinkingValue,
7
+ } from './thinking-utils.js'
8
+
9
+ describe('matchThinkingValue', () => {
10
+ const providers = [
11
+ {
12
+ id: 'anthropic',
13
+ models: {
14
+ 'claude-opus-4-6': {
15
+ variants: {
16
+ high: {},
17
+ max: {},
18
+ },
19
+ },
20
+ },
21
+ },
22
+ ]
23
+ const availableValues = getThinkingValuesForModel({
24
+ providers,
25
+ providerId: 'anthropic',
26
+ modelId: 'claude-opus-4-6',
27
+ })
28
+
29
+ test('matches a supported thinking level case-insensitively', () => {
30
+ expect(
31
+ matchThinkingValue({
32
+ requestedValue: 'HIGH',
33
+ availableValues,
34
+ }),
35
+ ).toBe('high')
36
+ })
37
+
38
+ test('returns undefined for an unsupported thinking level', () => {
39
+ expect(
40
+ matchThinkingValue({
41
+ requestedValue: 'not-a-real-level',
42
+ availableValues,
43
+ }),
44
+ ).toBeUndefined()
45
+ })
46
+ })
@@ -59,3 +59,24 @@ export function matchThinkingValue({
59
59
  return availableValue.toLowerCase() === normalizedRequestedValue
60
60
  })
61
61
  }
62
+
63
+ export function resolveRequestedThinkingVariant({
64
+ requestedValue,
65
+ providers,
66
+ providerId,
67
+ modelId,
68
+ }: {
69
+ requestedValue: string
70
+ providers: ThinkingProvider[]
71
+ providerId: string
72
+ modelId: string
73
+ }): string | undefined {
74
+ return matchThinkingValue({
75
+ requestedValue,
76
+ availableValues: getThinkingValuesForModel({
77
+ providers,
78
+ providerId,
79
+ modelId,
80
+ }),
81
+ })
82
+ }