kimaki 1.3.0 → 1.5.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 +13 -6
- package/dist/bot.js +17 -15
- package/dist/bot.js.map +1 -1
- package/dist/btw.e2e.test.js +3 -3
- package/dist/builtin-skills.d.ts +3 -0
- package/dist/builtin-skills.js +29 -0
- package/dist/builtin-skills.js.map +1 -1
- package/dist/builtin-skills.test.js +72 -1
- package/dist/builtin-skills.test.js.map +1 -1
- package/dist/catalog-refresh.e2e.test.js +2 -2
- package/dist/cli/bot.d.ts +1 -1
- package/dist/cli/bot.js +33 -18
- 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 +5 -6
- 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.d.ts +1 -0
- package/dist/coordinator-agents.e2e.test.js +113 -0
- package/dist/coordinator-agents.e2e.test.js.map +1 -0
- package/dist/coordinator-idle.e2e.test.d.ts +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.d.ts +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 +78 -3
- 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 +2 -3
- package/dist/event-loop.js +74 -44
- 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 +47 -12
- 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 +83 -31
- 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 +6 -3
- package/dist/main.js +14 -4
- package/dist/main.js.map +1 -1
- package/dist/mainmast-idle.e2e.test.d.ts +1 -0
- package/dist/mainmast-idle.e2e.test.js +99 -0
- package/dist/mainmast-idle.e2e.test.js.map +1 -0
- package/dist/mainmast-queue.e2e.test.d.ts +1 -0
- package/dist/mainmast-queue.e2e.test.js +244 -0
- package/dist/mainmast-queue.e2e.test.js.map +1 -0
- package/dist/mainmast-warmup.e2e.test.d.ts +1 -0
- package/dist/mainmast-warmup.e2e.test.js +130 -0
- package/dist/mainmast-warmup.e2e.test.js.map +1 -0
- package/dist/mainmast.d.ts +6 -0
- package/dist/mainmast.e2e.test.d.ts +1 -0
- package/dist/mainmast.e2e.test.js +428 -0
- package/dist/mainmast.e2e.test.js.map +1 -0
- package/dist/mainmast.js +156 -0
- package/dist/mainmast.js.map +1 -0
- package/dist/markdown/markdown.test.js +14 -4
- package/dist/markdown/markdown.test.js.map +1 -1
- package/dist/markdown/render-markdown.js +9 -6
- 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 +93 -61
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/mainmast.d.ts +4 -0
- package/dist/plugin/mainmast.js +379 -0
- package/dist/plugin/mainmast.js.map +1 -0
- 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/project.d.ts +1 -1
- package/dist/prompt.d.ts +22 -9
- 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 +3 -3
- 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 +152 -6
- 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 +18 -6
- package/dist/session-events.js +175 -27
- package/dist/session-events.js.map +1 -1
- package/dist/session-ops.e2e.test.js +63 -12
- package/dist/session-ops.e2e.test.js.map +1 -1
- package/dist/sessions.d.ts +44 -21
- package/dist/sessions.js +177 -65
- package/dist/sessions.js.map +1 -1
- package/dist/shell.e2e.test.js +4 -4
- package/dist/slash-commands.js +6 -4
- 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 +2 -1
- 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 +5 -15
- package/dist/system-prompt.js +180 -131
- package/dist/system-prompt.js.map +1 -1
- package/dist/test/generate-log-plugin/index.d.ts +3 -0
- 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 -2
- package/dist/test/harness.js +51 -11
- package/dist/test/harness.js.map +1 -1
- package/dist/test/replay.js +11 -8
- package/dist/test/replay.js.map +1 -1
- package/dist/thread-reducer.d.ts +29 -6
- package/dist/thread-reducer.fixtures.test.js +58 -51
- package/dist/thread-reducer.fixtures.test.js.map +1 -1
- package/dist/thread-reducer.js +109 -50
- package/dist/thread-reducer.js.map +1 -1
- package/dist/thread-reducer.test.js +190 -48
- package/dist/thread-reducer.test.js.map +1 -1
- package/dist/tunnel.d.ts +9 -0
- package/dist/tunnel.js +117 -28
- package/dist/tunnel.js.map +1 -1
- package/dist/tunnel.test.js +81 -5
- package/dist/tunnel.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.d.ts +1 -1
- package/dist/voice.js +72 -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 +11 -5
- package/skills/kimaki-tunnel/SKILL.md +3 -2
- 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 +36 -19
- package/src/btw.e2e.test.ts +3 -3
- package/src/builtin-skills.test.ts +73 -1
- package/src/builtin-skills.ts +33 -0
- package/src/catalog-refresh.e2e.test.ts +2 -2
- package/src/cli/bot.ts +30 -19
- 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 +145 -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 -45
- 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 +44 -11
- 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 +26 -10
- package/src/markdown/markdown.test.ts +15 -4
- package/src/markdown/render-markdown.ts +9 -6
- 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 +86 -59
- 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 +168 -25
- package/src/session-ops.e2e.test.ts +64 -11
- package/src/sessions.ts +217 -67
- package/src/shell.e2e.test.ts +4 -4
- package/src/slash-commands.test.ts +2 -0
- package/src/slash-commands.ts +6 -4
- 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 +190 -134
- package/src/test/generate-log-plugin/index.ts +16 -0
- package/src/test/harness.ts +77 -17
- package/src/test/replay.ts +12 -7
- package/src/thread-reducer.fixtures.test.ts +58 -51
- package/src/thread-reducer.test.ts +199 -49
- package/src/thread-reducer.ts +135 -66
- package/src/tunnel.test.ts +81 -5
- package/src/tunnel.ts +124 -25
- 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 +93 -26
- package/src/working-directory.e2e.test.ts +11 -11
- package/src/message-tool.e2e.test.ts +0 -45
package/src/agent-ui.e2e.test.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { execFile } from 'node:child_process'
|
|
4
|
+
import { promisify } from 'node:util'
|
|
2
5
|
import { ComponentType } from 'discord.js'
|
|
3
6
|
import { afterAll, beforeAll, expect, test } from 'vitest'
|
|
4
7
|
import type { BotHandle } from './main.ts'
|
|
5
8
|
import { TEST_USER_ID, seedProjectChannel, startOpencodeTestServer, startTestBot, startTwin, tempDataDir, waitFor, waitForFooter, warmUp, toolParts, textParts, type OpencodeTestServer, type TestTwin } from './test/harness.ts'
|
|
6
9
|
import { send, shell } from './prompt.ts'
|
|
7
10
|
|
|
11
|
+
const exec = promisify(execFile)
|
|
8
12
|
const dataDir = tempDataDir()
|
|
9
13
|
let server: OpencodeTestServer
|
|
10
14
|
let twin: TestTwin
|
|
@@ -19,7 +23,6 @@ beforeAll(async () => {
|
|
|
19
23
|
{ id: 'click', priority: 200, when: { latestUserTextIncludes: 'User clicked: Proceed' }, then: { parts: textParts('Proceed accepted.') } },
|
|
20
24
|
{ id: 'upload', priority: 100, when: { latestUserTextIncludes: 'upload-marker' }, then: { parts: toolParts({ toolCallId: 'upload-call', toolName: 'shell', input: { command: "kimaki upload-request --prompt 'Send the logo' --max-files 2", timeout: 600000, description: 'Request logo', hasSideEffect: true } }) } },
|
|
21
25
|
{ id: 'upload-done', priority: 110, when: { latestUserTextIncludes: 'upload-marker', rawPromptIncludes: 'upload-call' }, then: { parts: textParts('Upload received.') } },
|
|
22
|
-
{ id: 'branch', priority: 200, when: { latestUserTextIncludes: 'git-marker', rawPromptIncludes: '[current git branch is main]' }, then: { parts: textParts('Git context received.') } },
|
|
23
26
|
] }), startTwin()])
|
|
24
27
|
await seedProjectChannel({ dataDir, channelId: twin.channelId, guildId: twin.discord.guildId, directory: server.projectDirectory })
|
|
25
28
|
bot = await startTestBot({ dataDir, twin, server })
|
|
@@ -49,6 +52,11 @@ test('agent shim shows buttons after text and a click resumes the real session',
|
|
|
49
52
|
const shown = await ui(threadId, 'action_button:')
|
|
50
53
|
await waitForFooter({ discord: twin.discord, threadId })
|
|
51
54
|
const sessionId = bot.store.getState().roots[threadId]!
|
|
55
|
+
// The turn ended with buttons: `session wait` must say the session waits for a click.
|
|
56
|
+
const waited = await exec(process.execPath, ['--import', 'tsx', path.resolve('src/cli.ts'), 'session', 'wait', threadId, '--timeout', '8s', '--data-dir', dataDir], {
|
|
57
|
+
env: { ...process.env, KIMAKI_LOCK_PORT: String(bot.lock.port), KIMAKI_OPENCODE_SERVICE_FILE: server.serviceFile },
|
|
58
|
+
})
|
|
59
|
+
expect(waited.stdout.split('\n')[0]).toBe(`=== ${sessionId} is paused: it showed buttons (Proceed, Build) and waits for the user to click one, so its work may not be done ===`)
|
|
52
60
|
const started = shell(bot, { threadId, sessionId, command: 'printf synthetic' })
|
|
53
61
|
if (started instanceof Error) throw started
|
|
54
62
|
await waitFor({ label: 'synthetic shell output', check: async () => (await twin.discord.thread(threadId).text()).includes('```\nsynthetic\n```') })
|
|
@@ -56,22 +64,22 @@ test('agent shim shows buttons after text and a click resumes the real session',
|
|
|
56
64
|
await waitForFooter({ discord: twin.discord, threadId, count: 2 })
|
|
57
65
|
expect(await twin.discord.thread(threadId).text()).toMatchInlineSnapshot(`
|
|
58
66
|
"--- from: assistant (TestBot)
|
|
59
|
-
» **CLI:** button-marker
|
|
67
|
+
-# » **CLI:** button-marker
|
|
60
68
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
61
69
|
Choose the next action.
|
|
62
70
|
|
|
63
71
|
-# ┣ shell _Show choices_
|
|
64
|
-
-#
|
|
72
|
+
-# clicked "Proceed"
|
|
65
73
|
|
|
66
74
|
Buttons shown.
|
|
67
|
-
-# *
|
|
75
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
68
76
|
-# $ printf synthetic
|
|
69
77
|
\`\`\`
|
|
70
78
|
synthetic
|
|
71
79
|
\`\`\`
|
|
72
80
|
|
|
73
81
|
Proceed accepted.
|
|
74
|
-
-# *
|
|
82
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
75
83
|
`)
|
|
76
84
|
const messages = await twin.discord.thread(threadId).getMessages()
|
|
77
85
|
expect(messages.findIndex((message) => message.content.includes('Choose the next action.'))).toBeLessThan(messages.findIndex((message) => message.id === shown.message.id))
|
|
@@ -89,14 +97,14 @@ test('native file upload modal returns local paths to the agent shell', async ()
|
|
|
89
97
|
await waitForFooter({ discord: twin.discord, threadId })
|
|
90
98
|
expect(await twin.discord.thread(threadId).text()).toMatchInlineSnapshot(`
|
|
91
99
|
"--- from: assistant (TestBot)
|
|
92
|
-
» **CLI:** upload-marker
|
|
100
|
+
-# » **CLI:** upload-marker
|
|
93
101
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
94
102
|
-# ┣ shell _Request logo_
|
|
95
|
-
-#
|
|
103
|
+
-# dismissed
|
|
96
104
|
Uploaded 1 file(s)
|
|
97
105
|
|
|
98
106
|
Upload received.
|
|
99
|
-
-# *
|
|
107
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
100
108
|
`)
|
|
101
109
|
const dirs = fs.readdirSync(`${server.projectDirectory}/uploads`)
|
|
102
110
|
expect(fs.readFileSync(`${server.projectDirectory}/uploads/${dirs[0]}/0-logo.txt`, 'utf8')).toBe('logo')
|
|
@@ -106,17 +114,21 @@ test('git context is added only to marked Discord sessions', async () => {
|
|
|
106
114
|
await waitForFooter({ discord: twin.discord, threadId })
|
|
107
115
|
expect(await twin.discord.thread(threadId).text()).toMatchInlineSnapshot(`
|
|
108
116
|
"--- from: assistant (TestBot)
|
|
109
|
-
» **CLI:** git-marker
|
|
117
|
+
-# » **CLI:** git-marker
|
|
110
118
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
111
|
-
|
|
112
|
-
-# *
|
|
119
|
+
ok
|
|
120
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*"
|
|
113
121
|
`)
|
|
114
|
-
|
|
122
|
+
const branchSeen = (requests: typeof server.model.requests) => requests.some((request) => request.latestUserText.includes('git-marker') && request.raw.includes('[current git branch is main]'))
|
|
123
|
+
expect(branchSeen(server.model.requests)).toBe(true)
|
|
115
124
|
const client = await server.client()
|
|
116
125
|
// The branch is a synthetic message before the prompt, added again only when it changes.
|
|
117
126
|
const sessionId = bot.store.getState().roots[threadId]!
|
|
118
127
|
await client.session.prompt({ sessionID: sessionId, text: 'git-marker again' })
|
|
119
128
|
await client.session.wait({ sessionID: sessionId })
|
|
129
|
+
// Only a prompt after a reply carries the time since that reply.
|
|
130
|
+
const gaps = server.model.requests.filter((request) => request.latestUserText.includes('git-marker')).map((request) => /\[\d+s since your last message\]/.test(request.latestUserText))
|
|
131
|
+
expect([gaps[0], gaps.at(-1)]).toEqual([false, true])
|
|
120
132
|
const history = await client.session.context({ sessionID: sessionId })
|
|
121
133
|
expect(history.flatMap((message) => message.type === 'synthetic' || message.type === 'user' ? [`${message.type}: ${message.text.split('\n')[0]}`] : [])).toMatchInlineSnapshot(`
|
|
122
134
|
[
|
|
@@ -128,7 +140,9 @@ test('git context is added only to marked Discord sessions', async () => {
|
|
|
128
140
|
const plugins = await client.plugin.list({ location: { directory: server.projectDirectory } })
|
|
129
141
|
expect(plugins.data.find((plugin) => plugin.id === 'kimaki')?.state.status).toBe('active')
|
|
130
142
|
const session = await client.session.create({ location: { directory: server.projectDirectory } })
|
|
143
|
+
const before = server.model.requests.length
|
|
131
144
|
await client.session.prompt({ sessionID: session.id, text: 'git-marker' })
|
|
132
145
|
await client.session.wait({ sessionID: session.id })
|
|
133
|
-
expect(
|
|
146
|
+
expect(server.model.requests.length).toBeGreaterThan(before)
|
|
147
|
+
expect(branchSeen(server.model.requests.slice(before))).toBe(false)
|
|
134
148
|
})
|
package/src/agent-ui.ts
CHANGED
|
@@ -206,7 +206,8 @@ async function clickAgentButton(bot: Bot, interaction: ButtonInteraction) {
|
|
|
206
206
|
const item = prompt.buttons[Number(index)]
|
|
207
207
|
if (!item) return
|
|
208
208
|
bot.eventLoop.dispatch(interaction.channelId, { type: 'kimaki.agent-ui-dismiss', id, result: `clicked "${item.label}"` })
|
|
209
|
-
|
|
209
|
+
// The dismissed buttons show the click.
|
|
210
|
+
const result = await send(bot, { threadId: interaction.channelId, prompt: `User clicked: ${item.label}`, user: [interaction.user.id] }, { shownIn: interaction.message.id })
|
|
210
211
|
if (result instanceof Error) await interaction.followUp({ content: result.message, flags: MessageFlags.Ephemeral })
|
|
211
212
|
}
|
|
212
213
|
|
|
@@ -42,7 +42,7 @@ beforeAll(async () => {
|
|
|
42
42
|
const chunks: Buffer[] = []
|
|
43
43
|
request.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
44
44
|
request.on('end', () => {
|
|
45
|
-
if (request.url === '/v1/
|
|
45
|
+
if (request.url === '/v1/batch') {
|
|
46
46
|
const body = JSON.parse(Buffer.concat(chunks).toString('utf8')) as OtlpLogs
|
|
47
47
|
for (const record of body.resourceLogs.flatMap((logs) => logs.scopeLogs.flatMap((scope) => scope.logRecords))) {
|
|
48
48
|
received.push(Object.fromEntries(record.attributes.map(({ key, value }) => [key, value.stringValue ?? value.boolValue ?? value.doubleValue ?? Number(value.intValue)])))
|
package/src/autostart.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
// Start the bot when OpenCode starts. The bot writes <dataDir>/autostart.command
|
|
2
|
-
// on every start; the Kimaki OpenCode plugin opens it when
|
|
2
|
+
// on every start; the Kimaki OpenCode plugin opens it when nothing listens on
|
|
3
3
|
// the lock port:
|
|
4
4
|
//
|
|
5
5
|
// kimaki start ──writes──▶ ~/.kimaki/autostart.command (exec <bot node> <pkg>/dist/cli.js ...)
|
|
6
6
|
// ▲
|
|
7
|
-
// OpenCode loads plugin ──
|
|
8
|
-
//
|
|
7
|
+
// OpenCode loads plugin ──29988 refuses TCP──▶ open -g autostart.command
|
|
8
|
+
// (default terminal app, in background)
|
|
9
9
|
//
|
|
10
10
|
// The script exists only after onboarding succeeded, so a fresh install never
|
|
11
|
-
// opens a terminal with the onboarding prompts.
|
|
11
|
+
// opens a terminal with the onboarding prompts. `kimaki --no-autostart` deletes it
|
|
12
|
+
// on every start instead (bots under launchd, systemd or pm2). It always runs dist/cli.js,
|
|
12
13
|
// also when the bot ran from src: keep dist built. --if-not-running makes a bot that
|
|
13
14
|
// lost the race exit instead of stopping the running one.
|
|
14
15
|
// Imported by the plugin (OpenCode process): no logger, no bot modules.
|
|
15
16
|
|
|
16
17
|
import { spawn } from 'node:child_process'
|
|
17
18
|
import fs from 'node:fs'
|
|
19
|
+
import net from 'node:net'
|
|
18
20
|
import os from 'node:os'
|
|
19
21
|
import path from 'node:path'
|
|
20
22
|
import { fileURLToPath } from 'node:url'
|
|
@@ -31,6 +33,30 @@ export function distCliEntry(): string {
|
|
|
31
33
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'cli.js')
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
// process.execPath is the resolved binary, for example Homebrew's
|
|
37
|
+
// Cellar/node/26.12.0/bin/node, which `brew upgrade node` deletes. Prefer a
|
|
38
|
+
// PATH entry that links to the same binary (/opt/homebrew/bin/node): it
|
|
39
|
+
// survives upgrades. Scripts and respawns that outlive this process use it.
|
|
40
|
+
export function stableNodePath(): string {
|
|
41
|
+
const real = realpath(process.execPath)
|
|
42
|
+
if (!real) return process.execPath
|
|
43
|
+
const name = path.basename(process.execPath)
|
|
44
|
+
for (const directory of (process.env['PATH'] ?? '').split(path.delimiter)) {
|
|
45
|
+
if (!path.isAbsolute(directory)) continue
|
|
46
|
+
const candidate = path.join(directory, name)
|
|
47
|
+
if (realpath(candidate) === real) return candidate
|
|
48
|
+
}
|
|
49
|
+
return process.execPath
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function realpath(file: string): string | null {
|
|
53
|
+
try {
|
|
54
|
+
return fs.realpathSync(file)
|
|
55
|
+
} catch {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
// Windows has no autostart: it would need a .cmd script and `start`.
|
|
35
61
|
export async function writeAutostartScript({ dataDir, args }: { dataDir: string; args: readonly string[] }): Promise<Error | void> {
|
|
36
62
|
if (process.platform === 'win32') return
|
|
@@ -45,13 +71,14 @@ export async function writeAutostartScript({ dataDir, args }: { dataDir: string;
|
|
|
45
71
|
#!/bin/sh
|
|
46
72
|
# Generated by Kimaki on bot start. The Kimaki OpenCode plugin opens it when no bot runs.
|
|
47
73
|
unset KIMAKI_LOCK_PORT KIMAKI_BOT_TOKEN KIMAKI_OPENCODE_SERVICE_FILE KIMAKI_DATA_DIR KIMAKI_SUPERVISED
|
|
48
|
-
node=${quote(
|
|
74
|
+
node=${quote(stableNodePath())}
|
|
49
75
|
[ -x "$node" ] || node=node
|
|
50
76
|
echo 'Kimaki autostart'
|
|
51
|
-
echo 'OpenCode started and no Kimaki bot
|
|
77
|
+
echo 'OpenCode started and no Kimaki bot listened on port ${DEFAULT_LOCK_PORT}, so the Kimaki'
|
|
52
78
|
echo 'OpenCode plugin opened this window to start the Discord bot.'
|
|
53
79
|
echo 'The bot lets you talk to your OpenCode sessions from Discord.'
|
|
54
80
|
echo 'Close this window or press Ctrl+C to stop the bot.'
|
|
81
|
+
echo 'To turn autostart off, start kimaki once with --no-autostart.'
|
|
55
82
|
echo "Node: $node"
|
|
56
83
|
echo "Script: $0"
|
|
57
84
|
echo
|
|
@@ -61,16 +88,34 @@ export async function writeAutostartScript({ dataDir, args }: { dataDir: string;
|
|
|
61
88
|
.catch((cause: Error) => cause)
|
|
62
89
|
}
|
|
63
90
|
|
|
91
|
+
export function removeAutostartScript({ dataDir }: { dataDir: string }): Promise<Error | void> {
|
|
92
|
+
return fs.promises.rm(path.join(dataDir, SCRIPT_NAME), { force: true }).catch((cause: Error) => cause)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// True only when the connect is refused. The kernel accepts into the listen
|
|
96
|
+
// backlog even while the bot's event loop is busy, and a timeout (busy OpenCode
|
|
97
|
+
// start) is not proof of a free port: opening a window then is the bug of #243.
|
|
98
|
+
function lockPortIsFree(port: number): Promise<boolean> {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
const socket = net.connect({ host: '127.0.0.1', port })
|
|
101
|
+
const done = (free: boolean) => {
|
|
102
|
+
socket.destroy()
|
|
103
|
+
resolve(free)
|
|
104
|
+
}
|
|
105
|
+
socket.setTimeout(5_000, () => done(false))
|
|
106
|
+
socket.once('connect', () => done(false))
|
|
107
|
+
socket.once('error', (error) => done(Reflect.get(error, 'code') === 'ECONNREFUSED'))
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
64
111
|
// Only the default install: HOME and the default port, never KIMAKI_* env.
|
|
65
112
|
// Agent shells export KIMAKI_DATA_DIR, so test OpenCode servers started from
|
|
66
113
|
// them would otherwise start the real bot. Test servers set HOME to a temp dir.
|
|
67
114
|
async function startIfStopped(): Promise<void> {
|
|
68
115
|
const file = path.join(os.homedir(), '.kimaki', SCRIPT_NAME)
|
|
69
116
|
if (!fs.existsSync(file)) return
|
|
70
|
-
// Any
|
|
71
|
-
|
|
72
|
-
.then(() => true, () => false)
|
|
73
|
-
if (running) return
|
|
117
|
+
// Any listener means the port is taken (V2 or V1 bot); it could not bind anyway.
|
|
118
|
+
if (!(await lockPortIsFree(DEFAULT_LOCK_PORT))) return
|
|
74
119
|
// macOS opens .command files in the default terminal app; -g keeps it in the background.
|
|
75
120
|
const [command, args] = process.platform === 'darwin' ? ['open', ['-g', file]] as const : ['sh', [file]] as const
|
|
76
121
|
const child = spawn(command, args, { detached: true, stdio: 'ignore' })
|
package/src/bot.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { ChannelType, type Client, type TextChannel, type ThreadChannel } from '
|
|
|
11
11
|
|
|
12
12
|
import type { AgentUi } from './agent-ui.ts'
|
|
13
13
|
import type { Analytics } from './analytics.ts'
|
|
14
|
+
import type { PromptSection } from './builtin-skills.ts'
|
|
14
15
|
import type { KimakiDb } from './db.ts'
|
|
15
16
|
import type { EffectsRunner } from './effects.ts'
|
|
16
17
|
import { ConfigError, DbError, DiscordError, OpenCodeError, OpenCodeUnavailableError } from './errors.ts'
|
|
@@ -29,6 +30,11 @@ export type Author = { id: string; username: string }
|
|
|
29
30
|
|
|
30
31
|
export type PromptFile = { uri: string; name: string }
|
|
31
32
|
|
|
33
|
+
// Prompt cache warmup windows (plugin/coordinator.ts): a keep-alive every
|
|
34
|
+
// intervalMs while a coordinator's channel is busy (at most maxMs after its
|
|
35
|
+
// last turn) or a session sleeps.
|
|
36
|
+
export type WarmupTimers = { intervalMs: number; maxMs: number }
|
|
37
|
+
|
|
32
38
|
export type ModelChoice = { providerID: string; id: string; variant: string | null }
|
|
33
39
|
|
|
34
40
|
// Operations of feature-owned state that non-interaction code needs. Wizard
|
|
@@ -69,8 +75,13 @@ export type Bot = {
|
|
|
69
75
|
allowAllUsers: boolean
|
|
70
76
|
// Built-in skill ids (builtin-skills.ts) denied through the permissions of new sessions.
|
|
71
77
|
disabledSkills: readonly string[]
|
|
72
|
-
// `kimaki --
|
|
73
|
-
|
|
78
|
+
// `kimaki --disable-prompt`: static instruction parts left out of new sessions.
|
|
79
|
+
disabledPrompt: readonly PromptSection[]
|
|
80
|
+
// Default for channels without a channel_coordinator row (`kimaki --coordinator`, on unless false):
|
|
81
|
+
// project channel messages go to one long coordinator session in the channel.
|
|
82
|
+
coordinatorDefault: boolean
|
|
83
|
+
// Short warmup windows (tests), written into session markers; null uses the plugin defaults.
|
|
84
|
+
warmupTimers: WarmupTimers | null
|
|
74
85
|
features: BotFeatures
|
|
75
86
|
}
|
|
76
87
|
|
|
@@ -101,6 +112,11 @@ export async function sessionDirectory(bot: Pick<Bot, 'opencode'>, sessionId: st
|
|
|
101
112
|
return info.location.directory
|
|
102
113
|
}
|
|
103
114
|
|
|
115
|
+
// A project channel in coordinator mode: its /toggle-coordinator choice, else the default.
|
|
116
|
+
export function isCoordinatorChannel(bot: Bot, channelId: string): boolean {
|
|
117
|
+
return bot.store.getState().coordinator[channelId] ?? bot.coordinatorDefault
|
|
118
|
+
}
|
|
119
|
+
|
|
104
120
|
export function rootSession(bot: Bot, threadId: string): OpenCodeError | string {
|
|
105
121
|
return bot.store.getState().roots[threadId] ?? new OpenCodeError({ operation: `find the session of thread ${threadId}` })
|
|
106
122
|
}
|
|
@@ -112,13 +128,22 @@ export function threadOfSession(bot: Bot, sessionId: string): string | null {
|
|
|
112
128
|
return threadId && roots[threadId] === sessionId ? threadId : null
|
|
113
129
|
}
|
|
114
130
|
|
|
115
|
-
|
|
131
|
+
// Where a root session talks to the user: its thread, or a coordinator project
|
|
132
|
+
// channel (the channel itself is the conversation, roots[channelId]).
|
|
133
|
+
export type Conversation = ThreadChannel | TextChannel
|
|
134
|
+
|
|
135
|
+
// The project channel of a conversation.
|
|
136
|
+
export function projectChannelOf(conversation: Conversation): string | null {
|
|
137
|
+
return conversation.isThread() ? conversation.parentId : conversation.id
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function fetchConversation(bot: Bot, id: string): Promise<DiscordError | ConfigError | Conversation> {
|
|
116
141
|
const channel = await bot.discord.channels
|
|
117
|
-
.fetch(
|
|
118
|
-
.catch((cause) => new DiscordError({ operation: `fetch
|
|
142
|
+
.fetch(id)
|
|
143
|
+
.catch((cause) => new DiscordError({ operation: `fetch conversation ${id}`, cause }))
|
|
119
144
|
if (channel instanceof Error) return channel
|
|
120
|
-
if (
|
|
121
|
-
return channel
|
|
145
|
+
if (channel?.isThread() || channel?.type === ChannelType.GuildText) return channel
|
|
146
|
+
return new ConfigError({ reason: 'Target is not a thread or a text channel' })
|
|
122
147
|
}
|
|
123
148
|
|
|
124
149
|
export async function textChannel(bot: Bot, channelId: string): Promise<DiscordError | TextChannel> {
|
|
@@ -136,7 +161,6 @@ export type SessionMarker = {
|
|
|
136
161
|
fields: { readonly [key: string]: JsonValue }
|
|
137
162
|
dataDir: string | null
|
|
138
163
|
lockPort: number | null
|
|
139
|
-
messageTool: boolean
|
|
140
164
|
// Explicit `kimaki send --parent-session`, so /resume keeps the instruction line.
|
|
141
165
|
parentSessionId: string | null
|
|
142
166
|
// The scheduled run stored at session.create, so /resume keeps the same instruction section.
|
|
@@ -150,7 +174,7 @@ function jsonObject(value: JsonValue | undefined): { readonly [key: string]: Jso
|
|
|
150
174
|
export function readMarker(metadata: SessionMetadata | undefined): SessionMarker | null {
|
|
151
175
|
const fields = jsonObject(metadata?.['kimaki'])
|
|
152
176
|
if (!fields) return null
|
|
153
|
-
const { dataDir, lockPort,
|
|
177
|
+
const { dataDir, lockPort, parentSessionId } = fields
|
|
154
178
|
const task = jsonObject(fields['task'])
|
|
155
179
|
const taskId = task?.['id']
|
|
156
180
|
const cronExpr = task?.['cronExpr']
|
|
@@ -159,7 +183,6 @@ export function readMarker(metadata: SessionMetadata | undefined): SessionMarker
|
|
|
159
183
|
fields,
|
|
160
184
|
dataDir: typeof dataDir === 'string' ? dataDir : null,
|
|
161
185
|
lockPort: typeof lockPort === 'number' ? lockPort : null,
|
|
162
|
-
messageTool: messageTool === true,
|
|
163
186
|
parentSessionId: typeof parentSessionId === 'string' ? parentSessionId : null,
|
|
164
187
|
task: typeof taskId === 'number'
|
|
165
188
|
? { id: taskId, cronExpr: typeof cronExpr === 'string' ? cronExpr : null, timezone: typeof timezone === 'string' ? timezone : null }
|
|
@@ -167,15 +190,9 @@ export function readMarker(metadata: SessionMetadata | undefined): SessionMarker
|
|
|
167
190
|
}
|
|
168
191
|
}
|
|
169
192
|
|
|
170
|
-
// The marker fields that
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return { dataDir: bot.dataDir, lockPort: bot.lockPort, messageTool: bot.messageTool }
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// False when the session marker was written by another bot run or with another --message-tool.
|
|
177
|
-
export function markerIsCurrent(bot: Pick<Bot, 'dataDir' | 'lockPort' | 'messageTool'>, marker: SessionMarker): boolean {
|
|
178
|
-
return marker.dataDir === bot.dataDir && marker.lockPort === bot.lockPort && marker.messageTool === bot.messageTool
|
|
193
|
+
// The marker fields that point the agent's `kimaki` calls at this bot.
|
|
194
|
+
export function cliContext(bot: Bot) {
|
|
195
|
+
return { dataDir: bot.dataDir, lockPort: bot.lockPort }
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
// Default model of new sessions in channels without their own (/model "All channels").
|
package/src/btw.e2e.test.ts
CHANGED
|
@@ -87,14 +87,14 @@ test('. btw forks into a side thread while the source run continues', async () =
|
|
|
87
87
|
--- from: assistant (TestBot)
|
|
88
88
|
Session forked! Continue in <#THREAD>
|
|
89
89
|
slow-done
|
|
90
|
-
-# *
|
|
90
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
91
91
|
`)
|
|
92
92
|
expect(hideIds(await discord.thread(fork.id).text())).toMatchInlineSnapshot(`
|
|
93
93
|
"--- from: assistant (TestBot)
|
|
94
94
|
Reusing context from <#THREAD> to answer prompt...
|
|
95
95
|
What is 2+2 side-marker
|
|
96
96
|
side answer
|
|
97
|
-
-# *
|
|
97
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
98
98
|
`)
|
|
99
99
|
const rows = await bot.db.query.thread_sessions.findMany({ where: { thread_id: fork.id } })
|
|
100
100
|
expect(rows.length).toBe(1)
|
|
@@ -125,6 +125,6 @@ test('/btw forks from a thread with an idle session', async () => {
|
|
|
125
125
|
Reusing context from <#THREAD> to answer prompt...
|
|
126
126
|
Side slash-marker
|
|
127
127
|
slash answer
|
|
128
|
-
-# *
|
|
128
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
129
129
|
`)
|
|
130
130
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { expect, test } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import { BUILTIN_SKILL_IDS, disabledSkillRules, parseDisabledSkills, readBuiltinSkills } from './builtin-skills.ts'
|
|
3
|
+
import { BUILTIN_SKILL_IDS, disabledSkillRules, parseDisabledPromptSections, parseDisabledSkills, PROMPT_SECTIONS, readBuiltinSkills } from './builtin-skills.ts'
|
|
4
|
+
import { baseInstructions } from './system-prompt.ts'
|
|
4
5
|
|
|
5
6
|
test('every built-in skill file parses', async () => {
|
|
6
7
|
const skills = await readBuiltinSkills()
|
|
@@ -27,3 +28,74 @@ test('--disable-skills accepts all, a list, and rejects unknown ids', () => {
|
|
|
27
28
|
]
|
|
28
29
|
`)
|
|
29
30
|
})
|
|
31
|
+
|
|
32
|
+
test('--disable-prompt removes exactly the headings of each section', () => {
|
|
33
|
+
expect(parseDisabledPromptSections(['critique-diffs,discord-callouts', 'search-result-urls'])).toEqual(['critique-diffs', 'discord-callouts', 'search-result-urls'])
|
|
34
|
+
expect(parseDisabledPromptSections(['critique'])).toBeInstanceOf(Error)
|
|
35
|
+
const input = { sessionId: 'ses_1', threadId: '2', channelId: '3', guildId: '4', userId: '5', dataDir: '/data', channelTopic: null, agents: [], parentSessionId: null }
|
|
36
|
+
const headings = (text: string) => text.split('\n').filter((line) => line.startsWith('## '))
|
|
37
|
+
const full = headings(baseInstructions(input))
|
|
38
|
+
const removed = Object.fromEntries(PROMPT_SECTIONS.map((section) => {
|
|
39
|
+
const kept = new Set(headings(baseInstructions({ ...input, disabled: [section] })))
|
|
40
|
+
return [section, full.filter((heading) => !kept.has(heading))]
|
|
41
|
+
}))
|
|
42
|
+
expect(removed).toMatchInlineSnapshot(`
|
|
43
|
+
{
|
|
44
|
+
"action-buttons": [
|
|
45
|
+
"## action buttons",
|
|
46
|
+
"## ending conversations with options",
|
|
47
|
+
],
|
|
48
|
+
"critique-diffs": [
|
|
49
|
+
"## showing diffs",
|
|
50
|
+
],
|
|
51
|
+
"cross-project-commands": [
|
|
52
|
+
"## cross-project commands",
|
|
53
|
+
],
|
|
54
|
+
"diagrams": [
|
|
55
|
+
"## diagrams",
|
|
56
|
+
],
|
|
57
|
+
"discord-callouts": [
|
|
58
|
+
"## Callouts in Kimaki Discord",
|
|
59
|
+
],
|
|
60
|
+
"discord-permissions": [
|
|
61
|
+
"## permissions",
|
|
62
|
+
],
|
|
63
|
+
"file-upload-requests": [
|
|
64
|
+
"## requesting files from the user",
|
|
65
|
+
],
|
|
66
|
+
"kimaki-debugging": [
|
|
67
|
+
"## debugging kimaki issues",
|
|
68
|
+
],
|
|
69
|
+
"markdown-formatting": [
|
|
70
|
+
"## markdown formatting",
|
|
71
|
+
],
|
|
72
|
+
"search-result-urls": [
|
|
73
|
+
"## URLs in search results",
|
|
74
|
+
],
|
|
75
|
+
"session-abort": [
|
|
76
|
+
"## aborting a session",
|
|
77
|
+
],
|
|
78
|
+
"session-sleep": [
|
|
79
|
+
"## sleeping the session",
|
|
80
|
+
],
|
|
81
|
+
"waiting-sessions": [
|
|
82
|
+
"## waiting for a session to finish",
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
`)
|
|
86
|
+
expect(headings(baseInstructions({ ...input, disabled: [...PROMPT_SECTIONS] }))).toMatchInlineSnapshot(`
|
|
87
|
+
[
|
|
88
|
+
"## Discord output",
|
|
89
|
+
"## shell tool",
|
|
90
|
+
"## commit messages",
|
|
91
|
+
"## uploading files to discord",
|
|
92
|
+
"## generating audio from text",
|
|
93
|
+
"## archiving the current thread",
|
|
94
|
+
"## updating the session title",
|
|
95
|
+
"## discord user mentions",
|
|
96
|
+
"## starting new sessions from CLI",
|
|
97
|
+
"## scheduled sends and routines",
|
|
98
|
+
"## reading other sessions",
|
|
99
|
+
]
|
|
100
|
+
`)
|
|
101
|
+
})
|
package/src/builtin-skills.ts
CHANGED
|
@@ -53,6 +53,39 @@ export function parseDisabledSkills(value: string | undefined): ConfigError | Bu
|
|
|
53
53
|
return items as BuiltinSkillId[]
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Static parts of the session instructions (system-prompt.ts) that
|
|
57
|
+
// `kimaki --disable-prompt <section>` removes. Here, not in system-prompt.ts,
|
|
58
|
+
// so the CLI help can list them without loading zod.
|
|
59
|
+
export const PROMPT_SECTIONS = [
|
|
60
|
+
'discord-permissions',
|
|
61
|
+
'kimaki-debugging',
|
|
62
|
+
'file-upload-requests',
|
|
63
|
+
'action-buttons',
|
|
64
|
+
'session-sleep',
|
|
65
|
+
'session-abort',
|
|
66
|
+
'cross-project-commands',
|
|
67
|
+
'waiting-sessions',
|
|
68
|
+
'critique-diffs',
|
|
69
|
+
'markdown-formatting',
|
|
70
|
+
'discord-callouts',
|
|
71
|
+
'search-result-urls',
|
|
72
|
+
'diagrams',
|
|
73
|
+
] as const
|
|
74
|
+
|
|
75
|
+
export type PromptSection = (typeof PROMPT_SECTIONS)[number]
|
|
76
|
+
|
|
77
|
+
function isPromptSection(item: string): item is PromptSection {
|
|
78
|
+
return PROMPT_SECTIONS.some((section) => section === item)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Repeatable flag; each value can also be a comma separated list.
|
|
82
|
+
export function parseDisabledPromptSections(values: readonly string[] | undefined): ConfigError | PromptSection[] {
|
|
83
|
+
const items = (values ?? []).flatMap((value) => value.split(',')).map((item) => item.trim()).filter(Boolean)
|
|
84
|
+
const unknown = items.filter((item) => !isPromptSection(item))
|
|
85
|
+
if (unknown.length > 0) return new ConfigError({ reason: `Unknown prompt section ${unknown.join(', ')}. Choose from: ${PROMPT_SECTIONS.join(', ')}` })
|
|
86
|
+
return items.filter(isPromptSection)
|
|
87
|
+
}
|
|
88
|
+
|
|
56
89
|
// OpenCode hides a skill from the model and the skill tool when its permission is denied.
|
|
57
90
|
export function disabledSkillRules(disabled: readonly string[]) {
|
|
58
91
|
return disabled.map((id) => ({ action: 'skill', resource: id, effect: 'deny' }) as const)
|
|
@@ -68,10 +68,10 @@ test('agents loaded after startup get their slash commands', async () => {
|
|
|
68
68
|
await waitForFooter({ discord, threadId: thread.id })
|
|
69
69
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
70
70
|
"--- from: assistant (TestBot)
|
|
71
|
-
» **tommy:** Go late-marker
|
|
71
|
+
-# » **tommy:** Go late-marker
|
|
72
72
|
-# *using deterministic-provider/deterministic-v2 ⋅ late*
|
|
73
73
|
late reply
|
|
74
|
-
-# *
|
|
74
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2 ⋅ late*"
|
|
75
75
|
`)
|
|
76
76
|
})
|
|
77
77
|
|