kimaki 0.4.17 → 0.4.19

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/README.md CHANGED
@@ -30,27 +30,6 @@ This will guide you through:
30
30
  kimaki
31
31
  ```
32
32
 
33
- ### Send a session to Discord
34
-
35
- Send an OpenCode session to Discord from the CLI:
36
-
37
- ```bash
38
- kimaki send-to-discord <session-id>
39
- ```
40
-
41
- Options:
42
- - `-d, --directory <dir>` - Project directory (defaults to current working directory)
43
-
44
- ### OpenCode Integration
45
-
46
- To use the `/send-to-kimaki-discord` command in OpenCode:
47
-
48
- ```bash
49
- npx kimaki install-plugin
50
- ```
51
-
52
- Then use `/send-to-kimaki-discord` in OpenCode to send the current session to Discord.
53
-
54
33
  ## Discord Slash Commands
55
34
 
56
35
  Once the bot is running, you can use these commands in Discord:
package/dist/cli.js CHANGED
@@ -6,26 +6,40 @@ import { getChannelsWithDescriptions, createDiscordClient, getDatabase, startDis
6
6
  import { Events, ChannelType, REST, Routes, SlashCommandBuilder, AttachmentBuilder, } from 'discord.js';
7
7
  import path from 'node:path';
8
8
  import fs from 'node:fs';
9
- import { createRequire } from 'node:module';
10
- import os from 'node:os';
11
9
  import { createLogger } from './logger.js';
12
10
  import { spawn, spawnSync, execSync } from 'node:child_process';
11
+ import http from 'node:http';
13
12
  const cliLogger = createLogger('CLI');
14
13
  const cli = cac('kimaki');
15
14
  process.title = 'kimaki';
16
- process.on('SIGUSR2', () => {
17
- cliLogger.info('Received SIGUSR2, restarting process in 1000ms...');
18
- setTimeout(() => {
19
- cliLogger.info('Restarting...');
20
- spawn(process.argv[0], [...process.execArgv, ...process.argv.slice(1)], {
21
- stdio: 'inherit',
22
- detached: true,
23
- cwd: process.cwd(),
24
- env: process.env,
25
- }).unref();
26
- process.exit(0);
27
- }, 1000);
28
- });
15
+ const LOCK_PORT = 29988;
16
+ async function checkSingleInstance() {
17
+ try {
18
+ const response = await fetch(`http://127.0.0.1:${LOCK_PORT}`, {
19
+ signal: AbortSignal.timeout(1000),
20
+ });
21
+ if (response.ok) {
22
+ cliLogger.error('Another kimaki instance is already running');
23
+ process.exit(1);
24
+ }
25
+ }
26
+ catch {
27
+ // Connection refused means no instance running, continue
28
+ }
29
+ }
30
+ function startLockServer() {
31
+ const server = http.createServer((req, res) => {
32
+ res.writeHead(200);
33
+ res.end('kimaki');
34
+ });
35
+ server.listen(LOCK_PORT, '127.0.0.1');
36
+ server.on('error', (err) => {
37
+ if (err.code === 'EADDRINUSE') {
38
+ cliLogger.error('Another kimaki instance is already running');
39
+ process.exit(1);
40
+ }
41
+ });
42
+ }
29
43
  const EXIT_NO_RESTART = 64;
