kimaki 0.11.0 → 0.13.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 +91 -1
- package/dist/btw-prefix-detection.js +13 -15
- package/dist/btw-prefix-detection.test.js +60 -30
- package/dist/cli-runner.js +36 -12
- package/dist/cli.js +10 -0
- package/dist/commands/abort.js +1 -1
- package/dist/commands/agent.js +14 -16
- package/dist/commands/mention-mode.js +0 -1
- package/dist/commands/model-variant.js +2 -2
- package/dist/commands/model.js +47 -27
- package/dist/commands/restart-opencode-server.js +1 -1
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/unset-model.js +2 -2
- package/dist/commands/upgrade.js +1 -2
- package/dist/commands/verbosity.js +0 -1
- package/dist/commands/worktree-settings.js +0 -1
- package/dist/discord-bot.js +65 -15
- package/dist/discord-command-registration.js +1 -1
- package/dist/discord-utils.js +14 -0
- package/dist/discord-utils.test.js +51 -1
- package/dist/external-opencode-sync.js +119 -54
- package/dist/interaction-handler.js +4 -0
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +1 -1
- package/dist/message-formatting.js +91 -0
- package/dist/message-formatting.test.js +206 -1
- package/dist/message-preprocessing.js +1 -1
- package/dist/opencode-interrupt-plugin.js +14 -2
- package/dist/opencode-interrupt-plugin.test.js +22 -3
- package/dist/opencode.js +34 -158
- package/dist/queue-advanced-model-switch.e2e.test.js +1 -1
- package/dist/session-handler/agent-utils.js +9 -9
- package/dist/session-handler/thread-runtime-state.js +29 -0
- package/dist/session-handler/thread-session-runtime.js +51 -9
- package/dist/store.js +2 -0
- package/dist/system-message.test.js +16 -0
- package/dist/thread-message-queue.e2e.test.js +198 -1
- package/dist/voice-handler.js +91 -68
- package/package.json +6 -6
- package/skills/holocron/SKILL.md +432 -0
- package/skills/npm-package/SKILL.md +12 -2
- package/skills/termcast/SKILL.md +32 -846
- package/skills/tuistory/SKILL.md +71 -0
- package/src/agent-model.e2e.test.ts +117 -0
- package/src/btw-prefix-detection.test.ts +61 -30
- package/src/btw-prefix-detection.ts +15 -19
- package/src/cli-runner.ts +36 -12
- package/src/cli.ts +22 -0
- package/src/commands/abort.ts +1 -1
- package/src/commands/agent.ts +14 -17
- package/src/commands/mention-mode.ts +0 -1
- package/src/commands/model-variant.ts +2 -2
- package/src/commands/model.ts +63 -37
- package/src/commands/restart-opencode-server.ts +1 -1
- package/src/commands/undo-redo.ts +2 -2
- package/src/commands/unset-model.ts +1 -2
- package/src/commands/upgrade.ts +1 -2
- package/src/commands/verbosity.ts +0 -1
- package/src/commands/worktree-settings.ts +0 -1
- package/src/discord-bot.ts +76 -13
- package/src/discord-command-registration.ts +1 -1
- package/src/discord-utils.test.ts +63 -2
- package/src/discord-utils.ts +19 -0
- package/src/external-opencode-sync.ts +147 -64
- package/src/interaction-handler.ts +5 -0
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +1 -1
- package/src/message-formatting.test.ts +247 -1
- package/src/message-formatting.ts +93 -1
- package/src/message-preprocessing.ts +1 -1
- package/src/opencode-interrupt-plugin.test.ts +27 -3
- package/src/opencode-interrupt-plugin.ts +15 -3
- package/src/opencode.ts +36 -152
- package/src/queue-advanced-model-switch.e2e.test.ts +1 -1
- package/src/session-handler/agent-utils.ts +11 -11
- package/src/session-handler/thread-runtime-state.ts +35 -0
- package/src/session-handler/thread-session-runtime.ts +67 -8
- package/src/store.ts +17 -0
- package/src/system-message.test.ts +16 -0
- package/src/thread-message-queue.e2e.test.ts +227 -1
- package/src/voice-handler.ts +106 -78
|
@@ -19,7 +19,7 @@ import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provid
|
|
|
19
19
|
import { setDataDir } from './config.js';
|
|
20
20
|
import { store } from './store.js';
|
|
21
21
|
import { startDiscordBot } from './discord-bot.js';
|
|
22
|
-
import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, setChannelAgent, setChannelModel, } from './database.js';
|
|
22
|
+
import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, setChannelAgent, setChannelModel, getThreadSession, getSessionAgent, getChannelAgent, } from './database.js';
|
|
23
23
|
import { getDb } from './db.js';
|
|
24
24
|
import * as orm from 'drizzle-orm';
|
|
25
25
|
import * as schema from './schema.js';
|
|
@@ -258,6 +258,10 @@ describe('agent model resolution', () => {
|
|
|
258
258
|
description: `Switch to ${agentName} agent`,
|
|
259
259
|
}))
|
|
260
260
|
.setDMPermission(false)
|
|
261
|
+
.addStringOption((opt) => opt
|
|
262
|
+
.setName('prompt')
|
|
263
|
+
.setDescription('Send a prompt with this agent')
|
|
264
|
+
.setRequired(false))
|
|
261
265
|
.toJSON();
|
|
262
266
|
});
|
|
263
267
|
const rest = new REST({ version: '10', api: discord.restUrl }).setToken(discord.botToken);
|
|
@@ -681,6 +685,92 @@ describe('agent model resolution', () => {
|
|
|
681
685
|
expect(secondFooter.content).toContain(DEFAULT_MODEL);
|
|
682
686
|
expect(secondFooter.content).not.toContain(AGENT_MODEL);
|
|
683
687
|
}, 20_000);
|
|
688
|
+
test('/plan-agent with prompt starts a session with the plan agent', async () => {
|
|
689
|
+
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
690
|
+
const prompt = 'Reply with exactly: inline-plan-agent-msg';
|
|
691
|
+
const { id: interactionId } = await discord
|
|
692
|
+
.channel(TEXT_CHANNEL_ID)
|
|
693
|
+
.user(TEST_USER_ID)
|
|
694
|
+
.runSlashCommand({
|
|
695
|
+
name: 'plan-agent',
|
|
696
|
+
options: [{ name: 'prompt', type: 3, value: prompt }],
|
|
697
|
+
});
|
|
698
|
+
await discord
|
|
699
|
+
.channel(TEXT_CHANNEL_ID)
|
|
700
|
+
.waitForInteractionAck({ interactionId, timeout: 4_000 });
|
|
701
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
702
|
+
timeout: 4_000,
|
|
703
|
+
predicate: (t) => {
|
|
704
|
+
return t.name === prompt;
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
await waitForFooterMessage({
|
|
708
|
+
discord,
|
|
709
|
+
threadId: thread.id,
|
|
710
|
+
timeout: 4_000,
|
|
711
|
+
afterMessageIncludes: 'ok',
|
|
712
|
+
afterAuthorId: discord.botUserId,
|
|
713
|
+
});
|
|
714
|
+
const sessionId = await getThreadSession(thread.id);
|
|
715
|
+
expect(sessionId).toBeDefined();
|
|
716
|
+
expect(sessionId ? await getSessionAgent(sessionId) : undefined).toBe('plan');
|
|
717
|
+
expect(await getChannelAgent(TEXT_CHANNEL_ID)).toBe('test-agent');
|
|
718
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
719
|
+
"--- from: assistant (TestBot)
|
|
720
|
+
» **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-msg
|
|
721
|
+
*using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
722
|
+
⬥ ok
|
|
723
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
724
|
+
`);
|
|
725
|
+
}, 20_000);
|
|
726
|
+
test('/plan-agent with prompt in an existing thread changes the session agent', async () => {
|
|
727
|
+
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
728
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
729
|
+
content: 'Reply with exactly: inline-existing-first-msg',
|
|
730
|
+
});
|
|
731
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
732
|
+
timeout: 4_000,
|
|
733
|
+
predicate: (t) => {
|
|
734
|
+
return t.name === 'Reply with exactly: inline-existing-first-msg';
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
await waitForFooterMessage({
|
|
738
|
+
discord,
|
|
739
|
+
threadId: thread.id,
|
|
740
|
+
timeout: 4_000,
|
|
741
|
+
afterMessageIncludes: 'ok',
|
|
742
|
+
afterAuthorId: discord.botUserId,
|
|
743
|
+
});
|
|
744
|
+
const prompt = 'Reply with exactly: inline-existing-plan-msg';
|
|
745
|
+
const th = discord.thread(thread.id);
|
|
746
|
+
const { id: interactionId } = await th.user(TEST_USER_ID).runSlashCommand({
|
|
747
|
+
name: 'plan-agent',
|
|
748
|
+
options: [{ name: 'prompt', type: 3, value: prompt }],
|
|
749
|
+
});
|
|
750
|
+
await th.waitForInteractionAck({ interactionId, timeout: 4_000 });
|
|
751
|
+
await waitForFooterMessage({
|
|
752
|
+
discord,
|
|
753
|
+
threadId: thread.id,
|
|
754
|
+
timeout: 4_000,
|
|
755
|
+
afterMessageIncludes: 'inline-existing-plan-msg',
|
|
756
|
+
afterAuthorId: discord.botUserId,
|
|
757
|
+
});
|
|
758
|
+
const sessionId = await getThreadSession(thread.id);
|
|
759
|
+
expect(sessionId).toBeDefined();
|
|
760
|
+
expect(sessionId ? await getSessionAgent(sessionId) : undefined).toBe('plan');
|
|
761
|
+
expect(await getChannelAgent(TEXT_CHANNEL_ID)).toBe('test-agent');
|
|
762
|
+
expect(await th.text()).toMatchInlineSnapshot(`
|
|
763
|
+
"--- from: user (agent-model-tester)
|
|
764
|
+
Reply with exactly: inline-existing-first-msg
|
|
765
|
+
--- from: assistant (TestBot)
|
|
766
|
+
*using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
767
|
+
⬥ ok
|
|
768
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
769
|
+
» **agent-model-tester** (plan): Reply with exactly: inline-existing-plan-msg
|
|
770
|
+
⬥ ok
|
|
771
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
772
|
+
`);
|
|
773
|
+
}, 20_000);
|
|
684
774
|
test('/plan-agent inside a thread switches the model for that thread', async () => {
|
|
685
775
|
// 1. Start with test-agent on the channel
|
|
686
776
|
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
// Detects
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// Detects `. btw` suffix at the end of a Discord message, identical pattern
|
|
2
|
+
// to the queue suffix. When present the suffix is stripped and the remaining
|
|
3
|
+
// message is forked to a new btw thread via /btw.
|
|
4
|
+
//
|
|
5
|
+
// Supported forms:
|
|
6
|
+
// - punctuation + btw: ". btw", "! btw", ". btw.", "!btw."
|
|
7
|
+
// - btw as its own final line: "text\nbtw"
|
|
8
|
+
// Non-matches: "btw fix this" (start only), "hello btw" (no punctuation)
|
|
9
|
+
const BTW_SUFFIX_RE = /(?:[.!?,;:])\s*btw\.?\s*$|\n\s*btw\.?\s*$/i;
|
|
10
|
+
export function extractBtwSuffix(content) {
|
|
11
|
+
if (!BTW_SUFFIX_RE.test(content)) {
|
|
12
|
+
return { prompt: content, forceBtw: false };
|
|
6
13
|
}
|
|
7
|
-
|
|
8
|
-
const match = content.match(/^\s*btw[.,;:!?\s]\s*([\s\S]+)$/i);
|
|
9
|
-
if (!match) {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
const prompt = match[1]?.trim();
|
|
13
|
-
if (!prompt) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
return { prompt };
|
|
14
|
+
return { prompt: content.replace(BTW_SUFFIX_RE, '').trimEnd(), forceBtw: true };
|
|
17
15
|
}
|
|
@@ -1,63 +1,93 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
describe('
|
|
4
|
-
test('matches
|
|
5
|
-
expect(
|
|
2
|
+
import { extractBtwSuffix } from './btw-prefix-detection.js';
|
|
3
|
+
describe('extractBtwSuffix', () => {
|
|
4
|
+
test('matches after period', () => {
|
|
5
|
+
expect(extractBtwSuffix('fix the bug. btw')).toMatchInlineSnapshot(`
|
|
6
6
|
{
|
|
7
|
-
"
|
|
7
|
+
"forceBtw": true,
|
|
8
|
+
"prompt": "fix the bug",
|
|
8
9
|
}
|
|
9
10
|
`);
|
|
10
11
|
});
|
|
11
|
-
test('matches
|
|
12
|
-
expect(
|
|
12
|
+
test('matches after exclamation', () => {
|
|
13
|
+
expect(extractBtwSuffix('done! btw')).toMatchInlineSnapshot(`
|
|
13
14
|
{
|
|
14
|
-
"
|
|
15
|
+
"forceBtw": true,
|
|
16
|
+
"prompt": "done",
|
|
15
17
|
}
|
|
16
18
|
`);
|
|
17
19
|
});
|
|
18
|
-
test('
|
|
19
|
-
expect(
|
|
20
|
+
test('matches after comma', () => {
|
|
21
|
+
expect(extractBtwSuffix('sure, btw')).toMatchInlineSnapshot(`
|
|
20
22
|
{
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
+
"forceBtw": true,
|
|
24
|
+
"prompt": "sure",
|
|
23
25
|
}
|
|
24
26
|
`);
|
|
25
27
|
});
|
|
26
|
-
test('matches
|
|
27
|
-
expect(
|
|
28
|
+
test('matches after newline', () => {
|
|
29
|
+
expect(extractBtwSuffix('fix the bug\nbtw')).toMatchInlineSnapshot(`
|
|
28
30
|
{
|
|
29
|
-
"
|
|
31
|
+
"forceBtw": true,
|
|
32
|
+
"prompt": "fix the bug",
|
|
30
33
|
}
|
|
31
34
|
`);
|
|
32
35
|
});
|
|
33
|
-
test('matches
|
|
34
|
-
expect(
|
|
36
|
+
test('matches with trailing dot', () => {
|
|
37
|
+
expect(extractBtwSuffix('fix the bug. btw.')).toMatchInlineSnapshot(`
|
|
35
38
|
{
|
|
36
|
-
"
|
|
39
|
+
"forceBtw": true,
|
|
40
|
+
"prompt": "fix the bug",
|
|
37
41
|
}
|
|
38
42
|
`);
|
|
39
43
|
});
|
|
40
|
-
test('
|
|
41
|
-
expect(
|
|
44
|
+
test('case insensitive', () => {
|
|
45
|
+
expect(extractBtwSuffix('done. BTW')).toMatchInlineSnapshot(`
|
|
42
46
|
{
|
|
43
|
-
"
|
|
47
|
+
"forceBtw": true,
|
|
48
|
+
"prompt": "done",
|
|
44
49
|
}
|
|
45
50
|
`);
|
|
46
51
|
});
|
|
47
|
-
test('
|
|
48
|
-
expect(
|
|
52
|
+
test('no space between punctuation and btw', () => {
|
|
53
|
+
expect(extractBtwSuffix('done.btw')).toMatchInlineSnapshot(`
|
|
49
54
|
{
|
|
50
|
-
"
|
|
55
|
+
"forceBtw": true,
|
|
56
|
+
"prompt": "done",
|
|
51
57
|
}
|
|
52
58
|
`);
|
|
53
59
|
});
|
|
54
|
-
test('does not match
|
|
55
|
-
expect(
|
|
60
|
+
test('does not match at start of message', () => {
|
|
61
|
+
expect(extractBtwSuffix('btw fix this')).toMatchInlineSnapshot(`
|
|
62
|
+
{
|
|
63
|
+
"forceBtw": false,
|
|
64
|
+
"prompt": "btw fix this",
|
|
65
|
+
}
|
|
66
|
+
`);
|
|
56
67
|
});
|
|
57
|
-
test('does not match mid-message', () => {
|
|
58
|
-
expect(
|
|
68
|
+
test('does not match mid-message without punctuation', () => {
|
|
69
|
+
expect(extractBtwSuffix('hello btw')).toMatchInlineSnapshot(`
|
|
70
|
+
{
|
|
71
|
+
"forceBtw": false,
|
|
72
|
+
"prompt": "hello btw",
|
|
73
|
+
}
|
|
74
|
+
`);
|
|
59
75
|
});
|
|
60
|
-
test('does not match empty
|
|
61
|
-
expect(
|
|
76
|
+
test('does not match empty content', () => {
|
|
77
|
+
expect(extractBtwSuffix('')).toMatchInlineSnapshot(`
|
|
78
|
+
{
|
|
79
|
+
"forceBtw": false,
|
|
80
|
+
"prompt": "",
|
|
81
|
+
}
|
|
82
|
+
`);
|
|
83
|
+
});
|
|
84
|
+
test('multiline message with btw at end', () => {
|
|
85
|
+
expect(extractBtwSuffix('first line\nsecond line. btw')).toMatchInlineSnapshot(`
|
|
86
|
+
{
|
|
87
|
+
"forceBtw": true,
|
|
88
|
+
"prompt": "first line
|
|
89
|
+
second line",
|
|
90
|
+
}
|
|
91
|
+
`);
|
|
62
92
|
});
|
|
63
93
|
});
|
package/dist/cli-runner.js
CHANGED
|
@@ -111,7 +111,11 @@ export async function sendDiscordMessageWithOptionalAttachment({ channelId, prom
|
|
|
111
111
|
const discordMaxLength = 2000;
|
|
112
112
|
if (prompt.length <= discordMaxLength) {
|
|
113
113
|
return (await rest.post(Routes.channelMessages(channelId), {
|
|
114
|
-
body: {
|
|
114
|
+
body: {
|
|
115
|
+
content: prompt,
|
|
116
|
+
embeds,
|
|
117
|
+
allowed_mentions: { parse: store.getState().allowedMentions },
|
|
118
|
+
},
|
|
115
119
|
}));
|
|
116
120
|
}
|
|
117
121
|
const preview = prompt.slice(0, 100).replace(/\n/g, ' ');
|
|
@@ -158,6 +162,7 @@ export async function sendDiscordMessageWithOptionalAttachment({ channelId, prom
|
|
|
158
162
|
content: summaryContent,
|
|
159
163
|
attachments: [{ id: 0, filename: 'prompt.md' }],
|
|
160
164
|
embeds,
|
|
165
|
+
allowed_mentions: { parse: store.getState().allowedMentions },
|
|
161
166
|
}));
|
|
162
167
|
const buffer = fs.readFileSync(tmpFile);
|
|
163
168
|
formData.append('files[0]', new Blob([buffer], { type: 'text/markdown' }), 'prompt.md');
|
|
@@ -509,6 +514,7 @@ export async function ensureCommandAvailable({ name, envPathKey, installUnix, in
|
|
|
509
514
|
}
|
|
510
515
|
// Run opencode upgrade in the background so the user always has the latest version.
|
|
511
516
|
// Spawn caffeinate on macOS to prevent system sleep while bot is running.
|
|
517
|
+
// Uses -s to also prevent sleep on lid close (AC power only, not battery).
|
|
512
518
|
// Uses -w to watch the parent PID so caffeinate self-terminates if kimaki
|
|
513
519
|
// exits for any reason (SIGTERM, crash, process.exit, supervisor stop).
|
|
514
520
|
export function startCaffeinate() {
|
|
@@ -516,7 +522,7 @@ export function startCaffeinate() {
|
|
|
516
522
|
return;
|
|
517
523
|
}
|
|
518
524
|
try {
|
|
519
|
-
const proc = spawn('caffeinate', ['-
|
|
525
|
+
const proc = spawn('caffeinate', ['-s', '-w', String(process.pid)], {
|
|
520
526
|
stdio: 'ignore',
|
|
521
527
|
detached: false,
|
|
522
528
|
});
|
|
@@ -938,16 +944,34 @@ export async function run({ restartOnboarding, addChannels, useWorktrees, enable
|
|
|
938
944
|
startCaffeinate();
|
|
939
945
|
const forceRestartOnboarding = Boolean(restartOnboarding);
|
|
940
946
|
const forceGateway = Boolean(gateway);
|
|
941
|
-
// Step 0: Ensure bun
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
947
|
+
// Step 0: Ensure opencode and bun are installed
|
|
948
|
+
await Promise.all([
|
|
949
|
+
ensureCommandAvailable({
|
|
950
|
+
name: 'opencode',
|
|
951
|
+
envPathKey: 'OPENCODE_PATH',
|
|
952
|
+
installUnix: 'curl -fsSL https://opencode.ai/install | bash',
|
|
953
|
+
installWindows: 'irm https://opencode.ai/install.ps1 | iex',
|
|
954
|
+
possiblePathsUnix: [
|
|
955
|
+
'~/.local/bin/opencode',
|
|
956
|
+
'~/.opencode/bin/opencode',
|
|
957
|
+
'/usr/local/bin/opencode',
|
|
958
|
+
'/opt/opencode/bin/opencode',
|
|
959
|
+
],
|
|
960
|
+
possiblePathsWindows: [
|
|
961
|
+
'~\\.local\\bin\\opencode.exe',
|
|
962
|
+
'~\\AppData\\Local\\opencode\\opencode.exe',
|
|
963
|
+
'~\\.opencode\\bin\\opencode.exe',
|
|
964
|
+
],
|
|
965
|
+
}),
|
|
966
|
+
ensureCommandAvailable({
|
|
967
|
+
name: 'bun',
|
|
968
|
+
envPathKey: 'BUN_PATH',
|
|
969
|
+
installUnix: 'curl -fsSL https://bun.sh/install | bash',
|
|
970
|
+
installWindows: 'irm bun.sh/install.ps1 | iex',
|
|
971
|
+
possiblePathsUnix: ['~/.bun/bin/bun', '/usr/local/bin/bun'],
|
|
972
|
+
possiblePathsWindows: ['~\\.bun\\bin\\bun.exe'],
|
|
973
|
+
}),
|
|
974
|
+
]);
|
|
951
975
|
void backgroundUpgradeKimaki();
|
|
952
976
|
// Start in-process Hrana server before database init. Required for the bot
|
|
953
977
|
// process because it serves as both the DB server and the single-instance
|
package/dist/cli.js
CHANGED
|
@@ -40,9 +40,14 @@ cli
|
|
|
40
40
|
.option('--no-critique', 'Disable automatic diff upload to critique.work in system prompts')
|
|
41
41
|
.option('--auto-restart', 'Automatically restart the bot on crash or OOM kill')
|
|
42
42
|
.option('--allow-all-users', 'Allow all Discord users to start sessions without needing Kimaki role or admin permissions (no-kimaki role still blocks)')
|
|
43
|
+
.option('--disable-sync', 'Disable background sync of external OpenCode sessions into Discord')
|
|
43
44
|
.option('--no-sentry', 'Disable Sentry error reporting')
|
|
44
45
|
.option('--gateway', 'Force gateway mode (use the gateway Kimaki bot instead of a self-hosted bot)')
|
|
45
46
|
.option('--gateway-callback-url <url>', 'After gateway OAuth install, redirect to this URL instead of the default success page (appends ?guild_id=<id>)')
|
|
47
|
+
.option('--allow-mention <type>', z
|
|
48
|
+
.array(z.enum(['users', 'roles', 'everyone']))
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('Which mention types the bot can trigger (users, roles, everyone). Repeatable. Default: users only.'))
|
|
46
51
|
.option('--enable-skill <name>', z
|
|
47
52
|
.array(z.string())
|
|
48
53
|
.optional()
|
|
@@ -136,8 +141,10 @@ cli
|
|
|
136
141
|
...(options.mentionMode && { defaultMentionMode: true }),
|
|
137
142
|
...(options.noCritique && { critiqueEnabled: false }),
|
|
138
143
|
...(options.allowAllUsers && { allowAllUsers: true }),
|
|
144
|
+
...(options.disableSync && { syncEnabled: false }),
|
|
139
145
|
...(enabledSkills.length > 0 && { enabledSkills }),
|
|
140
146
|
...(disabledSkills.length > 0 && { disabledSkills }),
|
|
147
|
+
...(options.allowMention && { allowedMentions: options.allowMention }),
|
|
141
148
|
});
|
|
142
149
|
if (enabledSkills.length > 0) {
|
|
143
150
|
cliLogger.log(`Skill whitelist enabled: only [${enabledSkills.join(', ')}] will be injected`);
|
|
@@ -157,6 +164,9 @@ cli
|
|
|
157
164
|
if (options.noCritique) {
|
|
158
165
|
cliLogger.log('Critique disabled: diffs will not be auto-uploaded to critique.work');
|
|
159
166
|
}
|
|
167
|
+
if (options.disableSync) {
|
|
168
|
+
cliLogger.log('Background sync disabled: external OpenCode sessions will not appear in Discord');
|
|
169
|
+
}
|
|
160
170
|
if (options.noSentry) {
|
|
161
171
|
process.env.KIMAKI_SENTRY_DISABLED = '1';
|
|
162
172
|
cliLogger.log('Sentry error reporting disabled (--no-sentry)');
|
package/dist/commands/abort.js
CHANGED
package/dist/commands/agent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// /agent command - Set the preferred agent for this channel or session.
|
|
2
2
|
// Also provides quick agent commands like /plan-agent, /build-agent that switch instantly.
|
|
3
3
|
// When a prompt is provided to a quick agent command (e.g. /plan-agent "fix the bug"),
|
|
4
|
-
// the prompt is sent
|
|
4
|
+
// the prompt is sent with that agent and the session keeps that agent afterwards.
|
|
5
5
|
import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, ThreadAutoArchiveDuration, MessageFlags, } from 'discord.js';
|
|
6
6
|
import crypto from 'node:crypto';
|
|
7
7
|
import { setChannelAgent, setSessionAgent, clearSessionModel, getThreadSession, getSessionAgent, getChannelAgent, } from '../database.js';
|
|
@@ -162,7 +162,7 @@ export async function setAgentForContext({ context, agentName, }) {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
export async function handleAgentCommand({ interaction, appId, }) {
|
|
165
|
-
await interaction.deferReply(
|
|
165
|
+
await interaction.deferReply();
|
|
166
166
|
const context = await resolveAgentCommandContext({ interaction, appId });
|
|
167
167
|
if (!context) {
|
|
168
168
|
return;
|
|
@@ -290,13 +290,12 @@ export async function handleAgentSelectMenu(interaction) {
|
|
|
290
290
|
export async function handleQuickAgentCommand({ command, appId, }) {
|
|
291
291
|
const fallbackAgentName = command.commandName.replace(/-agent$/, '');
|
|
292
292
|
const prompt = command.options.getString('prompt') || undefined;
|
|
293
|
-
//
|
|
294
|
-
// without changing the persistent agent preference.
|
|
293
|
+
// Prompt mode: send the prompt with this agent immediately.
|
|
295
294
|
if (prompt) {
|
|
296
295
|
return handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, prompt });
|
|
297
296
|
}
|
|
298
297
|
// No prompt: switch the persistent agent preference (original behavior).
|
|
299
|
-
await command.deferReply(
|
|
298
|
+
await command.deferReply();
|
|
300
299
|
const context = await resolveAgentCommandContext({
|
|
301
300
|
interaction: command,
|
|
302
301
|
appId,
|
|
@@ -363,10 +362,10 @@ export async function handleQuickAgentCommand({ command, appId, }) {
|
|
|
363
362
|
}
|
|
364
363
|
}
|
|
365
364
|
/**
|
|
366
|
-
* Handle
|
|
367
|
-
* In a thread: enqueue the prompt
|
|
368
|
-
* In a channel: create a new thread
|
|
369
|
-
*
|
|
365
|
+
* Handle prompt mode: send a prompt with the requested agent.
|
|
366
|
+
* In a thread: enqueue the prompt on the existing session and switch that session.
|
|
367
|
+
* In a channel: create a new thread whose session starts with the requested agent.
|
|
368
|
+
* Channel-level preferences are not changed.
|
|
370
369
|
*/
|
|
371
370
|
async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, prompt, }) {
|
|
372
371
|
const channel = command.channel;
|
|
@@ -386,7 +385,7 @@ async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, p
|
|
|
386
385
|
].includes(channel.type);
|
|
387
386
|
const displayText = `${prompt.slice(0, 1000)}${prompt.length > 1000 ? '...' : ''}`;
|
|
388
387
|
if (isThread) {
|
|
389
|
-
// In a thread: enqueue the prompt
|
|
388
|
+
// In a thread: enqueue the prompt and switch the existing session to this agent.
|
|
390
389
|
const thread = channel;
|
|
391
390
|
const resolved = await resolveWorkingDirectory({ channel: thread });
|
|
392
391
|
if (!resolved) {
|
|
@@ -419,9 +418,8 @@ async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, p
|
|
|
419
418
|
});
|
|
420
419
|
}
|
|
421
420
|
else if (channel.type === ChannelType.GuildText) {
|
|
422
|
-
// In a channel: create a new thread and enqueue with the agent
|
|
423
|
-
const
|
|
424
|
-
const metadata = await getKimakiMetadata(textChannel);
|
|
421
|
+
// In a channel: create a new thread and enqueue with the requested agent.
|
|
422
|
+
const metadata = await getKimakiMetadata(channel);
|
|
425
423
|
const projectDirectory = metadata.projectDirectory;
|
|
426
424
|
if (!projectDirectory) {
|
|
427
425
|
await command.reply({
|
|
@@ -431,14 +429,14 @@ async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, p
|
|
|
431
429
|
return;
|
|
432
430
|
}
|
|
433
431
|
await command.deferReply();
|
|
434
|
-
const starterMessage = await
|
|
432
|
+
const starterMessage = await channel.send({
|
|
435
433
|
content: `» **${command.user.displayName}** (${resolvedAgentName}): ${displayText}`,
|
|
436
434
|
flags: SILENT_MESSAGE_FLAGS,
|
|
437
435
|
});
|
|
438
436
|
const thread = await starterMessage.startThread({
|
|
439
437
|
name: prompt.slice(0, 80),
|
|
440
438
|
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
441
|
-
reason:
|
|
439
|
+
reason: `${resolvedAgentName} agent prompt`,
|
|
442
440
|
});
|
|
443
441
|
await thread.members.add(command.user.id);
|
|
444
442
|
await command.editReply(`Sent with **${resolvedAgentName}** agent in ${thread.toString()}`);
|
|
@@ -447,7 +445,7 @@ async function handleQuickAgentWithPrompt({ command, appId, fallbackAgentName, p
|
|
|
447
445
|
thread,
|
|
448
446
|
projectDirectory,
|
|
449
447
|
sdkDirectory: projectDirectory,
|
|
450
|
-
channelId:
|
|
448
|
+
channelId: channel.id,
|
|
451
449
|
appId,
|
|
452
450
|
});
|
|
453
451
|
await runtime.enqueueIncoming({
|
|
@@ -38,6 +38,5 @@ export async function handleToggleMentionModeCommand({ command, }) {
|
|
|
38
38
|
content: nextEnabled
|
|
39
39
|
? `Mention mode **enabled** for this channel.\nThe bot will only start new sessions when @mentioned.\nMessages in existing threads are not affected.`
|
|
40
40
|
: `Mention mode **disabled** for this channel.\nThe bot will respond to all messages in **#${channel.name}**.`,
|
|
41
|
-
flags: MessageFlags.Ephemeral,
|
|
42
41
|
});
|
|
43
42
|
}
|
|
@@ -40,7 +40,7 @@ function formatSourceLabel(info) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
export async function handleModelVariantCommand({ interaction, appId, }) {
|
|
43
|
-
await interaction.deferReply(
|
|
43
|
+
await interaction.deferReply();
|
|
44
44
|
const channel = interaction.channel;
|
|
45
45
|
if (!channel) {
|
|
46
46
|
await interaction.editReply({
|
|
@@ -292,7 +292,7 @@ export async function handleVariantScopeSelectMenu(interaction) {
|
|
|
292
292
|
async function applyVariant({ interaction, context, variant, scope, contextHash, }) {
|
|
293
293
|
const modelId = context.modelId;
|
|
294
294
|
const variantSuffix = variant ? ` (${variant})` : '';
|
|
295
|
-
const agentTip = '\n_Tip: create [agent .md files](https://
|
|
295
|
+
const agentTip = '\n_Tip: create [agent .md files](https://kimaki.dev/docs/model-switching) in .opencode/agent/ for one-command model switching_';
|
|
296
296
|
try {
|
|
297
297
|
if (scope === 'session') {
|
|
298
298
|
if (!context.sessionId) {
|