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.
- package/dist/agent-model.e2e.test.js +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- 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',
|
package/src/task-schedule.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// Scheduled task parsing
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
+
})
|
package/src/thinking-utils.ts
CHANGED
|
@@ -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
|
+
}
|