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/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -43,14 +43,34 @@ import path from 'node:path'
|
|
|
43
43
|
import fs from 'node:fs'
|
|
44
44
|
import * as errore from 'errore'
|
|
45
45
|
|
|
46
|
-
import { createLogger } from './logger.js'
|
|
46
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
47
47
|
import { uploadFilesToDiscord } from './discord-utils.js'
|
|
48
48
|
import { spawn, spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
|
|
49
49
|
import http from 'node:http'
|
|
50
50
|
import { setDataDir, getDataDir, getLockPort } from './config.js'
|
|
51
51
|
import { sanitizeAgentName } from './commands/agent.js'
|
|
52
52
|
|
|
53
|
-
const cliLogger = createLogger(
|
|
53
|
+
const cliLogger = createLogger(LogPrefix.CLI)
|
|
54
|
+
|
|
55
|
+
// Spawn caffeinate on macOS to prevent system sleep while bot is running.
|
|
56
|
+
// Not detached, so it dies automatically with the parent process.
|
|
57
|
+
function startCaffeinate() {
|
|
58
|
+
if (process.platform !== 'darwin') {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const proc = spawn('caffeinate', ['-i'], {
|
|
63
|
+
stdio: 'ignore',
|
|
64
|
+
detached: false,
|
|
65
|
+
})
|
|
66
|
+
proc.on('error', (err) => {
|
|
67
|
+
cliLogger.warn('Failed to start caffeinate:', err.message)
|
|
68
|
+
})
|
|
69
|
+
cliLogger.log('Started caffeinate to prevent system sleep')
|
|
70
|
+
} catch (err) {
|
|
71
|
+
cliLogger.warn('Failed to spawn caffeinate:', err instanceof Error ? err.message : String(err))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
54
74
|
const cli = cac('kimaki')
|
|
55
75
|
|
|
56
76
|
process.title = 'kimaki'
|
|
@@ -301,6 +321,10 @@ async function registerCommands({
|
|
|
301
321
|
.setName('abort')
|
|
302
322
|
.setDescription('Abort the current OpenCode request in this thread')
|
|
303
323
|
.toJSON(),
|
|
324
|
+
new SlashCommandBuilder()
|
|
325
|
+
.setName('compact')
|
|
326
|
+
.setDescription('Compact the session context by summarizing conversation history')
|
|
327
|
+
.toJSON(),
|
|
304
328
|
new SlashCommandBuilder()
|
|
305
329
|
.setName('stop')
|
|
306
330
|
.setDescription('Abort the current OpenCode request in this thread')
|
|
@@ -545,6 +569,8 @@ async function backgroundInit({
|
|
|
545
569
|
}
|
|
546
570
|
|
|
547
571
|
async function run({ restart, addChannels, useWorktrees }: CliOptions) {
|
|
572
|
+
startCaffeinate()
|
|
573
|
+
|
|
548
574
|
const forceSetup = Boolean(restart)
|
|
549
575
|
|
|
550
576
|
intro('🤖 Discord Bot Setup')
|
package/src/commands/abort.ts
CHANGED
|
@@ -6,10 +6,10 @@ import { getDatabase } from '../database.js'
|
|
|
6
6
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
7
7
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
8
8
|
import { abortControllers } 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
11
|
|
|
12
|
-
const logger = createLogger(
|
|
12
|
+
const logger = createLogger(LogPrefix.ABORT)
|
|
13
13
|
|
|
14
14
|
export async function handleAbortCommand({ command }: CommandContext): Promise<void> {
|
|
15
15
|
const channel = command.channel
|
|
@@ -6,11 +6,11 @@ import type { CommandContext, AutocompleteContext } from './types.js'
|
|
|
6
6
|
import { getDatabase } from '../database.js'
|
|
7
7
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
8
8
|
import { createProjectChannels } from '../channel-management.js'
|
|
9
|
-
import { createLogger } from '../logger.js'
|
|
9
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
10
10
|
import { abbreviatePath } from '../utils.js'
|
|
11
11
|
import * as errore from 'errore'
|
|
12
12
|
|
|
13
|
-
const logger = createLogger(
|
|
13
|
+
const logger = createLogger(LogPrefix.ADD_PROJECT)
|
|
14
14
|
|
|
15
15
|
export async function handleAddProjectCommand({ command, appId }: CommandContext): Promise<void> {
|
|
16
16
|
await command.deferReply({ ephemeral: false })
|
package/src/commands/agent.ts
CHANGED
|
@@ -14,10 +14,10 @@ import crypto from 'node:crypto'
|
|
|
14
14
|
import { getDatabase, setChannelAgent, setSessionAgent, clearSessionModel, runModelMigrations } from '../database.js'
|
|
15
15
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
16
16
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
|
|
17
|
-
import { createLogger } from '../logger.js'
|
|
17
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
18
18
|
import * as errore from 'errore'
|
|
19
19
|
|
|
20
|
-
const agentLogger = createLogger(
|
|
20
|
+
const agentLogger = createLogger(LogPrefix.AGENT)
|
|
21
21
|
|
|
22
22
|
const pendingAgentContexts = new Map<
|
|
23
23
|
string,
|
|
@@ -257,7 +257,7 @@ export async function handleAgentSelectMenu(
|
|
|
257
257
|
})
|
|
258
258
|
} else {
|
|
259
259
|
await interaction.editReply({
|
|
260
|
-
content: `Agent preference set for this channel: **${selectedAgent}**\
|
|
260
|
+
content: `Agent preference set for this channel: **${selectedAgent}**\nAll new sessions in this channel will use this agent.`,
|
|
261
261
|
components: [],
|
|
262
262
|
})
|
|
263
263
|
}
|
|
@@ -331,7 +331,7 @@ export async function handleQuickAgentCommand({
|
|
|
331
331
|
})
|
|
332
332
|
} else {
|
|
333
333
|
await command.editReply({
|
|
334
|
-
content: `Switched to **${matchingAgent.name}** agent for this channel\
|
|
334
|
+
content: `Switched to **${matchingAgent.name}** agent for this channel\nAll new sessions will use this agent.`,
|
|
335
335
|
})
|
|
336
336
|
}
|
|
337
337
|
} catch (error) {
|
|
@@ -11,9 +11,9 @@ import {
|
|
|
11
11
|
import crypto from 'node:crypto'
|
|
12
12
|
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
13
13
|
import { getOpencodeClientV2 } from '../opencode.js'
|
|
14
|
-
import { createLogger } from '../logger.js'
|
|
14
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
15
15
|
|
|
16
|
-
const logger = createLogger(
|
|
16
|
+
const logger = createLogger(LogPrefix.ASK_QUESTION)
|
|
17
17
|
|
|
18
18
|
// Schema matching the question tool input
|
|
19
19
|
export type AskUserQuestionInput = {
|
|
@@ -268,9 +268,9 @@ export function parseAskUserQuestionTool(part: {
|
|
|
268
268
|
|
|
269
269
|
/**
|
|
270
270
|
* Cancel a pending question for a thread (e.g., when user sends a new message).
|
|
271
|
-
* Sends
|
|
271
|
+
* Sends the user's message as the answer to OpenCode so the model sees their actual response.
|
|
272
272
|
*/
|
|
273
|
-
export async function cancelPendingQuestion(threadId: string): Promise<boolean> {
|
|
273
|
+
export async function cancelPendingQuestion(threadId: string, userMessage?: string): Promise<boolean> {
|
|
274
274
|
// Find pending question for this thread
|
|
275
275
|
let contextHash: string | undefined
|
|
276
276
|
let context: PendingQuestionContext | undefined
|
|
@@ -292,9 +292,10 @@ export async function cancelPendingQuestion(threadId: string): Promise<boolean>
|
|
|
292
292
|
throw new Error('OpenCode server not found for directory')
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
//
|
|
295
|
+
// Use user's message as answer if provided, otherwise mark as "Other"
|
|
296
|
+
const customAnswer = userMessage || 'Other'
|
|
296
297
|
const answers = context.questions.map((_, i) => {
|
|
297
|
-
return context.answers[i] || [
|
|
298
|
+
return context.answers[i] || [customAnswer]
|
|
298
299
|
})
|
|
299
300
|
|
|
300
301
|
await clientV2.question.reply({
|
|
@@ -302,9 +303,9 @@ export async function cancelPendingQuestion(threadId: string): Promise<boolean>
|
|
|
302
303
|
answers,
|
|
303
304
|
})
|
|
304
305
|
|
|
305
|
-
logger.log(`
|
|
306
|
+
logger.log(`Answered question ${context.requestId} with user message`)
|
|
306
307
|
} catch (error) {
|
|
307
|
-
logger.error('Failed to
|
|
308
|
+
logger.error('Failed to answer question:', error)
|
|
308
309
|
}
|
|
309
310
|
|
|
310
311
|
// Clean up regardless of whether the API call succeeded
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// /compact command - Trigger context compaction (summarization) for the current session.
|
|
2
|
+
|
|
3
|
+
import { ChannelType, type ThreadChannel } from 'discord.js'
|
|
4
|
+
import type { CommandContext } from './types.js'
|
|
5
|
+
import { getDatabase } from '../database.js'
|
|
6
|
+
import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js'
|
|
7
|
+
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
9
|
+
|
|
10
|
+
const logger = createLogger(LogPrefix.COMPACT)
|
|
11
|
+
|
|
12
|
+
export async function handleCompactCommand({ command }: CommandContext): Promise<void> {
|
|
13
|
+
const channel = command.channel
|
|
14
|
+
|
|
15
|
+
if (!channel) {
|
|
16
|
+
await command.reply({
|
|
17
|
+
content: 'This command can only be used in a channel',
|
|
18
|
+
ephemeral: true,
|
|
19
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
20
|
+
})
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isThread = [
|
|
25
|
+
ChannelType.PublicThread,
|
|
26
|
+
ChannelType.PrivateThread,
|
|
27
|
+
ChannelType.AnnouncementThread,
|
|
28
|
+
].includes(channel.type)
|
|
29
|
+
|
|
30
|
+
if (!isThread) {
|
|
31
|
+
await command.reply({
|
|
32
|
+
content: 'This command can only be used in a thread with an active session',
|
|
33
|
+
ephemeral: true,
|
|
34
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
35
|
+
})
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const textChannel = await resolveTextChannel(channel as ThreadChannel)
|
|
40
|
+
const { projectDirectory: directory } = getKimakiMetadata(textChannel)
|
|
41
|
+
|
|
42
|
+
if (!directory) {
|
|
43
|
+
await command.reply({
|
|
44
|
+
content: 'Could not determine project directory for this channel',
|
|
45
|
+
ephemeral: true,
|
|
46
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
47
|
+
})
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const row = getDatabase()
|
|
52
|
+
.prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
|
|
53
|
+
.get(channel.id) as { session_id: string } | undefined
|
|
54
|
+
|
|
55
|
+
if (!row?.session_id) {
|
|
56
|
+
await command.reply({
|
|
57
|
+
content: 'No active session in this thread',
|
|
58
|
+
ephemeral: true,
|
|
59
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
60
|
+
})
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const sessionId = row.session_id
|
|
65
|
+
|
|
66
|
+
// Ensure server is running for this directory
|
|
67
|
+
const getClient = await initializeOpencodeForDirectory(directory)
|
|
68
|
+
if (getClient instanceof Error) {
|
|
69
|
+
await command.reply({
|
|
70
|
+
content: `Failed to compact: ${getClient.message}`,
|
|
71
|
+
ephemeral: true,
|
|
72
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
73
|
+
})
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const clientV2 = getOpencodeClientV2(directory)
|
|
78
|
+
if (!clientV2) {
|
|
79
|
+
await command.reply({
|
|
80
|
+
content: 'Failed to get OpenCode client',
|
|
81
|
+
ephemeral: true,
|
|
82
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
83
|
+
})
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Defer reply since compaction may take a moment
|
|
88
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Get session messages to find the model from the last user message
|
|
92
|
+
const messagesResult = await clientV2.session.messages({
|
|
93
|
+
sessionID: sessionId,
|
|
94
|
+
directory,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
if (messagesResult.error || !messagesResult.data) {
|
|
98
|
+
logger.error('[COMPACT] Failed to get messages:', messagesResult.error)
|
|
99
|
+
await command.editReply({
|
|
100
|
+
content: 'Failed to compact: Could not retrieve session messages',
|
|
101
|
+
})
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Find the last user message to get the model
|
|
106
|
+
const lastUserMessage = [...messagesResult.data]
|
|
107
|
+
.reverse()
|
|
108
|
+
.find((msg) => msg.info.role === 'user')
|
|
109
|
+
|
|
110
|
+
if (!lastUserMessage || lastUserMessage.info.role !== 'user') {
|
|
111
|
+
await command.editReply({
|
|
112
|
+
content: 'Failed to compact: No user message found in session',
|
|
113
|
+
})
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { providerID, modelID } = lastUserMessage.info.model
|
|
118
|
+
|
|
119
|
+
const result = await clientV2.session.summarize({
|
|
120
|
+
sessionID: sessionId,
|
|
121
|
+
directory,
|
|
122
|
+
providerID,
|
|
123
|
+
modelID,
|
|
124
|
+
auto: false,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (result.error) {
|
|
128
|
+
logger.error('[COMPACT] Error:', result.error)
|
|
129
|
+
const errorMessage = 'data' in result.error && result.error.data
|
|
130
|
+
? (result.error.data as { message?: string }).message || 'Unknown error'
|
|
131
|
+
: 'Unknown error'
|
|
132
|
+
await command.editReply({
|
|
133
|
+
content: `Failed to compact: ${errorMessage}`,
|
|
134
|
+
})
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await command.editReply({
|
|
139
|
+
content: `📦 Session **compacted** successfully`,
|
|
140
|
+
})
|
|
141
|
+
logger.log(`Session ${sessionId} compacted by user`)
|
|
142
|
+
} catch (error) {
|
|
143
|
+
logger.error('[COMPACT] Error:', error)
|
|
144
|
+
await command.editReply({
|
|
145
|
+
content: `Failed to compact: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -8,9 +8,9 @@ import { getProjectsDir } from '../config.js'
|
|
|
8
8
|
import { createProjectChannels } from '../channel-management.js'
|
|
9
9
|
import { handleOpencodeSession } from '../session-handler.js'
|
|
10
10
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
11
|
-
import { createLogger } from '../logger.js'
|
|
11
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
12
12
|
|
|
13
|
-
const logger = createLogger(
|
|
13
|
+
const logger = createLogger(LogPrefix.CREATE_PROJECT)
|
|
14
14
|
|
|
15
15
|
export async function handleCreateNewProjectCommand({
|
|
16
16
|
command,
|
|
@@ -74,7 +74,7 @@ export async function handleCreateNewProjectCommand({
|
|
|
74
74
|
const textChannel = (await guild.channels.fetch(textChannelId)) as TextChannel
|
|
75
75
|
|
|
76
76
|
await command.editReply(
|
|
77
|
-
`✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\
|
|
77
|
+
`✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n_Starting session..._`,
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
const starterMessage = await textChannel.send({
|
package/src/commands/fork.ts
CHANGED
|
@@ -13,11 +13,11 @@ import { getDatabase } from '../database.js'
|
|
|
13
13
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
14
14
|
import { resolveTextChannel, getKimakiMetadata, sendThreadMessage } from '../discord-utils.js'
|
|
15
15
|
import { collectLastAssistantParts } from '../message-formatting.js'
|
|
16
|
-
import { createLogger } from '../logger.js'
|
|
16
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
17
17
|
import * as errore from 'errore'
|
|
18
18
|
|
|
19
|
-
const sessionLogger = createLogger(
|
|
20
|
-
const forkLogger = createLogger(
|
|
19
|
+
const sessionLogger = createLogger(LogPrefix.SESSION)
|
|
20
|
+
const forkLogger = createLogger(LogPrefix.FORK)
|
|
21
21
|
|
|
22
22
|
export async function handleForkCommand(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
23
23
|
const channel = interaction.channel
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
import { type ThreadChannel } from 'discord.js'
|
|
6
6
|
import type { CommandContext } from './types.js'
|
|
7
7
|
import { getThreadWorktree } from '../database.js'
|
|
8
|
-
import { createLogger } from '../logger.js'
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
9
9
|
import { execAsync } from '../worktree-utils.js'
|
|
10
10
|
|
|
11
|
-
const logger = createLogger(
|
|
11
|
+
const logger = createLogger(LogPrefix.WORKTREE)
|
|
12
12
|
|
|
13
13
|
/** Worktree thread title prefix - indicates unmerged worktree */
|
|
14
14
|
export const WORKTREE_PREFIX = '⬦ '
|
package/src/commands/model.ts
CHANGED
|
@@ -14,10 +14,10 @@ import { getDatabase, setChannelModel, setSessionModel, runModelMigrations } fro
|
|
|
14
14
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
15
15
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
|
|
16
16
|
import { abortAndRetrySession } from '../session-handler.js'
|
|
17
|
-
import { createLogger } from '../logger.js'
|
|
17
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
18
18
|
import * as errore from 'errore'
|
|
19
19
|
|
|
20
|
-
const modelLogger = createLogger(
|
|
20
|
+
const modelLogger = createLogger(LogPrefix.MODEL)
|
|
21
21
|
|
|
22
22
|
// Store context by hash to avoid customId length limits (Discord max: 100 chars)
|
|
23
23
|
const pendingModelContexts = new Map<
|
|
@@ -385,12 +385,12 @@ export async function handleModelSelectMenu(
|
|
|
385
385
|
|
|
386
386
|
if (retried) {
|
|
387
387
|
await interaction.editReply({
|
|
388
|
-
content: `Model changed for this session:\n**${context.providerName}** / **${selectedModelId}**\n
|
|
388
|
+
content: `Model changed for this session:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\n_Retrying current request with new model..._`,
|
|
389
389
|
components: [],
|
|
390
390
|
})
|
|
391
391
|
} else {
|
|
392
392
|
await interaction.editReply({
|
|
393
|
-
content: `Model preference set for this session:\n**${context.providerName}** / **${selectedModelId}**\n
|
|
393
|
+
content: `Model preference set for this session:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\``,
|
|
394
394
|
components: [],
|
|
395
395
|
})
|
|
396
396
|
}
|
|
@@ -400,7 +400,7 @@ export async function handleModelSelectMenu(
|
|
|
400
400
|
modelLogger.log(`Set model ${fullModelId} for channel ${context.channelId}`)
|
|
401
401
|
|
|
402
402
|
await interaction.editReply({
|
|
403
|
-
content: `Model preference set for this channel:\n**${context.providerName}** / **${selectedModelId}**\n
|
|
403
|
+
content: `Model preference set for this channel:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\nAll new sessions in this channel will use this model.`,
|
|
404
404
|
components: [],
|
|
405
405
|
})
|
|
406
406
|
}
|
|
@@ -12,9 +12,9 @@ import crypto from 'node:crypto'
|
|
|
12
12
|
import type { PermissionRequest } from '@opencode-ai/sdk/v2'
|
|
13
13
|
import { getOpencodeClientV2 } from '../opencode.js'
|
|
14
14
|
import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
15
|
-
import { createLogger } from '../logger.js'
|
|
15
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
16
16
|
|
|
17
|
-
const logger = createLogger(
|
|
17
|
+
const logger = createLogger(LogPrefix.PERMISSIONS)
|
|
18
18
|
|
|
19
19
|
type PendingPermissionContext = {
|
|
20
20
|
permission: PermissionRequest
|
package/src/commands/queue.ts
CHANGED
|
@@ -16,9 +16,9 @@ import {
|
|
|
16
16
|
getQueueLength,
|
|
17
17
|
clearQueue,
|
|
18
18
|
} from '../session-handler.js'
|
|
19
|
-
import { createLogger } from '../logger.js'
|
|
19
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
20
20
|
|
|
21
|
-
const logger = createLogger(
|
|
21
|
+
const logger = createLogger(LogPrefix.QUEUE)
|
|
22
22
|
|
|
23
23
|
export async function handleQueueCommand({ command }: CommandContext): Promise<void> {
|
|
24
24
|
const message = command.options.getString('message', true)
|
|
@@ -4,10 +4,10 @@ import path from 'node:path'
|
|
|
4
4
|
import * as errore from 'errore'
|
|
5
5
|
import type { CommandContext, AutocompleteContext } from './types.js'
|
|
6
6
|
import { getDatabase } from '../database.js'
|
|
7
|
-
import { createLogger } from '../logger.js'
|
|
7
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
8
8
|
import { abbreviatePath } from '../utils.js'
|
|
9
9
|
|
|
10
|
-
const logger = createLogger(
|
|
10
|
+
const logger = createLogger(LogPrefix.REMOVE_PROJECT)
|
|
11
11
|
|
|
12
12
|
export async function handleRemoveProjectCommand({ command, appId }: CommandContext): Promise<void> {
|
|
13
13
|
await command.deferReply({ ephemeral: false })
|
package/src/commands/resume.ts
CHANGED
|
@@ -12,10 +12,10 @@ import { getDatabase, getChannelDirectory } from '../database.js'
|
|
|
12
12
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
13
13
|
import { sendThreadMessage, resolveTextChannel } from '../discord-utils.js'
|
|
14
14
|
import { collectLastAssistantParts } from '../message-formatting.js'
|
|
15
|
-
import { createLogger } from '../logger.js'
|
|
15
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
16
16
|
import * as errore from 'errore'
|
|
17
17
|
|
|
18
|
-
const logger = createLogger(
|
|
18
|
+
const logger = createLogger(LogPrefix.RESUME)
|
|
19
19
|
|
|
20
20
|
export async function handleResumeCommand({ command, appId }: CommandContext): Promise<void> {
|
|
21
21
|
await command.deferReply({ ephemeral: false })
|
package/src/commands/session.ts
CHANGED
|
@@ -8,10 +8,10 @@ import { getDatabase, getChannelDirectory } from '../database.js'
|
|
|
8
8
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
9
9
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
10
10
|
import { handleOpencodeSession } from '../session-handler.js'
|
|
11
|
-
import { createLogger } from '../logger.js'
|
|
11
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
12
12
|
import * as errore from 'errore'
|
|
13
13
|
|
|
14
|
-
const logger = createLogger(
|
|
14
|
+
const logger = createLogger(LogPrefix.SESSION)
|
|
15
15
|
|
|
16
16
|
export async function handleSessionCommand({ command, appId }: CommandContext): Promise<void> {
|
|
17
17
|
await command.deferReply({ ephemeral: false })
|
package/src/commands/share.ts
CHANGED
|
@@ -5,10 +5,10 @@ import type { CommandContext } from './types.js'
|
|
|
5
5
|
import { getDatabase } from '../database.js'
|
|
6
6
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
7
7
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } 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
10
|
|
|
11
|
-
const logger = createLogger(
|
|
11
|
+
const logger = createLogger(LogPrefix.SHARE)
|
|
12
12
|
|
|
13
13
|
export async function handleShareCommand({ command }: CommandContext): Promise<void> {
|
|
14
14
|
const channel = command.channel
|
|
@@ -5,10 +5,10 @@ import type { CommandContext } from './types.js'
|
|
|
5
5
|
import { getDatabase } from '../database.js'
|
|
6
6
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
7
7
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } 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
10
|
|
|
11
|
-
const logger = createLogger(
|
|
11
|
+
const logger = createLogger(LogPrefix.UNDO_REDO)
|
|
12
12
|
|
|
13
13
|
export async function handleUndoCommand({ command }: CommandContext): Promise<void> {
|
|
14
14
|
const channel = command.channel
|
|
@@ -5,11 +5,11 @@ import type { CommandContext, CommandHandler } from './types.js'
|
|
|
5
5
|
import { ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
|
|
6
6
|
import { handleOpencodeSession } from '../session-handler.js'
|
|
7
7
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
8
|
-
import { createLogger } from '../logger.js'
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
9
9
|
import { getDatabase, getChannelDirectory } from '../database.js'
|
|
10
10
|
import fs from 'node:fs'
|
|
11
11
|
|
|
12
|
-
const userCommandLogger = createLogger(
|
|
12
|
+
const userCommandLogger = createLogger(LogPrefix.USER_CMD)
|
|
13
13
|
|
|
14
14
|
export const handleUserCommand: CommandHandler = async ({ command, appId }: CommandContext) => {
|
|
15
15
|
const discordCommandName = command.commandName
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { ChatInputCommandInteraction, ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
|
|
7
7
|
import { getChannelVerbosity, setChannelVerbosity, type VerbosityLevel } from '../database.js'
|
|
8
|
-
import { createLogger } from '../logger.js'
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
9
9
|
|
|
10
|
-
const verbosityLogger = createLogger(
|
|
10
|
+
const verbosityLogger = createLogger(LogPrefix.VERBOSITY)
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Handle the /verbosity slash command.
|
|
@@ -65,7 +65,7 @@ export async function handleVerbosityCommand({
|
|
|
65
65
|
: 'All output will be shown, including tool executions and status messages.'
|
|
66
66
|
|
|
67
67
|
await command.reply({
|
|
68
|
-
content: `Verbosity set to **${level}**.\n
|
|
68
|
+
content: `Verbosity set to **${level}**.\n${description}\nThis applies to all new sessions in this channel.`,
|
|
69
69
|
ephemeral: true,
|
|
70
70
|
})
|
|
71
71
|
}
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import { ChatInputCommandInteraction, ChannelType, type TextChannel } from 'discord.js'
|
|
6
6
|
import { getChannelWorktreesEnabled, setChannelWorktreesEnabled } from '../database.js'
|
|
7
7
|
import { getKimakiMetadata } from '../discord-utils.js'
|
|
8
|
-
import { createLogger } from '../logger.js'
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
9
9
|
|
|
10
|
-
const worktreeSettingsLogger = createLogger(
|
|
10
|
+
const worktreeSettingsLogger = createLogger(LogPrefix.WORKTREE)
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Handle the /enable-worktrees slash command.
|
package/src/commands/worktree.ts
CHANGED
|
@@ -14,12 +14,12 @@ import {
|
|
|
14
14
|
} from '../database.js'
|
|
15
15
|
import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js'
|
|
16
16
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
17
|
-
import { createLogger } from '../logger.js'
|
|
18
|
-
import { createWorktreeWithSubmodules } from '../worktree-utils.js'
|
|
17
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
18
|
+
import { createWorktreeWithSubmodules, captureGitDiff, type CapturedDiff } from '../worktree-utils.js'
|
|
19
19
|
import { WORKTREE_PREFIX } from './merge-worktree.js'
|
|
20
20
|
import * as errore from 'errore'
|
|
21
21
|
|
|
22
|
-
const logger = createLogger(
|
|
22
|
+
const logger = createLogger(LogPrefix.WORKTREE)
|
|
23
23
|
|
|
24
24
|
class WorktreeError extends Error {
|
|
25
25
|
constructor(message: string, options?: { cause?: unknown }) {
|
|
@@ -91,6 +91,7 @@ function getProjectDirectoryFromChannel(
|
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* Create worktree in background and update starter message when done.
|
|
94
|
+
* If diff is provided, it's applied during worktree creation (before submodule init).
|
|
94
95
|
*/
|
|
95
96
|
async function createWorktreeInBackground({
|
|
96
97
|
thread,
|
|
@@ -98,19 +99,22 @@ async function createWorktreeInBackground({
|
|
|
98
99
|
worktreeName,
|
|
99
100
|
projectDirectory,
|
|
100
101
|
clientV2,
|
|
102
|
+
diff,
|
|
101
103
|
}: {
|
|
102
104
|
thread: ThreadChannel
|
|
103
105
|
starterMessage: Message
|
|
104
106
|
worktreeName: string
|
|
105
107
|
projectDirectory: string
|
|
106
108
|
clientV2: ReturnType<typeof getOpencodeClientV2> & {}
|
|
109
|
+
diff?: CapturedDiff | null
|
|
107
110
|
}): Promise<void> {
|
|
108
|
-
// Create worktree using SDK v2
|
|
111
|
+
// Create worktree using SDK v2, apply diff, then init submodules
|
|
109
112
|
logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory}`)
|
|
110
113
|
const worktreeResult = await createWorktreeWithSubmodules({
|
|
111
114
|
clientV2,
|
|
112
115
|
directory: projectDirectory,
|
|
113
116
|
name: worktreeName,
|
|
117
|
+
diff,
|
|
114
118
|
})
|
|
115
119
|
|
|
116
120
|
if (worktreeResult instanceof Error) {
|
|
@@ -123,10 +127,12 @@ async function createWorktreeInBackground({
|
|
|
123
127
|
|
|
124
128
|
// Success - update database and edit starter message
|
|
125
129
|
setWorktreeReady({ threadId: thread.id, worktreeDirectory: worktreeResult.directory })
|
|
130
|
+
const diffStatus = diff ? (worktreeResult.diffApplied ? '\n✅ Changes applied' : '\n⚠️ Failed to apply changes') : ''
|
|
126
131
|
await starterMessage.edit(
|
|
127
132
|
`🌳 **Worktree: ${worktreeName}**\n` +
|
|
128
133
|
`📁 \`${worktreeResult.directory}\`\n` +
|
|
129
|
-
`🌿 Branch: \`${worktreeResult.branch}\``
|
|
134
|
+
`🌿 Branch: \`${worktreeResult.branch}\`` +
|
|
135
|
+
diffStatus
|
|
130
136
|
)
|
|
131
137
|
}
|
|
132
138
|
|
|
@@ -337,6 +343,11 @@ async function handleWorktreeInThread({
|
|
|
337
343
|
return
|
|
338
344
|
}
|
|
339
345
|
|
|
346
|
+
// Capture git diff from project directory before creating worktree.
|
|
347
|
+
// This allows transferring uncommitted changes to the new worktree.
|
|
348
|
+
const diff = await captureGitDiff(projectDirectory)
|
|
349
|
+
const hasDiff = diff && (diff.staged || diff.unstaged)
|
|
350
|
+
|
|
340
351
|
// Store pending worktree in database for this existing thread
|
|
341
352
|
createPendingWorktree({
|
|
342
353
|
threadId: thread.id,
|
|
@@ -345,20 +356,22 @@ async function handleWorktreeInThread({
|
|
|
345
356
|
})
|
|
346
357
|
|
|
347
358
|
// Send status message in thread
|
|
359
|
+
const diffNote = hasDiff ? '\n📋 Will transfer uncommitted changes' : ''
|
|
348
360
|
const statusMessage = await thread.send({
|
|
349
|
-
content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up
|
|
361
|
+
content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...${diffNote}`,
|
|
350
362
|
flags: SILENT_MESSAGE_FLAGS,
|
|
351
363
|
})
|
|
352
364
|
|
|
353
365
|
await command.editReply(`Creating worktree \`${worktreeName}\` for this thread...`)
|
|
354
366
|
|
|
355
|
-
// Create worktree in background
|
|
367
|
+
// Create worktree in background, passing diff to apply after creation
|
|
356
368
|
createWorktreeInBackground({
|
|
357
369
|
thread,
|
|
358
370
|
starterMessage: statusMessage,
|
|
359
371
|
worktreeName,
|
|
360
372
|
projectDirectory,
|
|
361
373
|
clientV2,
|
|
374
|
+
diff,
|
|
362
375
|
}).catch((e) => {
|
|
363
376
|
logger.error('[NEW-WORKTREE] Background error:', e)
|
|
364
377
|
})
|