kimaki 0.1.2 → 0.1.3
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 +30 -16
- package/dist/discordBot.js +4 -2
- package/dist/utils.js +10 -21
- package/package.json +1 -1
- package/src/cli.ts +44 -20
- package/src/discordBot.ts +5 -5
- package/src/utils.ts +10 -21
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cac } from 'cac';
|
|
3
|
-
import { intro, outro, text, password, note, cancel, isCancel, log, multiselect, spinner, } from '@clack/prompts';
|
|
4
|
-
import { generateBotInstallUrl } from './utils.js';
|
|
3
|
+
import { intro, outro, text, password, note, cancel, isCancel, confirm, log, multiselect, spinner, } from '@clack/prompts';
|
|
4
|
+
import { deduplicateByKey, generateBotInstallUrl } from './utils.js';
|
|
5
5
|
import { getChannelsWithDescriptions, createDiscordClient, getDatabase, startDiscordBot, initializeOpencodeForDirectory, } from './discordBot.js';
|
|
6
6
|
import { Events, ChannelType, REST, Routes, SlashCommandBuilder, } from 'discord.js';
|
|
7
7
|
import path from 'node:path';
|
|
@@ -74,7 +74,6 @@ async function ensureKimakiCategory(guild) {
|
|
|
74
74
|
}
|
|
75
75
|
async function run({ restart, addChannels }) {
|
|
76
76
|
const forceSetup = Boolean(restart);
|
|
77
|
-
const shouldAddChannels = Boolean(addChannels);
|
|
78
77
|
intro('🤖 Discord Bot Setup');
|
|
79
78
|
const db = getDatabase();
|
|
80
79
|
let appId;
|
|
@@ -82,6 +81,7 @@ async function run({ restart, addChannels }) {
|
|
|
82
81
|
const existingBot = db
|
|
83
82
|
.prepare('SELECT app_id, token FROM bot_tokens ORDER BY created_at DESC LIMIT 1')
|
|
84
83
|
.get();
|
|
84
|
+
const shouldAddChannels = !existingBot?.token || forceSetup || Boolean(addChannels);
|
|
85
85
|
if (existingBot && !forceSetup) {
|
|
86
86
|
appId = existingBot.app_id;
|
|
87
87
|
token = existingBot.token;
|
|
@@ -112,8 +112,22 @@ async function run({ restart, addChannels }) {
|
|
|
112
112
|
}
|
|
113
113
|
appId = appIdInput;
|
|
114
114
|
note('1. Go to the "Bot" section in the left sidebar\n' +
|
|
115
|
-
'2.
|
|
116
|
-
|
|
115
|
+
'2. Scroll down to "Privileged Gateway Intents"\n' +
|
|
116
|
+
'3. Enable these intents by toggling them ON:\n' +
|
|
117
|
+
' • SERVER MEMBERS INTENT\n' +
|
|
118
|
+
' • MESSAGE CONTENT INTENT\n' +
|
|
119
|
+
'4. Click "Save Changes" at the bottom', 'Step 2: Enable Required Intents');
|
|
120
|
+
const intentsConfirmed = await text({
|
|
121
|
+
message: 'Press Enter after enabling both intents:',
|
|
122
|
+
placeholder: 'Enter',
|
|
123
|
+
});
|
|
124
|
+
if (isCancel(intentsConfirmed)) {
|
|
125
|
+
cancel('Setup cancelled');
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
note('1. Still in the "Bot" section\n' +
|
|
129
|
+
'2. Click "Reset Token" to generate a new bot token (in case of errors try again)\n' +
|
|
130
|
+
"3. Copy the token (you won't be able to see it again!)", 'Step 3: Get Bot Token');
|
|
117
131
|
const tokenInput = await password({
|
|
118
132
|
message: 'Enter your Discord Bot Token (will be hidden):',
|
|
119
133
|
validate(value) {
|
|
@@ -128,15 +142,10 @@ async function run({ restart, addChannels }) {
|
|
|
128
142
|
process.exit(0);
|
|
129
143
|
}
|
|
130
144
|
token = tokenInput;
|
|
131
|
-
|
|
132
|
-
note('Token saved to database', 'Credentials Stored');
|
|
133
|
-
note(`Bot install URL:\n${generateBotInstallUrl({ clientId: appId })}\n\nYou MUST install the bot in your Discord server before continuing.`, 'Step 3: Install Bot to Server');
|
|
145
|
+
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');
|
|
134
146
|
const installed = await text({
|
|
135
147
|
message: 'Press Enter AFTER you have installed the bot in your server:',
|
|
136
|
-
placeholder: '
|
|
137
|
-
validate() {
|
|
138
|
-
return undefined;
|
|
139
|
-
},
|
|
148
|
+
placeholder: 'Enter',
|
|
140
149
|
});
|
|
141
150
|
if (isCancel(installed)) {
|
|
142
151
|
cancel('Setup cancelled');
|
|
@@ -172,6 +181,7 @@ async function run({ restart, addChannels }) {
|
|
|
172
181
|
cliLogger.error('Error: ' + (error instanceof Error ? error.message : String(error)));
|
|
173
182
|
process.exit(EXIT_NO_RESTART);
|
|
174
183
|
}
|
|
184
|
+
db.prepare('INSERT OR REPLACE INTO bot_tokens (app_id, token) VALUES (?, ?)').run(appId, token);
|
|
175
185
|
for (const { guild, channels } of kimakiChannels) {
|
|
176
186
|
for (const channel of channels) {
|
|
177
187
|
if (channel.kimakiDirectory) {
|
|
@@ -216,12 +226,16 @@ async function run({ restart, addChannels }) {
|
|
|
216
226
|
discordClient.destroy();
|
|
217
227
|
process.exit(EXIT_NO_RESTART);
|
|
218
228
|
}
|
|
219
|
-
const existingDirs = kimakiChannels.flatMap(({ channels }) => channels
|
|
220
|
-
|
|
229
|
+
const existingDirs = kimakiChannels.flatMap(({ channels }) => channels
|
|
230
|
+
.filter((ch) => ch.kimakiDirectory && ch.kimakiApp === appId)
|
|
231
|
+
.map((ch) => ch.kimakiDirectory)
|
|
232
|
+
.filter(Boolean));
|
|
233
|
+
const availableProjects = deduplicateByKey(projects.filter((project) => !existingDirs.includes(project.worktree)), (x) => x.worktree);
|
|
221
234
|
if (availableProjects.length === 0) {
|
|
222
235
|
note('All OpenCode projects already have Discord channels', 'No New Projects');
|
|
223
236
|
}
|
|
224
|
-
if (
|
|
237
|
+
if ((!existingDirs?.length && availableProjects.length > 0) ||
|
|
238
|
+
shouldAddChannels) {
|
|
225
239
|
const selectedProjects = await multiselect({
|
|
226
240
|
message: 'Select projects to create Discord channels for:',
|
|
227
241
|
options: availableProjects.map((project) => ({
|
|
@@ -262,7 +276,7 @@ async function run({ restart, addChannels }) {
|
|
|
262
276
|
if (!project)
|
|
263
277
|
continue;
|
|
264
278
|
const baseName = path.basename(project.worktree);
|
|
265
|
-
const channelName =
|
|
279
|
+
const channelName = `${baseName}`
|
|
266
280
|
.toLowerCase()
|
|
267
281
|
.replace(/[^a-z0-9-]/g, '-')
|
|
268
282
|
.slice(0, 100);
|
package/dist/discordBot.js
CHANGED
|
@@ -227,14 +227,16 @@ async function setupVoiceHandling({ connection, guildId, channelId, }) {
|
|
|
227
227
|
.on('data', (frame) => {
|
|
228
228
|
// Check if a newer speaking session has started
|
|
229
229
|
if (currentSessionCount !== speakingSessionCount) {
|
|
230
|
-
voiceLogger.log(
|
|
230
|
+
// voiceLogger.log(
|
|
231
|
+
// `Skipping audio frame from session ${currentSessionCount} because newer session ${speakingSessionCount} has started`,
|
|
232
|
+
// )
|
|
231
233
|
return;
|
|
232
234
|
}
|
|
233
235
|
if (!voiceData.genAiWorker) {
|
|
234
236
|
voiceLogger.warn(`[VOICE] Received audio frame but no GenAI worker active for guild ${guildId}`);
|
|
235
237
|
return;
|
|
236
238
|
}
|
|
237
|
-
voiceLogger.debug('User audio chunk length', frame.length)
|
|
239
|
+
// voiceLogger.debug('User audio chunk length', frame.length)
|
|
238
240
|
// Write to PCM file if stream exists
|
|
239
241
|
voiceData.userAudioStream?.write(frame);
|
|
240
242
|
// stream incrementally — low latency
|
package/dist/utils.js
CHANGED
|
@@ -28,25 +28,14 @@ export function generateBotInstallUrl({ clientId, permissions = [
|
|
|
28
28
|
}
|
|
29
29
|
return url.toString();
|
|
30
30
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
PermissionsBitField.Flags.ManageMessages,
|
|
42
|
-
PermissionsBitField.Flags.UseExternalEmojis,
|
|
43
|
-
PermissionsBitField.Flags.AttachFiles,
|
|
44
|
-
PermissionsBitField.Flags.Connect,
|
|
45
|
-
PermissionsBitField.Flags.Speak,
|
|
46
|
-
];
|
|
47
|
-
}
|
|
48
|
-
function getPermissionNames() {
|
|
49
|
-
const permissions = getRequiredBotPermissions();
|
|
50
|
-
const permissionsBitField = new PermissionsBitField(permissions);
|
|
51
|
-
return permissionsBitField.toArray();
|
|
31
|
+
export function deduplicateByKey(arr, keyFn) {
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
return arr.filter(item => {
|
|
34
|
+
const key = keyFn(item);
|
|
35
|
+
if (seen.has(key)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
seen.add(key);
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
52
41
|
}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -8,11 +8,12 @@ import {
|
|
|
8
8
|
note,
|
|
9
9
|
cancel,
|
|
10
10
|
isCancel,
|
|
11
|
+
confirm,
|
|
11
12
|
log,
|
|
12
13
|
multiselect,
|
|
13
14
|
spinner,
|
|
14
15
|
} from '@clack/prompts'
|
|
15
|
-
import { generateBotInstallUrl } from './utils.js'
|
|
16
|
+
import { deduplicateByKey, generateBotInstallUrl } from './utils.js'
|
|
16
17
|
import {
|
|
17
18
|
getChannelsWithDescriptions,
|
|
18
19
|
createDiscordClient,
|
|
@@ -138,7 +139,6 @@ async function ensureKimakiCategory(guild: Guild): Promise<CategoryChannel> {
|
|
|
138
139
|
|
|
139
140
|
async function run({ restart, addChannels }: CliOptions) {
|
|
140
141
|
const forceSetup = Boolean(restart)
|
|
141
|
-
const shouldAddChannels = Boolean(addChannels)
|
|
142
142
|
|
|
143
143
|
intro('🤖 Discord Bot Setup')
|
|
144
144
|
|
|
@@ -152,6 +152,9 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
152
152
|
)
|
|
153
153
|
.get() as { app_id: string; token: string } | undefined
|
|
154
154
|
|
|
155
|
+
const shouldAddChannels =
|
|
156
|
+
!existingBot?.token || forceSetup || Boolean(addChannels)
|
|
157
|
+
|
|
155
158
|
if (existingBot && !forceSetup) {
|
|
156
159
|
appId = existingBot.app_id
|
|
157
160
|
token = existingBot.token
|
|
@@ -196,9 +199,29 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
196
199
|
|
|
197
200
|
note(
|
|
198
201
|
'1. Go to the "Bot" section in the left sidebar\n' +
|
|
199
|
-
'2.
|
|
202
|
+
'2. Scroll down to "Privileged Gateway Intents"\n' +
|
|
203
|
+
'3. Enable these intents by toggling them ON:\n' +
|
|
204
|
+
' • SERVER MEMBERS INTENT\n' +
|
|
205
|
+
' • MESSAGE CONTENT INTENT\n' +
|
|
206
|
+
'4. Click "Save Changes" at the bottom',
|
|
207
|
+
'Step 2: Enable Required Intents',
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
const intentsConfirmed = await text({
|
|
211
|
+
message: 'Press Enter after enabling both intents:',
|
|
212
|
+
placeholder: 'Enter',
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
if (isCancel(intentsConfirmed)) {
|
|
216
|
+
cancel('Setup cancelled')
|
|
217
|
+
process.exit(0)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
note(
|
|
221
|
+
'1. Still in the "Bot" section\n' +
|
|
222
|
+
'2. Click "Reset Token" to generate a new bot token (in case of errors try again)\n' +
|
|
200
223
|
"3. Copy the token (you won't be able to see it again!)",
|
|
201
|
-
'Step
|
|
224
|
+
'Step 3: Get Bot Token',
|
|
202
225
|
)
|
|
203
226
|
|
|
204
227
|
const tokenInput = await password({
|
|
@@ -215,23 +238,14 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
215
238
|
}
|
|
216
239
|
token = tokenInput
|
|
217
240
|
|
|
218
|
-
db.prepare(
|
|
219
|
-
'INSERT OR REPLACE INTO bot_tokens (app_id, token) VALUES (?, ?)',
|
|
220
|
-
).run(appId, token)
|
|
221
|
-
|
|
222
|
-
note('Token saved to database', 'Credentials Stored')
|
|
223
|
-
|
|
224
241
|
note(
|
|
225
242
|
`Bot install URL:\n${generateBotInstallUrl({ clientId: appId })}\n\nYou MUST install the bot in your Discord server before continuing.`,
|
|
226
|
-
'Step
|
|
243
|
+
'Step 4: Install Bot to Server',
|
|
227
244
|
)
|
|
228
245
|
|
|
229
246
|
const installed = await text({
|
|
230
247
|
message: 'Press Enter AFTER you have installed the bot in your server:',
|
|
231
|
-
placeholder: '
|
|
232
|
-
validate() {
|
|
233
|
-
return undefined
|
|
234
|
-
},
|
|
248
|
+
placeholder: 'Enter',
|
|
235
249
|
})
|
|
236
250
|
|
|
237
251
|
if (isCancel(installed)) {
|
|
@@ -282,6 +296,9 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
282
296
|
)
|
|
283
297
|
process.exit(EXIT_NO_RESTART)
|
|
284
298
|
}
|
|
299
|
+
db.prepare(
|
|
300
|
+
'INSERT OR REPLACE INTO bot_tokens (app_id, token) VALUES (?, ?)',
|
|
301
|
+
).run(appId, token)
|
|
285
302
|
|
|
286
303
|
for (const { guild, channels } of kimakiChannels) {
|
|
287
304
|
for (const channel of channels) {
|
|
@@ -350,11 +367,15 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
350
367
|
}
|
|
351
368
|
|
|
352
369
|
const existingDirs = kimakiChannels.flatMap(({ channels }) =>
|
|
353
|
-
channels
|
|
370
|
+
channels
|
|
371
|
+
.filter((ch) => ch.kimakiDirectory && ch.kimakiApp === appId)
|
|
372
|
+
.map((ch) => ch.kimakiDirectory)
|
|
373
|
+
.filter(Boolean),
|
|
354
374
|
)
|
|
355
375
|
|
|
356
|
-
const availableProjects =
|
|
357
|
-
(project) => !existingDirs.includes(project.worktree),
|
|
376
|
+
const availableProjects = deduplicateByKey(
|
|
377
|
+
projects.filter((project) => !existingDirs.includes(project.worktree)),
|
|
378
|
+
(x) => x.worktree,
|
|
358
379
|
)
|
|
359
380
|
|
|
360
381
|
if (availableProjects.length === 0) {
|
|
@@ -364,7 +385,10 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
364
385
|
)
|
|
365
386
|
}
|
|
366
387
|
|
|
367
|
-
if (
|
|
388
|
+
if (
|
|
389
|
+
(!existingDirs?.length && availableProjects.length > 0) ||
|
|
390
|
+
shouldAddChannels
|
|
391
|
+
) {
|
|
368
392
|
const selectedProjects = await multiselect({
|
|
369
393
|
message: 'Select projects to create Discord channels for:',
|
|
370
394
|
options: availableProjects.map((project) => ({
|
|
@@ -410,7 +434,7 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
410
434
|
if (!project) continue
|
|
411
435
|
|
|
412
436
|
const baseName = path.basename(project.worktree)
|
|
413
|
-
const channelName =
|
|
437
|
+
const channelName = `${baseName}`
|
|
414
438
|
.toLowerCase()
|
|
415
439
|
.replace(/[^a-z0-9-]/g, '-')
|
|
416
440
|
.slice(0, 100)
|
package/src/discordBot.ts
CHANGED
|
@@ -334,9 +334,9 @@ async function setupVoiceHandling({
|
|
|
334
334
|
.on('data', (frame: Buffer) => {
|
|
335
335
|
// Check if a newer speaking session has started
|
|
336
336
|
if (currentSessionCount !== speakingSessionCount) {
|
|
337
|
-
voiceLogger.log(
|
|
338
|
-
|
|
339
|
-
)
|
|
337
|
+
// voiceLogger.log(
|
|
338
|
+
// `Skipping audio frame from session ${currentSessionCount} because newer session ${speakingSessionCount} has started`,
|
|
339
|
+
// )
|
|
340
340
|
return
|
|
341
341
|
}
|
|
342
342
|
|
|
@@ -346,7 +346,7 @@ async function setupVoiceHandling({
|
|
|
346
346
|
)
|
|
347
347
|
return
|
|
348
348
|
}
|
|
349
|
-
voiceLogger.debug('User audio chunk length', frame.length)
|
|
349
|
+
// voiceLogger.debug('User audio chunk length', frame.length)
|
|
350
350
|
|
|
351
351
|
// Write to PCM file if stream exists
|
|
352
352
|
voiceData.userAudioStream?.write(frame)
|
|
@@ -1950,7 +1950,7 @@ export async function startDiscordBot({
|
|
|
1950
1950
|
return ''
|
|
1951
1951
|
})
|
|
1952
1952
|
.filter((t) => t.trim())
|
|
1953
|
-
|
|
1953
|
+
|
|
1954
1954
|
const userText = userTexts.join('\n\n')
|
|
1955
1955
|
if (userText) {
|
|
1956
1956
|
// Escape backticks in user messages to prevent formatting issues
|
package/src/utils.ts
CHANGED
|
@@ -48,26 +48,15 @@ export function generateBotInstallUrl({
|
|
|
48
48
|
return url.toString()
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function getRequiredBotPermissions(): bigint[] {
|
|
52
|
-
return [
|
|
53
|
-
PermissionsBitField.Flags.ViewChannel,
|
|
54
|
-
PermissionsBitField.Flags.ManageChannels,
|
|
55
|
-
PermissionsBitField.Flags.SendMessages,
|
|
56
|
-
PermissionsBitField.Flags.SendMessagesInThreads,
|
|
57
|
-
PermissionsBitField.Flags.CreatePublicThreads,
|
|
58
|
-
PermissionsBitField.Flags.ManageThreads,
|
|
59
|
-
PermissionsBitField.Flags.ReadMessageHistory,
|
|
60
|
-
PermissionsBitField.Flags.AddReactions,
|
|
61
|
-
PermissionsBitField.Flags.ManageMessages,
|
|
62
|
-
PermissionsBitField.Flags.UseExternalEmojis,
|
|
63
|
-
PermissionsBitField.Flags.AttachFiles,
|
|
64
|
-
PermissionsBitField.Flags.Connect,
|
|
65
|
-
PermissionsBitField.Flags.Speak,
|
|
66
|
-
]
|
|
67
|
-
}
|
|
68
51
|
|
|
69
|
-
function
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
52
|
+
export function deduplicateByKey<T, K>(arr: T[], keyFn: (item: T) => K): T[] {
|
|
53
|
+
const seen = new Set<K>()
|
|
54
|
+
return arr.filter(item => {
|
|
55
|
+
const key = keyFn(item)
|
|
56
|
+
if (seen.has(key)) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
seen.add(key)
|
|
60
|
+
return true
|
|
61
|
+
})
|
|
73
62
|
}
|