kimaki 0.1.5 ā 0.2.1
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 +63 -12
- package/dist/discordBot.js +151 -8
- package/package.json +1 -1
- package/src/cli.ts +80 -10
- package/src/discordBot.ts +198 -9
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Events, ChannelType, REST, Routes, SlashCommandBuilder, } from 'discord
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import { createLogger } from './logger.js';
|
|
10
|
+
import { spawnSync, execSync } from 'node:child_process';
|
|
10
11
|
const cliLogger = createLogger('CLI');
|
|
11
12
|
const cli = cac('kimaki');
|
|
12
13
|
process.title = 'kimaki';
|
|
@@ -75,6 +76,56 @@ async function ensureKimakiCategory(guild) {
|
|
|
75
76
|
async function run({ restart, addChannels }) {
|
|
76
77
|
const forceSetup = Boolean(restart);
|
|
77
78
|
intro('š¤ Discord Bot Setup');
|
|
79
|
+
// Step 0: Check if OpenCode CLI is available
|
|
80
|
+
const opencodeCheck = spawnSync('which', ['opencode'], { shell: true });
|
|
81
|
+
if (opencodeCheck.status !== 0) {
|
|
82
|
+
note('OpenCode CLI is required but not found in your PATH.', 'ā ļø OpenCode Not Found');
|
|
83
|
+
const shouldInstall = await confirm({
|
|
84
|
+
message: 'Would you like to install OpenCode right now?',
|
|
85
|
+
});
|
|
86
|
+
if (isCancel(shouldInstall) || !shouldInstall) {
|
|
87
|
+
cancel('OpenCode CLI is required to run this bot');
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
const s = spinner();
|
|
91
|
+
s.start('Installing OpenCode CLI...');
|
|
92
|
+
try {
|
|
93
|
+
execSync('curl -fsSL https://opencode.ai/install | bash', {
|
|
94
|
+
stdio: 'inherit',
|
|
95
|
+
shell: '/bin/bash',
|
|
96
|
+
});
|
|
97
|
+
s.stop('OpenCode CLI installed successfully!');
|
|
98
|
+
// The install script adds opencode to PATH via shell configuration
|
|
99
|
+
// For the current process, we need to check common installation paths
|
|
100
|
+
const possiblePaths = [
|
|
101
|
+
`${process.env.HOME}/.local/bin/opencode`,
|
|
102
|
+
`${process.env.HOME}/.opencode/bin/opencode`,
|
|
103
|
+
'/usr/local/bin/opencode',
|
|
104
|
+
'/opt/opencode/bin/opencode',
|
|
105
|
+
];
|
|
106
|
+
const installedPath = possiblePaths.find((p) => {
|
|
107
|
+
try {
|
|
108
|
+
fs.accessSync(p, fs.constants.F_OK);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
if (!installedPath) {
|
|
116
|
+
note('OpenCode was installed but may not be available in this session.\n' +
|
|
117
|
+
'Please restart your terminal and run this command again.', 'ā ļø Restart Required');
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
// For subsequent spawn calls in this session, we can use the full path
|
|
121
|
+
process.env.OPENCODE_PATH = installedPath;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
s.stop('Failed to install OpenCode CLI');
|
|
125
|
+
cliLogger.error('Installation error:', error instanceof Error ? error.message : String(error));
|
|
126
|
+
process.exit(EXIT_NO_RESTART);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
78
129
|
const db = getDatabase();
|
|
79
130
|
let appId;
|
|
80
131
|
let token;
|
|
@@ -231,7 +282,7 @@ async function run({ restart, addChannels }) {
|
|
|
231
282
|
s.start('Fetching OpenCode projects...');
|
|
232
283
|
let projects = [];
|
|
233
284
|
try {
|
|
234
|
-
const projectsResponse = await getClient().project.list();
|
|
285
|
+
const projectsResponse = await getClient().project.list({});
|
|
235
286
|
if (!projectsResponse.data) {
|
|
236
287
|
throw new Error('Failed to fetch projects');
|
|
237
288
|
}
|
|
@@ -270,23 +321,23 @@ async function run({ restart, addChannels }) {
|
|
|
270
321
|
}
|
|
271
322
|
if (guilds.length === 1) {
|
|
272
323
|
targetGuild = guilds[0];
|
|
324
|
+
note(`Using server: ${targetGuild.name}`, 'Server Selected');
|
|
273
325
|
}
|
|
274
326
|
else {
|
|
275
|
-
const
|
|
276
|
-
message: '
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
},
|
|
327
|
+
const guildSelection = await multiselect({
|
|
328
|
+
message: 'Select a Discord server to create channels in:',
|
|
329
|
+
options: guilds.map((guild) => ({
|
|
330
|
+
value: guild.id,
|
|
331
|
+
label: `${guild.name} (${guild.memberCount} members)`,
|
|
332
|
+
})),
|
|
333
|
+
required: true,
|
|
334
|
+
maxItems: 1,
|
|
284
335
|
});
|
|
285
|
-
if (isCancel(
|
|
336
|
+
if (isCancel(guildSelection)) {
|
|
286
337
|
cancel('Setup cancelled');
|
|
287
338
|
process.exit(0);
|
|
288
339
|
}
|
|
289
|
-
targetGuild = guilds.find((g) => g.id ===
|
|
340
|
+
targetGuild = guilds.find((g) => g.id === guildSelection[0]);
|
|
290
341
|
}
|
|
291
342
|
s.start('Creating Discord channels...');
|
|
292
343
|
for (const projectId of selectedProjects) {
|
package/dist/discordBot.js
CHANGED
|
@@ -281,7 +281,7 @@ async function setupVoiceHandling({ connection, guildId, channelId, appId, }) {
|
|
|
281
281
|
});
|
|
282
282
|
});
|
|
283
283
|
}
|
|
284
|
-
|
|
284
|
+
function frameMono16khz() {
|
|
285
285
|
// Hardcoded: 16 kHz, mono, 16-bit PCM, 20 ms -> 320 samples -> 640 bytes
|
|
286
286
|
const FRAME_BYTES = (100 /*ms*/ * 16_000 /*Hz*/ * 1 /*channels*/ * 2) /*bytes per sample*/ /
|
|
287
287
|
1000;
|
|
@@ -608,7 +608,8 @@ export async function initializeOpencodeForDirectory(directory) {
|
|
|
608
608
|
// console.log(
|
|
609
609
|
// `[OPENCODE] Starting new server on port ${port} for directory: ${directory}`,
|
|
610
610
|
// )
|
|
611
|
-
const
|
|
611
|
+
const opencodeCommand = process.env.OPENCODE_PATH || 'opencode';
|
|
612
|
+
const serverProcess = spawn(opencodeCommand, ['serve', '--port', port.toString()], {
|
|
612
613
|
stdio: 'pipe',
|
|
613
614
|
detached: false,
|
|
614
615
|
cwd: directory,
|
|
@@ -648,7 +649,13 @@ export async function initializeOpencodeForDirectory(directory) {
|
|
|
648
649
|
}
|
|
649
650
|
});
|
|
650
651
|
await waitForServer(port);
|
|
651
|
-
const client = createOpencodeClient({
|
|
652
|
+
const client = createOpencodeClient({
|
|
653
|
+
baseUrl: `http://localhost:${port}`,
|
|
654
|
+
fetch: (request) => fetch(request, {
|
|
655
|
+
// @ts-ignore
|
|
656
|
+
timeout: false,
|
|
657
|
+
}),
|
|
658
|
+
});
|
|
652
659
|
opencodeServers.set(directory, {
|
|
653
660
|
process: serverProcess,
|
|
654
661
|
client,
|
|
@@ -730,7 +737,7 @@ function formatPart(part) {
|
|
|
730
737
|
const icon = part.state.status === 'completed'
|
|
731
738
|
? 'ā¼ļø'
|
|
732
739
|
: part.state.status === 'error'
|
|
733
|
-
? '
|
|
740
|
+
? '⨯'
|
|
734
741
|
: '';
|
|
735
742
|
const title = `${icon} ${part.tool} ${toolTitle}`;
|
|
736
743
|
let text = title;
|
|
@@ -1400,11 +1407,147 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
1400
1407
|
await interaction.respond([]);
|
|
1401
1408
|
}
|
|
1402
1409
|
}
|
|
1410
|
+
else if (interaction.commandName === 'session') {
|
|
1411
|
+
const focusedOption = interaction.options.getFocused(true);
|
|
1412
|
+
if (focusedOption.name === 'files') {
|
|
1413
|
+
const focusedValue = focusedOption.value;
|
|
1414
|
+
// Split by comma to handle multiple files
|
|
1415
|
+
const parts = focusedValue.split(',');
|
|
1416
|
+
const previousFiles = parts
|
|
1417
|
+
.slice(0, -1)
|
|
1418
|
+
.map((f) => f.trim())
|
|
1419
|
+
.filter((f) => f);
|
|
1420
|
+
const currentQuery = (parts[parts.length - 1] || '').trim();
|
|
1421
|
+
// Get the channel's project directory from its topic
|
|
1422
|
+
let projectDirectory;
|
|
1423
|
+
if (interaction.channel &&
|
|
1424
|
+
interaction.channel.type === ChannelType.GuildText) {
|
|
1425
|
+
const textChannel = resolveTextChannel(interaction.channel);
|
|
1426
|
+
if (textChannel) {
|
|
1427
|
+
const { projectDirectory: directory, channelAppId } = getKimakiMetadata(textChannel);
|
|
1428
|
+
if (channelAppId && channelAppId !== currentAppId) {
|
|
1429
|
+
await interaction.respond([]);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
projectDirectory = directory;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (!projectDirectory) {
|
|
1436
|
+
await interaction.respond([]);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
// Get OpenCode client for this directory
|
|
1441
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
1442
|
+
// Use find.files to search for files based on current query
|
|
1443
|
+
const response = await getClient().find.files({
|
|
1444
|
+
query: {
|
|
1445
|
+
query: currentQuery || '',
|
|
1446
|
+
},
|
|
1447
|
+
});
|
|
1448
|
+
// Get file paths from the response
|
|
1449
|
+
const files = response.data || [];
|
|
1450
|
+
// Build the prefix with previous files
|
|
1451
|
+
const prefix = previousFiles.length > 0
|
|
1452
|
+
? previousFiles.join(', ') + ', '
|
|
1453
|
+
: '';
|
|
1454
|
+
// Map to Discord autocomplete format
|
|
1455
|
+
const choices = files
|
|
1456
|
+
.slice(0, 25) // Discord limit
|
|
1457
|
+
.map((file) => {
|
|
1458
|
+
const fullValue = prefix + file;
|
|
1459
|
+
// Get all basenames for display
|
|
1460
|
+
const allFiles = [...previousFiles, file];
|
|
1461
|
+
const allBasenames = allFiles.map((f) => f.split('/').pop() || f);
|
|
1462
|
+
let displayName = allBasenames.join(', ');
|
|
1463
|
+
// Truncate if too long
|
|
1464
|
+
if (displayName.length > 100) {
|
|
1465
|
+
displayName = 'ā¦' + displayName.slice(-97);
|
|
1466
|
+
}
|
|
1467
|
+
return {
|
|
1468
|
+
name: displayName,
|
|
1469
|
+
value: fullValue,
|
|
1470
|
+
};
|
|
1471
|
+
});
|
|
1472
|
+
await interaction.respond(choices);
|
|
1473
|
+
}
|
|
1474
|
+
catch (error) {
|
|
1475
|
+
voiceLogger.error('[AUTOCOMPLETE] Error fetching files:', error);
|
|
1476
|
+
await interaction.respond([]);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1403
1480
|
}
|
|
1404
1481
|
// Handle slash commands
|
|
1405
1482
|
if (interaction.isChatInputCommand()) {
|
|
1406
1483
|
const command = interaction;
|
|
1407
|
-
if (command.commandName === '
|
|
1484
|
+
if (command.commandName === 'session') {
|
|
1485
|
+
await command.deferReply({ ephemeral: false });
|
|
1486
|
+
const prompt = command.options.getString('prompt', true);
|
|
1487
|
+
const filesString = command.options.getString('files') || '';
|
|
1488
|
+
const channel = command.channel;
|
|
1489
|
+
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
1490
|
+
await command.editReply('This command can only be used in text channels');
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
const textChannel = channel;
|
|
1494
|
+
// Get project directory from channel topic
|
|
1495
|
+
let projectDirectory;
|
|
1496
|
+
let channelAppId;
|
|
1497
|
+
if (textChannel.topic) {
|
|
1498
|
+
const extracted = extractTagsArrays({
|
|
1499
|
+
xml: textChannel.topic,
|
|
1500
|
+
tags: ['kimaki.directory', 'kimaki.app'],
|
|
1501
|
+
});
|
|
1502
|
+
projectDirectory = extracted['kimaki.directory']?.[0]?.trim();
|
|
1503
|
+
channelAppId = extracted['kimaki.app']?.[0]?.trim();
|
|
1504
|
+
}
|
|
1505
|
+
// Check if this channel belongs to current bot instance
|
|
1506
|
+
if (channelAppId && channelAppId !== currentAppId) {
|
|
1507
|
+
await command.editReply('This channel is not configured for this bot');
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
if (!projectDirectory) {
|
|
1511
|
+
await command.editReply('This channel is not configured with a project directory');
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (!fs.existsSync(projectDirectory)) {
|
|
1515
|
+
await command.editReply(`Directory does not exist: ${projectDirectory}`);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
try {
|
|
1519
|
+
// Initialize OpenCode client for the directory
|
|
1520
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
1521
|
+
// Process file mentions - split by comma only
|
|
1522
|
+
const files = filesString
|
|
1523
|
+
.split(',')
|
|
1524
|
+
.map((f) => f.trim())
|
|
1525
|
+
.filter((f) => f);
|
|
1526
|
+
// Build the full prompt with file mentions
|
|
1527
|
+
let fullPrompt = prompt;
|
|
1528
|
+
if (files.length > 0) {
|
|
1529
|
+
fullPrompt = `${prompt}\n\n@${files.join(' @')}`;
|
|
1530
|
+
}
|
|
1531
|
+
// Send a message first, then create thread from it
|
|
1532
|
+
const starterMessage = await textChannel.send({
|
|
1533
|
+
content: `š **Starting OpenCode session**\nš ${prompt.slice(0, 200)}${prompt.length > 200 ? 'ā¦' : ''}${files.length > 0 ? `\nš Files: ${files.join(', ')}` : ''}`,
|
|
1534
|
+
});
|
|
1535
|
+
// Create thread from the message
|
|
1536
|
+
const thread = await starterMessage.startThread({
|
|
1537
|
+
name: prompt.slice(0, 100),
|
|
1538
|
+
autoArchiveDuration: 1440, // 24 hours
|
|
1539
|
+
reason: 'OpenCode session',
|
|
1540
|
+
});
|
|
1541
|
+
await command.editReply(`Created new session in ${thread.toString()}`);
|
|
1542
|
+
// Start the OpenCode session
|
|
1543
|
+
await handleOpencodeSession(fullPrompt, thread, projectDirectory);
|
|
1544
|
+
}
|
|
1545
|
+
catch (error) {
|
|
1546
|
+
voiceLogger.error('[SESSION] Error:', error);
|
|
1547
|
+
await command.editReply(`Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
else if (command.commandName === 'resume') {
|
|
1408
1551
|
await command.deferReply({ ephemeral: false });
|
|
1409
1552
|
const sessionId = command.options.getString('session', true);
|
|
1410
1553
|
const channel = command.channel;
|
|
@@ -1476,11 +1619,11 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
1476
1619
|
for (const message of messages) {
|
|
1477
1620
|
if (message.info.role === 'user') {
|
|
1478
1621
|
// Render user messages
|
|
1479
|
-
const userParts = message.parts.filter((p) => p.type === 'text');
|
|
1622
|
+
const userParts = message.parts.filter((p) => p.type === 'text' && !p.synthetic);
|
|
1480
1623
|
const userTexts = userParts
|
|
1481
1624
|
.map((p) => {
|
|
1482
|
-
if (
|
|
1483
|
-
return
|
|
1625
|
+
if (p.type === 'text') {
|
|
1626
|
+
return p.text;
|
|
1484
1627
|
}
|
|
1485
1628
|
return '';
|
|
1486
1629
|
})
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
import path from 'node:path'
|
|
36
36
|
import fs from 'node:fs'
|
|
37
37
|
import { createLogger } from './logger.js'
|
|
38
|
+
import { spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
|
|
38
39
|
|
|
39
40
|
const cliLogger = createLogger('CLI')
|
|
40
41
|
const cli = cac('kimaki')
|
|
@@ -142,6 +143,73 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
142
143
|
|
|
143
144
|
intro('š¤ Discord Bot Setup')
|
|
144
145
|
|
|
146
|
+
// Step 0: Check if OpenCode CLI is available
|
|
147
|
+
const opencodeCheck = spawnSync('which', ['opencode'], { shell: true })
|
|
148
|
+
|
|
149
|
+
if (opencodeCheck.status !== 0) {
|
|
150
|
+
note(
|
|
151
|
+
'OpenCode CLI is required but not found in your PATH.',
|
|
152
|
+
'ā ļø OpenCode Not Found',
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const shouldInstall = await confirm({
|
|
156
|
+
message: 'Would you like to install OpenCode right now?',
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (isCancel(shouldInstall) || !shouldInstall) {
|
|
160
|
+
cancel('OpenCode CLI is required to run this bot')
|
|
161
|
+
process.exit(0)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const s = spinner()
|
|
165
|
+
s.start('Installing OpenCode CLI...')
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
execSync('curl -fsSL https://opencode.ai/install | bash', {
|
|
169
|
+
stdio: 'inherit',
|
|
170
|
+
shell: '/bin/bash',
|
|
171
|
+
})
|
|
172
|
+
s.stop('OpenCode CLI installed successfully!')
|
|
173
|
+
|
|
174
|
+
// The install script adds opencode to PATH via shell configuration
|
|
175
|
+
// For the current process, we need to check common installation paths
|
|
176
|
+
const possiblePaths = [
|
|
177
|
+
`${process.env.HOME}/.local/bin/opencode`,
|
|
178
|
+
`${process.env.HOME}/.opencode/bin/opencode`,
|
|
179
|
+
'/usr/local/bin/opencode',
|
|
180
|
+
'/opt/opencode/bin/opencode',
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
const installedPath = possiblePaths.find((p) => {
|
|
184
|
+
try {
|
|
185
|
+
fs.accessSync(p, fs.constants.F_OK)
|
|
186
|
+
return true
|
|
187
|
+
} catch {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
if (!installedPath) {
|
|
193
|
+
note(
|
|
194
|
+
'OpenCode was installed but may not be available in this session.\n' +
|
|
195
|
+
'Please restart your terminal and run this command again.',
|
|
196
|
+
'ā ļø Restart Required',
|
|
197
|
+
)
|
|
198
|
+
process.exit(0)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// For subsequent spawn calls in this session, we can use the full path
|
|
202
|
+
process.env.OPENCODE_PATH = installedPath
|
|
203
|
+
} catch (error) {
|
|
204
|
+
s.stop('Failed to install OpenCode CLI')
|
|
205
|
+
cliLogger.error(
|
|
206
|
+
'Installation error:',
|
|
207
|
+
error instanceof Error ? error.message : String(error),
|
|
208
|
+
)
|
|
209
|
+
process.exit(EXIT_NO_RESTART)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
145
213
|
const db = getDatabase()
|
|
146
214
|
let appId: string
|
|
147
215
|
let token: string
|
|
@@ -377,7 +445,7 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
377
445
|
let projects: Project[] = []
|
|
378
446
|
|
|
379
447
|
try {
|
|
380
|
-
const projectsResponse = await getClient().project.list()
|
|
448
|
+
const projectsResponse = await getClient().project.list({})
|
|
381
449
|
if (!projectsResponse.data) {
|
|
382
450
|
throw new Error('Failed to fetch projects')
|
|
383
451
|
}
|
|
@@ -436,22 +504,24 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
436
504
|
|
|
437
505
|
if (guilds.length === 1) {
|
|
438
506
|
targetGuild = guilds[0]!
|
|
507
|
+
note(`Using server: ${targetGuild.name}`, 'Server Selected')
|
|
439
508
|
} else {
|
|
440
|
-
const
|
|
441
|
-
message: '
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
509
|
+
const guildSelection = await multiselect({
|
|
510
|
+
message: 'Select a Discord server to create channels in:',
|
|
511
|
+
options: guilds.map((guild) => ({
|
|
512
|
+
value: guild.id,
|
|
513
|
+
label: `${guild.name} (${guild.memberCount} members)`,
|
|
514
|
+
})),
|
|
515
|
+
required: true,
|
|
516
|
+
maxItems: 1,
|
|
447
517
|
})
|
|
448
518
|
|
|
449
|
-
if (isCancel(
|
|
519
|
+
if (isCancel(guildSelection)) {
|
|
450
520
|
cancel('Setup cancelled')
|
|
451
521
|
process.exit(0)
|
|
452
522
|
}
|
|
453
523
|
|
|
454
|
-
targetGuild = guilds.find((g) => g.id ===
|
|
524
|
+
targetGuild = guilds.find((g) => g.id === guildSelection[0])!
|
|
455
525
|
}
|
|
456
526
|
|
|
457
527
|
s.start('Creating Discord channels...')
|
package/src/discordBot.ts
CHANGED
|
@@ -403,7 +403,7 @@ async function setupVoiceHandling({
|
|
|
403
403
|
})
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
-
|
|
406
|
+
function frameMono16khz(): Transform {
|
|
407
407
|
// Hardcoded: 16 kHz, mono, 16-bit PCM, 20 ms -> 320 samples -> 640 bytes
|
|
408
408
|
const FRAME_BYTES =
|
|
409
409
|
(100 /*ms*/ * 16_000 /*Hz*/ * 1 /*channels*/ * 2) /*bytes per sample*/ /
|
|
@@ -822,8 +822,10 @@ export async function initializeOpencodeForDirectory(directory: string) {
|
|
|
822
822
|
// `[OPENCODE] Starting new server on port ${port} for directory: ${directory}`,
|
|
823
823
|
// )
|
|
824
824
|
|
|
825
|
+
const opencodeCommand = process.env.OPENCODE_PATH || 'opencode'
|
|
826
|
+
|
|
825
827
|
const serverProcess = spawn(
|
|
826
|
-
|
|
828
|
+
opencodeCommand,
|
|
827
829
|
['serve', '--port', port.toString()],
|
|
828
830
|
{
|
|
829
831
|
stdio: 'pipe',
|
|
@@ -876,7 +878,15 @@ export async function initializeOpencodeForDirectory(directory: string) {
|
|
|
876
878
|
})
|
|
877
879
|
|
|
878
880
|
await waitForServer(port)
|
|
879
|
-
|
|
881
|
+
|
|
882
|
+
const client = createOpencodeClient({
|
|
883
|
+
baseUrl: `http://localhost:${port}`,
|
|
884
|
+
fetch: (request: Request) =>
|
|
885
|
+
fetch(request, {
|
|
886
|
+
// @ts-ignore
|
|
887
|
+
timeout: false,
|
|
888
|
+
}),
|
|
889
|
+
})
|
|
880
890
|
|
|
881
891
|
opencodeServers.set(directory, {
|
|
882
892
|
process: serverProcess,
|
|
@@ -970,7 +980,7 @@ function formatPart(part: Part): string {
|
|
|
970
980
|
part.state.status === 'completed'
|
|
971
981
|
? 'ā¼ļø'
|
|
972
982
|
: part.state.status === 'error'
|
|
973
|
-
? '
|
|
983
|
+
? '⨯'
|
|
974
984
|
: ''
|
|
975
985
|
const title = `${icon} ${part.tool} ${toolTitle}`
|
|
976
986
|
|
|
@@ -1547,7 +1557,6 @@ export async function startDiscordBot({
|
|
|
1547
1557
|
discordLogger.log(`Bot Application ID (provided): ${currentAppId}`)
|
|
1548
1558
|
}
|
|
1549
1559
|
|
|
1550
|
-
|
|
1551
1560
|
// List all guilds and channels that belong to this bot
|
|
1552
1561
|
for (const guild of c.guilds.cache.values()) {
|
|
1553
1562
|
discordLogger.log(`${guild.name} (${guild.id})`)
|
|
@@ -1868,6 +1877,93 @@ export async function startDiscordBot({
|
|
|
1868
1877
|
)
|
|
1869
1878
|
await interaction.respond([])
|
|
1870
1879
|
}
|
|
1880
|
+
} else if (interaction.commandName === 'session') {
|
|
1881
|
+
const focusedOption = interaction.options.getFocused(true)
|
|
1882
|
+
|
|
1883
|
+
if (focusedOption.name === 'files') {
|
|
1884
|
+
const focusedValue = focusedOption.value
|
|
1885
|
+
|
|
1886
|
+
// Split by comma to handle multiple files
|
|
1887
|
+
const parts = focusedValue.split(',')
|
|
1888
|
+
const previousFiles = parts
|
|
1889
|
+
.slice(0, -1)
|
|
1890
|
+
.map((f) => f.trim())
|
|
1891
|
+
.filter((f) => f)
|
|
1892
|
+
const currentQuery = (parts[parts.length - 1] || '').trim()
|
|
1893
|
+
|
|
1894
|
+
// Get the channel's project directory from its topic
|
|
1895
|
+
let projectDirectory: string | undefined
|
|
1896
|
+
if (
|
|
1897
|
+
interaction.channel &&
|
|
1898
|
+
interaction.channel.type === ChannelType.GuildText
|
|
1899
|
+
) {
|
|
1900
|
+
const textChannel = resolveTextChannel(
|
|
1901
|
+
interaction.channel as TextChannel | ThreadChannel | null,
|
|
1902
|
+
)
|
|
1903
|
+
if (textChannel) {
|
|
1904
|
+
const { projectDirectory: directory, channelAppId } =
|
|
1905
|
+
getKimakiMetadata(textChannel)
|
|
1906
|
+
if (channelAppId && channelAppId !== currentAppId) {
|
|
1907
|
+
await interaction.respond([])
|
|
1908
|
+
return
|
|
1909
|
+
}
|
|
1910
|
+
projectDirectory = directory
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
if (!projectDirectory) {
|
|
1915
|
+
await interaction.respond([])
|
|
1916
|
+
return
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
try {
|
|
1920
|
+
// Get OpenCode client for this directory
|
|
1921
|
+
const getClient =
|
|
1922
|
+
await initializeOpencodeForDirectory(projectDirectory)
|
|
1923
|
+
|
|
1924
|
+
// Use find.files to search for files based on current query
|
|
1925
|
+
const response = await getClient().find.files({
|
|
1926
|
+
query: {
|
|
1927
|
+
query: currentQuery || '',
|
|
1928
|
+
},
|
|
1929
|
+
})
|
|
1930
|
+
|
|
1931
|
+
// Get file paths from the response
|
|
1932
|
+
const files = response.data || []
|
|
1933
|
+
|
|
1934
|
+
// Build the prefix with previous files
|
|
1935
|
+
const prefix =
|
|
1936
|
+
previousFiles.length > 0
|
|
1937
|
+
? previousFiles.join(', ') + ', '
|
|
1938
|
+
: ''
|
|
1939
|
+
|
|
1940
|
+
// Map to Discord autocomplete format
|
|
1941
|
+
const choices = files
|
|
1942
|
+
.slice(0, 25) // Discord limit
|
|
1943
|
+
.map((file: string) => {
|
|
1944
|
+
const fullValue = prefix + file
|
|
1945
|
+
// Get all basenames for display
|
|
1946
|
+
const allFiles = [...previousFiles, file]
|
|
1947
|
+
const allBasenames = allFiles.map(
|
|
1948
|
+
(f) => f.split('/').pop() || f,
|
|
1949
|
+
)
|
|
1950
|
+
let displayName = allBasenames.join(', ')
|
|
1951
|
+
// Truncate if too long
|
|
1952
|
+
if (displayName.length > 100) {
|
|
1953
|
+
displayName = 'ā¦' + displayName.slice(-97)
|
|
1954
|
+
}
|
|
1955
|
+
return {
|
|
1956
|
+
name: displayName,
|
|
1957
|
+
value: fullValue,
|
|
1958
|
+
}
|
|
1959
|
+
})
|
|
1960
|
+
|
|
1961
|
+
await interaction.respond(choices)
|
|
1962
|
+
} catch (error) {
|
|
1963
|
+
voiceLogger.error('[AUTOCOMPLETE] Error fetching files:', error)
|
|
1964
|
+
await interaction.respond([])
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1871
1967
|
}
|
|
1872
1968
|
}
|
|
1873
1969
|
|
|
@@ -1875,7 +1971,100 @@ export async function startDiscordBot({
|
|
|
1875
1971
|
if (interaction.isChatInputCommand()) {
|
|
1876
1972
|
const command = interaction
|
|
1877
1973
|
|
|
1878
|
-
if (command.commandName === '
|
|
1974
|
+
if (command.commandName === 'session') {
|
|
1975
|
+
await command.deferReply({ ephemeral: false })
|
|
1976
|
+
|
|
1977
|
+
const prompt = command.options.getString('prompt', true)
|
|
1978
|
+
const filesString = command.options.getString('files') || ''
|
|
1979
|
+
const channel = command.channel
|
|
1980
|
+
|
|
1981
|
+
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
1982
|
+
await command.editReply(
|
|
1983
|
+
'This command can only be used in text channels',
|
|
1984
|
+
)
|
|
1985
|
+
return
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
const textChannel = channel as TextChannel
|
|
1989
|
+
|
|
1990
|
+
// Get project directory from channel topic
|
|
1991
|
+
let projectDirectory: string | undefined
|
|
1992
|
+
let channelAppId: string | undefined
|
|
1993
|
+
|
|
1994
|
+
if (textChannel.topic) {
|
|
1995
|
+
const extracted = extractTagsArrays({
|
|
1996
|
+
xml: textChannel.topic,
|
|
1997
|
+
tags: ['kimaki.directory', 'kimaki.app'],
|
|
1998
|
+
})
|
|
1999
|
+
|
|
2000
|
+
projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
2001
|
+
channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// Check if this channel belongs to current bot instance
|
|
2005
|
+
if (channelAppId && channelAppId !== currentAppId) {
|
|
2006
|
+
await command.editReply(
|
|
2007
|
+
'This channel is not configured for this bot',
|
|
2008
|
+
)
|
|
2009
|
+
return
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
if (!projectDirectory) {
|
|
2013
|
+
await command.editReply(
|
|
2014
|
+
'This channel is not configured with a project directory',
|
|
2015
|
+
)
|
|
2016
|
+
return
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
if (!fs.existsSync(projectDirectory)) {
|
|
2020
|
+
await command.editReply(
|
|
2021
|
+
`Directory does not exist: ${projectDirectory}`,
|
|
2022
|
+
)
|
|
2023
|
+
return
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
try {
|
|
2027
|
+
// Initialize OpenCode client for the directory
|
|
2028
|
+
const getClient =
|
|
2029
|
+
await initializeOpencodeForDirectory(projectDirectory)
|
|
2030
|
+
|
|
2031
|
+
// Process file mentions - split by comma only
|
|
2032
|
+
const files = filesString
|
|
2033
|
+
.split(',')
|
|
2034
|
+
.map((f) => f.trim())
|
|
2035
|
+
.filter((f) => f)
|
|
2036
|
+
|
|
2037
|
+
// Build the full prompt with file mentions
|
|
2038
|
+
let fullPrompt = prompt
|
|
2039
|
+
if (files.length > 0) {
|
|
2040
|
+
fullPrompt = `${prompt}\n\n@${files.join(' @')}`
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// Send a message first, then create thread from it
|
|
2044
|
+
const starterMessage = await textChannel.send({
|
|
2045
|
+
content: `š **Starting OpenCode session**\nš ${prompt.slice(0, 200)}${prompt.length > 200 ? 'ā¦' : ''}${files.length > 0 ? `\nš Files: ${files.join(', ')}` : ''}`,
|
|
2046
|
+
})
|
|
2047
|
+
|
|
2048
|
+
// Create thread from the message
|
|
2049
|
+
const thread = await starterMessage.startThread({
|
|
2050
|
+
name: prompt.slice(0, 100),
|
|
2051
|
+
autoArchiveDuration: 1440, // 24 hours
|
|
2052
|
+
reason: 'OpenCode session',
|
|
2053
|
+
})
|
|
2054
|
+
|
|
2055
|
+
await command.editReply(
|
|
2056
|
+
`Created new session in ${thread.toString()}`,
|
|
2057
|
+
)
|
|
2058
|
+
|
|
2059
|
+
// Start the OpenCode session
|
|
2060
|
+
await handleOpencodeSession(fullPrompt, thread, projectDirectory)
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
voiceLogger.error('[SESSION] Error:', error)
|
|
2063
|
+
await command.editReply(
|
|
2064
|
+
`Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
2065
|
+
)
|
|
2066
|
+
}
|
|
2067
|
+
} else if (command.commandName === 'resume') {
|
|
1879
2068
|
await command.deferReply({ ephemeral: false })
|
|
1880
2069
|
|
|
1881
2070
|
const sessionId = command.options.getString('session', true)
|
|
@@ -1988,12 +2177,12 @@ export async function startDiscordBot({
|
|
|
1988
2177
|
if (message.info.role === 'user') {
|
|
1989
2178
|
// Render user messages
|
|
1990
2179
|
const userParts = message.parts.filter(
|
|
1991
|
-
(p) => p.type === 'text',
|
|
2180
|
+
(p) => p.type === 'text' && !p.synthetic,
|
|
1992
2181
|
)
|
|
1993
2182
|
const userTexts = userParts
|
|
1994
2183
|
.map((p) => {
|
|
1995
|
-
if (
|
|
1996
|
-
return
|
|
2184
|
+
if (p.type === 'text') {
|
|
2185
|
+
return p.text
|
|
1997
2186
|
}
|
|
1998
2187
|
return ''
|
|
1999
2188
|
})
|