kimaki 0.4.40 → 0.4.42

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli.js +89 -30
  3. package/package.json +14 -15
  4. package/src/cli.ts +146 -55
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kimaki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/cli.js CHANGED
@@ -292,6 +292,79 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
292
292
  throw error;
293
293
  }
294
294
  }
295
+ /**
296
+ * Store channel-directory mappings in the database.
297
+ * Called after Discord login to persist channel configurations.
298
+ */
299
+ function storeChannelDirectories({ kimakiChannels, db, }) {
300
+ for (const { guild, channels } of kimakiChannels) {
301
+ for (const channel of channels) {
302
+ if (channel.kimakiDirectory) {
303
+ db.prepare('INSERT OR IGNORE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)').run(channel.id, channel.kimakiDirectory, 'text', channel.kimakiApp || null);
304
+ const voiceChannel = guild.channels.cache.find((ch) => ch.type === ChannelType.GuildVoice && ch.name === channel.name);
305
+ if (voiceChannel) {
306
+ db.prepare('INSERT OR IGNORE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)').run(voiceChannel.id, channel.kimakiDirectory, 'voice', channel.kimakiApp || null);
307
+ }
308
+ }
309
+ }
310
+ }
311
+ }
312
+ /**
313
+ * Show the ready message with channel links.
314
+ * Called at the end of startup to display available channels.
315
+ */
316
+ function showReadyMessage({ kimakiChannels, createdChannels, appId, }) {
317
+ const allChannels = [];
318
+ allChannels.push(...createdChannels);
319
+ kimakiChannels.forEach(({ guild, channels }) => {
320
+ channels.forEach((ch) => {
321
+ allChannels.push({
322
+ name: ch.name,
323
+ id: ch.id,
324
+ guildId: guild.id,
325
+ directory: ch.kimakiDirectory,
326
+ });
327
+ });
328
+ });
329
+ if (allChannels.length > 0) {
330
+ const channelLinks = allChannels
331
+ .map((ch) => `• #${ch.name}: https://discord.com/channels/${ch.guildId}/${ch.id}`)
332
+ .join('\n');
333
+ 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');
334
+ }
335
+ 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');
336
+ }
337
+ /**
338
+ * Background initialization for quick start mode.
339
+ * Starts OpenCode server and registers slash commands without blocking bot startup.
340
+ */
341
+ async function backgroundInit({ currentDir, token, appId, }) {
342
+ try {
343
+ const opencodeResult = await initializeOpencodeForDirectory(currentDir);
344
+ if (opencodeResult instanceof Error) {
345
+ cliLogger.warn('Background OpenCode init failed:', opencodeResult.message);
346
+ // Still try to register basic commands without user commands/agents
347
+ await registerCommands({ token, appId, userCommands: [], agents: [] });
348
+ return;
349
+ }
350
+ const getClient = opencodeResult;
351
+ const [userCommands, agents] = await Promise.all([
352
+ getClient()
353
+ .command.list({ query: { directory: currentDir } })
354
+ .then((r) => r.data || [])
355
+ .catch(() => []),
356
+ getClient()
357
+ .app.agents({ query: { directory: currentDir } })
358
+ .then((r) => r.data || [])
359
+ .catch(() => []),
360
+ ]);
361
+ await registerCommands({ token, appId, userCommands, agents });
362
+ cliLogger.log('Slash commands registered!');
363
+ }
364
+ catch (error) {
365
+ cliLogger.error('Background init failed:', error instanceof Error ? error.message : String(error));
366
+ }
367
+ }
295
368
  async function run({ restart, addChannels }) {
296
369
  const forceSetup = Boolean(restart);
297
370
  intro('🤖 Discord Bot Setup');
@@ -512,17 +585,8 @@ async function run({ restart, addChannels }) {
512
585
  process.exit(EXIT_NO_RESTART);
513
586
  }
514
587
  db.prepare('INSERT OR REPLACE INTO bot_tokens (app_id, token) VALUES (?, ?)').run(appId, token);
515
- for (const { guild, channels } of kimakiChannels) {
516
- for (const channel of channels) {
517
- if (channel.kimakiDirectory) {
518
- db.prepare('INSERT OR IGNORE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)').run(channel.id, channel.kimakiDirectory, 'text', channel.kimakiApp || null);
519
- const voiceChannel = guild.channels.cache.find((ch) => ch.type === ChannelType.GuildVoice && ch.name === channel.name);
520
- if (voiceChannel) {
521
- db.prepare('INSERT OR IGNORE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)').run(voiceChannel.id, channel.kimakiDirectory, 'voice', channel.kimakiApp || null);
522
- }
523
- }
524
- }
525
- }
588
+ // Store channel-directory mappings
589
+ storeChannelDirectories({ kimakiChannels, db });
526
590
  if (kimakiChannels.length > 0) {
527
591
  const channelList = kimakiChannels
528
592
  .flatMap(({ guild, channels }) => channels.map((ch) => {
@@ -532,6 +596,19 @@ async function run({ restart, addChannels }) {
532
596
  .join('\n');
533
597
  note(channelList, 'Existing Kimaki Channels');
534
598
  }
599
+ // Quick start: if setup is already done, start bot immediately and background the rest
600
+ const isQuickStart = existingBot && !forceSetup && !addChannels;
601
+ if (isQuickStart) {
602
+ s.start('Starting Discord bot...');
603
+ await startDiscordBot({ token, appId, discordClient });
604
+ s.stop('Discord bot is running!');
605
+ // Background: OpenCode init + slash command registration (non-blocking)
606
+ void backgroundInit({ currentDir, token, appId });
607
+ showReadyMessage({ kimakiChannels, createdChannels, appId });
608
+ outro('✨ Bot ready! Listening for messages...');
609
+ return;
610
+ }
611
+ // Full setup path: wait for OpenCode, show prompts, create channels if needed
535
612
  // Await the OpenCode server that was started in parallel with Discord login
536
613
  s.start('Waiting for OpenCode server...');
537
614
  const getClient = await opencodePromise;
@@ -656,25 +733,7 @@ async function run({ restart, addChannels }) {
656
733
  s.start('Starting Discord bot...');
657
734
  await startDiscordBot({ token, appId, discordClient });
658
735
  s.stop('Discord bot is running!');
659
- const allChannels = [];
660
- allChannels.push(...createdChannels);
661
- kimakiChannels.forEach(({ guild, channels }) => {
662
- channels.forEach((ch) => {
663
- allChannels.push({
664
- name: ch.name,
665
- id: ch.id,
666
- guildId: guild.id,
667
- directory: ch.kimakiDirectory,
668
- });
669
- });
670
- });
671
- if (allChannels.length > 0) {
672
- const channelLinks = allChannels
673
- .map((ch) => `• #${ch.name}: https://discord.com/channels/${ch.guildId}/${ch.id}`)
674
- .join('\n');
675
- 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');
676
- }
677
- 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');
736
+ showReadyMessage({ kimakiChannels, createdChannels, appId });
678
737
  outro('✨ Setup complete! Listening for new messages... do not close this process.');
679
738
  }
680
739
  cli
package/package.json CHANGED
@@ -2,18 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.40",
6
- "scripts": {
7
- "dev": "tsx --env-file .env src/cli.ts",
8
- "prepublishOnly": "pnpm tsc",
9
- "dev:bun": "DEBUG=1 bun --env-file .env src/cli.ts",
10
- "watch": "tsx scripts/watch-session.ts",
11
- "test:events": "tsx test-events.ts",
12
- "pcm-to-mp3": "bun scripts/pcm-to-mp3",
13
- "test:send": "tsx send-test-message.ts",
14
- "register-commands": "tsx scripts/register-commands.ts",
15
- "format": "oxfmt src"
16
- },
5
+ "version": "0.4.42",
17
6
  "repository": "https://github.com/remorses/kimaki",
18
7
  "bin": "bin.js",
19
8
  "files": [
@@ -41,7 +30,6 @@
41
30
  "cac": "^6.7.14",
42
31
  "discord.js": "^14.16.3",
43
32
  "domhandler": "^5.0.3",
44
- "errore": "workspace:^",
45
33
  "glob": "^13.0.0",
46
34
  "htmlparser2": "^10.0.0",
47
35
  "js-yaml": "^4.1.0",
@@ -52,10 +40,21 @@
52
40
  "ripgrep-js": "^3.0.0",
53
41
  "string-dedent": "^3.0.2",
54
42
  "undici": "^7.16.0",
55
- "zod": "^4.2.1"
43
+ "zod": "^4.2.1",
44
+ "errore": "^0.8.0"
56
45
  },
57
46
  "optionalDependencies": {
58
47
  "@discordjs/opus": "^0.10.0",
59
48
  "prism-media": "^1.3.5"
49
+ },
50
+ "scripts": {
51
+ "dev": "tsx --env-file .env src/cli.ts",
52
+ "dev:bun": "DEBUG=1 bun --env-file .env src/cli.ts",
53
+ "watch": "tsx scripts/watch-session.ts",
54
+ "test:events": "tsx test-events.ts",
55
+ "pcm-to-mp3": "bun scripts/pcm-to-mp3",
56
+ "test:send": "tsx send-test-message.ts",
57
+ "register-commands": "tsx scripts/register-commands.ts",
58
+ "format": "oxfmt src"
60
59
  }
61
- }
60
+ }
package/src/cli.ts CHANGED
@@ -389,6 +389,133 @@ async function registerCommands({
389
389
  }
390
390
  }
391
391
 
392
+ /**
393
+ * Store channel-directory mappings in the database.
394
+ * Called after Discord login to persist channel configurations.
395
+ */
396
+ function storeChannelDirectories({
397
+ kimakiChannels,
398
+ db,
399
+ }: {
400
+ kimakiChannels: { guild: Guild; channels: ChannelWithTags[] }[]
401
+ db: ReturnType<typeof getDatabase>
402
+ }): void {
403
+ for (const { guild, channels } of kimakiChannels) {
404
+ for (const channel of channels) {
405
+ if (channel.kimakiDirectory) {
406
+ db.prepare(
407
+ 'INSERT OR IGNORE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)',
408
+ ).run(channel.id, channel.kimakiDirectory, 'text', channel.kimakiApp || null)
409
+
410
+ const voiceChannel = guild.channels.cache.find(
411
+ (ch) => ch.type === ChannelType.GuildVoice && ch.name === channel.name,
412
+ )
413
+
414
+ if (voiceChannel) {
415
+ db.prepare(
416
+ 'INSERT OR IGNORE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)',
417
+ ).run(voiceChannel.id, channel.kimakiDirectory, 'voice', channel.kimakiApp || null)
418
+ }
419
+ }
420
+ }
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Show the ready message with channel links.
426
+ * Called at the end of startup to display available channels.
427
+ */
428
+ function showReadyMessage({
429
+ kimakiChannels,
430
+ createdChannels,
431
+ appId,
432
+ }: {
433
+ kimakiChannels: { guild: Guild; channels: ChannelWithTags[] }[]
434
+ createdChannels: { name: string; id: string; guildId: string }[]
435
+ appId: string
436
+ }): void {
437
+ const allChannels: {
438
+ name: string
439
+ id: string
440
+ guildId: string
441
+ directory?: string
442
+ }[] = []
443
+
444
+ allChannels.push(...createdChannels)
445
+
446
+ kimakiChannels.forEach(({ guild, channels }) => {
447
+ channels.forEach((ch) => {
448
+ allChannels.push({
449
+ name: ch.name,
450
+ id: ch.id,
451
+ guildId: guild.id,
452
+ directory: ch.kimakiDirectory,
453
+ })
454
+ })
455
+ })
456
+
457
+ if (allChannels.length > 0) {
458
+ const channelLinks = allChannels
459
+ .map((ch) => `• #${ch.name}: https://discord.com/channels/${ch.guildId}/${ch.id}`)
460
+ .join('\n')
461
+
462
+ note(
463
+ `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!`,
464
+ '🚀 Ready to Use',
465
+ )
466
+ }
467
+
468
+ note(
469
+ '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.',
470
+ '⚠️ Keep Running',
471
+ )
472
+ }
473
+
474
+ /**
475
+ * Background initialization for quick start mode.
476
+ * Starts OpenCode server and registers slash commands without blocking bot startup.
477
+ */
478
+ async function backgroundInit({
479
+ currentDir,
480
+ token,
481
+ appId,
482
+ }: {
483
+ currentDir: string
484
+ token: string
485
+ appId: string
486
+ }): Promise<void> {
487
+ try {
488
+ const opencodeResult = await initializeOpencodeForDirectory(currentDir)
489
+ if (opencodeResult instanceof Error) {
490
+ cliLogger.warn('Background OpenCode init failed:', opencodeResult.message)
491
+ // Still try to register basic commands without user commands/agents
492
+ await registerCommands({ token, appId, userCommands: [], agents: [] })
493
+ return
494
+ }
495
+
496
+ const getClient = opencodeResult
497
+
498
+ const [userCommands, agents] = await Promise.all([
499
+ getClient()
500
+ .command.list({ query: { directory: currentDir } })
501
+ .then((r) => r.data || [])
502
+ .catch(() => []),
503
+ getClient()
504
+ .app.agents({ query: { directory: currentDir } })
505
+ .then((r) => r.data || [])
506
+ .catch(() => []),
507
+ ])
508
+
509
+ await registerCommands({ token, appId, userCommands, agents })
510
+ cliLogger.log('Slash commands registered!')
511
+ } catch (error) {
512
+ cliLogger.error(
513
+ 'Background init failed:',
514
+ error instanceof Error ? error.message : String(error),
515
+ )
516
+ }
517
+ }
518
+
392
519
  async function run({ restart, addChannels }: CliOptions) {
393
520
  const forceSetup = Boolean(restart)
394
521
 
@@ -679,25 +806,8 @@ async function run({ restart, addChannels }: CliOptions) {
679
806
  }
680
807
  db.prepare('INSERT OR REPLACE INTO bot_tokens (app_id, token) VALUES (?, ?)').run(appId, token)
681
808
 
682
- for (const { guild, channels } of kimakiChannels) {
683
- for (const channel of channels) {
684
- if (channel.kimakiDirectory) {
685
- db.prepare(
686
- 'INSERT OR IGNORE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)',
687
- ).run(channel.id, channel.kimakiDirectory, 'text', channel.kimakiApp || null)
688
-
689
- const voiceChannel = guild.channels.cache.find(
690
- (ch) => ch.type === ChannelType.GuildVoice && ch.name === channel.name,
691
- )
692
-
693
- if (voiceChannel) {
694
- db.prepare(
695
- 'INSERT OR IGNORE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)',
696
- ).run(voiceChannel.id, channel.kimakiDirectory, 'voice', channel.kimakiApp || null)
697
- }
698
- }
699
- }
700
- }
809
+ // Store channel-directory mappings
810
+ storeChannelDirectories({ kimakiChannels, db })
701
811
 
702
812
  if (kimakiChannels.length > 0) {
703
813
  const channelList = kimakiChannels
@@ -713,6 +823,22 @@ async function run({ restart, addChannels }: CliOptions) {
713
823
  note(channelList, 'Existing Kimaki Channels')
714
824
  }
715
825
 
826
+ // Quick start: if setup is already done, start bot immediately and background the rest
827
+ const isQuickStart = existingBot && !forceSetup && !addChannels
828
+ if (isQuickStart) {
829
+ s.start('Starting Discord bot...')
830
+ await startDiscordBot({ token, appId, discordClient })
831
+ s.stop('Discord bot is running!')
832
+
833
+ // Background: OpenCode init + slash command registration (non-blocking)
834
+ void backgroundInit({ currentDir, token, appId })
835
+
836
+ showReadyMessage({ kimakiChannels, createdChannels, appId })
837
+ outro('✨ Bot ready! Listening for messages...')
838
+ return
839
+ }
840
+
841
+ // Full setup path: wait for OpenCode, show prompts, create channels if needed
716
842
  // Await the OpenCode server that was started in parallel with Discord login
717
843
  s.start('Waiting for OpenCode server...')
718
844
  const getClient = await opencodePromise
@@ -875,42 +1001,7 @@ async function run({ restart, addChannels }: CliOptions) {
875
1001
  await startDiscordBot({ token, appId, discordClient })
876
1002
  s.stop('Discord bot is running!')
877
1003
 
878
- const allChannels: {
879
- name: string
880
- id: string
881
- guildId: string
882
- directory?: string
883
- }[] = []
884
-
885
- allChannels.push(...createdChannels)
886
-
887
- kimakiChannels.forEach(({ guild, channels }) => {
888
- channels.forEach((ch) => {
889
- allChannels.push({
890
- name: ch.name,
891
- id: ch.id,
892
- guildId: guild.id,
893
- directory: ch.kimakiDirectory,
894
- })
895
- })
896
- })
897
-
898
- if (allChannels.length > 0) {
899
- const channelLinks = allChannels
900
- .map((ch) => `• #${ch.name}: https://discord.com/channels/${ch.guildId}/${ch.id}`)
901
- .join('\n')
902
-
903
- note(
904
- `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!`,
905
- '🚀 Ready to Use',
906
- )
907
- }
908
-
909
- note(
910
- '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.',
911
- '⚠️ Keep Running',
912
- )
913
-
1004
+ showReadyMessage({ kimakiChannels, createdChannels, appId })
914
1005
  outro('✨ Setup complete! Listening for new messages... do not close this process.')
915
1006
  }
916
1007