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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.17",
5
+ "version": "0.4.19",
6
6
  "scripts": {
7
7
  "dev": "tsx --env-file .env src/cli.ts",
8
8
  "prepublishOnly": "pnpm tsc",
@@ -21,7 +21,7 @@
21
21
  "bin.js"
22
22
  ],
23
23
  "devDependencies": {
24
- "@opencode-ai/plugin": "^1.0.119",
24
+ "@opencode-ai/plugin": "^1.0.169",
25
25
  "@types/better-sqlite3": "^7.6.13",
26
26
  "@types/bun": "latest",
27
27
  "@types/js-yaml": "^4.0.9",
@@ -30,15 +30,15 @@
30
30
  "tsx": "^4.20.5"
31
31
  },
32
32
  "dependencies": {
33
- "@ai-sdk/google": "^2.0.16",
33
+ "@ai-sdk/google": "^2.0.47",
34
34
  "@clack/prompts": "^0.11.0",
35
35
  "@discordjs/opus": "^0.10.0",
36
36
  "@discordjs/voice": "^0.19.0",
37
- "@google/genai": "^1.16.0",
38
- "@opencode-ai/sdk": "^1.0.115",
37
+ "@google/genai": "^1.34.0",
38
+ "@opencode-ai/sdk": "^1.0.169",
39
39
  "@purinton/resampler": "^1.0.4",
40
40
  "@snazzah/davey": "^0.1.6",
41
- "ai": "^5.0.29",
41
+ "ai": "^5.0.114",
42
42
  "better-sqlite3": "^12.3.0",
43
43
  "cac": "^6.7.14",
44
44
  "date-fns": "^4.1.0",
@@ -53,6 +53,6 @@
53
53
  "prism-media": "^1.3.5",
54
54
  "string-dedent": "^3.0.2",
55
55
  "undici": "^7.16.0",
56
- "zod": "^4.0.17"
56
+ "zod": "^4.2.1"
57
57
  }
58
58
  }
package/src/cli.ts CHANGED
@@ -37,29 +37,48 @@ import {
37
37
  } from 'discord.js'
38
38
  import path from 'node:path'
39
39
  import fs from 'node:fs'
40
- import { createRequire } from 'node:module'
41
- import os from 'node:os'
40
+
41
+
42
42
  import { createLogger } from './logger.js'
43
43
  import { spawn, spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
44
+ import http from 'node:http'
44
45
 
45
46
  const cliLogger = createLogger('CLI')
46
47
  const cli = cac('kimaki')
47
48
 
48
49
  process.title = 'kimaki'
49
50
 
50
- process.on('SIGUSR2', () => {
51
- cliLogger.info('Received SIGUSR2, restarting process in 1000ms...')
52
- setTimeout(() => {
53
- cliLogger.info('Restarting...')
54
- spawn(process.argv[0]!, [...process.execArgv, ...process.argv.slice(1)], {
55
- stdio: 'inherit',
56
- detached: true,
57
- cwd: process.cwd(),
58
- env: process.env,
59
- }).unref()
60
- process.exit(0)
61
- }, 1000)
62
- })
51
+ const LOCK_PORT = 29988
52
+
53
+ async function checkSingleInstance(): Promise<void> {
54
+ try {
55
+ const response = await fetch(`http://127.0.0.1:${LOCK_PORT}`, {
56
+ signal: AbortSignal.timeout(1000),
57
+ })
58
+ if (response.ok) {
59
+ cliLogger.error('Another kimaki instance is already running')
60
+ process.exit(1)
61
+ }
62
+ } catch {
63
+ // Connection refused means no instance running, continue
64
+ }
65
+ }
66
+
67
+ function startLockServer(): void {
68
+ const server = http.createServer((req, res) => {
69
+ res.writeHead(200)
70
+ res.end('kimaki')
71
+ })
72
+ server.listen(LOCK_PORT, '127.0.0.1')
73
+ server.on('error', (err: NodeJS.ErrnoException) => {
74
+ if (err.code === 'EADDRINUSE') {
75
+ cliLogger.error('Another kimaki instance is already running')
76
+ process.exit(1)
77
+ }
78
+ })
79
+ }
80
+
81
+
63
82
 
64
83
  const EXIT_NO_RESTART = 64
65
84
 
@@ -129,6 +148,18 @@ async function registerCommands(token: string, appId: string) {
129
148
  return option
130
149
  })
131
150
  .toJSON(),
