kimaki 0.4.47 → 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 +42 -19
- package/dist/commands/abort.js +2 -0
- package/dist/commands/create-new-project.js +57 -27
- package/dist/commands/merge-worktree.js +21 -8
- package/dist/commands/permissions.js +3 -1
- package/dist/commands/session.js +4 -1
- package/dist/commands/verbosity.js +3 -3
- package/dist/config.js +7 -0
- package/dist/database.js +8 -5
- package/dist/discord-bot.js +27 -9
- package/dist/message-formatting.js +74 -52
- package/dist/opencode.js +17 -23
- package/dist/session-handler.js +42 -21
- package/dist/system-message.js +11 -9
- package/dist/tools.js +1 -0
- package/dist/utils.js +1 -0
- package/package.json +3 -3
- package/src/cli.ts +80 -19
- package/src/commands/abort.ts +2 -0
- package/src/commands/create-new-project.ts +84 -33
- package/src/commands/merge-worktree.ts +45 -8
- package/src/commands/permissions.ts +4 -0
- package/src/commands/session.ts +4 -1
- package/src/commands/verbosity.ts +3 -3
- package/src/config.ts +14 -0
- package/src/database.ts +11 -5
- package/src/discord-bot.ts +42 -9
- package/src/message-formatting.ts +81 -63
- package/src/opencode.ts +17 -24
- package/src/session-handler.ts +46 -21
- package/src/system-message.ts +11 -9
- package/src/tools.ts +1 -0
- package/src/utils.ts +1 -0
package/src/cli.ts
CHANGED
|
@@ -47,7 +47,7 @@ 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
|
-
import { setDataDir, getDataDir, getLockPort } from './config.js'
|
|
50
|
+
import { setDataDir, getDataDir, getLockPort, setDefaultVerbosity } from './config.js'
|
|
51
51
|
import { sanitizeAgentName } from './commands/agent.js'
|
|
52
52
|
|
|
53
53
|
const cliLogger = createLogger(LogPrefix.CLI)
|
|
@@ -145,7 +145,11 @@ async function checkSingleInstance(): Promise<void> {
|
|
|
145
145
|
setTimeout(resolve, 500)
|
|
146
146
|
})
|
|
147
147
|
}
|
|
148
|
-
} catch {
|
|
148
|
+
} catch (error) {
|
|
149
|
+
cliLogger.debug(
|
|
150
|
+
'Lock port check failed:',
|
|
151
|
+
error instanceof Error ? error.message : String(error),
|
|
152
|
+
)
|
|
149
153
|
cliLogger.debug('No other kimaki instance detected on lock port')
|
|
150
154
|
}
|
|
151
155
|
}
|
|
@@ -551,11 +555,23 @@ async function backgroundInit({
|
|
|
551
555
|
getClient()
|
|
552
556
|
.command.list({ query: { directory: currentDir } })
|
|
553
557
|
.then((r) => r.data || [])
|
|
554
|
-
.catch(() =>
|
|
558
|
+
.catch((error) => {
|
|
559
|
+
cliLogger.warn(
|
|
560
|
+
'Failed to load user commands during background init:',
|
|
561
|
+
error instanceof Error ? error.message : String(error),
|
|
562
|
+
)
|
|
563
|
+
return []
|
|
564
|
+
}),
|
|
555
565
|
getClient()
|
|
556
566
|
.app.agents({ query: { directory: currentDir } })
|
|
557
567
|
.then((r) => r.data || [])
|
|
558
|
-
.catch(() =>
|
|
568
|
+
.catch((error) => {
|
|
569
|
+
cliLogger.warn(
|
|
570
|
+
'Failed to load agents during background init:',
|
|
571
|
+
error instanceof Error ? error.message : String(error),
|
|
572
|
+
)
|
|
573
|
+
return []
|
|
574
|
+
}),
|
|
559
575
|
])
|
|
560
576
|
|
|
561
577
|
await registerCommands({ token, appId, userCommands, agents })
|
|
@@ -613,7 +629,11 @@ async function run({ restart, addChannels, useWorktrees }: CliOptions) {
|
|
|
613
629
|
try {
|
|
614
630
|
fs.accessSync(p, fs.constants.F_OK)
|
|
615
631
|
return true
|
|
616
|
-
} catch {
|
|
632
|
+
} catch (error) {
|
|
633
|
+
cliLogger.debug(
|
|
634
|
+
`OpenCode path not found at ${p}:`,
|
|
635
|
+
error instanceof Error ? error.message : String(error),
|
|
636
|
+
)
|
|
617
637
|
return false
|
|
618
638
|
}
|
|
619
639
|
})
|
|
@@ -914,11 +934,23 @@ async function run({ restart, addChannels, useWorktrees }: CliOptions) {
|
|
|
914
934
|
getClient()
|
|
915
935
|
.command.list({ query: { directory: currentDir } })
|
|
916
936
|
.then((r) => r.data || [])
|
|
917
|
-
.catch(() =>
|
|
937
|
+
.catch((error) => {
|
|
938
|
+
cliLogger.warn(
|
|
939
|
+
'Failed to load user commands during setup:',
|
|
940
|
+
error instanceof Error ? error.message : String(error),
|
|
941
|
+
)
|
|
942
|
+
return []
|
|
943
|
+
}),
|
|
918
944
|
getClient()
|
|
919
945
|
.app.agents({ query: { directory: currentDir } })
|
|
920
946
|
.then((r) => r.data || [])
|
|
921
|
-
.catch(() =>
|
|
947
|
+
.catch((error) => {
|
|
948
|
+
cliLogger.warn(
|
|
949
|
+
'Failed to load agents during setup:',
|
|
950
|
+
error instanceof Error ? error.message : String(error),
|
|
951
|
+
)
|
|
952
|
+
return []
|
|
953
|
+
}),
|
|
922
954
|
])
|
|
923
955
|
|
|
924
956
|
s.stop(`Found ${projects.length} OpenCode project(s)`)
|
|
@@ -1066,6 +1098,7 @@ cli
|
|
|
1066
1098
|
.option('--data-dir <path>', 'Data directory for config and database (default: ~/.kimaki)')
|
|
1067
1099
|
.option('--install-url', 'Print the bot install URL and exit')
|
|
1068
1100
|
.option('--use-worktrees', 'Create git worktrees for all new sessions started from channel messages')
|
|
1101
|
+
.option('--verbosity <level>', 'Default verbosity for all channels (tools-and-text or text-only)')
|
|
1069
1102
|
.action(
|
|
1070
1103
|
async (options: {
|
|
1071
1104
|
restart?: boolean
|
|
@@ -1073,6 +1106,7 @@ cli
|
|
|
1073
1106
|
dataDir?: string
|
|
1074
1107
|
installUrl?: boolean
|
|
1075
1108
|
useWorktrees?: boolean
|
|
1109
|
+
verbosity?: string
|
|
1076
1110
|
}) => {
|
|
1077
1111
|
try {
|
|
1078
1112
|
// Set data directory early, before any database access
|
|
@@ -1081,6 +1115,15 @@ cli
|
|
|
1081
1115
|
cliLogger.log(`Using data directory: ${getDataDir()}`)
|
|
1082
1116
|
}
|
|
1083
1117
|
|
|
1118
|
+
if (options.verbosity) {
|
|
1119
|
+
if (options.verbosity !== 'tools-and-text' && options.verbosity !== 'text-only') {
|
|
1120
|
+
cliLogger.error(`Invalid verbosity level: ${options.verbosity}. Use "tools-and-text" or "text-only".`)
|
|
1121
|
+
process.exit(EXIT_NO_RESTART)
|
|
1122
|
+
}
|
|
1123
|
+
setDefaultVerbosity(options.verbosity)
|
|
1124
|
+
cliLogger.log(`Default verbosity: ${options.verbosity}`)
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1084
1127
|
if (options.installUrl) {
|
|
1085
1128
|
const db = getDatabase()
|
|
1086
1129
|
const existingBot = db
|
|
@@ -1240,8 +1283,11 @@ cli
|
|
|
1240
1283
|
.prepare('SELECT app_id FROM bot_tokens ORDER BY created_at DESC LIMIT 1')
|
|
1241
1284
|
.get() as { app_id: string } | undefined
|
|
1242
1285
|
appId = botRow?.app_id
|
|
1243
|
-
} catch {
|
|
1244
|
-
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
cliLogger.debug(
|
|
1288
|
+
'Database lookup failed while resolving app ID:',
|
|
1289
|
+
error instanceof Error ? error.message : String(error),
|
|
1290
|
+
)
|
|
1245
1291
|
}
|
|
1246
1292
|
}
|
|
1247
1293
|
} else {
|
|
@@ -1356,8 +1402,11 @@ cli
|
|
|
1356
1402
|
if (ch && 'guild' in ch && ch.guild) {
|
|
1357
1403
|
return ch.guild
|
|
1358
1404
|
}
|
|
1359
|
-
} catch {
|
|
1360
|
-
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
cliLogger.debug(
|
|
1407
|
+
'Failed to fetch existing channel while selecting guild:',
|
|
1408
|
+
error instanceof Error ? error.message : String(error),
|
|
1409
|
+
)
|
|
1361
1410
|
}
|
|
1362
1411
|
}
|
|
1363
1412
|
// Fall back to first guild the bot is in
|
|
@@ -1597,8 +1646,11 @@ cli
|
|
|
1597
1646
|
.prepare('SELECT app_id FROM bot_tokens ORDER BY created_at DESC LIMIT 1')
|
|
1598
1647
|
.get() as { app_id: string } | undefined
|
|
1599
1648
|
appId = botRow?.app_id
|
|
1600
|
-
} catch {
|
|
1601
|
-
|
|
1649
|
+
} catch (error) {
|
|
1650
|
+
cliLogger.debug(
|
|
1651
|
+
'Database lookup failed while resolving app ID:',
|
|
1652
|
+
error instanceof Error ? error.message : String(error),
|
|
1653
|
+
)
|
|
1602
1654
|
}
|
|
1603
1655
|
}
|
|
1604
1656
|
} else {
|
|
@@ -1677,8 +1729,11 @@ cli
|
|
|
1677
1729
|
} else {
|
|
1678
1730
|
throw new Error('Channel has no guild')
|
|
1679
1731
|
}
|
|
1680
|
-
} catch {
|
|
1681
|
-
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
cliLogger.debug(
|
|
1734
|
+
'Failed to fetch existing channel while selecting guild:',
|
|
1735
|
+
error instanceof Error ? error.message : String(error),
|
|
1736
|
+
)
|
|
1682
1737
|
const firstGuild = client.guilds.cache.first()
|
|
1683
1738
|
if (!firstGuild) {
|
|
1684
1739
|
s.stop('No guild found')
|
|
@@ -1722,12 +1777,18 @@ cli
|
|
|
1722
1777
|
client.destroy()
|
|
1723
1778
|
process.exit(0)
|
|
1724
1779
|
}
|
|
1725
|
-
} catch {
|
|
1726
|
-
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
cliLogger.debug(
|
|
1782
|
+
`Failed to fetch channel ${existingChannel.channel_id} while checking existing channels:`,
|
|
1783
|
+
error instanceof Error ? error.message : String(error),
|
|
1784
|
+
)
|
|
1727
1785
|
}
|
|
1728
1786
|
}
|
|
1729
|
-
} catch {
|
|
1730
|
-
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
cliLogger.debug(
|
|
1789
|
+
'Database lookup failed while checking existing channels:',
|
|
1790
|
+
error instanceof Error ? error.message : String(error),
|
|
1791
|
+
)
|
|
1731
1792
|
}
|
|
1732
1793
|
|
|
1733
1794
|
s.message(`Creating channels in ${guild.name}...`)
|
package/src/commands/abort.ts
CHANGED
|
@@ -67,6 +67,7 @@ export async function handleAbortCommand({ command }: CommandContext): Promise<v
|
|
|
67
67
|
|
|
68
68
|
const existingController = abortControllers.get(sessionId)
|
|
69
69
|
if (existingController) {
|
|
70
|
+
logger.log(`[ABORT] reason=user-requested sessionId=${sessionId} channelId=${channel.id} - user ran /abort command`)
|
|
70
71
|
existingController.abort(new Error('User requested abort'))
|
|
71
72
|
abortControllers.delete(sessionId)
|
|
72
73
|
}
|
|
@@ -82,6 +83,7 @@ export async function handleAbortCommand({ command }: CommandContext): Promise<v
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
try {
|
|
86
|
+
logger.log(`[ABORT-API] reason=user-requested sessionId=${sessionId} channelId=${channel.id} - sending API abort from /abort command`)
|
|
85
87
|
await getClient().session.abort({
|
|
86
88
|
path: { id: sessionId },
|
|
87
89
|
})
|
|
@@ -1,8 +1,10 @@
|
|
|
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'
|
|
@@ -12,6 +14,67 @@ import { createLogger, LogPrefix } from '../logger.js'
|
|
|
12
14
|
|
|
13
15
|
const logger = createLogger(LogPrefix.CREATE_PROJECT)
|
|
14
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
|
+
}
|
|
49
|
+
|
|
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
|
+
}
|
|
77
|
+
|
|
15
78
|
export async function handleCreateNewProjectCommand({
|
|
16
79
|
command,
|
|
17
80
|
appId,
|
|
@@ -32,45 +95,33 @@ 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(
|
|
@@ -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)
|
|
@@ -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/session.ts
CHANGED
|
@@ -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
|
|
|
@@ -11,7 +11,7 @@ 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
|
}
|
package/src/config.ts
CHANGED
|
@@ -44,6 +44,20 @@ export function getProjectsDir(): string {
|
|
|
44
44
|
return path.join(getDataDir(), 'projects')
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Default verbosity for channels that haven't set a per-channel override.
|
|
48
|
+
// Set via --verbosity CLI flag at startup.
|
|
49
|
+
import type { VerbosityLevel } from './database.js'
|
|
50
|
+
|
|
51
|
+
let defaultVerbosity: VerbosityLevel = 'tools-and-text'
|
|
52
|
+
|
|
53
|
+
export function getDefaultVerbosity(): VerbosityLevel {
|
|
54
|
+
return defaultVerbosity
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function setDefaultVerbosity(level: VerbosityLevel): void {
|
|
58
|
+
defaultVerbosity = level
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
const DEFAULT_LOCK_PORT = 29988
|
|
48
62
|
|
|
49
63
|
/**
|
package/src/database.ts
CHANGED
|
@@ -7,7 +7,7 @@ import fs from 'node:fs'
|
|
|
7
7
|
import path from 'node:path'
|
|
8
8
|
import * as errore from 'errore'
|
|
9
9
|
import { createLogger, LogPrefix } from './logger.js'
|
|
10
|
-
import { getDataDir } from './config.js'
|
|
10
|
+
import { getDataDir, getDefaultVerbosity } from './config.js'
|
|
11
11
|
|
|
12
12
|
const dbLogger = createLogger(LogPrefix.DB)
|
|
13
13
|
|
|
@@ -69,8 +69,11 @@ export function getDatabase(): Database.Database {
|
|
|
69
69
|
// Migration: add app_id column to channel_directories for multi-bot support
|
|
70
70
|
try {
|
|
71
71
|
db.exec(`ALTER TABLE channel_directories ADD COLUMN app_id TEXT`)
|
|
72
|
-
} catch {
|
|
73
|
-
|
|
72
|
+
} catch (error) {
|
|
73
|
+
dbLogger.debug(
|
|
74
|
+
'Failed to add app_id column to channel_directories (likely exists):',
|
|
75
|
+
error instanceof Error ? error.message : String(error),
|
|
76
|
+
)
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
// Table for threads that should auto-start a session (created by CLI without --notify-only)
|
|
@@ -378,14 +381,17 @@ export function runVerbosityMigrations(database?: Database.Database): void {
|
|
|
378
381
|
|
|
379
382
|
/**
|
|
380
383
|
* Get the verbosity setting for a channel.
|
|
381
|
-
*
|
|
384
|
+
* Falls back to the global default set via --verbosity CLI flag if no per-channel override exists.
|
|
382
385
|
*/
|
|
383
386
|
export function getChannelVerbosity(channelId: string): VerbosityLevel {
|
|
384
387
|
const db = getDatabase()
|
|
385
388
|
const row = db
|
|
386
389
|
.prepare('SELECT verbosity FROM channel_verbosity WHERE channel_id = ?')
|
|
387
390
|
.get(channelId) as { verbosity: string } | undefined
|
|
388
|
-
|
|
391
|
+
if (row?.verbosity) {
|
|
392
|
+
return row.verbosity as VerbosityLevel
|
|
393
|
+
}
|
|
394
|
+
return getDefaultVerbosity()
|
|
389
395
|
}
|
|
390
396
|
|
|
391
397
|
/**
|