kimaki 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-model.e2e.test.js +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -228
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Verify startup default channels require local self-hosted guild configuration.
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, expect, test } from 'vitest'
|
|
5
|
+
import { ChannelType, Client, Events, GatewayIntentBits } from 'discord.js'
|
|
6
|
+
import { DigitalDiscord } from 'discord-digital-twin/src'
|
|
7
|
+
import { setDataDir } from './config.js'
|
|
8
|
+
import { closeDb } from './db.js'
|
|
9
|
+
import { findChannelsByDirectory, initDatabase, setChannelDirectory } from './database.js'
|
|
10
|
+
import { createProjectChannels } from './channel-management.js'
|
|
11
|
+
import { ensureDefaultChannelsWithWelcome } from './cli-runner.js'
|
|
12
|
+
import { registerInteractionHandler } from './interaction-handler.js'
|
|
13
|
+
|
|
14
|
+
const trustedGuildId = '100000000000000111'
|
|
15
|
+
const otherGuildId = '100000000000000222'
|
|
16
|
+
const projectChannelId = '100000000000000333'
|
|
17
|
+
let directory: string
|
|
18
|
+
let discord: DigitalDiscord
|
|
19
|
+
let client: Client
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
const root = path.resolve('tmp/default-channel-provisioning')
|
|
23
|
+
fs.mkdirSync(root, { recursive: true })
|
|
24
|
+
directory = fs.mkdtempSync(path.join(root, 'run-'))
|
|
25
|
+
setDataDir(directory)
|
|
26
|
+
await closeDb()
|
|
27
|
+
await initDatabase()
|
|
28
|
+
discord = new DigitalDiscord({
|
|
29
|
+
dbUrl: `file:${path.join(directory, 'discord.db')}`,
|
|
30
|
+
users: [{ id: '100000000000000555', username: 'server-owner' }],
|
|
31
|
+
guilds: [
|
|
32
|
+
{ id: trustedGuildId, name: 'Configured', ownerId: '100000000000000555', channels: [
|
|
33
|
+
{ id: projectChannelId, name: 'project', type: ChannelType.GuildText },
|
|
34
|
+
] },
|
|
35
|
+
{ id: otherGuildId, name: 'Unconfigured', ownerId: '100000000000000555', channels: [
|
|
36
|
+
{ name: 'project', type: ChannelType.GuildText, topic: directory },
|
|
37
|
+
] },
|
|
38
|
+
],
|
|
39
|
+
})
|
|
40
|
+
await discord.start()
|
|
41
|
+
client = new Client({
|
|
42
|
+
intents: [GatewayIntentBits.Guilds],
|
|
43
|
+
rest: { api: discord.restUrl, version: '10' },
|
|
44
|
+
})
|
|
45
|
+
const ready = new Promise<void>((resolve) => client.once(Events.ClientReady, () => resolve()))
|
|
46
|
+
await client.login(discord.botToken)
|
|
47
|
+
await ready
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
afterEach(async () => {
|
|
51
|
+
await client?.destroy()
|
|
52
|
+
await discord?.stop()
|
|
53
|
+
await closeDb()
|
|
54
|
+
fs.rmSync(directory, { recursive: true, force: true })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('self-hosted startup does not configure any guild without a live local mapping', async () => {
|
|
58
|
+
await setChannelDirectory({
|
|
59
|
+
channelId: '100000000000000444',
|
|
60
|
+
directory,
|
|
61
|
+
channelType: 'text',
|
|
62
|
+
guildId: otherGuildId,
|
|
63
|
+
})
|
|
64
|
+
const before = await findChannelsByDirectory({})
|
|
65
|
+
const created = await ensureDefaultChannelsWithWelcome({
|
|
66
|
+
guilds: [...client.guilds.cache.values()], discordClient: client,
|
|
67
|
+
appId: discord.botUserId, isGatewayMode: false,
|
|
68
|
+
})
|
|
69
|
+
expect(created).toEqual([])
|
|
70
|
+
expect(await findChannelsByDirectory({})).toEqual(before)
|
|
71
|
+
for (const guild of client.guilds.cache.values()) {
|
|
72
|
+
expect((await guild.channels.fetch()).size).toBe(1)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('setup commands require a local channel mapping even for the server owner', async () => {
|
|
77
|
+
registerInteractionHandler({ discordClient: client, appId: discord.botUserId })
|
|
78
|
+
const channel = discord.channel(projectChannelId)
|
|
79
|
+
// An invalid project name keeps the regression safe even when the guard fails.
|
|
80
|
+
const options = [{ name: 'name', type: 3, value: '---' }]
|
|
81
|
+
const ignored = await channel.user('100000000000000555').runSlashCommand({
|
|
82
|
+
name: 'create-new-project', options,
|
|
83
|
+
})
|
|
84
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
85
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
86
|
+
expect((await channel.getInteractionResponse(ignored.id))?.acknowledged ?? false).toBe(false)
|
|
87
|
+
}
|
|
88
|
+
expect(await findChannelsByDirectory({})).toEqual([])
|
|
89
|
+
|
|
90
|
+
await setChannelDirectory({ channelId: projectChannelId, directory, channelType: 'text' })
|
|
91
|
+
const allowed = await channel.user('100000000000000555').runSlashCommand({
|
|
92
|
+
name: 'create-new-project', options,
|
|
93
|
+
})
|
|
94
|
+
await channel.waitForInteractionAck({ interactionId: allowed.id })
|
|
95
|
+
await channel.waitForBotReply()
|
|
96
|
+
expect(await channel.text()).toMatchInlineSnapshot(`
|
|
97
|
+
"--- from: assistant (TestBot)
|
|
98
|
+
Invalid project name"
|
|
99
|
+
`)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
for (const setup of ['legacy mapping', 'explicit onboarding']) {
|
|
103
|
+
test(`self-hosted startup provisions only the configured guild after ${setup}`, async () => {
|
|
104
|
+
const guild = client.guilds.cache.get(trustedGuildId)!
|
|
105
|
+
if (setup === 'legacy mapping') {
|
|
106
|
+
await setChannelDirectory({ channelId: projectChannelId, directory, channelType: 'text' })
|
|
107
|
+
} else {
|
|
108
|
+
await createProjectChannels({ guild, projectDirectory: path.join(directory, 'project'), analyticsSource: 'onboarding' })
|
|
109
|
+
}
|
|
110
|
+
expect((await findChannelsByDirectory({}))[0]?.guild_id).toBeNull()
|
|
111
|
+
// Fetching actual guild channels must work even when startup cache is empty.
|
|
112
|
+
guild.channels.cache.clear()
|
|
113
|
+
const created = await ensureDefaultChannelsWithWelcome({
|
|
114
|
+
guilds: [...client.guilds.cache.values()], discordClient: client,
|
|
115
|
+
appId: discord.botUserId, isGatewayMode: false,
|
|
116
|
+
})
|
|
117
|
+
for (const channel of created) {
|
|
118
|
+
expect(await discord.channel(channel.id).text()).toMatchInlineSnapshot(`
|
|
119
|
+
"--- from: assistant (TestBot)
|
|
120
|
+
**Kimaki** lets you code from Discord. Send a message in any project channel and an AI agent edits code, runs commands, and searches your codebase — all on your machine.
|
|
121
|
+
**What you can do:**
|
|
122
|
+
- Use \`/add-project\` to create a Discord channel linked to one OpenCode project (git repo)
|
|
123
|
+
- Collaborate with teammates in the same session
|
|
124
|
+
- Upload images and files, the bot can share screenshots back
|
|
125
|
+
Want to build an example browser game? Respond in this thread."
|
|
126
|
+
`)
|
|
127
|
+
}
|
|
128
|
+
expect(created.map((channel) => channel.guildId)).toEqual([trustedGuildId])
|
|
129
|
+
expect((await client.guilds.cache.get(otherGuildId)!.channels.fetch()).size).toBe(1)
|
|
130
|
+
expect(await ensureDefaultChannelsWithWelcome({
|
|
131
|
+
guilds: [...client.guilds.cache.values()], discordClient: client,
|
|
132
|
+
appId: discord.botUserId, isGatewayMode: false,
|
|
133
|
+
})).toEqual([])
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
test('gateway startup provisions every proxy-authorized guild without local mappings', async () => {
|
|
138
|
+
const created = await ensureDefaultChannelsWithWelcome({
|
|
139
|
+
guilds: [...client.guilds.cache.values()], discordClient: client,
|
|
140
|
+
appId: discord.botUserId, isGatewayMode: true,
|
|
141
|
+
})
|
|
142
|
+
for (const channel of created) {
|
|
143
|
+
expect(await discord.channel(channel.id).text()).toMatchInlineSnapshot(`
|
|
144
|
+
"--- from: assistant (TestBot)
|
|
145
|
+
**Kimaki** lets you code from Discord. Send a message in any project channel and an AI agent edits code, runs commands, and searches your codebase — all on your machine.
|
|
146
|
+
**What you can do:**
|
|
147
|
+
- Use \`/add-project\` to create a Discord channel linked to one OpenCode project (git repo)
|
|
148
|
+
- Collaborate with teammates in the same session
|
|
149
|
+
- Upload images and files, the bot can share screenshots back
|
|
150
|
+
Want to build an example browser game? Respond in this thread."
|
|
151
|
+
`)
|
|
152
|
+
}
|
|
153
|
+
expect(created.map((channel) => channel.guildId).sort()).toEqual([trustedGuildId, otherGuildId])
|
|
154
|
+
expect(await findChannelsByDirectory({})).toHaveLength(2)
|
|
155
|
+
})
|
package/src/discord-bot.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
getChannelMentionMode,
|
|
24
24
|
getChannelDirectory,
|
|
25
25
|
cancelAllPendingIpcRequests,
|
|
26
|
+
consumeSessionSleepWake,
|
|
26
27
|
deleteChannelDirectoryById,
|
|
27
28
|
createPendingWorkspace,
|
|
28
29
|
setWorkspaceReady,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
import { formatAutoWorktreeName, createWorktreeInBackground, worktreeCreatingMessage } from './commands/new-worktree.js'
|
|
34
35
|
import { resolveSessionWorkingDirectory, git, isGitRepositoryRoot } from './worktrees.js'
|
|
35
36
|
import { WORKTREE_PREFIX } from './commands/merge-worktree.js'
|
|
37
|
+
import { STATUS_PREFIX } from './message-formatting.js'
|
|
36
38
|
import {
|
|
37
39
|
escapeBackticksInCodeBlocks,
|
|
38
40
|
splitMarkdownForDiscord,
|
|
@@ -88,6 +90,8 @@ import {
|
|
|
88
90
|
getRuntime,
|
|
89
91
|
getOrCreateRuntime,
|
|
90
92
|
disposeRuntime,
|
|
93
|
+
reserveThreadIngress,
|
|
94
|
+
runInThreadIngressSlot,
|
|
91
95
|
} from './session-handler/thread-session-runtime.js'
|
|
92
96
|
import { runShellCommand } from './commands/run-command.js'
|
|
93
97
|
import { registerInteractionHandler } from './interaction-handler.js'
|
|
@@ -112,7 +116,7 @@ export {
|
|
|
112
116
|
closeDatabase,
|
|
113
117
|
getChannelDirectory,
|
|
114
118
|
} from './database.js'
|
|
115
|
-
export { initializeOpencodeForDirectory } from './opencode.js'
|
|
119
|
+
export { initializeOpencodeForDirectory, assertCompatibleOpencodeVersion } from './opencode.js'
|
|
116
120
|
export {
|
|
117
121
|
escapeBackticksInCodeBlocks,
|
|
118
122
|
splitMarkdownForDiscord,
|
|
@@ -144,6 +148,11 @@ import path from 'node:path'
|
|
|
144
148
|
import dedent from 'string-dedent'
|
|
145
149
|
import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js'
|
|
146
150
|
import { writeHeapSnapshot, startHeapMonitor } from './heap-monitor.js'
|
|
151
|
+
import {
|
|
152
|
+
flushCpuProfiling,
|
|
153
|
+
startStdinCpuProfListener,
|
|
154
|
+
stopStdinCpuProfListener,
|
|
155
|
+
} from './cpu-profiler.js'
|
|
147
156
|
import { startTaskRunner } from './task-runner.js'
|
|
148
157
|
// Increase connection pool to prevent deadlock when multiple sessions have open SSE streams.
|
|
149
158
|
// Each session's event.subscribe() holds a connection; without enough connections,
|
|
@@ -435,8 +444,8 @@ export async function startDiscordBot({
|
|
|
435
444
|
discordLogger.error(
|
|
436
445
|
`[GATEWAY] Shard ${shardId} exceeded ${MAX_RECONNECT_ATTEMPTS} reconnect attempts, self-restarting`,
|
|
437
446
|
)
|
|
438
|
-
// Self-restart: cleanup, then
|
|
439
|
-
// restarts us. Without the wrapper this
|
|
447
|
+
// Self-restart: cleanup, then SIGKILL so the bin.ts wrapper
|
|
448
|
+
// restarts us. Without the wrapper this dies after logging a warning.
|
|
440
449
|
void selfRestart('gateway-reconnect-limit')
|
|
441
450
|
}
|
|
442
451
|
})
|
|
@@ -473,6 +482,11 @@ export async function startDiscordBot({
|
|
|
473
482
|
})
|
|
474
483
|
|
|
475
484
|
discordClient.on(Events.MessageCreate, async (message: Message) => {
|
|
485
|
+
const threadIngressSlot =
|
|
486
|
+
message.channel.isThread() || message.channel.type === ChannelType.GuildText
|
|
487
|
+
? reserveThreadIngress(message.channel.id)
|
|
488
|
+
: undefined
|
|
489
|
+
await runInThreadIngressSlot(threadIngressSlot, async () => {
|
|
476
490
|
try {
|
|
477
491
|
const isSelfBotMessage = Boolean(
|
|
478
492
|
discordClient.user && message.author?.id === discordClient.user.id,
|
|
@@ -727,6 +741,7 @@ export async function startDiscordBot({
|
|
|
727
741
|
) {
|
|
728
742
|
const shellCmd = message.content.slice(1).trim()
|
|
729
743
|
if (shellCmd) {
|
|
744
|
+
threadIngressSlot?.release()
|
|
730
745
|
const shellDir =
|
|
731
746
|
worktreeInfo?.status === 'ready' &&
|
|
732
747
|
worktreeInfo.workspace_directory
|
|
@@ -752,6 +767,7 @@ export async function startDiscordBot({
|
|
|
752
767
|
? extractBtwSuffix(message.content || '')
|
|
753
768
|
: null
|
|
754
769
|
if (btwResult?.forceBtw && projectDirectory && !isLeadingMentionToOtherUser) {
|
|
770
|
+
threadIngressSlot?.release()
|
|
755
771
|
const btwSdkDir =
|
|
756
772
|
worktreeInfo?.status === 'ready' &&
|
|
757
773
|
worktreeInfo.workspace_directory
|
|
@@ -821,7 +837,8 @@ export async function startDiscordBot({
|
|
|
821
837
|
// Cancel interactive UI when a real user sends a message.
|
|
822
838
|
// Context-only messages (user-to-user replies) should not interrupt
|
|
823
839
|
// the active run or dismiss pending UI.
|
|
824
|
-
|
|
840
|
+
const dismissSourceUi = async () => {
|
|
841
|
+
if (message.author.bot || isCliInjectedPrompt || isLeadingMentionToOtherUser) return
|
|
825
842
|
cancelPendingActionButtons(thread.id)
|
|
826
843
|
cancelHtmlActionsForThread(thread.id)
|
|
827
844
|
const dismissedPermission = await cancelPendingPermission(thread.id)
|
|
@@ -839,6 +856,27 @@ export async function startDiscordBot({
|
|
|
839
856
|
}
|
|
840
857
|
void cancelPendingFileUpload(thread.id)
|
|
841
858
|
}
|
|
859
|
+
if (!hasVoiceAttachment) {
|
|
860
|
+
await dismissSourceUi()
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// A sleep wake only becomes a turn if it can still claim its own row.
|
|
864
|
+
// The claim fails when the user cancelled the sleep while the wake was
|
|
865
|
+
// being posted, so the superseded wake is dropped instead of starting a
|
|
866
|
+
// turn the user already replaced. Cancellation for every other kind of
|
|
867
|
+
// message happens inside runtime.enqueueIncoming().
|
|
868
|
+
const isSleepWake = Boolean(promptMarker?.sleepWake)
|
|
869
|
+
if (isSleepWake) {
|
|
870
|
+
const claimedWake = promptMarker?.sleepId
|
|
871
|
+
? await consumeSessionSleepWake({ deliveryId: promptMarker.sleepId })
|
|
872
|
+
: false
|
|
873
|
+
if (!claimedWake) {
|
|
874
|
+
discordLogger.log(
|
|
875
|
+
`[SLEEP] ignoring superseded wake in thread ${thread.id}`,
|
|
876
|
+
)
|
|
877
|
+
return
|
|
878
|
+
}
|
|
879
|
+
}
|
|
842
880
|
|
|
843
881
|
// Expensive pre-processing (voice transcription, context fetch,
|
|
844
882
|
// attachment download) runs inside the runtime's serialized
|
|
@@ -859,6 +897,7 @@ export async function startDiscordBot({
|
|
|
859
897
|
permissions: cliInjectedPermissions,
|
|
860
898
|
injectionGuardPatterns: cliInjectedInjectionGuardPatterns,
|
|
861
899
|
parentSessionId: cliInjectedParentSessionId,
|
|
900
|
+
isSleepWake: isSleepWake || undefined,
|
|
862
901
|
noReply: isLeadingMentionToOtherUser || undefined,
|
|
863
902
|
sessionStartSource: sessionStartSource
|
|
864
903
|
? {
|
|
@@ -867,8 +906,8 @@ export async function startDiscordBot({
|
|
|
867
906
|
scheduledTaskRunId: sessionStartSource.scheduledTaskRunId,
|
|
868
907
|
}
|
|
869
908
|
: undefined,
|
|
870
|
-
preprocess: () => {
|
|
871
|
-
|
|
909
|
+
preprocess: async () => {
|
|
910
|
+
const result = await preprocessExistingThreadMessage({
|
|
872
911
|
message,
|
|
873
912
|
thread,
|
|
874
913
|
projectDirectory: resolvedProjectDir,
|
|
@@ -877,6 +916,12 @@ export async function startDiscordBot({
|
|
|
877
916
|
hasVoiceAttachment,
|
|
878
917
|
appId: currentAppId,
|
|
879
918
|
})
|
|
919
|
+
// Routing must finish before touching source UI. This chain is separate
|
|
920
|
+
// from dispatchAction, so abort waiting does not block session events.
|
|
921
|
+
if (hasVoiceAttachment && !result.skip) {
|
|
922
|
+
await dismissSourceUi()
|
|
923
|
+
}
|
|
924
|
+
return result
|
|
880
925
|
},
|
|
881
926
|
})
|
|
882
927
|
|
|
@@ -955,6 +1000,7 @@ export async function startDiscordBot({
|
|
|
955
1000
|
if (message.content?.startsWith('!')) {
|
|
956
1001
|
const shellCmd = message.content.slice(1).trim()
|
|
957
1002
|
if (shellCmd) {
|
|
1003
|
+
threadIngressSlot?.release()
|
|
958
1004
|
const loadingReply = await message.reply({
|
|
959
1005
|
content: `Running \`${shellCmd.slice(0, 1900)}\`...`,
|
|
960
1006
|
})
|
|
@@ -1038,14 +1084,9 @@ export async function startDiscordBot({
|
|
|
1038
1084
|
})
|
|
1039
1085
|
}
|
|
1040
1086
|
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
}
|
|
1045
|
-
const result = await worktreePromise
|
|
1046
|
-
if (result instanceof Error) return projectDirectory
|
|
1047
|
-
return result
|
|
1048
|
-
})()
|
|
1087
|
+
const worktreeResult = worktreePromise ? await worktreePromise : projectDirectory
|
|
1088
|
+
if (worktreeResult instanceof Error) return
|
|
1089
|
+
const sessionDirectory = worktreeResult
|
|
1049
1090
|
|
|
1050
1091
|
const channelRuntime = getOrCreateRuntime({
|
|
1051
1092
|
threadId: thread.id,
|
|
@@ -1094,6 +1135,7 @@ export async function startDiscordBot({
|
|
|
1094
1135
|
)
|
|
1095
1136
|
}
|
|
1096
1137
|
}
|
|
1138
|
+
})
|
|
1097
1139
|
})
|
|
1098
1140
|
|
|
1099
1141
|
// Handle user message edits to update queued messages.
|
|
@@ -1149,7 +1191,7 @@ export async function startDiscordBot({
|
|
|
1149
1191
|
)
|
|
1150
1192
|
await sendThreadMessage(
|
|
1151
1193
|
channel,
|
|
1152
|
-
|
|
1194
|
+
`${STATUS_PREFIX}**${displayName}** removed message from queue`,
|
|
1153
1195
|
)
|
|
1154
1196
|
} else {
|
|
1155
1197
|
discordLogger.log(
|
|
@@ -1157,7 +1199,7 @@ export async function startDiscordBot({
|
|
|
1157
1199
|
)
|
|
1158
1200
|
await sendThreadMessage(
|
|
1159
1201
|
channel,
|
|
1160
|
-
|
|
1202
|
+
`${STATUS_PREFIX}**${displayName}** edited queued message`,
|
|
1161
1203
|
)
|
|
1162
1204
|
}
|
|
1163
1205
|
}
|
|
@@ -1188,7 +1230,7 @@ export async function startDiscordBot({
|
|
|
1188
1230
|
)
|
|
1189
1231
|
await sendThreadMessage(
|
|
1190
1232
|
channel,
|
|
1191
|
-
|
|
1233
|
+
`${STATUS_PREFIX}**${removed.username}** removed message from queue`,
|
|
1192
1234
|
)
|
|
1193
1235
|
} catch (error) {
|
|
1194
1236
|
discordLogger.error(
|
|
@@ -1392,17 +1434,9 @@ export async function startDiscordBot({
|
|
|
1392
1434
|
|
|
1393
1435
|
const botThreadStartSource = parseSessionStartSourceFromMarker(marker)
|
|
1394
1436
|
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
}
|
|
1399
|
-
if (!worktreePromise) {
|
|
1400
|
-
return projectDirectory
|
|
1401
|
-
}
|
|
1402
|
-
const result = await worktreePromise
|
|
1403
|
-
if (result instanceof Error) return projectDirectory
|
|
1404
|
-
return result
|
|
1405
|
-
})()
|
|
1437
|
+
const worktreeResult = worktreePromise ? await worktreePromise : undefined
|
|
1438
|
+
if (worktreeResult instanceof Error) return
|
|
1439
|
+
const sessionDirectory = cwdDirectory ?? worktreeResult ?? projectDirectory
|
|
1406
1440
|
|
|
1407
1441
|
const runtime = getOrCreateRuntime({
|
|
1408
1442
|
threadId: thread.id,
|
|
@@ -1505,6 +1539,7 @@ export async function startDiscordBot({
|
|
|
1505
1539
|
}
|
|
1506
1540
|
|
|
1507
1541
|
startHeapMonitor()
|
|
1542
|
+
startStdinCpuProfListener()
|
|
1508
1543
|
const stopTaskRunner = startTaskRunner({ token })
|
|
1509
1544
|
const stopRuntimeIdleSweeper = startRuntimeIdleSweeper()
|
|
1510
1545
|
|
|
@@ -1533,6 +1568,14 @@ export async function startDiscordBot({
|
|
|
1533
1568
|
global.shuttingDown = true
|
|
1534
1569
|
|
|
1535
1570
|
try {
|
|
1571
|
+
stopStdinCpuProfListener()
|
|
1572
|
+
const flushed = await flushCpuProfiling()
|
|
1573
|
+
if (flushed instanceof Error) {
|
|
1574
|
+
discordLogger.warn(
|
|
1575
|
+
'Failed to flush CPU profile on shutdown:',
|
|
1576
|
+
flushed.message,
|
|
1577
|
+
)
|
|
1578
|
+
}
|
|
1536
1579
|
await stopRuntimeIdleSweeper()
|
|
1537
1580
|
await stopTaskRunner()
|
|
1538
1581
|
|
|
@@ -1626,13 +1669,11 @@ export async function startDiscordBot({
|
|
|
1626
1669
|
})
|
|
1627
1670
|
})
|
|
1628
1671
|
|
|
1629
|
-
// Self-restart:
|
|
1630
|
-
//
|
|
1631
|
-
//
|
|
1632
|
-
//
|
|
1633
|
-
// for auto-restart support.
|
|
1634
|
-
// unreliable: process.exit(0) could kill the child before it started,
|
|
1635
|
-
// there was no backoff between restarts, and spawn failures were silent.
|
|
1672
|
+
// Self-restart: die so the bin.ts wrapper restarts us with exponential
|
|
1673
|
+
// backoff and crash-loop detection. process.exit() can hang joining native
|
|
1674
|
+
// worker threads after Discord gateway failures, so SIGKILL instead.
|
|
1675
|
+
// When running without the wrapper (e.g. `tsx src/cli.ts`), the process
|
|
1676
|
+
// just dies — use `tsx src/bin.ts` for auto-restart support.
|
|
1636
1677
|
let selfRestarting = false
|
|
1637
1678
|
async function selfRestart(reason: string) {
|
|
1638
1679
|
if (selfRestarting) {
|
|
@@ -1641,6 +1682,7 @@ export async function startDiscordBot({
|
|
|
1641
1682
|
}
|
|
1642
1683
|
selfRestarting = true
|
|
1643
1684
|
discordLogger.log(`Self-restarting (reason: ${reason})...`)
|
|
1685
|
+
setTimeout(() => process.kill(process.pid, 'SIGKILL'), 15_000)
|
|
1644
1686
|
try {
|
|
1645
1687
|
await handleShutdown(reason, { skipExit: true })
|
|
1646
1688
|
} catch (error) {
|
|
@@ -1652,7 +1694,7 @@ export async function startDiscordBot({
|
|
|
1652
1694
|
'No restart wrapper detected. Run via `tsx src/bin.ts` (dev) or `kimaki` (npm) for auto-restart on crash.',
|
|
1653
1695
|
)
|
|
1654
1696
|
}
|
|
1655
|
-
process.
|
|
1697
|
+
process.kill(process.pid, 'SIGKILL')
|
|
1656
1698
|
}
|
|
1657
1699
|
|
|
1658
1700
|
process.on('SIGUSR2', () => {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Tests Discord slash command payloads for quick agent commands.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
import { buildQuickAgentSlashCommand } from './discord-command-registration.js'
|
|
5
|
+
|
|
6
|
+
describe('buildQuickAgentSlashCommand', () => {
|
|
7
|
+
test('puts variant last after prompt', () => {
|
|
8
|
+
const command = buildQuickAgentSlashCommand({
|
|
9
|
+
commandName: 'plan-agent',
|
|
10
|
+
description: 'Switch to plan agent',
|
|
11
|
+
}).toJSON()
|
|
12
|
+
|
|
13
|
+
expect(command.options?.map((option) => option.name)).toMatchInlineSnapshot(`
|
|
14
|
+
[
|
|
15
|
+
"prompt",
|
|
16
|
+
"variant",
|
|
17
|
+
]
|
|
18
|
+
`)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -65,6 +65,32 @@ function truncateCommandDescription(description: string): string {
|
|
|
65
65
|
return description.slice(0, 100)
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
export function buildQuickAgentSlashCommand({
|
|
69
|
+
commandName,
|
|
70
|
+
description,
|
|
71
|
+
}: {
|
|
72
|
+
commandName: string
|
|
73
|
+
description: string
|
|
74
|
+
}) {
|
|
75
|
+
return new SlashCommandBuilder()
|
|
76
|
+
.setName(commandName)
|
|
77
|
+
.setDescription(truncateCommandDescription(description))
|
|
78
|
+
.setDMPermission(false)
|
|
79
|
+
.addStringOption((opt) =>
|
|
80
|
+
opt
|
|
81
|
+
.setName('prompt')
|
|
82
|
+
.setDescription('Send a prompt with this agent')
|
|
83
|
+
.setRequired(false),
|
|
84
|
+
)
|
|
85
|
+
.addStringOption((opt) =>
|
|
86
|
+
opt
|
|
87
|
+
.setName('variant')
|
|
88
|
+
.setDescription('Model thinking level for this agent')
|
|
89
|
+
.setRequired(false)
|
|
90
|
+
.setAutocomplete(true),
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
68
94
|
export async function registerCommands({
|
|
69
95
|
token,
|
|
70
96
|
appId,
|
|
@@ -156,8 +182,22 @@ export async function registerCommands({
|
|
|
156
182
|
new SlashCommandBuilder()
|
|
157
183
|
.setName('merge-worktree')
|
|
158
184
|
.setDescription(
|
|
159
|
-
truncateCommandDescription('
|
|
185
|
+
truncateCommandDescription('Merge worktree commits into the default branch'),
|
|
160
186
|
)
|
|
187
|
+
.addStringOption((option) => {
|
|
188
|
+
option
|
|
189
|
+
.setName('strategy')
|
|
190
|
+
.setDescription(
|
|
191
|
+
truncateCommandDescription('How to merge commits (default: keep commits)'),
|
|
192
|
+
)
|
|
193
|
+
.setRequired(false)
|
|
194
|
+
.addChoices(
|
|
195
|
+
{ name: 'Keep commits (rebase)', value: 'rebase' },
|
|
196
|
+
{ name: 'Squash into one commit', value: 'squash' },
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return option
|
|
200
|
+
})
|
|
161
201
|
.addStringOption((option) => {
|
|
162
202
|
option
|
|
163
203
|
.setName('target-branch')
|
|
@@ -491,17 +531,7 @@ export async function registerCommands({
|
|
|
491
531
|
})
|
|
492
532
|
|
|
493
533
|
commands.push(
|
|
494
|
-
|
|
495
|
-
.setName(commandName)
|
|
496
|
-
.setDescription(truncateCommandDescription(description))
|
|
497
|
-
.setDMPermission(false)
|
|
498
|
-
.addStringOption((opt) =>
|
|
499
|
-
opt
|
|
500
|
-
.setName('prompt')
|
|
501
|
-
.setDescription('Send a prompt with this agent')
|
|
502
|
-
.setRequired(false),
|
|
503
|
-
)
|
|
504
|
-
.toJSON(),
|
|
534
|
+
buildQuickAgentSlashCommand({ commandName, description }).toJSON(),
|
|
505
535
|
)
|
|
506
536
|
}
|
|
507
537
|
|