kimaki 0.4.86 → 0.4.87
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 +3 -2
- package/dist/commands/btw.js +111 -0
- package/dist/discord-command-registration.js +53 -41
- package/dist/interaction-handler.js +4 -15
- package/dist/markdown.test.js +32 -0
- package/dist/queue-advanced-footer.e2e.test.js +40 -3
- package/dist/queue-advanced-model-switch.e2e.test.js +6 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +1 -0
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +8 -2
- package/dist/runtime-lifecycle.e2e.test.js +4 -1
- package/dist/thread-message-queue.e2e.test.js +2 -5
- package/dist/voice-message.e2e.test.js +6 -1
- package/package.json +4 -4
- package/skills/critique/SKILL.md +3 -37
- package/skills/gitchamber/SKILL.md +93 -0
- package/skills/goke/SKILL.md +3 -1
- package/src/agent-model.e2e.test.ts +3 -2
- package/src/commands/btw.ts +158 -0
- package/src/discord-command-registration.ts +64 -49
- package/src/interaction-handler.ts +8 -15
- package/src/markdown.test.ts +32 -0
- package/src/queue-advanced-footer.e2e.test.ts +40 -3
- package/src/queue-advanced-model-switch.e2e.test.ts +6 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +1 -0
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +8 -2
- package/src/runtime-lifecycle.e2e.test.ts +4 -1
- package/src/thread-message-queue.e2e.test.ts +2 -5
- package/src/voice-message.e2e.test.ts +6 -1
|
@@ -302,7 +302,8 @@ describe('agent model resolution', () => {
|
|
|
302
302
|
Reply with exactly: agent-model-check
|
|
303
303
|
--- from: assistant (TestBot)
|
|
304
304
|
⬥ ok
|
|
305
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
305
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
306
|
+
⬥ ok"
|
|
306
307
|
`);
|
|
307
308
|
expect(footerMessage).toBeDefined();
|
|
308
309
|
if (!footerMessage) {
|
|
@@ -345,7 +346,7 @@ describe('agent model resolution', () => {
|
|
|
345
346
|
Reply with exactly: system-context-check
|
|
346
347
|
--- from: assistant (TestBot)
|
|
347
348
|
⬥ system-context-ok
|
|
348
|
-
*project ⋅
|
|
349
|
+
*project ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
349
350
|
`);
|
|
350
351
|
}, 15_000);
|
|
351
352
|
test('new thread uses channel model when channel model preference is set', async () => {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// /btw command - Fork the current session with full context and send a new prompt.
|
|
2
|
+
// Unlike /fork, this does not replay past messages in Discord. It just creates
|
|
3
|
+
// a new thread, forks the entire session (no messageID), and immediately
|
|
4
|
+
// dispatches the user's prompt so the forked session starts working right away.
|
|
5
|
+
import { ChannelType, ThreadAutoArchiveDuration, MessageFlags, } from 'discord.js';
|
|
6
|
+
import { getThreadSession, setThreadSession } from '../database.js';
|
|
7
|
+
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
8
|
+
import { resolveWorkingDirectory, resolveTextChannel, sendThreadMessage, } from '../discord-utils.js';
|
|
9
|
+
import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js';
|
|
10
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
11
|
+
const logger = createLogger(LogPrefix.FORK);
|
|
12
|
+
export async function handleBtwCommand({ command, appId, }) {
|
|
13
|
+
const channel = command.channel;
|
|
14
|
+
if (!channel) {
|
|
15
|
+
await command.reply({
|
|
16
|
+
content: 'This command can only be used in a channel',
|
|
17
|
+
flags: MessageFlags.Ephemeral,
|
|
18
|
+
});
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const isThread = [
|
|
22
|
+
ChannelType.PublicThread,
|
|
23
|
+
ChannelType.PrivateThread,
|
|
24
|
+
ChannelType.AnnouncementThread,
|
|
25
|
+
].includes(channel.type);
|
|
26
|
+
if (!isThread) {
|
|
27
|
+
await command.reply({
|
|
28
|
+
content: 'This command can only be used in a thread with an active session',
|
|
29
|
+
flags: MessageFlags.Ephemeral,
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const prompt = command.options.getString('prompt', true);
|
|
34
|
+
const resolved = await resolveWorkingDirectory({
|
|
35
|
+
channel: channel,
|
|
36
|
+
});
|
|
37
|
+
if (!resolved) {
|
|
38
|
+
await command.reply({
|
|
39
|
+
content: 'Could not determine project directory for this channel',
|
|
40
|
+
flags: MessageFlags.Ephemeral,
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const { projectDirectory } = resolved;
|
|
45
|
+
const sessionId = await getThreadSession(channel.id);
|
|
46
|
+
if (!sessionId) {
|
|
47
|
+
await command.reply({
|
|
48
|
+
content: 'No active session in this thread',
|
|
49
|
+
flags: MessageFlags.Ephemeral,
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await command.deferReply({ flags: MessageFlags.Ephemeral });
|
|
54
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
55
|
+
if (getClient instanceof Error) {
|
|
56
|
+
await command.editReply({
|
|
57
|
+
content: `Failed to fork session: ${getClient.message}`,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
// Fork the entire session (no messageID = fork at the latest point)
|
|
63
|
+
const forkResponse = await getClient().session.fork({
|
|
64
|
+
sessionID: sessionId,
|
|
65
|
+
});
|
|
66
|
+
if (!forkResponse.data) {
|
|
67
|
+
await command.editReply('Failed to fork session');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const forkedSession = forkResponse.data;
|
|
71
|
+
const textChannel = await resolveTextChannel(channel);
|
|
72
|
+
if (!textChannel) {
|
|
73
|
+
await command.editReply('Could not resolve parent text channel');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const threadName = `btw: ${prompt}`.slice(0, 100);
|
|
77
|
+
const thread = await textChannel.threads.create({
|
|
78
|
+
name: threadName,
|
|
79
|
+
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
80
|
+
reason: `btw fork from session ${sessionId}`,
|
|
81
|
+
});
|
|
82
|
+
// Claim the forked session immediately so external polling does not race
|
|
83
|
+
await setThreadSession(thread.id, forkedSession.id);
|
|
84
|
+
await thread.members.add(command.user.id);
|
|
85
|
+
logger.log(`Created btw fork session ${forkedSession.id} in thread ${thread.id} from ${sessionId}`);
|
|
86
|
+
// Short status message with prompt instead of replaying past messages
|
|
87
|
+
const sourceThreadLink = `<#${channel.id}>`;
|
|
88
|
+
await sendThreadMessage(thread, `Reusing context from ${sourceThreadLink} to answer prompt...\n${prompt}`);
|
|
89
|
+
// Create runtime and dispatch the prompt immediately
|
|
90
|
+
const runtime = getOrCreateRuntime({
|
|
91
|
+
threadId: thread.id,
|
|
92
|
+
thread,
|
|
93
|
+
projectDirectory,
|
|
94
|
+
sdkDirectory: projectDirectory,
|
|
95
|
+
channelId: textChannel.id,
|
|
96
|
+
appId,
|
|
97
|
+
});
|
|
98
|
+
await runtime.enqueueIncoming({
|
|
99
|
+
prompt,
|
|
100
|
+
userId: command.user.id,
|
|
101
|
+
username: command.user.displayName,
|
|
102
|
+
appId,
|
|
103
|
+
mode: 'opencode',
|
|
104
|
+
});
|
|
105
|
+
await command.editReply(`Session forked! Continue in ${thread.toString()}`);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
logger.error('Error in /btw:', error);
|
|
109
|
+
await command.editReply(`Failed to fork session: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -161,11 +161,6 @@ export async function registerCommands({ token, appId, guildIds, userCommands =
|
|
|
161
161
|
})
|
|
162
162
|
.setDMPermission(false)
|
|
163
163
|
.toJSON(),
|
|
164
|
-
new SlashCommandBuilder()
|
|
165
|
-
.setName('toggle-mention-mode')
|
|
166
|
-
.setDescription(truncateCommandDescription('Toggle mention-only mode (bot only responds when @mentioned)'))
|
|
167
|
-
.setDMPermission(false)
|
|
168
|
-
.toJSON(),
|
|
169
164
|
new SlashCommandBuilder()
|
|
170
165
|
.setName('add-project')
|
|
171
166
|
.setDescription(truncateCommandDescription('Create Discord channels for a project. Use `npx kimaki project add` for unlisted projects'))
|
|
@@ -214,11 +209,6 @@ export async function registerCommands({ token, appId, guildIds, userCommands =
|
|
|
214
209
|
.setDescription(truncateCommandDescription('Compact the session context by summarizing conversation history'))
|
|
215
210
|
.setDMPermission(false)
|
|
216
211
|
.toJSON(),
|
|
217
|
-
new SlashCommandBuilder()
|
|
218
|
-
.setName('stop')
|
|
219
|
-
.setDescription(truncateCommandDescription('Abort the current OpenCode request in this thread'))
|
|
220
|
-
.setDMPermission(false)
|
|
221
|
-
.toJSON(),
|
|
222
212
|
new SlashCommandBuilder()
|
|
223
213
|
.setName('share')
|
|
224
214
|
.setDescription(truncateCommandDescription('Share the current session as a public URL'))
|
|
@@ -234,6 +224,18 @@ export async function registerCommands({ token, appId, guildIds, userCommands =
|
|
|
234
224
|
.setDescription(truncateCommandDescription('Fork the session from a past user message'))
|
|
235
225
|
.setDMPermission(false)
|
|
236
226
|
.toJSON(),
|
|
227
|
+
new SlashCommandBuilder()
|
|
228
|
+
.setName('btw')
|
|
229
|
+
.setDescription(truncateCommandDescription('Ask something without polluting or blocking the current session'))
|
|
230
|
+
.addStringOption((option) => {
|
|
231
|
+
option
|
|
232
|
+
.setName('prompt')
|
|
233
|
+
.setDescription(truncateCommandDescription('The message to send in the forked session'))
|
|
234
|
+
.setRequired(true);
|
|
235
|
+
return option;
|
|
236
|
+
})
|
|
237
|
+
.setDMPermission(false)
|
|
238
|
+
.toJSON(),
|
|
237
239
|
new SlashCommandBuilder()
|
|
238
240
|
.setName('model')
|
|
239
241
|
.setDescription(truncateCommandDescription('Set the preferred model for this channel or session'))
|
|
@@ -338,11 +340,6 @@ export async function registerCommands({ token, appId, guildIds, userCommands =
|
|
|
338
340
|
.setDescription(truncateCommandDescription('Show current session ID and opencode attach command for this thread'))
|
|
339
341
|
.setDMPermission(false)
|
|
340
342
|
.toJSON(),
|
|
341
|
-
new SlashCommandBuilder()
|
|
342
|
-
.setName('memory-snapshot')
|
|
343
|
-
.setDescription(truncateCommandDescription('Write a V8 heap snapshot to disk for memory debugging'))
|
|
344
|
-
.setDMPermission(false)
|
|
345
|
-
.toJSON(),
|
|
346
343
|
new SlashCommandBuilder()
|
|
347
344
|
.setName('upgrade-and-restart')
|
|
348
345
|
.setDescription(truncateCommandDescription('Upgrade kimaki to the latest version and restart the bot'))
|
|
@@ -369,10 +366,43 @@ export async function registerCommands({ token, appId, guildIds, userCommands =
|
|
|
369
366
|
.setDMPermission(false)
|
|
370
367
|
.toJSON(),
|
|
371
368
|
];
|
|
372
|
-
//
|
|
369
|
+
// Dynamic commands are registered in priority order: agents → user commands → skills → MCP prompts.
|
|
370
|
+
// This ordering matters because we slice to MAX_DISCORD_COMMANDS (100) at the end,
|
|
371
|
+
// so lower-priority dynamic commands get trimmed first if the total exceeds the limit.
|
|
372
|
+
// 1. Agent-specific quick commands like /plan-agent, /build-agent
|
|
373
|
+
// Filter to primary/all mode agents (same as /agent command shows), excluding hidden agents
|
|
374
|
+
const primaryAgents = agents.filter((a) => (a.mode === 'primary' || a.mode === 'all') && !a.hidden);
|
|
375
|
+
for (const agent of primaryAgents) {
|
|
376
|
+
const sanitizedName = sanitizeAgentName(agent.name);
|
|
377
|
+
// Skip if sanitized name is empty or would create invalid command name
|
|
378
|
+
// Discord command names must start with a lowercase letter or number
|
|
379
|
+
if (!sanitizedName || !/^[a-z0-9]/.test(sanitizedName)) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
// Truncate base name before appending suffix so the -agent suffix is never
|
|
383
|
+
// lost to Discord's 32-char command name limit.
|
|
384
|
+
const agentSuffix = '-agent';
|
|
385
|
+
const agentBaseName = sanitizedName.slice(0, 32 - agentSuffix.length);
|
|
386
|
+
const commandName = `${agentBaseName}${agentSuffix}`;
|
|
387
|
+
const description = buildQuickAgentCommandDescription({
|
|
388
|
+
agentName: agent.name,
|
|
389
|
+
description: agent.description,
|
|
390
|
+
});
|
|
391
|
+
commands.push(new SlashCommandBuilder()
|
|
392
|
+
.setName(commandName)
|
|
393
|
+
.setDescription(truncateCommandDescription(description))
|
|
394
|
+
.setDMPermission(false)
|
|
395
|
+
.toJSON());
|
|
396
|
+
}
|
|
397
|
+
// 2. User-defined commands, skills, and MCP prompts (ordered by priority)
|
|
373
398
|
// Also populate registeredUserCommands in the store for /queue-command autocomplete
|
|
374
399
|
const newRegisteredCommands = [];
|
|
375
|
-
|
|
400
|
+
// Sort: regular commands first, then skills, then MCP prompts
|
|
401
|
+
const sourceOrder = { config: 0, skill: 1, mcp: 2 };
|
|
402
|
+
const sortedUserCommands = [...userCommands].sort((a, b) => {
|
|
403
|
+
return (sourceOrder[a.source || ''] ?? 0) - (sourceOrder[b.source || ''] ?? 0);
|
|
404
|
+
});
|
|
405
|
+
for (const cmd of sortedUserCommands) {
|
|
376
406
|
if (SKIP_USER_COMMANDS.includes(cmd.name)) {
|
|
377
407
|
continue;
|
|
378
408
|
}
|
|
@@ -415,30 +445,12 @@ export async function registerCommands({ token, appId, guildIds, userCommands =
|
|
|
415
445
|
.toJSON());
|
|
416
446
|
}
|
|
417
447
|
store.setState({ registeredUserCommands: newRegisteredCommands });
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
// Discord command names must start with a lowercase letter or number
|
|
425
|
-
if (!sanitizedName || !/^[a-z0-9]/.test(sanitizedName)) {
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
// Truncate base name before appending suffix so the -agent suffix is never
|
|
429
|
-
// lost to Discord's 32-char command name limit.
|
|
430
|
-
const agentSuffix = '-agent';
|
|
431
|
-
const agentBaseName = sanitizedName.slice(0, 32 - agentSuffix.length);
|
|
432
|
-
const commandName = `${agentBaseName}${agentSuffix}`;
|
|
433
|
-
const description = buildQuickAgentCommandDescription({
|
|
434
|
-
agentName: agent.name,
|
|
435
|
-
description: agent.description,
|
|
436
|
-
});
|
|
437
|
-
commands.push(new SlashCommandBuilder()
|
|
438
|
-
.setName(commandName)
|
|
439
|
-
.setDescription(truncateCommandDescription(description))
|
|
440
|
-
.setDMPermission(false)
|
|
441
|
-
.toJSON());
|
|
448
|
+
// Discord allows max 100 guild commands. Slice to stay within the limit,
|
|
449
|
+
// trimming lowest-priority dynamic commands (MCP prompts, then skills) first.
|
|
450
|
+
const MAX_DISCORD_COMMANDS = 100;
|
|
451
|
+
if (commands.length > MAX_DISCORD_COMMANDS) {
|
|
452
|
+
cliLogger.warn(`COMMANDS: ${commands.length} commands exceed Discord limit of ${MAX_DISCORD_COMMANDS}, truncating to ${MAX_DISCORD_COMMANDS}`);
|
|
453
|
+
commands.length = MAX_DISCORD_COMMANDS;
|
|
442
454
|
}
|
|
443
455
|
const rest = createDiscordRest(token);
|
|
444
456
|
const uniqueGuildIds = Array.from(new Set(guildIds.filter((guildId) => guildId)));
|
|
@@ -8,7 +8,6 @@ import { handleMergeWorktreeCommand, handleMergeWorktreeAutocomplete, } from './
|
|
|
8
8
|
import { handleToggleWorktreesCommand } from './commands/worktree-settings.js';
|
|
9
9
|
import { handleWorktreesCommand } from './commands/worktrees.js';
|
|
10
10
|
import { handleTasksCommand } from './commands/tasks.js';
|
|
11
|
-
import { handleToggleMentionModeCommand } from './commands/mention-mode.js';
|
|
12
11
|
import { handleResumeCommand, handleResumeAutocomplete, } from './commands/resume.js';
|
|
13
12
|
import { handleAddProjectCommand, handleAddProjectAutocomplete, } from './commands/add-project.js';
|
|
14
13
|
import { handleRemoveProjectCommand, handleRemoveProjectAutocomplete, } from './commands/remove-project.js';
|
|
@@ -19,6 +18,7 @@ import { handleCompactCommand } from './commands/compact.js';
|
|
|
19
18
|
import { handleShareCommand } from './commands/share.js';
|
|
20
19
|
import { handleDiffCommand } from './commands/diff.js';
|
|
21
20
|
import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js';
|
|
21
|
+
import { handleBtwCommand } from './commands/btw.js';
|
|
22
22
|
import { handleModelCommand, handleProviderSelectMenu, handleModelSelectMenu, handleModelScopeSelectMenu, } from './commands/model.js';
|
|
23
23
|
import { handleUnsetModelCommand } from './commands/unset-model.js';
|
|
24
24
|
import { handleLoginCommand, handleLoginSelect, handleLoginTextButton, handleLoginTextModalSubmit, handleLoginApiKeyButton, handleOAuthCodeButton, handleOAuthCodeModalSubmit, handleApiKeyModalSubmit, } from './commands/login.js';
|
|
@@ -36,7 +36,6 @@ import { handleRestartOpencodeServerCommand } from './commands/restart-opencode-
|
|
|
36
36
|
import { handleRunCommand } from './commands/run-command.js';
|
|
37
37
|
import { handleContextUsageCommand } from './commands/context-usage.js';
|
|
38
38
|
import { handleSessionIdCommand } from './commands/session-id.js';
|
|
39
|
-
import { handleMemorySnapshotCommand } from './commands/memory-snapshot.js';
|
|
40
39
|
import { handleUpgradeAndRestartCommand } from './commands/upgrade.js';
|
|
41
40
|
import { handleMcpCommand, handleMcpSelectMenu } from './commands/mcp.js';
|
|
42
41
|
import { handleScreenshareCommand, handleScreenshareStopCommand, } from './commands/screenshare.js';
|
|
@@ -120,12 +119,6 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
120
119
|
appId,
|
|
121
120
|
});
|
|
122
121
|
return;
|
|
123
|
-
case 'toggle-mention-mode':
|
|
124
|
-
await handleToggleMentionModeCommand({
|
|
125
|
-
command: interaction,
|
|
126
|
-
appId,
|
|
127
|
-
});
|
|
128
|
-
return;
|
|
129
122
|
case 'resume':
|
|
130
123
|
await handleResumeCommand({ command: interaction, appId });
|
|
131
124
|
return;
|
|
@@ -142,7 +135,6 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
142
135
|
});
|
|
143
136
|
return;
|
|
144
137
|
case 'abort':
|
|
145
|
-
case 'stop':
|
|
146
138
|
await handleAbortCommand({ command: interaction, appId });
|
|
147
139
|
return;
|
|
148
140
|
case 'compact':
|
|
@@ -157,6 +149,9 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
157
149
|
case 'fork':
|
|
158
150
|
await handleForkCommand(interaction);
|
|
159
151
|
return;
|
|
152
|
+
case 'btw':
|
|
153
|
+
await handleBtwCommand({ command: interaction, appId });
|
|
154
|
+
return;
|
|
160
155
|
case 'model':
|
|
161
156
|
await handleModelCommand({ interaction, appId });
|
|
162
157
|
return;
|
|
@@ -205,12 +200,6 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
205
200
|
case 'session-id':
|
|
206
201
|
await handleSessionIdCommand({ command: interaction, appId });
|
|
207
202
|
return;
|
|
208
|
-
case 'memory-snapshot':
|
|
209
|
-
await handleMemorySnapshotCommand({
|
|
210
|
-
command: interaction,
|
|
211
|
-
appId,
|
|
212
|
-
});
|
|
213
|
-
return;
|
|
214
203
|
case 'upgrade-and-restart':
|
|
215
204
|
await handleUpgradeAndRestartCommand({
|
|
216
205
|
command: interaction,
|
package/dist/markdown.test.js
CHANGED
|
@@ -184,6 +184,22 @@ test('generate markdown with system info', async () => {
|
|
|
184
184
|
|
|
185
185
|
|
|
186
186
|
*Completed in Xs*
|
|
187
|
+
|
|
188
|
+
### 🤖 Assistant (deterministic-v2)
|
|
189
|
+
|
|
190
|
+
**Started using deterministic-provider/deterministic-v2**
|
|
191
|
+
|
|
192
|
+
Hello! This is a deterministic markdown test response.
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
*Completed in Xs*
|
|
196
|
+
|
|
197
|
+
### 🤖 Assistant (deterministic-v2)
|
|
198
|
+
|
|
199
|
+
**Started using deterministic-provider/deterministic-v2**
|
|
200
|
+
|
|
201
|
+
Hello! This is a deterministic markdown test response.
|
|
202
|
+
|
|
187
203
|
"
|
|
188
204
|
`);
|
|
189
205
|
});
|
|
@@ -219,6 +235,22 @@ test('generate markdown without system info', async () => {
|
|
|
219
235
|
|
|
220
236
|
|
|
221
237
|
*Completed in Xs*
|
|
238
|
+
|
|
239
|
+
### 🤖 Assistant (deterministic-v2)
|
|
240
|
+
|
|
241
|
+
**Started using deterministic-provider/deterministic-v2**
|
|
242
|
+
|
|
243
|
+
Hello! This is a deterministic markdown test response.
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
*Completed in Xs*
|
|
247
|
+
|
|
248
|
+
### 🤖 Assistant (deterministic-v2)
|
|
249
|
+
|
|
250
|
+
**Started using deterministic-provider/deterministic-v2**
|
|
251
|
+
|
|
252
|
+
Hello! This is a deterministic markdown test response.
|
|
253
|
+
|
|
222
254
|
"
|
|
223
255
|
`);
|
|
224
256
|
});
|
|
@@ -95,6 +95,7 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
95
95
|
Reply with exactly: footer-multi-second
|
|
96
96
|
--- from: assistant (TestBot)
|
|
97
97
|
⬥ ok
|
|
98
|
+
⬥ ok
|
|
98
99
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
99
100
|
`);
|
|
100
101
|
if (footerCount >= 2) {
|
|
@@ -190,11 +191,14 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
190
191
|
--- from: user (queue-advanced-tester)
|
|
191
192
|
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
192
193
|
--- from: assistant (TestBot)
|
|
194
|
+
⬥ ok
|
|
193
195
|
⬥ starting sleep 100
|
|
194
196
|
--- from: user (queue-advanced-tester)
|
|
195
197
|
Reply with exactly: interrupt-footer-followup
|
|
196
198
|
--- from: assistant (TestBot)
|
|
197
199
|
⬥ ok
|
|
200
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
201
|
+
⬥ ok
|
|
198
202
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
199
203
|
`);
|
|
200
204
|
expect(followupUserIdx).toBeGreaterThanOrEqual(0);
|
|
@@ -267,15 +271,19 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
267
271
|
--- from: assistant (TestBot)
|
|
268
272
|
⬥ ok
|
|
269
273
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
274
|
+
⬥ ok
|
|
270
275
|
--- from: user (queue-advanced-tester)
|
|
271
276
|
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
272
277
|
--- from: assistant (TestBot)
|
|
278
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
279
|
+
⬥ ok
|
|
273
280
|
⬥ starting sleep 100
|
|
274
281
|
--- from: user (queue-advanced-tester)
|
|
275
282
|
Reply with exactly: plugin-timeout-after
|
|
276
283
|
--- from: assistant (TestBot)
|
|
277
284
|
⬥ ok
|
|
278
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
285
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
286
|
+
⬥ ok"
|
|
279
287
|
`);
|
|
280
288
|
expect(afterIndex).toBeGreaterThanOrEqual(0);
|
|
281
289
|
const okReplyIndex = messagesWithFooter.findIndex((message, index) => {
|
|
@@ -355,8 +363,10 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
355
363
|
TOOL_CALL_FOOTER_MARKER
|
|
356
364
|
--- from: assistant (TestBot)
|
|
357
365
|
⬥ running tool
|
|
366
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
358
367
|
⬥ ok
|
|
359
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
368
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
369
|
+
⬥ ok"
|
|
360
370
|
`);
|
|
361
371
|
// Only ONE footer at the end — the tool-call step's footer is NOT
|
|
362
372
|
// emitted mid-turn. The final text follow-up gets the footer.
|
|
@@ -406,6 +416,19 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
406
416
|
MULTI_TOOL_FOOTER_MARKER
|
|
407
417
|
--- from: assistant (TestBot)
|
|
408
418
|
⬥ investigating the issue
|
|
419
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
420
|
+
⬥ all done, fixed 3 files
|
|
421
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
422
|
+
⬥ all done, fixed 3 files
|
|
423
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
424
|
+
⬥ all done, fixed 3 files
|
|
425
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
426
|
+
⬥ all done, fixed 3 files
|
|
427
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
428
|
+
⬥ all done, fixed 3 files
|
|
429
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
430
|
+
⬥ all done, fixed 3 files
|
|
431
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
409
432
|
⬥ all done, fixed 3 files
|
|
410
433
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
411
434
|
`);
|
|
@@ -459,10 +482,24 @@ e2eTest('queue advanced: footer emission', () => {
|
|
|
459
482
|
MULTI_STEP_CHAIN_MARKER
|
|
460
483
|
--- from: assistant (TestBot)
|
|
461
484
|
⬥ chain step 1: reading config
|
|
485
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
462
486
|
⬥ chain step 2: analyzing results
|
|
487
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
463
488
|
⬥ chain step 3: applying fix
|
|
489
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
464
490
|
⬥ chain complete: all 3 steps done
|
|
465
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
491
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
492
|
+
⬥ chain complete: all 3 steps done
|
|
493
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
494
|
+
⬥ chain complete: all 3 steps done
|
|
495
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
496
|
+
⬥ chain complete: all 3 steps done
|
|
497
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
498
|
+
⬥ chain complete: all 3 steps done
|
|
499
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
500
|
+
⬥ chain complete: all 3 steps done
|
|
501
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
502
|
+
⬥ chain complete: all 3 steps done"
|
|
466
503
|
`);
|
|
467
504
|
// The critical assertion: only 1 footer at the very end.
|
|
468
505
|
// With the naive "allow tool-calls as natural completion" fix,
|
|
@@ -252,14 +252,20 @@ describe('queue advanced: /model with interrupt recovery', () => {
|
|
|
252
252
|
--- from: assistant (TestBot)
|
|
253
253
|
⬥ ok
|
|
254
254
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
255
|
+
⬥ ok
|
|
255
256
|
Model set for this session:
|
|
256
257
|
**Deterministic Provider** / **deterministic-v3**
|
|
257
258
|
\`deterministic-provider/deterministic-v3\`
|
|
258
259
|
_Restarting current request with new model..._
|
|
259
260
|
_Tip: create [agent .md files](https://github.com/remorses/kimaki/blob/main/docs/model-switching.md) in .opencode/agent/ for one-command model switching_
|
|
261
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
262
|
+
⬥ ok
|
|
263
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
264
|
+
⬥ ok
|
|
260
265
|
--- from: user (queue-model-switch-tester)
|
|
261
266
|
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
262
267
|
--- from: assistant (TestBot)
|
|
268
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
263
269
|
⬥ ok
|
|
264
270
|
⬥ starting sleep 100
|
|
265
271
|
--- from: user (queue-model-switch-tester)
|
|
@@ -171,6 +171,7 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
171
171
|
--- from: user (queue-permission-tester)
|
|
172
172
|
Reply with exactly: post-permission-user-message
|
|
173
173
|
--- from: assistant (TestBot)
|
|
174
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
174
175
|
⬥ ok
|
|
175
176
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
176
177
|
`);
|
|
@@ -87,16 +87,22 @@ e2eTest('queue advanced: typing interrupt', () => {
|
|
|
87
87
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
88
88
|
--- from: user (queue-advanced-tester)
|
|
89
89
|
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
90
|
-
[bot typing]
|
|
91
90
|
--- from: assistant (TestBot)
|
|
91
|
+
⬥ ok
|
|
92
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
93
|
+
[bot typing]
|
|
94
|
+
⬥ ok
|
|
95
|
+
[bot typing]
|
|
92
96
|
⬥ starting sleep 100
|
|
93
97
|
--- from: user (queue-advanced-tester)
|
|
94
98
|
Reply with exactly: typing-stop-interrupt-final
|
|
95
99
|
[bot typing]
|
|
96
100
|
[bot typing]
|
|
101
|
+
[bot typing]
|
|
97
102
|
--- from: assistant (TestBot)
|
|
98
103
|
⬥ ok
|
|
99
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
104
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
105
|
+
[bot typing]"
|
|
100
106
|
`);
|
|
101
107
|
expect(finalUserIndex).toBeGreaterThanOrEqual(0);
|
|
102
108
|
expect(finalReplyIndex).toBeGreaterThan(finalUserIndex);
|
|
@@ -278,6 +278,7 @@ describe('runtime lifecycle', () => {
|
|
|
278
278
|
--- from: assistant (TestBot)
|
|
279
279
|
⬥ ok
|
|
280
280
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
281
|
+
⬥ ok
|
|
281
282
|
--- from: user (lifecycle-tester)
|
|
282
283
|
Reply with exactly: seq-beta
|
|
283
284
|
--- from: assistant (TestBot)
|
|
@@ -287,6 +288,7 @@ describe('runtime lifecycle', () => {
|
|
|
287
288
|
Reply with exactly: seq-gamma
|
|
288
289
|
--- from: assistant (TestBot)
|
|
289
290
|
⬥ ok
|
|
291
|
+
⬥ ok
|
|
290
292
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
291
293
|
`);
|
|
292
294
|
expect(runtimeAfterC).toBe(runtimeAfterA);
|
|
@@ -324,7 +326,8 @@ describe('runtime lifecycle', () => {
|
|
|
324
326
|
Reply with exactly: footer-check
|
|
325
327
|
--- from: assistant (TestBot)
|
|
326
328
|
⬥ ok
|
|
327
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
329
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
330
|
+
⬥ ok"
|
|
328
331
|
`);
|
|
329
332
|
expect(footerMessage).toBeDefined();
|
|
330
333
|
if (!footerMessage) {
|
|
@@ -488,7 +488,6 @@ e2eTest('thread message queue ordering', () => {
|
|
|
488
488
|
Reply with exactly: three
|
|
489
489
|
--- from: assistant (TestBot)
|
|
490
490
|
⬥ ok
|
|
491
|
-
⬥ ok
|
|
492
491
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
493
492
|
`);
|
|
494
493
|
const userThreeIndex = after.findIndex((message) => {
|
|
@@ -565,6 +564,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
565
564
|
Prompt from test: respond with short text for opencode queue mode.
|
|
566
565
|
--- from: assistant (TestBot)
|
|
567
566
|
⬥ ok
|
|
567
|
+
⬥ ok
|
|
568
568
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
569
569
|
`);
|
|
570
570
|
const followupUserIndex = messagesWithFollowupFooter.findIndex((message) => {
|
|
@@ -632,7 +632,6 @@ e2eTest('thread message queue ordering', () => {
|
|
|
632
632
|
Reply with exactly: BASH_TOOL_FILE_MARKER
|
|
633
633
|
--- from: assistant (TestBot)
|
|
634
634
|
⬥ running create file
|
|
635
|
-
⬥ ok
|
|
636
635
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
637
636
|
`);
|
|
638
637
|
expect(fs.existsSync(markerPath)).toBe(true);
|
|
@@ -815,10 +814,10 @@ e2eTest('thread message queue ordering', () => {
|
|
|
815
814
|
Reply with exactly: echo
|
|
816
815
|
--- from: assistant (TestBot)
|
|
817
816
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
818
|
-
⬥ ok
|
|
819
817
|
--- from: user (queue-tester)
|
|
820
818
|
Reply with exactly: foxtrot
|
|
821
819
|
--- from: assistant (TestBot)
|
|
820
|
+
⬥ ok
|
|
822
821
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
823
822
|
`);
|
|
824
823
|
expect(userEchoIndex).toBeGreaterThan(-1);
|
|
@@ -899,7 +898,6 @@ e2eTest('thread message queue ordering', () => {
|
|
|
899
898
|
Reply with exactly: india
|
|
900
899
|
--- from: assistant (TestBot)
|
|
901
900
|
⬥ ok
|
|
902
|
-
⬥ ok
|
|
903
901
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
904
902
|
`);
|
|
905
903
|
const userIndiaIndex = after.findIndex((m) => {
|
|
@@ -991,7 +989,6 @@ e2eTest('thread message queue ordering', () => {
|
|
|
991
989
|
Reply with exactly: november
|
|
992
990
|
--- from: assistant (TestBot)
|
|
993
991
|
⬥ ok
|
|
994
|
-
⬥ ok
|
|
995
992
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
996
993
|
`);
|
|
997
994
|
// E's user message appears before the final bot response
|
|
@@ -394,7 +394,8 @@ e2eTest('voice message handling', () => {
|
|
|
394
394
|
🎤 Transcribing voice message...
|
|
395
395
|
📝 **Transcribed message:** Fix the login bug in auth.ts
|
|
396
396
|
⬥ session-reply
|
|
397
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
397
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
398
|
+
⬥ ok"
|
|
398
399
|
`);
|
|
399
400
|
expect(finalState.sessionId).toBeDefined();
|
|
400
401
|
// Verify OpenCode session received the transcribed voice message as a prompt
|
|
@@ -566,6 +567,7 @@ e2eTest('voice message handling', () => {
|
|
|
566
567
|
--- from: assistant (TestBot)
|
|
567
568
|
🎤 Transcribing voice message...
|
|
568
569
|
📝 **Transcribed message:** Add error handling to the parser
|
|
570
|
+
⬥ fast-response-done
|
|
569
571
|
⬥ session-reply
|
|
570
572
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
571
573
|
`);
|
|
@@ -874,7 +876,10 @@ e2eTest('voice message handling', () => {
|
|
|
874
876
|
[attachment: voice-message.ogg]
|
|
875
877
|
--- from: assistant (TestBot)
|
|
876
878
|
🎤 Transcribing voice message...
|
|
879
|
+
⬥ fast-response-done
|
|
880
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
877
881
|
📝 **Transcribed message:** Delayed transcription result
|
|
882
|
+
⬥ fast-response-done
|
|
878
883
|
⬥ session-reply
|
|
879
884
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
880
885
|
`);
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "kimaki",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.4.
|
|
5
|
+
"version": "0.4.87",
|
|
6
6
|
"repository": "https://github.com/remorses/kimaki",
|
|
7
7
|
"bin": "bin.js",
|
|
8
8
|
"files": [
|
|
@@ -66,9 +66,9 @@
|
|
|
66
66
|
"xdg-basedir": "^5.1.0",
|
|
67
67
|
"zod": "^4.3.6",
|
|
68
68
|
"zustand": "^5.0.11",
|
|
69
|
-
"
|
|
70
|
-
"traforo": "^0.2.
|
|
71
|
-
"
|
|
69
|
+
"libsqlproxy": "^0.1.0",
|
|
70
|
+
"traforo": "^0.2.2",
|
|
71
|
+
"errore": "^0.14.1"
|
|
72
72
|
},
|
|
73
73
|
"optionalDependencies": {
|
|
74
74
|
"@discordjs/opus": "^0.10.0",
|