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/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
- // Database might not exist in CI, that's ok
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
- // Channel might be deleted, continue
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
- // Database might not exist in CI
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
- // Channel might be deleted, fall back to first guild
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
- // Channel might be deleted, continue checking
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
- // Database might not exist, continue to create
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}...`)
@@ -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
- const sanitizedName = projectName
36
- .toLowerCase()
37
- .replace(/[^a-z0-9-]/g, '-')
38
- .replace(/-+/g, '-')
39
- .replace(/^-|-$/g, '')
40
- .slice(0, 100)
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
- const projectsDir = getProjectsDir()
48
- const projectDirectory = path.join(projectsDir, sanitizedName)
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
- try {
51
- if (!fs.existsSync(projectsDir)) {
52
- fs.mkdirSync(projectsDir, { recursive: true })
53
- logger.log(`Created projects directory: ${projectsDir}`)
54
- }
114
+ if (!sanitizedName) {
115
+ await command.editReply('Invalid project name')
116
+ return
117
+ }
55
118
 
56
- if (fs.existsSync(projectDirectory)) {
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
- fs.mkdirSync(projectDirectory, { recursive: true })
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
- await execAsync(`git -C "${worktreeDir}" branch -D ${tempBranch}`).catch(() => {})
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],
@@ -133,7 +133,10 @@ async function handleAgentAutocomplete({ interaction, appId }: AutocompleteConte
133
133
  }
134
134
 
135
135
  const agents = agentsResponse.data
136
- .filter((a) => a.mode === 'primary' || a.mode === 'all')
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 to new sessions).
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}**.\n${description}\nThis applies to all new sessions in this channel.`,
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
- // Column already exists, ignore
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
- * @returns 'tools-and-text' (default) or 'text-only'
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
- return (row?.verbosity as VerbosityLevel) || 'tools-and-text'
391
+ if (row?.verbosity) {
392
+ return row.verbosity as VerbosityLevel
393
+ }
394
+ return getDefaultVerbosity()
389
395
  }
390
396
 
391
397
  /**