kimaki 0.30.0 → 0.31.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 +27 -19
- package/dist/analytics.js +3 -1
- package/dist/btw-prefix-detection.js +9 -0
- package/dist/btw-prefix-detection.test.js +38 -1
- package/dist/cli-commands/send.js +5 -11
- package/dist/cli-runner.js +22 -10
- package/dist/cli-runner.test.js +34 -1
- package/dist/cli-send-thread.e2e.test.js +1 -1
- package/dist/commands/abort.js +16 -4
- package/dist/commands/btw.js +147 -39
- package/dist/commands/context-usage.js +47 -0
- package/dist/commands/context-usage.test.js +49 -0
- package/dist/commands/fork.js +32 -1
- package/dist/commands/model.js +69 -14
- package/dist/commands/new-worktree.js +5 -2
- package/dist/commands/queue.js +37 -30
- package/dist/commands/session.js +7 -2
- package/dist/db.test.js +17 -7
- package/dist/discord-bot.js +70 -47
- package/dist/discord-utils.js +15 -1
- package/dist/errors.js +10 -5
- package/dist/external-directory-permissions.e2e.test.js +4 -4
- package/dist/external-directory-project-deny.e2e.test.js +2 -2
- package/dist/gateway-proxy.e2e.test.js +5 -5
- package/dist/message-formatting.js +50 -15
- package/dist/message-formatting.test.js +12 -11
- package/dist/message-preprocessing.js +53 -75
- package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +1 -1
- package/dist/queue-advanced-abort.e2e.test.js +62 -6
- package/dist/queue-advanced-action-buttons.e2e.test.js +3 -3
- package/dist/queue-advanced-footer.e2e.test.js +8 -8
- package/dist/queue-advanced-model-switch.e2e.test.js +2 -2
- package/dist/queue-advanced-permissions-typing.e2e.test.js +2 -2
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +3 -3
- package/dist/queue-delete-message.e2e.test.js +146 -5
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +2 -2
- package/dist/queue-interrupt-drain.e2e.test.js +24 -14
- package/dist/queue-question-select-drain.e2e.test.js +27 -27
- package/dist/runtime-lifecycle.e2e.test.js +6 -6
- package/dist/session-handler/agent-utils.js +16 -17
- package/dist/session-handler/event-stream-state.js +24 -16
- package/dist/session-handler/event-stream-state.test.js +281 -14
- package/dist/session-handler/thread-session-runtime.js +209 -128
- package/dist/sleep.e2e.test.js +6 -6
- package/dist/system-message.js +214 -392
- package/dist/system-message.test.js +204 -380
- package/dist/task-runner.js +3 -3
- package/dist/task-schedule.js +2 -2
- package/dist/test-utils.js +32 -6
- package/dist/thread-message-queue.e2e.test.js +132 -60
- package/dist/undo-redo.e2e.test.js +3 -3
- package/dist/voice-handler.js +2 -1
- package/dist/voice-message.e2e.test.js +22 -31
- package/dist/voice-routing-input.e2e.test.js +2 -2
- package/dist/voice-routing-question.e2e.test.js +59 -2
- package/dist/voice.js +306 -206
- package/dist/voice.test.js +161 -13
- package/dist/worktree-lifecycle.e2e.test.js +7 -7
- package/package.json +6 -9
- package/skills/critique/SKILL.md +4 -0
- package/skills/holocron/SKILL.md +0 -28
- package/skills/opensrc/SKILL.md +20 -13
- package/skills/playwriter/SKILL.md +1 -1
- package/src/agent-model.e2e.test.ts +28 -19
- package/src/analytics.ts +3 -1
- package/src/btw-prefix-detection.test.ts +39 -1
- package/src/btw-prefix-detection.ts +11 -0
- package/src/cli-commands/send.ts +11 -11
- package/src/cli-runner.test.ts +37 -0
- package/src/cli-runner.ts +26 -10
- package/src/cli-send-thread.e2e.test.ts +1 -1
- package/src/commands/abort.ts +20 -4
- package/src/commands/btw.ts +151 -41
- package/src/commands/context-usage.test.ts +57 -0
- package/src/commands/context-usage.ts +59 -0
- package/src/commands/fork.ts +34 -1
- package/src/commands/model.ts +81 -14
- package/src/commands/new-worktree.ts +5 -2
- package/src/commands/queue.ts +46 -30
- package/src/commands/session.ts +10 -2
- package/src/db.test.ts +18 -6
- package/src/discord-bot.ts +72 -56
- package/src/discord-utils.ts +17 -1
- package/src/errors.ts +12 -6
- package/src/external-directory-permissions.e2e.test.ts +4 -4
- package/src/external-directory-project-deny.e2e.test.ts +2 -2
- package/src/gateway-proxy.e2e.test.ts +5 -5
- package/src/message-formatting.test.ts +12 -11
- package/src/message-formatting.ts +52 -16
- package/src/message-preprocessing.ts +69 -91
- package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +1 -1
- package/src/queue-advanced-abort.e2e.test.ts +73 -6
- package/src/queue-advanced-action-buttons.e2e.test.ts +3 -3
- package/src/queue-advanced-footer.e2e.test.ts +8 -8
- package/src/queue-advanced-model-switch.e2e.test.ts +2 -2
- package/src/queue-advanced-permissions-typing.e2e.test.ts +2 -2
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +3 -3
- package/src/queue-delete-message.e2e.test.ts +164 -4
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +2 -2
- package/src/queue-interrupt-drain.e2e.test.ts +24 -13
- package/src/queue-question-select-drain.e2e.test.ts +31 -28
- package/src/runtime-lifecycle.e2e.test.ts +6 -6
- package/src/session-handler/agent-utils.ts +25 -20
- package/src/session-handler/event-stream-state.test.ts +284 -13
- package/src/session-handler/event-stream-state.ts +37 -21
- package/src/session-handler/thread-runtime-state.ts +7 -0
- package/src/session-handler/thread-session-runtime.ts +255 -155
- package/src/sleep.e2e.test.ts +6 -6
- package/src/system-message.test.ts +216 -379
- package/src/system-message.ts +253 -389
- package/src/task-runner.ts +3 -3
- package/src/task-schedule.ts +2 -2
- package/src/test-utils.ts +55 -12
- package/src/thread-message-queue.e2e.test.ts +136 -63
- package/src/undo-redo.e2e.test.ts +3 -3
- package/src/voice-handler.ts +2 -1
- package/src/voice-message.e2e.test.ts +22 -33
- package/src/voice-routing-input.e2e.test.ts +2 -2
- package/src/voice-routing-question.e2e.test.ts +59 -2
- package/src/voice.test.ts +164 -14
- package/src/voice.ts +400 -279
- package/src/worktree-lifecycle.e2e.test.ts +7 -7
|
@@ -400,7 +400,7 @@ describe('agent model resolution', () => {
|
|
|
400
400
|
Reply with exactly: agent-model-check
|
|
401
401
|
--- from: assistant (TestBot)
|
|
402
402
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
403
|
-
|
|
403
|
+
ok
|
|
404
404
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
405
405
|
`);
|
|
406
406
|
expect(footerMessage).toBeDefined();
|
|
@@ -444,7 +444,7 @@ describe('agent model resolution', () => {
|
|
|
444
444
|
Reply with exactly: system-context-check
|
|
445
445
|
--- from: assistant (TestBot)
|
|
446
446
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
447
|
-
|
|
447
|
+
system-context-ok
|
|
448
448
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
449
449
|
`);
|
|
450
450
|
}, 15_000);
|
|
@@ -550,7 +550,7 @@ describe('agent model resolution', () => {
|
|
|
550
550
|
Reply with exactly: channel-model-check
|
|
551
551
|
--- from: assistant (TestBot)
|
|
552
552
|
-# *using deterministic-provider/channel-model-v2*
|
|
553
|
-
|
|
553
|
+
ok
|
|
554
554
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*"
|
|
555
555
|
`);
|
|
556
556
|
expect(footerMessage).toBeDefined();
|
|
@@ -594,7 +594,7 @@ describe('agent model resolution', () => {
|
|
|
594
594
|
Reply with exactly: variant-check
|
|
595
595
|
--- from: assistant (TestBot)
|
|
596
596
|
-# *using deterministic-provider/channel-model-v2*
|
|
597
|
-
|
|
597
|
+
ok
|
|
598
598
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*"
|
|
599
599
|
`);
|
|
600
600
|
expect(footer?.content).toContain(CHANNEL_MODEL);
|
|
@@ -655,13 +655,21 @@ describe('agent model resolution', () => {
|
|
|
655
655
|
const forkedSessionModel = forkedSessionId
|
|
656
656
|
? await getSessionModel(forkedSessionId)
|
|
657
657
|
: undefined;
|
|
658
|
+
// The immediate btw ack is edited in place with the fork link.
|
|
659
|
+
const sourceThreadText = (await discord.thread(sourceThread.id).text())
|
|
660
|
+
.replace(`<#${forkedThread.id}>`, '<#FORK_THREAD>');
|
|
661
|
+
expect(sourceThreadText.split('\n').slice(-3).join('\n')).toMatchInlineSnapshot(`
|
|
662
|
+
"Reply with exactly: btw-model-check. btw
|
|
663
|
+
--- from: assistant (TestBot)
|
|
664
|
+
Session forked! Continue in <#FORK_THREAD>"
|
|
665
|
+
`);
|
|
658
666
|
const forkedThreadText = (await discord.thread(forkedThread.id).text())
|
|
659
667
|
.replace(`<#${sourceThread.id}>`, '<#SOURCE_THREAD>');
|
|
660
668
|
expect(forkedThreadText).toMatchInlineSnapshot(`
|
|
661
669
|
"--- from: assistant (TestBot)
|
|
662
670
|
Reusing context from <#SOURCE_THREAD> to answer prompt...
|
|
663
671
|
Reply with exactly: btw-model-check
|
|
664
|
-
|
|
672
|
+
ok
|
|
665
673
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2*"
|
|
666
674
|
`);
|
|
667
675
|
expect(forkedSessionModel).toMatchInlineSnapshot(`
|
|
@@ -726,12 +734,12 @@ describe('agent model resolution', () => {
|
|
|
726
734
|
Reply with exactly: first-thread-msg
|
|
727
735
|
--- from: assistant (TestBot)
|
|
728
736
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
729
|
-
|
|
737
|
+
ok
|
|
730
738
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
731
739
|
--- from: user (agent-model-tester)
|
|
732
740
|
Reply with exactly: second-thread-msg
|
|
733
741
|
--- from: assistant (TestBot)
|
|
734
|
-
|
|
742
|
+
ok
|
|
735
743
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
736
744
|
`);
|
|
737
745
|
const secondMessages = await discord.thread(thread.id).getMessages();
|
|
@@ -780,7 +788,7 @@ describe('agent model resolution', () => {
|
|
|
780
788
|
"--- from: assistant (TestBot)
|
|
781
789
|
» **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-msg
|
|
782
790
|
-# *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
783
|
-
|
|
791
|
+
ok
|
|
784
792
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
785
793
|
`);
|
|
786
794
|
}, 20_000);
|
|
@@ -816,7 +824,7 @@ describe('agent model resolution', () => {
|
|
|
816
824
|
"--- from: assistant (TestBot)
|
|
817
825
|
» **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-variant-msg
|
|
818
826
|
-# *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
819
|
-
|
|
827
|
+
ok
|
|
820
828
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
821
829
|
`);
|
|
822
830
|
expect(sessionId ? await getSessionModel(sessionId) : undefined).toMatchInlineSnapshot(`
|
|
@@ -909,10 +917,10 @@ describe('agent model resolution', () => {
|
|
|
909
917
|
Reply with exactly: inline-existing-first-msg
|
|
910
918
|
--- from: assistant (TestBot)
|
|
911
919
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
912
|
-
|
|
920
|
+
ok
|
|
913
921
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
914
922
|
» **agent-model-tester** (plan): Reply with exactly: inline-existing-plan-msg
|
|
915
|
-
|
|
923
|
+
ok
|
|
916
924
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
917
925
|
`);
|
|
918
926
|
}, 20_000);
|
|
@@ -963,7 +971,7 @@ describe('agent model resolution', () => {
|
|
|
963
971
|
Reply with exactly: switch-in-thread-msg
|
|
964
972
|
--- from: assistant (TestBot)
|
|
965
973
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
966
|
-
|
|
974
|
+
ok
|
|
967
975
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
968
976
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
969
977
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
@@ -971,7 +979,7 @@ describe('agent model resolution', () => {
|
|
|
971
979
|
--- from: user (agent-model-tester)
|
|
972
980
|
Reply with exactly: after-switch-msg
|
|
973
981
|
--- from: assistant (TestBot)
|
|
974
|
-
|
|
982
|
+
ok
|
|
975
983
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
976
984
|
`);
|
|
977
985
|
const secondFooter = [...(await discord.thread(thread.id).getMessages())]
|
|
@@ -1022,7 +1030,7 @@ describe('agent model resolution', () => {
|
|
|
1022
1030
|
Reply with exactly: race-switch-first-msg
|
|
1023
1031
|
--- from: assistant (TestBot)
|
|
1024
1032
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
1025
|
-
|
|
1033
|
+
ok
|
|
1026
1034
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
1027
1035
|
--- from: user (agent-model-tester)
|
|
1028
1036
|
Reply with exactly: race-switch-follow-up
|
|
@@ -1030,7 +1038,7 @@ describe('agent model resolution', () => {
|
|
|
1030
1038
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
1031
1039
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
1032
1040
|
The agent will change on the next message.
|
|
1033
|
-
|
|
1041
|
+
ok
|
|
1034
1042
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
1035
1043
|
`);
|
|
1036
1044
|
const secondFooter = [...(await discord.thread(thread.id).getMessages())]
|
|
@@ -1072,7 +1080,7 @@ describe('agent model resolution', () => {
|
|
|
1072
1080
|
Reply with exactly: race-channel-follow-up
|
|
1073
1081
|
--- from: assistant (TestBot)
|
|
1074
1082
|
-# *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
1075
|
-
|
|
1083
|
+
ok
|
|
1076
1084
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
1077
1085
|
`);
|
|
1078
1086
|
const footer = [...(await discord.thread(thread.id).getMessages())]
|
|
@@ -1120,7 +1128,7 @@ describe('agent model resolution', () => {
|
|
|
1120
1128
|
Reply with exactly: refresh-agent-model-msg
|
|
1121
1129
|
--- from: assistant (TestBot)
|
|
1122
1130
|
-# *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
1123
|
-
|
|
1131
|
+
ok
|
|
1124
1132
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***
|
|
1125
1133
|
Using **plan** agent for this session
|
|
1126
1134
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
@@ -1162,7 +1170,7 @@ describe('agent model resolution', () => {
|
|
|
1162
1170
|
Reply with exactly: channel-vs-agent-msg
|
|
1163
1171
|
--- from: assistant (TestBot)
|
|
1164
1172
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
1165
|
-
|
|
1173
|
+
ok
|
|
1166
1174
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
1167
1175
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
1168
1176
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
@@ -1204,7 +1212,7 @@ describe('agent model resolution', () => {
|
|
|
1204
1212
|
Reply with exactly: plain-agent-override-msg
|
|
1205
1213
|
--- from: assistant (TestBot)
|
|
1206
1214
|
-# *using deterministic-provider/channel-model-v2*
|
|
1207
|
-
|
|
1215
|
+
ok
|
|
1208
1216
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*
|
|
1209
1217
|
Switched to **plain** agent for this session
|
|
1210
1218
|
Model: *deterministic-provider/channel-model-v2* (channel override)
|
package/dist/analytics.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Anonymous product analytics via Strada (
|
|
1
|
+
// Anonymous product analytics via Strada (@strada.sh/sdk, no dependencies).
|
|
2
2
|
// Tracks install-level usage only: no Discord IDs, paths, prompts, or secrets.
|
|
3
3
|
// A random install id is stored in {dataDir}/install-id for DAU-style queries.
|
|
4
4
|
//
|
|
@@ -158,6 +158,8 @@ export function initAnalytics() {
|
|
|
158
158
|
environment,
|
|
159
159
|
version: getKimakiVersion(),
|
|
160
160
|
userId: installId,
|
|
161
|
+
// Kimaki owns crash handling in discord-bot.ts; the SDK must not exit the process.
|
|
162
|
+
captureUncaughtErrors: false,
|
|
161
163
|
});
|
|
162
164
|
initialized = true;
|
|
163
165
|
}
|
|
@@ -6,10 +6,19 @@
|
|
|
6
6
|
// - punctuation + btw: ". btw", "! btw", ". btw.", "!btw."
|
|
7
7
|
// - btw as its own final line: "text\nbtw"
|
|
8
8
|
// Non-matches: "btw fix this" (start only), "hello btw" (no punctuation)
|
|
9
|
+
import { extractQueueSuffix } from './message-formatting.js';
|
|
9
10
|
const BTW_SUFFIX_RE = /(?:[.!?,;:])\s*btw\.?\s*$|\n\s*btw\.?\s*$/i;
|
|
11
|
+
const BTW_QUEUE_SUFFIX_RE = /(?:[.!?,;:])\s*btw\s+queue\.?\s*$|\n\s*btw\s+queue\.?\s*$/i;
|
|
10
12
|
export function extractBtwSuffix(content) {
|
|
11
13
|
if (!BTW_SUFFIX_RE.test(content)) {
|
|
12
14
|
return { prompt: content, forceBtw: false };
|
|
13
15
|
}
|
|
14
16
|
return { prompt: content.replace(BTW_SUFFIX_RE, '').trimEnd(), forceBtw: true };
|
|
15
17
|
}
|
|
18
|
+
export function extractBtwQueueSuffix(content) {
|
|
19
|
+
const queued = BTW_QUEUE_SUFFIX_RE.test(content)
|
|
20
|
+
? { prompt: content.replace(/\s+queue\.?\s*$/i, ''), forceQueue: true }
|
|
21
|
+
: extractQueueSuffix(content);
|
|
22
|
+
const btw = extractBtwSuffix(queued.prompt);
|
|
23
|
+
return { prompt: btw.prompt, forceQueue: queued.forceQueue, forceBtw: btw.forceBtw };
|
|
24
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import { extractBtwSuffix } from './btw-prefix-detection.js';
|
|
2
|
+
import { extractBtwSuffix, extractBtwQueueSuffix } from './btw-prefix-detection.js';
|
|
3
3
|
describe('extractBtwSuffix', () => {
|
|
4
4
|
test('matches after period', () => {
|
|
5
5
|
expect(extractBtwSuffix('fix the bug. btw')).toMatchInlineSnapshot(`
|
|
@@ -91,3 +91,40 @@ describe('extractBtwSuffix', () => {
|
|
|
91
91
|
`);
|
|
92
92
|
});
|
|
93
93
|
});
|
|
94
|
+
test('btw queue suffix keeps both actions in order', () => {
|
|
95
|
+
expect([
|
|
96
|
+
'Explain this. btw queue',
|
|
97
|
+
'Explain this. btw. queue',
|
|
98
|
+
'Explain this\nbtw queue',
|
|
99
|
+
'Explain this. btw',
|
|
100
|
+
'Explain this. queue',
|
|
101
|
+
].map(extractBtwQueueSuffix)).toMatchInlineSnapshot(`
|
|
102
|
+
[
|
|
103
|
+
{
|
|
104
|
+
"forceBtw": true,
|
|
105
|
+
"forceQueue": true,
|
|
106
|
+
"prompt": "Explain this",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"forceBtw": true,
|
|
110
|
+
"forceQueue": true,
|
|
111
|
+
"prompt": "Explain this",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"forceBtw": true,
|
|
115
|
+
"forceQueue": true,
|
|
116
|
+
"prompt": "Explain this",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"forceBtw": true,
|
|
120
|
+
"forceQueue": false,
|
|
121
|
+
"prompt": "Explain this",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"forceBtw": false,
|
|
125
|
+
"forceQueue": true,
|
|
126
|
+
"prompt": "Explain this",
|
|
127
|
+
},
|
|
128
|
+
]
|
|
129
|
+
`);
|
|
130
|
+
});
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import { goke } from 'goke';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { note } from '@clack/prompts';
|
|
9
|
-
import YAML from 'yaml';
|
|
10
9
|
import * as errore from 'errore';
|
|
11
10
|
import { Events, ActivityType, Routes } from 'discord.js';
|
|
12
11
|
import path from 'node:path';
|
|
@@ -24,7 +23,7 @@ import { WORKTREE_PREFIX } from '../commands/merge-worktree.js';
|
|
|
24
23
|
import { QUEUE_PREFIX } from '../message-formatting.js';
|
|
25
24
|
import { buildOpencodeEventLogLine } from '../session-handler/opencode-session-event-log.js';
|
|
26
25
|
import { createDiscordRest } from '../discord-urls.js';
|
|
27
|
-
import { archiveThread, ensureThreadMember, uploadFilesToDiscord, stripMentions } from '../discord-utils.js';
|
|
26
|
+
import { archiveThread, buildThreadStartEmbeds, ensureThreadMember, uploadFilesToDiscord, stripMentions } from '../discord-utils.js';
|
|
28
27
|
import { setDataDir, setProjectsDir, getDataDir, getProjectsDir } from '../config.js';
|
|
29
28
|
import { execAsync, resolveSessionWorkingDirectory, isGitRepositoryRoot } from '../worktrees.js';
|
|
30
29
|
import { upgrade, getCurrentVersion } from '../upgrade.js';
|
|
@@ -38,7 +37,7 @@ cli
|
|
|
38
37
|
.alias('start-session') // backwards compatibility
|
|
39
38
|
.option('-c, --channel <channelId>', 'Discord channel ID')
|
|
40
39
|
.option('-d, --project <path>', 'Project directory (alternative to --channel)')
|
|
41
|
-
.option('-p, --prompt <prompt>', 'Message content')
|
|
40
|
+
.option('-p, --prompt <prompt>', 'Message content. With --thread/--session, end with ". queue" to wait for the current run instead of interrupting it, ". btw" to fork a side session now, or ". btw queue" to fork after the current run')
|
|
42
41
|
.option('-n, --name [name]', 'Thread name (optional, defaults to prompt preview)')
|
|
43
42
|
.option('-a, --app-id [appId]', 'Bot application ID (required if no local database)')
|
|
44
43
|
.option('--notify-only', 'Create notification thread without starting AI session')
|
|
@@ -56,7 +55,7 @@ cli
|
|
|
56
55
|
.option('--send-at <schedule>', 'Schedule send for future (UTC ISO date/time ending in Z, or cron expression)')
|
|
57
56
|
.option('--pre-run <command>', 'Run a shell command in the project before starting a scheduled task')
|
|
58
57
|
.option('--allow-concurrency', 'Allow concurrent sessions from the same scheduled task')
|
|
59
|
-
.option('--thread <threadId>', 'Post prompt to an existing thread')
|
|
58
|
+
.option('--thread <threadId>', 'Post prompt to an existing thread. Interrupts a busy session unless the prompt ends with ". queue"')
|
|
60
59
|
.option('--session <sessionId>', 'Post prompt to thread mapped to an existing session')
|
|
61
60
|
.option('--parent-session <sessionId>', 'Parent OpenCode session ID for newly created child sessions')
|
|
62
61
|
.option('--wait', 'Wait for session to complete, then print session text to stdout')
|
|
@@ -405,12 +404,7 @@ cli
|
|
|
405
404
|
...(options.injectionGuard?.length ? { injectionGuardPatterns: options.injectionGuard } : {}),
|
|
406
405
|
...(options.parentSession && { parentSessionId: options.parentSession }),
|
|
407
406
|
};
|
|
408
|
-
const promptEmbed =
|
|
409
|
-
{
|
|
410
|
-
color: 0x2b2d31,
|
|
411
|
-
footer: { text: YAML.stringify(threadPromptMarker) },
|
|
412
|
-
},
|
|
413
|
-
];
|
|
407
|
+
const promptEmbed = await buildThreadStartEmbeds(threadPromptMarker);
|
|
414
408
|
// Prefix the prompt so it's clear who sent it (matches /queue format).
|
|
415
409
|
// Use a newline between prefix and prompt so leading /command
|
|
416
410
|
// detection can find the command on its own line.
|
|
@@ -584,7 +578,7 @@ cli
|
|
|
584
578
|
...(options.parentSession && { parentSessionId: options.parentSession }),
|
|
585
579
|
};
|
|
586
580
|
const autoStartEmbed = embedMarker
|
|
587
|
-
?
|
|
581
|
+
? await buildThreadStartEmbeds(embedMarker)
|
|
588
582
|
: undefined;
|
|
589
583
|
const starterMessage = await sendDiscordMessageWithOptionalAttachment({
|
|
590
584
|
channelId,
|
package/dist/cli-runner.js
CHANGED
|
@@ -9,6 +9,7 @@ import * as dbSchema from './schema.js';
|
|
|
9
9
|
import { selectResolvedCommand } from './opencode-command.js';
|
|
10
10
|
import { Events, ChannelType, Routes, AttachmentBuilder, } from 'discord.js';
|
|
11
11
|
import { discordApiUrl, getDiscordRestApiUrl, getGatewayProxyRestBaseUrl, getInternetReachableBaseUrl } from './discord-urls.js';
|
|
12
|
+
import { extractBtwQueueSuffix } from './btw-prefix-detection.js';
|
|
12
13
|
import crypto from 'node:crypto';
|
|
13
14
|
import path from 'node:path';
|
|
14
15
|
import fs from 'node:fs';
|
|
@@ -160,6 +161,18 @@ function wrapPromptAttachmentText(prompt) {
|
|
|
160
161
|
})
|
|
161
162
|
.join('\n');
|
|
162
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Visible message content + prompt.md text for prompts over Discord's limit.
|
|
166
|
+
* The bot reads queue/btw suffixes only from message content, so the suffix
|
|
167
|
+
* moves from the file to the visible message and the send still queues or forks.
|
|
168
|
+
*/
|
|
169
|
+
export function buildLongPromptMessage(prompt) {
|
|
170
|
+
const { prompt: fileText, forceQueue, forceBtw } = extractBtwQueueSuffix(prompt);
|
|
171
|
+
const preview = fileText.slice(0, 100).replace(/\n/g, ' ');
|
|
172
|
+
const summary = `Prompt attached as file (${fileText.length} chars)\n\n> ${preview}…`;
|
|
173
|
+
const suffix = [forceBtw && 'btw', forceQueue && 'queue'].filter(Boolean).join(' ');
|
|
174
|
+
return { content: suffix ? `${summary}\n\n${suffix}` : summary, fileText };
|
|
175
|
+
}
|
|
163
176
|
function promptAttachmentBlob(text) {
|
|
164
177
|
return new Blob([new Uint8Array(Buffer.from(text, 'utf8'))], {
|
|
165
178
|
type: 'text/markdown',
|
|
@@ -182,18 +195,18 @@ export async function sendDiscordMessageWithOptionalAttachment({ channelId, prom
|
|
|
182
195
|
// When prompt exceeds Discord's limit, attach it as prompt.md alongside
|
|
183
196
|
// user files so nothing is silently lost. Build prompt.md from memory so
|
|
184
197
|
// parallel kimaki send processes never share or unlink a temp path.
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
198
|
+
const longPrompt = prompt.length > discordMaxLength
|
|
199
|
+
? buildLongPromptMessage(prompt)
|
|
200
|
+
: undefined;
|
|
201
|
+
const content = longPrompt ? longPrompt.content : prompt;
|
|
189
202
|
const allFiles = files.map((file) => ({
|
|
190
203
|
data: new Uint8Array(fs.readFileSync(file)),
|
|
191
204
|
filename: path.basename(file),
|
|
192
205
|
mimeType: mime.getType(file) || 'application/octet-stream',
|
|
193
206
|
}));
|
|
194
|
-
if (
|
|
207
|
+
if (longPrompt) {
|
|
195
208
|
allFiles.push({
|
|
196
|
-
data: new Uint8Array(Buffer.from(
|
|
209
|
+
data: new Uint8Array(Buffer.from(longPrompt.fileText, 'utf8')),
|
|
197
210
|
filename: 'prompt.md',
|
|
198
211
|
mimeType: 'text/markdown',
|
|
199
212
|
});
|
|
@@ -259,17 +272,16 @@ export async function sendDiscordMessageWithOptionalAttachment({ channelId, prom
|
|
|
259
272
|
}
|
|
260
273
|
return firstMessage;
|
|
261
274
|
}
|
|
262
|
-
const
|
|
263
|
-
const summaryContent = `Prompt attached as file (${prompt.length} chars)\n\n> ${preview}...`;
|
|
275
|
+
const longPrompt = buildLongPromptMessage(prompt);
|
|
264
276
|
// In-memory Blob only — no temp file. Parallel send must never share a path.
|
|
265
277
|
const formData = new FormData();
|
|
266
278
|
formData.append('payload_json', JSON.stringify({
|
|
267
|
-
content:
|
|
279
|
+
content: longPrompt.content,
|
|
268
280
|
attachments: [{ id: 0, filename: 'prompt.md' }],
|
|
269
281
|
embeds,
|
|
270
282
|
allowed_mentions: { parse: store.getState().allowedMentions },
|
|
271
283
|
}));
|
|
272
|
-
formData.append('files[0]', promptAttachmentBlob(wrapPromptAttachmentText(
|
|
284
|
+
formData.append('files[0]', promptAttachmentBlob(wrapPromptAttachmentText(longPrompt.fileText)), 'prompt.md');
|
|
273
285
|
const starterMessageResponse = await fetch(discordApiUrl(`/channels/${channelId}/messages`), {
|
|
274
286
|
method: 'POST',
|
|
275
287
|
headers: {
|
package/dist/cli-runner.test.js
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
2
|
import { REST } from 'discord.js';
|
|
3
|
-
import { getOpenUrlCommand, isTransientNetworkError, resolveDiscordUserOption, } from './cli-runner.js';
|
|
3
|
+
import { buildLongPromptMessage, getOpenUrlCommand, isTransientNetworkError, resolveDiscordUserOption, } from './cli-runner.js';
|
|
4
|
+
import { extractBtwQueueSuffix } from './btw-prefix-detection.js';
|
|
5
|
+
// Regression: a long `. btw` prompt lost its suffix and interrupted the source session.
|
|
6
|
+
test('long prompts keep queue and btw suffixes in the visible message', () => {
|
|
7
|
+
const body = 'x'.repeat(2100);
|
|
8
|
+
expect(['queue', 'btw', 'btw queue', ''].map((suffix) => {
|
|
9
|
+
const { content, fileText } = buildLongPromptMessage(suffix ? `${body}. ${suffix}` : body);
|
|
10
|
+
const parsed = extractBtwQueueSuffix(content);
|
|
11
|
+
return { fileText: fileText === body, forceQueue: parsed.forceQueue, forceBtw: parsed.forceBtw };
|
|
12
|
+
})).toMatchInlineSnapshot(`
|
|
13
|
+
[
|
|
14
|
+
{
|
|
15
|
+
"fileText": true,
|
|
16
|
+
"forceBtw": false,
|
|
17
|
+
"forceQueue": true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"fileText": true,
|
|
21
|
+
"forceBtw": true,
|
|
22
|
+
"forceQueue": false,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"fileText": true,
|
|
26
|
+
"forceBtw": true,
|
|
27
|
+
"forceQueue": true,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"fileText": true,
|
|
31
|
+
"forceBtw": false,
|
|
32
|
+
"forceQueue": false,
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
`);
|
|
36
|
+
});
|
|
4
37
|
test('raw Discord user ID does not invent a username', async () => {
|
|
5
38
|
const user = await resolveDiscordUserOption({
|
|
6
39
|
user: '535922349652836367',
|
|
@@ -320,7 +320,7 @@ describe('kimaki send --channel thread creation', () => {
|
|
|
320
320
|
thread empty content seed
|
|
321
321
|
--- from: assistant (TestBot)
|
|
322
322
|
-# *using deterministic-provider/deterministic-v2*
|
|
323
|
-
|
|
323
|
+
caught-by-model
|
|
324
324
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
325
325
|
--- from: user (cli-send-tester)
|
|
326
326
|
--- from: assistant (TestBot)
|
package/dist/commands/abort.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// /abort command - Abort the current OpenCode request in this thread.
|
|
2
2
|
import { ChannelType, MessageFlags, } from 'discord.js';
|
|
3
|
-
import { cancelSessionSleepForThread, getThreadSession } from '../database.js';
|
|
3
|
+
import { cancelSessionSleepForThread, deleteThreadQueueItems, getThreadSession, } from '../database.js';
|
|
4
4
|
import { getOpencodeClient, initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
5
|
import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
6
6
|
import { getRuntime } from '../session-handler/thread-session-runtime.js';
|
|
@@ -45,12 +45,21 @@ export async function handleAbortCommand({ command, }) {
|
|
|
45
45
|
// through enqueueIncoming, so cancel any pending sleep here too. Otherwise the
|
|
46
46
|
// wake would still fire later and restart a session the user just stopped.
|
|
47
47
|
await cancelSessionSleepForThread({ threadId: channel.id });
|
|
48
|
-
// abortActiveRun delegates to session.abort()
|
|
48
|
+
// abortActiveRun delegates to session.abort() and clears the /queue, so
|
|
49
|
+
// queued messages are not sent after abort or restored after a restart.
|
|
49
50
|
const runtime = getRuntime(channel.id);
|
|
51
|
+
let clearedCount = 0;
|
|
50
52
|
if (runtime) {
|
|
51
|
-
runtime.abortActiveRun('user-requested');
|
|
53
|
+
const cleared = await runtime.abortActiveRun('user-requested');
|
|
54
|
+
clearedCount = cleared.length;
|
|
52
55
|
}
|
|
53
56
|
else {
|
|
57
|
+
const deleteResult = await deleteThreadQueueItems(channel.id).catch((error) => {
|
|
58
|
+
return new Error('Failed to clear persisted queue', { cause: error });
|
|
59
|
+
});
|
|
60
|
+
if (deleteResult instanceof Error) {
|
|
61
|
+
logger.error(`[ABORT] ${deleteResult.message}:`, deleteResult.cause);
|
|
62
|
+
}
|
|
54
63
|
// No runtime but session exists — fall back to direct API abort
|
|
55
64
|
const serverResult = await initializeOpencodeForDirectory(projectDirectory);
|
|
56
65
|
if (serverResult instanceof Error) {
|
|
@@ -67,6 +76,9 @@ export async function handleAbortCommand({ command, }) {
|
|
|
67
76
|
logger.error('[ABORT] API abort failed:', error);
|
|
68
77
|
}
|
|
69
78
|
}
|
|
70
|
-
|
|
79
|
+
const queueNote = clearedCount > 0
|
|
80
|
+
? `, cleared ${clearedCount} queued message${clearedCount > 1 ? 's' : ''}`
|
|
81
|
+
: '';
|
|
82
|
+
await command.editReply(`Request **aborted**${queueNote}`);
|
|
71
83
|
logger.log(`Session ${sessionId} aborted by user`);
|
|
72
84
|
}
|