30
44
  async function registerCommands(token, appId) {
31
45
  const commands = [
@@ -72,6 +86,17 @@ async function registerCommands(token, appId) {
72
86
  return option;
73
87
  })
74
88
  .toJSON(),
89
+ new SlashCommandBuilder()
90
+ .setName('add-new-project')
91
+ .setDescription('Create a new project folder, initialize git, and start a session')
92
+ .addStringOption((option) => {
93
+ option
94
+ .setName('name')
95
+ .setDescription('Name for the new project folder')
96
+ .setRequired(true);
97
+ return option;
98
+ })
99
+ .toJSON(),
75
100
  new SlashCommandBuilder()
76
101
  .setName('accept')
77
102
  .setDescription('Accept a pending permission request (this request only)')
@@ -88,6 +113,10 @@ async function registerCommands(token, appId) {
88
113
  .setName('abort')
89
114
  .setDescription('Abort the current OpenCode request in this thread')
90
115
  .toJSON(),
116
+ new SlashCommandBuilder()
117
+ .setName('share')
118
+ .setDescription('Share the current session as a public URL')
119
+ .toJSON(),
91
120
  ];
92
121
  const rest = new REST().setToken(token);
93
122
  try {
@@ -431,6 +460,8 @@ cli
431
460
  .option('--add-channels', 'Select OpenCode projects to create Discord channels before starting')
432
461
  .action(async (options) => {
433
462
  try {
463
+ await checkSingleInstance();
464
+ startLockServer();
434
465
  await run({
435
466
  restart: options.restart,
436
467
  addChannels: options.addChannels,
@@ -441,126 +472,6 @@ cli
441
472
  process.exit(EXIT_NO_RESTART);
442
473
  }
443
474
  });
444
- cli
445
- .command('send-to-discord <sessionId>', 'Send an OpenCode session to Discord and create a thread for it')
446
- .option('-d, --directory <dir>', 'Project directory (defaults to current working directory)')
447
- .action(async (sessionId, options) => {
448
- try {
449
- const directory = options.directory || process.cwd();
450
- const db = getDatabase();
451
- const botRow = db
452
- .prepare('SELECT app_id, token FROM bot_tokens ORDER BY created_at DESC LIMIT 1')
453
- .get();
454
- if (!botRow) {
455
- cliLogger.error('No bot credentials found. Run `kimaki` first to set up the bot.');
456
- process.exit(EXIT_NO_RESTART);
457
- }
458
- const channelRow = db
459
- .prepare('SELECT channel_id FROM channel_directories WHERE directory = ? AND channel_type = ?')
460
- .get(directory, 'text');
461
- if (!channelRow) {
462
- cliLogger.error(`No Discord channel found for directory: ${directory}\n` +
463
- 'Run `kimaki --add-channels` to create a channel for this project.');
464
- process.exit(EXIT_NO_RESTART);
465
- }
466
- const s = spinner();
467
- s.start('Connecting to Discord...');
468
- const discordClient = await createDiscordClient();
469
- await new Promise((resolve, reject) => {
470
- discordClient.once(Events.ClientReady, () => {
471
- resolve();
472
- });
473
- discordClient.once(Events.Error, reject);
474
- discordClient.login(botRow.token).catch(reject);
475
- });
476
- s.stop('Connected to Discord!');
477
- const channel = await discordClient.channels.fetch(channelRow.channel_id);
478
- if (!channel || channel.type !== ChannelType.GuildText) {
479
- cliLogger.error('Could not find the text channel or it is not a text channel');
480
- discordClient.destroy();
481
- process.exit(EXIT_NO_RESTART);
482
- }
483
- const textChannel = channel;
484
- s.start('Fetching session from OpenCode...');
485
- const getClient = await initializeOpencodeForDirectory(directory);
486
- const sessionResponse = await getClient().session.get({
487
- path: { id: sessionId },
488
- });
489
- if (!sessionResponse.data) {
490
- s.stop('Session not found');
491
- discordClient.destroy();
492
- process.exit(EXIT_NO_RESTART);
493
- }
494
- const session = sessionResponse.data;
495
- s.stop(`Found session: ${session.title}`);
496
- s.start('Creating Discord thread...');
497
- const thread = await textChannel.threads.create({
498
- name: `Resume: ${session.title}`.slice(0, 100),
499
- autoArchiveDuration: 1440,
500
- reason: `Resuming session ${sessionId} from CLI`,
501
- });
502
- db.prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)').run(thread.id, sessionId);
503
- s.stop('Created Discord thread!');
504
- s.start('Loading session messages...');
505
- const messagesResponse = await getClient().session.messages({
506
- path: { id: sessionId },
507
- });
508
- if (!messagesResponse.data) {
509
- s.stop('Failed to fetch session messages');
510
- discordClient.destroy();
511
- process.exit(EXIT_NO_RESTART);
512
- }
513
- const messages = messagesResponse.data;
514
- await thread.send(`šŸ“‚ **Resumed session:** ${session.title}\nšŸ“… **Created:** ${new Date(session.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`);
515
- let messageCount = 0;
516
- for (const message of messages) {
517
- if (message.info.role === 'user') {
518
- const userParts = message.parts.filter((p) => p.type === 'text' && !p.synthetic);
519
- const userTexts = userParts
520
- .map((p) => {
521
- if (p.type === 'text') {
522
- return p.text;
523
- }
524
- return '';
525
- })
526
- .filter((t) => t.trim());
527
- const userText = userTexts.join('\n\n');
528
- if (userText) {
529
- const truncated = userText.length > 1900 ? userText.slice(0, 1900) + '…' : userText;
530
- await thread.send(`**User:**\n${truncated}`);
531
- }
532
- }
533
- else if (message.info.role === 'assistant') {
534
- const textParts = message.parts.filter((p) => p.type === 'text');
535
- const texts = textParts
536
- .map((p) => {
537
- if (p.type === 'text') {
538
- return p.text;
539
- }
540
- return '';
541
- })
542
- .filter((t) => t?.trim());
543
- if (texts.length > 0) {
544
- const combinedText = texts.join('\n\n');
545
- const truncated = combinedText.length > 1900 ? combinedText.slice(0, 1900) + '…' : combinedText;
546
- await thread.send(truncated);
547
- }
548
- }
549
- messageCount++;
550
- }
551
- await thread.send(`āœ… **Session resumed!** Loaded ${messageCount} messages.\n\nYou can now continue the conversation by sending messages in this thread.`);
552
- s.stop(`Loaded ${messageCount} messages`);
553
- const guildId = textChannel.guildId;
554
- const threadUrl = `https://discord.com/channels/${guildId}/${thread.id}`;
555
- note(`Session "${session.title}" has been sent to Discord!\n\nThread: ${threadUrl}`, 'āœ… Success');
556
- discordClient.destroy();
557
- process.exit(0);
558
- }
559
- catch (error) {
560
- cliLogger.error('Error:', error instanceof Error ? error.message : String(error));
561
- process.exit(EXIT_NO_RESTART);
562
- }
563
- });
564
475
  cli
565
476
  .command('upload-to-discord [...files]', 'Upload files to a Discord thread for a session')
566
477
  .option('-s, --session <sessionId>', 'OpenCode session ID')
@@ -627,24 +538,5 @@ cli
627
538
  process.exit(EXIT_NO_RESTART);
628
539
  }
629
540
  });
630
- cli
631
- .command('install-plugin', 'Install the OpenCode command for kimaki Discord integration')
632
- .action(async () => {
633
- try {
634
- const require = createRequire(import.meta.url);
635
- const sendCommandSrc = require.resolve('./opencode-command-send-to-discord.md');
636
- const opencodeConfig = path.join(os.homedir(), '.config', 'opencode');
637
- const commandDir = path.join(opencodeConfig, 'command');
638
- fs.mkdirSync(commandDir, { recursive: true });
639
- const sendCommandDest = path.join(commandDir, 'send-to-kimaki-discord.md');
640
- fs.copyFileSync(sendCommandSrc, sendCommandDest);
641
- note(`Command installed:\n- ${sendCommandDest}\n\nUse /send-to-kimaki-discord to send session to Discord.`, 'āœ… Installed');
642
- process.exit(0);
643
- }
644
- catch (error) {
645
- cliLogger.error('Error:', error instanceof Error ? error.message : String(error));
646
- process.exit(EXIT_NO_RESTART);
647
- }
648
- });
649
541
  cli.help();
650
542
  cli.parse();