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/dist/cli.js
CHANGED
|
@@ -14,9 +14,18 @@ import { createLogger, LogPrefix } from './logger.js';
|
|
|
14
14
|
import { uploadFilesToDiscord } from './discord-utils.js';
|
|
15
15
|
import { spawn, spawnSync, execSync } from 'node:child_process';
|
|
16
16
|
import http from 'node:http';
|
|
17
|
-
import { setDataDir, getDataDir, getLockPort } from './config.js';
|
|
17
|
+
import { setDataDir, getDataDir, getLockPort, setDefaultVerbosity } from './config.js';
|
|
18
18
|
import { sanitizeAgentName } from './commands/agent.js';
|
|
19
19
|
const cliLogger = createLogger(LogPrefix.CLI);
|
|
20
|
+
// Strip bracketed paste escape sequences from terminal input.
|
|
21
|
+
// iTerm2 and other terminals wrap pasted content with \x1b[200~ and \x1b[201~
|
|
22
|
+
// which can cause validation to fail on macOS. See: https://github.com/remorses/kimaki/issues/18
|
|
23
|
+
function stripBracketedPaste(value) {
|
|
24
|
+
if (!value) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
return value.replace(/\x1b\[200~/g, '').replace(/\x1b\[201~/g, '').trim();
|
|
28
|
+
}
|
|
20
29
|
// Spawn caffeinate on macOS to prevent system sleep while bot is running.
|
|
21
30
|
// Not detached, so it dies automatically with the parent process.
|
|
22
31
|
function startCaffeinate() {
|
|
@@ -106,7 +115,8 @@ async function checkSingleInstance() {
|
|
|
106
115
|
});
|
|
107
116
|
}
|
|
108
117
|
}
|
|
109
|
-
catch {
|
|
118
|
+
catch (error) {
|
|
119
|
+
cliLogger.debug('Lock port check failed:', error instanceof Error ? error.message : String(error));
|
|
110
120
|
cliLogger.debug('No other kimaki instance detected on lock port');
|
|
111
121
|
}
|
|
112
122
|
}
|
|
@@ -399,11 +409,17 @@ async function backgroundInit({ currentDir, token, appId, }) {
|
|
|
399
409
|
getClient()
|
|
400
410
|
.command.list({ query: { directory: currentDir } })
|
|
401
411
|
.then((r) => r.data || [])
|
|
402
|
-
.catch(() =>
|
|
412
|
+
.catch((error) => {
|
|
413
|
+
cliLogger.warn('Failed to load user commands during background init:', error instanceof Error ? error.message : String(error));
|
|
414
|
+
return [];
|
|
415
|
+
}),
|
|
403
416
|
getClient()
|
|
404
417
|
.app.agents({ query: { directory: currentDir } })
|
|
405
418
|
.then((r) => r.data || [])
|
|
406
|
-
.catch(() =>
|
|
419
|
+
.catch((error) => {
|
|
420
|
+
cliLogger.warn('Failed to load agents during background init:', error instanceof Error ? error.message : String(error));
|
|
421
|
+
return [];
|
|
422
|
+
}),
|
|
407
423
|
]);
|
|
408
424
|
await registerCommands({ token, appId, userCommands, agents });
|
|
409
425
|
cliLogger.log('Slash commands registered!');
|
|
@@ -448,7 +464,8 @@ async function run({ restart, addChannels, useWorktrees }) {
|
|
|
448
464
|
fs.accessSync(p, fs.constants.F_OK);
|
|
449
465
|
return true;
|
|
450
466
|
}
|
|
451
|
-
catch {
|
|
467
|
+
catch (error) {
|
|
468
|
+
cliLogger.debug(`OpenCode path not found at ${p}:`, error instanceof Error ? error.message : String(error));
|
|
452
469
|
return false;
|
|
453
470
|
}
|
|
454
471
|
});
|
|
@@ -491,17 +508,20 @@ async function run({ restart, addChannels, useWorktrees }) {
|
|
|
491
508
|
message: 'Enter your Discord Application ID:',
|
|
492
509
|
placeholder: 'e.g., 1234567890123456789',
|
|
493
510
|
validate(value) {
|
|
494
|
-
|
|
511
|
+
const cleaned = stripBracketedPaste(value);
|
|
512
|
+
if (!cleaned) {
|
|
495
513
|
return 'Application ID is required';
|
|
496
|
-
|
|
514
|
+
}
|
|
515
|
+
if (!/^\d{17,20}$/.test(cleaned)) {
|
|
497
516
|
return 'Invalid Application ID format (should be 17-20 digits)';
|
|
517
|
+
}
|
|
498
518
|
},
|
|
499
519
|
});
|
|
500
520
|
if (isCancel(appIdInput)) {
|
|
501
521
|
cancel('Setup cancelled');
|
|
502
522
|
process.exit(0);
|
|
503
523
|
}
|
|
504
|
-
appId = appIdInput;
|
|
524
|
+
appId = stripBracketedPaste(appIdInput);
|
|
505
525
|
note('1. Go to the "Bot" section in the left sidebar\n' +
|
|
506
526
|
'2. Scroll down to "Privileged Gateway Intents"\n' +
|
|
507
527
|
'3. Enable these intents by toggling them ON:\n' +
|
|
@@ -522,33 +542,39 @@ async function run({ restart, addChannels, useWorktrees }) {
|
|
|
522
542
|
const tokenInput = await password({
|
|
523
543
|
message: 'Enter your Discord Bot Token (from "Bot" section - click "Reset Token" if needed):',
|
|
524
544
|
validate(value) {
|
|
525
|
-
|
|
545
|
+
const cleaned = stripBracketedPaste(value);
|
|
546
|
+
if (!cleaned) {
|
|
526
547
|
return 'Bot token is required';
|
|
527
|
-
|
|
548
|
+
}
|
|
549
|
+
if (cleaned.length < 50) {
|
|
528
550
|
return 'Invalid token format (too short)';
|
|
551
|
+
}
|
|
529
552
|
},
|
|
530
553
|
});
|
|
531
554
|
if (isCancel(tokenInput)) {
|
|
532
555
|
cancel('Setup cancelled');
|
|
533
556
|
process.exit(0);
|
|
534
557
|
}
|
|
535
|
-
token = tokenInput;
|
|
558
|
+
token = stripBracketedPaste(tokenInput);
|
|
536
559
|
note(`You can get a Gemini api Key at https://aistudio.google.com/apikey`, `Gemini API Key`);
|
|
537
|
-
const
|
|
560
|
+
const geminiApiKeyInput = await password({
|
|
538
561
|
message: 'Enter your Gemini API Key for voice channels and audio transcription (optional, press Enter to skip):',
|
|
539
562
|
validate(value) {
|
|
540
|
-
|
|
563
|
+
const cleaned = stripBracketedPaste(value);
|
|
564
|
+
if (cleaned && cleaned.length < 10) {
|
|
541
565
|
return 'Invalid API key format';
|
|
566
|
+
}
|
|
542
567
|
return undefined;
|
|
543
568
|
},
|
|
544
569
|
});
|
|
545
|
-
if (isCancel(
|
|
570
|
+
if (isCancel(geminiApiKeyInput)) {
|
|
546
571
|
cancel('Setup cancelled');
|
|
547
572
|
process.exit(0);
|
|
548
573
|
}
|
|
574
|
+
const geminiApiKey = stripBracketedPaste(geminiApiKeyInput) || null;
|
|
549
575
|
// Store API key in database
|
|
550
576
|
if (geminiApiKey) {
|
|
551
|
-
db.prepare('INSERT OR REPLACE INTO bot_api_keys (app_id, gemini_api_key) VALUES (?, ?)').run(appId, geminiApiKey
|
|
577
|
+
db.prepare('INSERT OR REPLACE INTO bot_api_keys (app_id, gemini_api_key) VALUES (?, ?)').run(appId, geminiApiKey);
|
|
552
578
|
note('API key saved successfully', 'API Key Stored');
|
|
553
579
|
}
|
|
554
580
|
note(`Bot install URL:\n${generateBotInstallUrl({ clientId: appId })}\n\nYou MUST install the bot in your Discord server before continuing.`, 'Step 4: Install Bot to Server');
|
|
@@ -676,11 +702,17 @@ async function run({ restart, addChannels, useWorktrees }) {
|
|
|
676
702
|
getClient()
|
|
677
703
|
.command.list({ query: { directory: currentDir } })
|
|
678
704
|
.then((r) => r.data || [])
|
|
679
|
-
.catch(() =>
|
|
705
|
+
.catch((error) => {
|
|
706
|
+
cliLogger.warn('Failed to load user commands during setup:', error instanceof Error ? error.message : String(error));
|
|
707
|
+
return [];
|
|
708
|
+
}),
|
|
680
709
|
getClient()
|
|
681
710
|
.app.agents({ query: { directory: currentDir } })
|
|
682
711
|
.then((r) => r.data || [])
|
|
683
|
-
.catch(() =>
|
|
712
|
+
.catch((error) => {
|
|
713
|
+
cliLogger.warn('Failed to load agents during setup:', error instanceof Error ? error.message : String(error));
|
|
714
|
+
return [];
|
|
715
|
+
}),
|
|
684
716
|
]);
|
|
685
717
|
s.stop(`Found ${projects.length} OpenCode project(s)`);
|
|
686
718
|
const existingDirs = kimakiChannels.flatMap(({ channels }) => channels
|
|
@@ -791,6 +823,7 @@ cli
|
|
|
791
823
|
.option('--data-dir <path>', 'Data directory for config and database (default: ~/.kimaki)')
|
|
792
824
|
.option('--install-url', 'Print the bot install URL and exit')
|
|
793
825
|
.option('--use-worktrees', 'Create git worktrees for all new sessions started from channel messages')
|
|
826
|
+
.option('--verbosity <level>', 'Default verbosity for all channels (tools-and-text or text-only)')
|
|
794
827
|
.action(async (options) => {
|
|
795
828
|
try {
|
|
796
829
|
// Set data directory early, before any database access
|
|
@@ -798,6 +831,14 @@ cli
|
|
|
798
831
|
setDataDir(options.dataDir);
|
|
799
832
|
cliLogger.log(`Using data directory: ${getDataDir()}`);
|
|
800
833
|
}
|
|
834
|
+
if (options.verbosity) {
|
|
835
|
+
if (options.verbosity !== 'tools-and-text' && options.verbosity !== 'text-only') {
|
|
836
|
+
cliLogger.error(`Invalid verbosity level: ${options.verbosity}. Use "tools-and-text" or "text-only".`);
|
|
837
|
+
process.exit(EXIT_NO_RESTART);
|
|
838
|
+
}
|
|
839
|
+
setDefaultVerbosity(options.verbosity);
|
|
840
|
+
cliLogger.log(`Default verbosity: ${options.verbosity}`);
|
|
841
|
+
}
|
|
801
842
|
if (options.installUrl) {
|
|
802
843
|
const db = getDatabase();
|
|
803
844
|
const existingBot = db
|
|
@@ -920,8 +961,8 @@ cli
|
|
|
920
961
|
.get();
|
|
921
962
|
appId = botRow?.app_id;
|
|
922
963
|
}
|
|
923
|
-
catch {
|
|
924
|
-
|
|
964
|
+
catch (error) {
|
|
965
|
+
cliLogger.debug('Database lookup failed while resolving app ID:', error instanceof Error ? error.message : String(error));
|
|
925
966
|
}
|
|
926
967
|
}
|
|
927
968
|
}
|
|
@@ -1017,8 +1058,8 @@ cli
|
|
|
1017
1058
|
return ch.guild;
|
|
1018
1059
|
}
|
|
1019
1060
|
}
|
|
1020
|
-
catch {
|
|
1021
|
-
|
|
1061
|
+
catch (error) {
|
|
1062
|
+
cliLogger.debug('Failed to fetch existing channel while selecting guild:', error instanceof Error ? error.message : String(error));
|
|
1022
1063
|
}
|
|
1023
1064
|
}
|
|
1024
1065
|
// Fall back to first guild the bot is in
|
|
@@ -1199,8 +1240,8 @@ cli
|
|
|
1199
1240
|
.get();
|
|
1200
1241
|
appId = botRow?.app_id;
|
|
1201
1242
|
}
|
|
1202
|
-
catch {
|
|
1203
|
-
|
|
1243
|
+
catch (error) {
|
|
1244
|
+
cliLogger.debug('Database lookup failed while resolving app ID:', error instanceof Error ? error.message : String(error));
|
|
1204
1245
|
}
|
|
1205
1246
|
}
|
|
1206
1247
|
}
|
|
@@ -1270,8 +1311,8 @@ cli
|
|
|
1270
1311
|
throw new Error('Channel has no guild');
|
|
1271
1312
|
}
|
|
1272
1313
|
}
|
|
1273
|
-
catch {
|
|
1274
|
-
|
|
1314
|
+
catch (error) {
|
|
1315
|
+
cliLogger.debug('Failed to fetch existing channel while selecting guild:', error instanceof Error ? error.message : String(error));
|
|
1275
1316
|
const firstGuild = client.guilds.cache.first();
|
|
1276
1317
|
if (!firstGuild) {
|
|
1277
1318
|
s.stop('No guild found');
|
|
@@ -1310,13 +1351,13 @@ cli
|
|
|
1310
1351
|
process.exit(0);
|
|
1311
1352
|
}
|
|
1312
1353
|
}
|
|
1313
|
-
catch {
|
|
1314
|
-
|
|
1354
|
+
catch (error) {
|
|
1355
|
+
cliLogger.debug(`Failed to fetch channel ${existingChannel.channel_id} while checking existing channels:`, error instanceof Error ? error.message : String(error));
|
|
1315
1356
|
}
|
|
1316
1357
|
}
|
|
1317
1358
|
}
|
|
1318
|
-
catch {
|
|
1319
|
-
|
|
1359
|
+
catch (error) {
|
|
1360
|
+
cliLogger.debug('Database lookup failed while checking existing channels:', error instanceof Error ? error.message : String(error));
|
|
1320
1361
|
}
|
|
1321
1362
|
s.message(`Creating channels in ${guild.name}...`);
|
|
1322
1363
|
const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
|
package/dist/commands/abort.js
CHANGED
|
@@ -54,6 +54,7 @@ export async function handleAbortCommand({ command }) {
|
|
|
54
54
|
const sessionId = row.session_id;
|
|
55
55
|
const existingController = abortControllers.get(sessionId);
|
|
56
56
|
if (existingController) {
|
|
57
|
+
logger.log(`[ABORT] reason=user-requested sessionId=${sessionId} channelId=${channel.id} - user ran /abort command`);
|
|
57
58
|
existingController.abort(new Error('User requested abort'));
|
|
58
59
|
abortControllers.delete(sessionId);
|
|
59
60
|
}
|
|
@@ -67,6 +68,7 @@ export async function handleAbortCommand({ command }) {
|
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
70
|
try {
|
|
71
|
+
logger.log(`[ABORT-API] reason=user-requested sessionId=${sessionId} channelId=${channel.id} - sending API abort from /abort command`);
|
|
70
72
|
await getClient().session.abort({
|
|
71
73
|
path: { id: sessionId },
|
|
72
74
|
});
|
|
@@ -1,13 +1,51 @@
|
|
|
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
|
import { ChannelType } from 'discord.js';
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import path from 'node:path';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
5
7
|
import { getProjectsDir } from '../config.js';
|
|
6
8
|
import { createProjectChannels } from '../channel-management.js';
|
|
7
9
|
import { handleOpencodeSession } from '../session-handler.js';
|
|
8
10
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
9
11
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
10
12
|
const logger = createLogger(LogPrefix.CREATE_PROJECT);
|
|
13
|
+
/**
|
|
14
|
+
* Core project creation logic: creates directory, inits git, creates Discord channels.
|
|
15
|
+
* Reused by the slash command handler and by onboarding (welcome channel).
|
|
16
|
+
* Returns null if the project directory already exists.
|
|
17
|
+
*/
|
|
18
|
+
export async function createNewProject({ guild, projectName, appId, botName, }) {
|
|
19
|
+
const sanitizedName = projectName
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
22
|
+
.replace(/-+/g, '-')
|
|
23
|
+
.replace(/^-|-$/g, '')
|
|
24
|
+
.slice(0, 100);
|
|
25
|
+
if (!sanitizedName) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const projectsDir = getProjectsDir();
|
|
29
|
+
const projectDirectory = path.join(projectsDir, sanitizedName);
|
|
30
|
+
if (!fs.existsSync(projectsDir)) {
|
|
31
|
+
fs.mkdirSync(projectsDir, { recursive: true });
|
|
32
|
+
logger.log(`Created projects directory: ${projectsDir}`);
|
|
33
|
+
}
|
|
34
|
+
if (fs.existsSync(projectDirectory)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
38
|
+
logger.log(`Created project directory: ${projectDirectory}`);
|
|
39
|
+
execSync('git init', { cwd: projectDirectory, stdio: 'pipe' });
|
|
40
|
+
logger.log(`Initialized git in: ${projectDirectory}`);
|
|
41
|
+
const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
|
|
42
|
+
guild,
|
|
43
|
+
projectDirectory,
|
|
44
|
+
appId,
|
|
45
|
+
botName,
|
|
46
|
+
});
|
|
47
|
+
return { textChannelId, voiceChannelId, channelName, projectDirectory, sanitizedName };
|
|
48
|
+
}
|
|
11
49
|
export async function handleCreateNewProjectCommand({ command, appId, }) {
|
|
12
50
|
await command.deferReply({ ephemeral: false });
|
|
13
51
|
const projectName = command.options.getString('name', true);
|
|
@@ -21,37 +59,29 @@ export async function handleCreateNewProjectCommand({ command, appId, }) {
|
|
|
21
59
|
await command.editReply('This command can only be used in a text channel');
|
|
22
60
|
return;
|
|
23
61
|
}
|
|
24
|
-
const sanitizedName = projectName
|
|
25
|
-
.toLowerCase()
|
|
26
|
-
.replace(/[^a-z0-9-]/g, '-')
|
|
27
|
-
.replace(/-+/g, '-')
|
|
28
|
-
.replace(/^-|-$/g, '')
|
|
29
|
-
.slice(0, 100);
|
|
30
|
-
if (!sanitizedName) {
|
|
31
|
-
await command.editReply('Invalid project name');
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const projectsDir = getProjectsDir();
|
|
35
|
-
const projectDirectory = path.join(projectsDir, sanitizedName);
|
|
36
62
|
try {
|
|
37
|
-
|
|
38
|
-
fs.mkdirSync(projectsDir, { recursive: true });
|
|
39
|
-
logger.log(`Created projects directory: ${projectsDir}`);
|
|
40
|
-
}
|
|
41
|
-
if (fs.existsSync(projectDirectory)) {
|
|
42
|
-
await command.editReply(`Project directory already exists: ${projectDirectory}`);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
46
|
-
logger.log(`Created project directory: ${projectDirectory}`);
|
|
47
|
-
const { execSync } = await import('node:child_process');
|
|
48
|
-
execSync('git init', { cwd: projectDirectory, stdio: 'pipe' });
|
|
49
|
-
logger.log(`Initialized git in: ${projectDirectory}`);
|
|
50
|
-
const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
|
|
63
|
+
const result = await createNewProject({
|
|
51
64
|
guild,
|
|
52
|
-
|
|
65
|
+
projectName,
|
|
53
66
|
appId,
|
|
67
|
+
botName: command.client.user?.username,
|
|
54
68
|
});
|
|
69
|
+
if (!result) {
|
|
70
|
+
const sanitizedName = projectName
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
73
|
+
.replace(/-+/g, '-')
|
|
74
|
+
.replace(/^-|-$/g, '')
|
|
75
|
+
.slice(0, 100);
|
|
76
|
+
if (!sanitizedName) {
|
|
77
|
+
await command.editReply('Invalid project name');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const projectDirectory = path.join(getProjectsDir(), sanitizedName);
|
|
81
|
+
await command.editReply(`Project directory already exists: ${projectDirectory}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const { textChannelId, voiceChannelId, channelName, projectDirectory, sanitizedName } = result;
|
|
55
85
|
const textChannel = (await guild.channels.fetch(textChannelId));
|
|
56
86
|
await command.editReply(`✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n_Starting session..._`);
|
|
57
87
|
const starterMessage = await textChannel.send({
|
|
@@ -38,7 +38,8 @@ async function isDetachedHead(worktreeDir) {
|
|
|
38
38
|
await execAsync(`git -C "${worktreeDir}" symbolic-ref HEAD`);
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
|
-
catch {
|
|
41
|
+
catch (error) {
|
|
42
|
+
logger.debug(`Failed to resolve HEAD for ${worktreeDir}:`, error instanceof Error ? error.message : String(error));
|
|
42
43
|
return true;
|
|
43
44
|
}
|
|
44
45
|
}
|
|
@@ -50,7 +51,8 @@ async function getCurrentBranch(worktreeDir) {
|
|
|
50
51
|
const { stdout } = await execAsync(`git -C "${worktreeDir}" symbolic-ref --short HEAD`);
|
|
51
52
|
return stdout.trim() || null;
|
|
52
53
|
}
|
|
53
|
-
catch {
|
|
54
|
+
catch (error) {
|
|
55
|
+
logger.debug(`Failed to get current branch for ${worktreeDir}:`, error instanceof Error ? error.message : String(error));
|
|
54
56
|
return null;
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -89,7 +91,8 @@ export async function handleMergeWorktreeCommand({ command, appId }) {
|
|
|
89
91
|
const { stdout } = await execAsync(`git -C "${mainRepoDir}" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@'`);
|
|
90
92
|
defaultBranch = stdout.trim() || 'main';
|
|
91
93
|
}
|
|
92
|
-
catch {
|
|
94
|
+
catch (error) {
|
|
95
|
+
logger.warn(`Failed to detect default branch for ${mainRepoDir}, falling back to main:`, error instanceof Error ? error.message : String(error));
|
|
93
96
|
defaultBranch = 'main';
|
|
94
97
|
}
|
|
95
98
|
// 3. Determine if we're on a branch or detached HEAD
|
|
@@ -115,11 +118,17 @@ export async function handleMergeWorktreeCommand({ command, appId }) {
|
|
|
115
118
|
}
|
|
116
119
|
catch (e) {
|
|
117
120
|
// If merge fails (conflicts), abort and report
|
|
118
|
-
await execAsync(`git -C "${worktreeDir}" merge --abort`).catch(() => {
|
|
121
|
+
await execAsync(`git -C "${worktreeDir}" merge --abort`).catch((error) => {
|
|
122
|
+
logger.warn(`Failed to abort merge in ${worktreeDir}:`, error instanceof Error ? error.message : String(error));
|
|
123
|
+
});
|
|
119
124
|
// Clean up temp branch if we created one
|
|
120
125
|
if (tempBranch) {
|
|
121
|
-
await execAsync(`git -C "${worktreeDir}" checkout --detach`).catch(() => {
|
|
122
|
-
|
|
126
|
+
await execAsync(`git -C "${worktreeDir}" checkout --detach`).catch((error) => {
|
|
127
|
+
logger.warn(`Failed to detach HEAD after merge conflict in ${worktreeDir}:`, error instanceof Error ? error.message : String(error));
|
|
128
|
+
});
|
|
129
|
+
await execAsync(`git -C "${worktreeDir}" branch -D ${tempBranch}`).catch((error) => {
|
|
130
|
+
logger.warn(`Failed to delete temp branch ${tempBranch} in ${worktreeDir}:`, error instanceof Error ? error.message : String(error));
|
|
131
|
+
});
|
|
123
132
|
}
|
|
124
133
|
throw new Error(`Merge conflict - resolve manually in worktree then retry`);
|
|
125
134
|
}
|
|
@@ -133,10 +142,14 @@ export async function handleMergeWorktreeCommand({ command, appId }) {
|
|
|
133
142
|
await execAsync(`git -C "${worktreeDir}" checkout --detach ${defaultBranch}`);
|
|
134
143
|
// 7. Delete the merged branch (temp or original)
|
|
135
144
|
logger.log(`Deleting merged branch ${branchToMerge}`);
|
|
136
|
-
await execAsync(`git -C "${worktreeDir}" branch -D ${branchToMerge}`).catch(() => {
|
|
145
|
+
await execAsync(`git -C "${worktreeDir}" branch -D ${branchToMerge}`).catch((error) => {
|
|
146
|
+
logger.warn(`Failed to delete merged branch ${branchToMerge} in ${worktreeDir}:`, error instanceof Error ? error.message : String(error));
|
|
147
|
+
});
|
|
137
148
|
// Also delete the original worktree branch if different from what we merged
|
|
138
149
|
if (!isDetached && branchToMerge !== worktreeInfo.worktree_name) {
|
|
139
|
-
await execAsync(`git -C "${worktreeDir}" branch -D ${worktreeInfo.worktree_name}`).catch(() => {
|
|
150
|
+
await execAsync(`git -C "${worktreeDir}" branch -D ${worktreeInfo.worktree_name}`).catch((error) => {
|
|
151
|
+
logger.warn(`Failed to delete worktree branch ${worktreeInfo.worktree_name} in ${worktreeDir}:`, error instanceof Error ? error.message : String(error));
|
|
152
|
+
});
|
|
140
153
|
}
|
|
141
154
|
// 8. Remove worktree prefix from thread title (fire and forget with timeout)
|
|
142
155
|
void removeWorktreePrefixFromTitle(thread);
|
|
@@ -13,7 +13,7 @@ export const pendingPermissionContexts = new Map();
|
|
|
13
13
|
* Show permission dropdown for a permission request.
|
|
14
14
|
* Returns the message ID and context hash for tracking.
|
|
15
15
|
*/
|
|
16
|
-
export async function showPermissionDropdown({ thread, permission, directory, }) {
|
|
16
|
+
export async function showPermissionDropdown({ thread, permission, directory, subtaskLabel, }) {
|
|
17
17
|
const contextHash = crypto.randomBytes(8).toString('hex');
|
|
18
18
|
const context = {
|
|
19
19
|
permission,
|
|
@@ -47,8 +47,10 @@ export async function showPermissionDropdown({ thread, permission, directory, })
|
|
|
47
47
|
.setPlaceholder('Choose an action')
|
|
48
48
|
.addOptions(options);
|
|
49
49
|
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
50
|
+
const subtaskLine = subtaskLabel ? `**From:** \`${subtaskLabel}\`\n` : '';
|
|
50
51
|
const permissionMessage = await thread.send({
|
|
51
52
|
content: `⚠️ **Permission Required**\n\n` +
|
|
53
|
+
subtaskLine +
|
|
52
54
|
`**Type:** \`${permission.permission}\`\n` +
|
|
53
55
|
(patternStr ? `**Pattern:** \`${patternStr}\`` : ''),
|
|
54
56
|
components: [actionRow],
|
package/dist/commands/session.js
CHANGED
|
@@ -105,7 +105,10 @@ async function handleAgentAutocomplete({ interaction, appId }) {
|
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
107
|
const agents = agentsResponse.data
|
|
108
|
-
.filter((a) =>
|
|
108
|
+
.filter((a) => {
|
|
109
|
+
const hidden = a.hidden;
|
|
110
|
+
return (a.mode === 'primary' || a.mode === 'all') && !hidden;
|
|
111
|
+
})
|
|
109
112
|
.filter((a) => a.name.toLowerCase().includes(focusedValue.toLowerCase()))
|
|
110
113
|
.slice(0, 25);
|
|
111
114
|
const choices = agents.map((agent) => ({
|
|
@@ -8,7 +8,7 @@ import { createLogger, LogPrefix } from '../logger.js';
|
|
|
8
8
|
const verbosityLogger = createLogger(LogPrefix.VERBOSITY);
|
|
9
9
|
/**
|
|
10
10
|
* Handle the /verbosity slash command.
|
|
11
|
-
* Sets output verbosity for the channel (applies
|
|
11
|
+
* Sets output verbosity for the channel (applies immediately, even mid-session).
|
|
12
12
|
*/
|
|
13
13
|
export async function handleVerbosityCommand({ command, appId, }) {
|
|
14
14
|
verbosityLogger.log('[VERBOSITY] Command called');
|
|
@@ -36,7 +36,7 @@ export async function handleVerbosityCommand({ command, appId, }) {
|
|
|
36
36
|
const currentLevel = getChannelVerbosity(channelId);
|
|
37
37
|
if (currentLevel === level) {
|
|
38
38
|
await command.reply({
|
|
39
|
-
content: `Verbosity is already set to **${level}
|
|
39
|
+
content: `Verbosity is already set to **${level}** for this channel.`,
|
|
40
40
|
ephemeral: true,
|
|
41
41
|
});
|
|
42
42
|
return;
|
|
@@ -47,7 +47,7 @@ export async function handleVerbosityCommand({ command, appId, }) {
|
|
|
47
47
|
? 'Only text responses will be shown. Tool executions, status messages, and thinking will be hidden.'
|
|
48
48
|
: 'All output will be shown, including tool executions and status messages.';
|
|
49
49
|
await command.reply({
|
|
50
|
-
content: `Verbosity set to **${level}
|
|
50
|
+
content: `Verbosity set to **${level}** for this channel.\n${description}\nThis is a per-channel setting and applies immediately, including any active sessions.`,
|
|
51
51
|
ephemeral: true,
|
|
52
52
|
});
|
|
53
53
|
}
|
package/dist/config.js
CHANGED
|
@@ -35,6 +35,13 @@ export function setDataDir(dir) {
|
|
|
35
35
|
export function getProjectsDir() {
|
|
36
36
|
return path.join(getDataDir(), 'projects');
|
|
37
37
|
}
|
|
38
|
+
let defaultVerbosity = 'tools-and-text';
|
|
39
|
+
export function getDefaultVerbosity() {
|
|
40
|
+
return defaultVerbosity;
|
|
41
|
+
}
|
|
42
|
+
export function setDefaultVerbosity(level) {
|
|
43
|
+
defaultVerbosity = level;
|
|
44
|
+
}
|
|
38
45
|
const DEFAULT_LOCK_PORT = 29988;
|
|
39
46
|
/**
|
|
40
47
|
* Derive a lock port from the data directory path.
|
package/dist/database.js
CHANGED
|
@@ -6,7 +6,7 @@ import fs from 'node:fs';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import * as errore from 'errore';
|
|
8
8
|
import { createLogger, LogPrefix } from './logger.js';
|
|
9
|
-
import { getDataDir } from './config.js';
|
|
9
|
+
import { getDataDir, getDefaultVerbosity } from './config.js';
|
|
10
10
|
const dbLogger = createLogger(LogPrefix.DB);
|
|
11
11
|
let db = null;
|
|
12
12
|
export function getDatabase() {
|
|
@@ -58,8 +58,8 @@ export function getDatabase() {
|
|
|
58
58
|
try {
|
|
59
59
|
db.exec(`ALTER TABLE channel_directories ADD COLUMN app_id TEXT`);
|
|
60
60
|
}
|
|
61
|
-
catch {
|
|
62
|
-
|
|
61
|
+
catch (error) {
|
|
62
|
+
dbLogger.debug('Failed to add app_id column to channel_directories (likely exists):', error instanceof Error ? error.message : String(error));
|
|
63
63
|
}
|
|
64
64
|
// Table for threads that should auto-start a session (created by CLI without --notify-only)
|
|
65
65
|
db.exec(`
|
|
@@ -281,14 +281,17 @@ export function runVerbosityMigrations(database) {
|
|
|
281
281
|
}
|
|
282
282
|
/**
|
|
283
283
|
* Get the verbosity setting for a channel.
|
|
284
|
-
*
|
|
284
|
+
* Falls back to the global default set via --verbosity CLI flag if no per-channel override exists.
|
|
285
285
|
*/
|
|
286
286
|
export function getChannelVerbosity(channelId) {
|
|
287
287
|
const db = getDatabase();
|
|
288
288
|
const row = db
|
|
289
289
|
.prepare('SELECT verbosity FROM channel_verbosity WHERE channel_id = ?')
|
|
290
290
|
.get(channelId);
|
|
291
|
-
|
|
291
|
+
if (row?.verbosity) {
|
|
292
|
+
return row.verbosity;
|
|
293
|
+
}
|
|
294
|
+
return getDefaultVerbosity();
|
|
292
295
|
}
|
|
293
296
|
/**
|
|
294
297
|
* Set the verbosity setting for a channel.
|
package/dist/discord-bot.js
CHANGED
|
@@ -30,6 +30,9 @@ import { setGlobalDispatcher, Agent } from 'undici';
|
|
|
30
30
|
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0, connections: 500 }));
|
|
31
31
|
const discordLogger = createLogger(LogPrefix.DISCORD);
|
|
32
32
|
const voiceLogger = createLogger(LogPrefix.VOICE);
|
|
33
|
+
function prefixWithDiscordUser({ username, prompt }) {
|
|
34
|
+
return `<discord-user name="${username}" />\n${prompt}`;
|
|
35
|
+
}
|
|
33
36
|
export async function createDiscordClient() {
|
|
34
37
|
return new Client({
|
|
35
38
|
intents: [
|
|
@@ -93,6 +96,15 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
93
96
|
if (message.author?.bot) {
|
|
94
97
|
return;
|
|
95
98
|
}
|
|
99
|
+
// Ignore messages that start with a mention of another user (not the bot).
|
|
100
|
+
// These are likely users talking to each other, not the bot.
|
|
101
|
+
const leadingMentionMatch = message.content?.match(/^<@!?(\d+)>/);
|
|
102
|
+
if (leadingMentionMatch) {
|
|
103
|
+
const mentionedUserId = leadingMentionMatch[1];
|
|
104
|
+
if (mentionedUserId !== discordClient.user?.id) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
96
108
|
if (message.partial) {
|
|
97
109
|
discordLogger.log(`Fetching partial message ${message.id}`);
|
|
98
110
|
const fetched = await errore.tryAsync({
|
|
@@ -184,12 +196,15 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
184
196
|
}
|
|
185
197
|
// Include starter message as context for the session
|
|
186
198
|
let prompt = message.content;
|
|
187
|
-
const starterMessage = await thread.fetchStarterMessage().catch(() =>
|
|
199
|
+
const starterMessage = await thread.fetchStarterMessage().catch((error) => {
|
|
200
|
+
discordLogger.warn(`[SESSION] Failed to fetch starter message for thread ${thread.id}:`, error instanceof Error ? error.message : String(error));
|
|
201
|
+
return null;
|
|
202
|
+
});
|
|
188
203
|
if (starterMessage?.content && starterMessage.content !== message.content) {
|
|
189
204
|
prompt = `Context from thread:\n${starterMessage.content}\n\nUser request:\n${message.content}`;
|
|
190
205
|
}
|
|
191
206
|
await handleOpencodeSession({
|
|
192
|
-
prompt,
|
|
207
|
+
prompt: prefixWithDiscordUser({ username: message.member?.displayName || message.author.displayName, prompt }),
|
|
193
208
|
thread,
|
|
194
209
|
projectDirectory,
|
|
195
210
|
channelId: parent?.id || '',
|
|
@@ -259,7 +274,7 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
259
274
|
? `${messageContent}\n\n${textAttachmentsContent}`
|
|
260
275
|
: messageContent;
|
|
261
276
|
await handleOpencodeSession({
|
|
262
|
-
prompt: promptWithAttachments,
|
|
277
|
+
prompt: prefixWithDiscordUser({ username: message.member?.displayName || message.author.displayName, prompt: promptWithAttachments }),
|
|
263
278
|
thread,
|
|
264
279
|
projectDirectory,
|
|
265
280
|
originalMessage: message,
|
|
@@ -379,7 +394,7 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
379
394
|
? `${messageContent}\n\n${textAttachmentsContent}`
|
|
380
395
|
: messageContent;
|
|
381
396
|
await handleOpencodeSession({
|
|
382
|
-
prompt: promptWithAttachments,
|
|
397
|
+
prompt: prefixWithDiscordUser({ username: message.member?.displayName || message.author.displayName, prompt: promptWithAttachments }),
|
|
383
398
|
thread,
|
|
384
399
|
projectDirectory: sessionDirectory,
|
|
385
400
|
originalMessage: message,
|
|
@@ -397,8 +412,8 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
397
412
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
398
413
|
await message.reply({ content: `Error: ${errMsg}`, flags: SILENT_MESSAGE_FLAGS });
|
|
399
414
|
}
|
|
400
|
-
catch {
|
|
401
|
-
voiceLogger.error('Discord handler error (fallback):',
|
|
415
|
+
catch (sendError) {
|
|
416
|
+
voiceLogger.error('Discord handler error (fallback):', sendError instanceof Error ? sendError.message : String(sendError));
|
|
402
417
|
}
|
|
403
418
|
}
|
|
404
419
|
});
|
|
@@ -416,7 +431,10 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
416
431
|
return;
|
|
417
432
|
}
|
|
418
433
|
// Get the starter message to check for auto-start marker
|
|
419
|
-
const starterMessage = await thread.fetchStarterMessage().catch(() =>
|
|
434
|
+
const starterMessage = await thread.fetchStarterMessage().catch((error) => {
|
|
435
|
+
discordLogger.warn(`[THREAD_CREATE] Failed to fetch starter message for thread ${thread.id}:`, error instanceof Error ? error.message : String(error));
|
|
436
|
+
return null;
|
|
437
|
+
});
|
|
420
438
|
if (!starterMessage) {
|
|
421
439
|
discordLogger.log(`[THREAD_CREATE] Could not fetch starter message for thread ${thread.id}`);
|
|
422
440
|
return;
|
|
@@ -466,8 +484,8 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
466
484
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
467
485
|
await thread.send({ content: `Error: ${errMsg}`, flags: SILENT_MESSAGE_FLAGS });
|
|
468
486
|
}
|
|
469
|
-
catch {
|
|
470
|
-
|
|
487
|
+
catch (sendError) {
|
|
488
|
+
voiceLogger.error('[BOT_SESSION] Failed to send error message:', sendError instanceof Error ? sendError.message : String(sendError));
|
|
471
489
|
}
|
|
472
490
|
}
|
|
473
491
|
});
|