kimaki 0.4.18 ā 0.4.20
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 +0 -21
- package/dist/cli.js +46 -154
- package/dist/discordBot.js +308 -107
- package/dist/genai.js +1 -1
- package/dist/xai-realtime.js +95 -0
- package/package.json +7 -7
- package/src/cli.ts +52 -216
- package/src/discordBot.ts +369 -114
- package/src/genai-worker.ts +1 -1
- package/src/genai.ts +1 -1
- package/src/opencode-command-send-to-discord.md +0 -12
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
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();
|