kimaki 1.4.0 → 1.6.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/assets/fonts/Geist-Bold.ttf +0 -0
- package/assets/fonts/Geist-Regular.ttf +0 -0
- package/assets/fonts/GeistMono-Regular.ttf +0 -0
- package/assets/fonts/OFL.txt +92 -0
- package/dist/agent-ui.e2e.test.js +27 -13
- package/dist/agent-ui.e2e.test.js.map +1 -1
- package/dist/agent-ui.js +2 -1
- package/dist/agent-ui.js.map +1 -1
- package/dist/analytics.e2e.test.js +1 -1
- package/dist/analytics.e2e.test.js.map +1 -1
- package/dist/autostart.d.ts +4 -0
- package/dist/autostart.js +55 -10
- package/dist/autostart.js.map +1 -1
- package/dist/bot.d.ts +10 -1
- package/dist/bot.js +14 -6
- package/dist/bot.js.map +1 -1
- package/dist/btw.e2e.test.js +3 -3
- package/dist/builtin-skills.d.ts +1 -1
- package/dist/builtin-skills.js +1 -1
- package/dist/builtin-skills.js.map +1 -1
- package/dist/builtin-skills.test.js +3 -4
- package/dist/builtin-skills.test.js.map +1 -1
- package/dist/catalog-refresh.e2e.test.js +2 -2
- package/dist/cli/bot.js +25 -14
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli/discord.js +32 -6
- package/dist/cli/discord.js.map +1 -1
- package/dist/cli/schedule.js +23 -9
- package/dist/cli/schedule.js.map +1 -1
- package/dist/cli/session.js +37 -12
- package/dist/cli/session.js.map +1 -1
- package/dist/cli/shared.d.ts +5 -3
- package/dist/cli/shared.js +45 -10
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli-discord.e2e.test.js +13 -8
- package/dist/cli-discord.e2e.test.js.map +1 -1
- package/dist/cli-lock.e2e.test.js +87 -20
- package/dist/cli-lock.e2e.test.js.map +1 -1
- package/dist/cli-read.e2e.test.js +18 -9
- package/dist/cli-read.e2e.test.js.map +1 -1
- package/dist/cli-sessions.e2e.test.js +4 -3
- package/dist/cli-sessions.e2e.test.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/preference-commands.d.ts +4 -0
- package/dist/commands/preference-commands.js +38 -4
- package/dist/commands/preference-commands.js.map +1 -1
- package/dist/commands/session-commands.js +11 -8
- package/dist/commands/session-commands.js.map +1 -1
- package/dist/commands/thread-commands.js +94 -24
- package/dist/commands/thread-commands.js.map +1 -1
- package/dist/commands/worktree-commands.d.ts +4 -5
- package/dist/commands/worktree-commands.js +8 -7
- package/dist/commands/worktree-commands.js.map +1 -1
- package/dist/commands.e2e.test.js +25 -21
- package/dist/commands.e2e.test.js.map +1 -1
- package/dist/coordinator-agents.e2e.test.js +135 -0
- package/dist/coordinator-agents.e2e.test.js.map +1 -0
- package/dist/coordinator-idle.e2e.test.js +100 -0
- package/dist/coordinator-idle.e2e.test.js.map +1 -0
- package/dist/coordinator-queue.e2e.test.js +244 -0
- package/dist/coordinator-queue.e2e.test.js.map +1 -0
- package/dist/coordinator-warmup.e2e.test.d.ts +1 -0
- package/dist/coordinator-warmup.e2e.test.js +131 -0
- package/dist/coordinator-warmup.e2e.test.js.map +1 -0
- package/dist/coordinator.d.ts +6 -0
- package/dist/coordinator.e2e.test.d.ts +1 -0
- package/dist/coordinator.e2e.test.js +505 -0
- package/dist/coordinator.e2e.test.js.map +1 -0
- package/dist/coordinator.js +164 -0
- package/dist/coordinator.js.map +1 -0
- package/dist/db.d.ts +75 -0
- package/dist/db.test.js +1 -0
- package/dist/db.test.js.map +1 -1
- package/dist/diagram.d.ts +11 -0
- package/dist/diagram.js +232 -0
- package/dist/diagram.js.map +1 -0
- package/dist/diagram.test.d.ts +1 -0
- package/dist/diagram.test.js +52 -0
- package/dist/diagram.test.js.map +1 -0
- package/dist/duration.js +13 -5
- package/dist/duration.js.map +1 -1
- package/dist/effects.d.ts +6 -1
- package/dist/effects.js +97 -26
- package/dist/effects.js.map +1 -1
- package/dist/event-loop.d.ts +1 -1
- package/dist/event-loop.js +73 -43
- package/dist/event-loop.js.map +1 -1
- package/dist/format-parts.d.ts +21 -4
- package/dist/format-parts.js +40 -23
- package/dist/format-parts.js.map +1 -1
- package/dist/gateway-replay.e2e.test.js +4 -4
- package/dist/harness.e2e.test.js +2 -2
- package/dist/harness.e2e.test.js.map +1 -1
- package/dist/hello-thread.e2e.test.js +56 -5
- package/dist/hello-thread.e2e.test.js.map +1 -1
- package/dist/ingress.js +68 -13
- package/dist/ingress.js.map +1 -1
- package/dist/interaction-context.d.ts +4 -4
- package/dist/interaction-context.js +9 -7
- package/dist/interaction-context.js.map +1 -1
- package/dist/interrupt.e2e.test.js +8 -5
- package/dist/interrupt.e2e.test.js.map +1 -1
- package/dist/legacy.e2e.test.js +1 -1
- package/dist/lock-routes.d.ts +68 -16
- package/dist/lock-routes.js +68 -8
- package/dist/lock-routes.js.map +1 -1
- package/dist/lock-server.d.ts +3 -2
- package/dist/lock-server.js +23 -7
- package/dist/lock-server.js.map +1 -1
- package/dist/main.d.ts +4 -2
- package/dist/main.js +13 -3
- package/dist/main.js.map +1 -1
- package/dist/markdown/markdown.test.js +4 -4
- package/dist/markdown/render-markdown.js +6 -5
- package/dist/markdown/render-markdown.js.map +1 -1
- package/dist/onboarding-cli.e2e.test.js +7 -6
- package/dist/onboarding-cli.e2e.test.js.map +1 -1
- package/dist/onboarding.d.ts +3 -10
- package/dist/onboarding.e2e.test.js +8 -10
- package/dist/onboarding.e2e.test.js.map +1 -1
- package/dist/onboarding.js +14 -24
- package/dist/onboarding.js.map +1 -1
- package/dist/permissions.e2e.test.js +3 -3
- package/dist/plugin/coordinator.d.ts +4 -0
- package/dist/plugin/coordinator.js +389 -0
- package/dist/plugin/coordinator.js.map +1 -0
- package/dist/plugin/index.js +76 -17
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin.e2e.test.js +1 -1
- package/dist/preference-commands.e2e.test.js +103 -24
- package/dist/preference-commands.e2e.test.js.map +1 -1
- package/dist/prompt.d.ts +21 -8
- package/dist/prompt.js +64 -48
- package/dist/prompt.js.map +1 -1
- package/dist/questions.e2e.test.js +13 -13
- package/dist/queue-sources.e2e.test.d.ts +1 -0
- package/dist/queue-sources.e2e.test.js +168 -0
- package/dist/queue-sources.e2e.test.js.map +1 -0
- package/dist/queue.d.ts +9 -1
- package/dist/queue.e2e.test.js +48 -30
- package/dist/queue.e2e.test.js.map +1 -1
- package/dist/queue.js +97 -24
- package/dist/queue.js.map +1 -1
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +57 -0
- package/dist/queue.test.js.map +1 -0
- package/dist/remote-send.e2e.test.js +6 -6
- package/dist/remote-send.js +2 -1
- package/dist/remote-send.js.map +1 -1
- package/dist/renderer.e2e.test.js +7 -7
- package/dist/restart.e2e.test.js +2 -2
- package/dist/routines.e2e.test.js +2 -2
- package/dist/scheduler.d.ts +1 -1
- package/dist/scheduler.e2e.test.js +43 -20
- package/dist/scheduler.e2e.test.js.map +1 -1
- package/dist/scheduler.js +4 -2
- package/dist/scheduler.js.map +1 -1
- package/dist/schema-sql.d.ts +1 -1
- package/dist/schema-sql.js +1 -1
- package/dist/schema-sql.js.map +1 -1
- package/dist/schema.d.ts +146 -0
- package/dist/schema.js +12 -0
- package/dist/schema.js.map +1 -1
- package/dist/session-commands.e2e.test.js +36 -11
- package/dist/session-commands.e2e.test.js.map +1 -1
- package/dist/session-events.d.ts +19 -7
- package/dist/session-events.js +166 -22
- package/dist/session-events.js.map +1 -1
- package/dist/session-ops.e2e.test.js +88 -11
- package/dist/session-ops.e2e.test.js.map +1 -1
- package/dist/sessions.d.ts +41 -18
- package/dist/sessions.js +174 -63
- package/dist/sessions.js.map +1 -1
- package/dist/shell.e2e.test.js +4 -4
- package/dist/slash-commands.js +12 -6
- package/dist/slash-commands.js.map +1 -1
- package/dist/slash-commands.test.js +2 -0
- package/dist/slash-commands.test.js.map +1 -1
- package/dist/sleep.e2e.test.js +29 -14
- package/dist/sleep.e2e.test.js.map +1 -1
- package/dist/sleeps.d.ts +1 -0
- package/dist/sleeps.js +21 -3
- package/dist/sleeps.js.map +1 -1
- package/dist/store.d.ts +1 -0
- package/dist/store.js +3 -2
- package/dist/store.js.map +1 -1
- package/dist/system-prompt.d.ts +3 -1
- package/dist/system-prompt.js +138 -64
- package/dist/system-prompt.js.map +1 -1
- package/dist/test/generate-log-plugin/index.js +15 -0
- package/dist/test/generate-log-plugin/index.js.map +1 -0
- package/dist/test/harness.d.ts +20 -1
- package/dist/test/harness.js +51 -10
- package/dist/test/harness.js.map +1 -1
- package/dist/test/replay.js +11 -7
- package/dist/test/replay.js.map +1 -1
- package/dist/thread-reducer.d.ts +29 -3
- package/dist/thread-reducer.fixtures.test.js +58 -51
- package/dist/thread-reducer.fixtures.test.js.map +1 -1
- package/dist/thread-reducer.js +112 -25
- package/dist/thread-reducer.js.map +1 -1
- package/dist/thread-reducer.test.js +197 -24
- package/dist/thread-reducer.test.js.map +1 -1
- package/dist/voice-attachments.e2e.test.js +108 -18
- package/dist/voice-attachments.e2e.test.js.map +1 -1
- package/dist/voice-call.e2e.test.js +9 -9
- package/dist/voice-call.js +6 -6
- package/dist/voice-call.js.map +1 -1
- package/dist/voice-openai.e2e.test.js +59 -23
- package/dist/voice-openai.e2e.test.js.map +1 -1
- package/dist/voice.js +80 -24
- package/dist/voice.js.map +1 -1
- package/dist/working-directory.e2e.test.js +11 -11
- package/dist/working-directory.e2e.test.js.map +1 -1
- package/package.json +12 -6
- package/src/agent-ui.e2e.test.ts +27 -13
- package/src/agent-ui.ts +2 -1
- package/src/analytics.e2e.test.ts +1 -1
- package/src/autostart.ts +55 -10
- package/src/bot.ts +29 -5
- package/src/btw.e2e.test.ts +3 -3
- package/src/builtin-skills.test.ts +3 -4
- package/src/builtin-skills.ts +1 -1
- package/src/catalog-refresh.e2e.test.ts +2 -2
- package/src/cli/bot.ts +23 -14
- package/src/cli/discord.ts +30 -6
- package/src/cli/schedule.ts +23 -9
- package/src/cli/session.ts +35 -12
- package/src/cli/shared.ts +43 -8
- package/src/cli-discord.e2e.test.ts +13 -8
- package/src/cli-lock.e2e.test.ts +88 -20
- package/src/cli-read.e2e.test.ts +17 -8
- package/src/cli-sessions.e2e.test.ts +4 -3
- package/src/cli.ts +2 -0
- package/src/commands/preference-commands.ts +38 -4
- package/src/commands/session-commands.ts +12 -7
- package/src/commands/thread-commands.ts +92 -24
- package/src/commands/worktree-commands.ts +10 -9
- package/src/commands.e2e.test.ts +25 -21
- package/src/coordinator-agents.e2e.test.ts +168 -0
- package/src/coordinator-idle.e2e.test.ts +122 -0
- package/src/coordinator-queue.e2e.test.ts +279 -0
- package/src/coordinator-warmup.e2e.test.ts +158 -0
- package/src/coordinator.e2e.test.ts +545 -0
- package/src/coordinator.ts +168 -0
- package/src/db.test.ts +1 -0
- package/src/diagram.test.ts +56 -0
- package/src/diagram.ts +243 -0
- package/src/duration.ts +13 -5
- package/src/effects.ts +99 -27
- package/src/event-loop.ts +71 -42
- package/src/format-parts.ts +53 -22
- package/src/gateway-replay.e2e.test.ts +4 -4
- package/src/harness.e2e.test.ts +2 -2
- package/src/hello-thread.e2e.test.ts +59 -5
- package/src/ingress.ts +62 -12
- package/src/interaction-context.ts +12 -11
- package/src/interrupt.e2e.test.ts +8 -5
- package/src/legacy.e2e.test.ts +1 -1
- package/src/lock-routes.ts +62 -8
- package/src/lock-server.ts +23 -7
- package/src/main.ts +22 -7
- package/src/markdown/markdown.test.ts +4 -4
- package/src/markdown/render-markdown.ts +6 -5
- package/src/onboarding-cli.e2e.test.ts +7 -6
- package/src/onboarding.e2e.test.ts +8 -10
- package/src/onboarding.ts +13 -29
- package/src/permissions.e2e.test.ts +3 -3
- package/src/plugin/coordinator.ts +401 -0
- package/src/plugin/index.ts +68 -15
- package/src/plugin.e2e.test.ts +1 -1
- package/src/preference-commands.e2e.test.ts +103 -23
- package/src/prompt.ts +87 -43
- package/src/questions.e2e.test.ts +13 -13
- package/src/queue-sources.e2e.test.ts +197 -0
- package/src/queue.e2e.test.ts +48 -30
- package/src/queue.test.ts +59 -0
- package/src/queue.ts +104 -24
- package/src/remote-send.e2e.test.ts +6 -6
- package/src/remote-send.ts +2 -1
- package/src/renderer.e2e.test.ts +7 -7
- package/src/restart.e2e.test.ts +2 -2
- package/src/routines.e2e.test.ts +2 -2
- package/src/scheduler.e2e.test.ts +45 -19
- package/src/scheduler.ts +4 -2
- package/src/schema-sql.ts +1 -1
- package/src/schema.ts +13 -0
- package/src/session-commands.e2e.test.ts +38 -11
- package/src/session-events.ts +160 -21
- package/src/session-ops.e2e.test.ts +90 -10
- package/src/sessions.ts +214 -64
- package/src/shell.e2e.test.ts +4 -4
- package/src/slash-commands.test.ts +2 -0
- package/src/slash-commands.ts +12 -6
- package/src/sleep.e2e.test.ts +30 -13
- package/src/sleeps.ts +20 -3
- package/src/store.ts +5 -2
- package/src/system-prompt.ts +143 -63
- package/src/test/generate-log-plugin/index.ts +16 -0
- package/src/test/harness.ts +77 -14
- package/src/test/replay.ts +12 -6
- package/src/thread-reducer.fixtures.test.ts +58 -51
- package/src/thread-reducer.test.ts +207 -26
- package/src/thread-reducer.ts +138 -37
- package/src/voice-attachments.e2e.test.ts +110 -18
- package/src/voice-call.e2e.test.ts +9 -9
- package/src/voice-call.ts +6 -6
- package/src/voice-openai.e2e.test.ts +64 -24
- package/src/voice.ts +101 -26
- package/src/working-directory.e2e.test.ts +11 -11
- package/dist/message-tool.e2e.test.js +0 -39
- package/dist/message-tool.e2e.test.js.map +0 -1
- package/dist/plugin/read-media.d.ts +0 -23
- package/dist/plugin/read-media.js +0 -72
- package/dist/plugin/read-media.js.map +0 -1
- package/dist/plugin/session-id/index.js +0 -18
- package/dist/plugin/session-id/index.js.map +0 -1
- package/dist/probe-live.tmp.js +0 -82
- package/dist/probe-live.tmp.js.map +0 -1
- package/dist/tasks.e2e.test.js +0 -173
- package/dist/tasks.e2e.test.js.map +0 -1
- /package/dist/{message-tool.e2e.test.d.ts → coordinator-agents.e2e.test.d.ts} +0 -0
- /package/dist/{probe-live.tmp.d.ts → coordinator-idle.e2e.test.d.ts} +0 -0
- /package/dist/{tasks.e2e.test.d.ts → coordinator-queue.e2e.test.d.ts} +0 -0
- /package/dist/{plugin/session-id → test/generate-log-plugin}/index.d.ts +0 -0
package/src/harness.e2e.test.ts
CHANGED
|
@@ -90,7 +90,7 @@ test('a second bot on the same lock port stops the running one and takes over',
|
|
|
90
90
|
import { disabledAnalytics } from ${JSON.stringify(path.resolve('src/analytics.ts'))}
|
|
91
91
|
const lock = await startLockServer({ port: ${port}, dataDir: ${JSON.stringify(otherData)} })
|
|
92
92
|
if (lock instanceof Error) throw lock
|
|
93
|
-
const result = await startBot({ analytics: disabledAnalytics, lock, ...${JSON.stringify({ dataDir: otherData, token: twin.discord.botToken, appId: twin.discord.botUserId, discordRestUrl: twin.discord.restUrl, opencodeServiceFile: server.serviceFile, opencodeConfigDir: server.configDir, opencodeDbFile: server.dbFile, ensureOpencode: false,
|
|
93
|
+
const result = await startBot({ analytics: disabledAnalytics, lock, ...${JSON.stringify({ dataDir: otherData, token: twin.discord.botToken, appId: twin.discord.botUserId, discordRestUrl: twin.discord.restUrl, opencodeServiceFile: server.serviceFile, opencodeConfigDir: server.configDir, opencodeDbFile: server.dbFile, ensureOpencode: false, kimakiArgv: ['kimaki'] })} })
|
|
94
94
|
if (result instanceof Error) throw result
|
|
95
95
|
process.send({ ready: true })
|
|
96
96
|
process.once('SIGTERM', async () => { await result.stop(); process.exit(0) })
|
|
@@ -119,6 +119,6 @@ test('a second bot on the same lock port stops the running one and takes over',
|
|
|
119
119
|
--- from: assistant (TestBot)
|
|
120
120
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
121
121
|
ok
|
|
122
|
-
-# *
|
|
122
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*"
|
|
123
123
|
`)
|
|
124
124
|
})
|
|
@@ -29,6 +29,8 @@ let bot: BotHandle
|
|
|
29
29
|
let dataDir: string
|
|
30
30
|
let helloThreadId: string
|
|
31
31
|
|
|
32
|
+
const MEDIA_URL = 'https://cdn.discordapp.com/attachments/1012345678900020080/1234567891233211234/shot.png?ex=65d903de&is=65c68ede&hm=2481f30d&'
|
|
33
|
+
|
|
32
34
|
beforeAll(async () => {
|
|
33
35
|
dataDir = tempDataDir()
|
|
34
36
|
;[server, twin] = await Promise.all([
|
|
@@ -60,6 +62,19 @@ beforeAll(async () => {
|
|
|
60
62
|
],
|
|
61
63
|
},
|
|
62
64
|
},
|
|
65
|
+
{
|
|
66
|
+
id: 'media',
|
|
67
|
+
priority: 10,
|
|
68
|
+
when: { latestUserTextIncludes: 'media-marker' },
|
|
69
|
+
then: {
|
|
70
|
+
parts: [
|
|
71
|
+
{ type: 'text-start', id: 't3' },
|
|
72
|
+
{ type: 'text-delta', id: 't3', delta: `Here is the screenshot: ${MEDIA_URL}` },
|
|
73
|
+
{ type: 'text-end', id: 't3' },
|
|
74
|
+
{ type: 'finish', finishReason: 'stop', usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 } },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
63
78
|
],
|
|
64
79
|
}),
|
|
65
80
|
startTwin(),
|
|
@@ -106,12 +121,12 @@ test('channel message creates a thread with reply and footer, follow-up continue
|
|
|
106
121
|
--- from: assistant (TestBot)
|
|
107
122
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
108
123
|
Hello from the deterministic model.
|
|
109
|
-
-# *
|
|
124
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
110
125
|
--- from: user (tommy)
|
|
111
126
|
Again please second-marker
|
|
112
127
|
--- from: assistant (TestBot)
|
|
113
128
|
Second answer, same session.
|
|
114
|
-
-# *
|
|
129
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
115
130
|
`)
|
|
116
131
|
expect(thread.name).toBe('Say hello hello-marker')
|
|
117
132
|
|
|
@@ -139,7 +154,7 @@ test('channel message creates a thread with reply and footer, follow-up continue
|
|
|
139
154
|
{
|
|
140
155
|
"silent": false,
|
|
141
156
|
"suppressEmbeds": true,
|
|
142
|
-
"text": "-# *
|
|
157
|
+
"text": "-# *main ⋅ <1s ⋅ 0% ",
|
|
143
158
|
},
|
|
144
159
|
{
|
|
145
160
|
"silent": true,
|
|
@@ -149,7 +164,7 @@ test('channel message creates a thread with reply and footer, follow-up continue
|
|
|
149
164
|
{
|
|
150
165
|
"silent": false,
|
|
151
166
|
"suppressEmbeds": true,
|
|
152
|
-
"text": "-# *
|
|
167
|
+
"text": "-# *main ⋅ <1s ⋅ 0% ",
|
|
153
168
|
},
|
|
154
169
|
]
|
|
155
170
|
`)
|
|
@@ -170,6 +185,45 @@ test('channel message creates a thread with reply and footer, follow-up continue
|
|
|
170
185
|
expect(session.time.idle).toBeGreaterThan(0)
|
|
171
186
|
})
|
|
172
187
|
|
|
188
|
+
test('a reply with a Discord media URL keeps its embed, other lines stay suppressed', async () => {
|
|
189
|
+
const { discord, channelId } = twin
|
|
190
|
+
await discord.channel(channelId).user(TEST_USER_ID).sendMessage({ content: 'Show the shot media-marker' })
|
|
191
|
+
const thread = await discord.channel(channelId).waitForThread({ predicate: (thread) => thread.name?.startsWith('Show the shot') ?? false })
|
|
192
|
+
await waitForFooter({ discord, threadId: thread.id })
|
|
193
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
194
|
+
"--- from: user (tommy)
|
|
195
|
+
Show the shot media-marker
|
|
196
|
+
--- from: assistant (TestBot)
|
|
197
|
+
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
198
|
+
Here is the screenshot: https://cdn.discordapp.com/attachments/1012345678900020080/1234567891233211234/shot.png?ex=65d903de&is=65c68ede&hm=2481f30d&
|
|
199
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
200
|
+
`)
|
|
201
|
+
|
|
202
|
+
const messages = await discord.thread(thread.id).getMessages()
|
|
203
|
+
const flags = messages
|
|
204
|
+
.filter((message) => message.author.bot)
|
|
205
|
+
.map((message) => ({
|
|
206
|
+
text: message.content.slice(0, 20),
|
|
207
|
+
suppressEmbeds: Boolean((message.flags ?? 0) & MessageFlags.SuppressEmbeds),
|
|
208
|
+
}))
|
|
209
|
+
expect(flags).toMatchInlineSnapshot(`
|
|
210
|
+
[
|
|
211
|
+
{
|
|
212
|
+
"suppressEmbeds": true,
|
|
213
|
+
"text": "-# *using determinis",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"suppressEmbeds": false,
|
|
217
|
+
"text": "Here is the screensh",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"suppressEmbeds": true,
|
|
221
|
+
"text": "-# *main ⋅ <1s ⋅ 0% ",
|
|
222
|
+
},
|
|
223
|
+
]
|
|
224
|
+
`)
|
|
225
|
+
})
|
|
226
|
+
|
|
173
227
|
test('message from a user without permission creates no thread', async () => {
|
|
174
228
|
const { discord, channelId } = twin
|
|
175
229
|
const before = (await discord.channel(channelId).getThreads()).length
|
|
@@ -203,7 +257,7 @@ test('another bot needs the Kimaki role, and no-kimaki still denies it', async (
|
|
|
203
257
|
--- from: assistant (TestBot)
|
|
204
258
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
205
259
|
Hello from the deterministic model.
|
|
206
|
-
-# *
|
|
260
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
207
261
|
`)
|
|
208
262
|
|
|
209
263
|
const withRole = (await discord.channel(channelId).getThreads()).length
|
package/src/ingress.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Discord ingress (spec 4): gates every messageCreate, parses it into a Route
|
|
2
2
|
// and calls actions. Gates, in order: webhooks and our own messages (except
|
|
3
|
-
// remote send envelopes),
|
|
3
|
+
// remote send envelopes), system messages (a thread rename only sets the
|
|
4
|
+
// session title), channel ownership (only channels mapped in this
|
|
4
5
|
// machine's SQLite), permission (other bots need the Kimaki role).
|
|
5
6
|
// Messages of one channel are handled in arrival order, after OpenCode is ready.
|
|
6
7
|
//
|
|
@@ -17,9 +18,11 @@ import fs from 'node:fs'
|
|
|
17
18
|
import path from 'node:path'
|
|
18
19
|
import { pathToFileURL } from 'node:url'
|
|
19
20
|
import {
|
|
21
|
+
ChannelType,
|
|
20
22
|
Events,
|
|
21
23
|
GuildMember,
|
|
22
24
|
MessageFlags,
|
|
25
|
+
MessageType,
|
|
23
26
|
PermissionFlagsBits,
|
|
24
27
|
type Attachment,
|
|
25
28
|
type Guild,
|
|
@@ -27,18 +30,20 @@ import {
|
|
|
27
30
|
} from 'discord.js'
|
|
28
31
|
import * as errore from 'errore'
|
|
29
32
|
|
|
30
|
-
import { projectOf, sessionDirectory, type Bot, type PromptFile } from './bot.ts'
|
|
33
|
+
import { isCoordinatorChannel, oc, projectOf, sessionDirectory, type Bot, type PromptFile } from './bot.ts'
|
|
31
34
|
import { formatError, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from './format-parts.ts'
|
|
32
35
|
import { createLogger } from './logger.ts'
|
|
33
36
|
import { parseInput, sendInput } from './lock-routes.ts'
|
|
34
37
|
import { cancelQueuedPrompt, dispatch, requeueEdited, send } from './prompt.ts'
|
|
35
38
|
import { REMOTE_PROMPT_FILE, REMOTE_RESULT_PREFIX, REMOTE_SEND_PREFIX } from './remote-send.ts'
|
|
36
|
-
import { formatEcho, queuedItemFor } from './queue.ts'
|
|
39
|
+
import { echoPayloads, formatEcho, queuedItemFor } from './queue.ts'
|
|
37
40
|
import { parseTextMessage, type Route } from './routes.ts'
|
|
38
|
-
import { primaryAgents, startSession } from './sessions.ts'
|
|
41
|
+
import { primaryAgents, startCoordinator, startSession } from './sessions.ts'
|
|
39
42
|
import { isVoiceAttachment, parseVoiceMessage, transcribe, voiceMediaType, type AttachmentLike } from './voice.ts'
|
|
40
43
|
|
|
41
44
|
const logger = createLogger('INGRESS')
|
|
45
|
+
// Later messages of the channel wait for downloads (registerIngress).
|
|
46
|
+
const DOWNLOAD_TIMEOUT_MS = 60_000
|
|
42
47
|
|
|
43
48
|
// Owner, Administrator, Manage Server, or a role named "Kimaki"; with
|
|
44
49
|
// --allow-all-users every member. A role named "no-kimaki" always denies.
|
|
@@ -114,14 +119,14 @@ class AttachmentError extends errore.createTaggedError({
|
|
|
114
119
|
}) {}
|
|
115
120
|
|
|
116
121
|
async function remotePromptText(url: string) {
|
|
117
|
-
const response = await fetch(url).catch((cause) => new AttachmentError({ file: REMOTE_PROMPT_FILE, cause }))
|
|
122
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) }).catch((cause) => new AttachmentError({ file: REMOTE_PROMPT_FILE, cause }))
|
|
118
123
|
if (response instanceof Error) return response
|
|
119
124
|
if (!response.ok) return new AttachmentError({ file: REMOTE_PROMPT_FILE, cause: new Error(`HTTP ${response.status}`) })
|
|
120
125
|
return response.text().catch((cause) => new AttachmentError({ file: REMOTE_PROMPT_FILE, cause }))
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
async function download(url: string): Promise<AttachmentError | Buffer> {
|
|
124
|
-
const response = await fetch(url).catch((e) => new AttachmentError({ file: url, cause: e }))
|
|
129
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) }).catch((e) => new AttachmentError({ file: url, cause: e }))
|
|
125
130
|
if (response instanceof Error) return response
|
|
126
131
|
if (!response.ok) return new AttachmentError({ file: `${url} (HTTP ${response.status})` })
|
|
127
132
|
const body = await response.arrayBuffer().catch((e) => new AttachmentError({ file: url, cause: e }))
|
|
@@ -192,7 +197,7 @@ async function handleRemoteEnvelope(bot: Bot, { message, channelId, threadId }:
|
|
|
192
197
|
const input = prompt instanceof Error ? prompt : parseInput(sendInput, { ...value.options, ...target, prompt })
|
|
193
198
|
const attachments = [...message.attachments.values()].filter((attachment) => attachment !== promptAttachment)
|
|
194
199
|
const files = await saveAttachments({ dataDir: bot.dataDir, messageId: message.id, attachments })
|
|
195
|
-
const result = input instanceof Error ? input : files instanceof Error ? files : await send(bot, { ...input, files }, { localOnly: true })
|
|
200
|
+
const result = input instanceof Error ? input : files instanceof Error ? files : await send(bot, { ...input, files }, { localOnly: true, shownIn: message.id })
|
|
196
201
|
const answer = JSON.stringify(result instanceof Error ? { error: result.message } : result)
|
|
197
202
|
await message
|
|
198
203
|
.reply({
|
|
@@ -232,7 +237,9 @@ async function handleMessage(bot: Bot, message: Message) {
|
|
|
232
237
|
logger.log(`ignoring ${message.author.username}: no Kimaki permission`)
|
|
233
238
|
return
|
|
234
239
|
}
|
|
235
|
-
|
|
240
|
+
// A coordinator channel has its root session under the channel ID.
|
|
241
|
+
const coordinator = !thread && isCoordinatorChannel(bot, channelId)
|
|
242
|
+
const sessionId = bot.store.getState().roots[thread?.id ?? (coordinator ? channelId : '')]
|
|
236
243
|
if (thread && !sessionId) return
|
|
237
244
|
const author = { id: message.author.id, username: message.author.username }
|
|
238
245
|
const reportError = async (error: Error) => {
|
|
@@ -259,8 +266,35 @@ async function handleMessage(bot: Bot, message: Message) {
|
|
|
259
266
|
const attached = files.length > 0 ? `, ${files.length} file(s)` : ''
|
|
260
267
|
logger.log(`${route.kind}${detail} from ${author.username} in ${thread ? `thread ${thread.id}` : `channel ${channelId}`}${attached}`)
|
|
261
268
|
// The transcription is not visible anywhere else.
|
|
262
|
-
if (voice && thread && route.kind !== 'shell' && route.kind !== 'command' && route.kind !== 'skill') {
|
|
263
|
-
|
|
269
|
+
if (voice && (thread || coordinator) && route.kind !== 'shell' && route.kind !== 'command' && route.kind !== 'skill') {
|
|
270
|
+
// A long transcript continues in more messages after the reply.
|
|
271
|
+
for (const [index, payload] of echoPayloads(formatEcho({ username: author.username, text: route.text })).entries()) {
|
|
272
|
+
const options = { ...payload, allowedMentions: { parse: [] }, flags: ('flags' in payload ? payload.flags : 0) | SILENT_MESSAGE_FLAGS }
|
|
273
|
+
if (index === 0) await message.reply(options)
|
|
274
|
+
else if (channel.isSendable()) await channel.send(options)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Coordinator: the channel is the conversation of one long session.
|
|
279
|
+
if (coordinator && channel.type === ChannelType.GuildText) {
|
|
280
|
+
// A spoken agent is for a new thread: the coordinator keeps its own agent and model.
|
|
281
|
+
const result = route.kind === 'new-session' || (route.kind === 'steer' && route.agent)
|
|
282
|
+
? await startSession(bot, { channelId, directory: project.directory, route: { kind: 'steer', text: route.text, ...(route.agent && { agent: route.agent }) }, author, messageId: message.id, startMessageId: null, files })
|
|
283
|
+
: bot.store.getState().roots[channel.id]
|
|
284
|
+
? await dispatch(bot, { conversation: channel, route, author, messageId: message.id, files })
|
|
285
|
+
: await startCoordinator(bot, {
|
|
286
|
+
channel,
|
|
287
|
+
// Queue and btw need a session to wait for or fork from: the first input is a plain prompt.
|
|
288
|
+
route: route.kind === 'shell' || route.kind === 'command' || route.kind === 'skill' || route.kind === 'steer' ? route : { kind: 'steer', text: route.text },
|
|
289
|
+
author,
|
|
290
|
+
messageId: message.id,
|
|
291
|
+
files,
|
|
292
|
+
})
|
|
293
|
+
if (result instanceof Error) return reportError(result)
|
|
294
|
+
if (!result || result.threadId === channel.id) return
|
|
295
|
+
const note = route.kind === 'btw' ? `Session forked! Continue in <#${result.threadId}>` : `Started a new session in <#${result.threadId}>`
|
|
296
|
+
await message.reply({ content: note, allowedMentions: { repliedUser: false }, flags: SILENT_MESSAGE_FLAGS }).catch(() => undefined)
|
|
297
|
+
return
|
|
264
298
|
}
|
|
265
299
|
|
|
266
300
|
if (!thread) {
|
|
@@ -280,7 +314,7 @@ async function handleMessage(bot: Bot, message: Message) {
|
|
|
280
314
|
return
|
|
281
315
|
}
|
|
282
316
|
|
|
283
|
-
const result = await dispatch(bot, { thread, route, author, messageId: message.id, files })
|
|
317
|
+
const result = await dispatch(bot, { conversation: thread, route, author, messageId: message.id, files })
|
|
284
318
|
if (result instanceof Error) return reportError(result)
|
|
285
319
|
if (!result) return
|
|
286
320
|
const note =
|
|
@@ -288,7 +322,20 @@ async function handleMessage(bot: Bot, message: Message) {
|
|
|
288
322
|
await message.reply({ content: note, allowedMentions: { repliedUser: false }, flags: SILENT_MESSAGE_FLAGS }).catch(() => undefined)
|
|
289
323
|
}
|
|
290
324
|
|
|
291
|
-
//
|
|
325
|
+
// A rename in the Discord UI posts a CHANNEL_NAME_CHANGE message with the new
|
|
326
|
+
// name as content (issue #258): the session title follows it, nothing is prompted.
|
|
327
|
+
async function handleThreadRename(bot: Bot, message: Message) {
|
|
328
|
+
if (!message.inGuild() || !message.channel.isThread()) return
|
|
329
|
+
const sessionId = bot.store.getState().roots[message.channel.id]
|
|
330
|
+
if (!sessionId || !(await authorCanUseKimaki(bot, message))) return
|
|
331
|
+
const title = message.content
|
|
332
|
+
const renamed = await oc(bot, 'session.update', (client) => client.session.update({ sessionID: sessionId, title }))
|
|
333
|
+
if (renamed instanceof Error) return logger.warn(`title of ${sessionId} from thread rename`, renamed)
|
|
334
|
+
logger.log(`session ${sessionId} renamed to "${title}" by ${message.author.username}`)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Registers the message listeners. Messages of one channel are handled in arrival order:
|
|
338
|
+
// a text sent while a voice message is transcribed waits for it (voice-attachments.e2e.test.ts).
|
|
292
339
|
export function registerIngress(bot: Bot) {
|
|
293
340
|
const chains = new Map<string, Promise<void>>()
|
|
294
341
|
|
|
@@ -308,6 +355,9 @@ export function registerIngress(bot: Bot) {
|
|
|
308
355
|
// Webhooks have no member to check. Own messages only matter as remote send envelopes.
|
|
309
356
|
if (message.webhookId) return
|
|
310
357
|
if (message.author.id === bot.discord.user?.id && !message.embeds[0]?.footer?.text.startsWith(REMOTE_SEND_PREFIX)) return
|
|
358
|
+
if (message.type === MessageType.ChannelNameChange) return serialize(message.channelId, () => handleThreadRename(bot, message))
|
|
359
|
+
// Pins, joins, boosts and other system messages are never prompts.
|
|
360
|
+
if (message.type !== MessageType.Default && message.type !== MessageType.Reply) return
|
|
311
361
|
serialize(message.channelId, () => handleMessage(bot, message))
|
|
312
362
|
})
|
|
313
363
|
// Deleting a queued Discord message removes it from the queue.
|
|
@@ -16,14 +16,13 @@ import {
|
|
|
16
16
|
type ModalSubmitInteraction,
|
|
17
17
|
type RESTPostAPIChatInputApplicationCommandsJSONBody,
|
|
18
18
|
type StringSelectMenuInteraction,
|
|
19
|
-
type ThreadChannel,
|
|
20
19
|
} from 'discord.js'
|
|
21
20
|
|
|
22
|
-
import { projectOf, sessionDirectory, type Author, type Bot } from './bot.ts'
|
|
21
|
+
import { isCoordinatorChannel, projectOf, sessionDirectory, type Author, type Bot, type Conversation } from './bot.ts'
|
|
23
22
|
import { ConfigError, DiscordError } from './errors.ts'
|
|
24
23
|
import { formatError, SILENT_MESSAGE_FLAGS } from './format-parts.ts'
|
|
25
24
|
import { createLogger } from './logger.ts'
|
|
26
|
-
import {
|
|
25
|
+
import { formatEchoReply } from './queue.ts'
|
|
27
26
|
|
|
28
27
|
const logger = createLogger('COMMANDS')
|
|
29
28
|
|
|
@@ -51,11 +50,11 @@ export type InteractionRoutes = {
|
|
|
51
50
|
// --- Context
|
|
52
51
|
|
|
53
52
|
// Where an interaction happens: the project channel and, in a session
|
|
54
|
-
// thread,
|
|
53
|
+
// thread or a coordinator channel, that conversation and its root session.
|
|
55
54
|
export type InteractionTarget = {
|
|
56
55
|
channelId: string
|
|
57
56
|
directory: string
|
|
58
|
-
|
|
57
|
+
conversation: Conversation | null
|
|
59
58
|
sessionId: string | null
|
|
60
59
|
projectDirectory: string
|
|
61
60
|
}
|
|
@@ -76,13 +75,15 @@ export async function resolveTarget(bot: Bot, channelId: string | null): Promise
|
|
|
76
75
|
const row = await projectOf(bot, projectChannelId)
|
|
77
76
|
if (row instanceof Error) return row
|
|
78
77
|
if (!row) return new ConfigError({ reason: 'This channel is not configured with a project directory' })
|
|
79
|
-
|
|
78
|
+
// A coordinator channel is the conversation of its root session. With coordinator mode off its binding is ignored, like in ingress.
|
|
79
|
+
const conversation = thread ?? (channel?.type === ChannelType.GuildText && isCoordinatorChannel(bot, channel.id) && bot.store.getState().roots[channel.id] ? channel : null)
|
|
80
|
+
const sessionId = conversation ? (bot.store.getState().roots[conversation.id] ?? null) : null
|
|
80
81
|
const directory = sessionId ? await sessionDirectory(bot, sessionId) : row.directory
|
|
81
82
|
if (directory instanceof Error) return directory
|
|
82
|
-
return { channelId: projectChannelId, directory, projectDirectory: row.directory,
|
|
83
|
+
return { channelId: projectChannelId, directory, projectDirectory: row.directory, conversation, sessionId }
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
export type SessionTarget = InteractionTarget & {
|
|
86
|
+
export type SessionTarget = InteractionTarget & { conversation: Conversation; sessionId: string }
|
|
86
87
|
|
|
87
88
|
// The session thread of a command, or null after a reply saying where it works.
|
|
88
89
|
export async function sessionTarget(bot: Bot, interaction: ChatInputCommandInteraction): Promise<SessionTarget | null> {
|
|
@@ -91,11 +92,11 @@ export async function sessionTarget(bot: Bot, interaction: ChatInputCommandInter
|
|
|
91
92
|
await replyError(interaction, target)
|
|
92
93
|
return null
|
|
93
94
|
}
|
|
94
|
-
if (!target.
|
|
95
|
+
if (!target.conversation || !target.sessionId) {
|
|
95
96
|
await interaction.reply({ content: 'Use this command in a thread with a Kimaki session', flags: MessageFlags.Ephemeral })
|
|
96
97
|
return null
|
|
97
98
|
}
|
|
98
|
-
return { ...target,
|
|
99
|
+
return { ...target, conversation: target.conversation, sessionId: target.sessionId }
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
// --- Replies
|
|
@@ -116,7 +117,7 @@ export async function replyError(
|
|
|
116
117
|
|
|
117
118
|
// The visible reply stands in for the user message: acks and prompts point at its ID.
|
|
118
119
|
export async function replyWithEcho(interaction: ChatInputCommandInteraction, { author, text }: { author: Author; text: string }) {
|
|
119
|
-
await interaction.reply({ content:
|
|
120
|
+
await interaction.reply({ content: formatEchoReply({ username: author.username, text }), allowedMentions: { parse: [] }, flags: SILENT_MESSAGE_FLAGS })
|
|
120
121
|
return interaction.fetchReply().catch((e: Error) => e)
|
|
121
122
|
}
|
|
122
123
|
|
|
@@ -97,7 +97,7 @@ test('plain message interrupts the run and is answered', async () => {
|
|
|
97
97
|
Stop and do this steer-marker
|
|
98
98
|
--- from: assistant (TestBot)
|
|
99
99
|
steer ok
|
|
100
|
-
-# *
|
|
100
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
101
101
|
`)
|
|
102
102
|
await expectNever({ threadId: thread.id, text: 'slow-done' })
|
|
103
103
|
})
|
|
@@ -112,7 +112,7 @@ test('/abort stops the run and clears the queue', async () => {
|
|
|
112
112
|
await waitForBotMessageContaining({ discord: twin.discord, threadId: thread.id, text: 'Removed from queue' })
|
|
113
113
|
await expectNever({ threadId: thread.id, text: 'slow-done' })
|
|
114
114
|
await expectNever({ threadId: thread.id, text: 'queued one ok' })
|
|
115
|
-
expect(await twin.discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
115
|
+
expect(await twin.discord.thread(thread.id).text({ showReplies: true })).toMatchInlineSnapshot(`
|
|
116
116
|
"--- from: user (tommy)
|
|
117
117
|
Abort me slow-marker
|
|
118
118
|
--- from: assistant (TestBot)
|
|
@@ -120,6 +120,7 @@ test('/abort stops the run and clears the queue', async () => {
|
|
|
120
120
|
--- from: user (tommy)
|
|
121
121
|
Never runs queued-one. queue
|
|
122
122
|
--- from: assistant (TestBot)
|
|
123
|
+
[reply to tommy: Never runs queued-one. queue]
|
|
123
124
|
-# Removed from queue
|
|
124
125
|
Request **aborted**, cleared 1 queued message"
|
|
125
126
|
`)
|
|
@@ -134,7 +135,7 @@ test('a restarted bot still echoes and runs the queue it did not see', async ()
|
|
|
134
135
|
bot = await startTestBot({ dataDir, twin, server })
|
|
135
136
|
await waitForBotMessageContaining({ discord: twin.discord, threadId: thread.id, text: 'queued one ok' })
|
|
136
137
|
await waitForFooter({ discord: twin.discord, threadId: thread.id })
|
|
137
|
-
expect(await twin.discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
138
|
+
expect(await twin.discord.thread(thread.id).text({ showReplies: true })).toMatchInlineSnapshot(`
|
|
138
139
|
"--- from: user (tommy)
|
|
139
140
|
Restart during queue slow-marker
|
|
140
141
|
--- from: assistant (TestBot)
|
|
@@ -142,10 +143,12 @@ test('a restarted bot still echoes and runs the queue it did not see', async ()
|
|
|
142
143
|
--- from: user (tommy)
|
|
143
144
|
Survives restart queued-one. queue
|
|
144
145
|
--- from: assistant (TestBot)
|
|
146
|
+
[reply to tommy: Survives restart queued-one. queue]
|
|
145
147
|
-# Queued at position 1. Delete the original message to remove it, or use /clear-queue position:1
|
|
146
148
|
slow-done
|
|
147
|
-
|
|
149
|
+
[reply to tommy: Survives restart queued-one. queue]
|
|
150
|
+
-# Queued message sent
|
|
148
151
|
queued one ok
|
|
149
|
-
-# *
|
|
152
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
150
153
|
`)
|
|
151
154
|
})
|
package/src/legacy.e2e.test.ts
CHANGED
|
@@ -82,7 +82,7 @@ test('bot starts on a V1 database and answers in an old thread with the old sess
|
|
|
82
82
|
hello again after the upgrade
|
|
83
83
|
--- from: assistant (TestBot)
|
|
84
84
|
ok
|
|
85
|
-
-# *
|
|
85
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*"
|
|
86
86
|
`)
|
|
87
87
|
|
|
88
88
|
const messages = await client.message.list({ sessionID: session.id })
|
package/src/lock-routes.ts
CHANGED
|
@@ -13,7 +13,7 @@ import * as orm from 'drizzle-orm'
|
|
|
13
13
|
import { z } from 'zod'
|
|
14
14
|
|
|
15
15
|
import { parseButton } from './agent-ui.ts'
|
|
16
|
-
import {
|
|
16
|
+
import { fetchConversation, isCoordinatorChannel, oc, parseModel, projectOf, type Bot } from './bot.ts'
|
|
17
17
|
import { credential, loginCli } from './commands/login-commands.ts'
|
|
18
18
|
import { setChannelAgent, setChannelModel, setGlobalModel, setVerbosity } from './commands/preference-commands.ts'
|
|
19
19
|
import { channelWorktrees, manageWorktree, newWorktree, setAutoWorktrees } from './commands/worktree-commands.ts'
|
|
@@ -23,7 +23,8 @@ import { abort, cancelQueuedPrompt, clearQueue, send, shell, upload } from './pr
|
|
|
23
23
|
import { createTask, deleteTask, editTask, runTaskNow } from './scheduler.ts'
|
|
24
24
|
import * as schema from './schema.ts'
|
|
25
25
|
import { channelForDirectory, channelForSession, fork, resume, sessionCwd } from './sessions.ts'
|
|
26
|
-
import { createSleep } from './sleeps.ts'
|
|
26
|
+
import { createSleep, sleepsWarm } from './sleeps.ts'
|
|
27
|
+
import { conversationTyping, workersTyping } from './thread-reducer.ts'
|
|
27
28
|
import { worktreeName } from './worktrees.ts'
|
|
28
29
|
|
|
29
30
|
type LockRoute<S extends z.ZodType, R> = {
|
|
@@ -82,6 +83,10 @@ const sendFields = {
|
|
|
82
83
|
// '' asks for an automatic name.
|
|
83
84
|
worktree: z.string({ error: 'worktree must be a name or an empty string for an automatic name' }).optional(),
|
|
84
85
|
notifyOnly: z.boolean({ error: 'notifyOnly must be boolean' }).optional(),
|
|
86
|
+
// Seconds the CLI waits for the run (`--wait [seconds]`): a run that ends within them sends no coordinator note.
|
|
87
|
+
wait: z.number({ error: 'wait must be a number of seconds' }).positive({ error: 'wait must be a positive number of seconds' }).optional(),
|
|
88
|
+
// OPENCODE_SESSION_ID of the shell that ran `kimaki send`: a coordinator wakes when the runs it prompted end.
|
|
89
|
+
senderSessionId: text('senderSessionId').optional(),
|
|
85
90
|
permissions: z.array(z.string(), { error: 'Permission rules must be strings' }).optional(),
|
|
86
91
|
prompt: text('prompt'),
|
|
87
92
|
files: z.array(z.object({ uri: z.string(), name: z.string() }), { error: 'files must contain uri and name strings' }).optional(),
|
|
@@ -265,6 +270,19 @@ async function snapshotHeap({ dataDir }: { dataDir: string }) {
|
|
|
265
270
|
return { path: written }
|
|
266
271
|
}
|
|
267
272
|
|
|
273
|
+
// The coordinator or a thread of its channel runs, from the thread views.
|
|
274
|
+
function coordinatorBusy(bot: Bot, sessionId: string): boolean {
|
|
275
|
+
const { sessionThreads, roots, threads } = bot.store.getState()
|
|
276
|
+
// A coordinator's conversation is its channel. A replaced one (/reset-coordinator), one of a
|
|
277
|
+
// channel with coordinator mode off, or a thread session is never busy.
|
|
278
|
+
const channelId = sessionThreads[sessionId]
|
|
279
|
+
if (!channelId || roots[channelId] !== sessionId || !isCoordinatorChannel(bot, channelId)) return false
|
|
280
|
+
const view = threads[channelId]
|
|
281
|
+
// An idle coordinator has no view after a bot restart; its threads do.
|
|
282
|
+
if (!view) return workersTyping({ threads, channelId })
|
|
283
|
+
return view.coordinator && conversationTyping({ threads, threadId: channelId })
|
|
284
|
+
}
|
|
285
|
+
|
|
268
286
|
const loginField = z.string({ error: 'Login fields must be non-empty strings' }).min(1, { error: 'Login fields must be non-empty strings' }).optional()
|
|
269
287
|
|
|
270
288
|
export const lockRoutes = {
|
|
@@ -297,6 +315,16 @@ export const lockRoutes = {
|
|
|
297
315
|
}
|
|
298
316
|
},
|
|
299
317
|
}),
|
|
318
|
+
// The plugin keeps a session's prompt cache warm while its coordinator channel is busy
|
|
319
|
+
// or it sleeps (plugin/coordinator.ts).
|
|
320
|
+
'session.warmup': route({
|
|
321
|
+
input: z.object({ sessionId: text('sessionId') }),
|
|
322
|
+
run: async (bot, { sessionId }) => {
|
|
323
|
+
const sleeping = await sleepsWarm(bot, sessionId)
|
|
324
|
+
if (sleeping instanceof Error) return sleeping
|
|
325
|
+
return { busy: coordinatorBusy(bot, sessionId), sleeping }
|
|
326
|
+
},
|
|
327
|
+
}),
|
|
300
328
|
restart: route({
|
|
301
329
|
input: z.object({}),
|
|
302
330
|
run: async (bot) => {
|
|
@@ -371,6 +399,17 @@ export const lockRoutes = {
|
|
|
371
399
|
return bot.features.agentUi.request({ ...input, sessionId, content: { buttons }, signal })
|
|
372
400
|
},
|
|
373
401
|
}),
|
|
402
|
+
// Labels of buttons each session showed and the user did not click yet (`kimaki session wait`).
|
|
403
|
+
'session.buttons': route({
|
|
404
|
+
input: z.object({ sessionIds: z.array(text('sessionId'), { error: 'sessionIds must be an array' }) }),
|
|
405
|
+
run: async (bot, { sessionIds }) => {
|
|
406
|
+
const prompts = Object.values(bot.store.getState().threads).flatMap((view) => view.agentUi)
|
|
407
|
+
return Object.fromEntries(sessionIds.map((sessionId) => [
|
|
408
|
+
sessionId,
|
|
409
|
+
prompts.flatMap((prompt) => prompt.sessionId === sessionId ? (prompt.buttons ?? []).map((item) => item.label) : []),
|
|
410
|
+
]))
|
|
411
|
+
},
|
|
412
|
+
}),
|
|
374
413
|
'upload-request': route({
|
|
375
414
|
input: z.object({
|
|
376
415
|
...agentUiFields,
|
|
@@ -493,9 +532,9 @@ export const lockRoutes = {
|
|
|
493
532
|
run: async (bot, input) => {
|
|
494
533
|
const target = localSession(bot, input)
|
|
495
534
|
if (target instanceof Error) return target
|
|
496
|
-
const thread = await
|
|
535
|
+
const thread = await fetchConversation(bot, target.threadId)
|
|
497
536
|
if (thread instanceof Error) return thread
|
|
498
|
-
return sessionCwd(bot, { thread, directory: input.directory })
|
|
537
|
+
return sessionCwd(bot, { conversation: thread, directory: input.directory })
|
|
499
538
|
},
|
|
500
539
|
}),
|
|
501
540
|
'session.title': route({
|
|
@@ -506,8 +545,10 @@ export const lockRoutes = {
|
|
|
506
545
|
const title = input.text
|
|
507
546
|
const renamed = await oc(bot, 'session.update', (client) => client.session.update({ sessionID: target.sessionId, title }))
|
|
508
547
|
if (renamed instanceof Error) return renamed
|
|
509
|
-
const thread = await
|
|
548
|
+
const thread = await fetchConversation(bot, target.threadId)
|
|
510
549
|
if (thread instanceof Error) return thread
|
|
550
|
+
// A coordinator channel keeps its project name.
|
|
551
|
+
if (!thread.isThread()) return { title }
|
|
511
552
|
const updated = await thread.setName(title.slice(0, 100)).catch((cause) => new DiscordError({ operation: 'rename thread', cause }))
|
|
512
553
|
if (updated instanceof Error) return updated
|
|
513
554
|
return { title }
|
|
@@ -518,8 +559,9 @@ export const lockRoutes = {
|
|
|
518
559
|
run: async (bot, input) => {
|
|
519
560
|
const target = localSession(bot, input)
|
|
520
561
|
if (target instanceof Error) return target
|
|
521
|
-
const thread = await
|
|
562
|
+
const thread = await fetchConversation(bot, target.threadId)
|
|
522
563
|
if (thread instanceof Error) return thread
|
|
564
|
+
if (!thread.isThread()) return new ConfigError({ reason: 'A coordinator channel cannot be archived' })
|
|
523
565
|
const archived = await thread.setArchived(true).catch((cause) => new DiscordError({ operation: 'archive thread', cause }))
|
|
524
566
|
if (archived instanceof Error) return archived
|
|
525
567
|
return { archived: true }
|
|
@@ -533,15 +575,27 @@ export const lockRoutes = {
|
|
|
533
575
|
return abort(bot, { threadId: target.threadId })
|
|
534
576
|
},
|
|
535
577
|
}),
|
|
578
|
+
'session.compact': route({
|
|
579
|
+
input: sessionInput({}),
|
|
580
|
+
run: async (bot, input) => {
|
|
581
|
+
const target = localSession(bot, input)
|
|
582
|
+
if (target instanceof Error) return target
|
|
583
|
+
// Queued, so an agent compacting its own session finishes its turn first.
|
|
584
|
+
const sessionID = input.sessionId ?? target.sessionId
|
|
585
|
+
const compaction = await oc(bot, 'session.compact', (client) => client.session.compact({ sessionID, delivery: 'queue' }))
|
|
586
|
+
if (compaction instanceof Error) return compaction
|
|
587
|
+
return { sessionId: sessionID, queued: true }
|
|
588
|
+
},
|
|
589
|
+
}),
|
|
536
590
|
'session.fork': route({
|
|
537
591
|
input: sessionInput({ before: text('before').optional(), name: text('name').optional() }),
|
|
538
592
|
run: async (bot, input) => {
|
|
539
593
|
const target = localSession(bot, input)
|
|
540
594
|
if (target instanceof Error) return target
|
|
541
|
-
const thread = await
|
|
595
|
+
const thread = await fetchConversation(bot, target.threadId)
|
|
542
596
|
if (thread instanceof Error) return thread
|
|
543
597
|
// A child session ID forks that child, not the root.
|
|
544
|
-
return fork(bot, {
|
|
598
|
+
return fork(bot, { source: thread, sessionId: input.sessionId ?? target.sessionId, before: input.before, name: input.name, author: cliAuthor(bot) })
|
|
545
599
|
},
|
|
546
600
|
}),
|
|
547
601
|
'session.command': route({
|
package/src/lock-server.ts
CHANGED
|
@@ -28,17 +28,33 @@ export { DEFAULT_LOCK_PORT }
|
|
|
28
28
|
// Exit code that tells the supervisor of the root `kimaki` command (cli/bot.ts) to start the bot again.
|
|
29
29
|
export const RESTART_EXIT_CODE = 75
|
|
30
30
|
|
|
31
|
-
export
|
|
31
|
+
export function shellQuote(arg: string): string {
|
|
32
|
+
return /^[\w@%+=:,./-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// cmd.exe quoting for batch files: Windows paths cannot contain `"`, and `%` expands variables.
|
|
36
|
+
function cmdQuote(arg: string): string {
|
|
37
|
+
return `"${arg.replaceAll('%', '%%')}"`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Writes `<dataDir>/bin/kimaki`, which runs `argv` (this Kimaki install). On Windows also
|
|
41
|
+
// `kimaki.cmd` for PowerShell and cmd.exe; Git Bash only finds the extensionless sh script.
|
|
42
|
+
export async function installShim({ dataDir, argv }: { dataDir: string; argv: readonly string[] }): Promise<ConfigError | void> {
|
|
32
43
|
const directory = path.join(dataDir, 'bin')
|
|
33
44
|
const created = await fs.promises.mkdir(directory, { recursive: true }).catch((cause) => new ConfigError({ reason: 'Cannot create Kimaki shim directory', cause }))
|
|
34
45
|
if (created instanceof Error) return created
|
|
35
|
-
const
|
|
46
|
+
const windows = process.platform === 'win32'
|
|
36
47
|
// Bind the shim to its data dir through the env: not every command takes --data-dir.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
// MSYS sh runs `C:/...` paths; backslashes in a Windows path are not portable there.
|
|
49
|
+
const posixArgv = windows ? argv.map((arg) => arg.replaceAll('\\', '/')) : argv
|
|
50
|
+
const shims = [{ file: 'kimaki', script: `#!/bin/sh\nexport KIMAKI_DATA_DIR='${dataDir.replaceAll("'", "'\\''")}'\nexec ${posixArgv.map(shellQuote).join(' ')} "$@"\n` }]
|
|
51
|
+
if (windows) shims.push({ file: 'kimaki.cmd', script: `@echo off\r\nset "KIMAKI_DATA_DIR=${dataDir.replaceAll('%', '%%')}"\r\n${argv.map(cmdQuote).join(' ')} %*\r\n` })
|
|
52
|
+
for (const shim of shims) {
|
|
53
|
+
const file = path.join(directory, shim.file)
|
|
54
|
+
const written = await fs.promises.writeFile(file, shim.script, { mode: 0o700 }).then(() => fs.promises.chmod(file, 0o700))
|
|
55
|
+
.catch((cause) => new ConfigError({ reason: 'Cannot install Kimaki command shim', cause }))
|
|
56
|
+
if (written instanceof Error) return written
|
|
57
|
+
}
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
export type LockServer = {
|