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 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. Click "Reset Token" to generate a new bot token\n' +
116
- "3. Copy the token (you won't be able to see it again!)", 'Step 2: Get Bot Token');
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
- db.prepare('INSERT OR REPLACE INTO bot_tokens (app_id, token) VALUES (?, ?)').run(appId, token);
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: 'Press Enter to continue',
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.map((ch) => ch.kimakiDirectory).filter(Boolean));
220
- const availableProjects = projects.filter((project) => !existingDirs.includes(project.worktree));
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 (shouldAddChannels && availableProjects.length > 0) {
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 = `kimaki-${baseName}`
279
+ const channelName = `${baseName}`
266
280
  .toLowerCase()
267
281
  .replace(/[^a-z0-9-]/g, '-')
268
282
  .slice(0, 100);
@@ -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(`Skipping audio frame from session ${currentSessionCount} because newer session ${speakingSessionCount} has started`);
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 getRequiredBotPermissions() {
32
- return [
33
- PermissionsBitField.Flags.ViewChannel,
34
- PermissionsBitField.Flags.ManageChannels,
35
- PermissionsBitField.Flags.SendMessages,
36
- PermissionsBitField.Flags.SendMessagesInThreads,
37
- PermissionsBitField.Flags.CreatePublicThreads,
38
- PermissionsBitField.Flags.ManageThreads,
39
- PermissionsBitField.Flags.ReadMessageHistory,
40
- PermissionsBitField.Flags.AddReactions,
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
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.1.2",
5
+ "version": "0.1.3",
6
6
  "repository": "https://github.com/remorses/kimaki",
7
7
  "bin": "bin.js",
8
8
  "files": [
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. Click "Reset Token" to generate a new bot token\n' +
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 2: Get Bot Token',
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 3: Install Bot to Server',
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: 'Press Enter to continue',
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.map((ch) => ch.kimakiDirectory).filter(Boolean),
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 = projects.filter(
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 (shouldAddChannels && availableProjects.length > 0) {
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 = `kimaki-${baseName}`
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
- `Skipping audio frame from session ${currentSessionCount} because newer session ${speakingSessionCount} has started`,
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 getPermissionNames(): string[] {
70
- const permissions = getRequiredBotPermissions()
71
- const permissionsBitField = new PermissionsBitField(permissions)
72
- return permissionsBitField.toArray()
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
  }