kimaki 0.26.0 → 0.27.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 +25 -122
- package/dist/anthropic-account-identity.js +1 -0
- package/dist/anthropic-auth-plugin.js +1 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/send.js +24 -0
- package/dist/cli-commands/session.js +183 -8
- package/dist/cli-commands/task.js +13 -1
- package/dist/cli-parsing.test.js +59 -3
- package/dist/cli-runner.js +28 -4
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +1 -1
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/agent.js +6 -8
- package/dist/commands/ask-question.js +21 -32
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +36 -10
- package/dist/commands/model-variant.js +7 -1
- package/dist/commands/model.js +14 -8
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/database.js +124 -3
- package/dist/db.js +22 -0
- package/dist/db.test.js +167 -1
- package/dist/discord-bot.js +34 -32
- package/dist/discord-command-registration.js +9 -1
- package/dist/discord-utils.js +13 -5
- package/dist/discord-utils.test.js +17 -1
- package/dist/errors.js +5 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +2 -2
- 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 +5 -5
- package/dist/git-worktree-core.js +182 -17
- package/dist/ipc-tools-plugin.js +63 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +7 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +10 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +18 -7
- package/dist/message-formatting.test.js +56 -9
- 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.test.js +60 -0
- package/dist/opencode.js +53 -12
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +1 -1
- package/dist/queue-advanced-action-buttons.e2e.test.js +3 -3
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +40 -246
- package/dist/queue-advanced-model-switch.e2e.test.js +2 -2
- package/dist/queue-advanced-permissions-typing.e2e.test.js +7 -9
- package/dist/queue-advanced-question.e2e.test.js +31 -26
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +2 -2
- package/dist/queue-delete-message.e2e.test.js +1 -1
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +2 -2
- package/dist/queue-interrupt-drain.e2e.test.js +112 -2
- package/dist/queue-question-select-drain.e2e.test.js +30 -128
- package/dist/runtime-lifecycle.e2e.test.js +10 -37
- package/dist/schema.js +34 -0
- package/dist/session-handler/event-stream-state.js +267 -50
- package/dist/session-handler/event-stream-state.test.js +415 -8
- package/dist/session-handler/model-utils.js +159 -5
- package/dist/session-handler/model-utils.test.js +345 -0
- package/dist/session-handler/thread-session-runtime.js +252 -182
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +7 -39
- package/dist/sleep.e2e.test.js +491 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +59 -6
- package/dist/system-message.test.js +76 -6
- package/dist/task-runner.js +94 -4
- package/dist/task-schedule.js +56 -0
- package/dist/task-schedule.test.js +71 -1
- package/dist/thread-message-queue.e2e.test.js +10 -169
- package/dist/undo-redo.e2e.test.js +2 -2
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-message.e2e.test.js +10 -10
- package/dist/voice.test.js +55 -29
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +29 -10
- 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/errore/SKILL.md +18 -0
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +37 -0
- 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 +25 -151
- package/src/anthropic-account-identity.ts +1 -0
- package/src/anthropic-auth-plugin.ts +1 -0
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/send.ts +25 -0
- package/src/cli-commands/session.ts +203 -9
- package/src/cli-commands/task.ts +13 -1
- package/src/cli-parsing.test.ts +69 -3
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +28 -4
- package/src/cli-send-thread.e2e.test.ts +1 -1
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/agent.ts +6 -6
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +21 -32
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +43 -12
- package/src/commands/model-variant.ts +8 -1
- package/src/commands/model.ts +18 -8
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/database.ts +195 -3
- package/src/db.test.ts +188 -0
- package/src/db.ts +26 -0
- package/src/discord-bot.ts +35 -29
- package/src/discord-command-registration.ts +15 -1
- package/src/discord-utils.test.ts +19 -0
- package/src/discord-utils.ts +21 -6
- package/src/errors.ts +5 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +2 -2
- 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 +5 -5
- package/src/git-worktree-core.ts +247 -20
- package/src/ipc-tools-plugin.ts +75 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +7 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +13 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +86 -12
- package/src/message-formatting.ts +19 -7
- 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 +81 -0
- package/src/opencode.ts +71 -11
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +1 -1
- package/src/queue-advanced-action-buttons.e2e.test.ts +3 -3
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +47 -294
- package/src/queue-advanced-model-switch.e2e.test.ts +2 -2
- package/src/queue-advanced-permissions-typing.e2e.test.ts +7 -9
- package/src/queue-advanced-question.e2e.test.ts +35 -29
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +2 -2
- package/src/queue-delete-message.e2e.test.ts +1 -1
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +2 -2
- package/src/queue-interrupt-drain.e2e.test.ts +123 -2
- package/src/queue-question-select-drain.e2e.test.ts +32 -157
- package/src/runtime-lifecycle.e2e.test.ts +10 -41
- package/src/schema.sql +14 -1
- package/src/schema.ts +36 -0
- package/src/session-handler/event-stream-state.test.ts +437 -7
- package/src/session-handler/event-stream-state.ts +389 -53
- package/src/session-handler/model-utils.test.ts +415 -0
- package/src/session-handler/model-utils.ts +249 -5
- package/src/session-handler/thread-session-runtime.ts +300 -215
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +9 -48
- package/src/sleep.e2e.test.ts +562 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +21 -0
- package/src/system-message.test.ts +86 -6
- package/src/system-message.ts +66 -7
- package/src/task-runner.ts +136 -2
- package/src/task-schedule.test.ts +76 -0
- package/src/task-schedule.ts +95 -0
- package/src/thread-message-queue.e2e.test.ts +10 -202
- package/src/undo-redo.e2e.test.ts +2 -2
- package/src/voice-attachment.ts +29 -1
- package/src/voice-message.e2e.test.ts +10 -10
- package/src/voice.test.ts +63 -29
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +29 -9
- 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
|
@@ -25,6 +25,15 @@ afterEach(async () => {
|
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
describe('system-message', () => {
|
|
28
|
+
test('requires reading the report-bugs guide before filing kimaki issues', () => {
|
|
29
|
+
const message = getOpencodeSystemMessage({
|
|
30
|
+
sessionId: 'ses_123',
|
|
31
|
+
})
|
|
32
|
+
expect(message).toContain(
|
|
33
|
+
'Never open a pull request on remorses/kimaki unless remorses asked for one in a comment on the issue',
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
|
|
28
37
|
test('includes callout guidance for important content', () => {
|
|
29
38
|
const message = getOpencodeSystemMessage({
|
|
30
39
|
sessionId: 'ses_123',
|
|
@@ -48,6 +57,7 @@ describe('system-message', () => {
|
|
|
48
57
|
expect(message).toContain('NEVER call `question` before your text')
|
|
49
58
|
expect(message).toContain('`kimaki_action_buttons`')
|
|
50
59
|
expect(message).toContain('`kimaki_file_upload`')
|
|
60
|
+
expect(message).toContain('`kimaki_sleep`')
|
|
51
61
|
})
|
|
52
62
|
|
|
53
63
|
test('persists and reads session system prompt for command path', async () => {
|
|
@@ -100,6 +110,22 @@ describe('system-message', () => {
|
|
|
100
110
|
).rejects.toMatchObject({ code: 'EISDIR' })
|
|
101
111
|
})
|
|
102
112
|
|
|
113
|
+
test('includes session title update guidance when scope or goal changed', () => {
|
|
114
|
+
const message = getOpencodeSystemMessage({
|
|
115
|
+
sessionId: 'ses_123',
|
|
116
|
+
threadId: 'thread_123',
|
|
117
|
+
})
|
|
118
|
+
expect(message).toContain('## updating the session title')
|
|
119
|
+
expect(message).toContain(
|
|
120
|
+
"kimaki session title 'Short title' --session ses_123",
|
|
121
|
+
)
|
|
122
|
+
expect(message).toContain(
|
|
123
|
+
'When the session scope or goal changed from the first message, update the title:',
|
|
124
|
+
)
|
|
125
|
+
expect(message).toContain('thread-name="..."')
|
|
126
|
+
expect(message).toContain('Do not retitle every turn')
|
|
127
|
+
})
|
|
128
|
+
|
|
103
129
|
test('includes parent session context when parentSessionId is set', () => {
|
|
104
130
|
const message = getOpencodeSystemMessage({
|
|
105
131
|
sessionId: 'ses_child',
|
|
@@ -151,6 +177,9 @@ describe('system-message', () => {
|
|
|
151
177
|
expect(message).toContain(
|
|
152
178
|
'When pulling submodules and they jump to a new commit, commit that submodule pointer update right away before doing other work.',
|
|
153
179
|
)
|
|
180
|
+
expect(message).toContain(
|
|
181
|
+
'while kimaki session list --active --exclude ses_123; do sleep 5; done',
|
|
182
|
+
)
|
|
154
183
|
|
|
155
184
|
expect(message).toMatchInlineSnapshot(`
|
|
156
185
|
"
|
|
@@ -180,7 +209,7 @@ describe('system-message', () => {
|
|
|
180
209
|
Your current Discord thread ID is: thread_123
|
|
181
210
|
Your current Discord guild ID is: guild_123
|
|
182
211
|
|
|
183
|
-
Per-turn Discord metadata like the current user
|
|
212
|
+
Per-turn Discord metadata like the current user, current agent, and Discord thread title is delivered in synthetic user message parts.
|
|
184
213
|
|
|
185
214
|
## permissions
|
|
186
215
|
|
|
@@ -203,6 +232,7 @@ describe('system-message', () => {
|
|
|
203
232
|
|
|
204
233
|
## debugging kimaki issues
|
|
205
234
|
|
|
235
|
+
ALWAYS read https://kimaki.dev/docs/guides/report-bugs first before submitting any issue to Kimaki. That page is the source of truth for exporting session jsonl, sharing evidence in a gist, and filing bugs. Never open a pull request on remorses/kimaki unless remorses asked for one in a comment on the issue.
|
|
206
236
|
If there are internal kimaki issues (sessions not responding, bot errors, unexpected behavior), read the log file at \`<data-dir>/kimaki.log\`. This file contains detailed logs of all bot activity including session creation, event handling, errors, and API calls. The log file is reset every time the bot restarts, so it only contains logs from the current run.
|
|
207
237
|
|
|
208
238
|
## uploading files to discord
|
|
@@ -232,9 +262,16 @@ describe('system-message', () => {
|
|
|
232
262
|
To ask the user to upload files from their device, use \`kimaki_file_upload\`. This shows a native file picker dialog in Discord. The files are downloaded to the project's \`uploads/\` directory and the tool returns the local file paths.
|
|
233
263
|
You MUST call \`kimaki_file_upload\` LAST, after ALL text.
|
|
234
264
|
|
|
265
|
+
## sleeping the session
|
|
266
|
+
|
|
267
|
+
Use \`kimaki_sleep\` to pause this session for hours or days, then continue when the time is reached. The sleep is stored in SQLite and survives bot restarts.
|
|
268
|
+
Pass either \`duration\` (\`30s\`, \`2h\`, \`1d\`) or \`until\` (UTC ISO ending with \`Z\`, example \`2026-08-20T09:00:00Z\`).
|
|
269
|
+
You MUST call \`kimaki_sleep\` LAST, after ALL text. Do not call more tools after it.
|
|
270
|
+
A new user message cancels the sleep. After wake, continue the wait reason.
|
|
271
|
+
|
|
235
272
|
## archiving the current thread
|
|
236
273
|
|
|
237
|
-
To archive the current Discord thread (hide it from sidebar)
|
|
274
|
+
To archive the current Discord thread (hide it from sidebar) without stopping the session, run:
|
|
238
275
|
|
|
239
276
|
kimaki session archive thread_123 (or --session ses_123)
|
|
240
277
|
|
|
@@ -249,6 +286,17 @@ describe('system-message', () => {
|
|
|
249
286
|
This stops the AI from processing but keeps the thread visible in Discord.
|
|
250
287
|
Different from \`kimaki session archive\` which hides the thread.
|
|
251
288
|
|
|
289
|
+
## updating the session title
|
|
290
|
+
|
|
291
|
+
When the session scope or goal changed from the first message, update the title:
|
|
292
|
+
|
|
293
|
+
kimaki session title 'Short title' --session ses_123
|
|
294
|
+
|
|
295
|
+
The current Discord thread title is in the per-turn \`<discord-user thread-name="..." />\` metadata.
|
|
296
|
+
This updates the OpenCode title. Discord follows automatically.
|
|
297
|
+
Do not retitle every turn. Discord rate-limits thread renames.
|
|
298
|
+
Keep titles short. Do not add emoji. Do not copy ⬦, btw:, or Fork: prefixes.
|
|
299
|
+
|
|
252
300
|
## discord user mentions
|
|
253
301
|
|
|
254
302
|
Prefer Discord user IDs for mentions. Discord bots cannot ping by @name; use \`<@userId>\` in message text or pass the ID to \`--user\`.
|
|
@@ -480,6 +528,9 @@ describe('system-message', () => {
|
|
|
480
528
|
kimaki session list
|
|
481
529
|
kimaki session list --json # machine-readable output
|
|
482
530
|
kimaki session list --project /path/to/project # specific project
|
|
531
|
+
|
|
532
|
+
# List only in-progress sessions. Exit status is 1 when none remain.
|
|
533
|
+
kimaki session list --active
|
|
483
534
|
\`\`\`
|
|
484
535
|
|
|
485
536
|
To search past sessions for this project (supports plain text or /regex/flags):
|
|
@@ -500,6 +551,29 @@ describe('system-message', () => {
|
|
|
500
551
|
|
|
501
552
|
Then use grep/read tools on the file to find what you need.
|
|
502
553
|
|
|
554
|
+
### who edited a file
|
|
555
|
+
|
|
556
|
+
To find the session that last edited a file, run:
|
|
557
|
+
|
|
558
|
+
\`\`\`bash
|
|
559
|
+
kimaki session editors src/foo.ts
|
|
560
|
+
kimaki session editors src/foo.ts --json
|
|
561
|
+
\`\`\`
|
|
562
|
+
|
|
563
|
+
Output is newest first. Each row has:
|
|
564
|
+
|
|
565
|
+
- **session ID** (\`ses_xxx\`)
|
|
566
|
+
- **title** (Discord thread name, so you can tell what that session was doing)
|
|
567
|
+
- **time ago** (when that session last edited the file)
|
|
568
|
+
|
|
569
|
+
Use this before a commit when this session did not edit the file. Put the original session ID as the last line of the commit message:
|
|
570
|
+
|
|
571
|
+
\`\`\`
|
|
572
|
+
Session: ses_xxx
|
|
573
|
+
\`\`\`
|
|
574
|
+
|
|
575
|
+
If this session edited the file, use this session ID instead. If several files come from different sessions, split the commit by session. Do not attribute another session's edits to this one.
|
|
576
|
+
|
|
503
577
|
## cross-project commands
|
|
504
578
|
|
|
505
579
|
When the user references another project by name, run \`kimaki project list\` to find its directory path and channel ID. Then read files, search code, or run commands directly in that directory. If the project is not listed, use \`kimaki project add /path/to/repo\` to register it and create a Discord channel for it. Do not add subfolders of an existing project — only add root project directories.
|
|
@@ -566,7 +640,7 @@ describe('system-message', () => {
|
|
|
566
640
|
|
|
567
641
|
When the user asks you to wait for an existing session, run \`kimaki session wait <session_id>\` yourself via Bash, then continue from the printed session markdown. Do not tell the user to run the command.
|
|
568
642
|
|
|
569
|
-
IMPORTANT: if you run \`kimaki send --wait
|
|
643
|
+
IMPORTANT: if you run \`kimaki send --wait\`, \`kimaki session wait <session_id>\`, or the active-session wait loop via the Bash tool, you must set the Bash tool \`timeout\` to **20 minutes or more** (example: \`timeout: 1_500_000\`). Otherwise the tool will terminate early (default is 2 minutes) and you won't see long sessions.
|
|
570
644
|
|
|
571
645
|
If your Bash tool timeout triggers anyway, fall back to reading the session output from disk:
|
|
572
646
|
|
|
@@ -581,9 +655,14 @@ describe('system-message', () => {
|
|
|
581
655
|
|
|
582
656
|
# Wait for a session that was already started elsewhere
|
|
583
657
|
kimaki session wait <session_id>
|
|
658
|
+
|
|
659
|
+
# Wait until every other in-progress session in this project finishes
|
|
660
|
+
while kimaki session list --active --exclude ses_123; do sleep 5; done
|
|
584
661
|
\`\`\`
|
|
585
662
|
|
|
586
|
-
|
|
663
|
+
\`session list --active\` exits with status 0 while it finds active sessions and status 1 when none remain. Exclude the current session in a wait loop so the loop does not wait for itself.
|
|
664
|
+
|
|
665
|
+
\`session wait\` exits with the session markdown on stdout once the model finishes responding.
|
|
587
666
|
|
|
588
667
|
Use \`--wait\` when you need to:
|
|
589
668
|
- **Fix a bug in another project** before continuing here (e.g. fix a dependency, then resume)
|
|
@@ -848,7 +927,7 @@ describe('system-message', () => {
|
|
|
848
927
|
You MUST call \`question\` LAST, after ALL text parts.
|
|
849
928
|
NEVER call \`question\` before your text. Discord will hide the message.
|
|
850
929
|
|
|
851
|
-
The same rule applies to \`kimaki_action_buttons\` and \`
|
|
930
|
+
The same rule applies to \`kimaki_action_buttons\`, \`kimaki_file_upload\`, and \`kimaki_sleep\`.
|
|
852
931
|
You MUST call them LAST, after ALL text.
|
|
853
932
|
|
|
854
933
|
ALWAYS use \`question\` when you ask the user a question. Do not write a numbered list in plain text.
|
|
@@ -878,6 +957,7 @@ describe('system-message', () => {
|
|
|
878
957
|
userId: 'user_123',
|
|
879
958
|
sourceMessageId: 'msg_123',
|
|
880
959
|
sourceThreadId: 'thread_123',
|
|
960
|
+
threadName: 'Fix auth bug',
|
|
881
961
|
repliedMessage: {
|
|
882
962
|
authorUsername: 'alice',
|
|
883
963
|
text: 'Original replied message',
|
|
@@ -891,7 +971,7 @@ describe('system-message', () => {
|
|
|
891
971
|
},
|
|
892
972
|
}),
|
|
893
973
|
).toMatchInlineSnapshot(`
|
|
894
|
-
"<discord-user name="Tommy" user-id="user_123" message-id="msg_123" thread-id="thread_123" />
|
|
974
|
+
"<discord-user name="Tommy" user-id="user_123" message-id="msg_123" thread-id="thread_123" thread-name="Fix auth bug" />
|
|
895
975
|
|
|
896
976
|
This message was a reply to message
|
|
897
977
|
|
package/src/system-message.ts
CHANGED
|
@@ -316,9 +316,13 @@ export type ThreadStartMarker = {
|
|
|
316
316
|
/**
|
|
317
317
|
* OpenCode session ID of the parent session that spawned this thread via
|
|
318
318
|
* `kimaki send --parent-session`. Exposed in the child system message so the
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
* child can message the parent only when the user explicitly asks.
|
|
320
|
+
*/
|
|
321
321
|
parentSessionId?: string
|
|
322
|
+
/** Wake prompt posted after kimaki_sleep. Ingress must not cancel this sleep. */
|
|
323
|
+
sleepWake?: boolean
|
|
324
|
+
/** delivery_id of the session_sleeps row this wake delivers. */
|
|
325
|
+
sleepId?: string
|
|
322
326
|
}
|
|
323
327
|
|
|
324
328
|
export function isInjectedPromptMarker({
|
|
@@ -357,6 +361,7 @@ export function getOpencodePromptContext({
|
|
|
357
361
|
userId,
|
|
358
362
|
sourceMessageId,
|
|
359
363
|
sourceThreadId,
|
|
364
|
+
threadName,
|
|
360
365
|
repliedMessage,
|
|
361
366
|
worktree,
|
|
362
367
|
currentAgent,
|
|
@@ -366,6 +371,7 @@ export function getOpencodePromptContext({
|
|
|
366
371
|
userId?: string
|
|
367
372
|
sourceMessageId?: string
|
|
368
373
|
sourceThreadId?: string
|
|
374
|
+
threadName?: string
|
|
369
375
|
repliedMessage?: RepliedMessageContext
|
|
370
376
|
worktree?: WorktreeInfo
|
|
371
377
|
currentAgent?: string
|
|
@@ -384,6 +390,9 @@ export function getOpencodePromptContext({
|
|
|
384
390
|
...(sourceThreadId
|
|
385
391
|
? [` thread-id="${escapePromptAttribute(sourceThreadId)}"`]
|
|
386
392
|
: []),
|
|
393
|
+
...(threadName
|
|
394
|
+
? [` thread-name="${escapePromptAttribute(threadName)}"`]
|
|
395
|
+
: []),
|
|
387
396
|
].join('')
|
|
388
397
|
const repliedMessageXml = repliedMessage
|
|
389
398
|
? `This message was a reply to message
|
|
@@ -486,7 +495,7 @@ interface BashToolInput {
|
|
|
486
495
|
|
|
487
496
|
Your current OpenCode session ID is: ${sessionId}${channelId ? `\nYour current Discord channel ID is: ${channelId}` : ''}${threadId ? `\nYour current Discord thread ID is: ${threadId}` : ''}${guildId ? `\nYour current Discord guild ID is: ${guildId}` : ''}${parentSessionContext}
|
|
488
497
|
|
|
489
|
-
Per-turn Discord metadata like the current user
|
|
498
|
+
Per-turn Discord metadata like the current user, current agent, and Discord thread title is delivered in synthetic user message parts.
|
|
490
499
|
|
|
491
500
|
## permissions
|
|
492
501
|
|
|
@@ -509,6 +518,7 @@ Do not restart the bot unless the user explicitly asks for it.
|
|
|
509
518
|
|
|
510
519
|
## debugging kimaki issues
|
|
511
520
|
|
|
521
|
+
ALWAYS read https://kimaki.dev/docs/guides/report-bugs first before submitting any issue to Kimaki. That page is the source of truth for exporting session jsonl, sharing evidence in a gist, and filing bugs. Never open a pull request on remorses/kimaki unless remorses asked for one in a comment on the issue.
|
|
512
522
|
If there are internal kimaki issues (sessions not responding, bot errors, unexpected behavior), read the log file at \`${getDataDir()}/kimaki.log\`. This file contains detailed logs of all bot activity including session creation, event handling, errors, and API calls. The log file is reset every time the bot restarts, so it only contains logs from the current run.
|
|
513
523
|
|
|
514
524
|
## uploading files to discord
|
|
@@ -538,9 +548,16 @@ see --help for options like voice, speed, etc.
|
|
|
538
548
|
To ask the user to upload files from their device, use \`kimaki_file_upload\`. This shows a native file picker dialog in Discord. The files are downloaded to the project's \`uploads/\` directory and the tool returns the local file paths.
|
|
539
549
|
You MUST call \`kimaki_file_upload\` LAST, after ALL text.
|
|
540
550
|
|
|
551
|
+
## sleeping the session
|
|
552
|
+
|
|
553
|
+
Use \`kimaki_sleep\` to pause this session for hours or days, then continue when the time is reached. The sleep is stored in SQLite and survives bot restarts.
|
|
554
|
+
Pass either \`duration\` (\`30s\`, \`2h\`, \`1d\`) or \`until\` (UTC ISO ending with \`Z\`, example \`2026-08-20T09:00:00Z\`).
|
|
555
|
+
You MUST call \`kimaki_sleep\` LAST, after ALL text. Do not call more tools after it.
|
|
556
|
+
A new user message cancels the sleep. After wake, continue the wait reason.
|
|
557
|
+
|
|
541
558
|
## archiving the current thread
|
|
542
559
|
|
|
543
|
-
To archive the current Discord thread (hide it from sidebar)
|
|
560
|
+
To archive the current Discord thread (hide it from sidebar) without stopping the session, run:
|
|
544
561
|
|
|
545
562
|
kimaki session archive ${archiveTarget}
|
|
546
563
|
|
|
@@ -555,6 +572,17 @@ kimaki session abort <session_id>
|
|
|
555
572
|
This stops the AI from processing but keeps the thread visible in Discord.
|
|
556
573
|
Different from \`kimaki session archive\` which hides the thread.
|
|
557
574
|
|
|
575
|
+
## updating the session title
|
|
576
|
+
|
|
577
|
+
When the session scope or goal changed from the first message, update the title:
|
|
578
|
+
|
|
579
|
+
kimaki session title 'Short title' --session ${sessionId}
|
|
580
|
+
|
|
581
|
+
The current Discord thread title is in the per-turn \`<discord-user thread-name="..." />\` metadata.
|
|
582
|
+
This updates the OpenCode title. Discord follows automatically.
|
|
583
|
+
Do not retitle every turn. Discord rate-limits thread renames.
|
|
584
|
+
Keep titles short. Do not add emoji. Do not copy ⬦, btw:, or Fork: prefixes.
|
|
585
|
+
|
|
558
586
|
## discord user mentions
|
|
559
587
|
|
|
560
588
|
Prefer Discord user IDs for mentions. Discord bots cannot ping by @name; use \`<@userId>\` in message text or pass the ID to \`--user\`.
|
|
@@ -784,6 +812,9 @@ To list all sessions in this project (shows which were started via kimaki):
|
|
|
784
812
|
kimaki session list
|
|
785
813
|
kimaki session list --json # machine-readable output
|
|
786
814
|
kimaki session list --project /path/to/project # specific project
|
|
815
|
+
|
|
816
|
+
# List only in-progress sessions. Exit status is 1 when none remain.
|
|
817
|
+
kimaki session list --active
|
|
787
818
|
\`\`\`
|
|
788
819
|
|
|
789
820
|
To search past sessions for this project (supports plain text or /regex/flags):
|
|
@@ -804,6 +835,29 @@ kimaki session read <sessionId> > ./tmp/session.md 2>/dev/null
|
|
|
804
835
|
|
|
805
836
|
Then use grep/read tools on the file to find what you need.
|
|
806
837
|
|
|
838
|
+
### who edited a file
|
|
839
|
+
|
|
840
|
+
To find the session that last edited a file, run:
|
|
841
|
+
|
|
842
|
+
\`\`\`bash
|
|
843
|
+
kimaki session editors src/foo.ts
|
|
844
|
+
kimaki session editors src/foo.ts --json
|
|
845
|
+
\`\`\`
|
|
846
|
+
|
|
847
|
+
Output is newest first. Each row has:
|
|
848
|
+
|
|
849
|
+
- **session ID** (\`ses_xxx\`)
|
|
850
|
+
- **title** (Discord thread name, so you can tell what that session was doing)
|
|
851
|
+
- **time ago** (when that session last edited the file)
|
|
852
|
+
|
|
853
|
+
Use this before a commit when this session did not edit the file. Put the original session ID as the last line of the commit message:
|
|
854
|
+
|
|
855
|
+
\`\`\`
|
|
856
|
+
Session: ses_xxx
|
|
857
|
+
\`\`\`
|
|
858
|
+
|
|
859
|
+
If this session edited the file, use this session ID instead. If several files come from different sessions, split the commit by session. Do not attribute another session's edits to this one.
|
|
860
|
+
|
|
807
861
|
## cross-project commands
|
|
808
862
|
|
|
809
863
|
When the user references another project by name, run \`kimaki project list\` to find its directory path and channel ID. Then read files, search code, or run commands directly in that directory. If the project is not listed, use \`kimaki project add /path/to/repo\` to register it and create a Discord channel for it. Do not add subfolders of an existing project — only add root project directories.
|
|
@@ -870,7 +924,7 @@ Use \`--wait\` to block until a session completes and print its full conversatio
|
|
|
870
924
|
|
|
871
925
|
When the user asks you to wait for an existing session, run \`kimaki session wait <session_id>\` yourself via Bash, then continue from the printed session markdown. Do not tell the user to run the command.
|
|
872
926
|
|
|
873
|
-
IMPORTANT: if you run \`kimaki send --wait
|
|
927
|
+
IMPORTANT: if you run \`kimaki send --wait\`, \`kimaki session wait <session_id>\`, or the active-session wait loop via the Bash tool, you must set the Bash tool \`timeout\` to **20 minutes or more** (example: \`timeout: 1_500_000\`). Otherwise the tool will terminate early (default is 2 minutes) and you won't see long sessions.
|
|
874
928
|
|
|
875
929
|
If your Bash tool timeout triggers anyway, fall back to reading the session output from disk:
|
|
876
930
|
|
|
@@ -885,9 +939,14 @@ kimaki send --thread <thread_id> --prompt 'Run the tests' --wait --agent <curren
|
|
|
885
939
|
|
|
886
940
|
# Wait for a session that was already started elsewhere
|
|
887
941
|
kimaki session wait <session_id>
|
|
942
|
+
|
|
943
|
+
# Wait until every other in-progress session in this project finishes
|
|
944
|
+
while kimaki session list --active --exclude ${sessionId}; do sleep 5; done
|
|
888
945
|
\`\`\`
|
|
889
946
|
|
|
890
|
-
|
|
947
|
+
\`session list --active\` exits with status 0 while it finds active sessions and status 1 when none remain. Exclude the current session in a wait loop so the loop does not wait for itself.
|
|
948
|
+
|
|
949
|
+
\`session wait\` exits with the session markdown on stdout once the model finishes responding.
|
|
891
950
|
|
|
892
951
|
Use \`--wait\` when you need to:
|
|
893
952
|
- **Fix a bug in another project** before continuing here (e.g. fix a dependency, then resume)
|
|
@@ -1001,7 +1060,7 @@ You MUST write ALL user-visible text FIRST.
|
|
|
1001
1060
|
You MUST call \`question\` LAST, after ALL text parts.
|
|
1002
1061
|
NEVER call \`question\` before your text. Discord will hide the message.
|
|
1003
1062
|
|
|
1004
|
-
The same rule applies to \`kimaki_action_buttons\` and \`
|
|
1063
|
+
The same rule applies to \`kimaki_action_buttons\`, \`kimaki_file_upload\`, and \`kimaki_sleep\`.
|
|
1005
1064
|
You MUST call them LAST, after ALL text.
|
|
1006
1065
|
|
|
1007
1066
|
ALWAYS use \`question\` when you ask the user a question. Do not write a numbered list in plain text.
|
package/src/task-runner.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Scheduled task runner for
|
|
1
|
+
// Scheduled task runner for due `send --send-at` jobs and session sleep wakes.
|
|
2
2
|
|
|
3
|
-
import { type REST, Routes } from 'discord.js'
|
|
3
|
+
import { DiscordAPIError, type REST, Routes } from 'discord.js'
|
|
4
4
|
import { createDiscordRest } from './discord-urls.js'
|
|
5
5
|
import { ensureThreadMember } from './discord-utils.js'
|
|
6
6
|
import YAML from 'yaml'
|
|
@@ -20,6 +20,11 @@ import {
|
|
|
20
20
|
releaseScheduledTaskClaim,
|
|
21
21
|
setScheduledTaskRunThread,
|
|
22
22
|
type ScheduledTask,
|
|
23
|
+
type SessionSleep,
|
|
24
|
+
claimSessionSleepAttempt,
|
|
25
|
+
getDueSessionSleeps,
|
|
26
|
+
getThreadIdBySessionId,
|
|
27
|
+
markSessionSleepFailed,
|
|
23
28
|
} from './database.js'
|
|
24
29
|
import { execAsync } from './exec-async.js'
|
|
25
30
|
import { initializeOpencodeForDirectory } from './opencode.js'
|
|
@@ -29,6 +34,7 @@ import type { ThreadStartMarker } from './system-message.js'
|
|
|
29
34
|
import {
|
|
30
35
|
type ScheduledTaskPayload,
|
|
31
36
|
appendTaskCommandOutput,
|
|
37
|
+
formatSessionSleepWakePrompt,
|
|
32
38
|
getNextCronRun,
|
|
33
39
|
getPromptPreview,
|
|
34
40
|
parseScheduledTaskPayload,
|
|
@@ -129,6 +135,132 @@ async function executeThreadScheduledTask({
|
|
|
129
135
|
return payload.threadId
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
/** Wait before retrying a wake that ingress has not consumed yet. */
|
|
139
|
+
const SLEEP_WAKE_RETRY_AFTER_MS = 30_000
|
|
140
|
+
const SLEEP_WAKE_MAX_ATTEMPTS = 5
|
|
141
|
+
|
|
142
|
+
type SleepWakeFailure = { error: Error; permanent: boolean }
|
|
143
|
+
|
|
144
|
+
export function buildSessionSleepWakeBody({
|
|
145
|
+
deliveryId,
|
|
146
|
+
wakeAt,
|
|
147
|
+
reason,
|
|
148
|
+
}: {
|
|
149
|
+
deliveryId: string
|
|
150
|
+
wakeAt: Date
|
|
151
|
+
reason: string | null
|
|
152
|
+
}) {
|
|
153
|
+
const marker: ThreadStartMarker = {
|
|
154
|
+
start: true,
|
|
155
|
+
sleepWake: true,
|
|
156
|
+
sleepId: deliveryId,
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
content: formatSessionSleepWakePrompt({ wakeAt, reason }),
|
|
160
|
+
embeds: [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }],
|
|
161
|
+
// Discord limits nonces to 25 characters. The full ID stays in the marker.
|
|
162
|
+
// A stable nonce deduplicates retries when a successful response is lost.
|
|
163
|
+
nonce: deliveryId.replaceAll('-', '').slice(0, 25),
|
|
164
|
+
enforce_nonce: true,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function postSessionSleepWake({
|
|
169
|
+
rest,
|
|
170
|
+
sleep,
|
|
171
|
+
threadId,
|
|
172
|
+
}: {
|
|
173
|
+
rest: REST
|
|
174
|
+
sleep: SessionSleep
|
|
175
|
+
threadId: string
|
|
176
|
+
}): Promise<SleepWakeFailure | null> {
|
|
177
|
+
// `.then(() => null)` keeps the success type concrete. Returning the raw post
|
|
178
|
+
// value would widen the union to `unknown` and erase the failure shape.
|
|
179
|
+
return await rest
|
|
180
|
+
.post(Routes.channelMessages(threadId), {
|
|
181
|
+
body: buildSessionSleepWakeBody({
|
|
182
|
+
deliveryId: sleep.delivery_id,
|
|
183
|
+
wakeAt: sleep.wake_at,
|
|
184
|
+
reason: sleep.reason,
|
|
185
|
+
}),
|
|
186
|
+
})
|
|
187
|
+
.then((): SleepWakeFailure | null => {
|
|
188
|
+
return null
|
|
189
|
+
})
|
|
190
|
+
.catch((error): SleepWakeFailure => {
|
|
191
|
+
// Only an unknown channel/message is genuinely unrecoverable. A 403 is
|
|
192
|
+
// often temporary (permissions revoked, thread locked) so it keeps
|
|
193
|
+
// retrying until the attempt budget runs out.
|
|
194
|
+
const permanent =
|
|
195
|
+
error instanceof DiscordAPIError &&
|
|
196
|
+
error.status === 404
|
|
197
|
+
return {
|
|
198
|
+
error: new Error(
|
|
199
|
+
`Failed to post sleep wake for session ${sleep.session_id}`,
|
|
200
|
+
{ cause: error },
|
|
201
|
+
),
|
|
202
|
+
permanent,
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function wakeDueSessionSleeps({
|
|
208
|
+
rest,
|
|
209
|
+
now = new Date(),
|
|
210
|
+
limit = 20,
|
|
211
|
+
}: {
|
|
212
|
+
rest: REST
|
|
213
|
+
now?: Date
|
|
214
|
+
limit?: number
|
|
215
|
+
}): Promise<void> {
|
|
216
|
+
const dueSleeps = await getDueSessionSleeps({
|
|
217
|
+
now,
|
|
218
|
+
retryAfterMs: SLEEP_WAKE_RETRY_AFTER_MS,
|
|
219
|
+
limit,
|
|
220
|
+
})
|
|
221
|
+
for (const sleep of dueSleeps) {
|
|
222
|
+
// Reserves an attempt but stays `planned`. The row only leaves `planned`
|
|
223
|
+
// when ingress consumes the wake, so dying anywhere below costs one retry
|
|
224
|
+
// delay instead of losing the wake.
|
|
225
|
+
const claimed = await claimSessionSleepAttempt({
|
|
226
|
+
sessionId: sleep.session_id,
|
|
227
|
+
deliveryId: sleep.delivery_id,
|
|
228
|
+
now,
|
|
229
|
+
retryAfterMs: SLEEP_WAKE_RETRY_AFTER_MS,
|
|
230
|
+
})
|
|
231
|
+
if (!claimed) continue
|
|
232
|
+
|
|
233
|
+
// Resolved now, not when the tool ran, so a session rebound by /resume
|
|
234
|
+
// wakes in the thread that currently owns it.
|
|
235
|
+
const threadId = await getThreadIdBySessionId(claimed.session_id)
|
|
236
|
+
if (!threadId) {
|
|
237
|
+
await markSessionSleepFailed({
|
|
238
|
+
sessionId: claimed.session_id,
|
|
239
|
+
deliveryId: claimed.delivery_id,
|
|
240
|
+
})
|
|
241
|
+
taskLogger.warn(
|
|
242
|
+
`[task-runner] no thread owns session ${claimed.session_id}, dropping sleep wake`,
|
|
243
|
+
)
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// A successful post changes nothing here on purpose: ingress owns the
|
|
248
|
+
// transition out of `planned` when the wake actually becomes a turn.
|
|
249
|
+
const postResult = await postSessionSleepWake({ rest, sleep: claimed, threadId })
|
|
250
|
+
if (!postResult) continue
|
|
251
|
+
|
|
252
|
+
const exhausted = claimed.attempts >= SLEEP_WAKE_MAX_ATTEMPTS
|
|
253
|
+
if (postResult.permanent || exhausted) {
|
|
254
|
+
await markSessionSleepFailed({
|
|
255
|
+
sessionId: claimed.session_id,
|
|
256
|
+
deliveryId: claimed.delivery_id,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
taskLogger.error(`[task-runner] ${formatErrorWithStack(postResult.error)}`)
|
|
260
|
+
void notifyError(postResult.error, 'Session sleep wake failed')
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
132
264
|
async function executeChannelScheduledTask({
|
|
133
265
|
rest,
|
|
134
266
|
task,
|
|
@@ -564,6 +696,8 @@ async function runTaskRunnerTick({
|
|
|
564
696
|
await previous
|
|
565
697
|
await processDueTask({ rest, task })
|
|
566
698
|
}, Promise.resolve())
|
|
699
|
+
|
|
700
|
+
await wakeDueSessionSleeps({ rest })
|
|
567
701
|
}
|
|
568
702
|
|
|
569
703
|
export function startTaskRunner({
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
appendTaskCommandOutput,
|
|
19
19
|
parseScheduledTaskPayload,
|
|
20
20
|
parseSendAtValue,
|
|
21
|
+
parseSleepWakeAt,
|
|
21
22
|
} from './task-schedule.js'
|
|
22
23
|
|
|
23
24
|
describe('parseSendAtValue', () => {
|
|
@@ -100,6 +101,81 @@ describe('parseSendAtValue', () => {
|
|
|
100
101
|
})
|
|
101
102
|
})
|
|
102
103
|
|
|
104
|
+
describe('parseSleepWakeAt', () => {
|
|
105
|
+
const now = new Date('2026-08-19T12:00:00Z')
|
|
106
|
+
|
|
107
|
+
test('adds 2h to now', () => {
|
|
108
|
+
const result = parseSleepWakeAt({ duration: '2h', now })
|
|
109
|
+
expect(result).not.toBeInstanceOf(Error)
|
|
110
|
+
if (result instanceof Error) throw result
|
|
111
|
+
expect(result.toISOString()).toBe('2026-08-19T14:00:00.000Z')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('adds 30s and 1d', () => {
|
|
115
|
+
const thirtySeconds = parseSleepWakeAt({ duration: '30s', now })
|
|
116
|
+
expect(thirtySeconds).not.toBeInstanceOf(Error)
|
|
117
|
+
if (thirtySeconds instanceof Error) throw thirtySeconds
|
|
118
|
+
expect(thirtySeconds.toISOString()).toBe('2026-08-19T12:00:30.000Z')
|
|
119
|
+
|
|
120
|
+
const oneDay = parseSleepWakeAt({ duration: '1d', now })
|
|
121
|
+
expect(oneDay).not.toBeInstanceOf(Error)
|
|
122
|
+
if (oneDay instanceof Error) throw oneDay
|
|
123
|
+
expect(oneDay.toISOString()).toBe('2026-08-20T12:00:00.000Z')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('accepts until UTC ISO date ending with Z', () => {
|
|
127
|
+
const result = parseSleepWakeAt({
|
|
128
|
+
until: '2026-08-20T09:00:00Z',
|
|
129
|
+
now,
|
|
130
|
+
})
|
|
131
|
+
expect(result).not.toBeInstanceOf(Error)
|
|
132
|
+
if (result instanceof Error) throw result
|
|
133
|
+
expect(result.toISOString()).toBe('2026-08-20T09:00:00.000Z')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('rejects past until', () => {
|
|
137
|
+
const result = parseSleepWakeAt({
|
|
138
|
+
until: '2026-08-19T11:59:59Z',
|
|
139
|
+
now,
|
|
140
|
+
})
|
|
141
|
+
expect(result).toBeInstanceOf(Error)
|
|
142
|
+
if (result instanceof Error) {
|
|
143
|
+
expect(result.message).toContain('until must be in the future (UTC)')
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('rejects both duration and until', () => {
|
|
148
|
+
const result = parseSleepWakeAt({
|
|
149
|
+
duration: '1h',
|
|
150
|
+
until: '2026-08-20T09:00:00Z',
|
|
151
|
+
now,
|
|
152
|
+
})
|
|
153
|
+
expect(result).toBeInstanceOf(Error)
|
|
154
|
+
if (result instanceof Error) {
|
|
155
|
+
expect(result.message).toContain('either duration or until')
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('rejects neither duration nor until', () => {
|
|
160
|
+
const result = parseSleepWakeAt({ now })
|
|
161
|
+
expect(result).toBeInstanceOf(Error)
|
|
162
|
+
if (result instanceof Error) {
|
|
163
|
+
expect(result.message).toContain('duration or until')
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('rejects until with a non-UTC offset', () => {
|
|
168
|
+
const result = parseSleepWakeAt({
|
|
169
|
+
until: '2026-08-20T09:00:00+01:00',
|
|
170
|
+
now,
|
|
171
|
+
})
|
|
172
|
+
expect(result).toBeInstanceOf(Error)
|
|
173
|
+
if (result instanceof Error) {
|
|
174
|
+
expect(result.message).toContain('UTC ISO format ending with Z')
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
103
179
|
describe('scheduled task execution options', () => {
|
|
104
180
|
const tempDirectories: string[] = []
|
|
105
181
|
|