151
+ new SlashCommandBuilder()
152
+ .setName('add-new-project')
153
+ .setDescription('Create a new project folder, initialize git, and start a session')
154
+ .addStringOption((option) => {
155
+ option
156
+ .setName('name')
157
+ .setDescription('Name for the new project folder')
158
+ .setRequired(true)
159
+
160
+ return option
161
+ })
162
+ .toJSON(),
132
163
  new SlashCommandBuilder()
133
164
  .setName('accept')
134
165
  .setDescription('Accept a pending permission request (this request only)')
@@ -145,6 +176,10 @@ async function registerCommands(token: string, appId: string) {
145
176
  .setName('abort')
146
177
  .setDescription('Abort the current OpenCode request in this thread')
147
178
  .toJSON(),
179
+ new SlashCommandBuilder()
180
+ .setName('share')
181
+ .setDescription('Share the current session as a public URL')
182
+ .toJSON(),
148
183
  ]
149
184
 
150
185
  const rest = new REST().setToken(token)
@@ -649,6 +684,8 @@ cli
649
684
  )
650
685
  .action(async (options: { restart?: boolean; addChannels?: boolean }) => {
651
686
  try {
687
+ await checkSingleInstance()
688
+ startLockServer()
652
689
  await run({
653
690
  restart: options.restart,
654
691
  addChannels: options.addChannels,
@@ -662,179 +699,7 @@ cli
662
699
  }
663
700
  })
664
701
 
665
- cli
666
- .command(
667
- 'send-to-discord <sessionId>',
668
- 'Send an OpenCode session to Discord and create a thread for it',
669
- )
670
- .option('-d, --directory <dir>', 'Project directory (defaults to current working directory)')
671
- .action(async (sessionId: string, options: { directory?: string }) => {
672
- try {
673
- const directory = options.directory || process.cwd()
674
-
675
- const db = getDatabase()
676
-
677
- const botRow = db
678
- .prepare(
679
- 'SELECT app_id, token FROM bot_tokens ORDER BY created_at DESC LIMIT 1',
680
- )
681
- .get() as { app_id: string; token: string } | undefined
682
-
683
- if (!botRow) {
684
- cliLogger.error('No bot credentials found. Run `kimaki` first to set up the bot.')
685
- process.exit(EXIT_NO_RESTART)
686
- }
687
-
688
- const channelRow = db
689
- .prepare(
690
- 'SELECT channel_id FROM channel_directories WHERE directory = ? AND channel_type = ?',
691
- )
692
- .get(directory, 'text') as { channel_id: string } | undefined
693
-
694
- if (!channelRow) {
695
- cliLogger.error(
696
- `No Discord channel found for directory: ${directory}\n` +
697
- 'Run `kimaki --add-channels` to create a channel for this project.',
698
- )
699
- process.exit(EXIT_NO_RESTART)
700
- }
701
-
702
- const s = spinner()
703
- s.start('Connecting to Discord...')
704
-
705
- const discordClient = await createDiscordClient()
706
702
 
707
- await new Promise<void>((resolve, reject) => {
708
- discordClient.once(Events.ClientReady, () => {
709
- resolve()
710
- })
711
- discordClient.once(Events.Error, reject)
712
- discordClient.login(botRow.token).catch(reject)
713
- })
714
-
715
- s.stop('Connected to Discord!')
716
-
717
- const channel = await discordClient.channels.fetch(channelRow.channel_id)
718
- if (!channel || channel.type !== ChannelType.GuildText) {
719
- cliLogger.error('Could not find the text channel or it is not a text channel')
720
- discordClient.destroy()
721
- process.exit(EXIT_NO_RESTART)
722
- }
723
-
724
- const textChannel = channel as import('discord.js').TextChannel
725
-
726
- s.start('Fetching session from OpenCode...')
727
-
728
- const getClient = await initializeOpencodeForDirectory(directory)
729
- const sessionResponse = await getClient().session.get({
730
- path: { id: sessionId },
731
- })
732
-
733
- if (!sessionResponse.data) {
734
- s.stop('Session not found')
735
- discordClient.destroy()
736
- process.exit(EXIT_NO_RESTART)
737
- }
738
-
739
- const session = sessionResponse.data
740
- s.stop(`Found session: ${session.title}`)
741
-
742
- s.start('Creating Discord thread...')
743
-
744
- const thread = await textChannel.threads.create({
745
- name: `Resume: ${session.title}`.slice(0, 100),
746
- autoArchiveDuration: 1440,
747
- reason: `Resuming session ${sessionId} from CLI`,
748
- })
749
-
750
- db.prepare(
751
- 'INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)',
752
- ).run(thread.id, sessionId)
753
-
754
- s.stop('Created Discord thread!')
755
-
756
- s.start('Loading session messages...')
757
-
758
- const messagesResponse = await getClient().session.messages({
759
- path: { id: sessionId },
760
- })
761
-
762
- if (!messagesResponse.data) {
763
- s.stop('Failed to fetch session messages')
764
- discordClient.destroy()
765
- process.exit(EXIT_NO_RESTART)
766
- }
767
-
768
- const messages = messagesResponse.data
769
-
770
- await thread.send(
771
- `šŸ“‚ **Resumed session:** ${session.title}\nšŸ“… **Created:** ${new Date(session.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`,
772
- )
773
-
774
- let messageCount = 0
775
- for (const message of messages) {
776
- if (message.info.role === 'user') {
777
- const userParts = message.parts.filter(
778
- (p) => p.type === 'text' && !p.synthetic,
779
- )
780
- const userTexts = userParts
781
- .map((p) => {
782
- if (p.type === 'text') {
783
- return p.text
784
- }
785
- return ''
786
- })
787
- .filter((t) => t.trim())
788
-
789
- const userText = userTexts.join('\n\n')
790
- if (userText) {
791
- const truncated = userText.length > 1900 ? userText.slice(0, 1900) + '…' : userText
792
- await thread.send(`**User:**\n${truncated}`)
793
- }
794
- } else if (message.info.role === 'assistant') {
795
- const textParts = message.parts.filter((p) => p.type === 'text')
796
- const texts = textParts
797
- .map((p) => {
798
- if (p.type === 'text') {
799
- return p.text
800
- }
801
- return ''
802
- })
803
- .filter((t) => t?.trim())
804
-
805
- if (texts.length > 0) {
806
- const combinedText = texts.join('\n\n')
807
- const truncated = combinedText.length > 1900 ? combinedText.slice(0, 1900) + '…' : combinedText
808
- await thread.send(truncated)
809
- }
810
- }
811
- messageCount++
812
- }
813
-
814
- await thread.send(
815
- `āœ… **Session resumed!** Loaded ${messageCount} messages.\n\nYou can now continue the conversation by sending messages in this thread.`,
816
- )
817
-
818
- s.stop(`Loaded ${messageCount} messages`)
819
-
820
- const guildId = textChannel.guildId
821
- const threadUrl = `https://discord.com/channels/${guildId}/${thread.id}`
822
-
823
- note(
824
- `Session "${session.title}" has been sent to Discord!\n\nThread: ${threadUrl}`,
825
- 'āœ… Success',
826
- )
827
-
828
- discordClient.destroy()
829
- process.exit(0)
830
- } catch (error) {
831
- cliLogger.error(
832
- 'Error:',
833
- error instanceof Error ? error.message : String(error),
834
- )
835
- process.exit(EXIT_NO_RESTART)
836
- }
837
- })
838
703
 
839
704
  cli
840
705
  .command('upload-to-discord [...files]', 'Upload files to a Discord thread for a session')
@@ -929,36 +794,7 @@ cli
929
794
  }
930
795
  })
931
796
 
932
- cli
933
- .command('install-plugin', 'Install the OpenCode command for kimaki Discord integration')
934
- .action(async () => {
935
- try {
936
- const require = createRequire(import.meta.url)
937
- const sendCommandSrc = require.resolve('./opencode-command-send-to-discord.md')
938
-
939
- const opencodeConfig = path.join(os.homedir(), '.config', 'opencode')
940
- const commandDir = path.join(opencodeConfig, 'command')
941
797
 
942
- fs.mkdirSync(commandDir, { recursive: true })
943
-
944
- const sendCommandDest = path.join(commandDir, 'send-to-kimaki-discord.md')
945
-
946
- fs.copyFileSync(sendCommandSrc, sendCommandDest)
947
-
948
- note(
949
- `Command installed:\n- ${sendCommandDest}\n\nUse /send-to-kimaki-discord to send session to Discord.`,
950
- 'āœ… Installed',
951
- )
952
-
953
- process.exit(0)
954
- } catch (error) {
955
- cliLogger.error(
956
- 'Error:',
957
- error instanceof Error ? error.message : String(error),
958
- )
959
- process.exit(EXIT_NO_RESTART)
960
- }
961
- })
962
798
 
963
799
  cli.help()
964
800
  cli.parse()