kimaki 0.4.78 → 0.4.80
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/anthropic-auth-plugin.js +628 -0
- package/dist/channel-management.js +2 -2
- package/dist/cli.js +316 -129
- package/dist/commands/action-buttons.js +1 -1
- package/dist/commands/login.js +634 -277
- package/dist/commands/model.js +91 -6
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/resume.js +2 -2
- package/dist/commands/tasks.js +205 -0
- package/dist/commands/undo-redo.js +80 -18
- package/dist/context-awareness-plugin.js +347 -0
- package/dist/database.js +103 -7
- package/dist/db.js +39 -1
- package/dist/discord-bot.js +42 -19
- package/dist/discord-urls.js +11 -0
- package/dist/discord-ws-proxy.js +350 -0
- package/dist/discord-ws-proxy.test.js +500 -0
- package/dist/errors.js +1 -1
- package/dist/gateway-session.js +163 -0
- package/dist/hrana-server.js +114 -4
- package/dist/interaction-handler.js +30 -7
- package/dist/ipc-tools-plugin.js +186 -0
- package/dist/message-preprocessing.js +56 -11
- package/dist/onboarding-welcome.js +1 -1
- package/dist/opencode-interrupt-plugin.js +133 -75
- package/dist/opencode-plugin.js +12 -389
- package/dist/opencode.js +59 -5
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
- package/dist/session-handler/thread-session-runtime.js +68 -29
- package/dist/startup-time.e2e.test.js +295 -0
- package/dist/store.js +1 -0
- package/dist/system-message.js +3 -1
- package/dist/task-runner.js +7 -3
- package/dist/task-schedule.js +12 -0
- package/dist/thread-message-queue.e2e.test.js +13 -1
- package/dist/undo-redo.e2e.test.js +166 -0
- package/dist/utils.js +4 -1
- package/dist/voice-attachment.js +34 -0
- package/dist/voice-handler.js +11 -9
- package/dist/voice-message.e2e.test.js +78 -0
- package/dist/voice.test.js +31 -0
- package/package.json +12 -7
- package/skills/egaki/SKILL.md +80 -15
- package/skills/errore/SKILL.md +13 -0
- package/skills/lintcn/SKILL.md +749 -0
- package/skills/npm-package/SKILL.md +17 -3
- package/skills/spiceflow/SKILL.md +14 -0
- package/skills/zele/SKILL.md +9 -0
- package/src/anthropic-auth-plugin.ts +732 -0
- package/src/channel-management.ts +2 -2
- package/src/cli.ts +354 -132
- package/src/commands/action-buttons.ts +1 -0
- package/src/commands/login.ts +836 -337
- package/src/commands/model.ts +102 -7
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/resume.ts +6 -1
- package/src/commands/tasks.ts +293 -0
- package/src/commands/undo-redo.ts +87 -20
- package/src/context-awareness-plugin.ts +469 -0
- package/src/database.ts +138 -7
- package/src/db.ts +40 -1
- package/src/discord-bot.ts +46 -19
- package/src/discord-urls.ts +12 -0
- package/src/errors.ts +1 -1
- package/src/hrana-server.ts +124 -3
- package/src/interaction-handler.ts +41 -9
- package/src/ipc-tools-plugin.ts +228 -0
- package/src/message-preprocessing.ts +82 -11
- package/src/onboarding-welcome.ts +1 -1
- package/src/opencode-interrupt-plugin.ts +164 -91
- package/src/opencode-plugin.ts +13 -483
- package/src/opencode.ts +60 -5
- package/src/parse-permission-rules.test.ts +127 -0
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
- package/src/session-handler/thread-runtime-state.ts +4 -1
- package/src/session-handler/thread-session-runtime.ts +82 -20
- package/src/startup-time.e2e.test.ts +372 -0
- package/src/store.ts +8 -0
- package/src/system-message.ts +10 -1
- package/src/task-runner.ts +9 -22
- package/src/task-schedule.ts +15 -0
- package/src/thread-message-queue.e2e.test.ts +14 -1
- package/src/undo-redo.e2e.test.ts +207 -0
- package/src/utils.ts +7 -0
- package/src/voice-attachment.ts +51 -0
- package/src/voice-handler.ts +15 -7
- package/src/voice-message.e2e.test.ts +95 -0
- package/src/voice.test.ts +36 -0
- package/src/onboarding-tutorial-plugin.ts +0 -93
package/src/cli.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Handles interactive setup, Discord OAuth, slash command registration,
|
|
4
4
|
// project channel creation, and launching the bot with opencode integration.
|
|
5
5
|
import { goke } from 'goke'
|
|
6
|
+
import { z } from 'zod'
|
|
6
7
|
import {
|
|
7
8
|
intro,
|
|
8
9
|
outro,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
} from './discord-bot.js'
|
|
40
41
|
import {
|
|
41
42
|
getBotTokenWithMode,
|
|
43
|
+
ensureServiceAuthToken,
|
|
42
44
|
setBotToken,
|
|
43
45
|
setBotMode,
|
|
44
46
|
setChannelDirectory,
|
|
@@ -50,6 +52,8 @@ import {
|
|
|
50
52
|
createScheduledTask,
|
|
51
53
|
listScheduledTasks,
|
|
52
54
|
cancelScheduledTask,
|
|
55
|
+
getScheduledTask,
|
|
56
|
+
updateScheduledTask,
|
|
53
57
|
getSessionStartSourcesBySessionIds,
|
|
54
58
|
} from './database.js'
|
|
55
59
|
import { ShareMarkdown } from './markdown.js'
|
|
@@ -83,7 +87,7 @@ import {
|
|
|
83
87
|
SlashCommandBuilder,
|
|
84
88
|
AttachmentBuilder,
|
|
85
89
|
} from 'discord.js'
|
|
86
|
-
import { createDiscordRest, discordApiUrl, getDiscordRestApiUrl, getGatewayProxyRestBaseUrl } from './discord-urls.js'
|
|
90
|
+
import { createDiscordRest, discordApiUrl, getDiscordRestApiUrl, getGatewayProxyRestBaseUrl, getInternetReachableBaseUrl } from './discord-urls.js'
|
|
87
91
|
import crypto from 'node:crypto'
|
|
88
92
|
import path from 'node:path'
|
|
89
93
|
import fs from 'node:fs'
|
|
@@ -117,9 +121,9 @@ import {
|
|
|
117
121
|
import { startHranaServer } from './hrana-server.js'
|
|
118
122
|
import { startIpcPolling, stopIpcPolling } from './ipc-polling.js'
|
|
119
123
|
import {
|
|
120
|
-
getLocalTimeZone,
|
|
121
124
|
getPromptPreview,
|
|
122
125
|
parseSendAtValue,
|
|
126
|
+
parseScheduledTaskPayload,
|
|
123
127
|
serializeScheduledTaskPayload,
|
|
124
128
|
type ParsedSendAt,
|
|
125
129
|
type ScheduledTaskPayload,
|
|
@@ -248,7 +252,37 @@ async function sendDiscordMessageWithOptionalAttachment({
|
|
|
248
252
|
fs.mkdirSync(tmpDir, { recursive: true })
|
|
249
253
|
}
|
|
250
254
|
const tmpFile = path.join(tmpDir, `prompt-${Date.now()}.md`)
|
|
251
|
-
|
|
255
|
+
// Wrap long lines so the file is readable in Discord's preview
|
|
256
|
+
// (Discord doesn't wrap text in file attachments)
|
|
257
|
+
const wrappedPrompt = prompt
|
|
258
|
+
.split('\n')
|
|
259
|
+
.flatMap((line) => {
|
|
260
|
+
if (line.length <= 120) {
|
|
261
|
+
return [line]
|
|
262
|
+
}
|
|
263
|
+
const wrapped: string[] = []
|
|
264
|
+
let remaining = line
|
|
265
|
+
const maxCol = 120
|
|
266
|
+
// Only soft-break at a space if it's reasonably close to maxCol,
|
|
267
|
+
// otherwise hard-break to avoid tiny fragments from early spaces
|
|
268
|
+
const minSoftBreak = 90
|
|
269
|
+
while (remaining.length > maxCol) {
|
|
270
|
+
const lastSpace = remaining.lastIndexOf(' ', maxCol)
|
|
271
|
+
const useSoftBreak = lastSpace >= minSoftBreak
|
|
272
|
+
const breakAt = useSoftBreak ? lastSpace : maxCol
|
|
273
|
+
wrapped.push(remaining.slice(0, breakAt))
|
|
274
|
+
// Only consume the separator space on soft breaks
|
|
275
|
+
remaining = useSoftBreak
|
|
276
|
+
? remaining.slice(breakAt + 1)
|
|
277
|
+
: remaining.slice(breakAt)
|
|
278
|
+
}
|
|
279
|
+
if (remaining.length > 0) {
|
|
280
|
+
wrapped.push(remaining)
|
|
281
|
+
}
|
|
282
|
+
return wrapped
|
|
283
|
+
})
|
|
284
|
+
.join('\n')
|
|
285
|
+
fs.writeFileSync(tmpFile, wrappedPrompt)
|
|
252
286
|
|
|
253
287
|
try {
|
|
254
288
|
const formData = new FormData()
|
|
@@ -543,7 +577,7 @@ async function ensureCommandAvailable({
|
|
|
543
577
|
cliLogger.log(`Failed to install ${name}`)
|
|
544
578
|
cliLogger.error(
|
|
545
579
|
'Installation error:',
|
|
546
|
-
error instanceof Error ? error.
|
|
580
|
+
error instanceof Error ? error.stack : String(error),
|
|
547
581
|
)
|
|
548
582
|
process.exit(EXIT_NO_RESTART)
|
|
549
583
|
}
|
|
@@ -603,16 +637,18 @@ async function ensureCommandAvailable({
|
|
|
603
637
|
// Run opencode upgrade in the background so the user always has the latest version.
|
|
604
638
|
|
|
605
639
|
// Spawn caffeinate on macOS to prevent system sleep while bot is running.
|
|
606
|
-
//
|
|
640
|
+
// Uses -w to watch the parent PID so caffeinate self-terminates if kimaki
|
|
641
|
+
// exits for any reason (SIGTERM, crash, process.exit, supervisor stop).
|
|
607
642
|
function startCaffeinate() {
|
|
608
643
|
if (process.platform !== 'darwin') {
|
|
609
644
|
return
|
|
610
645
|
}
|
|
611
646
|
try {
|
|
612
|
-
const proc = spawn('caffeinate', ['-i'], {
|
|
647
|
+
const proc = spawn('caffeinate', ['-i', '-w', String(process.pid)], {
|
|
613
648
|
stdio: 'ignore',
|
|
614
649
|
detached: false,
|
|
615
650
|
})
|
|
651
|
+
proc.unref()
|
|
616
652
|
proc.on('error', (err) => {
|
|
617
653
|
cliLogger.warn('Failed to start caffeinate:', err.message)
|
|
618
654
|
})
|
|
@@ -740,11 +776,17 @@ async function deleteLegacyGlobalCommands({
|
|
|
740
776
|
}
|
|
741
777
|
} catch (error) {
|
|
742
778
|
cliLogger.warn(
|
|
743
|
-
`COMMANDS: Could not clean legacy global commands: ${error instanceof Error ? error.
|
|
779
|
+
`COMMANDS: Could not clean legacy global commands: ${error instanceof Error ? error.stack : String(error)}`,
|
|
744
780
|
)
|
|
745
781
|
}
|
|
746
782
|
}
|
|
747
783
|
|
|
784
|
+
// Discord slash command descriptions must be 1-100 chars.
|
|
785
|
+
// Truncate to 100 so @sapphire/shapeshift validation never throws.
|
|
786
|
+
function truncateCommandDescription(description: string): string {
|
|
787
|
+
return description.slice(0, 100)
|
|
788
|
+
}
|
|
789
|
+
|
|
748
790
|
async function registerCommands({
|
|
749
791
|
token,
|
|
750
792
|
appId,
|
|
@@ -761,11 +803,11 @@ async function registerCommands({
|
|
|
761
803
|
const commands = [
|
|
762
804
|
new SlashCommandBuilder()
|
|
763
805
|
.setName('resume')
|
|
764
|
-
.setDescription('Resume an existing OpenCode session')
|
|
806
|
+
.setDescription(truncateCommandDescription('Resume an existing OpenCode session'))
|
|
765
807
|
.addStringOption((option) => {
|
|
766
808
|
option
|
|
767
809
|
.setName('session')
|
|
768
|
-
.setDescription('The session to resume')
|
|
810
|
+
.setDescription(truncateCommandDescription('The session to resume'))
|
|
769
811
|
.setRequired(true)
|
|
770
812
|
.setAutocomplete(true)
|
|
771
813
|
|
|
@@ -775,11 +817,11 @@ async function registerCommands({
|
|
|
775
817
|
.toJSON(),
|
|
776
818
|
new SlashCommandBuilder()
|
|
777
819
|
.setName('new-session')
|
|
778
|
-
.setDescription('Start a new OpenCode session')
|
|
820
|
+
.setDescription(truncateCommandDescription('Start a new OpenCode session'))
|
|
779
821
|
.addStringOption((option) => {
|
|
780
822
|
option
|
|
781
823
|
.setName('prompt')
|
|
782
|
-
.setDescription('Prompt content for the session')
|
|
824
|
+
.setDescription(truncateCommandDescription('Prompt content for the session'))
|
|
783
825
|
.setRequired(true)
|
|
784
826
|
|
|
785
827
|
return option
|
|
@@ -788,7 +830,7 @@ async function registerCommands({
|
|
|
788
830
|
option
|
|
789
831
|
.setName('files')
|
|
790
832
|
.setDescription(
|
|
791
|
-
'Files to mention (comma or space separated; autocomplete)',
|
|
833
|
+
truncateCommandDescription('Files to mention (comma or space separated; autocomplete)'),
|
|
792
834
|
)
|
|
793
835
|
.setAutocomplete(true)
|
|
794
836
|
.setMaxLength(6000)
|
|
@@ -798,7 +840,7 @@ async function registerCommands({
|
|
|
798
840
|
.addStringOption((option) => {
|
|
799
841
|
option
|
|
800
842
|
.setName('agent')
|
|
801
|
-
.setDescription('Agent to use for this session')
|
|
843
|
+
.setDescription(truncateCommandDescription('Agent to use for this session'))
|
|
802
844
|
.setAutocomplete(true)
|
|
803
845
|
|
|
804
846
|
return option
|
|
@@ -808,13 +850,13 @@ async function registerCommands({
|
|
|
808
850
|
new SlashCommandBuilder()
|
|
809
851
|
.setName('new-worktree')
|
|
810
852
|
.setDescription(
|
|
811
|
-
'Create a git worktree branch from origin/HEAD (or main). Optionally pick a base branch.',
|
|
853
|
+
truncateCommandDescription('Create a git worktree branch from origin/HEAD (or main). Optionally pick a base branch.'),
|
|
812
854
|
)
|
|
813
855
|
.addStringOption((option) => {
|
|
814
856
|
option
|
|
815
857
|
.setName('name')
|
|
816
858
|
.setDescription(
|
|
817
|
-
'Name for worktree (optional in threads - uses thread name)',
|
|
859
|
+
truncateCommandDescription('Name for worktree (optional in threads - uses thread name)'),
|
|
818
860
|
)
|
|
819
861
|
.setRequired(false)
|
|
820
862
|
|
|
@@ -824,7 +866,7 @@ async function registerCommands({
|
|
|
824
866
|
option
|
|
825
867
|
.setName('base-branch')
|
|
826
868
|
.setDescription(
|
|
827
|
-
'Branch to create the worktree from (default: origin/HEAD or main)',
|
|
869
|
+
truncateCommandDescription('Branch to create the worktree from (default: origin/HEAD or main)'),
|
|
828
870
|
)
|
|
829
871
|
.setRequired(false)
|
|
830
872
|
.setAutocomplete(true)
|
|
@@ -836,13 +878,13 @@ async function registerCommands({
|
|
|
836
878
|
new SlashCommandBuilder()
|
|
837
879
|
.setName('merge-worktree')
|
|
838
880
|
.setDescription(
|
|
839
|
-
'Squash-merge worktree into
|
|
881
|
+
truncateCommandDescription('Squash-merge worktree into default branch. Aborts if main has uncommitted changes.'),
|
|
840
882
|
)
|
|
841
883
|
.addStringOption((option) => {
|
|
842
884
|
option
|
|
843
885
|
.setName('target-branch')
|
|
844
886
|
.setDescription(
|
|
845
|
-
'Branch to merge into (default: origin/HEAD or main)',
|
|
887
|
+
truncateCommandDescription('Branch to merge into (default: origin/HEAD or main)'),
|
|
846
888
|
)
|
|
847
889
|
.setRequired(false)
|
|
848
890
|
.setAutocomplete(true)
|
|
@@ -854,32 +896,44 @@ async function registerCommands({
|
|
|
854
896
|
new SlashCommandBuilder()
|
|
855
897
|
.setName('toggle-worktrees')
|
|
856
898
|
.setDescription(
|
|
857
|
-
'Toggle automatic git worktree creation for new sessions in this channel',
|
|
899
|
+
truncateCommandDescription('Toggle automatic git worktree creation for new sessions in this channel'),
|
|
858
900
|
)
|
|
859
901
|
.setDMPermission(false)
|
|
860
902
|
.toJSON(),
|
|
861
903
|
new SlashCommandBuilder()
|
|
862
904
|
.setName('worktrees')
|
|
863
|
-
.setDescription('List all active worktree sessions')
|
|
905
|
+
.setDescription(truncateCommandDescription('List all active worktree sessions'))
|
|
906
|
+
.setDMPermission(false)
|
|
907
|
+
.toJSON(),
|
|
908
|
+
new SlashCommandBuilder()
|
|
909
|
+
.setName('tasks')
|
|
910
|
+
.setDescription(truncateCommandDescription('List scheduled tasks created via send --send-at'))
|
|
911
|
+
.addBooleanOption((option) => {
|
|
912
|
+
return option
|
|
913
|
+
.setName('all')
|
|
914
|
+
.setDescription(
|
|
915
|
+
truncateCommandDescription('Include completed, cancelled, and failed tasks'),
|
|
916
|
+
)
|
|
917
|
+
})
|
|
864
918
|
.setDMPermission(false)
|
|
865
919
|
.toJSON(),
|
|
866
920
|
new SlashCommandBuilder()
|
|
867
921
|
.setName('toggle-mention-mode')
|
|
868
922
|
.setDescription(
|
|
869
|
-
'Toggle mention-only mode (bot only responds when @mentioned)',
|
|
923
|
+
truncateCommandDescription('Toggle mention-only mode (bot only responds when @mentioned)'),
|
|
870
924
|
)
|
|
871
925
|
.setDMPermission(false)
|
|
872
926
|
.toJSON(),
|
|
873
927
|
new SlashCommandBuilder()
|
|
874
928
|
.setName('add-project')
|
|
875
929
|
.setDescription(
|
|
876
|
-
'Create Discord channels for a project. Use `npx kimaki project add` for unlisted projects',
|
|
930
|
+
truncateCommandDescription('Create Discord channels for a project. Use `npx kimaki project add` for unlisted projects'),
|
|
877
931
|
)
|
|
878
932
|
.addStringOption((option) => {
|
|
879
933
|
option
|
|
880
934
|
.setName('project')
|
|
881
935
|
.setDescription(
|
|
882
|
-
'Recent OpenCode projects. Use `npx kimaki project add` if not listed',
|
|
936
|
+
truncateCommandDescription('Recent OpenCode projects. Use `npx kimaki project add` if not listed'),
|
|
883
937
|
)
|
|
884
938
|
.setRequired(true)
|
|
885
939
|
.setAutocomplete(true)
|
|
@@ -890,11 +944,11 @@ async function registerCommands({
|
|
|
890
944
|
.toJSON(),
|
|
891
945
|
new SlashCommandBuilder()
|
|
892
946
|
.setName('remove-project')
|
|
893
|
-
.setDescription('Remove Discord channels for a project')
|
|
947
|
+
.setDescription(truncateCommandDescription('Remove Discord channels for a project'))
|
|
894
948
|
.addStringOption((option) => {
|
|
895
949
|
option
|
|
896
950
|
.setName('project')
|
|
897
|
-
.setDescription('Select a project to remove')
|
|
951
|
+
.setDescription(truncateCommandDescription('Select a project to remove'))
|
|
898
952
|
.setRequired(true)
|
|
899
953
|
.setAutocomplete(true)
|
|
900
954
|
|
|
@@ -905,12 +959,12 @@ async function registerCommands({
|
|
|
905
959
|
new SlashCommandBuilder()
|
|
906
960
|
.setName('create-new-project')
|
|
907
961
|
.setDescription(
|
|
908
|
-
'Create a new project folder, initialize git, and start a session',
|
|
962
|
+
truncateCommandDescription('Create a new project folder, initialize git, and start a session'),
|
|
909
963
|
)
|
|
910
964
|
.addStringOption((option) => {
|
|
911
965
|
option
|
|
912
966
|
.setName('name')
|
|
913
|
-
.setDescription('Name for the new project folder')
|
|
967
|
+
.setDescription(truncateCommandDescription('Name for the new project folder'))
|
|
914
968
|
.setRequired(true)
|
|
915
969
|
|
|
916
970
|
return option
|
|
@@ -919,74 +973,74 @@ async function registerCommands({
|
|
|
919
973
|
.toJSON(),
|
|
920
974
|
new SlashCommandBuilder()
|
|
921
975
|
.setName('abort')
|
|
922
|
-
.setDescription('Abort the current OpenCode request in this thread')
|
|
976
|
+
.setDescription(truncateCommandDescription('Abort the current OpenCode request in this thread'))
|
|
923
977
|
.setDMPermission(false)
|
|
924
978
|
.toJSON(),
|
|
925
979
|
new SlashCommandBuilder()
|
|
926
980
|
.setName('compact')
|
|
927
981
|
.setDescription(
|
|
928
|
-
'Compact the session context by summarizing conversation history',
|
|
982
|
+
truncateCommandDescription('Compact the session context by summarizing conversation history'),
|
|
929
983
|
)
|
|
930
984
|
.setDMPermission(false)
|
|
931
985
|
.toJSON(),
|
|
932
986
|
new SlashCommandBuilder()
|
|
933
987
|
.setName('stop')
|
|
934
|
-
.setDescription('Abort the current OpenCode request in this thread')
|
|
988
|
+
.setDescription(truncateCommandDescription('Abort the current OpenCode request in this thread'))
|
|
935
989
|
.setDMPermission(false)
|
|
936
990
|
.toJSON(),
|
|
937
991
|
new SlashCommandBuilder()
|
|
938
992
|
.setName('share')
|
|
939
|
-
.setDescription('Share the current session as a public URL')
|
|
993
|
+
.setDescription(truncateCommandDescription('Share the current session as a public URL'))
|
|
940
994
|
.setDMPermission(false)
|
|
941
995
|
.toJSON(),
|
|
942
996
|
new SlashCommandBuilder()
|
|
943
997
|
.setName('diff')
|
|
944
|
-
.setDescription('Show git diff as a shareable URL')
|
|
998
|
+
.setDescription(truncateCommandDescription('Show git diff as a shareable URL'))
|
|
945
999
|
.setDMPermission(false)
|
|
946
1000
|
.toJSON(),
|
|
947
1001
|
new SlashCommandBuilder()
|
|
948
1002
|
.setName('fork')
|
|
949
|
-
.setDescription('Fork the session from a past user message')
|
|
1003
|
+
.setDescription(truncateCommandDescription('Fork the session from a past user message'))
|
|
950
1004
|
.setDMPermission(false)
|
|
951
1005
|
.toJSON(),
|
|
952
1006
|
new SlashCommandBuilder()
|
|
953
1007
|
.setName('model')
|
|
954
|
-
.setDescription('Set the preferred model for this channel or session')
|
|
1008
|
+
.setDescription(truncateCommandDescription('Set the preferred model for this channel or session'))
|
|
955
1009
|
.setDMPermission(false)
|
|
956
1010
|
.toJSON(),
|
|
957
1011
|
new SlashCommandBuilder()
|
|
958
1012
|
.setName('model-variant')
|
|
959
1013
|
.setDescription(
|
|
960
|
-
'Quickly change the thinking level variant for the current model',
|
|
1014
|
+
truncateCommandDescription('Quickly change the thinking level variant for the current model'),
|
|
961
1015
|
)
|
|
962
1016
|
.setDMPermission(false)
|
|
963
1017
|
.toJSON(),
|
|
964
1018
|
new SlashCommandBuilder()
|
|
965
1019
|
.setName('unset-model-override')
|
|
966
|
-
.setDescription('Remove model override and use default instead')
|
|
1020
|
+
.setDescription(truncateCommandDescription('Remove model override and use default instead'))
|
|
967
1021
|
.setDMPermission(false)
|
|
968
1022
|
.toJSON(),
|
|
969
1023
|
new SlashCommandBuilder()
|
|
970
1024
|
.setName('login')
|
|
971
1025
|
.setDescription(
|
|
972
|
-
'Authenticate with an AI provider (OAuth or API key). Use this instead of /connect',
|
|
1026
|
+
truncateCommandDescription('Authenticate with an AI provider (OAuth or API key). Use this instead of /connect'),
|
|
973
1027
|
)
|
|
974
1028
|
.setDMPermission(false)
|
|
975
1029
|
.toJSON(),
|
|
976
1030
|
new SlashCommandBuilder()
|
|
977
1031
|
.setName('agent')
|
|
978
|
-
.setDescription('Set the preferred agent for this channel or session')
|
|
1032
|
+
.setDescription(truncateCommandDescription('Set the preferred agent for this channel or session'))
|
|
979
1033
|
.setDMPermission(false)
|
|
980
1034
|
.toJSON(),
|
|
981
1035
|
new SlashCommandBuilder()
|
|
982
1036
|
.setName('queue')
|
|
983
1037
|
.setDescription(
|
|
984
|
-
'Queue a message to be sent after the current response finishes',
|
|
1038
|
+
truncateCommandDescription('Queue a message to be sent after the current response finishes'),
|
|
985
1039
|
)
|
|
986
1040
|
.addStringOption((option) => {
|
|
987
1041
|
option
|
|
988
1042
|
.setName('message')
|
|
989
|
-
.setDescription('The message to queue')
|
|
1043
|
+
.setDescription(truncateCommandDescription('The message to queue'))
|
|
990
1044
|
.setRequired(true)
|
|
991
1045
|
|
|
992
1046
|
return option
|
|
@@ -995,18 +1049,18 @@ async function registerCommands({
|
|
|
995
1049
|
.toJSON(),
|
|
996
1050
|
new SlashCommandBuilder()
|
|
997
1051
|
.setName('clear-queue')
|
|
998
|
-
.setDescription('Clear all queued messages in this thread')
|
|
1052
|
+
.setDescription(truncateCommandDescription('Clear all queued messages in this thread'))
|
|
999
1053
|
.setDMPermission(false)
|
|
1000
1054
|
.toJSON(),
|
|
1001
1055
|
new SlashCommandBuilder()
|
|
1002
1056
|
.setName('queue-command')
|
|
1003
1057
|
.setDescription(
|
|
1004
|
-
'Queue a user command to run after the current response finishes',
|
|
1058
|
+
truncateCommandDescription('Queue a user command to run after the current response finishes'),
|
|
1005
1059
|
)
|
|
1006
1060
|
.addStringOption((option) => {
|
|
1007
1061
|
option
|
|
1008
1062
|
.setName('command')
|
|
1009
|
-
.setDescription('The command to run')
|
|
1063
|
+
.setDescription(truncateCommandDescription('The command to run'))
|
|
1010
1064
|
.setRequired(true)
|
|
1011
1065
|
.setAutocomplete(true)
|
|
1012
1066
|
return option
|
|
@@ -1014,7 +1068,7 @@ async function registerCommands({
|
|
|
1014
1068
|
.addStringOption((option) => {
|
|
1015
1069
|
option
|
|
1016
1070
|
.setName('arguments')
|
|
1017
|
-
.setDescription('Arguments to pass to the command')
|
|
1071
|
+
.setDescription(truncateCommandDescription('Arguments to pass to the command'))
|
|
1018
1072
|
.setRequired(false)
|
|
1019
1073
|
return option
|
|
1020
1074
|
})
|
|
@@ -1022,35 +1076,35 @@ async function registerCommands({
|
|
|
1022
1076
|
.toJSON(),
|
|
1023
1077
|
new SlashCommandBuilder()
|
|
1024
1078
|
.setName('undo')
|
|
1025
|
-
.setDescription('Undo the last assistant message (revert file changes)')
|
|
1079
|
+
.setDescription(truncateCommandDescription('Undo the last assistant message (revert file changes)'))
|
|
1026
1080
|
.setDMPermission(false)
|
|
1027
1081
|
.toJSON(),
|
|
1028
1082
|
new SlashCommandBuilder()
|
|
1029
1083
|
.setName('redo')
|
|
1030
|
-
.setDescription('Redo previously undone changes')
|
|
1084
|
+
.setDescription(truncateCommandDescription('Redo previously undone changes'))
|
|
1031
1085
|
.setDMPermission(false)
|
|
1032
1086
|
.toJSON(),
|
|
1033
1087
|
new SlashCommandBuilder()
|
|
1034
1088
|
.setName('verbosity')
|
|
1035
|
-
.setDescription('Set output verbosity for this channel')
|
|
1089
|
+
.setDescription(truncateCommandDescription('Set output verbosity for this channel'))
|
|
1036
1090
|
.setDMPermission(false)
|
|
1037
1091
|
.toJSON(),
|
|
1038
1092
|
new SlashCommandBuilder()
|
|
1039
1093
|
.setName('restart-opencode-server')
|
|
1040
1094
|
.setDescription(
|
|
1041
|
-
'Restart the shared opencode server (fixes state/auth/plugins)',
|
|
1095
|
+
truncateCommandDescription('Restart the shared opencode server (fixes state/auth/plugins)'),
|
|
1042
1096
|
)
|
|
1043
1097
|
.setDMPermission(false)
|
|
1044
1098
|
.toJSON(),
|
|
1045
1099
|
new SlashCommandBuilder()
|
|
1046
1100
|
.setName('run-shell-command')
|
|
1047
1101
|
.setDescription(
|
|
1048
|
-
'Run a shell command in the project directory. Tip: prefix messages with ! as shortcut',
|
|
1102
|
+
truncateCommandDescription('Run a shell command in the project directory. Tip: prefix messages with ! as shortcut'),
|
|
1049
1103
|
)
|
|
1050
1104
|
.addStringOption((option) => {
|
|
1051
1105
|
option
|
|
1052
1106
|
.setName('command')
|
|
1053
|
-
.setDescription('Command to run')
|
|
1107
|
+
.setDescription(truncateCommandDescription('Command to run'))
|
|
1054
1108
|
.setRequired(true)
|
|
1055
1109
|
return option
|
|
1056
1110
|
})
|
|
@@ -1059,44 +1113,44 @@ async function registerCommands({
|
|
|
1059
1113
|
new SlashCommandBuilder()
|
|
1060
1114
|
.setName('context-usage')
|
|
1061
1115
|
.setDescription(
|
|
1062
|
-
'Show token usage and context window percentage for this session',
|
|
1116
|
+
truncateCommandDescription('Show token usage and context window percentage for this session'),
|
|
1063
1117
|
)
|
|
1064
1118
|
.setDMPermission(false)
|
|
1065
1119
|
.toJSON(),
|
|
1066
1120
|
new SlashCommandBuilder()
|
|
1067
1121
|
.setName('session-id')
|
|
1068
1122
|
.setDescription(
|
|
1069
|
-
'Show current session ID and opencode attach command for this thread',
|
|
1123
|
+
truncateCommandDescription('Show current session ID and opencode attach command for this thread'),
|
|
1070
1124
|
)
|
|
1071
1125
|
.setDMPermission(false)
|
|
1072
1126
|
.toJSON(),
|
|
1073
1127
|
new SlashCommandBuilder()
|
|
1074
1128
|
.setName('upgrade-and-restart')
|
|
1075
1129
|
.setDescription(
|
|
1076
|
-
'Upgrade kimaki to the latest version and restart the bot',
|
|
1130
|
+
truncateCommandDescription('Upgrade kimaki to the latest version and restart the bot'),
|
|
1077
1131
|
)
|
|
1078
1132
|
.setDMPermission(false)
|
|
1079
1133
|
.toJSON(),
|
|
1080
1134
|
new SlashCommandBuilder()
|
|
1081
1135
|
.setName('transcription-key')
|
|
1082
1136
|
.setDescription(
|
|
1083
|
-
'Set API key for voice message transcription (OpenAI or Gemini)',
|
|
1137
|
+
truncateCommandDescription('Set API key for voice message transcription (OpenAI or Gemini)'),
|
|
1084
1138
|
)
|
|
1085
1139
|
.setDMPermission(false)
|
|
1086
1140
|
.toJSON(),
|
|
1087
1141
|
new SlashCommandBuilder()
|
|
1088
1142
|
.setName('mcp')
|
|
1089
|
-
.setDescription('List and manage MCP servers for this project')
|
|
1143
|
+
.setDescription(truncateCommandDescription('List and manage MCP servers for this project'))
|
|
1090
1144
|
.setDMPermission(false)
|
|
1091
1145
|
.toJSON(),
|
|
1092
1146
|
new SlashCommandBuilder()
|
|
1093
1147
|
.setName('screenshare')
|
|
1094
|
-
.setDescription('Start screen sharing via VNC tunnel (auto-stops after 1 hour)')
|
|
1148
|
+
.setDescription(truncateCommandDescription('Start screen sharing via VNC tunnel (auto-stops after 1 hour)'))
|
|
1095
1149
|
.setDMPermission(false)
|
|
1096
1150
|
.toJSON(),
|
|
1097
1151
|
new SlashCommandBuilder()
|
|
1098
1152
|
.setName('screenshare-stop')
|
|
1099
|
-
.setDescription('Stop screen sharing')
|
|
1153
|
+
.setDescription(truncateCommandDescription('Stop screen sharing'))
|
|
1100
1154
|
.setDMPermission(false)
|
|
1101
1155
|
.toJSON(),
|
|
1102
1156
|
]
|
|
@@ -1142,11 +1196,11 @@ async function registerCommands({
|
|
|
1142
1196
|
commands.push(
|
|
1143
1197
|
new SlashCommandBuilder()
|
|
1144
1198
|
.setName(commandName)
|
|
1145
|
-
.setDescription(description
|
|
1199
|
+
.setDescription(truncateCommandDescription(description))
|
|
1146
1200
|
.addStringOption((option) => {
|
|
1147
1201
|
option
|
|
1148
1202
|
.setName('arguments')
|
|
1149
|
-
.setDescription('Arguments to pass to the command')
|
|
1203
|
+
.setDescription(truncateCommandDescription('Arguments to pass to the command'))
|
|
1150
1204
|
.setRequired(false)
|
|
1151
1205
|
return option
|
|
1152
1206
|
})
|
|
@@ -1181,7 +1235,7 @@ async function registerCommands({
|
|
|
1181
1235
|
commands.push(
|
|
1182
1236
|
new SlashCommandBuilder()
|
|
1183
1237
|
.setName(commandName)
|
|
1184
|
-
.setDescription(description)
|
|
1238
|
+
.setDescription(truncateCommandDescription(description))
|
|
1185
1239
|
.setDMPermission(false)
|
|
1186
1240
|
.toJSON(),
|
|
1187
1241
|
)
|
|
@@ -1470,7 +1524,7 @@ async function ensureDefaultChannelsWithWelcome({
|
|
|
1470
1524
|
}
|
|
1471
1525
|
} catch (error) {
|
|
1472
1526
|
cliLogger.warn(
|
|
1473
|
-
`Failed to create default kimaki channel in ${guild.name}: ${error instanceof Error ? error.
|
|
1527
|
+
`Failed to create default kimaki channel in ${guild.name}: ${error instanceof Error ? error.stack : String(error)}`,
|
|
1474
1528
|
)
|
|
1475
1529
|
}
|
|
1476
1530
|
}
|
|
@@ -1516,7 +1570,7 @@ async function backgroundInit({
|
|
|
1516
1570
|
.catch((error) => {
|
|
1517
1571
|
cliLogger.warn(
|
|
1518
1572
|
'Failed to load user commands during background init:',
|
|
1519
|
-
error instanceof Error ? error.
|
|
1573
|
+
error instanceof Error ? error.stack : String(error),
|
|
1520
1574
|
)
|
|
1521
1575
|
return []
|
|
1522
1576
|
}),
|
|
@@ -1526,7 +1580,7 @@ async function backgroundInit({
|
|
|
1526
1580
|
.catch((error) => {
|
|
1527
1581
|
cliLogger.warn(
|
|
1528
1582
|
'Failed to load agents during background init:',
|
|
1529
|
-
error instanceof Error ? error.
|
|
1583
|
+
error instanceof Error ? error.stack : String(error),
|
|
1530
1584
|
)
|
|
1531
1585
|
return []
|
|
1532
1586
|
}),
|
|
@@ -1537,7 +1591,7 @@ async function backgroundInit({
|
|
|
1537
1591
|
} catch (error) {
|
|
1538
1592
|
cliLogger.error(
|
|
1539
1593
|
'Background init failed:',
|
|
1540
|
-
error instanceof Error ? error.
|
|
1594
|
+
error instanceof Error ? error.stack : String(error),
|
|
1541
1595
|
)
|
|
1542
1596
|
void notifyError(error, 'Background init failed')
|
|
1543
1597
|
}
|
|
@@ -1689,6 +1743,7 @@ async function resolveCredentials({
|
|
|
1689
1743
|
clientId,
|
|
1690
1744
|
clientSecret,
|
|
1691
1745
|
gatewayCallbackUrl,
|
|
1746
|
+
reachableUrl: getInternetReachableBaseUrl() || undefined,
|
|
1692
1747
|
})
|
|
1693
1748
|
if (oauthUrlResult instanceof Error) {
|
|
1694
1749
|
throw oauthUrlResult
|
|
@@ -1894,33 +1949,35 @@ async function run({
|
|
|
1894
1949
|
const forceRestartOnboarding = Boolean(restartOnboarding)
|
|
1895
1950
|
const forceGateway = Boolean(gateway)
|
|
1896
1951
|
|
|
1897
|
-
// Step 0: Ensure required CLI tools are installed (OpenCode + Bun)
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
'
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1952
|
+
// Step 0: Ensure required CLI tools are installed (OpenCode + Bun).
|
|
1953
|
+
// Run checks in parallel since they're independent `which` calls.
|
|
1954
|
+
await Promise.all([
|
|
1955
|
+
ensureCommandAvailable({
|
|
1956
|
+
name: 'opencode',
|
|
1957
|
+
envPathKey: 'OPENCODE_PATH',
|
|
1958
|
+
installUnix: 'curl -fsSL https://opencode.ai/install | bash',
|
|
1959
|
+
installWindows: 'irm https://opencode.ai/install.ps1 | iex',
|
|
1960
|
+
possiblePathsUnix: [
|
|
1961
|
+
'~/.local/bin/opencode',
|
|
1962
|
+
'~/.opencode/bin/opencode',
|
|
1963
|
+
'/usr/local/bin/opencode',
|
|
1964
|
+
'/opt/opencode/bin/opencode',
|
|
1965
|
+
],
|
|
1966
|
+
possiblePathsWindows: [
|
|
1967
|
+
'~\\.local\\bin\\opencode.exe',
|
|
1968
|
+
'~\\AppData\\Local\\opencode\\opencode.exe',
|
|
1969
|
+
'~\\.opencode\\bin\\opencode.exe',
|
|
1970
|
+
],
|
|
1971
|
+
}),
|
|
1972
|
+
ensureCommandAvailable({
|
|
1973
|
+
name: 'bun',
|
|
1974
|
+
envPathKey: 'BUN_PATH',
|
|
1975
|
+
installUnix: 'curl -fsSL https://bun.sh/install | bash',
|
|
1976
|
+
installWindows: 'irm bun.sh/install.ps1 | iex',
|
|
1977
|
+
possiblePathsUnix: ['~/.bun/bin/bun', '/usr/local/bin/bun'],
|
|
1978
|
+
possiblePathsWindows: ['~\\.bun\\bin\\bun.exe'],
|
|
1979
|
+
}),
|
|
1980
|
+
])
|
|
1924
1981
|
|
|
1925
1982
|
|
|
1926
1983
|
backgroundUpgradeKimaki()
|
|
@@ -1931,6 +1988,7 @@ async function run({
|
|
|
1931
1988
|
// don't work. CLI subcommands skip the server and use file: directly.
|
|
1932
1989
|
const hranaResult = await startHranaServer({
|
|
1933
1990
|
dbPath: path.join(getDataDir(), 'discord-sessions.db'),
|
|
1991
|
+
bindAll: getInternetReachableBaseUrl() !== null,
|
|
1934
1992
|
})
|
|
1935
1993
|
if (hranaResult instanceof Error) {
|
|
1936
1994
|
cliLogger.error('Failed to start hrana server:', hranaResult.message)
|
|
@@ -1946,6 +2004,14 @@ async function run({
|
|
|
1946
2004
|
gatewayCallbackUrl,
|
|
1947
2005
|
})
|
|
1948
2006
|
|
|
2007
|
+
const gatewayToken = await ensureServiceAuthToken({
|
|
2008
|
+
appId,
|
|
2009
|
+
preferredGatewayToken: isGatewayMode ? token : undefined,
|
|
2010
|
+
})
|
|
2011
|
+
// Always set service auth token so local and internet control-plane paths
|
|
2012
|
+
// share one auth model (/kimaki/wake and future service endpoints).
|
|
2013
|
+
store.setState({ gatewayToken })
|
|
2014
|
+
|
|
1949
2015
|
// In gateway mode, ensure REST calls route through the gateway proxy.
|
|
1950
2016
|
// getBotTokenWithMode() sets this for saved-credential paths, but the fresh
|
|
1951
2017
|
// onboarding path returns directly without going through getBotTokenWithMode(),
|
|
@@ -1956,6 +2022,34 @@ async function run({
|
|
|
1956
2022
|
store.setState({ discordBaseUrl: KIMAKI_GATEWAY_PROXY_REST_BASE_URL })
|
|
1957
2023
|
}
|
|
1958
2024
|
|
|
2025
|
+
// When KIMAKI_INTERNET_REACHABLE_URL is set, the hrana server exposes
|
|
2026
|
+
// a /kimaki/wake endpoint for the gateway-proxy to wake this instance and
|
|
2027
|
+
// wait until discord.js is connected. Keep Discord traffic on the normal
|
|
2028
|
+
// configured base URL (gateway-proxy in gateway mode).
|
|
2029
|
+
if (getInternetReachableBaseUrl()) {
|
|
2030
|
+
cliLogger.log('Internet-reachable mode: enabling /kimaki/wake endpoint on hrana server')
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// Start OpenCode server as early as possible — non-blocking.
|
|
2034
|
+
// All dependencies are met (dataDir, lockPort, gatewayToken, hranaUrl set).
|
|
2035
|
+
// Runs in parallel with last_used_at update, skipChannelSetup check, and
|
|
2036
|
+
// Discord Gateway login so cold start is not blocked by OpenCode spawn.
|
|
2037
|
+
const currentDir = process.cwd()
|
|
2038
|
+
cliLogger.log('Starting OpenCode server...')
|
|
2039
|
+
const opencodePromise = initializeOpencodeForDirectory(currentDir).then(
|
|
2040
|
+
(result) => {
|
|
2041
|
+
if (result instanceof Error) {
|
|
2042
|
+
throw new Error(result.message)
|
|
2043
|
+
}
|
|
2044
|
+
cliLogger.log('OpenCode server ready!')
|
|
2045
|
+
return result
|
|
2046
|
+
},
|
|
2047
|
+
)
|
|
2048
|
+
// Prevent unhandled rejection if OpenCode fails before backgroundInit
|
|
2049
|
+
// or the channel setup path awaits it. Errors are handled by the
|
|
2050
|
+
// respective consumers (backgroundInit catches, channel setup re-throws).
|
|
2051
|
+
opencodePromise.catch(() => {})
|
|
2052
|
+
|
|
1959
2053
|
// Mark this bot as the most recently used so subcommands in separate
|
|
1960
2054
|
// processes (send, upload-to-discord, project list) pick the correct bot.
|
|
1961
2055
|
// getBotTokenWithMode() orders by last_used_at DESC as cross-process
|
|
@@ -1995,19 +2089,6 @@ async function run({
|
|
|
1995
2089
|
return true
|
|
1996
2090
|
})()
|
|
1997
2091
|
|
|
1998
|
-
// Start OpenCode server EARLY - let it initialize in parallel with Discord login.
|
|
1999
|
-
// This is the biggest startup bottleneck (can take 1-30 seconds to spawn and wait for ready)
|
|
2000
|
-
const currentDir = process.cwd()
|
|
2001
|
-
cliLogger.log('Starting OpenCode server...')
|
|
2002
|
-
const opencodePromise = initializeOpencodeForDirectory(currentDir).then(
|
|
2003
|
-
(result) => {
|
|
2004
|
-
if (result instanceof Error) {
|
|
2005
|
-
throw new Error(result.message)
|
|
2006
|
-
}
|
|
2007
|
-
return result
|
|
2008
|
-
},
|
|
2009
|
-
)
|
|
2010
|
-
|
|
2011
2092
|
cliLogger.log(`Connecting to ${getDiscordRestApiUrl()}...`)
|
|
2012
2093
|
const discordClient = await createDiscordClient()
|
|
2013
2094
|
|
|
@@ -2066,7 +2147,7 @@ async function run({
|
|
|
2066
2147
|
} catch (error) {
|
|
2067
2148
|
cliLogger.log('Failed to connect to Discord', discordClient.ws.gateway)
|
|
2068
2149
|
cliLogger.error(
|
|
2069
|
-
'Error: ' + (error instanceof Error ? error.
|
|
2150
|
+
'Error: ' + (error instanceof Error ? error.stack : String(error)),
|
|
2070
2151
|
)
|
|
2071
2152
|
process.exit(EXIT_NO_RESTART)
|
|
2072
2153
|
}
|
|
@@ -2126,7 +2207,7 @@ async function run({
|
|
|
2126
2207
|
} catch (error) {
|
|
2127
2208
|
cliLogger.warn(
|
|
2128
2209
|
'Background channel sync failed:',
|
|
2129
|
-
error instanceof Error ? error.
|
|
2210
|
+
error instanceof Error ? error.stack : String(error),
|
|
2130
2211
|
)
|
|
2131
2212
|
}
|
|
2132
2213
|
|
|
@@ -2143,7 +2224,7 @@ async function run({
|
|
|
2143
2224
|
} catch (error) {
|
|
2144
2225
|
cliLogger.warn(
|
|
2145
2226
|
'Background default channel creation failed:',
|
|
2146
|
-
error instanceof Error ? error.
|
|
2227
|
+
error instanceof Error ? error.stack : String(error),
|
|
2147
2228
|
)
|
|
2148
2229
|
}
|
|
2149
2230
|
})()
|
|
@@ -2184,7 +2265,6 @@ async function run({
|
|
|
2184
2265
|
// Wait for OpenCode, fetch projects, show prompts, create channels if needed
|
|
2185
2266
|
cliLogger.log('Waiting for OpenCode server...')
|
|
2186
2267
|
const getClient = await opencodePromise
|
|
2187
|
-
cliLogger.log('OpenCode server ready!')
|
|
2188
2268
|
|
|
2189
2269
|
cliLogger.log('Fetching OpenCode data...')
|
|
2190
2270
|
|
|
@@ -2197,7 +2277,7 @@ async function run({
|
|
|
2197
2277
|
cliLogger.log('Failed to fetch projects')
|
|
2198
2278
|
cliLogger.error(
|
|
2199
2279
|
'Error:',
|
|
2200
|
-
error instanceof Error ? error.
|
|
2280
|
+
error instanceof Error ? error.stack : String(error),
|
|
2201
2281
|
)
|
|
2202
2282
|
discordClient.destroy()
|
|
2203
2283
|
process.exit(EXIT_NO_RESTART)
|
|
@@ -2208,7 +2288,7 @@ async function run({
|
|
|
2208
2288
|
.catch((error) => {
|
|
2209
2289
|
cliLogger.warn(
|
|
2210
2290
|
'Failed to load user commands during setup:',
|
|
2211
|
-
error instanceof Error ? error.
|
|
2291
|
+
error instanceof Error ? error.stack : String(error),
|
|
2212
2292
|
)
|
|
2213
2293
|
return []
|
|
2214
2294
|
}),
|
|
@@ -2218,7 +2298,7 @@ async function run({
|
|
|
2218
2298
|
.catch((error) => {
|
|
2219
2299
|
cliLogger.warn(
|
|
2220
2300
|
'Failed to load agents during setup:',
|
|
2221
|
-
error instanceof Error ? error.
|
|
2301
|
+
error instanceof Error ? error.stack : String(error),
|
|
2222
2302
|
)
|
|
2223
2303
|
return []
|
|
2224
2304
|
}),
|
|
@@ -2375,7 +2455,7 @@ async function run({
|
|
|
2375
2455
|
.catch((error) => {
|
|
2376
2456
|
cliLogger.error(
|
|
2377
2457
|
'Failed to register slash commands:',
|
|
2378
|
-
error instanceof Error ? error.
|
|
2458
|
+
error instanceof Error ? error.stack : String(error),
|
|
2379
2459
|
)
|
|
2380
2460
|
})
|
|
2381
2461
|
|
|
@@ -2594,7 +2674,7 @@ cli
|
|
|
2594
2674
|
} catch (error) {
|
|
2595
2675
|
cliLogger.error(
|
|
2596
2676
|
'Error:',
|
|
2597
|
-
error instanceof Error ? error.
|
|
2677
|
+
error instanceof Error ? error.stack : String(error),
|
|
2598
2678
|
)
|
|
2599
2679
|
process.exit(EXIT_NO_RESTART)
|
|
2600
2680
|
}
|
|
@@ -2649,7 +2729,7 @@ cli
|
|
|
2649
2729
|
} catch (error) {
|
|
2650
2730
|
cliLogger.error(
|
|
2651
2731
|
'Error:',
|
|
2652
|
-
error instanceof Error ? error.
|
|
2732
|
+
error instanceof Error ? error.stack : String(error),
|
|
2653
2733
|
)
|
|
2654
2734
|
process.exit(EXIT_NO_RESTART)
|
|
2655
2735
|
}
|
|
@@ -2799,7 +2879,7 @@ cli
|
|
|
2799
2879
|
} catch (error) {
|
|
2800
2880
|
cliLogger.error(
|
|
2801
2881
|
'Error:',
|
|
2802
|
-
error instanceof Error ? error.
|
|
2882
|
+
error instanceof Error ? error.stack : String(error),
|
|
2803
2883
|
)
|
|
2804
2884
|
process.exit(EXIT_NO_RESTART)
|
|
2805
2885
|
}
|
|
@@ -2853,7 +2933,7 @@ cli
|
|
|
2853
2933
|
} catch (error) {
|
|
2854
2934
|
cliLogger.error(
|
|
2855
2935
|
'Error:',
|
|
2856
|
-
error instanceof Error ? error.
|
|
2936
|
+
error instanceof Error ? error.stack : String(error),
|
|
2857
2937
|
)
|
|
2858
2938
|
process.exit(EXIT_NO_RESTART)
|
|
2859
2939
|
}
|
|
@@ -2924,7 +3004,7 @@ cli
|
|
|
2924
3004
|
} catch (error) {
|
|
2925
3005
|
cliLogger.error(
|
|
2926
3006
|
'Error:',
|
|
2927
|
-
error instanceof Error ? error.
|
|
3007
|
+
error instanceof Error ? error.stack : String(error),
|
|
2928
3008
|
)
|
|
2929
3009
|
process.exit(EXIT_NO_RESTART)
|
|
2930
3010
|
}
|
|
@@ -2961,6 +3041,13 @@ cli
|
|
|
2961
3041
|
.option('-u, --user <username>', 'Discord username to add to thread')
|
|
2962
3042
|
.option('--agent <agent>', 'Agent to use for the session')
|
|
2963
3043
|
.option('--model <model>', 'Model to use (format: provider/model)')
|
|
3044
|
+
.option(
|
|
3045
|
+
'--permission <rule>',
|
|
3046
|
+
z.array(z.string()).describe(
|
|
3047
|
+
'Session permission rule (repeatable). Format: "tool:action" or "tool:pattern:action". ' +
|
|
3048
|
+
'Actions: allow, deny, ask. Examples: --permission "bash:deny" --permission "edit:deny"',
|
|
3049
|
+
),
|
|
3050
|
+
)
|
|
2964
3051
|
.option(
|
|
2965
3052
|
'--send-at <schedule>',
|
|
2966
3053
|
'Schedule send for future (UTC ISO date/time ending in Z, or cron expression)',
|
|
@@ -2986,6 +3073,7 @@ cli
|
|
|
2986
3073
|
user?: string
|
|
2987
3074
|
agent?: string
|
|
2988
3075
|
model?: string
|
|
3076
|
+
permission?: string[]
|
|
2989
3077
|
sendAt?: string
|
|
2990
3078
|
thread?: string
|
|
2991
3079
|
session?: string
|
|
@@ -3045,10 +3133,12 @@ cli
|
|
|
3045
3133
|
if (!sendAt) {
|
|
3046
3134
|
return null
|
|
3047
3135
|
}
|
|
3136
|
+
// Cron expressions use UTC so the schedule is consistent regardless of
|
|
3137
|
+
// which machine runs the bot. The system message tells the model to use UTC.
|
|
3048
3138
|
return parseSendAtValue({
|
|
3049
3139
|
value: sendAt,
|
|
3050
3140
|
now: new Date(),
|
|
3051
|
-
timezone:
|
|
3141
|
+
timezone: 'UTC',
|
|
3052
3142
|
})
|
|
3053
3143
|
})()
|
|
3054
3144
|
if (parsedSchedule instanceof Error) {
|
|
@@ -3190,7 +3280,7 @@ cli
|
|
|
3190
3280
|
} catch (error) {
|
|
3191
3281
|
cliLogger.debug(
|
|
3192
3282
|
'Failed to fetch existing channel while selecting guild:',
|
|
3193
|
-
error instanceof Error ? error.
|
|
3283
|
+
error instanceof Error ? error.stack : String(error),
|
|
3194
3284
|
)
|
|
3195
3285
|
}
|
|
3196
3286
|
}
|
|
@@ -3282,6 +3372,7 @@ cli
|
|
|
3282
3372
|
model: options.model || null,
|
|
3283
3373
|
username: null,
|
|
3284
3374
|
userId: null,
|
|
3375
|
+
permissions: options.permission?.length ? options.permission : null,
|
|
3285
3376
|
}
|
|
3286
3377
|
const taskId = await createScheduledTask({
|
|
3287
3378
|
scheduleKind: parsedSchedule.scheduleKind,
|
|
@@ -3308,6 +3399,7 @@ cli
|
|
|
3308
3399
|
|
|
3309
3400
|
const threadPromptMarker: ThreadStartMarker = {
|
|
3310
3401
|
cliThreadPrompt: true,
|
|
3402
|
+
...(options.permission?.length ? { permissions: options.permission } : {}),
|
|
3311
3403
|
}
|
|
3312
3404
|
const promptEmbed = [
|
|
3313
3405
|
{
|
|
@@ -3439,6 +3531,7 @@ cli
|
|
|
3439
3531
|
model: options.model || null,
|
|
3440
3532
|
username: resolvedUser?.username || null,
|
|
3441
3533
|
userId: resolvedUser?.id || null,
|
|
3534
|
+
permissions: options.permission?.length ? options.permission : null,
|
|
3442
3535
|
}
|
|
3443
3536
|
const taskId = await createScheduledTask({
|
|
3444
3537
|
scheduleKind: parsedSchedule.scheduleKind,
|
|
@@ -3474,6 +3567,7 @@ cli
|
|
|
3474
3567
|
}),
|
|
3475
3568
|
...(options.agent && { agent: options.agent }),
|
|
3476
3569
|
...(options.model && { model: options.model }),
|
|
3570
|
+
...(options.permission?.length && { permissions: options.permission }),
|
|
3477
3571
|
}
|
|
3478
3572
|
const autoStartEmbed = embedMarker
|
|
3479
3573
|
? [{ color: 0x2b2d31, footer: { text: yaml.dump(embedMarker) } }]
|
|
@@ -3532,7 +3626,7 @@ cli
|
|
|
3532
3626
|
} catch (error) {
|
|
3533
3627
|
cliLogger.error(
|
|
3534
3628
|
'Error:',
|
|
3535
|
-
error instanceof Error ? error.
|
|
3629
|
+
error instanceof Error ? error.stack : String(error),
|
|
3536
3630
|
)
|
|
3537
3631
|
process.exit(EXIT_NO_RESTART)
|
|
3538
3632
|
}
|
|
@@ -3583,7 +3677,7 @@ cli
|
|
|
3583
3677
|
} catch (error) {
|
|
3584
3678
|
cliLogger.error(
|
|
3585
3679
|
'Error:',
|
|
3586
|
-
error instanceof Error ? error.
|
|
3680
|
+
error instanceof Error ? error.stack : String(error),
|
|
3587
3681
|
)
|
|
3588
3682
|
process.exit(EXIT_NO_RESTART)
|
|
3589
3683
|
}
|
|
@@ -3611,7 +3705,100 @@ cli
|
|
|
3611
3705
|
} catch (error) {
|
|
3612
3706
|
cliLogger.error(
|
|
3613
3707
|
'Error:',
|
|
3614
|
-
error instanceof Error ? error.
|
|
3708
|
+
error instanceof Error ? error.stack : String(error),
|
|
3709
|
+
)
|
|
3710
|
+
process.exit(EXIT_NO_RESTART)
|
|
3711
|
+
}
|
|
3712
|
+
})
|
|
3713
|
+
|
|
3714
|
+
cli
|
|
3715
|
+
.command('task edit <id>', 'Edit prompt or schedule of a planned task')
|
|
3716
|
+
.option('--prompt <prompt>', 'New prompt text')
|
|
3717
|
+
.option('--send-at <sendAt>', 'New schedule (UTC ISO date or cron expression)')
|
|
3718
|
+
.action(async (id: string, options: { prompt?: string; sendAt?: string }) => {
|
|
3719
|
+
try {
|
|
3720
|
+
const trimmedPrompt =
|
|
3721
|
+
options.prompt === undefined ? undefined : options.prompt.trim()
|
|
3722
|
+
|
|
3723
|
+
if (!trimmedPrompt && !options.sendAt) {
|
|
3724
|
+
cliLogger.error('Provide at least --prompt or --send-at')
|
|
3725
|
+
process.exit(EXIT_NO_RESTART)
|
|
3726
|
+
}
|
|
3727
|
+
if (trimmedPrompt !== undefined && trimmedPrompt.length === 0) {
|
|
3728
|
+
cliLogger.error('--prompt cannot be empty')
|
|
3729
|
+
process.exit(EXIT_NO_RESTART)
|
|
3730
|
+
}
|
|
3731
|
+
if (trimmedPrompt !== undefined && trimmedPrompt.length > 1900) {
|
|
3732
|
+
cliLogger.error('--prompt currently supports up to 1900 characters')
|
|
3733
|
+
process.exit(EXIT_NO_RESTART)
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
const taskId = Number.parseInt(id, 10)
|
|
3737
|
+
if (Number.isNaN(taskId) || taskId < 1) {
|
|
3738
|
+
cliLogger.error(`Invalid task ID: ${id}`)
|
|
3739
|
+
process.exit(EXIT_NO_RESTART)
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
await initDatabase()
|
|
3743
|
+
const task = await getScheduledTask(taskId)
|
|
3744
|
+
if (!task) {
|
|
3745
|
+
cliLogger.error(`Task ${taskId} not found`)
|
|
3746
|
+
process.exit(EXIT_NO_RESTART)
|
|
3747
|
+
}
|
|
3748
|
+
if (task.status !== 'planned') {
|
|
3749
|
+
cliLogger.error(
|
|
3750
|
+
`Task ${taskId} is ${task.status}, only planned tasks can be edited`,
|
|
3751
|
+
)
|
|
3752
|
+
process.exit(EXIT_NO_RESTART)
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
const existingPayload = parseScheduledTaskPayload(task.payload_json)
|
|
3756
|
+
if (existingPayload instanceof Error) {
|
|
3757
|
+
cliLogger.error(`Failed to parse task payload: ${existingPayload.message}`)
|
|
3758
|
+
process.exit(EXIT_NO_RESTART)
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
const newPrompt = trimmedPrompt ?? existingPayload.prompt
|
|
3762
|
+
const updatedPayload: ScheduledTaskPayload = {
|
|
3763
|
+
...existingPayload,
|
|
3764
|
+
prompt: newPrompt,
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
const updateData: Parameters<typeof updateScheduledTask>[0] = {
|
|
3768
|
+
taskId,
|
|
3769
|
+
payloadJson: serializeScheduledTaskPayload(updatedPayload),
|
|
3770
|
+
promptPreview: getPromptPreview(newPrompt),
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
if (options.sendAt) {
|
|
3774
|
+
const parsed = parseSendAtValue({
|
|
3775
|
+
value: options.sendAt,
|
|
3776
|
+
now: new Date(),
|
|
3777
|
+
timezone: 'UTC',
|
|
3778
|
+
})
|
|
3779
|
+
if (parsed instanceof Error) {
|
|
3780
|
+
cliLogger.error(`Invalid --send-at: ${parsed.message}`)
|
|
3781
|
+
process.exit(EXIT_NO_RESTART)
|
|
3782
|
+
}
|
|
3783
|
+
updateData.scheduleKind = parsed.scheduleKind
|
|
3784
|
+
updateData.runAt = parsed.runAt
|
|
3785
|
+
updateData.cronExpr = parsed.cronExpr
|
|
3786
|
+
updateData.timezone = parsed.timezone
|
|
3787
|
+
updateData.nextRunAt = parsed.nextRunAt
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
const updated = await updateScheduledTask(updateData)
|
|
3791
|
+
if (!updated) {
|
|
3792
|
+
cliLogger.error(`Task ${taskId} could not be updated (status may have changed)`)
|
|
3793
|
+
process.exit(EXIT_NO_RESTART)
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
cliLogger.log(`Updated task ${taskId}`)
|
|
3797
|
+
process.exit(0)
|
|
3798
|
+
} catch (error) {
|
|
3799
|
+
cliLogger.error(
|
|
3800
|
+
'Error:',
|
|
3801
|
+
error instanceof Error ? error.stack : String(error),
|
|
3615
3802
|
)
|
|
3616
3803
|
process.exit(EXIT_NO_RESTART)
|
|
3617
3804
|
}
|
|
@@ -3703,7 +3890,7 @@ cli
|
|
|
3703
3890
|
} catch (error) {
|
|
3704
3891
|
cliLogger.debug(
|
|
3705
3892
|
'Failed to fetch existing channel while selecting guild:',
|
|
3706
|
-
error instanceof Error ? error.
|
|
3893
|
+
error instanceof Error ? error.stack : String(error),
|
|
3707
3894
|
)
|
|
3708
3895
|
let firstGuild = client.guilds.cache.first()
|
|
3709
3896
|
if (!firstGuild) {
|
|
@@ -3763,14 +3950,14 @@ cli
|
|
|
3763
3950
|
} catch (error) {
|
|
3764
3951
|
cliLogger.debug(
|
|
3765
3952
|
`Failed to fetch channel ${existingChannel.channel_id} while checking existing channels:`,
|
|
3766
|
-
error instanceof Error ? error.
|
|
3953
|
+
error instanceof Error ? error.stack : String(error),
|
|
3767
3954
|
)
|
|
3768
3955
|
}
|
|
3769
3956
|
}
|
|
3770
3957
|
} catch (error) {
|
|
3771
3958
|
cliLogger.debug(
|
|
3772
3959
|
'Database lookup failed while checking existing channels:',
|
|
3773
|
-
error instanceof Error ? error.
|
|
3960
|
+
error instanceof Error ? error.stack : String(error),
|
|
3774
3961
|
)
|
|
3775
3962
|
}
|
|
3776
3963
|
|
|
@@ -4096,7 +4283,7 @@ cli
|
|
|
4096
4283
|
} catch (error) {
|
|
4097
4284
|
cliLogger.error(
|
|
4098
4285
|
'Error:',
|
|
4099
|
-
error instanceof Error ? error.
|
|
4286
|
+
error instanceof Error ? error.stack : String(error),
|
|
4100
4287
|
)
|
|
4101
4288
|
process.exit(EXIT_NO_RESTART)
|
|
4102
4289
|
}
|
|
@@ -4276,7 +4463,7 @@ cli
|
|
|
4276
4463
|
} catch (error) {
|
|
4277
4464
|
cliLogger.error(
|
|
4278
4465
|
'Error:',
|
|
4279
|
-
error instanceof Error ? error.
|
|
4466
|
+
error instanceof Error ? error.stack : String(error),
|
|
4280
4467
|
)
|
|
4281
4468
|
process.exit(EXIT_NO_RESTART)
|
|
4282
4469
|
}
|
|
@@ -4350,7 +4537,7 @@ cli
|
|
|
4350
4537
|
} catch (error) {
|
|
4351
4538
|
cliLogger.error(
|
|
4352
4539
|
'Error:',
|
|
4353
|
-
error instanceof Error ? error.
|
|
4540
|
+
error instanceof Error ? error.stack : String(error),
|
|
4354
4541
|
)
|
|
4355
4542
|
process.exit(EXIT_NO_RESTART)
|
|
4356
4543
|
}
|
|
@@ -4559,7 +4746,7 @@ cli
|
|
|
4559
4746
|
} catch (error) {
|
|
4560
4747
|
cliLogger.error(
|
|
4561
4748
|
'Error:',
|
|
4562
|
-
error instanceof Error ? error.
|
|
4749
|
+
error instanceof Error ? error.stack : String(error),
|
|
4563
4750
|
)
|
|
4564
4751
|
process.exit(EXIT_NO_RESTART)
|
|
4565
4752
|
}
|
|
@@ -4752,12 +4939,47 @@ cli
|
|
|
4752
4939
|
} catch (error) {
|
|
4753
4940
|
cliLogger.error(
|
|
4754
4941
|
'Error:',
|
|
4755
|
-
error instanceof Error ? error.
|
|
4942
|
+
error instanceof Error ? error.stack : String(error),
|
|
4756
4943
|
)
|
|
4757
4944
|
process.exit(EXIT_NO_RESTART)
|
|
4758
4945
|
}
|
|
4759
4946
|
})
|
|
4760
4947
|
|
|
4948
|
+
cli
|
|
4949
|
+
.command(
|
|
4950
|
+
'session discord-url <sessionId>',
|
|
4951
|
+
'Print the Discord thread URL for a session',
|
|
4952
|
+
)
|
|
4953
|
+
.option('--json', 'Output as JSON')
|
|
4954
|
+
.action(async (sessionId, options) => {
|
|
4955
|
+
await initDatabase()
|
|
4956
|
+
const threadId = await getThreadIdBySessionId(sessionId)
|
|
4957
|
+
if (!threadId) {
|
|
4958
|
+
cliLogger.error(`No Discord thread found for session: ${sessionId}`)
|
|
4959
|
+
process.exit(EXIT_NO_RESTART)
|
|
4960
|
+
}
|
|
4961
|
+
const { token: botToken } = await resolveBotCredentials()
|
|
4962
|
+
const rest = createDiscordRest(botToken)
|
|
4963
|
+
const threadData = (await rest.get(Routes.channel(threadId))) as {
|
|
4964
|
+
id: string
|
|
4965
|
+
guild_id: string
|
|
4966
|
+
name?: string
|
|
4967
|
+
}
|
|
4968
|
+
const url = `https://discord.com/channels/${threadData.guild_id}/${threadData.id}`
|
|
4969
|
+
if (options.json) {
|
|
4970
|
+
console.log(JSON.stringify({
|
|
4971
|
+
url,
|
|
4972
|
+
threadId: threadData.id,
|
|
4973
|
+
guildId: threadData.guild_id,
|
|
4974
|
+
sessionId,
|
|
4975
|
+
threadName: threadData.name,
|
|
4976
|
+
}))
|
|
4977
|
+
} else {
|
|
4978
|
+
console.log(url)
|
|
4979
|
+
}
|
|
4980
|
+
process.exit(0)
|
|
4981
|
+
})
|
|
4982
|
+
|
|
4761
4983
|
cli
|
|
4762
4984
|
.command(
|
|
4763
4985
|
'upgrade',
|
|
@@ -4795,7 +5017,7 @@ cli
|
|
|
4795
5017
|
} catch (error) {
|
|
4796
5018
|
cliLogger.error(
|
|
4797
5019
|
'Upgrade failed:',
|
|
4798
|
-
error instanceof Error ? error.
|
|
5020
|
+
error instanceof Error ? error.stack : String(error),
|
|
4799
5021
|
)
|
|
4800
5022
|
process.exit(EXIT_NO_RESTART)
|
|
4801
5023
|
}
|
|
@@ -4897,7 +5119,7 @@ cli
|
|
|
4897
5119
|
} catch (error) {
|
|
4898
5120
|
cliLogger.error(
|
|
4899
5121
|
'Merge failed:',
|
|
4900
|
-
error instanceof Error ? error.
|
|
5122
|
+
error instanceof Error ? error.stack : String(error),
|
|
4901
5123
|
)
|
|
4902
5124
|
process.exit(EXIT_NO_RESTART)
|
|
4903
5125
|
}
|