kimaki 0.4.46 → 0.4.47
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/cli.js +27 -2
- package/dist/commands/abort.js +2 -2
- package/dist/commands/add-project.js +2 -2
- package/dist/commands/agent.js +4 -4
- package/dist/commands/ask-question.js +9 -8
- package/dist/commands/compact.js +126 -0
- package/dist/commands/create-new-project.js +3 -3
- package/dist/commands/fork.js +3 -3
- package/dist/commands/merge-worktree.js +2 -2
- package/dist/commands/model.js +5 -5
- package/dist/commands/permissions.js +2 -2
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +2 -2
- package/dist/commands/resume.js +2 -2
- package/dist/commands/session.js +2 -2
- package/dist/commands/share.js +2 -2
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +2 -2
- package/dist/commands/verbosity.js +3 -3
- package/dist/commands/worktree-settings.js +2 -2
- package/dist/commands/worktree.js +18 -8
- package/dist/database.js +2 -2
- package/dist/discord-bot.js +3 -3
- package/dist/discord-utils.js +2 -2
- package/dist/genai-worker-wrapper.js +3 -3
- package/dist/genai-worker.js +2 -2
- package/dist/genai.js +2 -2
- package/dist/interaction-handler.js +6 -2
- package/dist/logger.js +57 -9
- package/dist/markdown.js +2 -2
- package/dist/message-formatting.js +69 -6
- package/dist/openai-realtime.js +2 -2
- package/dist/opencode.js +2 -2
- package/dist/session-handler.js +53 -14
- package/dist/tools.js +2 -2
- package/dist/voice-handler.js +2 -2
- package/dist/voice.js +2 -2
- package/dist/worktree-utils.js +91 -7
- package/dist/xml.js +2 -2
- package/package.json +1 -1
- package/src/cli.ts +28 -2
- package/src/commands/abort.ts +2 -2
- package/src/commands/add-project.ts +2 -2
- package/src/commands/agent.ts +4 -4
- package/src/commands/ask-question.ts +9 -8
- package/src/commands/compact.ts +148 -0
- package/src/commands/create-new-project.ts +3 -3
- package/src/commands/fork.ts +3 -3
- package/src/commands/merge-worktree.ts +2 -2
- package/src/commands/model.ts +5 -5
- package/src/commands/permissions.ts +2 -2
- package/src/commands/queue.ts +2 -2
- package/src/commands/remove-project.ts +2 -2
- package/src/commands/resume.ts +2 -2
- package/src/commands/session.ts +2 -2
- package/src/commands/share.ts +2 -2
- package/src/commands/undo-redo.ts +2 -2
- package/src/commands/user-command.ts +2 -2
- package/src/commands/verbosity.ts +3 -3
- package/src/commands/worktree-settings.ts +2 -2
- package/src/commands/worktree.ts +20 -7
- package/src/database.ts +2 -2
- package/src/discord-bot.ts +3 -3
- package/src/discord-utils.ts +2 -2
- package/src/genai-worker-wrapper.ts +3 -3
- package/src/genai-worker.ts +2 -2
- package/src/genai.ts +2 -2
- package/src/interaction-handler.ts +7 -2
- package/src/logger.ts +64 -10
- package/src/markdown.ts +2 -2
- package/src/message-formatting.ts +82 -6
- package/src/openai-realtime.ts +2 -2
- package/src/opencode.ts +2 -2
- package/src/session-handler.ts +62 -14
- package/src/tools.ts +2 -2
- package/src/voice-handler.ts +2 -2
- package/src/voice.ts +2 -2
- package/src/worktree-utils.ts +111 -7
- package/src/xml.ts +2 -2
package/dist/cli.js
CHANGED
|
@@ -10,13 +10,33 @@ import { Events, ChannelType, REST, Routes, SlashCommandBuilder, AttachmentBuild
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import * as errore from 'errore';
|
|
13
|
-
import { createLogger } from './logger.js';
|
|
13
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
14
14
|
import { uploadFilesToDiscord } from './discord-utils.js';
|
|
15
15
|
import { spawn, spawnSync, execSync } from 'node:child_process';
|
|
16
16
|
import http from 'node:http';
|
|
17
17
|
import { setDataDir, getDataDir, getLockPort } from './config.js';
|
|
18
18
|
import { sanitizeAgentName } from './commands/agent.js';
|
|
19
|
-
const cliLogger = createLogger(
|
|
19
|
+
const cliLogger = createLogger(LogPrefix.CLI);
|
|
20
|
+
// Spawn caffeinate on macOS to prevent system sleep while bot is running.
|
|
21
|
+
// Not detached, so it dies automatically with the parent process.
|
|
22
|
+
function startCaffeinate() {
|
|
23
|
+
if (process.platform !== 'darwin') {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const proc = spawn('caffeinate', ['-i'], {
|
|
28
|
+
stdio: 'ignore',
|
|
29
|
+
detached: false,
|
|
30
|
+
});
|
|
31
|
+
proc.on('error', (err) => {
|
|
32
|
+
cliLogger.warn('Failed to start caffeinate:', err.message);
|
|
33
|
+
});
|
|
34
|
+
cliLogger.log('Started caffeinate to prevent system sleep');
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
cliLogger.warn('Failed to spawn caffeinate:', err instanceof Error ? err.message : String(err));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
20
40
|
const cli = cac('kimaki');
|
|
21
41
|
process.title = 'kimaki';
|
|
22
42
|
async function killProcessOnPort(port) {
|
|
@@ -217,6 +237,10 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
|
|
|
217
237
|
.setName('abort')
|
|
218
238
|
.setDescription('Abort the current OpenCode request in this thread')
|
|
219
239
|
.toJSON(),
|
|
240
|
+
new SlashCommandBuilder()
|
|
241
|
+
.setName('compact')
|
|
242
|
+
.setDescription('Compact the session context by summarizing conversation history')
|
|
243
|
+
.toJSON(),
|
|
220
244
|
new SlashCommandBuilder()
|
|
221
245
|
.setName('stop')
|
|
222
246
|
.setDescription('Abort the current OpenCode request in this thread')
|
|
@@ -389,6 +413,7 @@ async function backgroundInit({ currentDir, token, appId, }) {
|
|
|
389
413
|
}
|
|
390
414
|
}
|
|
391
415
|
async function run({ restart, addChannels, useWorktrees }) {
|
|
416
|
+
startCaffeinate();
|
|
392
417
|
const forceSetup = Boolean(restart);
|
|
393
418
|
intro('🤖 Discord Bot Setup');
|
|
394
419
|
// Step 0: Check if OpenCode CLI is available
|
package/dist/commands/abort.js
CHANGED
|
@@ -4,9 +4,9 @@ import { getDatabase } from '../database.js';
|
|
|
4
4
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
6
|
import { abortControllers } from '../session-handler.js';
|
|
7
|
-
import { createLogger } from '../logger.js';
|
|
7
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
8
8
|
import * as errore from 'errore';
|
|
9
|
-
const logger = createLogger(
|
|
9
|
+
const logger = createLogger(LogPrefix.ABORT);
|
|
10
10
|
export async function handleAbortCommand({ command }) {
|
|
11
11
|
const channel = command.channel;
|
|
12
12
|
if (!channel) {
|
|
@@ -4,10 +4,10 @@ import path from 'node:path';
|
|
|
4
4
|
import { getDatabase } from '../database.js';
|
|
5
5
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
6
|
import { createProjectChannels } from '../channel-management.js';
|
|
7
|
-
import { createLogger } from '../logger.js';
|
|
7
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
8
8
|
import { abbreviatePath } from '../utils.js';
|
|
9
9
|
import * as errore from 'errore';
|
|
10
|
-
const logger = createLogger(
|
|
10
|
+
const logger = createLogger(LogPrefix.ADD_PROJECT);
|
|
11
11
|
export async function handleAddProjectCommand({ command, appId }) {
|
|
12
12
|
await command.deferReply({ ephemeral: false });
|
|
13
13
|
const projectId = command.options.getString('project', true);
|
package/dist/commands/agent.js
CHANGED
|
@@ -5,9 +5,9 @@ import crypto from 'node:crypto';
|
|
|
5
5
|
import { getDatabase, setChannelAgent, setSessionAgent, clearSessionModel, runModelMigrations } from '../database.js';
|
|
6
6
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
7
7
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
8
|
-
import { createLogger } from '../logger.js';
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
9
|
import * as errore from 'errore';
|
|
10
|
-
const agentLogger = createLogger(
|
|
10
|
+
const agentLogger = createLogger(LogPrefix.AGENT);
|
|
11
11
|
const pendingAgentContexts = new Map();
|
|
12
12
|
/**
|
|
13
13
|
* Sanitize an agent name to be a valid Discord command name component.
|
|
@@ -181,7 +181,7 @@ export async function handleAgentSelectMenu(interaction) {
|
|
|
181
181
|
}
|
|
182
182
|
else {
|
|
183
183
|
await interaction.editReply({
|
|
184
|
-
content: `Agent preference set for this channel: **${selectedAgent}**\
|
|
184
|
+
content: `Agent preference set for this channel: **${selectedAgent}**\nAll new sessions in this channel will use this agent.`,
|
|
185
185
|
components: [],
|
|
186
186
|
});
|
|
187
187
|
}
|
|
@@ -237,7 +237,7 @@ export async function handleQuickAgentCommand({ command, appId, }) {
|
|
|
237
237
|
}
|
|
238
238
|
else {
|
|
239
239
|
await command.editReply({
|
|
240
|
-
content: `Switched to **${matchingAgent.name}** agent for this channel\
|
|
240
|
+
content: `Switched to **${matchingAgent.name}** agent for this channel\nAll new sessions will use this agent.`,
|
|
241
241
|
});
|
|
242
242
|
}
|
|
243
243
|
}
|
|
@@ -5,8 +5,8 @@ import { StringSelectMenuBuilder, StringSelectMenuInteraction, ActionRowBuilder,
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
7
7
|
import { getOpencodeClientV2 } from '../opencode.js';
|
|
8
|
-
import { createLogger } from '../logger.js';
|
|
9
|
-
const logger = createLogger(
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
|
+
const logger = createLogger(LogPrefix.ASK_QUESTION);
|
|
10
10
|
// Store pending question contexts by hash
|
|
11
11
|
export const pendingQuestionContexts = new Map();
|
|
12
12
|
/**
|
|
@@ -178,9 +178,9 @@ export function parseAskUserQuestionTool(part) {
|
|
|
178
178
|
}
|
|
179
179
|
/**
|
|
180
180
|
* Cancel a pending question for a thread (e.g., when user sends a new message).
|
|
181
|
-
* Sends
|
|
181
|
+
* Sends the user's message as the answer to OpenCode so the model sees their actual response.
|
|
182
182
|
*/
|
|
183
|
-
export async function cancelPendingQuestion(threadId) {
|
|
183
|
+
export async function cancelPendingQuestion(threadId, userMessage) {
|
|
184
184
|
// Find pending question for this thread
|
|
185
185
|
let contextHash;
|
|
186
186
|
let context;
|
|
@@ -199,18 +199,19 @@ export async function cancelPendingQuestion(threadId) {
|
|
|
199
199
|
if (!clientV2) {
|
|
200
200
|
throw new Error('OpenCode server not found for directory');
|
|
201
201
|
}
|
|
202
|
-
//
|
|
202
|
+
// Use user's message as answer if provided, otherwise mark as "Other"
|
|
203
|
+
const customAnswer = userMessage || 'Other';
|
|
203
204
|
const answers = context.questions.map((_, i) => {
|
|
204
|
-
return context.answers[i] || [
|
|
205
|
+
return context.answers[i] || [customAnswer];
|
|
205
206
|
});
|
|
206
207
|
await clientV2.question.reply({
|
|
207
208
|
requestID: context.requestId,
|
|
208
209
|
answers,
|
|
209
210
|
});
|
|
210
|
-
logger.log(`
|
|
211
|
+
logger.log(`Answered question ${context.requestId} with user message`);
|
|
211
212
|
}
|
|
212
213
|
catch (error) {
|
|
213
|
-
logger.error('Failed to
|
|
214
|
+
logger.error('Failed to answer question:', error);
|
|
214
215
|
}
|
|
215
216
|
// Clean up regardless of whether the API call succeeded
|
|
216
217
|
pendingQuestionContexts.delete(contextHash);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// /compact command - Trigger context compaction (summarization) for the current session.
|
|
2
|
+
import { ChannelType } from 'discord.js';
|
|
3
|
+
import { getDatabase } from '../database.js';
|
|
4
|
+
import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js';
|
|
5
|
+
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
7
|
+
const logger = createLogger(LogPrefix.COMPACT);
|
|
8
|
+
export async function handleCompactCommand({ command }) {
|
|
9
|
+
const channel = command.channel;
|
|
10
|
+
if (!channel) {
|
|
11
|
+
await command.reply({
|
|
12
|
+
content: 'This command can only be used in a channel',
|
|
13
|
+
ephemeral: true,
|
|
14
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const isThread = [
|
|
19
|
+
ChannelType.PublicThread,
|
|
20
|
+
ChannelType.PrivateThread,
|
|
21
|
+
ChannelType.AnnouncementThread,
|
|
22
|
+
].includes(channel.type);
|
|
23
|
+
if (!isThread) {
|
|
24
|
+
await command.reply({
|
|
25
|
+
content: 'This command can only be used in a thread with an active session',
|
|
26
|
+
ephemeral: true,
|
|
27
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const textChannel = await resolveTextChannel(channel);
|
|
32
|
+
const { projectDirectory: directory } = getKimakiMetadata(textChannel);
|
|
33
|
+
if (!directory) {
|
|
34
|
+
await command.reply({
|
|
35
|
+
content: 'Could not determine project directory for this channel',
|
|
36
|
+
ephemeral: true,
|
|
37
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const row = getDatabase()
|
|
42
|
+
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
43
|
+
.get(channel.id);
|
|
44
|
+
if (!row?.session_id) {
|
|
45
|
+
await command.reply({
|
|
46
|
+
content: 'No active session in this thread',
|
|
47
|
+
ephemeral: true,
|
|
48
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const sessionId = row.session_id;
|
|
53
|
+
// Ensure server is running for this directory
|
|
54
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
|
+
if (getClient instanceof Error) {
|
|
56
|
+
await command.reply({
|
|
57
|
+
content: `Failed to compact: ${getClient.message}`,
|
|
58
|
+
ephemeral: true,
|
|
59
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const clientV2 = getOpencodeClientV2(directory);
|
|
64
|
+
if (!clientV2) {
|
|
65
|
+
await command.reply({
|
|
66
|
+
content: 'Failed to get OpenCode client',
|
|
67
|
+
ephemeral: true,
|
|
68
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Defer reply since compaction may take a moment
|
|
73
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
74
|
+
try {
|
|
75
|
+
// Get session messages to find the model from the last user message
|
|
76
|
+
const messagesResult = await clientV2.session.messages({
|
|
77
|
+
sessionID: sessionId,
|
|
78
|
+
directory,
|
|
79
|
+
});
|
|
80
|
+
if (messagesResult.error || !messagesResult.data) {
|
|
81
|
+
logger.error('[COMPACT] Failed to get messages:', messagesResult.error);
|
|
82
|
+
await command.editReply({
|
|
83
|
+
content: 'Failed to compact: Could not retrieve session messages',
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Find the last user message to get the model
|
|
88
|
+
const lastUserMessage = [...messagesResult.data]
|
|
89
|
+
.reverse()
|
|
90
|
+
.find((msg) => msg.info.role === 'user');
|
|
91
|
+
if (!lastUserMessage || lastUserMessage.info.role !== 'user') {
|
|
92
|
+
await command.editReply({
|
|
93
|
+
content: 'Failed to compact: No user message found in session',
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const { providerID, modelID } = lastUserMessage.info.model;
|
|
98
|
+
const result = await clientV2.session.summarize({
|
|
99
|
+
sessionID: sessionId,
|
|
100
|
+
directory,
|
|
101
|
+
providerID,
|
|
102
|
+
modelID,
|
|
103
|
+
auto: false,
|
|
104
|
+
});
|
|
105
|
+
if (result.error) {
|
|
106
|
+
logger.error('[COMPACT] Error:', result.error);
|
|
107
|
+
const errorMessage = 'data' in result.error && result.error.data
|
|
108
|
+
? result.error.data.message || 'Unknown error'
|
|
109
|
+
: 'Unknown error';
|
|
110
|
+
await command.editReply({
|
|
111
|
+
content: `Failed to compact: ${errorMessage}`,
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
await command.editReply({
|
|
116
|
+
content: `📦 Session **compacted** successfully`,
|
|
117
|
+
});
|
|
118
|
+
logger.log(`Session ${sessionId} compacted by user`);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
logger.error('[COMPACT] Error:', error);
|
|
122
|
+
await command.editReply({
|
|
123
|
+
content: `Failed to compact: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -6,8 +6,8 @@ import { getProjectsDir } from '../config.js';
|
|
|
6
6
|
import { createProjectChannels } from '../channel-management.js';
|
|
7
7
|
import { handleOpencodeSession } from '../session-handler.js';
|
|
8
8
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
9
|
-
import { createLogger } from '../logger.js';
|
|
10
|
-
const logger = createLogger(
|
|
9
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
10
|
+
const logger = createLogger(LogPrefix.CREATE_PROJECT);
|
|
11
11
|
export async function handleCreateNewProjectCommand({ command, appId, }) {
|
|
12
12
|
await command.deferReply({ ephemeral: false });
|
|
13
13
|
const projectName = command.options.getString('name', true);
|
|
@@ -53,7 +53,7 @@ export async function handleCreateNewProjectCommand({ command, appId, }) {
|
|
|
53
53
|
appId,
|
|
54
54
|
});
|
|
55
55
|
const textChannel = (await guild.channels.fetch(textChannelId));
|
|
56
|
-
await command.editReply(`✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\
|
|
56
|
+
await command.editReply(`✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n_Starting session..._`);
|
|
57
57
|
const starterMessage = await textChannel.send({
|
|
58
58
|
content: `🚀 **New project initialized**\n📁 \`${projectDirectory}\``,
|
|
59
59
|
flags: SILENT_MESSAGE_FLAGS,
|
package/dist/commands/fork.js
CHANGED
|
@@ -4,10 +4,10 @@ import { getDatabase } from '../database.js';
|
|
|
4
4
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, sendThreadMessage } from '../discord-utils.js';
|
|
6
6
|
import { collectLastAssistantParts } from '../message-formatting.js';
|
|
7
|
-
import { createLogger } from '../logger.js';
|
|
7
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
8
8
|
import * as errore from 'errore';
|
|
9
|
-
const sessionLogger = createLogger(
|
|
10
|
-
const forkLogger = createLogger(
|
|
9
|
+
const sessionLogger = createLogger(LogPrefix.SESSION);
|
|
10
|
+
const forkLogger = createLogger(LogPrefix.FORK);
|
|
11
11
|
export async function handleForkCommand(interaction) {
|
|
12
12
|
const channel = interaction.channel;
|
|
13
13
|
if (!channel) {
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// After merge, switches to detached HEAD at main so user can keep working.
|
|
4
4
|
import {} from 'discord.js';
|
|
5
5
|
import { getThreadWorktree } from '../database.js';
|
|
6
|
-
import { createLogger } from '../logger.js';
|
|
6
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
7
7
|
import { execAsync } from '../worktree-utils.js';
|
|
8
|
-
const logger = createLogger(
|
|
8
|
+
const logger = createLogger(LogPrefix.WORKTREE);
|
|
9
9
|
/** Worktree thread title prefix - indicates unmerged worktree */
|
|
10
10
|
export const WORKTREE_PREFIX = '⬦ ';
|
|
11
11
|
/**
|
package/dist/commands/model.js
CHANGED
|
@@ -5,9 +5,9 @@ import { getDatabase, setChannelModel, setSessionModel, runModelMigrations } fro
|
|
|
5
5
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
6
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
7
7
|
import { abortAndRetrySession } from '../session-handler.js';
|
|
8
|
-
import { createLogger } from '../logger.js';
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
9
|
import * as errore from 'errore';
|
|
10
|
-
const modelLogger = createLogger(
|
|
10
|
+
const modelLogger = createLogger(LogPrefix.MODEL);
|
|
11
11
|
// Store context by hash to avoid customId length limits (Discord max: 100 chars)
|
|
12
12
|
const pendingModelContexts = new Map();
|
|
13
13
|
/**
|
|
@@ -292,13 +292,13 @@ export async function handleModelSelectMenu(interaction) {
|
|
|
292
292
|
}
|
|
293
293
|
if (retried) {
|
|
294
294
|
await interaction.editReply({
|
|
295
|
-
content: `Model changed for this session:\n**${context.providerName}** / **${selectedModelId}**\n
|
|
295
|
+
content: `Model changed for this session:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\n_Retrying current request with new model..._`,
|
|
296
296
|
components: [],
|
|
297
297
|
});
|
|
298
298
|
}
|
|
299
299
|
else {
|
|
300
300
|
await interaction.editReply({
|
|
301
|
-
content: `Model preference set for this session:\n**${context.providerName}** / **${selectedModelId}**\n
|
|
301
|
+
content: `Model preference set for this session:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\``,
|
|
302
302
|
components: [],
|
|
303
303
|
});
|
|
304
304
|
}
|
|
@@ -308,7 +308,7 @@ export async function handleModelSelectMenu(interaction) {
|
|
|
308
308
|
setChannelModel(context.channelId, fullModelId);
|
|
309
309
|
modelLogger.log(`Set model ${fullModelId} for channel ${context.channelId}`);
|
|
310
310
|
await interaction.editReply({
|
|
311
|
-
content: `Model preference set for this channel:\n**${context.providerName}** / **${selectedModelId}**\n
|
|
311
|
+
content: `Model preference set for this channel:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\nAll new sessions in this channel will use this model.`,
|
|
312
312
|
components: [],
|
|
313
313
|
});
|
|
314
314
|
}
|
|
@@ -5,8 +5,8 @@ import { StringSelectMenuBuilder, StringSelectMenuInteraction, ActionRowBuilder,
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { getOpencodeClientV2 } from '../opencode.js';
|
|
7
7
|
import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
8
|
-
import { createLogger } from '../logger.js';
|
|
9
|
-
const logger = createLogger(
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
|
+
const logger = createLogger(LogPrefix.PERMISSIONS);
|
|
10
10
|
// Store pending permission contexts by hash
|
|
11
11
|
export const pendingPermissionContexts = new Map();
|
|
12
12
|
/**
|
package/dist/commands/queue.js
CHANGED
|
@@ -3,8 +3,8 @@ import { ChannelType } from 'discord.js';
|
|
|
3
3
|
import { getDatabase } from '../database.js';
|
|
4
4
|
import { resolveTextChannel, getKimakiMetadata, sendThreadMessage, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
5
5
|
import { handleOpencodeSession, abortControllers, addToQueue, getQueueLength, clearQueue, } from '../session-handler.js';
|
|
6
|
-
import { createLogger } from '../logger.js';
|
|
7
|
-
const logger = createLogger(
|
|
6
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
7
|
+
const logger = createLogger(LogPrefix.QUEUE);
|
|
8
8
|
export async function handleQueueCommand({ command }) {
|
|
9
9
|
const message = command.options.getString('message', true);
|
|
10
10
|
const channel = command.channel;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import * as errore from 'errore';
|
|
4
4
|
import { getDatabase } from '../database.js';
|
|
5
|
-
import { createLogger } from '../logger.js';
|
|
5
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
6
6
|
import { abbreviatePath } from '../utils.js';
|
|
7
|
-
const logger = createLogger(
|
|
7
|
+
const logger = createLogger(LogPrefix.REMOVE_PROJECT);
|
|
8
8
|
export async function handleRemoveProjectCommand({ command, appId }) {
|
|
9
9
|
await command.deferReply({ ephemeral: false });
|
|
10
10
|
const directory = command.options.getString('project', true);
|
package/dist/commands/resume.js
CHANGED
|
@@ -5,9 +5,9 @@ import { getDatabase, getChannelDirectory } from '../database.js';
|
|
|
5
5
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
6
|
import { sendThreadMessage, resolveTextChannel } from '../discord-utils.js';
|
|
7
7
|
import { collectLastAssistantParts } from '../message-formatting.js';
|
|
8
|
-
import { createLogger } from '../logger.js';
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
9
|
import * as errore from 'errore';
|
|
10
|
-
const logger = createLogger(
|
|
10
|
+
const logger = createLogger(LogPrefix.RESUME);
|
|
11
11
|
export async function handleResumeCommand({ command, appId }) {
|
|
12
12
|
await command.deferReply({ ephemeral: false });
|
|
13
13
|
const sessionId = command.options.getString('session', true);
|
package/dist/commands/session.js
CHANGED
|
@@ -6,9 +6,9 @@ import { getDatabase, getChannelDirectory } from '../database.js';
|
|
|
6
6
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
7
7
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
8
8
|
import { handleOpencodeSession } from '../session-handler.js';
|
|
9
|
-
import { createLogger } from '../logger.js';
|
|
9
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
10
10
|
import * as errore from 'errore';
|
|
11
|
-
const logger = createLogger(
|
|
11
|
+
const logger = createLogger(LogPrefix.SESSION);
|
|
12
12
|
export async function handleSessionCommand({ command, appId }) {
|
|
13
13
|
await command.deferReply({ ephemeral: false });
|
|
14
14
|
const prompt = command.options.getString('prompt', true);
|
package/dist/commands/share.js
CHANGED
|
@@ -3,9 +3,9 @@ import { ChannelType } from 'discord.js';
|
|
|
3
3
|
import { getDatabase } from '../database.js';
|
|
4
4
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
|
-
import { createLogger } from '../logger.js';
|
|
6
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
7
7
|
import * as errore from 'errore';
|
|
8
|
-
const logger = createLogger(
|
|
8
|
+
const logger = createLogger(LogPrefix.SHARE);
|
|
9
9
|
export async function handleShareCommand({ command }) {
|
|
10
10
|
const channel = command.channel;
|
|
11
11
|
if (!channel) {
|
|
@@ -3,9 +3,9 @@ import { ChannelType } from 'discord.js';
|
|
|
3
3
|
import { getDatabase } from '../database.js';
|
|
4
4
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
|
-
import { createLogger } from '../logger.js';
|
|
6
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
7
7
|
import * as errore from 'errore';
|
|
8
|
-
const logger = createLogger(
|
|
8
|
+
const logger = createLogger(LogPrefix.UNDO_REDO);
|
|
9
9
|
export async function handleUndoCommand({ command }) {
|
|
10
10
|
const channel = command.channel;
|
|
11
11
|
if (!channel) {
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import { ChannelType } from 'discord.js';
|
|
4
4
|
import { handleOpencodeSession } from '../session-handler.js';
|
|
5
5
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
|
-
import { createLogger } from '../logger.js';
|
|
6
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
7
7
|
import { getDatabase, getChannelDirectory } from '../database.js';
|
|
8
8
|
import fs from 'node:fs';
|
|
9
|
-
const userCommandLogger = createLogger(
|
|
9
|
+
const userCommandLogger = createLogger(LogPrefix.USER_CMD);
|
|
10
10
|
export const handleUserCommand = async ({ command, appId }) => {
|
|
11
11
|
const discordCommandName = command.commandName;
|
|
12
12
|
// Strip the -cmd suffix to get the actual OpenCode command name
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// 'text-only': only shows text responses (⬥ diamond parts)
|
|
5
5
|
import { ChatInputCommandInteraction, ChannelType } from 'discord.js';
|
|
6
6
|
import { getChannelVerbosity, setChannelVerbosity } from '../database.js';
|
|
7
|
-
import { createLogger } from '../logger.js';
|
|
8
|
-
const verbosityLogger = createLogger(
|
|
7
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
8
|
+
const verbosityLogger = createLogger(LogPrefix.VERBOSITY);
|
|
9
9
|
/**
|
|
10
10
|
* Handle the /verbosity slash command.
|
|
11
11
|
* Sets output verbosity for the channel (applies to new sessions).
|
|
@@ -47,7 +47,7 @@ export async function handleVerbosityCommand({ command, appId, }) {
|
|
|
47
47
|
? 'Only text responses will be shown. Tool executions, status messages, and thinking will be hidden.'
|
|
48
48
|
: 'All output will be shown, including tool executions and status messages.';
|
|
49
49
|
await command.reply({
|
|
50
|
-
content: `Verbosity set to **${level}**.\n
|
|
50
|
+
content: `Verbosity set to **${level}**.\n${description}\nThis applies to all new sessions in this channel.`,
|
|
51
51
|
ephemeral: true,
|
|
52
52
|
});
|
|
53
53
|
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
import { ChatInputCommandInteraction, ChannelType } from 'discord.js';
|
|
5
5
|
import { getChannelWorktreesEnabled, setChannelWorktreesEnabled } from '../database.js';
|
|
6
6
|
import { getKimakiMetadata } from '../discord-utils.js';
|
|
7
|
-
import { createLogger } from '../logger.js';
|
|
8
|
-
const worktreeSettingsLogger = createLogger(
|
|
7
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
8
|
+
const worktreeSettingsLogger = createLogger(LogPrefix.WORKTREE);
|
|
9
9
|
/**
|
|
10
10
|
* Handle the /enable-worktrees slash command.
|
|
11
11
|
* Enables automatic worktree creation for new sessions in this channel.
|
|
@@ -6,11 +6,11 @@ import fs from 'node:fs';
|
|
|
6
6
|
import { createPendingWorktree, setWorktreeReady, setWorktreeError, getChannelDirectory, getThreadWorktree, } from '../database.js';
|
|
7
7
|
import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js';
|
|
8
8
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
9
|
-
import { createLogger } from '../logger.js';
|
|
10
|
-
import { createWorktreeWithSubmodules } from '../worktree-utils.js';
|
|
9
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
10
|
+
import { createWorktreeWithSubmodules, captureGitDiff } from '../worktree-utils.js';
|
|
11
11
|
import { WORKTREE_PREFIX } from './merge-worktree.js';
|
|
12
12
|
import * as errore from 'errore';
|
|
13
|
-
const logger = createLogger(
|
|
13
|
+
const logger = createLogger(LogPrefix.WORKTREE);
|
|
14
14
|
class WorktreeError extends Error {
|
|
15
15
|
constructor(message, options) {
|
|
16
16
|
super(message, options);
|
|
@@ -69,14 +69,16 @@ function getProjectDirectoryFromChannel(channel, appId) {
|
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
71
|
* Create worktree in background and update starter message when done.
|
|
72
|
+
* If diff is provided, it's applied during worktree creation (before submodule init).
|
|
72
73
|
*/
|
|
73
|
-
async function createWorktreeInBackground({ thread, starterMessage, worktreeName, projectDirectory, clientV2, }) {
|
|
74
|
-
// Create worktree using SDK v2
|
|
74
|
+
async function createWorktreeInBackground({ thread, starterMessage, worktreeName, projectDirectory, clientV2, diff, }) {
|
|
75
|
+
// Create worktree using SDK v2, apply diff, then init submodules
|
|
75
76
|
logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory}`);
|
|
76
77
|
const worktreeResult = await createWorktreeWithSubmodules({
|
|
77
78
|
clientV2,
|
|
78
79
|
directory: projectDirectory,
|
|
79
80
|
name: worktreeName,
|
|
81
|
+
diff,
|
|
80
82
|
});
|
|
81
83
|
if (worktreeResult instanceof Error) {
|
|
82
84
|
const errorMsg = worktreeResult.message;
|
|
@@ -87,9 +89,11 @@ async function createWorktreeInBackground({ thread, starterMessage, worktreeName
|
|
|
87
89
|
}
|
|
88
90
|
// Success - update database and edit starter message
|
|
89
91
|
setWorktreeReady({ threadId: thread.id, worktreeDirectory: worktreeResult.directory });
|
|
92
|
+
const diffStatus = diff ? (worktreeResult.diffApplied ? '\n✅ Changes applied' : '\n⚠️ Failed to apply changes') : '';
|
|
90
93
|
await starterMessage.edit(`🌳 **Worktree: ${worktreeName}**\n` +
|
|
91
94
|
`📁 \`${worktreeResult.directory}\`\n` +
|
|
92
|
-
`🌿 Branch: \`${worktreeResult.branch}\``
|
|
95
|
+
`🌿 Branch: \`${worktreeResult.branch}\`` +
|
|
96
|
+
diffStatus);
|
|
93
97
|
}
|
|
94
98
|
export async function handleNewWorktreeCommand({ command, appId, }) {
|
|
95
99
|
await command.deferReply({ ephemeral: false });
|
|
@@ -253,6 +257,10 @@ async function handleWorktreeInThread({ command, appId, thread, }) {
|
|
|
253
257
|
await command.editReply(`Worktree \`${worktreeName}\` already exists at \`${existingWorktreePath}\``);
|
|
254
258
|
return;
|
|
255
259
|
}
|
|
260
|
+
// Capture git diff from project directory before creating worktree.
|
|
261
|
+
// This allows transferring uncommitted changes to the new worktree.
|
|
262
|
+
const diff = await captureGitDiff(projectDirectory);
|
|
263
|
+
const hasDiff = diff && (diff.staged || diff.unstaged);
|
|
256
264
|
// Store pending worktree in database for this existing thread
|
|
257
265
|
createPendingWorktree({
|
|
258
266
|
threadId: thread.id,
|
|
@@ -260,18 +268,20 @@ async function handleWorktreeInThread({ command, appId, thread, }) {
|
|
|
260
268
|
projectDirectory,
|
|
261
269
|
});
|
|
262
270
|
// Send status message in thread
|
|
271
|
+
const diffNote = hasDiff ? '\n📋 Will transfer uncommitted changes' : '';
|
|
263
272
|
const statusMessage = await thread.send({
|
|
264
|
-
content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up
|
|
273
|
+
content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...${diffNote}`,
|
|
265
274
|
flags: SILENT_MESSAGE_FLAGS,
|
|
266
275
|
});
|
|
267
276
|
await command.editReply(`Creating worktree \`${worktreeName}\` for this thread...`);
|
|
268
|
-
// Create worktree in background
|
|
277
|
+
// Create worktree in background, passing diff to apply after creation
|
|
269
278
|
createWorktreeInBackground({
|
|
270
279
|
thread,
|
|
271
280
|
starterMessage: statusMessage,
|
|
272
281
|
worktreeName,
|
|
273
282
|
projectDirectory,
|
|
274
283
|
clientV2,
|
|
284
|
+
diff,
|
|
275
285
|
}).catch((e) => {
|
|
276
286
|
logger.error('[NEW-WORKTREE] Background error:', e);
|
|
277
287
|
});
|
package/dist/database.js
CHANGED
|
@@ -5,9 +5,9 @@ import Database from 'better-sqlite3';
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import * as errore from 'errore';
|
|
8
|
-
import { createLogger } from './logger.js';
|
|
8
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
9
9
|
import { getDataDir } from './config.js';
|
|
10
|
-
const dbLogger = createLogger(
|
|
10
|
+
const dbLogger = createLogger(LogPrefix.DB);
|
|
11
11
|
let db = null;
|
|
12
12
|
export function getDatabase() {
|
|
13
13
|
if (!db) {
|
package/dist/discord-bot.js
CHANGED
|
@@ -22,14 +22,14 @@ export { ensureKimakiCategory, ensureKimakiAudioCategory, createProjectChannels,
|
|
|
22
22
|
import { ChannelType, Client, Events, GatewayIntentBits, Partials, PermissionsBitField, ThreadAutoArchiveDuration, } from 'discord.js';
|
|
23
23
|
import fs from 'node:fs';
|
|
24
24
|
import * as errore from 'errore';
|
|
25
|
-
import { createLogger } from './logger.js';
|
|
25
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
26
26
|
import { setGlobalDispatcher, Agent } from 'undici';
|
|
27
27
|
// Increase connection pool to prevent deadlock when multiple sessions have open SSE streams.
|
|
28
28
|
// Each session's event.subscribe() holds a connection; without enough connections,
|
|
29
29
|
// regular HTTP requests (question.reply, session.prompt) get blocked → deadlock.
|
|
30
30
|
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0, connections: 500 }));
|
|
31
|
-
const discordLogger = createLogger(
|
|
32
|
-
const voiceLogger = createLogger(
|
|
31
|
+
const discordLogger = createLogger(LogPrefix.DISCORD);
|
|
32
|
+
const voiceLogger = createLogger(LogPrefix.VOICE);
|
|
33
33
|
export async function createDiscordClient() {
|
|
34
34
|
return new Client({
|
|
35
35
|
intents: [
|
package/dist/discord-utils.js
CHANGED
|
@@ -7,11 +7,11 @@ import { formatMarkdownTables } from './format-tables.js';
|
|
|
7
7
|
import { getChannelDirectory } from './database.js';
|
|
8
8
|
import { limitHeadingDepth } from './limit-heading-depth.js';
|
|
9
9
|
import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js';
|
|
10
|
-
import { createLogger } from './logger.js';
|
|
10
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
11
11
|
import mime from 'mime';
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
-
const discordLogger = createLogger(
|
|
14
|
+
const discordLogger = createLogger(LogPrefix.DISCORD);
|
|
15
15
|
export const SILENT_MESSAGE_FLAGS = 4 | 4096;
|
|
16
16
|
// Same as SILENT but without SuppressNotifications - triggers badge/notification
|
|
17
17
|
export const NOTIFY_MESSAGE_FLAGS = 4;
|