kimaki 0.4.47 → 0.4.49
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 +70 -29
- 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 +114 -29
- 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,11 +47,21 @@ 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)
|
|
54
54
|
|
|
55
|
+
// Strip bracketed paste escape sequences from terminal input.
|
|
56
|
+
// iTerm2 and other terminals wrap pasted content with \x1b[200~ and \x1b[201~
|
|
57
|
+
// which can cause validation to fail on macOS. See: https://github.com/remorses/kimaki/issues/18
|
|
58
|
+
function stripBracketedPaste(value: string | undefined): string {
|
|
59
|
+
if (!value) {
|
|
60
|
+
return ''
|
|
61
|
+
}
|
|
62
|
+
return value.replace(/\x1b\[200~/g, '').replace(/\x1b\[201~/g, '').trim()
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
// Spawn caffeinate on macOS to prevent system sleep while bot is running.
|
|
56
66
|
// Not detached, so it dies automatically with the parent process.
|
|
57
67
|
function startCaffeinate() {
|
|
@@ -145,7 +155,11 @@ async function checkSingleInstance(): Promise<void> {
|
|
|
145
155
|
setTimeout(resolve, 500)
|
|
146
156
|
})
|
|
147
157
|
}
|
|
148
|
-
} catch {
|
|
158
|
+
} catch (error) {
|
|
159
|
+
cliLogger.debug(
|
|
160
|
+
'Lock port check failed:',
|
|
161
|
+
error instanceof Error ? error.message : String(error),
|
|
162
|
+
)
|
|
149
163
|
cliLogger.debug('No other kimaki instance detected on lock port')
|
|
150
164
|
}
|
|
151
165
|
}
|
|
@@ -551,11 +565,23 @@ async function backgroundInit({
|
|
|
551
565
|
getClient()
|
|
552
566
|
.command.list({ query: { directory: currentDir } })
|
|
553
567
|
.then((r) => r.data || [])
|
|
554
|
-
.catch(() =>
|
|
568
|
+
.catch((error) => {
|
|
569
|
+
cliLogger.warn(
|
|
570
|
+
'Failed to load user commands during background init:',
|
|
571
|
+
error instanceof Error ? error.message : String(error),
|
|
572
|
+
)
|
|
573
|
+
return []
|
|
574
|
+
}),
|
|
555
575
|
getClient()
|
|
556
576
|
.app.agents({ query: { directory: currentDir } })
|
|
557
577
|
.then((r) => r.data || [])
|
|
558
|
-
.catch(() =>
|
|
578
|
+
.catch((error) => {
|
|
579
|
+
cliLogger.warn(
|
|
580
|
+
'Failed to load agents during background init:',
|
|
581
|
+
error instanceof Error ? error.message : String(error),
|
|
582
|
+
)
|
|
583
|
+
return []
|
|
584
|
+
}),
|
|
559
585
|
])
|
|
560
586
|
|
|
561
587
|
await registerCommands({ token, appId, userCommands, agents })
|
|
@@ -613,7 +639,11 @@ async function run({ restart, addChannels, useWorktrees }: CliOptions) {
|
|
|
613
639
|
try {
|
|
614
640
|
fs.accessSync(p, fs.constants.F_OK)
|
|
615
641
|
return true
|
|
616
|
-
} catch {
|
|
642
|
+
} catch (error) {
|
|
643
|
+
cliLogger.debug(
|
|
644
|
+
`OpenCode path not found at ${p}:`,
|
|
645
|
+
error instanceof Error ? error.message : String(error),
|
|
646
|
+
)
|
|
617
647
|
return false
|
|
618
648
|
}
|
|
619
649
|
})
|
|
@@ -676,9 +706,13 @@ async function run({ restart, addChannels, useWorktrees }: CliOptions) {
|
|
|
676
706
|
message: 'Enter your Discord Application ID:',
|
|
677
707
|
placeholder: 'e.g., 1234567890123456789',
|
|
678
708
|
validate(value) {
|
|
679
|
-
|
|
680
|
-
if (
|
|
709
|
+
const cleaned = stripBracketedPaste(value)
|
|
710
|
+
if (!cleaned) {
|
|
711
|
+
return 'Application ID is required'
|
|
712
|
+
}
|
|
713
|
+
if (!/^\d{17,20}$/.test(cleaned)) {
|
|
681
714
|
return 'Invalid Application ID format (should be 17-20 digits)'
|
|
715
|
+
}
|
|
682
716
|
},
|
|
683
717
|
})
|
|
684
718
|
|
|
@@ -686,7 +720,7 @@ async function run({ restart, addChannels, useWorktrees }: CliOptions) {
|
|
|
686
720
|
cancel('Setup cancelled')
|
|
687
721
|
process.exit(0)
|
|
688
722
|
}
|
|
689
|
-
appId = appIdInput
|
|
723
|
+
appId = stripBracketedPaste(appIdInput)
|
|
690
724
|
|
|
691
725
|
note(
|
|
692
726
|
'1. Go to the "Bot" section in the left sidebar\n' +
|
|
@@ -717,8 +751,13 @@ async function run({ restart, addChannels, useWorktrees }: CliOptions) {
|
|
|
717
751
|
const tokenInput = await password({
|
|
718
752
|
message: 'Enter your Discord Bot Token (from "Bot" section - click "Reset Token" if needed):',
|
|
719
753
|
validate(value) {
|
|
720
|
-
|
|
721
|
-
if (
|
|
754
|
+
const cleaned = stripBracketedPaste(value)
|
|
755
|
+
if (!cleaned) {
|
|
756
|
+
return 'Bot token is required'
|
|
757
|
+
}
|
|
758
|
+
if (cleaned.length < 50) {
|
|
759
|
+
return 'Invalid token format (too short)'
|
|
760
|
+
}
|
|
722
761
|
},
|
|
723
762
|
})
|
|
724
763
|
|
|
@@ -726,29 +765,34 @@ async function run({ restart, addChannels, useWorktrees }: CliOptions) {
|
|
|
726
765
|
cancel('Setup cancelled')
|
|
727
766
|
process.exit(0)
|
|
728
767
|
}
|
|
729
|
-
token = tokenInput
|
|
768
|
+
token = stripBracketedPaste(tokenInput)
|
|
730
769
|
|
|
731
770
|
note(`You can get a Gemini api Key at https://aistudio.google.com/apikey`, `Gemini API Key`)
|
|
732
771
|
|
|
733
|
-
const
|
|
772
|
+
const geminiApiKeyInput = await password({
|
|
734
773
|
message:
|
|
735
774
|
'Enter your Gemini API Key for voice channels and audio transcription (optional, press Enter to skip):',
|
|
736
775
|
validate(value) {
|
|
737
|
-
|
|
776
|
+
const cleaned = stripBracketedPaste(value)
|
|
777
|
+
if (cleaned && cleaned.length < 10) {
|
|
778
|
+
return 'Invalid API key format'
|
|
779
|
+
}
|
|
738
780
|
return undefined
|
|
739
781
|
},
|
|
740
782
|
})
|
|
741
783
|
|
|
742
|
-
if (isCancel(
|
|
784
|
+
if (isCancel(geminiApiKeyInput)) {
|
|
743
785
|
cancel('Setup cancelled')
|
|
744
786
|
process.exit(0)
|
|
745
787
|
}
|
|
746
788
|
|
|
789
|
+
const geminiApiKey = stripBracketedPaste(geminiApiKeyInput) || null
|
|
790
|
+
|
|
747
791
|
// Store API key in database
|
|
748
792
|
if (geminiApiKey) {
|
|
749
793
|
db.prepare('INSERT OR REPLACE INTO bot_api_keys (app_id, gemini_api_key) VALUES (?, ?)').run(
|
|
750
794
|
appId,
|
|
751
|
-
geminiApiKey
|
|
795
|
+
geminiApiKey,
|
|
752
796
|
)
|
|
753
797
|
note('API key saved successfully', 'API Key Stored')
|
|
754
798
|
}
|
|
@@ -914,11 +958,23 @@ async function run({ restart, addChannels, useWorktrees }: CliOptions) {
|
|
|
914
958
|
getClient()
|
|
915
959
|
.command.list({ query: { directory: currentDir } })
|
|
916
960
|
.then((r) => r.data || [])
|
|
917
|
-
.catch(() =>
|
|
961
|
+
.catch((error) => {
|
|
962
|
+
cliLogger.warn(
|
|
963
|
+
'Failed to load user commands during setup:',
|
|
964
|
+
error instanceof Error ? error.message : String(error),
|
|
965
|
+
)
|
|
966
|
+
return []
|
|
967
|
+
}),
|
|
918
968
|
getClient()
|
|
919
969
|
.app.agents({ query: { directory: currentDir } })
|
|
920
970
|
.then((r) => r.data || [])
|
|
921
|
-
.catch(() =>
|
|
971
|
+
.catch((error) => {
|
|
972
|
+
cliLogger.warn(
|
|
973
|
+
'Failed to load agents during setup:',
|
|
974
|
+
error instanceof Error ? error.message : String(error),
|
|
975
|
+
)
|
|
976
|
+
return []
|
|
977
|
+
}),
|
|
922
978
|
])
|
|
923
979
|
|
|
924
980
|
s.stop(`Found ${projects.length} OpenCode project(s)`)
|
|
@@ -1066,6 +1122,7 @@ cli
|
|
|
1066
1122
|
.option('--data-dir <path>', 'Data directory for config and database (default: ~/.kimaki)')
|
|
1067
1123
|
.option('--install-url', 'Print the bot install URL and exit')
|
|
1068
1124
|
.option('--use-worktrees', 'Create git worktrees for all new sessions started from channel messages')
|
|
1125
|
+
.option('--verbosity <level>', 'Default verbosity for all channels (tools-and-text or text-only)')
|
|
1069
1126
|
.action(
|
|
1070
1127
|
async (options: {
|
|
1071
1128
|
restart?: boolean
|
|
@@ -1073,6 +1130,7 @@ cli
|
|
|
1073
1130
|
dataDir?: string
|
|
1074
1131
|
installUrl?: boolean
|
|
1075
1132
|
useWorktrees?: boolean
|
|
1133
|
+
verbosity?: string
|
|
1076
1134
|
}) => {
|
|
1077
1135
|
try {
|
|
1078
1136
|
// Set data directory early, before any database access
|
|
@@ -1081,6 +1139,15 @@ cli
|
|
|
1081
1139
|
cliLogger.log(`Using data directory: ${getDataDir()}`)
|
|
1082
1140
|
}
|
|
1083
1141
|
|
|
1142
|
+
if (options.verbosity) {
|
|
1143
|
+
if (options.verbosity !== 'tools-and-text' && options.verbosity !== 'text-only') {
|
|
1144
|
+
cliLogger.error(`Invalid verbosity level: ${options.verbosity}. Use "tools-and-text" or "text-only".`)
|
|
1145
|
+
process.exit(EXIT_NO_RESTART)
|
|
1146
|
+
}
|
|
1147
|
+
setDefaultVerbosity(options.verbosity)
|
|
1148
|
+
cliLogger.log(`Default verbosity: ${options.verbosity}`)
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1084
1151
|
if (options.installUrl) {
|
|
1085
1152
|
const db = getDatabase()
|
|
1086
1153
|
const existingBot = db
|
|
@@ -1240,8 +1307,11 @@ cli
|
|
|
1240
1307
|
.prepare('SELECT app_id FROM bot_tokens ORDER BY created_at DESC LIMIT 1')
|
|
1241
1308
|
.get() as { app_id: string } | undefined
|
|
1242
1309
|
appId = botRow?.app_id
|
|
1243
|
-
} catch {
|
|
1244
|
-
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
cliLogger.debug(
|
|
1312
|
+
'Database lookup failed while resolving app ID:',
|
|
1313
|
+
error instanceof Error ? error.message : String(error),
|
|
1314
|
+
)
|
|
1245
1315
|
}
|
|
1246
1316
|
}
|
|
1247
1317
|
} else {
|
|
@@ -1356,8 +1426,11 @@ cli
|
|
|
1356
1426
|
if (ch && 'guild' in ch && ch.guild) {
|
|
1357
1427
|
return ch.guild
|
|
1358
1428
|
}
|
|
1359
|
-
} catch {
|
|
1360
|
-
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
cliLogger.debug(
|
|
1431
|
+
'Failed to fetch existing channel while selecting guild:',
|
|
1432
|
+
error instanceof Error ? error.message : String(error),
|
|
1433
|
+
)
|
|
1361
1434
|
}
|
|
1362
1435
|
}
|
|
1363
1436
|
// Fall back to first guild the bot is in
|
|
@@ -1597,8 +1670,11 @@ cli
|
|
|
1597
1670
|
.prepare('SELECT app_id FROM bot_tokens ORDER BY created_at DESC LIMIT 1')
|
|
1598
1671
|
.get() as { app_id: string } | undefined
|
|
1599
1672
|
appId = botRow?.app_id
|
|
1600
|
-
} catch {
|
|
1601
|
-
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
cliLogger.debug(
|
|
1675
|
+
'Database lookup failed while resolving app ID:',
|
|
1676
|
+
error instanceof Error ? error.message : String(error),
|
|
1677
|
+
)
|
|
1602
1678
|
}
|
|
1603
1679
|
}
|
|
1604
1680
|
} else {
|
|
@@ -1677,8 +1753,11 @@ cli
|
|
|
1677
1753
|
} else {
|
|
1678
1754
|
throw new Error('Channel has no guild')
|
|
1679
1755
|
}
|
|
1680
|
-
} catch {
|
|
1681
|
-
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
cliLogger.debug(
|
|
1758
|
+
'Failed to fetch existing channel while selecting guild:',
|
|
1759
|
+
error instanceof Error ? error.message : String(error),
|
|
1760
|
+
)
|
|
1682
1761
|
const firstGuild = client.guilds.cache.first()
|
|
1683
1762
|
if (!firstGuild) {
|
|
1684
1763
|
s.stop('No guild found')
|
|
@@ -1722,12 +1801,18 @@ cli
|
|
|
1722
1801
|
client.destroy()
|
|
1723
1802
|
process.exit(0)
|
|
1724
1803
|
}
|
|
1725
|
-
} catch {
|
|
1726
|
-
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
cliLogger.debug(
|
|
1806
|
+
`Failed to fetch channel ${existingChannel.channel_id} while checking existing channels:`,
|
|
1807
|
+
error instanceof Error ? error.message : String(error),
|
|
1808
|
+
)
|
|
1727
1809
|
}
|
|
1728
1810
|
}
|
|
1729
|
-
} catch {
|
|
1730
|
-
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
cliLogger.debug(
|
|
1813
|
+
'Database lookup failed while checking existing channels:',
|
|
1814
|
+
error instanceof Error ? error.message : String(error),
|
|
1815
|
+
)
|
|
1731
1816
|
}
|
|
1732
1817
|
|
|
1733
1818
|
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
|
/**
|