kimaki 0.4.35 → 0.4.37
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 +5 -5
- package/dist/cli.js +182 -46
- 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/database.js +7 -0
- package/dist/discord-bot.js +37 -20
- 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 +10 -4
- 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 +18 -29
- package/src/cli.ts +334 -205
- 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 +17 -8
- package/src/discord-bot.ts +60 -58
- 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 +18 -4
- 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,31 +623,26 @@ 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) {
|
|
636
633
|
if (channel.kimakiDirectory) {
|
|
637
634
|
db.prepare(
|
|
638
|
-
'INSERT OR IGNORE INTO channel_directories (channel_id, directory, channel_type) VALUES (?, ?, ?)',
|
|
639
|
-
).run(channel.id, channel.kimakiDirectory, 'text')
|
|
635
|
+
'INSERT OR IGNORE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)',
|
|
636
|
+
).run(channel.id, channel.kimakiDirectory, 'text', channel.kimakiApp || null)
|
|
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) {
|
|
647
643
|
db.prepare(
|
|
648
|
-
'INSERT OR IGNORE INTO channel_directories (channel_id, directory, channel_type) VALUES (?, ?, ?)',
|
|
649
|
-
).run(voiceChannel.id, channel.kimakiDirectory, 'voice')
|
|
644
|
+
'INSERT OR IGNORE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)',
|
|
645
|
+
).run(voiceChannel.id, channel.kimakiDirectory, 'voice', channel.kimakiApp || null)
|
|
650
646
|
}
|
|
651
647
|
}
|
|
652
648
|
}
|
|
@@ -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,61 @@ 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**'
|
|
1005
|
+
// Notify-only prefix - bot won't start a session, just creates thread for notifications.
|
|
1006
|
+
// Reply to the thread to start a session with the notification as context.
|
|
1007
|
+
const BOT_NOTIFY_PREFIX = '📢 **Notification**'
|
|
1020
1008
|
|
|
1021
1009
|
cli
|
|
1022
|
-
.command(
|
|
1010
|
+
.command(
|
|
1011
|
+
'send',
|
|
1012
|
+
'Send a message to Discord channel, creating a thread. Use --notify-only to skip AI session.',
|
|
1013
|
+
)
|
|
1014
|
+
.alias('start-session') // backwards compatibility
|
|
1023
1015
|
.option('-c, --channel <channelId>', 'Discord channel ID')
|
|
1024
|
-
.option('-
|
|
1016
|
+
.option('-d, --project <path>', 'Project directory (alternative to --channel)')
|
|
1017
|
+
.option('-p, --prompt <prompt>', 'Message content')
|
|
1025
1018
|
.option('-n, --name [name]', 'Thread name (optional, defaults to prompt preview)')
|
|
1026
1019
|
.option('-a, --app-id [appId]', 'Bot application ID (required if no local database)')
|
|
1027
|
-
.
|
|
1028
|
-
|
|
1029
|
-
|
|
1020
|
+
.option('--notify-only', 'Create notification thread without starting AI session')
|
|
1021
|
+
.action(
|
|
1022
|
+
async (options: {
|
|
1023
|
+
channel?: string
|
|
1024
|
+
project?: string
|
|
1025
|
+
prompt?: string
|
|
1026
|
+
name?: string
|
|
1027
|
+
appId?: string
|
|
1028
|
+
notifyOnly?: boolean
|
|
1029
|
+
}) => {
|
|
1030
|
+
try {
|
|
1031
|
+
let { channel: channelId, prompt, name, appId: optionAppId, notifyOnly } = options
|
|
1032
|
+
const { project: projectPath } = options
|
|
1033
|
+
|
|
1034
|
+
// Get raw channel ID from argv to prevent JS number precision loss on large Discord IDs
|
|
1035
|
+
// cac parses large numbers and loses precision, so we extract the original string value
|
|
1036
|
+
if (channelId) {
|
|
1037
|
+
const channelArgIndex = process.argv.findIndex((arg) => arg === '--channel' || arg === '-c')
|
|
1038
|
+
if (channelArgIndex !== -1 && process.argv[channelArgIndex + 1]) {
|
|
1039
|
+
channelId = process.argv[channelArgIndex + 1]
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1030
1042
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1043
|
+
if (!channelId && !projectPath) {
|
|
1044
|
+
cliLogger.error('Either --channel or --project is required')
|
|
1045
|
+
process.exit(EXIT_NO_RESTART)
|
|
1046
|
+
}
|
|
1035
1047
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1048
|
+
if (!prompt) {
|
|
1049
|
+
cliLogger.error('Prompt is required. Use --prompt <prompt>')
|
|
1050
|
+
process.exit(EXIT_NO_RESTART)
|
|
1051
|
+
}
|
|
1040
1052
|
|
|
1041
1053
|
// Get bot token from env var or database
|
|
1042
1054
|
const envToken = process.env.KIMAKI_BOT_TOKEN
|
|
@@ -1076,22 +1088,137 @@ cli
|
|
|
1076
1088
|
}
|
|
1077
1089
|
|
|
1078
1090
|
if (!botToken) {
|
|
1079
|
-
cliLogger.error(
|
|
1091
|
+
cliLogger.error(
|
|
1092
|
+
'No bot token found. Set KIMAKI_BOT_TOKEN env var or run `kimaki` first to set up.',
|
|
1093
|
+
)
|
|
1080
1094
|
process.exit(EXIT_NO_RESTART)
|
|
1081
1095
|
}
|
|
1082
1096
|
|
|
1083
1097
|
const s = spinner()
|
|
1098
|
+
|
|
1099
|
+
// If --project provided, resolve to channel ID
|
|
1100
|
+
if (projectPath) {
|
|
1101
|
+
const absolutePath = path.resolve(projectPath)
|
|
1102
|
+
|
|
1103
|
+
if (!fs.existsSync(absolutePath)) {
|
|
1104
|
+
cliLogger.error(`Directory does not exist: ${absolutePath}`)
|
|
1105
|
+
process.exit(EXIT_NO_RESTART)
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
s.start('Looking up channel for project...')
|
|
1109
|
+
|
|
1110
|
+
// Check if channel already exists for this directory or a parent directory
|
|
1111
|
+
// This allows running from subfolders of a registered project
|
|
1112
|
+
try {
|
|
1113
|
+
const db = getDatabase()
|
|
1114
|
+
|
|
1115
|
+
// Helper to find channel for a path (prefers current bot's channel)
|
|
1116
|
+
const findChannelForPath = (dirPath: string): { channel_id: string; directory: string } | undefined => {
|
|
1117
|
+
const withAppId = db
|
|
1118
|
+
.prepare(
|
|
1119
|
+
'SELECT channel_id, directory FROM channel_directories WHERE directory = ? AND channel_type = ? AND app_id = ?',
|
|
1120
|
+
)
|
|
1121
|
+
.get(dirPath, 'text', appId) as { channel_id: string; directory: string } | undefined
|
|
1122
|
+
if (withAppId) return withAppId
|
|
1123
|
+
|
|
1124
|
+
return db
|
|
1125
|
+
.prepare(
|
|
1126
|
+
'SELECT channel_id, directory FROM channel_directories WHERE directory = ? AND channel_type = ?',
|
|
1127
|
+
)
|
|
1128
|
+
.get(dirPath, 'text') as { channel_id: string; directory: string } | undefined
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Try exact match first, then walk up parent directories
|
|
1132
|
+
let existingChannel: { channel_id: string; directory: string } | undefined
|
|
1133
|
+
let searchPath = absolutePath
|
|
1134
|
+
while (searchPath !== path.dirname(searchPath)) {
|
|
1135
|
+
existingChannel = findChannelForPath(searchPath)
|
|
1136
|
+
if (existingChannel) break
|
|
1137
|
+
searchPath = path.dirname(searchPath)
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
if (existingChannel) {
|
|
1141
|
+
channelId = existingChannel.channel_id
|
|
1142
|
+
if (existingChannel.directory !== absolutePath) {
|
|
1143
|
+
s.message(`Found parent project channel: ${existingChannel.directory}`)
|
|
1144
|
+
} else {
|
|
1145
|
+
s.message(`Found existing channel: ${channelId}`)
|
|
1146
|
+
}
|
|
1147
|
+
} else {
|
|
1148
|
+
// Need to create a new channel
|
|
1149
|
+
s.message('Creating new channel...')
|
|
1150
|
+
|
|
1151
|
+
if (!appId) {
|
|
1152
|
+
s.stop('Missing app ID')
|
|
1153
|
+
cliLogger.error(
|
|
1154
|
+
'App ID is required to create channels. Use --app-id or run `kimaki` first.',
|
|
1155
|
+
)
|
|
1156
|
+
process.exit(EXIT_NO_RESTART)
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const client = await createDiscordClient()
|
|
1160
|
+
|
|
1161
|
+
await new Promise<void>((resolve, reject) => {
|
|
1162
|
+
client.once(Events.ClientReady, () => {
|
|
1163
|
+
resolve()
|
|
1164
|
+
})
|
|
1165
|
+
client.once(Events.Error, reject)
|
|
1166
|
+
client.login(botToken)
|
|
1167
|
+
})
|
|
1168
|
+
|
|
1169
|
+
// Get guild from existing channels or first available
|
|
1170
|
+
const guild = await (async () => {
|
|
1171
|
+
// Try to find a guild from existing channels belonging to this bot
|
|
1172
|
+
const existingChannelRow = db
|
|
1173
|
+
.prepare(
|
|
1174
|
+
'SELECT channel_id FROM channel_directories WHERE app_id = ? ORDER BY created_at DESC LIMIT 1',
|
|
1175
|
+
)
|
|
1176
|
+
.get(appId) as { channel_id: string } | undefined
|
|
1177
|
+
|
|
1178
|
+
if (existingChannelRow) {
|
|
1179
|
+
try {
|
|
1180
|
+
const ch = await client.channels.fetch(existingChannelRow.channel_id)
|
|
1181
|
+
if (ch && 'guild' in ch && ch.guild) {
|
|
1182
|
+
return ch.guild
|
|
1183
|
+
}
|
|
1184
|
+
} catch {
|
|
1185
|
+
// Channel might be deleted, continue
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
// Fall back to first guild the bot is in
|
|
1189
|
+
const firstGuild = client.guilds.cache.first()
|
|
1190
|
+
if (!firstGuild) {
|
|
1191
|
+
throw new Error('No guild found. Add the bot to a server first.')
|
|
1192
|
+
}
|
|
1193
|
+
return firstGuild
|
|
1194
|
+
})()
|
|
1195
|
+
|
|
1196
|
+
const { textChannelId } = await createProjectChannels({
|
|
1197
|
+
guild,
|
|
1198
|
+
projectDirectory: absolutePath,
|
|
1199
|
+
appId,
|
|
1200
|
+
botName: client.user?.username,
|
|
1201
|
+
})
|
|
1202
|
+
|
|
1203
|
+
channelId = textChannelId
|
|
1204
|
+
s.message(`Created channel: ${channelId}`)
|
|
1205
|
+
|
|
1206
|
+
client.destroy()
|
|
1207
|
+
}
|
|
1208
|
+
} catch (e) {
|
|
1209
|
+
s.stop('Failed to resolve project')
|
|
1210
|
+
throw e
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1084
1214
|
s.start('Fetching channel info...')
|
|
1085
1215
|
|
|
1086
1216
|
// Get channel info to extract directory from topic
|
|
1087
|
-
const channelResponse = await fetch(
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
},
|
|
1093
|
-
}
|
|
1094
|
-
)
|
|
1217
|
+
const channelResponse = await fetch(`https://discord.com/api/v10/channels/${channelId}`, {
|
|
1218
|
+
headers: {
|
|
1219
|
+
Authorization: `Bot ${botToken}`,
|
|
1220
|
+
},
|
|
1221
|
+
})
|
|
1095
1222
|
|
|
1096
1223
|
if (!channelResponse.ok) {
|
|
1097
1224
|
const error = await channelResponse.text()
|
|
@@ -1099,7 +1226,7 @@ cli
|
|
|
1099
1226
|
throw new Error(`Discord API error: ${channelResponse.status} - ${error}`)
|
|
1100
1227
|
}
|
|
1101
1228
|
|
|
1102
|
-
const channelData = await channelResponse.json() as {
|
|
1229
|
+
const channelData = (await channelResponse.json()) as {
|
|
1103
1230
|
id: string
|
|
1104
1231
|
name: string
|
|
1105
1232
|
topic?: string
|
|
@@ -1108,7 +1235,9 @@ cli
|
|
|
1108
1235
|
|
|
1109
1236
|
if (!channelData.topic) {
|
|
1110
1237
|
s.stop('Channel has no topic')
|
|
1111
|
-
throw new Error(
|
|
1238
|
+
throw new Error(
|
|
1239
|
+
`Channel #${channelData.name} has no topic. It must have a <kimaki.directory> tag.`,
|
|
1240
|
+
)
|
|
1112
1241
|
}
|
|
1113
1242
|
|
|
1114
1243
|
const extracted = extractTagsArrays({
|
|
@@ -1127,25 +1256,28 @@ cli
|
|
|
1127
1256
|
// Verify app ID matches if both are present
|
|
1128
1257
|
if (channelAppId && appId && channelAppId !== appId) {
|
|
1129
1258
|
s.stop('Channel belongs to different bot')
|
|
1130
|
-
throw new Error(
|
|
1259
|
+
throw new Error(
|
|
1260
|
+
`Channel belongs to a different bot (expected: ${appId}, got: ${channelAppId})`,
|
|
1261
|
+
)
|
|
1131
1262
|
}
|
|
1132
1263
|
|
|
1133
1264
|
s.message('Creating starter message...')
|
|
1134
1265
|
|
|
1135
1266
|
// Create starter message with magic prefix
|
|
1136
|
-
//
|
|
1267
|
+
// BOT_SESSION_PREFIX triggers AI session, BOT_NOTIFY_PREFIX is notification-only
|
|
1268
|
+
const messagePrefix = notifyOnly ? BOT_NOTIFY_PREFIX : BOT_SESSION_PREFIX
|
|
1137
1269
|
const starterMessageResponse = await fetch(
|
|
1138
1270
|
`https://discord.com/api/v10/channels/${channelId}/messages`,
|
|
1139
1271
|
{
|
|
1140
1272
|
method: 'POST',
|
|
1141
1273
|
headers: {
|
|
1142
|
-
|
|
1274
|
+
Authorization: `Bot ${botToken}`,
|
|
1143
1275
|
'Content-Type': 'application/json',
|
|
1144
1276
|
},
|
|
1145
1277
|
body: JSON.stringify({
|
|
1146
|
-
content: `${
|
|
1278
|
+
content: `${messagePrefix}\n${prompt}`,
|
|
1147
1279
|
}),
|
|
1148
|
-
}
|
|
1280
|
+
},
|
|
1149
1281
|
)
|
|
1150
1282
|
|
|
1151
1283
|
if (!starterMessageResponse.ok) {
|
|
@@ -1154,7 +1286,7 @@ cli
|
|
|
1154
1286
|
throw new Error(`Discord API error: ${starterMessageResponse.status} - ${error}`)
|
|
1155
1287
|
}
|
|
1156
1288
|
|
|
1157
|
-
const starterMessage = await starterMessageResponse.json() as { id: string }
|
|
1289
|
+
const starterMessage = (await starterMessageResponse.json()) as { id: string }
|
|
1158
1290
|
|
|
1159
1291
|
s.message('Creating thread...')
|
|
1160
1292
|
|
|
@@ -1165,14 +1297,14 @@ cli
|
|
|
1165
1297
|
{
|
|
1166
1298
|
method: 'POST',
|
|
1167
1299
|
headers: {
|
|
1168
|
-
|
|
1300
|
+
Authorization: `Bot ${botToken}`,
|
|
1169
1301
|
'Content-Type': 'application/json',
|
|
1170
1302
|
},
|
|
1171
1303
|
body: JSON.stringify({
|
|
1172
1304
|
name: threadName.slice(0, 100),
|
|
1173
1305
|
auto_archive_duration: 1440, // 1 day
|
|
1174
1306
|
}),
|
|
1175
|
-
}
|
|
1307
|
+
},
|
|
1176
1308
|
)
|
|
1177
1309
|
|
|
1178
1310
|
if (!threadResponse.ok) {
|
|
@@ -1181,29 +1313,26 @@ cli
|
|
|
1181
1313
|
throw new Error(`Discord API error: ${threadResponse.status} - ${error}`)
|
|
1182
1314
|
}
|
|
1183
1315
|
|
|
1184
|
-
const threadData = await threadResponse.json() as { id: string; name: string }
|
|
1316
|
+
const threadData = (await threadResponse.json()) as { id: string; name: string }
|
|
1185
1317
|
|
|
1186
1318
|
s.stop('Thread created!')
|
|
1187
1319
|
|
|
1188
1320
|
const threadUrl = `https://discord.com/channels/${channelData.guild_id}/${threadData.id}`
|
|
1189
1321
|
|
|
1190
|
-
|
|
1191
|
-
`Thread: ${threadData.name}\nDirectory: ${projectDirectory}\n\
|
|
1192
|
-
|
|
1193
|
-
|
|
1322
|
+
const successMessage = notifyOnly
|
|
1323
|
+
? `Thread: ${threadData.name}\nDirectory: ${projectDirectory}\n\nNotification created. Reply to start a session.\n\nURL: ${threadUrl}`
|
|
1324
|
+
: `Thread: ${threadData.name}\nDirectory: ${projectDirectory}\n\nThe running bot will pick this up and start the session.\n\nURL: ${threadUrl}`
|
|
1325
|
+
|
|
1326
|
+
note(successMessage, '✅ Thread Created')
|
|
1194
1327
|
|
|
1195
1328
|
console.log(threadUrl)
|
|
1196
1329
|
|
|
1197
1330
|
process.exit(0)
|
|
1198
1331
|
} catch (error) {
|
|
1199
|
-
cliLogger.error(
|
|
1200
|
-
'Error:',
|
|
1201
|
-
error instanceof Error ? error.message : String(error),
|
|
1202
|
-
)
|
|
1332
|
+
cliLogger.error('Error:', error instanceof Error ? error.message : String(error))
|
|
1203
1333
|
process.exit(EXIT_NO_RESTART)
|
|
1204
1334
|
}
|
|
1205
1335
|
})
|
|
1206
1336
|
|
|
1207
|
-
|
|
1208
1337
|
cli.help()
|
|
1209
1338
|
cli.parse()
|