kimaki 0.4.34 → 0.4.36
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/ai-tool-to-genai.js +1 -3
- package/dist/channel-management.js +1 -1
- package/dist/cli.js +142 -39
- package/dist/commands/abort.js +1 -1
- package/dist/commands/add-project.js +1 -1
- package/dist/commands/agent.js +6 -2
- package/dist/commands/ask-question.js +2 -1
- package/dist/commands/fork.js +7 -7
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +109 -0
- package/dist/commands/resume.js +3 -5
- package/dist/commands/session.js +56 -1
- package/dist/commands/share.js +1 -1
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +3 -6
- package/dist/config.js +1 -1
- package/dist/discord-bot.js +4 -10
- package/dist/discord-utils.js +33 -9
- package/dist/genai.js +4 -6
- package/dist/interaction-handler.js +8 -1
- package/dist/markdown.js +1 -3
- package/dist/message-formatting.js +7 -3
- package/dist/openai-realtime.js +3 -5
- package/dist/opencode.js +2 -3
- package/dist/session-handler.js +42 -25
- package/dist/system-message.js +5 -3
- package/dist/tools.js +9 -22
- package/dist/unnest-code-blocks.js +4 -2
- package/dist/unnest-code-blocks.test.js +40 -15
- package/dist/voice-handler.js +9 -12
- package/dist/voice.js +5 -3
- package/dist/xml.js +2 -4
- package/package.json +3 -2
- package/src/__snapshots__/compact-session-context-no-system.md +24 -24
- package/src/__snapshots__/compact-session-context.md +31 -31
- package/src/ai-tool-to-genai.ts +3 -11
- package/src/channel-management.ts +14 -25
- package/src/cli.ts +290 -195
- package/src/commands/abort.ts +1 -3
- package/src/commands/add-project.ts +8 -14
- package/src/commands/agent.ts +16 -9
- package/src/commands/ask-question.ts +8 -7
- package/src/commands/create-new-project.ts +8 -14
- package/src/commands/fork.ts +23 -27
- package/src/commands/model.ts +14 -11
- package/src/commands/permissions.ts +1 -1
- package/src/commands/queue.ts +6 -19
- package/src/commands/remove-project.ts +136 -0
- package/src/commands/resume.ts +11 -30
- package/src/commands/session.ts +68 -9
- package/src/commands/share.ts +1 -3
- package/src/commands/types.ts +1 -3
- package/src/commands/undo-redo.ts +6 -18
- package/src/commands/user-command.ts +8 -10
- package/src/config.ts +5 -5
- package/src/database.ts +10 -8
- package/src/discord-bot.ts +22 -46
- package/src/discord-utils.ts +35 -18
- package/src/escape-backticks.test.ts +0 -2
- package/src/format-tables.ts +1 -4
- package/src/genai-worker-wrapper.ts +3 -9
- package/src/genai-worker.ts +4 -19
- package/src/genai.ts +10 -42
- package/src/interaction-handler.ts +133 -121
- package/src/markdown.test.ts +10 -32
- package/src/markdown.ts +6 -14
- package/src/message-formatting.ts +13 -14
- package/src/openai-realtime.ts +25 -47
- package/src/opencode.ts +26 -37
- package/src/session-handler.ts +111 -75
- package/src/system-message.ts +13 -3
- package/src/tools.ts +13 -39
- package/src/unnest-code-blocks.test.ts +42 -15
- package/src/unnest-code-blocks.ts +4 -2
- package/src/utils.ts +1 -4
- package/src/voice-handler.ts +34 -78
- package/src/voice.ts +11 -19
- package/src/xml.test.ts +1 -1
- package/src/xml.ts +3 -12
package/dist/ai-tool-to-genai.js
CHANGED
|
@@ -150,9 +150,7 @@ export function aiToolToCallableTool(tool, name) {
|
|
|
150
150
|
const parts = [];
|
|
151
151
|
for (const functionCall of functionCalls) {
|
|
152
152
|
// Check if this function call matches our tool
|
|
153
|
-
if (functionCall.name !== toolName &&
|
|
154
|
-
name &&
|
|
155
|
-
functionCall.name !== name) {
|
|
153
|
+
if (functionCall.name !== toolName && name && functionCall.name !== name) {
|
|
156
154
|
continue;
|
|
157
155
|
}
|
|
158
156
|
// Execute the tool if it has an execute function
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Discord channel and category management.
|
|
2
2
|
// Creates and manages Kimaki project channels (text + voice pairs),
|
|
3
3
|
// extracts channel metadata from topic tags, and ensures category structure.
|
|
4
|
-
import { ChannelType
|
|
4
|
+
import { ChannelType } from 'discord.js';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { getDatabase } from './database.js';
|
|
7
7
|
import { extractTagsArrays } from './xml.js';
|
package/dist/cli.js
CHANGED
|
@@ -23,11 +23,18 @@ async function killProcessOnPort(port) {
|
|
|
23
23
|
try {
|
|
24
24
|
if (isWindows) {
|
|
25
25
|
// Windows: find PID using netstat, then kill
|
|
26
|
-
const result = spawnSync('cmd', [
|
|
26
|
+
const result = spawnSync('cmd', [
|
|
27
|
+
'/c',
|
|
28
|
+
`for /f "tokens=5" %a in ('netstat -ano ^| findstr :${port} ^| findstr LISTENING') do @echo %a`,
|
|
29
|
+
], {
|
|
27
30
|
shell: false,
|
|
28
31
|
encoding: 'utf-8',
|
|
29
32
|
});
|
|
30
|
-
const pids = result.stdout
|
|
33
|
+
const pids = result.stdout
|
|
34
|
+
?.trim()
|
|
35
|
+
.split('\n')
|
|
36
|
+
.map((p) => p.trim())
|
|
37
|
+
.filter((p) => /^\d+$/.test(p));
|
|
31
38
|
// Filter out our own PID and take the first (oldest)
|
|
32
39
|
const targetPid = pids?.find((p) => parseInt(p, 10) !== myPid);
|
|
33
40
|
if (targetPid) {
|
|
@@ -42,7 +49,11 @@ async function killProcessOnPort(port) {
|
|
|
42
49
|
shell: false,
|
|
43
50
|
encoding: 'utf-8',
|
|
44
51
|
});
|
|
45
|
-
const pids = result.stdout
|
|
52
|
+
const pids = result.stdout
|
|
53
|
+
?.trim()
|
|
54
|
+
.split('\n')
|
|
55
|
+
.map((p) => p.trim())
|
|
56
|
+
.filter((p) => /^\d+$/.test(p));
|
|
46
57
|
// Filter out our own PID and take the first (oldest)
|
|
47
58
|
const targetPid = pids?.find((p) => parseInt(p, 10) !== myPid);
|
|
48
59
|
if (targetPid) {
|
|
@@ -68,7 +79,9 @@ async function checkSingleInstance() {
|
|
|
68
79
|
cliLogger.log(`Another kimaki instance detected for data dir: ${getDataDir()}`);
|
|
69
80
|
await killProcessOnPort(lockPort);
|
|
70
81
|
// Wait a moment for port to be released
|
|
71
|
-
await new Promise((resolve) => {
|
|
82
|
+
await new Promise((resolve) => {
|
|
83
|
+
setTimeout(resolve, 500);
|
|
84
|
+
});
|
|
72
85
|
}
|
|
73
86
|
}
|
|
74
87
|
catch {
|
|
@@ -91,7 +104,9 @@ async function startLockServer() {
|
|
|
91
104
|
if (err.code === 'EADDRINUSE') {
|
|
92
105
|
cliLogger.log('Port still in use, retrying...');
|
|
93
106
|
await killProcessOnPort(lockPort);
|
|
94
|
-
await new Promise((r) => {
|
|
107
|
+
await new Promise((r) => {
|
|
108
|
+
setTimeout(r, 500);
|
|
109
|
+
});
|
|
95
110
|
// Retry once
|
|
96
111
|
server.listen(lockPort, '127.0.0.1');
|
|
97
112
|
}
|
|
@@ -122,10 +137,7 @@ async function registerCommands(token, appId, userCommands = []) {
|
|
|
122
137
|
.setName('session')
|
|
123
138
|
.setDescription('Start a new OpenCode session')
|
|
124
139
|
.addStringOption((option) => {
|
|
125
|
-
option
|
|
126
|
-
.setName('prompt')
|
|
127
|
-
.setDescription('Prompt content for the session')
|
|
128
|
-
.setRequired(true);
|
|
140
|
+
option.setName('prompt').setDescription('Prompt content for the session').setRequired(true);
|
|
129
141
|
return option;
|
|
130
142
|
})
|
|
131
143
|
.addStringOption((option) => {
|
|
@@ -135,6 +147,13 @@ async function registerCommands(token, appId, userCommands = []) {
|
|
|
135
147
|
.setAutocomplete(true)
|
|
136
148
|
.setMaxLength(6000);
|
|
137
149
|
return option;
|
|
150
|
+
})
|
|
151
|
+
.addStringOption((option) => {
|
|
152
|
+
option
|
|
153
|
+
.setName('agent')
|
|
154
|
+
.setDescription('Agent to use for this session')
|
|
155
|
+
.setAutocomplete(true);
|
|
156
|
+
return option;
|
|
138
157
|
})
|
|
139
158
|
.toJSON(),
|
|
140
159
|
new SlashCommandBuilder()
|
|
@@ -149,14 +168,23 @@ async function registerCommands(token, appId, userCommands = []) {
|
|
|
149
168
|
return option;
|
|
150
169
|
})
|
|
151
170
|
.toJSON(),
|
|
171
|
+
new SlashCommandBuilder()
|
|
172
|
+
.setName('remove-project')
|
|
173
|
+
.setDescription('Remove Discord channels for a project')
|
|
174
|
+
.addStringOption((option) => {
|
|
175
|
+
option
|
|
176
|
+
.setName('project')
|
|
177
|
+
.setDescription('Select a project to remove')
|
|
178
|
+
.setRequired(true)
|
|
179
|
+
.setAutocomplete(true);
|
|
180
|
+
return option;
|
|
181
|
+
})
|
|
182
|
+
.toJSON(),
|
|
152
183
|
new SlashCommandBuilder()
|
|
153
184
|
.setName('create-new-project')
|
|
154
185
|
.setDescription('Create a new project folder, initialize git, and start a session')
|
|
155
186
|
.addStringOption((option) => {
|
|
156
|
-
option
|
|
157
|
-
.setName('name')
|
|
158
|
-
.setDescription('Name for the new project folder')
|
|
159
|
-
.setRequired(true);
|
|
187
|
+
option.setName('name').setDescription('Name for the new project folder').setRequired(true);
|
|
160
188
|
return option;
|
|
161
189
|
})
|
|
162
190
|
.toJSON(),
|
|
@@ -188,10 +216,7 @@ async function registerCommands(token, appId, userCommands = []) {
|
|
|
188
216
|
.setName('queue')
|
|
189
217
|
.setDescription('Queue a message to be sent after the current response finishes')
|
|
190
218
|
.addStringOption((option) => {
|
|
191
|
-
option
|
|
192
|
-
.setName('message')
|
|
193
|
-
.setDescription('The message to queue')
|
|
194
|
-
.setRequired(true);
|
|
219
|
+
option.setName('message').setDescription('The message to queue').setRequired(true);
|
|
195
220
|
return option;
|
|
196
221
|
})
|
|
197
222
|
.toJSON(),
|
|
@@ -218,7 +243,7 @@ async function registerCommands(token, appId, userCommands = []) {
|
|
|
218
243
|
const commandName = `${sanitizedName}-cmd`;
|
|
219
244
|
const description = cmd.description || `Run /${cmd.name} command`;
|
|
220
245
|
commands.push(new SlashCommandBuilder()
|
|
221
|
-
.setName(commandName)
|
|
246
|
+
.setName(commandName.slice(0, 32)) // Discord limits to 32 chars
|
|
222
247
|
.setDescription(description.slice(0, 100)) // Discord limits to 100 chars
|
|
223
248
|
.addStringOption((option) => {
|
|
224
249
|
option
|
|
@@ -470,11 +495,7 @@ async function run({ restart, addChannels }) {
|
|
|
470
495
|
if (kimakiChannels.length > 0) {
|
|
471
496
|
const channelList = kimakiChannels
|
|
472
497
|
.flatMap(({ guild, channels }) => channels.map((ch) => {
|
|
473
|
-
const appInfo = ch.kimakiApp === appId
|
|
474
|
-
? ' (this bot)'
|
|
475
|
-
: ch.kimakiApp
|
|
476
|
-
? ` (app: ${ch.kimakiApp})`
|
|
477
|
-
: '';
|
|
498
|
+
const appInfo = ch.kimakiApp === appId ? ' (this bot)' : ch.kimakiApp ? ` (app: ${ch.kimakiApp})` : '';
|
|
478
499
|
return `#${ch.name} in ${guild.name}: ${ch.kimakiDirectory}${appInfo}`;
|
|
479
500
|
}))
|
|
480
501
|
.join('\n');
|
|
@@ -487,13 +508,19 @@ async function run({ restart, addChannels }) {
|
|
|
487
508
|
s.start('Fetching OpenCode data...');
|
|
488
509
|
// Fetch projects and commands in parallel
|
|
489
510
|
const [projects, allUserCommands] = await Promise.all([
|
|
490
|
-
getClient()
|
|
511
|
+
getClient()
|
|
512
|
+
.project.list({})
|
|
513
|
+
.then((r) => r.data || [])
|
|
514
|
+
.catch((error) => {
|
|
491
515
|
s.stop('Failed to fetch projects');
|
|
492
516
|
cliLogger.error('Error:', error instanceof Error ? error.message : String(error));
|
|
493
517
|
discordClient.destroy();
|
|
494
518
|
process.exit(EXIT_NO_RESTART);
|
|
495
519
|
}),
|
|
496
|
-
getClient()
|
|
520
|
+
getClient()
|
|
521
|
+
.command.list({ query: { directory: currentDir } })
|
|
522
|
+
.then((r) => r.data || [])
|
|
523
|
+
.catch(() => []),
|
|
497
524
|
]);
|
|
498
525
|
s.stop(`Found ${projects.length} OpenCode project(s)`);
|
|
499
526
|
const existingDirs = kimakiChannels.flatMap(({ channels }) => channels
|
|
@@ -512,8 +539,7 @@ async function run({ restart, addChannels }) {
|
|
|
512
539
|
if (availableProjects.length === 0) {
|
|
513
540
|
note('All OpenCode projects already have Discord channels', 'No New Projects');
|
|
514
541
|
}
|
|
515
|
-
if ((!existingDirs?.length && availableProjects.length > 0) ||
|
|
516
|
-
shouldAddChannels) {
|
|
542
|
+
if ((!existingDirs?.length && availableProjects.length > 0) || shouldAddChannels) {
|
|
517
543
|
const selectedProjects = await multiselect({
|
|
518
544
|
message: 'Select projects to create Discord channels for:',
|
|
519
545
|
options: availableProjects.map((project) => ({
|
|
@@ -613,7 +639,8 @@ async function run({ restart, addChannels }) {
|
|
|
613
639
|
.join('\n');
|
|
614
640
|
note(`Your kimaki channels are ready! Click any link below to open in Discord:\n\n${channelLinks}\n\nSend a message in any channel to start using OpenCode!`, '🚀 Ready to Use');
|
|
615
641
|
}
|
|
616
|
-
|
|
642
|
+
note('Leave this process running to keep the bot active.\n\nIf you close this process or restart your machine, run `npx kimaki` again to start the bot.', '⚠️ Keep Running');
|
|
643
|
+
outro('✨ Setup complete! Listening for new messages... do not close this process.');
|
|
617
644
|
}
|
|
618
645
|
cli
|
|
619
646
|
.command('', 'Set up and run the Kimaki Discord bot')
|
|
@@ -695,13 +722,13 @@ cli
|
|
|
695
722
|
const buffer = fs.readFileSync(file);
|
|
696
723
|
const formData = new FormData();
|
|
697
724
|
formData.append('payload_json', JSON.stringify({
|
|
698
|
-
attachments: [{ id: 0, filename: path.basename(file) }]
|
|
725
|
+
attachments: [{ id: 0, filename: path.basename(file) }],
|
|
699
726
|
}));
|
|
700
727
|
formData.append('files[0]', new Blob([buffer]), path.basename(file));
|
|
701
728
|
const response = await fetch(`https://discord.com/api/v10/channels/${threadRow.thread_id}/messages`, {
|
|
702
729
|
method: 'POST',
|
|
703
730
|
headers: {
|
|
704
|
-
|
|
731
|
+
Authorization: `Bot ${botRow.token}`,
|
|
705
732
|
},
|
|
706
733
|
body: formData,
|
|
707
734
|
});
|
|
@@ -725,14 +752,16 @@ const BOT_SESSION_PREFIX = '🤖 **Bot-initiated session**';
|
|
|
725
752
|
cli
|
|
726
753
|
.command('start-session', 'Start a new session in a Discord channel (creates thread, bot handles the rest)')
|
|
727
754
|
.option('-c, --channel <channelId>', 'Discord channel ID')
|
|
755
|
+
.option('-d, --project <path>', 'Project directory (alternative to --channel)')
|
|
728
756
|
.option('-p, --prompt <prompt>', 'Initial prompt for the session')
|
|
729
757
|
.option('-n, --name [name]', 'Thread name (optional, defaults to prompt preview)')
|
|
730
758
|
.option('-a, --app-id [appId]', 'Bot application ID (required if no local database)')
|
|
731
759
|
.action(async (options) => {
|
|
732
760
|
try {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
761
|
+
let { channel: channelId, prompt, name, appId: optionAppId } = options;
|
|
762
|
+
const { project: projectPath } = options;
|
|
763
|
+
if (!channelId && !projectPath) {
|
|
764
|
+
cliLogger.error('Either --channel or --project is required');
|
|
736
765
|
process.exit(EXIT_NO_RESTART);
|
|
737
766
|
}
|
|
738
767
|
if (!prompt) {
|
|
@@ -781,11 +810,85 @@ cli
|
|
|
781
810
|
process.exit(EXIT_NO_RESTART);
|
|
782
811
|
}
|
|
783
812
|
const s = spinner();
|
|
813
|
+
// If --project provided, resolve to channel ID
|
|
814
|
+
if (projectPath) {
|
|
815
|
+
const absolutePath = path.resolve(projectPath);
|
|
816
|
+
if (!fs.existsSync(absolutePath)) {
|
|
817
|
+
cliLogger.error(`Directory does not exist: ${absolutePath}`);
|
|
818
|
+
process.exit(EXIT_NO_RESTART);
|
|
819
|
+
}
|
|
820
|
+
s.start('Looking up channel for project...');
|
|
821
|
+
// Check if channel already exists for this directory
|
|
822
|
+
try {
|
|
823
|
+
const db = getDatabase();
|
|
824
|
+
const existingChannel = db
|
|
825
|
+
.prepare('SELECT channel_id FROM channel_directories WHERE directory = ? AND channel_type = ?')
|
|
826
|
+
.get(absolutePath, 'text');
|
|
827
|
+
if (existingChannel) {
|
|
828
|
+
channelId = existingChannel.channel_id;
|
|
829
|
+
s.message(`Found existing channel: ${channelId}`);
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
// Need to create a new channel
|
|
833
|
+
s.message('Creating new channel...');
|
|
834
|
+
if (!appId) {
|
|
835
|
+
s.stop('Missing app ID');
|
|
836
|
+
cliLogger.error('App ID is required to create channels. Use --app-id or run `kimaki` first.');
|
|
837
|
+
process.exit(EXIT_NO_RESTART);
|
|
838
|
+
}
|
|
839
|
+
const client = await createDiscordClient();
|
|
840
|
+
await new Promise((resolve, reject) => {
|
|
841
|
+
client.once(Events.ClientReady, () => {
|
|
842
|
+
resolve();
|
|
843
|
+
});
|
|
844
|
+
client.once(Events.Error, reject);
|
|
845
|
+
client.login(botToken);
|
|
846
|
+
});
|
|
847
|
+
// Get guild from existing channels or first available
|
|
848
|
+
const guild = await (async () => {
|
|
849
|
+
// Try to find a guild from existing channels
|
|
850
|
+
const existingChannelRow = db
|
|
851
|
+
.prepare('SELECT channel_id FROM channel_directories ORDER BY created_at DESC LIMIT 1')
|
|
852
|
+
.get();
|
|
853
|
+
if (existingChannelRow) {
|
|
854
|
+
try {
|
|
855
|
+
const ch = await client.channels.fetch(existingChannelRow.channel_id);
|
|
856
|
+
if (ch && 'guild' in ch && ch.guild) {
|
|
857
|
+
return ch.guild;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
catch {
|
|
861
|
+
// Channel might be deleted, continue
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
// Fall back to first guild
|
|
865
|
+
const firstGuild = client.guilds.cache.first();
|
|
866
|
+
if (!firstGuild) {
|
|
867
|
+
throw new Error('No guild found. Add the bot to a server first.');
|
|
868
|
+
}
|
|
869
|
+
return firstGuild;
|
|
870
|
+
})();
|
|
871
|
+
const { textChannelId } = await createProjectChannels({
|
|
872
|
+
guild,
|
|
873
|
+
projectDirectory: absolutePath,
|
|
874
|
+
appId,
|
|
875
|
+
botName: client.user?.username,
|
|
876
|
+
});
|
|
877
|
+
channelId = textChannelId;
|
|
878
|
+
s.message(`Created channel: ${channelId}`);
|
|
879
|
+
client.destroy();
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
catch (e) {
|
|
883
|
+
s.stop('Failed to resolve project');
|
|
884
|
+
throw e;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
784
887
|
s.start('Fetching channel info...');
|
|
785
888
|
// Get channel info to extract directory from topic
|
|
786
889
|
const channelResponse = await fetch(`https://discord.com/api/v10/channels/${channelId}`, {
|
|
787
890
|
headers: {
|
|
788
|
-
|
|
891
|
+
Authorization: `Bot ${botToken}`,
|
|
789
892
|
},
|
|
790
893
|
});
|
|
791
894
|
if (!channelResponse.ok) {
|
|
@@ -793,7 +896,7 @@ cli
|
|
|
793
896
|
s.stop('Failed to fetch channel');
|
|
794
897
|
throw new Error(`Discord API error: ${channelResponse.status} - ${error}`);
|
|
795
898
|
}
|
|
796
|
-
const channelData = await channelResponse.json();
|
|
899
|
+
const channelData = (await channelResponse.json());
|
|
797
900
|
if (!channelData.topic) {
|
|
798
901
|
s.stop('Channel has no topic');
|
|
799
902
|
throw new Error(`Channel #${channelData.name} has no topic. It must have a <kimaki.directory> tag.`);
|
|
@@ -819,7 +922,7 @@ cli
|
|
|
819
922
|
const starterMessageResponse = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
|
|
820
923
|
method: 'POST',
|
|
821
924
|
headers: {
|
|
822
|
-
|
|
925
|
+
Authorization: `Bot ${botToken}`,
|
|
823
926
|
'Content-Type': 'application/json',
|
|
824
927
|
},
|
|
825
928
|
body: JSON.stringify({
|
|
@@ -831,14 +934,14 @@ cli
|
|
|
831
934
|
s.stop('Failed to create message');
|
|
832
935
|
throw new Error(`Discord API error: ${starterMessageResponse.status} - ${error}`);
|
|
833
936
|
}
|
|
834
|
-
const starterMessage = await starterMessageResponse.json();
|
|
937
|
+
const starterMessage = (await starterMessageResponse.json());
|
|
835
938
|
s.message('Creating thread...');
|
|
836
939
|
// Create thread from the message
|
|
837
940
|
const threadName = name || (prompt.length > 80 ? prompt.slice(0, 77) + '...' : prompt);
|
|
838
941
|
const threadResponse = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages/${starterMessage.id}/threads`, {
|
|
839
942
|
method: 'POST',
|
|
840
943
|
headers: {
|
|
841
|
-
|
|
944
|
+
Authorization: `Bot ${botToken}`,
|
|
842
945
|
'Content-Type': 'application/json',
|
|
843
946
|
},
|
|
844
947
|
body: JSON.stringify({
|
|
@@ -851,7 +954,7 @@ cli
|
|
|
851
954
|
s.stop('Failed to create thread');
|
|
852
955
|
throw new Error(`Discord API error: ${threadResponse.status} - ${error}`);
|
|
853
956
|
}
|
|
854
|
-
const threadData = await threadResponse.json();
|
|
957
|
+
const threadData = (await threadResponse.json());
|
|
855
958
|
s.stop('Thread created!');
|
|
856
959
|
const threadUrl = `https://discord.com/channels/${channelData.guild_id}/${threadData.id}`;
|
|
857
960
|
note(`Thread: ${threadData.name}\nDirectory: ${projectDirectory}\n\nThe running bot will pick this up and start the session.\n\nURL: ${threadUrl}`, '✅ Thread Created');
|
package/dist/commands/abort.js
CHANGED
|
@@ -6,7 +6,7 @@ import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../
|
|
|
6
6
|
import { abortControllers } from '../session-handler.js';
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
8
8
|
const logger = createLogger('ABORT');
|
|
9
|
-
export async function handleAbortCommand({ command
|
|
9
|
+
export async function handleAbortCommand({ command }) {
|
|
10
10
|
const channel = command.channel;
|
|
11
11
|
if (!channel) {
|
|
12
12
|
await command.reply({
|
|
@@ -7,7 +7,7 @@ import { createProjectChannels } from '../channel-management.js';
|
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
8
8
|
import { abbreviatePath } from '../utils.js';
|
|
9
9
|
const logger = createLogger('ADD-PROJECT');
|
|
10
|
-
export async function handleAddProjectCommand({ command, appId
|
|
10
|
+
export async function handleAddProjectCommand({ command, appId }) {
|
|
11
11
|
await command.deferReply({ ephemeral: false });
|
|
12
12
|
const projectId = command.options.getString('project', true);
|
|
13
13
|
const guild = command.guild;
|
package/dist/commands/agent.js
CHANGED
|
@@ -44,7 +44,9 @@ export async function handleAgentCommand({ interaction, appId, }) {
|
|
|
44
44
|
targetChannelId = channel.id;
|
|
45
45
|
}
|
|
46
46
|
else {
|
|
47
|
-
await interaction.editReply({
|
|
47
|
+
await interaction.editReply({
|
|
48
|
+
content: 'This command can only be used in text channels or threads',
|
|
49
|
+
});
|
|
48
50
|
return;
|
|
49
51
|
}
|
|
50
52
|
if (channelAppId && channelAppId !== appId) {
|
|
@@ -52,7 +54,9 @@ export async function handleAgentCommand({ interaction, appId, }) {
|
|
|
52
54
|
return;
|
|
53
55
|
}
|
|
54
56
|
if (!projectDirectory) {
|
|
55
|
-
await interaction.editReply({
|
|
57
|
+
await interaction.editReply({
|
|
58
|
+
content: 'This channel is not configured with a project directory',
|
|
59
|
+
});
|
|
56
60
|
return;
|
|
57
61
|
}
|
|
58
62
|
try {
|
|
@@ -44,9 +44,10 @@ export async function showAskUserQuestionDropdowns({ thread, sessionId, director
|
|
|
44
44
|
description: 'Provide a custom answer in chat',
|
|
45
45
|
},
|
|
46
46
|
];
|
|
47
|
+
const placeholder = options.find((x) => x.label)?.label || 'Select an option';
|
|
47
48
|
const selectMenu = new StringSelectMenuBuilder()
|
|
48
49
|
.setCustomId(`ask_question:${contextHash}:${i}`)
|
|
49
|
-
.setPlaceholder(
|
|
50
|
+
.setPlaceholder(placeholder)
|
|
50
51
|
.addOptions(options);
|
|
51
52
|
// Enable multi-select if the question supports it
|
|
52
53
|
if (q.multiple) {
|
package/dist/commands/fork.js
CHANGED
|
@@ -84,8 +84,7 @@ export async function handleForkCommand(interaction) {
|
|
|
84
84
|
.setCustomId(`fork_select:${sessionId}:${encodedDir}`)
|
|
85
85
|
.setPlaceholder('Select a message to fork from')
|
|
86
86
|
.addOptions(options);
|
|
87
|
-
const actionRow = new ActionRowBuilder()
|
|
88
|
-
.addComponents(selectMenu);
|
|
87
|
+
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
89
88
|
await interaction.editReply({
|
|
90
89
|
content: '**Fork Session**\nSelect the user message to fork from. The forked session will continue as if you had not sent that message:',
|
|
91
90
|
components: [actionRow],
|
|
@@ -133,11 +132,12 @@ export async function handleForkSelectMenu(interaction) {
|
|
|
133
132
|
}
|
|
134
133
|
const forkedSession = forkResponse.data;
|
|
135
134
|
const parentChannel = interaction.channel;
|
|
136
|
-
if (!parentChannel ||
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
if (!parentChannel ||
|
|
136
|
+
![
|
|
137
|
+
ChannelType.PublicThread,
|
|
138
|
+
ChannelType.PrivateThread,
|
|
139
|
+
ChannelType.AnnouncementThread,
|
|
140
|
+
].includes(parentChannel.type)) {
|
|
141
141
|
await interaction.editReply('Could not access parent channel');
|
|
142
142
|
return;
|
|
143
143
|
}
|
package/dist/commands/queue.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolveTextChannel, getKimakiMetadata, sendThreadMessage, SILENT_MESSAG
|
|
|
5
5
|
import { handleOpencodeSession, abortControllers, addToQueue, getQueueLength, clearQueue, } from '../session-handler.js';
|
|
6
6
|
import { createLogger } from '../logger.js';
|
|
7
7
|
const logger = createLogger('QUEUE');
|
|
8
|
-
export async function handleQueueCommand({ command
|
|
8
|
+
export async function handleQueueCommand({ command }) {
|
|
9
9
|
const message = command.options.getString('message', true);
|
|
10
10
|
const channel = command.channel;
|
|
11
11
|
if (!channel) {
|
|
@@ -88,7 +88,7 @@ export async function handleQueueCommand({ command, }) {
|
|
|
88
88
|
});
|
|
89
89
|
logger.log(`[QUEUE] User ${command.user.displayName} queued message in thread ${channel.id}`);
|
|
90
90
|
}
|
|
91
|
-
export async function handleClearQueueCommand({ command
|
|
91
|
+
export async function handleClearQueueCommand({ command }) {
|
|
92
92
|
const channel = command.channel;
|
|
93
93
|
if (!channel) {
|
|
94
94
|
await command.reply({
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// /remove-project command - Remove Discord channels for a project.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getDatabase } from '../database.js';
|
|
4
|
+
import { createLogger } from '../logger.js';
|
|
5
|
+
import { abbreviatePath } from '../utils.js';
|
|
6
|
+
const logger = createLogger('REMOVE-PROJECT');
|
|
7
|
+
export async function handleRemoveProjectCommand({ command, appId }) {
|
|
8
|
+
await command.deferReply({ ephemeral: false });
|
|
9
|
+
const directory = command.options.getString('project', true);
|
|
10
|
+
const guild = command.guild;
|
|
11
|
+
if (!guild) {
|
|
12
|
+
await command.editReply('This command can only be used in a guild');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const db = getDatabase();
|
|
17
|
+
// Get channel IDs for this directory
|
|
18
|
+
const channels = db
|
|
19
|
+
.prepare('SELECT channel_id, channel_type FROM channel_directories WHERE directory = ?')
|
|
20
|
+
.all(directory);
|
|
21
|
+
if (channels.length === 0) {
|
|
22
|
+
await command.editReply(`No channels found for directory: \`${directory}\``);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const deletedChannels = [];
|
|
26
|
+
const failedChannels = [];
|
|
27
|
+
for (const { channel_id, channel_type } of channels) {
|
|
28
|
+
try {
|
|
29
|
+
const channel = await guild.channels.fetch(channel_id).catch(() => null);
|
|
30
|
+
if (channel) {
|
|
31
|
+
await channel.delete(`Removed by /remove-project command`);
|
|
32
|
+
deletedChannels.push(`${channel_type}: ${channel_id}`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Channel doesn't exist in this guild or was already deleted
|
|
36
|
+
deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
logger.error(`Failed to delete channel ${channel_id}:`, error);
|
|
41
|
+
failedChannels.push(`${channel_type}: ${channel_id}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Remove from database
|
|
45
|
+
db.prepare('DELETE FROM channel_directories WHERE directory = ?').run(directory);
|
|
46
|
+
const projectName = path.basename(directory);
|
|
47
|
+
let message = `Removed project **${projectName}**\n`;
|
|
48
|
+
message += `Directory: \`${directory}\`\n\n`;
|
|
49
|
+
if (deletedChannels.length > 0) {
|
|
50
|
+
message += `Deleted channels:\n${deletedChannels.map((c) => `- ${c}`).join('\n')}`;
|
|
51
|
+
}
|
|
52
|
+
if (failedChannels.length > 0) {
|
|
53
|
+
message += `\n\nFailed to delete (may be in another server):\n${failedChannels.map((c) => `- ${c}`).join('\n')}`;
|
|
54
|
+
}
|
|
55
|
+
await command.editReply(message);
|
|
56
|
+
logger.log(`Removed project ${projectName} at ${directory}`);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
logger.error('[REMOVE-PROJECT] Error:', error);
|
|
60
|
+
await command.editReply(`Failed to remove project: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function handleRemoveProjectAutocomplete({ interaction, appId, }) {
|
|
64
|
+
const focusedValue = interaction.options.getFocused();
|
|
65
|
+
const guild = interaction.guild;
|
|
66
|
+
if (!guild) {
|
|
67
|
+
await interaction.respond([]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const db = getDatabase();
|
|
72
|
+
// Get all directories with channels
|
|
73
|
+
const allChannels = db
|
|
74
|
+
.prepare('SELECT DISTINCT directory, channel_id FROM channel_directories WHERE channel_type = ?')
|
|
75
|
+
.all('text');
|
|
76
|
+
// Filter to only channels that exist in this guild
|
|
77
|
+
const projectsInGuild = [];
|
|
78
|
+
for (const { directory, channel_id } of allChannels) {
|
|
79
|
+
try {
|
|
80
|
+
const channel = await guild.channels.fetch(channel_id).catch(() => null);
|
|
81
|
+
if (channel) {
|
|
82
|
+
projectsInGuild.push({ directory, channelId: channel_id });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Channel not in this guild, skip
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const projects = projectsInGuild
|
|
90
|
+
.filter(({ directory }) => {
|
|
91
|
+
const baseName = path.basename(directory);
|
|
92
|
+
const searchText = `${baseName} ${directory}`.toLowerCase();
|
|
93
|
+
return searchText.includes(focusedValue.toLowerCase());
|
|
94
|
+
})
|
|
95
|
+
.slice(0, 25)
|
|
96
|
+
.map(({ directory }) => {
|
|
97
|
+
const name = `${path.basename(directory)} (${abbreviatePath(directory)})`;
|
|
98
|
+
return {
|
|
99
|
+
name: name.length > 100 ? name.slice(0, 99) + '...' : name,
|
|
100
|
+
value: directory,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
await interaction.respond(projects);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
logger.error('[AUTOCOMPLETE] Error fetching projects:', error);
|
|
107
|
+
await interaction.respond([]);
|
|
108
|
+
}
|
|
109
|
+
}
|
package/dist/commands/resume.js
CHANGED
|
@@ -3,12 +3,12 @@ import { ChannelType, ThreadAutoArchiveDuration, } from 'discord.js';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { getDatabase } from '../database.js';
|
|
5
5
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
|
-
import { sendThreadMessage, resolveTextChannel, getKimakiMetadata
|
|
6
|
+
import { sendThreadMessage, resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
7
7
|
import { extractTagsArrays } from '../xml.js';
|
|
8
8
|
import { collectLastAssistantParts } from '../message-formatting.js';
|
|
9
9
|
import { createLogger } from '../logger.js';
|
|
10
10
|
const logger = createLogger('RESUME');
|
|
11
|
-
export async function handleResumeCommand({ command, appId
|
|
11
|
+
export async function handleResumeCommand({ command, appId }) {
|
|
12
12
|
await command.deferReply({ ephemeral: false });
|
|
13
13
|
const sessionId = command.options.getString('session', true);
|
|
14
14
|
const channel = command.channel;
|
|
@@ -116,9 +116,7 @@ export async function handleResumeAutocomplete({ interaction, appId, }) {
|
|
|
116
116
|
await interaction.respond([]);
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
|
-
const existingSessionIds = new Set(getDatabase()
|
|
120
|
-
.prepare('SELECT session_id FROM thread_sessions')
|
|
121
|
-
.all().map((row) => row.session_id));
|
|
119
|
+
const existingSessionIds = new Set(getDatabase().prepare('SELECT session_id FROM thread_sessions').all().map((row) => row.session_id));
|
|
122
120
|
const sessions = sessionsResponse.data
|
|
123
121
|
.filter((session) => !existingSessionIds.has(session.id))
|
|
124
122
|
.filter((session) => session.title.toLowerCase().includes(focusedValue.toLowerCase()))
|