kimaki 0.4.35 → 0.4.36
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/ai-tool-to-genai.js +1 -3
- package/dist/channel-management.js +1 -1
- package/dist/cli.js +135 -39
- package/dist/commands/abort.js +1 -1
- package/dist/commands/add-project.js +1 -1
- package/dist/commands/agent.js +6 -2
- package/dist/commands/ask-question.js +2 -1
- package/dist/commands/fork.js +7 -7
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +109 -0
- package/dist/commands/resume.js +3 -5
- package/dist/commands/session.js +2 -2
- package/dist/commands/share.js +1 -1
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +3 -6
- package/dist/config.js +1 -1
- package/dist/discord-bot.js +4 -10
- package/dist/discord-utils.js +33 -9
- package/dist/genai.js +4 -6
- package/dist/interaction-handler.js +8 -1
- package/dist/markdown.js +1 -3
- package/dist/message-formatting.js +7 -3
- package/dist/openai-realtime.js +3 -5
- package/dist/opencode.js +1 -1
- package/dist/session-handler.js +25 -15
- package/dist/system-message.js +5 -3
- package/dist/tools.js +9 -22
- package/dist/voice-handler.js +9 -12
- package/dist/voice.js +5 -3
- package/dist/xml.js +2 -4
- package/package.json +3 -2
- package/src/__snapshots__/compact-session-context-no-system.md +24 -24
- package/src/__snapshots__/compact-session-context.md +31 -31
- package/src/ai-tool-to-genai.ts +3 -11
- package/src/channel-management.ts +14 -25
- package/src/cli.ts +282 -195
- package/src/commands/abort.ts +1 -3
- package/src/commands/add-project.ts +8 -14
- package/src/commands/agent.ts +16 -9
- package/src/commands/ask-question.ts +8 -7
- package/src/commands/create-new-project.ts +8 -14
- package/src/commands/fork.ts +23 -27
- package/src/commands/model.ts +14 -11
- package/src/commands/permissions.ts +1 -1
- package/src/commands/queue.ts +6 -19
- package/src/commands/remove-project.ts +136 -0
- package/src/commands/resume.ts +11 -30
- package/src/commands/session.ts +4 -13
- package/src/commands/share.ts +1 -3
- package/src/commands/types.ts +1 -3
- package/src/commands/undo-redo.ts +6 -18
- package/src/commands/user-command.ts +8 -10
- package/src/config.ts +5 -5
- package/src/database.ts +10 -8
- package/src/discord-bot.ts +22 -46
- package/src/discord-utils.ts +35 -18
- package/src/escape-backticks.test.ts +0 -2
- package/src/format-tables.ts +1 -4
- package/src/genai-worker-wrapper.ts +3 -9
- package/src/genai-worker.ts +4 -19
- package/src/genai.ts +10 -42
- package/src/interaction-handler.ts +133 -121
- package/src/markdown.test.ts +10 -32
- package/src/markdown.ts +6 -14
- package/src/message-formatting.ts +13 -14
- package/src/openai-realtime.ts +25 -47
- package/src/opencode.ts +24 -34
- package/src/session-handler.ts +91 -61
- package/src/system-message.ts +13 -3
- package/src/tools.ts +13 -39
- package/src/utils.ts +1 -4
- package/src/voice-handler.ts +34 -78
- package/src/voice.ts +11 -19
- package/src/xml.test.ts +1 -1
- package/src/xml.ts +3 -12
package/src/cli.ts
CHANGED
|
@@ -41,7 +41,6 @@ import {
|
|
|
41
41
|
import path from 'node:path'
|
|
42
42
|
import fs from 'node:fs'
|
|
43
43
|
|
|
44
|
-
|
|
45
44
|
import { createLogger } from './logger.js'
|
|
46
45
|
import { spawn, spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
|
|
47
46
|
import http from 'node:http'
|
|
@@ -60,11 +59,22 @@ async function killProcessOnPort(port: number): Promise<boolean> {
|
|
|
60
59
|
try {
|
|
61
60
|
if (isWindows) {
|
|
62
61
|
// Windows: find PID using netstat, then kill
|
|
63
|
-
const result = spawnSync(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
const result = spawnSync(
|
|
63
|
+
'cmd',
|
|
64
|
+
[
|
|
65
|
+
'/c',
|
|
66
|
+
`for /f "tokens=5" %a in ('netstat -ano ^| findstr :${port} ^| findstr LISTENING') do @echo %a`,
|
|
67
|
+
],
|
|
68
|
+
{
|
|
69
|
+
shell: false,
|
|
70
|
+
encoding: 'utf-8',
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
const pids = result.stdout
|
|
74
|
+
?.trim()
|
|
75
|
+
.split('\n')
|
|
76
|
+
.map((p) => p.trim())
|
|
77
|
+
.filter((p) => /^\d+$/.test(p))
|
|
68
78
|
// Filter out our own PID and take the first (oldest)
|
|
69
79
|
const targetPid = pids?.find((p) => parseInt(p, 10) !== myPid)
|
|
70
80
|
if (targetPid) {
|
|
@@ -78,7 +88,11 @@ async function killProcessOnPort(port: number): Promise<boolean> {
|
|
|
78
88
|
shell: false,
|
|
79
89
|
encoding: 'utf-8',
|
|
80
90
|
})
|
|
81
|
-
const pids = result.stdout
|
|
91
|
+
const pids = result.stdout
|
|
92
|
+
?.trim()
|
|
93
|
+
.split('\n')
|
|
94
|
+
.map((p) => p.trim())
|
|
95
|
+
.filter((p) => /^\d+$/.test(p))
|
|
82
96
|
// Filter out our own PID and take the first (oldest)
|
|
83
97
|
const targetPid = pids?.find((p) => parseInt(p, 10) !== myPid)
|
|
84
98
|
if (targetPid) {
|
|
@@ -104,7 +118,9 @@ async function checkSingleInstance(): Promise<void> {
|
|
|
104
118
|
cliLogger.log(`Another kimaki instance detected for data dir: ${getDataDir()}`)
|
|
105
119
|
await killProcessOnPort(lockPort)
|
|
106
120
|
// Wait a moment for port to be released
|
|
107
|
-
await new Promise((resolve) => {
|
|
121
|
+
await new Promise((resolve) => {
|
|
122
|
+
setTimeout(resolve, 500)
|
|
123
|
+
})
|
|
108
124
|
}
|
|
109
125
|
} catch {
|
|
110
126
|
cliLogger.debug('No other kimaki instance detected on lock port')
|
|
@@ -127,7 +143,9 @@ async function startLockServer(): Promise<void> {
|
|
|
127
143
|
if (err.code === 'EADDRINUSE') {
|
|
128
144
|
cliLogger.log('Port still in use, retrying...')
|
|
129
145
|
await killProcessOnPort(lockPort)
|
|
130
|
-
await new Promise((r) => {
|
|
146
|
+
await new Promise((r) => {
|
|
147
|
+
setTimeout(r, 500)
|
|
148
|
+
})
|
|
131
149
|
// Retry once
|
|
132
150
|
server.listen(lockPort, '127.0.0.1')
|
|
133
151
|
} else {
|
|
@@ -137,8 +155,6 @@ async function startLockServer(): Promise<void> {
|
|
|
137
155
|
})
|
|
138
156
|
}
|
|
139
157
|
|
|
140
|
-
|
|
141
|
-
|
|
142
158
|
const EXIT_NO_RESTART = 64
|
|
143
159
|
|
|
144
160
|
type Project = {
|
|
@@ -160,7 +176,11 @@ type CliOptions = {
|
|
|
160
176
|
// Commands to skip when registering user commands (reserved names)
|
|
161
177
|
const SKIP_USER_COMMANDS = ['init']
|
|
162
178
|
|
|
163
|
-
async function registerCommands(
|
|
179
|
+
async function registerCommands(
|
|
180
|
+
token: string,
|
|
181
|
+
appId: string,
|
|
182
|
+
userCommands: OpencodeCommand[] = [],
|
|
183
|
+
) {
|
|
164
184
|
const commands = [
|
|
165
185
|
new SlashCommandBuilder()
|
|
166
186
|
.setName('resume')
|
|
@@ -179,19 +199,14 @@ async function registerCommands(token: string, appId: string, userCommands: Open
|
|
|
179
199
|
.setName('session')
|
|
180
200
|
.setDescription('Start a new OpenCode session')
|
|
181
201
|
.addStringOption((option) => {
|
|
182
|
-
option
|
|
183
|
-
.setName('prompt')
|
|
184
|
-
.setDescription('Prompt content for the session')
|
|
185
|
-
.setRequired(true)
|
|
202
|
+
option.setName('prompt').setDescription('Prompt content for the session').setRequired(true)
|
|
186
203
|
|
|
187
204
|
return option
|
|
188
205
|
})
|
|
189
206
|
.addStringOption((option) => {
|
|
190
207
|
option
|
|
191
208
|
.setName('files')
|
|
192
|
-
.setDescription(
|
|
193
|
-
'Files to mention (comma or space separated; autocomplete)',
|
|
194
|
-
)
|
|
209
|
+
.setDescription('Files to mention (comma or space separated; autocomplete)')
|
|
195
210
|
.setAutocomplete(true)
|
|
196
211
|
.setMaxLength(6000)
|
|
197
212
|
|
|
@@ -220,13 +235,23 @@ async function registerCommands(token: string, appId: string, userCommands: Open
|
|
|
220
235
|
})
|
|
221
236
|
.toJSON(),
|
|
222
237
|
new SlashCommandBuilder()
|
|
223
|
-
.setName('
|
|
224
|
-
.setDescription('
|
|
238
|
+
.setName('remove-project')
|
|
239
|
+
.setDescription('Remove Discord channels for a project')
|
|
225
240
|
.addStringOption((option) => {
|
|
226
241
|
option
|
|
227
|
-
.setName('
|
|
228
|
-
.setDescription('
|
|
242
|
+
.setName('project')
|
|
243
|
+
.setDescription('Select a project to remove')
|
|
229
244
|
.setRequired(true)
|
|
245
|
+
.setAutocomplete(true)
|
|
246
|
+
|
|
247
|
+
return option
|
|
248
|
+
})
|
|
249
|
+
.toJSON(),
|
|
250
|
+
new SlashCommandBuilder()
|
|
251
|
+
.setName('create-new-project')
|
|
252
|
+
.setDescription('Create a new project folder, initialize git, and start a session')
|
|
253
|
+
.addStringOption((option) => {
|
|
254
|
+
option.setName('name').setDescription('Name for the new project folder').setRequired(true)
|
|
230
255
|
|
|
231
256
|
return option
|
|
232
257
|
})
|
|
@@ -259,10 +284,7 @@ async function registerCommands(token: string, appId: string, userCommands: Open
|
|
|
259
284
|
.setName('queue')
|
|
260
285
|
.setDescription('Queue a message to be sent after the current response finishes')
|
|
261
286
|
.addStringOption((option) => {
|
|
262
|
-
option
|
|
263
|
-
.setName('message')
|
|
264
|
-
.setDescription('The message to queue')
|
|
265
|
-
.setRequired(true)
|
|
287
|
+
option.setName('message').setDescription('The message to queue').setRequired(true)
|
|
266
288
|
|
|
267
289
|
return option
|
|
268
290
|
})
|
|
@@ -294,7 +316,7 @@ async function registerCommands(token: string, appId: string, userCommands: Open
|
|
|
294
316
|
|
|
295
317
|
commands.push(
|
|
296
318
|
new SlashCommandBuilder()
|
|
297
|
-
.setName(commandName)
|
|
319
|
+
.setName(commandName.slice(0, 32)) // Discord limits to 32 chars
|
|
298
320
|
.setDescription(description.slice(0, 100)) // Discord limits to 100 chars
|
|
299
321
|
.addStringOption((option) => {
|
|
300
322
|
option
|
|
@@ -314,19 +336,13 @@ async function registerCommands(token: string, appId: string, userCommands: Open
|
|
|
314
336
|
body: commands,
|
|
315
337
|
})) as any[]
|
|
316
338
|
|
|
317
|
-
cliLogger.info(
|
|
318
|
-
`COMMANDS: Successfully registered ${data.length} slash commands`,
|
|
319
|
-
)
|
|
339
|
+
cliLogger.info(`COMMANDS: Successfully registered ${data.length} slash commands`)
|
|
320
340
|
} catch (error) {
|
|
321
|
-
cliLogger.error(
|
|
322
|
-
'COMMANDS: Failed to register slash commands: ' + String(error),
|
|
323
|
-
)
|
|
341
|
+
cliLogger.error('COMMANDS: Failed to register slash commands: ' + String(error))
|
|
324
342
|
throw error
|
|
325
343
|
}
|
|
326
344
|
}
|
|
327
345
|
|
|
328
|
-
|
|
329
|
-
|
|
330
346
|
async function run({ restart, addChannels }: CliOptions) {
|
|
331
347
|
const forceSetup = Boolean(restart)
|
|
332
348
|
|
|
@@ -336,10 +352,7 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
336
352
|
const opencodeCheck = spawnSync('which', ['opencode'], { shell: true })
|
|
337
353
|
|
|
338
354
|
if (opencodeCheck.status !== 0) {
|
|
339
|
-
note(
|
|
340
|
-
'OpenCode CLI is required but not found in your PATH.',
|
|
341
|
-
'⚠️ OpenCode Not Found',
|
|
342
|
-
)
|
|
355
|
+
note('OpenCode CLI is required but not found in your PATH.', '⚠️ OpenCode Not Found')
|
|
343
356
|
|
|
344
357
|
const shouldInstall = await confirm({
|
|
345
358
|
message: 'Would you like to install OpenCode right now?',
|
|
@@ -391,10 +404,7 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
391
404
|
process.env.OPENCODE_PATH = installedPath
|
|
392
405
|
} catch (error) {
|
|
393
406
|
s.stop('Failed to install OpenCode CLI')
|
|
394
|
-
cliLogger.error(
|
|
395
|
-
'Installation error:',
|
|
396
|
-
error instanceof Error ? error.message : String(error),
|
|
397
|
-
)
|
|
407
|
+
cliLogger.error('Installation error:', error instanceof Error ? error.message : String(error))
|
|
398
408
|
process.exit(EXIT_NO_RESTART)
|
|
399
409
|
}
|
|
400
410
|
}
|
|
@@ -404,13 +414,10 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
404
414
|
let token: string
|
|
405
415
|
|
|
406
416
|
const existingBot = db
|
|
407
|
-
.prepare(
|
|
408
|
-
'SELECT app_id, token FROM bot_tokens ORDER BY created_at DESC LIMIT 1',
|
|
409
|
-
)
|
|
417
|
+
.prepare('SELECT app_id, token FROM bot_tokens ORDER BY created_at DESC LIMIT 1')
|
|
410
418
|
.get() as { app_id: string; token: string } | undefined
|
|
411
419
|
|
|
412
|
-
const shouldAddChannels =
|
|
413
|
-
!existingBot?.token || forceSetup || Boolean(addChannels)
|
|
420
|
+
const shouldAddChannels = !existingBot?.token || forceSetup || Boolean(addChannels)
|
|
414
421
|
|
|
415
422
|
if (existingBot && !forceSetup) {
|
|
416
423
|
appId = existingBot.app_id
|
|
@@ -481,8 +488,7 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
481
488
|
'Step 3: Get Bot Token',
|
|
482
489
|
)
|
|
483
490
|
const tokenInput = await password({
|
|
484
|
-
message:
|
|
485
|
-
'Enter your Discord Bot Token (from "Bot" section - click "Reset Token" if needed):',
|
|
491
|
+
message: 'Enter your Discord Bot Token (from "Bot" section - click "Reset Token" if needed):',
|
|
486
492
|
validate(value) {
|
|
487
493
|
if (!value) return 'Bot token is required'
|
|
488
494
|
if (value.length < 50) return 'Invalid token format (too short)'
|
|
@@ -495,10 +501,7 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
495
501
|
}
|
|
496
502
|
token = tokenInput
|
|
497
503
|
|
|
498
|
-
note(
|
|
499
|
-
`You can get a Gemini api Key at https://aistudio.google.com/apikey`,
|
|
500
|
-
`Gemini API Key`,
|
|
501
|
-
)
|
|
504
|
+
note(`You can get a Gemini api Key at https://aistudio.google.com/apikey`, `Gemini API Key`)
|
|
502
505
|
|
|
503
506
|
const geminiApiKey = await password({
|
|
504
507
|
message:
|
|
@@ -516,9 +519,10 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
516
519
|
|
|
517
520
|
// Store API key in database
|
|
518
521
|
if (geminiApiKey) {
|
|
519
|
-
db.prepare(
|
|
520
|
-
|
|
521
|
-
|
|
522
|
+
db.prepare('INSERT OR REPLACE INTO bot_api_keys (app_id, gemini_api_key) VALUES (?, ?)').run(
|
|
523
|
+
appId,
|
|
524
|
+
geminiApiKey || null,
|
|
525
|
+
)
|
|
522
526
|
note('API key saved successfully', 'API Key Stored')
|
|
523
527
|
}
|
|
524
528
|
|
|
@@ -565,9 +569,7 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
565
569
|
guild.roles
|
|
566
570
|
.fetch()
|
|
567
571
|
.then(async (roles) => {
|
|
568
|
-
const existingRole = roles.find(
|
|
569
|
-
(role) => role.name.toLowerCase() === 'kimaki',
|
|
570
|
-
)
|
|
572
|
+
const existingRole = roles.find((role) => role.name.toLowerCase() === 'kimaki')
|
|
571
573
|
if (existingRole) {
|
|
572
574
|
// Move to bottom if not already there
|
|
573
575
|
if (existingRole.position > 1) {
|
|
@@ -596,8 +598,7 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
596
598
|
|
|
597
599
|
const channels = await getChannelsWithDescriptions(guild)
|
|
598
600
|
const kimakiChans = channels.filter(
|
|
599
|
-
(ch) =>
|
|
600
|
-
ch.kimakiDirectory && (!ch.kimakiApp || ch.kimakiApp === appId),
|
|
601
|
+
(ch) => ch.kimakiDirectory && (!ch.kimakiApp || ch.kimakiApp === appId),
|
|
601
602
|
)
|
|
602
603
|
|
|
603
604
|
return { guild, channels: kimakiChans }
|
|
@@ -622,14 +623,10 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
622
623
|
s.stop('Connected to Discord!')
|
|
623
624
|
} catch (error) {
|
|
624
625
|
s.stop('Failed to connect to Discord')
|
|
625
|
-
cliLogger.error(
|
|
626
|
-
'Error: ' + (error instanceof Error ? error.message : String(error)),
|
|
627
|
-
)
|
|
626
|
+
cliLogger.error('Error: ' + (error instanceof Error ? error.message : String(error)))
|
|
628
627
|
process.exit(EXIT_NO_RESTART)
|
|
629
628
|
}
|
|
630
|
-
db.prepare(
|
|
631
|
-
'INSERT OR REPLACE INTO bot_tokens (app_id, token) VALUES (?, ?)',
|
|
632
|
-
).run(appId, token)
|
|
629
|
+
db.prepare('INSERT OR REPLACE INTO bot_tokens (app_id, token) VALUES (?, ?)').run(appId, token)
|
|
633
630
|
|
|
634
631
|
for (const { guild, channels } of kimakiChannels) {
|
|
635
632
|
for (const channel of channels) {
|
|
@@ -639,8 +636,7 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
639
636
|
).run(channel.id, channel.kimakiDirectory, 'text')
|
|
640
637
|
|
|
641
638
|
const voiceChannel = guild.channels.cache.find(
|
|
642
|
-
(ch) =>
|
|
643
|
-
ch.type === ChannelType.GuildVoice && ch.name === channel.name,
|
|
639
|
+
(ch) => ch.type === ChannelType.GuildVoice && ch.name === channel.name,
|
|
644
640
|
)
|
|
645
641
|
|
|
646
642
|
if (voiceChannel) {
|
|
@@ -657,11 +653,7 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
657
653
|
.flatMap(({ guild, channels }) =>
|
|
658
654
|
channels.map((ch) => {
|
|
659
655
|
const appInfo =
|
|
660
|
-
ch.kimakiApp === appId
|
|
661
|
-
? ' (this bot)'
|
|
662
|
-
: ch.kimakiApp
|
|
663
|
-
? ` (app: ${ch.kimakiApp})`
|
|
664
|
-
: ''
|
|
656
|
+
ch.kimakiApp === appId ? ' (this bot)' : ch.kimakiApp ? ` (app: ${ch.kimakiApp})` : ''
|
|
665
657
|
return `#${ch.name} in ${guild.name}: ${ch.kimakiDirectory}${appInfo}`
|
|
666
658
|
}),
|
|
667
659
|
)
|
|
@@ -679,13 +671,19 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
679
671
|
|
|
680
672
|
// Fetch projects and commands in parallel
|
|
681
673
|
const [projects, allUserCommands] = await Promise.all([
|
|
682
|
-
getClient()
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
674
|
+
getClient()
|
|
675
|
+
.project.list({})
|
|
676
|
+
.then((r) => r.data || [])
|
|
677
|
+
.catch((error) => {
|
|
678
|
+
s.stop('Failed to fetch projects')
|
|
679
|
+
cliLogger.error('Error:', error instanceof Error ? error.message : String(error))
|
|
680
|
+
discordClient.destroy()
|
|
681
|
+
process.exit(EXIT_NO_RESTART)
|
|
682
|
+
}),
|
|
683
|
+
getClient()
|
|
684
|
+
.command.list({ query: { directory: currentDir } })
|
|
685
|
+
.then((r) => r.data || [])
|
|
686
|
+
.catch(() => []),
|
|
689
687
|
])
|
|
690
688
|
|
|
691
689
|
s.stop(`Found ${projects.length} OpenCode project(s)`)
|
|
@@ -711,16 +709,10 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
711
709
|
)
|
|
712
710
|
|
|
713
711
|
if (availableProjects.length === 0) {
|
|
714
|
-
note(
|
|
715
|
-
'All OpenCode projects already have Discord channels',
|
|
716
|
-
'No New Projects',
|
|
717
|
-
)
|
|
712
|
+
note('All OpenCode projects already have Discord channels', 'No New Projects')
|
|
718
713
|
}
|
|
719
714
|
|
|
720
|
-
if (
|
|
721
|
-
(!existingDirs?.length && availableProjects.length > 0) ||
|
|
722
|
-
shouldAddChannels
|
|
723
|
-
) {
|
|
715
|
+
if ((!existingDirs?.length && availableProjects.length > 0) || shouldAddChannels) {
|
|
724
716
|
const selectedProjects = await multiselect({
|
|
725
717
|
message: 'Select projects to create Discord channels for:',
|
|
726
718
|
options: availableProjects.map((project) => ({
|
|
@@ -781,17 +773,17 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
781
773
|
guildId: targetGuild.id,
|
|
782
774
|
})
|
|
783
775
|
} catch (error) {
|
|
784
|
-
cliLogger.error(
|
|
776
|
+
cliLogger.error(
|
|
777
|
+
`Failed to create channels for ${path.basename(project.worktree)}:`,
|
|
778
|
+
error,
|
|
779
|
+
)
|
|
785
780
|
}
|
|
786
781
|
}
|
|
787
782
|
|
|
788
783
|
s.stop(`Created ${createdChannels.length} channel(s)`)
|
|
789
784
|
|
|
790
785
|
if (createdChannels.length > 0) {
|
|
791
|
-
note(
|
|
792
|
-
createdChannels.map((ch) => `#${ch.name}`).join('\n'),
|
|
793
|
-
'Created Channels',
|
|
794
|
-
)
|
|
786
|
+
note(createdChannels.map((ch) => `#${ch.name}`).join('\n'), 'Created Channels')
|
|
795
787
|
}
|
|
796
788
|
}
|
|
797
789
|
}
|
|
@@ -850,10 +842,7 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
850
842
|
|
|
851
843
|
if (allChannels.length > 0) {
|
|
852
844
|
const channelLinks = allChannels
|
|
853
|
-
.map(
|
|
854
|
-
(ch) =>
|
|
855
|
-
`• #${ch.name}: https://discord.com/channels/${ch.guildId}/${ch.id}`,
|
|
856
|
-
)
|
|
845
|
+
.map((ch) => `• #${ch.name}: https://discord.com/channels/${ch.guildId}/${ch.id}`)
|
|
857
846
|
.join('\n')
|
|
858
847
|
|
|
859
848
|
note(
|
|
@@ -862,63 +851,62 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
862
851
|
)
|
|
863
852
|
}
|
|
864
853
|
|
|
865
|
-
|
|
854
|
+
note(
|
|
855
|
+
'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.',
|
|
856
|
+
'⚠️ Keep Running',
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
outro('✨ Setup complete! Listening for new messages... do not close this process.')
|
|
866
860
|
}
|
|
867
861
|
|
|
868
862
|
cli
|
|
869
863
|
.command('', 'Set up and run the Kimaki Discord bot')
|
|
870
864
|
.option('--restart', 'Prompt for new credentials even if saved')
|
|
871
|
-
.option(
|
|
872
|
-
|
|
873
|
-
'Select OpenCode projects to create Discord channels before starting',
|
|
874
|
-
)
|
|
875
|
-
.option(
|
|
876
|
-
'--data-dir <path>',
|
|
877
|
-
'Data directory for config and database (default: ~/.kimaki)',
|
|
878
|
-
)
|
|
865
|
+
.option('--add-channels', 'Select OpenCode projects to create Discord channels before starting')
|
|
866
|
+
.option('--data-dir <path>', 'Data directory for config and database (default: ~/.kimaki)')
|
|
879
867
|
.option('--install-url', 'Print the bot install URL and exit')
|
|
880
|
-
.action(
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
868
|
+
.action(
|
|
869
|
+
async (options: {
|
|
870
|
+
restart?: boolean
|
|
871
|
+
addChannels?: boolean
|
|
872
|
+
dataDir?: string
|
|
873
|
+
installUrl?: boolean
|
|
874
|
+
}) => {
|
|
875
|
+
try {
|
|
876
|
+
// Set data directory early, before any database access
|
|
877
|
+
if (options.dataDir) {
|
|
878
|
+
setDataDir(options.dataDir)
|
|
879
|
+
cliLogger.log(`Using data directory: ${getDataDir()}`)
|
|
880
|
+
}
|
|
887
881
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
)
|
|
894
|
-
.get() as { app_id: string } | undefined
|
|
882
|
+
if (options.installUrl) {
|
|
883
|
+
const db = getDatabase()
|
|
884
|
+
const existingBot = db
|
|
885
|
+
.prepare('SELECT app_id FROM bot_tokens ORDER BY created_at DESC LIMIT 1')
|
|
886
|
+
.get() as { app_id: string } | undefined
|
|
895
887
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
888
|
+
if (!existingBot) {
|
|
889
|
+
cliLogger.error('No bot configured yet. Run `kimaki` first to set up.')
|
|
890
|
+
process.exit(EXIT_NO_RESTART)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
console.log(generateBotInstallUrl({ clientId: existingBot.app_id }))
|
|
894
|
+
process.exit(0)
|
|
899
895
|
}
|
|
900
896
|
|
|
901
|
-
|
|
902
|
-
|
|
897
|
+
await checkSingleInstance()
|
|
898
|
+
await startLockServer()
|
|
899
|
+
await run({
|
|
900
|
+
restart: options.restart,
|
|
901
|
+
addChannels: options.addChannels,
|
|
902
|
+
dataDir: options.dataDir,
|
|
903
|
+
})
|
|
904
|
+
} catch (error) {
|
|
905
|
+
cliLogger.error('Unhandled error:', error instanceof Error ? error.message : String(error))
|
|
906
|
+
process.exit(EXIT_NO_RESTART)
|
|
903
907
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
await startLockServer()
|
|
907
|
-
await run({
|
|
908
|
-
restart: options.restart,
|
|
909
|
-
addChannels: options.addChannels,
|
|
910
|
-
dataDir: options.dataDir,
|
|
911
|
-
})
|
|
912
|
-
} catch (error) {
|
|
913
|
-
cliLogger.error(
|
|
914
|
-
'Unhandled error:',
|
|
915
|
-
error instanceof Error ? error.message : String(error),
|
|
916
|
-
)
|
|
917
|
-
process.exit(EXIT_NO_RESTART)
|
|
918
|
-
}
|
|
919
|
-
})
|
|
920
|
-
|
|
921
|
-
|
|
908
|
+
},
|
|
909
|
+
)
|
|
922
910
|
|
|
923
911
|
cli
|
|
924
912
|
.command('upload-to-discord [...files]', 'Upload files to a Discord thread for a session')
|
|
@@ -957,9 +945,7 @@ cli
|
|
|
957
945
|
}
|
|
958
946
|
|
|
959
947
|
const botRow = db
|
|
960
|
-
.prepare(
|
|
961
|
-
'SELECT app_id, token FROM bot_tokens ORDER BY created_at DESC LIMIT 1',
|
|
962
|
-
)
|
|
948
|
+
.prepare('SELECT app_id, token FROM bot_tokens ORDER BY created_at DESC LIMIT 1')
|
|
963
949
|
.get() as { app_id: string; token: string } | undefined
|
|
964
950
|
|
|
965
951
|
if (!botRow) {
|
|
@@ -974,9 +960,12 @@ cli
|
|
|
974
960
|
const buffer = fs.readFileSync(file)
|
|
975
961
|
|
|
976
962
|
const formData = new FormData()
|
|
977
|
-
formData.append(
|
|
978
|
-
|
|
979
|
-
|
|
963
|
+
formData.append(
|
|
964
|
+
'payload_json',
|
|
965
|
+
JSON.stringify({
|
|
966
|
+
attachments: [{ id: 0, filename: path.basename(file) }],
|
|
967
|
+
}),
|
|
968
|
+
)
|
|
980
969
|
formData.append('files[0]', new Blob([buffer]), path.basename(file))
|
|
981
970
|
|
|
982
971
|
const response = await fetch(
|
|
@@ -984,10 +973,10 @@ cli
|
|
|
984
973
|
{
|
|
985
974
|
method: 'POST',
|
|
986
975
|
headers: {
|
|
987
|
-
|
|
976
|
+
Authorization: `Bot ${botRow.token}`,
|
|
988
977
|
},
|
|
989
978
|
body: formData,
|
|
990
|
-
}
|
|
979
|
+
},
|
|
991
980
|
)
|
|
992
981
|
|
|
993
982
|
if (!response.ok) {
|
|
@@ -1005,38 +994,46 @@ cli
|
|
|
1005
994
|
|
|
1006
995
|
process.exit(0)
|
|
1007
996
|
} catch (error) {
|
|
1008
|
-
cliLogger.error(
|
|
1009
|
-
'Error:',
|
|
1010
|
-
error instanceof Error ? error.message : String(error),
|
|
1011
|
-
)
|
|
997
|
+
cliLogger.error('Error:', error instanceof Error ? error.message : String(error))
|
|
1012
998
|
process.exit(EXIT_NO_RESTART)
|
|
1013
999
|
}
|
|
1014
1000
|
})
|
|
1015
1001
|
|
|
1016
|
-
|
|
1017
1002
|
// Magic prefix used to identify bot-initiated sessions.
|
|
1018
1003
|
// The running bot will recognize this prefix and start a session.
|
|
1019
1004
|
const BOT_SESSION_PREFIX = '🤖 **Bot-initiated session**'
|
|
1020
1005
|
|
|
1021
1006
|
cli
|
|
1022
|
-
.command(
|
|
1007
|
+
.command(
|
|
1008
|
+
'start-session',
|
|
1009
|
+
'Start a new session in a Discord channel (creates thread, bot handles the rest)',
|
|
1010
|
+
)
|
|
1023
1011
|
.option('-c, --channel <channelId>', 'Discord channel ID')
|
|
1012
|
+
.option('-d, --project <path>', 'Project directory (alternative to --channel)')
|
|
1024
1013
|
.option('-p, --prompt <prompt>', 'Initial prompt for the session')
|
|
1025
1014
|
.option('-n, --name [name]', 'Thread name (optional, defaults to prompt preview)')
|
|
1026
1015
|
.option('-a, --app-id [appId]', 'Bot application ID (required if no local database)')
|
|
1027
|
-
.action(
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1016
|
+
.action(
|
|
1017
|
+
async (options: {
|
|
1018
|
+
channel?: string
|
|
1019
|
+
project?: string
|
|
1020
|
+
prompt?: string
|
|
1021
|
+
name?: string
|
|
1022
|
+
appId?: string
|
|
1023
|
+
}) => {
|
|
1024
|
+
try {
|
|
1025
|
+
let { channel: channelId, prompt, name, appId: optionAppId } = options
|
|
1026
|
+
const { project: projectPath } = options
|
|
1027
|
+
|
|
1028
|
+
if (!channelId && !projectPath) {
|
|
1029
|
+
cliLogger.error('Either --channel or --project is required')
|
|
1030
|
+
process.exit(EXIT_NO_RESTART)
|
|
1031
|
+
}
|
|
1035
1032
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1033
|
+
if (!prompt) {
|
|
1034
|
+
cliLogger.error('Prompt is required. Use --prompt <prompt>')
|
|
1035
|
+
process.exit(EXIT_NO_RESTART)
|
|
1036
|
+
}
|
|
1040
1037
|
|
|
1041
1038
|
// Get bot token from env var or database
|
|
1042
1039
|
const envToken = process.env.KIMAKI_BOT_TOKEN
|
|
@@ -1076,22 +1073,112 @@ cli
|
|
|
1076
1073
|
}
|
|
1077
1074
|
|
|
1078
1075
|
if (!botToken) {
|
|
1079
|
-
cliLogger.error(
|
|
1076
|
+
cliLogger.error(
|
|
1077
|
+
'No bot token found. Set KIMAKI_BOT_TOKEN env var or run `kimaki` first to set up.',
|
|
1078
|
+
)
|
|
1080
1079
|
process.exit(EXIT_NO_RESTART)
|
|
1081
1080
|
}
|
|
1082
1081
|
|
|
1083
1082
|
const s = spinner()
|
|
1083
|
+
|
|
1084
|
+
// If --project provided, resolve to channel ID
|
|
1085
|
+
if (projectPath) {
|
|
1086
|
+
const absolutePath = path.resolve(projectPath)
|
|
1087
|
+
|
|
1088
|
+
if (!fs.existsSync(absolutePath)) {
|
|
1089
|
+
cliLogger.error(`Directory does not exist: ${absolutePath}`)
|
|
1090
|
+
process.exit(EXIT_NO_RESTART)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
s.start('Looking up channel for project...')
|
|
1094
|
+
|
|
1095
|
+
// Check if channel already exists for this directory
|
|
1096
|
+
try {
|
|
1097
|
+
const db = getDatabase()
|
|
1098
|
+
const existingChannel = db
|
|
1099
|
+
.prepare(
|
|
1100
|
+
'SELECT channel_id FROM channel_directories WHERE directory = ? AND channel_type = ?',
|
|
1101
|
+
)
|
|
1102
|
+
.get(absolutePath, 'text') as { channel_id: string } | undefined
|
|
1103
|
+
|
|
1104
|
+
if (existingChannel) {
|
|
1105
|
+
channelId = existingChannel.channel_id
|
|
1106
|
+
s.message(`Found existing channel: ${channelId}`)
|
|
1107
|
+
} else {
|
|
1108
|
+
// Need to create a new channel
|
|
1109
|
+
s.message('Creating new channel...')
|
|
1110
|
+
|
|
1111
|
+
if (!appId) {
|
|
1112
|
+
s.stop('Missing app ID')
|
|
1113
|
+
cliLogger.error(
|
|
1114
|
+
'App ID is required to create channels. Use --app-id or run `kimaki` first.',
|
|
1115
|
+
)
|
|
1116
|
+
process.exit(EXIT_NO_RESTART)
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const client = await createDiscordClient()
|
|
1120
|
+
|
|
1121
|
+
await new Promise<void>((resolve, reject) => {
|
|
1122
|
+
client.once(Events.ClientReady, () => {
|
|
1123
|
+
resolve()
|
|
1124
|
+
})
|
|
1125
|
+
client.once(Events.Error, reject)
|
|
1126
|
+
client.login(botToken)
|
|
1127
|
+
})
|
|
1128
|
+
|
|
1129
|
+
// Get guild from existing channels or first available
|
|
1130
|
+
const guild = await (async () => {
|
|
1131
|
+
// Try to find a guild from existing channels
|
|
1132
|
+
const existingChannelRow = db
|
|
1133
|
+
.prepare(
|
|
1134
|
+
'SELECT channel_id FROM channel_directories ORDER BY created_at DESC LIMIT 1',
|
|
1135
|
+
)
|
|
1136
|
+
.get() as { channel_id: string } | undefined
|
|
1137
|
+
|
|
1138
|
+
if (existingChannelRow) {
|
|
1139
|
+
try {
|
|
1140
|
+
const ch = await client.channels.fetch(existingChannelRow.channel_id)
|
|
1141
|
+
if (ch && 'guild' in ch && ch.guild) {
|
|
1142
|
+
return ch.guild
|
|
1143
|
+
}
|
|
1144
|
+
} catch {
|
|
1145
|
+
// Channel might be deleted, continue
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
// Fall back to first guild
|
|
1149
|
+
const firstGuild = client.guilds.cache.first()
|
|
1150
|
+
if (!firstGuild) {
|
|
1151
|
+
throw new Error('No guild found. Add the bot to a server first.')
|
|
1152
|
+
}
|
|
1153
|
+
return firstGuild
|
|
1154
|
+
})()
|
|
1155
|
+
|
|
1156
|
+
const { textChannelId } = await createProjectChannels({
|
|
1157
|
+
guild,
|
|
1158
|
+
projectDirectory: absolutePath,
|
|
1159
|
+
appId,
|
|
1160
|
+
botName: client.user?.username,
|
|
1161
|
+
})
|
|
1162
|
+
|
|
1163
|
+
channelId = textChannelId
|
|
1164
|
+
s.message(`Created channel: ${channelId}`)
|
|
1165
|
+
|
|
1166
|
+
client.destroy()
|
|
1167
|
+
}
|
|
1168
|
+
} catch (e) {
|
|
1169
|
+
s.stop('Failed to resolve project')
|
|
1170
|
+
throw e
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1084
1174
|
s.start('Fetching channel info...')
|
|
1085
1175
|
|
|
1086
1176
|
// Get channel info to extract directory from topic
|
|
1087
|
-
const channelResponse = await fetch(
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
},
|
|
1093
|
-
}
|
|
1094
|
-
)
|
|
1177
|
+
const channelResponse = await fetch(`https://discord.com/api/v10/channels/${channelId}`, {
|
|
1178
|
+
headers: {
|
|
1179
|
+
Authorization: `Bot ${botToken}`,
|
|
1180
|
+
},
|
|
1181
|
+
})
|
|
1095
1182
|
|
|
1096
1183
|
if (!channelResponse.ok) {
|
|
1097
1184
|
const error = await channelResponse.text()
|
|
@@ -1099,7 +1186,7 @@ cli
|
|
|
1099
1186
|
throw new Error(`Discord API error: ${channelResponse.status} - ${error}`)
|
|
1100
1187
|
}
|
|
1101
1188
|
|
|
1102
|
-
const channelData = await channelResponse.json() as {
|
|
1189
|
+
const channelData = (await channelResponse.json()) as {
|
|
1103
1190
|
id: string
|
|
1104
1191
|
name: string
|
|
1105
1192
|
topic?: string
|
|
@@ -1108,7 +1195,9 @@ cli
|
|
|
1108
1195
|
|
|
1109
1196
|
if (!channelData.topic) {
|
|
1110
1197
|
s.stop('Channel has no topic')
|
|
1111
|
-
throw new Error(
|
|
1198
|
+
throw new Error(
|
|
1199
|
+
`Channel #${channelData.name} has no topic. It must have a <kimaki.directory> tag.`,
|
|
1200
|
+
)
|
|
1112
1201
|
}
|
|
1113
1202
|
|
|
1114
1203
|
const extracted = extractTagsArrays({
|
|
@@ -1127,7 +1216,9 @@ cli
|
|
|
1127
1216
|
// Verify app ID matches if both are present
|
|
1128
1217
|
if (channelAppId && appId && channelAppId !== appId) {
|
|
1129
1218
|
s.stop('Channel belongs to different bot')
|
|
1130
|
-
throw new Error(
|
|
1219
|
+
throw new Error(
|
|
1220
|
+
`Channel belongs to a different bot (expected: ${appId}, got: ${channelAppId})`,
|
|
1221
|
+
)
|
|
1131
1222
|
}
|
|
1132
1223
|
|
|
1133
1224
|
s.message('Creating starter message...')
|
|
@@ -1139,13 +1230,13 @@ cli
|
|
|
1139
1230
|
{
|
|
1140
1231
|
method: 'POST',
|
|
1141
1232
|
headers: {
|
|
1142
|
-
|
|
1233
|
+
Authorization: `Bot ${botToken}`,
|
|
1143
1234
|
'Content-Type': 'application/json',
|
|
1144
1235
|
},
|
|
1145
1236
|
body: JSON.stringify({
|
|
1146
1237
|
content: `${BOT_SESSION_PREFIX}\n${prompt}`,
|
|
1147
1238
|
}),
|
|
1148
|
-
}
|
|
1239
|
+
},
|
|
1149
1240
|
)
|
|
1150
1241
|
|
|
1151
1242
|
if (!starterMessageResponse.ok) {
|
|
@@ -1154,7 +1245,7 @@ cli
|
|
|
1154
1245
|
throw new Error(`Discord API error: ${starterMessageResponse.status} - ${error}`)
|
|
1155
1246
|
}
|
|
1156
1247
|
|
|
1157
|
-
const starterMessage = await starterMessageResponse.json() as { id: string }
|
|
1248
|
+
const starterMessage = (await starterMessageResponse.json()) as { id: string }
|
|
1158
1249
|
|
|
1159
1250
|
s.message('Creating thread...')
|
|
1160
1251
|
|
|
@@ -1165,14 +1256,14 @@ cli
|
|
|
1165
1256
|
{
|
|
1166
1257
|
method: 'POST',
|
|
1167
1258
|
headers: {
|
|
1168
|
-
|
|
1259
|
+
Authorization: `Bot ${botToken}`,
|
|
1169
1260
|
'Content-Type': 'application/json',
|
|
1170
1261
|
},
|
|
1171
1262
|
body: JSON.stringify({
|
|
1172
1263
|
name: threadName.slice(0, 100),
|
|
1173
1264
|
auto_archive_duration: 1440, // 1 day
|
|
1174
1265
|
}),
|
|
1175
|
-
}
|
|
1266
|
+
},
|
|
1176
1267
|
)
|
|
1177
1268
|
|
|
1178
1269
|
if (!threadResponse.ok) {
|
|
@@ -1181,7 +1272,7 @@ cli
|
|
|
1181
1272
|
throw new Error(`Discord API error: ${threadResponse.status} - ${error}`)
|
|
1182
1273
|
}
|
|
1183
1274
|
|
|
1184
|
-
const threadData = await threadResponse.json() as { id: string; name: string }
|
|
1275
|
+
const threadData = (await threadResponse.json()) as { id: string; name: string }
|
|
1185
1276
|
|
|
1186
1277
|
s.stop('Thread created!')
|
|
1187
1278
|
|
|
@@ -1196,14 +1287,10 @@ cli
|
|
|
1196
1287
|
|
|
1197
1288
|
process.exit(0)
|
|
1198
1289
|
} catch (error) {
|
|
1199
|
-
cliLogger.error(
|
|
1200
|
-
'Error:',
|
|
1201
|
-
error instanceof Error ? error.message : String(error),
|
|
1202
|
-
)
|
|
1290
|
+
cliLogger.error('Error:', error instanceof Error ? error.message : String(error))
|
|
1203
1291
|
process.exit(EXIT_NO_RESTART)
|
|
1204
1292
|
}
|
|
1205
1293
|
})
|
|
1206
1294
|
|
|
1207
|
-
|
|
1208
1295
|
cli.help()
|
|
1209
1296
|
cli.parse()
|