kimaki 0.4.45 → 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 +5 -3
- package/dist/commands/fork.js +5 -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 +4 -2
- package/dist/commands/session.js +4 -2
- package/dist/commands/share.js +2 -2
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +4 -2
- package/dist/commands/verbosity.js +3 -3
- package/dist/commands/worktree-settings.js +2 -2
- package/dist/commands/worktree.js +20 -8
- package/dist/database.js +2 -2
- package/dist/discord-bot.js +5 -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 +93 -15
- 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 +6 -3
- package/src/commands/fork.ts +6 -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 +5 -2
- package/src/commands/session.ts +5 -2
- package/src/commands/share.ts +2 -2
- package/src/commands/undo-redo.ts +2 -2
- package/src/commands/user-command.ts +5 -2
- package/src/commands/verbosity.ts +3 -3
- package/src/commands/worktree-settings.ts +2 -2
- package/src/commands/worktree.ts +23 -7
- package/src/database.ts +2 -2
- package/src/discord-bot.ts +6 -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 +105 -15
- 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,
|
|
@@ -63,6 +63,8 @@ export async function handleCreateNewProjectCommand({ command, appId, }) {
|
|
|
63
63
|
autoArchiveDuration: 1440,
|
|
64
64
|
reason: 'New project session',
|
|
65
65
|
});
|
|
66
|
+
// Add user to thread so it appears in their sidebar
|
|
67
|
+
await thread.members.add(command.user.id);
|
|
66
68
|
await handleOpencodeSession({
|
|
67
69
|
prompt: 'The project was just initialized. Say hi and ask what the user wants to build.',
|
|
68
70
|
thread,
|
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) {
|
|
@@ -162,6 +162,8 @@ export async function handleForkSelectMenu(interaction) {
|
|
|
162
162
|
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
163
163
|
reason: `Forked from session ${sessionId}`,
|
|
164
164
|
});
|
|
165
|
+
// Add user to thread so it appears in their sidebar
|
|
166
|
+
await thread.members.add(interaction.user.id);
|
|
165
167
|
getDatabase()
|
|
166
168
|
.prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
|
|
167
169
|
.run(thread.id, forkedSession.id);
|
|
@@ -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);
|
|
@@ -51,6 +51,8 @@ export async function handleResumeCommand({ command, appId }) {
|
|
|
51
51
|
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
52
52
|
reason: `Resuming session ${sessionId}`,
|
|
53
53
|
});
|
|
54
|
+
// Add user to thread so it appears in their sidebar
|
|
55
|
+
await thread.members.add(command.user.id);
|
|
54
56
|
getDatabase()
|
|
55
57
|
.prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
|
|
56
58
|
.run(thread.id, sessionId);
|
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);
|
|
@@ -58,6 +58,8 @@ export async function handleSessionCommand({ command, appId }) {
|
|
|
58
58
|
autoArchiveDuration: 1440,
|
|
59
59
|
reason: 'OpenCode session',
|
|
60
60
|
});
|
|
61
|
+
// Add user to thread so it appears in their sidebar
|
|
62
|
+
await thread.members.add(command.user.id);
|
|
61
63
|
await command.editReply(`Created new session in ${thread.toString()}`);
|
|
62
64
|
await handleOpencodeSession({
|
|
63
65
|
prompt: fullPrompt,
|
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
|
|
@@ -105,6 +105,8 @@ export const handleUserCommand = async ({ command, appId }) => {
|
|
|
105
105
|
autoArchiveDuration: 1440,
|
|
106
106
|
reason: `OpenCode command: ${commandName}`,
|
|
107
107
|
});
|
|
108
|
+
// Add user to thread so it appears in their sidebar
|
|
109
|
+
await newThread.members.add(command.user.id);
|
|
108
110
|
await command.editReply(`Started /${commandName} in ${newThread.toString()}`);
|
|
109
111
|
await handleOpencodeSession({
|
|
110
112
|
prompt: '', // Not used when command is set
|
|
@@ -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 });
|
|
@@ -167,6 +171,8 @@ export async function handleNewWorktreeCommand({ command, appId, }) {
|
|
|
167
171
|
autoArchiveDuration: 1440,
|
|
168
172
|
reason: 'Worktree session',
|
|
169
173
|
});
|
|
174
|
+
// Add user to thread so it appears in their sidebar
|
|
175
|
+
await thread.members.add(command.user.id);
|
|
170
176
|
return { thread, starterMessage };
|
|
171
177
|
},
|
|
172
178
|
catch: (e) => new WorktreeError('Failed to create thread', { cause: e }),
|
|
@@ -251,6 +257,10 @@ async function handleWorktreeInThread({ command, appId, thread, }) {
|
|
|
251
257
|
await command.editReply(`Worktree \`${worktreeName}\` already exists at \`${existingWorktreePath}\``);
|
|
252
258
|
return;
|
|
253
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);
|
|
254
264
|
// Store pending worktree in database for this existing thread
|
|
255
265
|
createPendingWorktree({
|
|
256
266
|
threadId: thread.id,
|
|
@@ -258,18 +268,20 @@ async function handleWorktreeInThread({ command, appId, thread, }) {
|
|
|
258
268
|
projectDirectory,
|
|
259
269
|
});
|
|
260
270
|
// Send status message in thread
|
|
271
|
+
const diffNote = hasDiff ? '\n📋 Will transfer uncommitted changes' : '';
|
|
261
272
|
const statusMessage = await thread.send({
|
|
262
|
-
content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up
|
|
273
|
+
content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...${diffNote}`,
|
|
263
274
|
flags: SILENT_MESSAGE_FLAGS,
|
|
264
275
|
});
|
|
265
276
|
await command.editReply(`Creating worktree \`${worktreeName}\` for this thread...`);
|
|
266
|
-
// Create worktree in background
|
|
277
|
+
// Create worktree in background, passing diff to apply after creation
|
|
267
278
|
createWorktreeInBackground({
|
|
268
279
|
thread,
|
|
269
280
|
starterMessage: statusMessage,
|
|
270
281
|
worktreeName,
|
|
271
282
|
projectDirectory,
|
|
272
283
|
clientV2,
|
|
284
|
+
diff,
|
|
273
285
|
}).catch((e) => {
|
|
274
286
|
logger.error('[NEW-WORKTREE] Background error:', e);
|
|
275
287
|
});
|