kimaki 0.4.46 → 0.4.48
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 +69 -21
- package/dist/commands/abort.js +4 -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 +60 -30
- package/dist/commands/fork.js +3 -3
- package/dist/commands/merge-worktree.js +23 -10
- package/dist/commands/model.js +5 -5
- package/dist/commands/permissions.js +5 -3
- 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 +6 -3
- 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 +5 -5
- package/dist/commands/worktree-settings.js +2 -2
- package/dist/commands/worktree.js +18 -8
- package/dist/config.js +7 -0
- package/dist/database.js +10 -7
- package/dist/discord-bot.js +30 -12
- 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 +91 -6
- package/dist/openai-realtime.js +2 -2
- package/dist/opencode.js +19 -25
- package/dist/session-handler.js +89 -29
- package/dist/system-message.js +11 -9
- package/dist/tools.js +3 -2
- package/dist/utils.js +1 -0
- 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 +3 -3
- package/src/cli.ts +108 -21
- package/src/commands/abort.ts +4 -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 +87 -36
- package/src/commands/fork.ts +3 -3
- package/src/commands/merge-worktree.ts +47 -10
- package/src/commands/model.ts +5 -5
- package/src/commands/permissions.ts +6 -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 +6 -3
- 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 +5 -5
- package/src/commands/worktree-settings.ts +2 -2
- package/src/commands/worktree.ts +20 -7
- package/src/config.ts +14 -0
- package/src/database.ts +13 -7
- package/src/discord-bot.ts +45 -12
- 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 +100 -6
- package/src/openai-realtime.ts +2 -2
- package/src/opencode.ts +19 -26
- package/src/session-handler.ts +102 -29
- package/src/system-message.ts +11 -9
- package/src/tools.ts +3 -2
- package/src/utils.ts +1 -0
- 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
|
@@ -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
|
+
}
|
|
@@ -1,16 +1,79 @@
|
|
|
1
1
|
// /create-new-project command - Create a new project folder, initialize git, and start a session.
|
|
2
|
+
// Also exports createNewProject() for reuse during onboarding (welcome channel creation).
|
|
2
3
|
|
|
3
|
-
import { ChannelType, type TextChannel } from 'discord.js'
|
|
4
|
+
import { ChannelType, type Guild, type TextChannel } from 'discord.js'
|
|
4
5
|
import fs from 'node:fs'
|
|
5
6
|
import path from 'node:path'
|
|
7
|
+
import { execSync } from 'node:child_process'
|
|
6
8
|
import type { CommandContext } from './types.js'
|
|
7
9
|
import { getProjectsDir } from '../config.js'
|
|
8
10
|
import { createProjectChannels } from '../channel-management.js'
|
|
9
11
|
import { handleOpencodeSession } from '../session-handler.js'
|
|
10
12
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
11
|
-
import { createLogger } from '../logger.js'
|
|
13
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
14
|
+
|
|
15
|
+
const logger = createLogger(LogPrefix.CREATE_PROJECT)
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Core project creation logic: creates directory, inits git, creates Discord channels.
|
|
19
|
+
* Reused by the slash command handler and by onboarding (welcome channel).
|
|
20
|
+
* Returns null if the project directory already exists.
|
|
21
|
+
*/
|
|
22
|
+
export async function createNewProject({
|
|
23
|
+
guild,
|
|
24
|
+
projectName,
|
|
25
|
+
appId,
|
|
26
|
+
botName,
|
|
27
|
+
}: {
|
|
28
|
+
guild: Guild
|
|
29
|
+
projectName: string
|
|
30
|
+
appId: string
|
|
31
|
+
botName?: string
|
|
32
|
+
}): Promise<{
|
|
33
|
+
textChannelId: string
|
|
34
|
+
voiceChannelId: string
|
|
35
|
+
channelName: string
|
|
36
|
+
projectDirectory: string
|
|
37
|
+
sanitizedName: string
|
|
38
|
+
} | null> {
|
|
39
|
+
const sanitizedName = projectName
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
42
|
+
.replace(/-+/g, '-')
|
|
43
|
+
.replace(/^-|-$/g, '')
|
|
44
|
+
.slice(0, 100)
|
|
45
|
+
|
|
46
|
+
if (!sanitizedName) {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
12
49
|
|
|
13
|
-
const
|
|
50
|
+
const projectsDir = getProjectsDir()
|
|
51
|
+
const projectDirectory = path.join(projectsDir, sanitizedName)
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(projectsDir)) {
|
|
54
|
+
fs.mkdirSync(projectsDir, { recursive: true })
|
|
55
|
+
logger.log(`Created projects directory: ${projectsDir}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (fs.existsSync(projectDirectory)) {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fs.mkdirSync(projectDirectory, { recursive: true })
|
|
63
|
+
logger.log(`Created project directory: ${projectDirectory}`)
|
|
64
|
+
|
|
65
|
+
execSync('git init', { cwd: projectDirectory, stdio: 'pipe' })
|
|
66
|
+
logger.log(`Initialized git in: ${projectDirectory}`)
|
|
67
|
+
|
|
68
|
+
const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
|
|
69
|
+
guild,
|
|
70
|
+
projectDirectory,
|
|
71
|
+
appId,
|
|
72
|
+
botName,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return { textChannelId, voiceChannelId, channelName, projectDirectory, sanitizedName }
|
|
76
|
+
}
|
|
14
77
|
|
|
15
78
|
export async function handleCreateNewProjectCommand({
|
|
16
79
|
command,
|
|
@@ -32,49 +95,37 @@ export async function handleCreateNewProjectCommand({
|
|
|
32
95
|
return
|
|
33
96
|
}
|
|
34
97
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!sanitizedName) {
|
|
43
|
-
await command.editReply('Invalid project name')
|
|
44
|
-
return
|
|
45
|
-
}
|
|
98
|
+
try {
|
|
99
|
+
const result = await createNewProject({
|
|
100
|
+
guild,
|
|
101
|
+
projectName,
|
|
102
|
+
appId,
|
|
103
|
+
botName: command.client.user?.username,
|
|
104
|
+
})
|
|
46
105
|
|
|
47
|
-
|
|
48
|
-
|
|
106
|
+
if (!result) {
|
|
107
|
+
const sanitizedName = projectName
|
|
108
|
+
.toLowerCase()
|
|
109
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
110
|
+
.replace(/-+/g, '-')
|
|
111
|
+
.replace(/^-|-$/g, '')
|
|
112
|
+
.slice(0, 100)
|
|
49
113
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
114
|
+
if (!sanitizedName) {
|
|
115
|
+
await command.editReply('Invalid project name')
|
|
116
|
+
return
|
|
117
|
+
}
|
|
55
118
|
|
|
56
|
-
|
|
119
|
+
const projectDirectory = path.join(getProjectsDir(), sanitizedName)
|
|
57
120
|
await command.editReply(`Project directory already exists: ${projectDirectory}`)
|
|
58
121
|
return
|
|
59
122
|
}
|
|
60
123
|
|
|
61
|
-
|
|
62
|
-
logger.log(`Created project directory: ${projectDirectory}`)
|
|
63
|
-
|
|
64
|
-
const { execSync } = await import('node:child_process')
|
|
65
|
-
execSync('git init', { cwd: projectDirectory, stdio: 'pipe' })
|
|
66
|
-
logger.log(`Initialized git in: ${projectDirectory}`)
|
|
67
|
-
|
|
68
|
-
const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
|
|
69
|
-
guild,
|
|
70
|
-
projectDirectory,
|
|
71
|
-
appId,
|
|
72
|
-
})
|
|
73
|
-
|
|
124
|
+
const { textChannelId, voiceChannelId, channelName, projectDirectory, sanitizedName } = result
|
|
74
125
|
const textChannel = (await guild.channels.fetch(textChannelId)) as TextChannel
|
|
75
126
|
|
|
76
127
|
await command.editReply(
|
|
77
|
-
`✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\
|
|
128
|
+
`✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n_Starting session..._`,
|
|
78
129
|
)
|
|
79
130
|
|
|
80
131
|
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 = '⬦ '
|
|
@@ -47,7 +47,11 @@ async function isDetachedHead(worktreeDir: string): Promise<boolean> {
|
|
|
47
47
|
try {
|
|
48
48
|
await execAsync(`git -C "${worktreeDir}" symbolic-ref HEAD`)
|
|
49
49
|
return false
|
|
50
|
-
} catch {
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.debug(
|
|
52
|
+
`Failed to resolve HEAD for ${worktreeDir}:`,
|
|
53
|
+
error instanceof Error ? error.message : String(error),
|
|
54
|
+
)
|
|
51
55
|
return true
|
|
52
56
|
}
|
|
53
57
|
}
|
|
@@ -59,7 +63,11 @@ async function getCurrentBranch(worktreeDir: string): Promise<string | null> {
|
|
|
59
63
|
try {
|
|
60
64
|
const { stdout } = await execAsync(`git -C "${worktreeDir}" symbolic-ref --short HEAD`)
|
|
61
65
|
return stdout.trim() || null
|
|
62
|
-
} catch {
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger.debug(
|
|
68
|
+
`Failed to get current branch for ${worktreeDir}:`,
|
|
69
|
+
error instanceof Error ? error.message : String(error),
|
|
70
|
+
)
|
|
63
71
|
return null
|
|
64
72
|
}
|
|
65
73
|
}
|
|
@@ -113,7 +121,11 @@ export async function handleMergeWorktreeCommand({ command, appId }: CommandCont
|
|
|
113
121
|
`git -C "${mainRepoDir}" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@'`,
|
|
114
122
|
)
|
|
115
123
|
defaultBranch = stdout.trim() || 'main'
|
|
116
|
-
} catch {
|
|
124
|
+
} catch (error) {
|
|
125
|
+
logger.warn(
|
|
126
|
+
`Failed to detect default branch for ${mainRepoDir}, falling back to main:`,
|
|
127
|
+
error instanceof Error ? error.message : String(error),
|
|
128
|
+
)
|
|
117
129
|
defaultBranch = 'main'
|
|
118
130
|
}
|
|
119
131
|
|
|
@@ -141,11 +153,26 @@ export async function handleMergeWorktreeCommand({ command, appId }: CommandCont
|
|
|
141
153
|
await execAsync(`git -C "${worktreeDir}" merge ${defaultBranch} --no-edit`)
|
|
142
154
|
} catch (e) {
|
|
143
155
|
// If merge fails (conflicts), abort and report
|
|
144
|
-
await execAsync(`git -C "${worktreeDir}" merge --abort`).catch(() => {
|
|
156
|
+
await execAsync(`git -C "${worktreeDir}" merge --abort`).catch((error) => {
|
|
157
|
+
logger.warn(
|
|
158
|
+
`Failed to abort merge in ${worktreeDir}:`,
|
|
159
|
+
error instanceof Error ? error.message : String(error),
|
|
160
|
+
)
|
|
161
|
+
})
|
|
145
162
|
// Clean up temp branch if we created one
|
|
146
163
|
if (tempBranch) {
|
|
147
|
-
await execAsync(`git -C "${worktreeDir}" checkout --detach`).catch(() => {
|
|
148
|
-
|
|
164
|
+
await execAsync(`git -C "${worktreeDir}" checkout --detach`).catch((error) => {
|
|
165
|
+
logger.warn(
|
|
166
|
+
`Failed to detach HEAD after merge conflict in ${worktreeDir}:`,
|
|
167
|
+
error instanceof Error ? error.message : String(error),
|
|
168
|
+
)
|
|
169
|
+
})
|
|
170
|
+
await execAsync(`git -C "${worktreeDir}" branch -D ${tempBranch}`).catch((error) => {
|
|
171
|
+
logger.warn(
|
|
172
|
+
`Failed to delete temp branch ${tempBranch} in ${worktreeDir}:`,
|
|
173
|
+
error instanceof Error ? error.message : String(error),
|
|
174
|
+
)
|
|
175
|
+
})
|
|
149
176
|
}
|
|
150
177
|
throw new Error(`Merge conflict - resolve manually in worktree then retry`)
|
|
151
178
|
}
|
|
@@ -162,11 +189,21 @@ export async function handleMergeWorktreeCommand({ command, appId }: CommandCont
|
|
|
162
189
|
|
|
163
190
|
// 7. Delete the merged branch (temp or original)
|
|
164
191
|
logger.log(`Deleting merged branch ${branchToMerge}`)
|
|
165
|
-
await execAsync(`git -C "${worktreeDir}" branch -D ${branchToMerge}`).catch(() => {
|
|
192
|
+
await execAsync(`git -C "${worktreeDir}" branch -D ${branchToMerge}`).catch((error) => {
|
|
193
|
+
logger.warn(
|
|
194
|
+
`Failed to delete merged branch ${branchToMerge} in ${worktreeDir}:`,
|
|
195
|
+
error instanceof Error ? error.message : String(error),
|
|
196
|
+
)
|
|
197
|
+
})
|
|
166
198
|
|
|
167
199
|
// Also delete the original worktree branch if different from what we merged
|
|
168
200
|
if (!isDetached && branchToMerge !== worktreeInfo.worktree_name) {
|
|
169
|
-
await execAsync(`git -C "${worktreeDir}" branch -D ${worktreeInfo.worktree_name}`).catch(() => {
|
|
201
|
+
await execAsync(`git -C "${worktreeDir}" branch -D ${worktreeInfo.worktree_name}`).catch((error) => {
|
|
202
|
+
logger.warn(
|
|
203
|
+
`Failed to delete worktree branch ${worktreeInfo.worktree_name} in ${worktreeDir}:`,
|
|
204
|
+
error instanceof Error ? error.message : String(error),
|
|
205
|
+
)
|
|
206
|
+
})
|
|
170
207
|
}
|
|
171
208
|
|
|
172
209
|
// 8. Remove worktree prefix from thread title (fire and forget with timeout)
|
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
|
|
@@ -35,10 +35,12 @@ export async function showPermissionDropdown({
|
|
|
35
35
|
thread,
|
|
36
36
|
permission,
|
|
37
37
|
directory,
|
|
38
|
+
subtaskLabel,
|
|
38
39
|
}: {
|
|
39
40
|
thread: ThreadChannel
|
|
40
41
|
permission: PermissionRequest
|
|
41
42
|
directory: string
|
|
43
|
+
subtaskLabel?: string
|
|
42
44
|
}): Promise<{ messageId: string; contextHash: string }> {
|
|
43
45
|
const contextHash = crypto.randomBytes(8).toString('hex')
|
|
44
46
|
|
|
@@ -80,9 +82,11 @@ export async function showPermissionDropdown({
|
|
|
80
82
|
|
|
81
83
|
const actionRow = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
|
|
82
84
|
|
|
85
|
+
const subtaskLine = subtaskLabel ? `**From:** \`${subtaskLabel}\`\n` : ''
|
|
83
86
|
const permissionMessage = await thread.send({
|
|
84
87
|
content:
|
|
85
88
|
`⚠️ **Permission Required**\n\n` +
|
|
89
|
+
subtaskLine +
|
|
86
90
|
`**Type:** \`${permission.permission}\`\n` +
|
|
87
91
|
(patternStr ? `**Pattern:** \`${patternStr}\`` : ''),
|
|
88
92
|
components: [actionRow],
|
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 })
|
|
@@ -133,7 +133,10 @@ async function handleAgentAutocomplete({ interaction, appId }: AutocompleteConte
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
const agents = agentsResponse.data
|
|
136
|
-
.filter((a) =>
|
|
136
|
+
.filter((a) => {
|
|
137
|
+
const hidden = (a as { hidden?: boolean }).hidden
|
|
138
|
+
return (a.mode === 'primary' || a.mode === 'all') && !hidden
|
|
139
|
+
})
|
|
137
140
|
.filter((a) => a.name.toLowerCase().includes(focusedValue.toLowerCase()))
|
|
138
141
|
.slice(0, 25)
|
|
139
142
|
|
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,13 +5,13 @@
|
|
|
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.
|
|
14
|
-
* Sets output verbosity for the channel (applies
|
|
14
|
+
* Sets output verbosity for the channel (applies immediately, even mid-session).
|
|
15
15
|
*/
|
|
16
16
|
export async function handleVerbosityCommand({
|
|
17
17
|
command,
|
|
@@ -51,7 +51,7 @@ export async function handleVerbosityCommand({
|
|
|
51
51
|
|
|
52
52
|
if (currentLevel === level) {
|
|
53
53
|
await command.reply({
|
|
54
|
-
content: `Verbosity is already set to **${level}
|
|
54
|
+
content: `Verbosity is already set to **${level}** for this channel.`,
|
|
55
55
|
ephemeral: true,
|
|
56
56
|
})
|
|
57
57
|
return
|
|
@@ -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}
|
|
68
|
+
content: `Verbosity set to **${level}** for this channel.\n${description}\nThis is a per-channel setting and applies immediately, including any active sessions.`,
|
|
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.
|