kimaki 0.31.0 → 1.1.0
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/agent-ui.d.ts +33 -0
- package/dist/agent-ui.e2e.test.d.ts +1 -0
- package/dist/agent-ui.e2e.test.js +140 -0
- package/dist/agent-ui.e2e.test.js.map +1 -0
- package/dist/agent-ui.js +199 -0
- package/dist/agent-ui.js.map +1 -0
- package/dist/analytics.d.ts +42 -0
- package/dist/analytics.e2e.test.d.ts +1 -0
- package/dist/analytics.e2e.test.js +112 -0
- package/dist/analytics.e2e.test.js.map +1 -0
- package/dist/analytics.js +162 -223
- package/dist/analytics.js.map +1 -0
- package/dist/analytics.test.d.ts +1 -0
- package/dist/analytics.test.js +121 -157
- package/dist/analytics.test.js.map +1 -0
- package/dist/autostart.d.ts +7 -0
- package/dist/autostart.js +87 -0
- package/dist/autostart.js.map +1 -0
- package/dist/bot.d.ts +91 -0
- package/dist/bot.js +120 -0
- package/dist/bot.js.map +1 -0
- package/dist/btw.e2e.test.d.ts +1 -0
- package/dist/btw.e2e.test.js +102 -0
- package/dist/btw.e2e.test.js.map +1 -0
- package/dist/builtin-skills.d.ts +18 -0
- package/dist/builtin-skills.js +51 -0
- package/dist/builtin-skills.js.map +1 -0
- package/dist/builtin-skills.test.d.ts +1 -0
- package/dist/builtin-skills.test.js +28 -0
- package/dist/builtin-skills.test.js.map +1 -0
- package/dist/catalog-refresh.e2e.test.d.ts +1 -0
- package/dist/catalog-refresh.e2e.test.js +90 -0
- package/dist/catalog-refresh.e2e.test.js.map +1 -0
- package/dist/cli/bot.d.ts +5 -0
- package/dist/cli/bot.js +434 -0
- package/dist/cli/bot.js.map +1 -0
- package/dist/cli/discord.d.ts +3 -0
- package/dist/cli/discord.js +118 -0
- package/dist/cli/discord.js.map +1 -0
- package/dist/cli/project.d.ts +5 -0
- package/dist/cli/project.js +143 -0
- package/dist/cli/project.js.map +1 -0
- package/dist/cli/schedule.d.ts +3 -0
- package/dist/cli/schedule.js +156 -0
- package/dist/cli/schedule.js.map +1 -0
- package/dist/cli/session.d.ts +4 -0
- package/dist/cli/session.js +245 -0
- package/dist/cli/session.js.map +1 -0
- package/dist/cli/shared.d.ts +55 -0
- package/dist/cli/shared.js +138 -0
- package/dist/cli/shared.js.map +1 -0
- package/dist/cli-discord.e2e.test.d.ts +1 -0
- package/dist/cli-discord.e2e.test.js +82 -0
- package/dist/cli-discord.e2e.test.js.map +1 -0
- package/dist/cli-lock.e2e.test.d.ts +1 -0
- package/dist/cli-lock.e2e.test.js +520 -0
- package/dist/cli-lock.e2e.test.js.map +1 -0
- package/dist/cli-read.e2e.test.d.ts +1 -0
- package/dist/cli-read.e2e.test.js +86 -0
- package/dist/cli-read.e2e.test.js.map +1 -0
- package/dist/cli-sessions.e2e.test.d.ts +1 -0
- package/dist/cli-sessions.e2e.test.js +116 -0
- package/dist/cli-sessions.e2e.test.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +34 -263
- package/dist/cli.js.map +1 -0
- package/dist/commands/login-commands.d.ts +59 -0
- package/dist/commands/login-commands.js +341 -0
- package/dist/commands/login-commands.js.map +1 -0
- package/dist/commands/preference-commands.d.ts +44 -0
- package/dist/commands/preference-commands.js +565 -0
- package/dist/commands/preference-commands.js.map +1 -0
- package/dist/commands/session-commands.d.ts +8 -0
- package/dist/commands/session-commands.js +267 -0
- package/dist/commands/session-commands.js.map +1 -0
- package/dist/commands/thread-commands.d.ts +16 -0
- package/dist/commands/thread-commands.js +285 -0
- package/dist/commands/thread-commands.js.map +1 -0
- package/dist/commands/worktree-commands.d.ts +41 -0
- package/dist/commands/worktree-commands.js +279 -0
- package/dist/commands/worktree-commands.js.map +1 -0
- package/dist/commands.e2e.test.d.ts +1 -0
- package/dist/commands.e2e.test.js +255 -0
- package/dist/commands.e2e.test.js.map +1 -0
- package/dist/credentials.d.ts +84 -0
- package/dist/credentials.js +320 -0
- package/dist/credentials.js.map +1 -0
- package/dist/db.d.ts +1532 -0
- package/dist/db.js +57 -245
- package/dist/db.js.map +1 -0
- package/dist/db.test.d.ts +1 -0
- package/dist/db.test.js +351 -682
- package/dist/db.test.js.map +1 -0
- package/dist/duration.d.ts +2 -0
- package/dist/duration.js +18 -0
- package/dist/duration.js.map +1 -0
- package/dist/effects.d.ts +15 -0
- package/dist/effects.js +315 -0
- package/dist/effects.js.map +1 -0
- package/dist/errors.d.ts +38 -0
- package/dist/errors.js +40 -175
- package/dist/errors.js.map +1 -0
- package/dist/event-loop.d.ts +41 -0
- package/dist/event-loop.js +418 -0
- package/dist/event-loop.js.map +1 -0
- package/dist/file-edit-log.d.ts +41 -0
- package/dist/file-edit-log.js +122 -164
- package/dist/file-edit-log.js.map +1 -0
- package/dist/file-edit-log.test.d.ts +1 -0
- package/dist/file-edit-log.test.js +68 -244
- package/dist/file-edit-log.test.js.map +1 -0
- package/dist/format-parts.d.ts +137 -0
- package/dist/format-parts.js +300 -0
- package/dist/format-parts.js.map +1 -0
- package/dist/format-parts.test.d.ts +1 -0
- package/dist/format-parts.test.js +144 -0
- package/dist/format-parts.test.js.map +1 -0
- package/dist/gateway-replay.e2e.test.d.ts +1 -0
- package/dist/gateway-replay.e2e.test.js +95 -0
- package/dist/gateway-replay.e2e.test.js.map +1 -0
- package/dist/harness.e2e.test.d.ts +1 -0
- package/dist/harness.e2e.test.js +107 -0
- package/dist/harness.e2e.test.js.map +1 -0
- package/dist/hello-thread.e2e.test.d.ts +1 -0
- package/dist/hello-thread.e2e.test.js +194 -0
- package/dist/hello-thread.e2e.test.js.map +1 -0
- package/dist/ingress.d.ts +7 -0
- package/dist/ingress.js +337 -0
- package/dist/ingress.js.map +1 -0
- package/dist/interaction-context.d.ts +45 -0
- package/dist/interaction-context.js +77 -0
- package/dist/interaction-context.js.map +1 -0
- package/dist/interrupt.e2e.test.d.ts +1 -0
- package/dist/interrupt.e2e.test.js +124 -0
- package/dist/interrupt.e2e.test.js.map +1 -0
- package/dist/legacy.e2e.test.d.ts +1 -0
- package/dist/legacy.e2e.test.js +85 -0
- package/dist/legacy.e2e.test.js.map +1 -0
- package/dist/live-call.d.ts +51 -0
- package/dist/live-call.js +207 -0
- package/dist/live-call.js.map +1 -0
- package/dist/lock-routes.d.ts +812 -0
- package/dist/lock-routes.js +635 -0
- package/dist/lock-routes.js.map +1 -0
- package/dist/lock-server.d.ts +36 -0
- package/dist/lock-server.js +223 -0
- package/dist/lock-server.js.map +1 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.js +112 -162
- package/dist/logger.js.map +1 -0
- package/dist/login.e2e.test.d.ts +1 -0
- package/dist/login.e2e.test.js +83 -0
- package/dist/login.e2e.test.js.map +1 -0
- package/dist/main.d.ts +33 -0
- package/dist/main.js +261 -0
- package/dist/main.js.map +1 -0
- package/dist/markdown/components.d.ts +10 -0
- package/dist/markdown/components.js +77 -0
- package/dist/markdown/components.js.map +1 -0
- package/dist/markdown/markdown.test.d.ts +1 -0
- package/dist/markdown/markdown.test.js +393 -0
- package/dist/markdown/markdown.test.js.map +1 -0
- package/dist/markdown/render-markdown.d.ts +30 -0
- package/dist/markdown/render-markdown.js +265 -0
- package/dist/markdown/render-markdown.js.map +1 -0
- package/dist/message-tool.e2e.test.d.ts +1 -0
- package/dist/message-tool.e2e.test.js +39 -0
- package/dist/message-tool.e2e.test.js.map +1 -0
- package/dist/migrations.d.ts +8 -0
- package/dist/migrations.js +187 -0
- package/dist/migrations.js.map +1 -0
- package/dist/onboarding-cli.e2e.test.d.ts +1 -0
- package/dist/onboarding-cli.e2e.test.js +178 -0
- package/dist/onboarding-cli.e2e.test.js.map +1 -0
- package/dist/onboarding.d.ts +36 -0
- package/dist/onboarding.e2e.test.d.ts +1 -0
- package/dist/onboarding.e2e.test.js +148 -0
- package/dist/onboarding.e2e.test.js.map +1 -0
- package/dist/onboarding.js +261 -0
- package/dist/onboarding.js.map +1 -0
- package/dist/opencode-server.d.ts +54 -0
- package/dist/opencode-server.js +417 -0
- package/dist/opencode-server.js.map +1 -0
- package/dist/opencode-server.test.d.ts +1 -0
- package/dist/opencode-server.test.js +21 -0
- package/dist/opencode-server.test.js.map +1 -0
- package/dist/permissions.d.ts +31 -0
- package/dist/permissions.e2e.test.d.ts +1 -0
- package/dist/permissions.e2e.test.js +166 -0
- package/dist/permissions.e2e.test.js.map +1 -0
- package/dist/permissions.js +95 -0
- package/dist/permissions.js.map +1 -0
- package/dist/plugin/anthropic/index.d.ts +13 -0
- package/dist/plugin/anthropic/index.js +201 -0
- package/dist/plugin/anthropic/index.js.map +1 -0
- package/dist/plugin/anthropic/index.test.d.ts +1 -0
- package/dist/plugin/anthropic/index.test.js +39 -0
- package/dist/plugin/anthropic/index.test.js.map +1 -0
- package/dist/plugin/index.d.ts +3 -0
- package/dist/plugin/index.js +124 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/read-media.d.ts +23 -0
- package/dist/plugin/read-media.js +72 -0
- package/dist/plugin/read-media.js.map +1 -0
- package/dist/plugin/session-id/index.d.ts +3 -0
- package/dist/plugin/session-id/index.js +18 -0
- package/dist/plugin/session-id/index.js.map +1 -0
- package/dist/plugin.e2e.test.d.ts +1 -0
- package/dist/plugin.e2e.test.js +92 -0
- package/dist/plugin.e2e.test.js.map +1 -0
- package/dist/preference-commands.e2e.test.d.ts +1 -0
- package/dist/preference-commands.e2e.test.js +555 -0
- package/dist/preference-commands.e2e.test.js.map +1 -0
- package/dist/probe-live.tmp.d.ts +1 -0
- package/dist/probe-live.tmp.js +82 -0
- package/dist/probe-live.tmp.js.map +1 -0
- package/dist/project.d.ts +58 -0
- package/dist/project.js +169 -0
- package/dist/project.js.map +1 -0
- package/dist/prompt.d.ts +112 -0
- package/dist/prompt.js +577 -0
- package/dist/prompt.js.map +1 -0
- package/dist/questions.d.ts +59 -0
- package/dist/questions.e2e.test.d.ts +1 -0
- package/dist/questions.e2e.test.js +290 -0
- package/dist/questions.e2e.test.js.map +1 -0
- package/dist/questions.js +231 -0
- package/dist/questions.js.map +1 -0
- package/dist/queue.d.ts +45 -0
- package/dist/queue.e2e.test.d.ts +1 -0
- package/dist/queue.e2e.test.js +220 -0
- package/dist/queue.e2e.test.js.map +1 -0
- package/dist/queue.js +132 -0
- package/dist/queue.js.map +1 -0
- package/dist/remote-send.d.ts +32 -0
- package/dist/remote-send.e2e.test.d.ts +1 -0
- package/dist/remote-send.e2e.test.js +89 -0
- package/dist/remote-send.e2e.test.js.map +1 -0
- package/dist/remote-send.js +105 -0
- package/dist/remote-send.js.map +1 -0
- package/dist/renderer.e2e.test.d.ts +1 -0
- package/dist/renderer.e2e.test.js +357 -0
- package/dist/renderer.e2e.test.js.map +1 -0
- package/dist/restart.e2e.test.d.ts +1 -0
- package/dist/restart.e2e.test.js +68 -0
- package/dist/restart.e2e.test.js.map +1 -0
- package/dist/routes.d.ts +24 -0
- package/dist/routes.js +42 -0
- package/dist/routes.js.map +1 -0
- package/dist/routes.test.d.ts +1 -0
- package/dist/routes.test.js +79 -0
- package/dist/routes.test.js.map +1 -0
- package/dist/routines.e2e.test.d.ts +1 -0
- package/dist/routines.e2e.test.js +188 -0
- package/dist/routines.e2e.test.js.map +1 -0
- package/dist/scheduler.d.ts +116 -0
- package/dist/scheduler.e2e.test.d.ts +1 -0
- package/dist/scheduler.e2e.test.js +163 -0
- package/dist/scheduler.e2e.test.js.map +1 -0
- package/dist/scheduler.js +602 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/scheduler.test.d.ts +1 -0
- package/dist/scheduler.test.js +180 -0
- package/dist/scheduler.test.js.map +1 -0
- package/dist/schema-sql.d.ts +1 -0
- package/dist/schema-sql.js +3 -0
- package/dist/schema-sql.js.map +1 -0
- package/dist/schema.d.ts +2968 -0
- package/dist/schema.js +95 -252
- package/dist/schema.js.map +1 -0
- package/dist/session-commands.e2e.test.d.ts +1 -0
- package/dist/session-commands.e2e.test.js +285 -0
- package/dist/session-commands.e2e.test.js.map +1 -0
- package/dist/session-events.d.ts +83 -0
- package/dist/session-events.js +260 -0
- package/dist/session-events.js.map +1 -0
- package/dist/session-ops.e2e.test.d.ts +1 -0
- package/dist/session-ops.e2e.test.js +114 -0
- package/dist/session-ops.e2e.test.js.map +1 -0
- package/dist/sessions.d.ts +121 -0
- package/dist/sessions.js +663 -0
- package/dist/sessions.js.map +1 -0
- package/dist/shell.e2e.test.d.ts +1 -0
- package/dist/shell.e2e.test.js +147 -0
- package/dist/shell.e2e.test.js.map +1 -0
- package/dist/slash-commands.d.ts +62 -0
- package/dist/slash-commands.js +476 -0
- package/dist/slash-commands.js.map +1 -0
- package/dist/slash-commands.test.d.ts +1 -0
- package/dist/slash-commands.test.js +87 -0
- package/dist/slash-commands.test.js.map +1 -0
- package/dist/sleep.e2e.test.d.ts +1 -0
- package/dist/sleep.e2e.test.js +115 -487
- package/dist/sleep.e2e.test.js.map +1 -0
- package/dist/sleeps.d.ts +37 -0
- package/dist/sleeps.js +220 -0
- package/dist/sleeps.js.map +1 -0
- package/dist/store.d.ts +10 -0
- package/dist/store.js +8 -28
- package/dist/store.js.map +1 -0
- package/dist/system-prompt.d.ts +76 -0
- package/dist/system-prompt.js +852 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/tasks.e2e.test.d.ts +1 -0
- package/dist/tasks.e2e.test.js +173 -0
- package/dist/tasks.e2e.test.js.map +1 -0
- package/dist/test/auth-plugin/index.d.ts +3 -0
- package/dist/test/auth-plugin/index.js +18 -0
- package/dist/test/auth-plugin/index.js.map +1 -0
- package/dist/test/fake-mcp-server.d.ts +1 -0
- package/dist/test/fake-mcp-server.js +45 -0
- package/dist/test/fake-mcp-server.js.map +1 -0
- package/dist/test/harness.d.ts +246 -0
- package/dist/test/harness.js +704 -0
- package/dist/test/harness.js.map +1 -0
- package/dist/test/replay.d.ts +14 -0
- package/dist/test/replay.js +75 -0
- package/dist/test/replay.js.map +1 -0
- package/dist/thread-reducer.d.ts +172 -0
- package/dist/thread-reducer.fixtures.test.d.ts +1 -0
- package/dist/thread-reducer.fixtures.test.js +360 -0
- package/dist/thread-reducer.fixtures.test.js.map +1 -0
- package/dist/thread-reducer.js +497 -0
- package/dist/thread-reducer.js.map +1 -0
- package/dist/thread-reducer.test.d.ts +1 -0
- package/dist/thread-reducer.test.js +369 -0
- package/dist/thread-reducer.test.js.map +1 -0
- package/dist/tts.test.d.ts +1 -0
- package/dist/tts.test.js +147 -0
- package/dist/tts.test.js.map +1 -0
- package/dist/voice-agent.d.ts +33 -0
- package/dist/voice-agent.js +159 -0
- package/dist/voice-agent.js.map +1 -0
- package/dist/voice-attachments.e2e.test.d.ts +1 -0
- package/dist/voice-attachments.e2e.test.js +256 -0
- package/dist/voice-attachments.e2e.test.js.map +1 -0
- package/dist/voice-call.d.ts +59 -0
- package/dist/voice-call.e2e.test.d.ts +1 -0
- package/dist/voice-call.e2e.test.js +259 -0
- package/dist/voice-call.e2e.test.js.map +1 -0
- package/dist/voice-call.js +760 -0
- package/dist/voice-call.js.map +1 -0
- package/dist/voice-openai.e2e.test.d.ts +1 -0
- package/dist/voice-openai.e2e.test.js +161 -0
- package/dist/voice-openai.e2e.test.js.map +1 -0
- package/dist/voice.d.ts +116 -0
- package/dist/voice.js +619 -717
- package/dist/voice.js.map +1 -0
- package/dist/working-directory.e2e.test.d.ts +1 -0
- package/dist/working-directory.e2e.test.js +231 -0
- package/dist/working-directory.e2e.test.js.map +1 -0
- package/dist/worktrees.d.ts +54 -0
- package/dist/worktrees.js +247 -709
- package/dist/worktrees.js.map +1 -0
- package/dist/worktrees.test.d.ts +1 -0
- package/dist/worktrees.test.js +97 -952
- package/dist/worktrees.test.js.map +1 -0
- package/package.json +38 -73
- package/skills/kimaki-debug/SKILL.md +44 -0
- package/skills/kimaki-tunnel/SKILL.md +34 -0
- package/src/agent-ui.e2e.test.ts +134 -0
- package/src/agent-ui.ts +216 -0
- package/src/analytics.e2e.test.ts +138 -0
- package/src/analytics.test.ts +122 -182
- package/src/analytics.ts +206 -258
- package/src/autostart.ts +90 -0
- package/src/bot.ts +196 -0
- package/src/btw.e2e.test.ts +130 -0
- package/src/builtin-skills.test.ts +29 -0
- package/src/builtin-skills.ts +59 -0
- package/src/catalog-refresh.e2e.test.ts +112 -0
- package/src/cli/bot.ts +425 -0
- package/src/cli/discord.ts +116 -0
- package/src/cli/project.ts +147 -0
- package/src/cli/schedule.ts +151 -0
- package/src/cli/session.ts +252 -0
- package/src/cli/shared.ts +139 -0
- package/src/cli-discord.e2e.test.ts +79 -0
- package/src/cli-lock.e2e.test.ts +530 -0
- package/src/cli-read.e2e.test.ts +83 -0
- package/src/cli-sessions.e2e.test.ts +120 -0
- package/src/cli.ts +35 -418
- package/src/commands/login-commands.ts +360 -0
- package/src/commands/preference-commands.ts +599 -0
- package/src/commands/session-commands.ts +289 -0
- package/src/commands/thread-commands.ts +324 -0
- package/src/commands/worktree-commands.ts +288 -0
- package/src/commands.e2e.test.ts +290 -0
- package/src/credentials.ts +425 -0
- package/src/db.test.ts +344 -759
- package/src/db.ts +66 -284
- package/src/duration.ts +18 -0
- package/src/effects.ts +337 -0
- package/src/errors.ts +39 -228
- package/src/event-loop.ts +471 -0
- package/src/file-edit-log.test.ts +68 -272
- package/src/file-edit-log.ts +108 -231
- package/src/fixtures/v1-schema.sql +241 -0
- package/src/fixtures/voice.ogg +0 -0
- package/src/format-parts.test.ts +150 -0
- package/src/format-parts.ts +405 -0
- package/src/gateway-replay.e2e.test.ts +116 -0
- package/src/harness.e2e.test.ts +124 -0
- package/src/hello-thread.e2e.test.ts +223 -0
- package/src/ingress.ts +334 -0
- package/src/interaction-context.ts +131 -0
- package/src/interrupt.e2e.test.ts +151 -0
- package/src/legacy.e2e.test.ts +105 -0
- package/src/live-call.ts +249 -0
- package/src/lock-routes.ts +625 -0
- package/src/lock-server.ts +233 -0
- package/src/logger.ts +120 -207
- package/src/login.e2e.test.ts +86 -0
- package/src/main.ts +238 -0
- package/src/markdown/components.ts +102 -0
- package/src/markdown/markdown.test.ts +414 -0
- package/src/markdown/render-markdown.ts +288 -0
- package/src/migrations.ts +198 -0
- package/src/onboarding-cli.e2e.test.ts +215 -0
- package/src/onboarding.e2e.test.ts +171 -0
- package/src/onboarding.ts +297 -0
- package/src/opencode-server.test.ts +24 -0
- package/src/opencode-server.ts +464 -0
- package/src/permissions.e2e.test.ts +191 -0
- package/src/permissions.ts +121 -0
- package/src/plugin/anthropic/index.test.ts +39 -0
- package/src/plugin/anthropic/index.ts +213 -0
- package/src/plugin/index.ts +112 -0
- package/src/plugin.e2e.test.ts +117 -0
- package/src/preference-commands.e2e.test.ts +597 -0
- package/src/project.ts +216 -0
- package/src/prompt.ts +664 -0
- package/src/questions.e2e.test.ts +328 -0
- package/src/questions.ts +322 -0
- package/src/queue.e2e.test.ts +253 -0
- package/src/queue.ts +158 -0
- package/src/remote-send.e2e.test.ts +91 -0
- package/src/remote-send.ts +110 -0
- package/src/renderer.e2e.test.ts +390 -0
- package/src/restart.e2e.test.ts +90 -0
- package/src/routes.test.ts +81 -0
- package/src/routes.ts +48 -0
- package/src/routines.e2e.test.ts +210 -0
- package/src/scheduler.e2e.test.ts +182 -0
- package/src/scheduler.test.ts +183 -0
- package/src/scheduler.ts +648 -0
- package/src/schema-sql.ts +3 -0
- package/src/schema.ts +95 -271
- package/src/session-commands.e2e.test.ts +326 -0
- package/src/session-events.ts +301 -0
- package/src/session-ops.e2e.test.ts +141 -0
- package/src/sessions.ts +804 -0
- package/src/shell.e2e.test.ts +173 -0
- package/src/slash-commands.test.ts +89 -0
- package/src/slash-commands.ts +540 -0
- package/src/sleep.e2e.test.ts +135 -552
- package/src/sleeps.ts +230 -0
- package/src/store.ts +19 -217
- package/src/system-prompt.ts +923 -0
- package/src/test/auth-plugin/index.ts +17 -0
- package/src/test/fake-mcp-server.ts +49 -0
- package/src/test/harness.ts +957 -0
- package/src/test/replay.ts +90 -0
- package/src/thread-reducer.fixtures.test.ts +374 -0
- package/src/thread-reducer.test.ts +380 -0
- package/src/thread-reducer.ts +671 -0
- package/src/tts.test.ts +150 -0
- package/src/voice-agent.ts +196 -0
- package/src/voice-attachments.e2e.test.ts +301 -0
- package/src/voice-call.e2e.test.ts +293 -0
- package/src/voice-call.ts +764 -0
- package/src/voice-openai.e2e.test.ts +191 -0
- package/src/voice.ts +668 -988
- package/src/working-directory.e2e.test.ts +226 -0
- package/src/worktrees.test.ts +89 -1115
- package/src/worktrees.ts +195 -957
- package/bin.js +0 -2
- package/dist/agent-model.e2e.test.js +0 -1223
- package/dist/ai-tool-to-genai.js +0 -233
- package/dist/ai-tool-to-genai.test.js +0 -267
- package/dist/ai-tool.js +0 -6
- package/dist/anthropic-account-identity.js +0 -70
- package/dist/anthropic-account-identity.test.js +0 -38
- package/dist/anthropic-auth-plugin.js +0 -958
- package/dist/anthropic-auth-plugin.test.js +0 -115
- package/dist/anthropic-auth-state.js +0 -186
- package/dist/anthropic-auth-state.test.js +0 -203
- package/dist/bash-tool-schema-plugin.js +0 -50
- package/dist/bash-tool-schema-plugin.test.js +0 -68
- package/dist/bin.js +0 -151
- package/dist/bot-lifecycle.test.js +0 -206
- package/dist/btw-prefix-detection.js +0 -24
- package/dist/btw-prefix-detection.test.js +0 -130
- package/dist/cache-drift-plugin.js +0 -183
- package/dist/cache-rewrite.js +0 -39
- package/dist/cache-rewrite.test.js +0 -41
- package/dist/channel-management.js +0 -347
- package/dist/cli-commands/bot.js +0 -254
- package/dist/cli-commands/maintenance.js +0 -171
- package/dist/cli-commands/misc.js +0 -153
- package/dist/cli-commands/project.js +0 -510
- package/dist/cli-commands/send.js +0 -659
- package/dist/cli-commands/session.js +0 -889
- package/dist/cli-commands/task.js +0 -277
- package/dist/cli-commands/thread.js +0 -63
- package/dist/cli-commands/user.js +0 -145
- package/dist/cli-parsing.test.js +0 -226
- package/dist/cli-runner.js +0 -1591
- package/dist/cli-runner.test.js +0 -119
- package/dist/cli-send-thread.e2e.test.js +0 -470
- package/dist/cloudflare-transcription.js +0 -56
- package/dist/commands/abort.js +0 -84
- package/dist/commands/action-buttons.js +0 -248
- package/dist/commands/add-project.js +0 -111
- package/dist/commands/agent.js +0 -658
- package/dist/commands/ask-question.js +0 -371
- package/dist/commands/ask-question.test.js +0 -150
- package/dist/commands/btw.js +0 -275
- package/dist/commands/compact.js +0 -117
- package/dist/commands/context-usage.js +0 -194
- package/dist/commands/context-usage.test.js +0 -49
- package/dist/commands/create-new-project.js +0 -138
- package/dist/commands/diff.js +0 -63
- package/dist/commands/file-upload.js +0 -280
- package/dist/commands/fork-subagent.js +0 -175
- package/dist/commands/fork.js +0 -291
- package/dist/commands/gemini-apikey.js +0 -123
- package/dist/commands/last-sessions.js +0 -129
- package/dist/commands/login.js +0 -893
- package/dist/commands/mcp.js +0 -239
- package/dist/commands/memory-snapshot.js +0 -24
- package/dist/commands/mention-mode.js +0 -42
- package/dist/commands/merge-worktree.js +0 -205
- package/dist/commands/model-variant.js +0 -390
- package/dist/commands/model.js +0 -1012
- package/dist/commands/multioauth.js +0 -425
- package/dist/commands/new-worktree.js +0 -625
- package/dist/commands/paginated-select.js +0 -57
- package/dist/commands/permissions.js +0 -327
- package/dist/commands/queue.js +0 -347
- package/dist/commands/queue.test.js +0 -15
- package/dist/commands/remove-project.js +0 -111
- package/dist/commands/restart-opencode-server.js +0 -133
- package/dist/commands/resume.js +0 -148
- package/dist/commands/run-command.js +0 -79
- package/dist/commands/screenshare.js +0 -307
- package/dist/commands/screenshare.test.js +0 -20
- package/dist/commands/session-id.js +0 -78
- package/dist/commands/session.js +0 -248
- package/dist/commands/share.js +0 -80
- package/dist/commands/tasks.js +0 -296
- package/dist/commands/types.js +0 -2
- package/dist/commands/undo-redo.js +0 -313
- package/dist/commands/unset-model.js +0 -116
- package/dist/commands/upgrade.js +0 -41
- package/dist/commands/user-command.js +0 -196
- package/dist/commands/verbosity.js +0 -124
- package/dist/commands/vscode.js +0 -295
- package/dist/commands/worktrees.js +0 -548
- package/dist/condense-memory.js +0 -33
- package/dist/config.js +0 -126
- package/dist/context-awareness-plugin.js +0 -357
- package/dist/context-awareness-plugin.test.js +0 -126
- package/dist/cpu-profiler.js +0 -211
- package/dist/cpu-profiler.test.js +0 -160
- package/dist/critique-utils.js +0 -95
- package/dist/database.js +0 -1074
- package/dist/debounce-timeout.js +0 -28
- package/dist/debounced-process-flush.js +0 -77
- package/dist/default-channel-provisioning.test.js +0 -149
- package/dist/discord-bot.js +0 -1432
- package/dist/discord-command-registration.js +0 -519
- package/dist/discord-command-registration.test.js +0 -17
- package/dist/discord-urls.js +0 -81
- package/dist/discord-utils.js +0 -789
- package/dist/discord-utils.test.js +0 -400
- package/dist/escape-backticks.test.js +0 -429
- package/dist/event-stream-real-capture.e2e.test.js +0 -533
- package/dist/eventsource-parser.test.js +0 -327
- package/dist/exec-async.js +0 -38
- package/dist/external-directory-permissions.e2e.test.js +0 -86
- package/dist/external-directory-project-deny.e2e.test.js +0 -70
- package/dist/external-opencode-sync.js +0 -620
- package/dist/external-opencode-sync.test.js +0 -206
- package/dist/extract-sdk-error-message.test.js +0 -13
- package/dist/format-tables.js +0 -728
- package/dist/format-tables.test.js +0 -915
- package/dist/forum-sync/config.js +0 -79
- package/dist/forum-sync/discord-operations.js +0 -157
- package/dist/forum-sync/index.js +0 -5
- package/dist/forum-sync/markdown.js +0 -110
- package/dist/forum-sync/sync-to-discord.js +0 -417
- package/dist/forum-sync/sync-to-files.js +0 -190
- package/dist/forum-sync/types.js +0 -53
- package/dist/forum-sync/watchers.js +0 -322
- package/dist/gateway-proxy-reconnect.e2e.test.js +0 -414
- package/dist/gateway-proxy.e2e.test.js +0 -488
- package/dist/genai-worker-wrapper.js +0 -111
- package/dist/genai-worker.js +0 -309
- package/dist/genai.js +0 -232
- package/dist/git-worktree-core.js +0 -461
- package/dist/guild-categories.test.js +0 -225
- package/dist/heap-monitor.js +0 -122
- package/dist/hrana-server.js +0 -322
- package/dist/hrana-server.test.js +0 -99
- package/dist/html-actions.js +0 -123
- package/dist/html-actions.test.js +0 -70
- package/dist/html-components.js +0 -115
- package/dist/html-components.test.js +0 -34
- package/dist/image-optimizer-plugin.js +0 -153
- package/dist/image-utils.js +0 -112
- package/dist/interaction-handler.js +0 -578
- package/dist/ipc-polling.js +0 -245
- package/dist/ipc-tools-plugin.js +0 -251
- package/dist/kimaki-digital-twin.e2e.test.js +0 -161
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +0 -158
- package/dist/kimaki-opencode-plugin.js +0 -33
- package/dist/kimaki-opencode-plugin.test.js +0 -126
- package/dist/kimaki-workspace-adaptor.js +0 -118
- package/dist/limit-heading-depth.js +0 -25
- package/dist/limit-heading-depth.test.js +0 -105
- package/dist/list-channel-threads.js +0 -163
- package/dist/list-channel-threads.test.js +0 -80
- package/dist/markdown.js +0 -455
- package/dist/markdown.test.js +0 -411
- package/dist/memory-overview-plugin.js +0 -123
- package/dist/message-formatting.js +0 -788
- package/dist/message-formatting.test.js +0 -1132
- package/dist/message-preprocessing.js +0 -419
- package/dist/oauth-rotation-shared.js +0 -253
- package/dist/onboarding-tutorial.js +0 -163
- package/dist/onboarding-welcome.js +0 -37
- package/dist/openai-auth-plugin.js +0 -125
- package/dist/openai-auth-state.js +0 -237
- package/dist/openai-realtime.js +0 -224
- package/dist/opencode-command-detection.js +0 -65
- package/dist/opencode-command-detection.test.js +0 -240
- package/dist/opencode-command.js +0 -181
- package/dist/opencode-command.test.js +0 -218
- package/dist/opencode-interrupt-plugin.js +0 -253
- package/dist/opencode-interrupt-plugin.test.js +0 -293
- package/dist/opencode.js +0 -1217
- package/dist/parse-permission-rules.test.js +0 -117
- package/dist/patch-text-parser.js +0 -97
- package/dist/plugin-logger.js +0 -105
- package/dist/plugin-opencode-client.js +0 -54
- package/dist/privacy-sanitizer.js +0 -105
- package/dist/project-add.test.js +0 -62
- package/dist/question-abort-clears-dropdown.e2e.test.js +0 -89
- package/dist/question-abort-resume.e2e.test.js +0 -100
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +0 -150
- package/dist/question-text-before-dropdown.e2e.test.js +0 -52
- package/dist/queue-advanced-abort.e2e.test.js +0 -348
- package/dist/queue-advanced-action-buttons.e2e.test.js +0 -209
- package/dist/queue-advanced-e2e-setup.js +0 -1043
- package/dist/queue-advanced-footer.e2e.test.js +0 -286
- package/dist/queue-advanced-model-switch.e2e.test.js +0 -297
- package/dist/queue-advanced-permissions-typing.e2e.test.js +0 -189
- package/dist/queue-advanced-question.e2e.test.js +0 -266
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +0 -113
- package/dist/queue-advanced-typing.e2e.test.js +0 -151
- package/dist/queue-delete-message.e2e.test.js +0 -216
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +0 -120
- package/dist/queue-interrupt-drain.e2e.test.js +0 -256
- package/dist/queue-question-select-drain.e2e.test.js +0 -162
- package/dist/runtime-idle-sweeper.js +0 -52
- package/dist/runtime-lifecycle.e2e.test.js +0 -570
- package/dist/sentry.js +0 -23
- package/dist/session-handler/agent-utils.js +0 -65
- package/dist/session-handler/event-stream-state.js +0 -1225
- package/dist/session-handler/event-stream-state.test.js +0 -3160
- package/dist/session-handler/global-event-listener.js +0 -212
- package/dist/session-handler/model-utils.js +0 -311
- package/dist/session-handler/model-utils.test.js +0 -445
- package/dist/session-handler/opencode-session-event-log.js +0 -121
- package/dist/session-handler/thread-ingress.test.js +0 -41
- package/dist/session-handler/thread-runtime-state.js +0 -209
- package/dist/session-handler/thread-session-runtime-summary.test.js +0 -121
- package/dist/session-handler/thread-session-runtime.js +0 -4320
- package/dist/session-handler.js +0 -9
- package/dist/session-search.js +0 -219
- package/dist/session-search.test.js +0 -287
- package/dist/session-sleep.test.js +0 -224
- package/dist/session-title-rename.test.js +0 -128
- package/dist/skill-filter.js +0 -43
- package/dist/skill-filter.test.js +0 -86
- package/dist/startup-service.js +0 -153
- package/dist/startup-time.e2e.test.js +0 -297
- package/dist/system-message.js +0 -806
- package/dist/system-message.test.js +0 -917
- package/dist/task-id-plugin.js +0 -16
- package/dist/task-runner.js +0 -554
- package/dist/task-schedule.js +0 -298
- package/dist/task-schedule.test.js +0 -366
- package/dist/test-utils.js +0 -385
- package/dist/thinking-utils.js +0 -45
- package/dist/thinking-utils.test.js +0 -35
- package/dist/thread-message-queue.e2e.test.js +0 -1227
- package/dist/tools.js +0 -357
- package/dist/undo-redo.e2e.test.js +0 -171
- package/dist/unnest-code-blocks.js +0 -146
- package/dist/unnest-code-blocks.test.js +0 -673
- package/dist/upgrade.js +0 -114
- package/dist/utils.js +0 -148
- package/dist/voice-attachment.js +0 -56
- package/dist/voice-handler.js +0 -727
- package/dist/voice-message.e2e.test.js +0 -1254
- package/dist/voice-routing-input.e2e.test.js +0 -195
- package/dist/voice-routing-question.e2e.test.js +0 -182
- package/dist/voice.test.js +0 -500
- package/dist/wait-session.js +0 -193
- package/dist/websockify.js +0 -69
- package/dist/worker-types.js +0 -4
- package/dist/worktree-clone-isolation.e2e.test.js +0 -188
- package/dist/worktree-lifecycle.e2e.test.js +0 -735
- package/dist/worktree-utils.js +0 -3
- package/dist/xai-auth-plugin.js +0 -238
- package/dist/xai-auth-state.js +0 -234
- package/dist/xai-auth-state.test.js +0 -183
- package/dist/xml.js +0 -92
- package/dist/xml.test.js +0 -32
- package/skills/bump-holocron-consumers/SKILL.md +0 -137
- package/skills/critique/SKILL.md +0 -116
- package/skills/egaki/SKILL.md +0 -86
- package/skills/errore/SKILL.md +0 -501
- package/skills/goke/SKILL.md +0 -695
- package/skills/holocron/SKILL.md +0 -942
- package/skills/lintcn/SKILL.md +0 -873
- package/skills/new-skill/SKILL.md +0 -339
- package/skills/npm-package/SKILL.md +0 -692
- package/skills/opensrc/SKILL.md +0 -102
- package/skills/playwriter/SKILL.md +0 -35
- package/skills/profano/SKILL.md +0 -33
- package/skills/readme.md +0 -20
- package/skills/sigillo/SKILL.md +0 -370
- package/skills/spiceflow/SKILL.md +0 -80
- package/skills/strada/SKILL.md +0 -104
- package/skills/termcast/SKILL.md +0 -133
- package/skills/tuistory/SKILL.md +0 -237
- package/skills/usecomputer/SKILL.md +0 -264
- package/skills/zele/SKILL.md +0 -81
- package/src/agent-model.e2e.test.ts +0 -1557
- package/src/ai-tool-to-genai.test.ts +0 -296
- package/src/ai-tool-to-genai.ts +0 -283
- package/src/ai-tool.ts +0 -39
- package/src/anthropic-account-identity.test.ts +0 -52
- package/src/anthropic-account-identity.ts +0 -96
- package/src/anthropic-auth-plugin.test.ts +0 -135
- package/src/anthropic-auth-plugin.ts +0 -1180
- package/src/anthropic-auth-state.test.ts +0 -259
- package/src/anthropic-auth-state.ts +0 -272
- package/src/bash-tool-schema-plugin.test.ts +0 -84
- package/src/bash-tool-schema-plugin.ts +0 -77
- package/src/bin.ts +0 -173
- package/src/bot-lifecycle.test.ts +0 -252
- package/src/btw-prefix-detection.test.ts +0 -142
- package/src/btw-prefix-detection.ts +0 -30
- package/src/cache-drift-plugin.ts +0 -249
- package/src/cache-rewrite.test.ts +0 -49
- package/src/cache-rewrite.ts +0 -96
- package/src/channel-management.ts +0 -494
- package/src/cli-commands/bot.ts +0 -374
- package/src/cli-commands/maintenance.ts +0 -237
- package/src/cli-commands/misc.ts +0 -208
- package/src/cli-commands/project.ts +0 -680
- package/src/cli-commands/send.ts +0 -885
- package/src/cli-commands/session.ts +0 -1181
- package/src/cli-commands/task.ts +0 -358
- package/src/cli-commands/thread.ts +0 -86
- package/src/cli-commands/user.ts +0 -195
- package/src/cli-parsing.test.ts +0 -305
- package/src/cli-runner.test.ts +0 -166
- package/src/cli-runner.ts +0 -2157
- package/src/cli-send-thread.e2e.test.ts +0 -609
- package/src/cloudflare-transcription.ts +0 -76
- package/src/commands/abort.ts +0 -113
- package/src/commands/action-buttons.ts +0 -371
- package/src/commands/add-project.ts +0 -146
- package/src/commands/agent.ts +0 -911
- package/src/commands/ask-question.test.ts +0 -177
- package/src/commands/ask-question.ts +0 -532
- package/src/commands/btw.ts +0 -356
- package/src/commands/compact.ts +0 -154
- package/src/commands/context-usage.test.ts +0 -57
- package/src/commands/context-usage.ts +0 -267
- package/src/commands/create-new-project.ts +0 -203
- package/src/commands/diff.ts +0 -91
- package/src/commands/file-upload.ts +0 -398
- package/src/commands/fork-subagent.ts +0 -262
- package/src/commands/fork.ts +0 -421
- package/src/commands/gemini-apikey.ts +0 -205
- package/src/commands/last-sessions.ts +0 -176
- package/src/commands/login.ts +0 -1182
- package/src/commands/mcp.ts +0 -307
- package/src/commands/memory-snapshot.ts +0 -30
- package/src/commands/mention-mode.ts +0 -66
- package/src/commands/merge-worktree.ts +0 -282
- package/src/commands/model-variant.ts +0 -522
- package/src/commands/model.ts +0 -1377
- package/src/commands/multioauth.ts +0 -508
- package/src/commands/new-worktree.ts +0 -835
- package/src/commands/paginated-select.ts +0 -81
- package/src/commands/permissions.ts +0 -458
- package/src/commands/queue.test.ts +0 -19
- package/src/commands/queue.ts +0 -471
- package/src/commands/remove-project.ts +0 -152
- package/src/commands/restart-opencode-server.ts +0 -173
- package/src/commands/resume.ts +0 -229
- package/src/commands/run-command.ts +0 -123
- package/src/commands/screenshare.test.ts +0 -30
- package/src/commands/screenshare.ts +0 -388
- package/src/commands/session-id.ts +0 -109
- package/src/commands/session.ts +0 -319
- package/src/commands/share.ts +0 -106
- package/src/commands/tasks.ts +0 -399
- package/src/commands/types.ts +0 -25
- package/src/commands/undo-redo.ts +0 -394
- package/src/commands/unset-model.ts +0 -152
- package/src/commands/upgrade.ts +0 -51
- package/src/commands/user-command.ts +0 -260
- package/src/commands/verbosity.ts +0 -172
- package/src/commands/vscode.ts +0 -378
- package/src/commands/worktrees.ts +0 -762
- package/src/condense-memory.ts +0 -36
- package/src/config.ts +0 -147
- package/src/context-awareness-plugin.test.ts +0 -144
- package/src/context-awareness-plugin.ts +0 -508
- package/src/cpu-profiler.test.ts +0 -183
- package/src/cpu-profiler.ts +0 -293
- package/src/critique-utils.ts +0 -139
- package/src/database.ts +0 -1484
- package/src/debounce-timeout.ts +0 -43
- package/src/debounced-process-flush.ts +0 -104
- package/src/default-channel-provisioning.test.ts +0 -156
- package/src/discord-bot.ts +0 -1852
- package/src/discord-command-registration.test.ts +0 -20
- package/src/discord-command-registration.ts +0 -704
- package/src/discord-urls.ts +0 -88
- package/src/discord-utils.test.ts +0 -470
- package/src/discord-utils.ts +0 -1079
- package/src/escape-backticks.test.ts +0 -469
- package/src/event-stream-real-capture.e2e.test.ts +0 -692
- package/src/eventsource-parser.test.ts +0 -351
- package/src/exec-async.ts +0 -44
- package/src/external-directory-permissions.e2e.test.ts +0 -103
- package/src/external-directory-project-deny.e2e.test.ts +0 -84
- package/src/external-opencode-sync.test.ts +0 -248
- package/src/external-opencode-sync.ts +0 -867
- package/src/extract-sdk-error-message.test.ts +0 -18
- package/src/format-tables.test.ts +0 -988
- package/src/format-tables.ts +0 -1031
- package/src/forum-sync/config.ts +0 -92
- package/src/forum-sync/discord-operations.ts +0 -245
- package/src/forum-sync/index.ts +0 -9
- package/src/forum-sync/markdown.ts +0 -172
- package/src/forum-sync/sync-to-discord.ts +0 -595
- package/src/forum-sync/sync-to-files.ts +0 -294
- package/src/forum-sync/types.ts +0 -175
- package/src/forum-sync/watchers.ts +0 -474
- package/src/gateway-proxy-reconnect.e2e.test.ts +0 -553
- package/src/gateway-proxy.e2e.test.ts +0 -648
- package/src/genai-worker-wrapper.ts +0 -164
- package/src/genai-worker.ts +0 -385
- package/src/genai.ts +0 -318
- package/src/git-worktree-core.ts +0 -616
- package/src/guild-categories.test.ts +0 -260
- package/src/heap-monitor.ts +0 -152
- package/src/hrana-server.test.ts +0 -130
- package/src/hrana-server.ts +0 -399
- package/src/html-actions.test.ts +0 -87
- package/src/html-actions.ts +0 -174
- package/src/html-components.test.ts +0 -38
- package/src/html-components.ts +0 -177
- package/src/image-optimizer-plugin.ts +0 -194
- package/src/image-utils.ts +0 -150
- package/src/interaction-handler.ts +0 -775
- package/src/ipc-polling.ts +0 -325
- package/src/ipc-tools-plugin.ts +0 -306
- package/src/kimaki-digital-twin.e2e.test.ts +0 -199
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +0 -208
- package/src/kimaki-opencode-plugin.test.ts +0 -142
- package/src/kimaki-opencode-plugin.ts +0 -34
- package/src/kimaki-workspace-adaptor.ts +0 -144
- package/src/limit-heading-depth.test.ts +0 -116
- package/src/limit-heading-depth.ts +0 -26
- package/src/list-channel-threads.test.ts +0 -90
- package/src/list-channel-threads.ts +0 -252
- package/src/markdown.test.ts +0 -498
- package/src/markdown.ts +0 -577
- package/src/memory-overview-plugin.ts +0 -159
- package/src/message-formatting.test.ts +0 -1401
- package/src/message-formatting.ts +0 -999
- package/src/message-preprocessing.ts +0 -557
- package/src/oauth-rotation-shared.ts +0 -332
- package/src/onboarding-tutorial.ts +0 -167
- package/src/onboarding-welcome.ts +0 -49
- package/src/openai-auth-plugin.ts +0 -158
- package/src/openai-auth-state.ts +0 -297
- package/src/openai-realtime.ts +0 -358
- package/src/opencode-command-detection.test.ts +0 -307
- package/src/opencode-command-detection.ts +0 -76
- package/src/opencode-command.test.ts +0 -290
- package/src/opencode-command.ts +0 -251
- package/src/opencode-interrupt-plugin.test.ts +0 -419
- package/src/opencode-interrupt-plugin.ts +0 -306
- package/src/opencode.ts +0 -1590
- package/src/parse-permission-rules.test.ts +0 -127
- package/src/patch-text-parser.ts +0 -107
- package/src/plugin-logger.ts +0 -139
- package/src/plugin-opencode-client.ts +0 -71
- package/src/privacy-sanitizer.ts +0 -142
- package/src/project-add.test.ts +0 -80
- package/src/question-abort-clears-dropdown.e2e.test.ts +0 -109
- package/src/question-abort-resume.e2e.test.ts +0 -121
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +0 -172
- package/src/question-text-before-dropdown.e2e.test.ts +0 -67
- package/src/queue-advanced-abort.e2e.test.ts +0 -449
- package/src/queue-advanced-action-buttons.e2e.test.ts +0 -271
- package/src/queue-advanced-e2e-setup.ts +0 -1164
- package/src/queue-advanced-footer.e2e.test.ts +0 -357
- package/src/queue-advanced-model-switch.e2e.test.ts +0 -382
- package/src/queue-advanced-permissions-typing.e2e.test.ts +0 -256
- package/src/queue-advanced-question.e2e.test.ts +0 -322
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +0 -147
- package/src/queue-advanced-typing.e2e.test.ts +0 -199
- package/src/queue-delete-message.e2e.test.ts +0 -262
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +0 -152
- package/src/queue-interrupt-drain.e2e.test.ts +0 -299
- package/src/queue-question-select-drain.e2e.test.ts +0 -209
- package/src/runtime-idle-sweeper.ts +0 -76
- package/src/runtime-lifecycle.e2e.test.ts +0 -714
- package/src/schema.sql +0 -251
- package/src/sentry.ts +0 -26
- package/src/session-handler/agent-utils.ts +0 -104
- package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +0 -45
- package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +0 -40
- package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +0 -23
- package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +0 -22
- package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +0 -277
- package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +0 -46
- package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +0 -21
- package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +0 -56
- package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +0 -44
- package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +0 -29
- package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +0 -29
- package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +0 -50
- package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +0 -59
- package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +0 -52
- package/src/session-handler/event-stream-state.test.ts +0 -3431
- package/src/session-handler/event-stream-state.ts +0 -1792
- package/src/session-handler/global-event-listener.ts +0 -258
- package/src/session-handler/model-utils.test.ts +0 -537
- package/src/session-handler/model-utils.ts +0 -489
- package/src/session-handler/opencode-session-event-log.ts +0 -159
- package/src/session-handler/thread-ingress.test.ts +0 -54
- package/src/session-handler/thread-runtime-state.ts +0 -358
- package/src/session-handler/thread-session-runtime-summary.test.ts +0 -169
- package/src/session-handler/thread-session-runtime.ts +0 -5696
- package/src/session-handler.ts +0 -15
- package/src/session-search.test.ts +0 -331
- package/src/session-search.ts +0 -382
- package/src/session-sleep.test.ts +0 -294
- package/src/session-title-rename.test.ts +0 -186
- package/src/skill-filter.test.ts +0 -110
- package/src/skill-filter.ts +0 -63
- package/src/startup-service.ts +0 -200
- package/src/startup-time.e2e.test.ts +0 -377
- package/src/system-message.test.ts +0 -1013
- package/src/system-message.ts +0 -1043
- package/src/task-id-plugin.ts +0 -17
- package/src/task-runner.ts +0 -768
- package/src/task-schedule.test.ts +0 -424
- package/src/task-schedule.ts +0 -470
- package/src/test-utils.ts +0 -549
- package/src/thinking-utils.test.ts +0 -46
- package/src/thinking-utils.ts +0 -82
- package/src/thread-message-queue.e2e.test.ts +0 -1468
- package/src/tools.ts +0 -430
- package/src/undici.d.ts +0 -12
- package/src/undo-redo.e2e.test.ts +0 -219
- package/src/unnest-code-blocks.test.ts +0 -713
- package/src/unnest-code-blocks.ts +0 -185
- package/src/upgrade.ts +0 -127
- package/src/utils.ts +0 -216
- package/src/voice-attachment.ts +0 -79
- package/src/voice-handler.ts +0 -1000
- package/src/voice-message.e2e.test.ts +0 -1513
- package/src/voice-routing-input.e2e.test.ts +0 -203
- package/src/voice-routing-question.e2e.test.ts +0 -188
- package/src/voice.test.ts +0 -565
- package/src/wait-session.ts +0 -291
- package/src/websockify.ts +0 -101
- package/src/worker-types.ts +0 -64
- package/src/worktree-clone-isolation.e2e.test.ts +0 -213
- package/src/worktree-lifecycle.e2e.test.ts +0 -900
- package/src/worktree-utils.ts +0 -4
- package/src/xai-auth-plugin.ts +0 -303
- package/src/xai-auth-state.test.ts +0 -224
- package/src/xai-auth-state.ts +0 -294
- package/src/xml.test.ts +0 -38
- package/src/xml.ts +0 -121
package/src/cli-runner.ts
DELETED
|
@@ -1,2157 +0,0 @@
|
|
|
1
|
-
// Runtime startup and shared helpers for the Kimaki goke CLI.
|
|
2
|
-
// Keeps cli.ts focused on command composition while preserving the bot onboarding flow.
|
|
3
|
-
import {
|
|
4
|
-
intro,
|
|
5
|
-
outro,
|
|
6
|
-
text,
|
|
7
|
-
password,
|
|
8
|
-
note,
|
|
9
|
-
cancel,
|
|
10
|
-
isCancel,
|
|
11
|
-
confirm,
|
|
12
|
-
log,
|
|
13
|
-
multiselect,
|
|
14
|
-
select,
|
|
15
|
-
spinner,
|
|
16
|
-
} from '@clack/prompts'
|
|
17
|
-
import {
|
|
18
|
-
deduplicateByKey,
|
|
19
|
-
generateBotInstallUrl,
|
|
20
|
-
generateDiscordInstallUrlForBot,
|
|
21
|
-
KIMAKI_GATEWAY_APP_ID,
|
|
22
|
-
KIMAKI_WEBSITE_URL,
|
|
23
|
-
abbreviatePath,
|
|
24
|
-
} from './utils.js'
|
|
25
|
-
import {
|
|
26
|
-
getChannelsWithDescriptions,
|
|
27
|
-
createDiscordClient,
|
|
28
|
-
initDatabase,
|
|
29
|
-
getChannelDirectory,
|
|
30
|
-
startDiscordBot,
|
|
31
|
-
registerBotLifecycleHandlers,
|
|
32
|
-
initializeOpencodeForDirectory,
|
|
33
|
-
assertCompatibleOpencodeVersion,
|
|
34
|
-
createProjectChannels,
|
|
35
|
-
createDefaultKimakiChannel,
|
|
36
|
-
type ChannelWithTags,
|
|
37
|
-
} from './discord-bot.js'
|
|
38
|
-
import {
|
|
39
|
-
getBotTokenWithMode,
|
|
40
|
-
ensureServiceAuthToken,
|
|
41
|
-
setBotToken,
|
|
42
|
-
setBotMode,
|
|
43
|
-
setChannelDirectory,
|
|
44
|
-
findChannelsByDirectory,
|
|
45
|
-
getDb,
|
|
46
|
-
} from './database.js'
|
|
47
|
-
import * as orm from 'drizzle-orm'
|
|
48
|
-
import * as dbSchema from './schema.js'
|
|
49
|
-
import { selectResolvedCommand } from './opencode-command.js'
|
|
50
|
-
import {
|
|
51
|
-
Events,
|
|
52
|
-
ChannelType,
|
|
53
|
-
type Guild,
|
|
54
|
-
type REST,
|
|
55
|
-
Routes,
|
|
56
|
-
AttachmentBuilder,
|
|
57
|
-
} from 'discord.js'
|
|
58
|
-
import { discordApiUrl, getDiscordRestApiUrl, getGatewayProxyRestBaseUrl, getInternetReachableBaseUrl } from './discord-urls.js'
|
|
59
|
-
import { extractBtwQueueSuffix } from './btw-prefix-detection.js'
|
|
60
|
-
import crypto from 'node:crypto'
|
|
61
|
-
import path from 'node:path'
|
|
62
|
-
import fs from 'node:fs'
|
|
63
|
-
import { spawn } from 'node:child_process'
|
|
64
|
-
import { createLogger, LogPrefix } from './logger.js'
|
|
65
|
-
import { notifyError } from './sentry.js'
|
|
66
|
-
import {
|
|
67
|
-
uploadFilesToDiscord,
|
|
68
|
-
stripMentions,
|
|
69
|
-
isThreadChannelType,
|
|
70
|
-
} from './discord-utils.js'
|
|
71
|
-
import { setDataDir, getDataDir } from './config.js'
|
|
72
|
-
import { execAsync } from './worktrees.js'
|
|
73
|
-
import { backgroundUpgradeKimaki } from './upgrade.js'
|
|
74
|
-
import { initAnalytics, setAnalyticsBotMode } from './analytics.js'
|
|
75
|
-
import { sendWelcomeMessage } from './onboarding-welcome.js'
|
|
76
|
-
import { startHranaServer } from './hrana-server.js'
|
|
77
|
-
import { startIpcPolling, stopIpcPolling } from './ipc-polling.js'
|
|
78
|
-
import { type ParsedSendAt } from './task-schedule.js'
|
|
79
|
-
import { store } from './store.js'
|
|
80
|
-
import { registerCommands, SKIP_USER_COMMANDS } from './discord-command-registration.js'
|
|
81
|
-
|
|
82
|
-
export const cliLogger = createLogger(LogPrefix.CLI)
|
|
83
|
-
|
|
84
|
-
// Gateway bot mode constants.
|
|
85
|
-
// KIMAKI_GATEWAY_APP_ID is the Discord Application ID of the gateway bot.
|
|
86
|
-
// KIMAKI_WEBSITE_URL is the website that handles OAuth callback + onboarding status.
|
|
87
|
-
// KIMAKI_GATEWAY_PROXY_URL is the gateway-proxy base URL.
|
|
88
|
-
// We derive REST base from this URL by swapping ws/wss to http/https.
|
|
89
|
-
// These are hardcoded because they're deploy-time constants for the gateway infrastructure.
|
|
90
|
-
export const KIMAKI_GATEWAY_PROXY_URL =
|
|
91
|
-
process.env.KIMAKI_GATEWAY_PROXY_URL ||
|
|
92
|
-
'wss://discord-gateway.kimaki.dev'
|
|
93
|
-
|
|
94
|
-
export const KIMAKI_GATEWAY_PROXY_REST_BASE_URL = getGatewayProxyRestBaseUrl({
|
|
95
|
-
gatewayUrl: KIMAKI_GATEWAY_PROXY_URL,
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
export type OpenUrlCommand = {
|
|
99
|
-
command: string
|
|
100
|
-
args: string[]
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function getOpenUrlCommand(
|
|
104
|
-
url: string,
|
|
105
|
-
platform: NodeJS.Platform = process.platform,
|
|
106
|
-
): OpenUrlCommand {
|
|
107
|
-
if (platform === 'darwin') {
|
|
108
|
-
return { command: 'open', args: [url] }
|
|
109
|
-
}
|
|
110
|
-
if (platform === 'win32') {
|
|
111
|
-
return { command: 'rundll32.exe', args: ['url.dll,FileProtocolHandler', url] }
|
|
112
|
-
}
|
|
113
|
-
return { command: 'xdg-open', args: [url] }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function openUrlInDefaultBrowser(url: string): void {
|
|
117
|
-
const { command, args } = getOpenUrlCommand(url)
|
|
118
|
-
const child = spawn(command, args, {
|
|
119
|
-
detached: true,
|
|
120
|
-
stdio: 'ignore',
|
|
121
|
-
windowsHide: true,
|
|
122
|
-
})
|
|
123
|
-
child.on('error', (error) => {
|
|
124
|
-
cliLogger.warn(
|
|
125
|
-
'Failed to open install URL:',
|
|
126
|
-
error instanceof Error ? error.message : String(error),
|
|
127
|
-
)
|
|
128
|
-
})
|
|
129
|
-
child.unref()
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Strip bracketed paste escape sequences from terminal input.
|
|
133
|
-
// iTerm2 and other terminals wrap pasted content with \x1b[200~ and \x1b[201~
|
|
134
|
-
// which can cause validation to fail on macOS. See: https://github.com/remorses/kimaki/issues/18
|
|
135
|
-
export function stripBracketedPaste(value: string | undefined): string {
|
|
136
|
-
if (!value) {
|
|
137
|
-
return ''
|
|
138
|
-
}
|
|
139
|
-
return value
|
|
140
|
-
.replace(/\x1b\[200~/g, '')
|
|
141
|
-
.replace(/\x1b\[201~/g, '')
|
|
142
|
-
.trim()
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// Derive the Discord Application ID from a bot token.
|
|
147
|
-
// Discord bot tokens have the format: base64(userId).timestamp.hmac
|
|
148
|
-
// The first segment is the bot's user ID (= Application ID) base64-encoded.
|
|
149
|
-
// For gateway mode tokens (client_id:secret format), this function returns
|
|
150
|
-
// undefined -- the caller should use KIMAKI_GATEWAY_APP_ID instead.
|
|
151
|
-
export function appIdFromToken(token: string): string | undefined {
|
|
152
|
-
// Gateway mode tokens use "client_id:secret" format, not base64.
|
|
153
|
-
if (token.includes(':')) {
|
|
154
|
-
return undefined
|
|
155
|
-
}
|
|
156
|
-
const segment = token.split('.')[0]
|
|
157
|
-
if (!segment) {
|
|
158
|
-
return undefined
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
const decoded = Buffer.from(segment, 'base64').toString('utf8')
|
|
162
|
-
if (/^\d{17,20}$/.test(decoded)) {
|
|
163
|
-
return decoded
|
|
164
|
-
}
|
|
165
|
-
return undefined
|
|
166
|
-
} catch {
|
|
167
|
-
return undefined
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Resolve bot token and app ID from env var or database.
|
|
172
|
-
// Used by CLI subcommands (send, project add) that need credentials
|
|
173
|
-
// but don't run the interactive wizard.
|
|
174
|
-
// In gateway mode, also sets store.discordBaseUrl so REST calls
|
|
175
|
-
// are routed through the gateway-proxy REST endpoint.
|
|
176
|
-
//
|
|
177
|
-
// Priority: KIMAKI_BOT_TOKEN env var takes precedence over saved DB
|
|
178
|
-
// credentials. This lets CI and cross-instance commands override the
|
|
179
|
-
// local bot identity. If the env token looks like a gateway credential
|
|
180
|
-
// (clientId:clientSecret), the gateway proxy REST URL is set automatically.
|
|
181
|
-
export async function resolveBotCredentials({ appIdOverride }: { appIdOverride?: string } = {}): Promise<{
|
|
182
|
-
token: string
|
|
183
|
-
appId: string | undefined
|
|
184
|
-
}> {
|
|
185
|
-
const envToken = process.env.KIMAKI_BOT_TOKEN
|
|
186
|
-
if (envToken) {
|
|
187
|
-
const isGatewayToken = envToken.includes(':')
|
|
188
|
-
if (isGatewayToken) {
|
|
189
|
-
// Gateway tokens need REST calls routed through the proxy, not discord.com
|
|
190
|
-
store.setState({ discordBaseUrl: KIMAKI_GATEWAY_PROXY_REST_BASE_URL })
|
|
191
|
-
}
|
|
192
|
-
const appId = appIdOverride || appIdFromToken(envToken)
|
|
193
|
-
return { token: envToken, appId }
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Fall back to saved credentials in the database
|
|
197
|
-
const botRow = await getBotTokenWithMode().catch((e) => {
|
|
198
|
-
cliLogger.error('Database error:', e instanceof Error ? e.message : String(e))
|
|
199
|
-
return null
|
|
200
|
-
})
|
|
201
|
-
if (botRow) {
|
|
202
|
-
return { token: botRow.token, appId: appIdOverride || botRow.appId }
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
cliLogger.error('No bot token found. Set KIMAKI_BOT_TOKEN env var or run `kimaki` first to set up.')
|
|
206
|
-
process.exit(EXIT_NO_RESTART)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export { isThreadChannelType }
|
|
210
|
-
|
|
211
|
-
/** Wrap long lines so prompt.md is readable in Discord's attachment preview. */
|
|
212
|
-
function wrapPromptAttachmentText(prompt: string): string {
|
|
213
|
-
return prompt
|
|
214
|
-
.split('\n')
|
|
215
|
-
.flatMap((line) => {
|
|
216
|
-
if (line.length <= 120) {
|
|
217
|
-
return [line]
|
|
218
|
-
}
|
|
219
|
-
const wrapped: string[] = []
|
|
220
|
-
let remaining = line
|
|
221
|
-
const maxCol = 120
|
|
222
|
-
// Only soft-break at a space if it's reasonably close to maxCol,
|
|
223
|
-
// otherwise hard-break to avoid tiny fragments from early spaces
|
|
224
|
-
const minSoftBreak = 90
|
|
225
|
-
while (remaining.length > maxCol) {
|
|
226
|
-
const lastSpace = remaining.lastIndexOf(' ', maxCol)
|
|
227
|
-
const useSoftBreak = lastSpace >= minSoftBreak
|
|
228
|
-
const breakAt = useSoftBreak ? lastSpace : maxCol
|
|
229
|
-
wrapped.push(remaining.slice(0, breakAt))
|
|
230
|
-
// Only consume the separator space on soft breaks
|
|
231
|
-
remaining = useSoftBreak
|
|
232
|
-
? remaining.slice(breakAt + 1)
|
|
233
|
-
: remaining.slice(breakAt)
|
|
234
|
-
}
|
|
235
|
-
if (remaining.length > 0) {
|
|
236
|
-
wrapped.push(remaining)
|
|
237
|
-
}
|
|
238
|
-
return wrapped
|
|
239
|
-
})
|
|
240
|
-
.join('\n')
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Visible message content + prompt.md text for prompts over Discord's limit.
|
|
245
|
-
* The bot reads queue/btw suffixes only from message content, so the suffix
|
|
246
|
-
* moves from the file to the visible message and the send still queues or forks.
|
|
247
|
-
*/
|
|
248
|
-
export function buildLongPromptMessage(prompt: string): {
|
|
249
|
-
content: string
|
|
250
|
-
fileText: string
|
|
251
|
-
} {
|
|
252
|
-
const { prompt: fileText, forceQueue, forceBtw } = extractBtwQueueSuffix(prompt)
|
|
253
|
-
const preview = fileText.slice(0, 100).replace(/\n/g, ' ')
|
|
254
|
-
const summary = `Prompt attached as file (${fileText.length} chars)\n\n> ${preview}…`
|
|
255
|
-
const suffix = [forceBtw && 'btw', forceQueue && 'queue'].filter(Boolean).join(' ')
|
|
256
|
-
return { content: suffix ? `${summary}\n\n${suffix}` : summary, fileText }
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function promptAttachmentBlob(text: string) {
|
|
260
|
-
return new Blob([new Uint8Array(Buffer.from(text, 'utf8'))], {
|
|
261
|
-
type: 'text/markdown',
|
|
262
|
-
})
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export async function sendDiscordMessageWithOptionalAttachment({
|
|
266
|
-
channelId,
|
|
267
|
-
prompt,
|
|
268
|
-
botToken,
|
|
269
|
-
embeds,
|
|
270
|
-
rest,
|
|
271
|
-
splitInsteadOfAttach,
|
|
272
|
-
files,
|
|
273
|
-
}: {
|
|
274
|
-
channelId: string
|
|
275
|
-
prompt: string
|
|
276
|
-
botToken: string
|
|
277
|
-
embeds?: Array<{ color: number; footer: { text: string } }>
|
|
278
|
-
/** Only `post` is used (short prompt / split paths). Accept a narrow shape so tests need no cast. */
|
|
279
|
-
rest: Pick<REST, 'post'>
|
|
280
|
-
/** When true, long messages are split into multiple Discord messages instead of
|
|
281
|
-
* being attached as a file. Useful for notify-only messages where the content
|
|
282
|
-
* should be directly visible in the channel. */
|
|
283
|
-
splitInsteadOfAttach?: boolean
|
|
284
|
-
/** Local file paths to attach to the message (images, text files, etc.). */
|
|
285
|
-
files?: string[]
|
|
286
|
-
}): Promise<{ id: string }> {
|
|
287
|
-
const discordMaxLength = 2000
|
|
288
|
-
|
|
289
|
-
// When files are provided, always use multipart FormData upload
|
|
290
|
-
if (files?.length) {
|
|
291
|
-
const { DISCORD_DEFAULT_MAX_FILE_SIZE } = await import('./discord-utils.js')
|
|
292
|
-
const { default: mime } = await import('mime')
|
|
293
|
-
|
|
294
|
-
for (const file of files) {
|
|
295
|
-
const stat = fs.statSync(file)
|
|
296
|
-
if (stat.size > DISCORD_DEFAULT_MAX_FILE_SIZE) {
|
|
297
|
-
const fileMB = (stat.size / 1024 / 1024).toFixed(1)
|
|
298
|
-
const limitMB = (DISCORD_DEFAULT_MAX_FILE_SIZE / 1024 / 1024).toFixed(0)
|
|
299
|
-
throw new Error(
|
|
300
|
-
`File "${path.basename(file)}" is ${fileMB} MB, which exceeds Discord's ${limitMB} MB upload limit`,
|
|
301
|
-
)
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// When prompt exceeds Discord's limit, attach it as prompt.md alongside
|
|
306
|
-
// user files so nothing is silently lost. Build prompt.md from memory so
|
|
307
|
-
// parallel kimaki send processes never share or unlink a temp path.
|
|
308
|
-
const longPrompt = prompt.length > discordMaxLength
|
|
309
|
-
? buildLongPromptMessage(prompt)
|
|
310
|
-
: undefined
|
|
311
|
-
const content = longPrompt ? longPrompt.content : prompt
|
|
312
|
-
|
|
313
|
-
const allFiles: Array<{ data: Uint8Array; filename: string; mimeType: string }> =
|
|
314
|
-
files.map((file) => ({
|
|
315
|
-
data: new Uint8Array(fs.readFileSync(file)),
|
|
316
|
-
filename: path.basename(file),
|
|
317
|
-
mimeType: mime.getType(file) || 'application/octet-stream',
|
|
318
|
-
}))
|
|
319
|
-
|
|
320
|
-
if (longPrompt) {
|
|
321
|
-
allFiles.push({
|
|
322
|
-
data: new Uint8Array(Buffer.from(longPrompt.fileText, 'utf8')),
|
|
323
|
-
filename: 'prompt.md',
|
|
324
|
-
mimeType: 'text/markdown',
|
|
325
|
-
})
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const attachments = allFiles.map((f, index) => ({
|
|
329
|
-
id: index,
|
|
330
|
-
filename: f.filename,
|
|
331
|
-
}))
|
|
332
|
-
|
|
333
|
-
const formData = new FormData()
|
|
334
|
-
formData.append(
|
|
335
|
-
'payload_json',
|
|
336
|
-
JSON.stringify({
|
|
337
|
-
content,
|
|
338
|
-
attachments,
|
|
339
|
-
embeds,
|
|
340
|
-
allowed_mentions: { parse: store.getState().allowedMentions },
|
|
341
|
-
}),
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
for (const [index, f] of allFiles.entries()) {
|
|
345
|
-
formData.append(
|
|
346
|
-
`files[${index}]`,
|
|
347
|
-
new Blob([f.data], { type: f.mimeType }),
|
|
348
|
-
f.filename,
|
|
349
|
-
)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const response = await fetch(
|
|
353
|
-
discordApiUrl(`/channels/${channelId}/messages`),
|
|
354
|
-
{
|
|
355
|
-
method: 'POST',
|
|
356
|
-
headers: { Authorization: `Bot ${botToken}` },
|
|
357
|
-
body: formData,
|
|
358
|
-
},
|
|
359
|
-
)
|
|
360
|
-
if (!response.ok) {
|
|
361
|
-
const error = await response.text()
|
|
362
|
-
throw new Error(`Discord API error: ${response.status} - ${error}`)
|
|
363
|
-
}
|
|
364
|
-
return (await response.json()) as { id: string }
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (prompt.length <= discordMaxLength) {
|
|
368
|
-
return (await rest.post(Routes.channelMessages(channelId), {
|
|
369
|
-
body: {
|
|
370
|
-
content: prompt,
|
|
371
|
-
embeds,
|
|
372
|
-
allowed_mentions: { parse: store.getState().allowedMentions },
|
|
373
|
-
},
|
|
374
|
-
})) as { id: string }
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (splitInsteadOfAttach) {
|
|
378
|
-
const { splitMarkdownForDiscord } = await import('./discord-utils.js')
|
|
379
|
-
const chunks = splitMarkdownForDiscord({
|
|
380
|
-
content: prompt,
|
|
381
|
-
maxLength: discordMaxLength,
|
|
382
|
-
})
|
|
383
|
-
let firstMessage: { id: string } | undefined
|
|
384
|
-
for (let chunk of chunks) {
|
|
385
|
-
if (!chunk?.trim()) continue
|
|
386
|
-
// Safety net: hard-truncate if splitting still produced an oversized chunk
|
|
387
|
-
if (chunk.length > discordMaxLength) {
|
|
388
|
-
chunk = chunk.slice(0, discordMaxLength - 4) + '...'
|
|
389
|
-
}
|
|
390
|
-
const message = (await rest.post(Routes.channelMessages(channelId), {
|
|
391
|
-
body: {
|
|
392
|
-
content: chunk,
|
|
393
|
-
// Only attach embeds to the first message
|
|
394
|
-
...(firstMessage ? {} : { embeds }),
|
|
395
|
-
allowed_mentions: { parse: store.getState().allowedMentions },
|
|
396
|
-
},
|
|
397
|
-
})) as { id: string }
|
|
398
|
-
if (!firstMessage) firstMessage = message
|
|
399
|
-
}
|
|
400
|
-
return firstMessage!
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const longPrompt = buildLongPromptMessage(prompt)
|
|
404
|
-
// In-memory Blob only — no temp file. Parallel send must never share a path.
|
|
405
|
-
const formData = new FormData()
|
|
406
|
-
formData.append(
|
|
407
|
-
'payload_json',
|
|
408
|
-
JSON.stringify({
|
|
409
|
-
content: longPrompt.content,
|
|
410
|
-
attachments: [{ id: 0, filename: 'prompt.md' }],
|
|
411
|
-
embeds,
|
|
412
|
-
allowed_mentions: { parse: store.getState().allowedMentions },
|
|
413
|
-
}),
|
|
414
|
-
)
|
|
415
|
-
formData.append(
|
|
416
|
-
'files[0]',
|
|
417
|
-
promptAttachmentBlob(wrapPromptAttachmentText(longPrompt.fileText)),
|
|
418
|
-
'prompt.md',
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
const starterMessageResponse = await fetch(
|
|
422
|
-
discordApiUrl(`/channels/${channelId}/messages`),
|
|
423
|
-
{
|
|
424
|
-
method: 'POST',
|
|
425
|
-
headers: {
|
|
426
|
-
Authorization: `Bot ${botToken}`,
|
|
427
|
-
},
|
|
428
|
-
body: formData,
|
|
429
|
-
},
|
|
430
|
-
)
|
|
431
|
-
|
|
432
|
-
if (!starterMessageResponse.ok) {
|
|
433
|
-
const error = await starterMessageResponse.text()
|
|
434
|
-
throw new Error(
|
|
435
|
-
`Discord API error: ${starterMessageResponse.status} - ${error}`,
|
|
436
|
-
)
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return (await starterMessageResponse.json()) as { id: string }
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
export function formatRelativeTime(target: Date): string {
|
|
443
|
-
const diffMs = target.getTime() - Date.now()
|
|
444
|
-
if (diffMs <= 0) {
|
|
445
|
-
return 'due now'
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const totalSeconds = Math.floor(diffMs / 1000)
|
|
449
|
-
if (totalSeconds < 60) {
|
|
450
|
-
return `${totalSeconds}s`
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const totalMinutes = Math.floor(totalSeconds / 60)
|
|
454
|
-
if (totalMinutes < 60) {
|
|
455
|
-
return `${totalMinutes}m`
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const hours = Math.floor(totalMinutes / 60)
|
|
459
|
-
const minutes = totalMinutes % 60
|
|
460
|
-
if (hours < 24) {
|
|
461
|
-
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const days = Math.floor(hours / 24)
|
|
465
|
-
const remainingHours = hours % 24
|
|
466
|
-
return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
export function formatTaskScheduleLine(schedule: ParsedSendAt): string {
|
|
470
|
-
if (schedule.scheduleKind === 'at') {
|
|
471
|
-
return `one-time at ${schedule.runAt.toISOString()}`
|
|
472
|
-
}
|
|
473
|
-
return `cron "${schedule.cronExpr}" (${schedule.timezone}) next ${schedule.nextRunAt.toISOString()}`
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
export const EXIT_NO_RESTART = 64
|
|
477
|
-
// Temporary failure (sysexits EX_TEMPFAIL). Wrapper retries forever with
|
|
478
|
-
// backoff and does not count this toward the crash-loop detector. Used when
|
|
479
|
-
// Discord login fails because the network is down.
|
|
480
|
-
export const EXIT_TEMPFAIL = 75
|
|
481
|
-
|
|
482
|
-
export type GuildMemberSearchResult = {
|
|
483
|
-
user: { id: string; username: string; global_name?: string }
|
|
484
|
-
nick?: string
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
export type DiscordUserTarget = {
|
|
488
|
-
id: string
|
|
489
|
-
username?: string
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
export function isGuildMemberSearchResult(value: object | null): value is GuildMemberSearchResult {
|
|
493
|
-
if (!value) {
|
|
494
|
-
return false
|
|
495
|
-
}
|
|
496
|
-
const user = Reflect.get(value, 'user')
|
|
497
|
-
return (
|
|
498
|
-
typeof user === 'object' &&
|
|
499
|
-
user !== null &&
|
|
500
|
-
typeof Reflect.get(user, 'id') === 'string' &&
|
|
501
|
-
typeof Reflect.get(user, 'username') === 'string'
|
|
502
|
-
)
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
export function getDiscordUserIdFromUserOption(user: string): string | null {
|
|
506
|
-
const trimmed = user.trim()
|
|
507
|
-
const mentionMatch = trimmed.match(/^<@!?(\d{15,25})>$/)
|
|
508
|
-
if (mentionMatch?.[1]) {
|
|
509
|
-
return mentionMatch[1]
|
|
510
|
-
}
|
|
511
|
-
if (/^\d{15,25}$/.test(trimmed)) {
|
|
512
|
-
return trimmed
|
|
513
|
-
}
|
|
514
|
-
return null
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function readErrorField(error: object | null, key: string): unknown {
|
|
518
|
-
if (!error) {
|
|
519
|
-
return undefined
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
const directValue = Reflect.get(error, key)
|
|
523
|
-
if (directValue !== undefined) {
|
|
524
|
-
return directValue
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
const rawError = Reflect.get(error, 'rawError')
|
|
528
|
-
if (typeof rawError === 'object' && rawError !== null) {
|
|
529
|
-
const rawValue = Reflect.get(rawError, key)
|
|
530
|
-
if (rawValue !== undefined) {
|
|
531
|
-
return rawValue
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const cause = Reflect.get(error, 'cause')
|
|
536
|
-
if (typeof cause === 'object' && cause !== null) {
|
|
537
|
-
return readErrorField(cause, key)
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
return undefined
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/** Transient network errors that may resolve on retry (DNS down, gateway unreachable, TLS blips). */
|
|
544
|
-
const TRANSIENT_ERROR_CODES = new Set([
|
|
545
|
-
'ENOTFOUND',
|
|
546
|
-
'ECONNREFUSED',
|
|
547
|
-
'ETIMEDOUT',
|
|
548
|
-
'ECONNRESET',
|
|
549
|
-
'EAI_AGAIN',
|
|
550
|
-
'EPIPE',
|
|
551
|
-
'EHOSTUNREACH',
|
|
552
|
-
'ENETUNREACH',
|
|
553
|
-
// TLS/cert errors can be transient (intermediate CA blip, MITM proxy, cert rotation).
|
|
554
|
-
// Without these, Discord login failure exits EXIT_NO_RESTART and the bin wrapper
|
|
555
|
-
// never restarts — the bot dies permanently on "unable to verify the first certificate".
|
|
556
|
-
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
|
557
|
-
'UNABLE_TO_GET_ISSUER_CERT',
|
|
558
|
-
'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
|
|
559
|
-
'UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY',
|
|
560
|
-
'CERT_SIGNATURE_FAILURE',
|
|
561
|
-
'CERT_NOT_YET_VALID',
|
|
562
|
-
'CERT_HAS_EXPIRED',
|
|
563
|
-
'CRL_HAS_EXPIRED',
|
|
564
|
-
'DEPTH_ZERO_SELF_SIGNED_CERT',
|
|
565
|
-
'SELF_SIGNED_CERT_IN_CHAIN',
|
|
566
|
-
'ERR_TLS_CERT_ALTNAME_INVALID',
|
|
567
|
-
// undici fetch/WebSocket connect failures when the network is down.
|
|
568
|
-
// Without these, Discord login exits EXIT_NO_RESTART and the wrapper never
|
|
569
|
-
// comes back after a connect timeout to discord-gateway.kimaki.dev.
|
|
570
|
-
'UND_ERR_CONNECT_TIMEOUT',
|
|
571
|
-
'UND_ERR_HEADERS_TIMEOUT',
|
|
572
|
-
'UND_ERR_BODY_TIMEOUT',
|
|
573
|
-
'UND_ERR_SOCKET',
|
|
574
|
-
])
|
|
575
|
-
|
|
576
|
-
const TRANSIENT_ERROR_NAMES = new Set([
|
|
577
|
-
'ConnectTimeoutError',
|
|
578
|
-
'HeadersTimeoutError',
|
|
579
|
-
'BodyTimeoutError',
|
|
580
|
-
'SocketError',
|
|
581
|
-
])
|
|
582
|
-
|
|
583
|
-
const TRANSIENT_ERROR_MESSAGE_PATTERNS = [
|
|
584
|
-
/unable to verify the first certificate/i,
|
|
585
|
-
/certificate has expired/i,
|
|
586
|
-
/self[- ]signed certificate/i,
|
|
587
|
-
/unable to get local issuer certificate/i,
|
|
588
|
-
/connect timeout error/i,
|
|
589
|
-
/headers timeout error/i,
|
|
590
|
-
/body timeout error/i,
|
|
591
|
-
]
|
|
592
|
-
|
|
593
|
-
export function isTransientNetworkError(error: unknown): boolean {
|
|
594
|
-
if (!(error instanceof Error)) return false
|
|
595
|
-
const code = (error as NodeJS.ErrnoException).code
|
|
596
|
-
if (code && TRANSIENT_ERROR_CODES.has(code)) return true
|
|
597
|
-
if (TRANSIENT_ERROR_NAMES.has(error.name)) return true
|
|
598
|
-
// Fallback when code is stripped by wrappers (discord.js sometimes rethrows by message only).
|
|
599
|
-
if (
|
|
600
|
-
TRANSIENT_ERROR_MESSAGE_PATTERNS.some((pattern) =>
|
|
601
|
-
pattern.test(error.message),
|
|
602
|
-
)
|
|
603
|
-
) {
|
|
604
|
-
return true
|
|
605
|
-
}
|
|
606
|
-
// discord.js wraps errors in cause chains
|
|
607
|
-
if (error.cause instanceof Error) return isTransientNetworkError(error.cause)
|
|
608
|
-
return false
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
export function isDiscordMemberLookupUnavailable(error: Error): boolean {
|
|
612
|
-
const status = readErrorField(error, 'status')
|
|
613
|
-
if (status === 403) {
|
|
614
|
-
return true
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const code = readErrorField(error, 'code')
|
|
618
|
-
if (code === 50001 || code === 50013) {
|
|
619
|
-
return true
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const message = String(readErrorField(error, 'message') || '').toLowerCase()
|
|
623
|
-
return (
|
|
624
|
-
(message.includes('missing access') ||
|
|
625
|
-
message.includes('missing permissions') ||
|
|
626
|
-
message.includes('intent')) &&
|
|
627
|
-
message.includes('member')
|
|
628
|
-
)
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
export function formatMemberLookupUnavailableMessage(): string {
|
|
632
|
-
return [
|
|
633
|
-
'Discord member search is unavailable for this bot.',
|
|
634
|
-
'Most Kimaki features still work. Searching names with `--user` needs Server Members Intent.',
|
|
635
|
-
'Use a Discord user ID or raw mention with the same `--user` flag instead:',
|
|
636
|
-
` kimaki send --channel <channelId> --prompt '...' --user 535922349652836367`,
|
|
637
|
-
` kimaki send --channel <channelId> --prompt '...' --user '<@535922349652836367>'`,
|
|
638
|
-
].join('\n')
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
export async function resolveDiscordUserOption({
|
|
642
|
-
user,
|
|
643
|
-
guildId,
|
|
644
|
-
rest,
|
|
645
|
-
}: {
|
|
646
|
-
user: string | undefined
|
|
647
|
-
guildId: string
|
|
648
|
-
rest: REST
|
|
649
|
-
}): Promise<Error | DiscordUserTarget | undefined> {
|
|
650
|
-
if (!user) {
|
|
651
|
-
return undefined
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
const directUserId = getDiscordUserIdFromUserOption(user)
|
|
655
|
-
if (directUserId) {
|
|
656
|
-
cliLogger.log(`Using Discord user ID: ${directUserId}`)
|
|
657
|
-
return { id: directUserId }
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
cliLogger.log(`Searching for user "${user}" in guild...`)
|
|
661
|
-
const searchResult = await rest
|
|
662
|
-
.get(Routes.guildMembersSearch(guildId), {
|
|
663
|
-
query: new URLSearchParams({ query: user, limit: '10' }),
|
|
664
|
-
})
|
|
665
|
-
.catch((error) => new Error('Discord member search failed', { cause: error }))
|
|
666
|
-
|
|
667
|
-
if (searchResult instanceof Error) {
|
|
668
|
-
if (isDiscordMemberLookupUnavailable(searchResult)) {
|
|
669
|
-
return new Error(formatMemberLookupUnavailableMessage())
|
|
670
|
-
}
|
|
671
|
-
return searchResult
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const searchResults = Array.isArray(searchResult)
|
|
675
|
-
? searchResult.filter(isGuildMemberSearchResult)
|
|
676
|
-
: []
|
|
677
|
-
const exactMatch = searchResults.find((member) => {
|
|
678
|
-
const displayName = member.nick || member.user.global_name || member.user.username
|
|
679
|
-
return (
|
|
680
|
-
displayName.toLowerCase() === user.toLowerCase() ||
|
|
681
|
-
member.user.username.toLowerCase() === user.toLowerCase()
|
|
682
|
-
)
|
|
683
|
-
})
|
|
684
|
-
const member = exactMatch || searchResults[0]
|
|
685
|
-
if (!member) {
|
|
686
|
-
return new Error(`User "${user}" not found in guild`)
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
const username = member.nick || member.user.global_name || member.user.username
|
|
690
|
-
cliLogger.log(`Found user: ${username} (${member.user.id})`)
|
|
691
|
-
return { id: member.user.id, username }
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
export function canUseInteractivePrompts(): boolean {
|
|
695
|
-
return Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
export function exitNonInteractiveSetup(): never {
|
|
699
|
-
cliLogger.error(
|
|
700
|
-
'Setup requires an interactive terminal (TTY) for prompts. Run `kimaki` in an interactive shell to complete setup.',
|
|
701
|
-
)
|
|
702
|
-
process.exit(EXIT_NO_RESTART)
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// All structured events emitted on stdout in non-TTY mode (cloud sandboxes, CI).
|
|
706
|
-
// Consumers parse these with the eventsource-parser npm package.
|
|
707
|
-
export type ProgrammaticEvent =
|
|
708
|
-
| { type: 'install_url'; url: string }
|
|
709
|
-
| { type: 'authorized'; guild_id: string }
|
|
710
|
-
| { type: 'ready'; app_id: string; guild_ids: string[] }
|
|
711
|
-
| { type: 'error'; message: string; install_url?: string }
|
|
712
|
-
|
|
713
|
-
// Emit a structured JSON line on stdout for non-TTY consumers (cloud sandboxes, CI).
|
|
714
|
-
// Each line is a self-contained JSON object with a "type" field for easy parsing.
|
|
715
|
-
// Lines are prefixed with "data: " and terminated with "\n\n" (SSE format) so consumers
|
|
716
|
-
// can use the eventsource-parser npm package to robustly extract JSON events from noisy
|
|
717
|
-
// process output (other log lines, warnings, etc. are ignored by the parser).
|
|
718
|
-
export function emitJsonEvent(event: ProgrammaticEvent): void {
|
|
719
|
-
process.stdout.write(`data: ${JSON.stringify(event)}\n\n`)
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
export async function resolveGatewayInstallCredentials(): Promise<
|
|
723
|
-
Error | { clientId: string; clientSecret: string; createdNow: boolean }
|
|
724
|
-
> {
|
|
725
|
-
if (!KIMAKI_GATEWAY_APP_ID) {
|
|
726
|
-
return new Error(
|
|
727
|
-
'Gateway mode is not available yet. KIMAKI_GATEWAY_APP_ID is not configured.',
|
|
728
|
-
)
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
const db = await getDb()
|
|
732
|
-
const gatewayBot = await db.query.bot_tokens.findFirst({
|
|
733
|
-
where: { app_id: KIMAKI_GATEWAY_APP_ID },
|
|
734
|
-
})
|
|
735
|
-
|
|
736
|
-
if (gatewayBot?.client_id && gatewayBot.client_secret) {
|
|
737
|
-
return {
|
|
738
|
-
clientId: gatewayBot.client_id,
|
|
739
|
-
clientSecret: gatewayBot.client_secret,
|
|
740
|
-
createdNow: false,
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
const clientId = crypto.randomUUID()
|
|
745
|
-
const clientSecret = crypto.randomBytes(32).toString('hex')
|
|
746
|
-
|
|
747
|
-
await setBotMode({
|
|
748
|
-
appId: KIMAKI_GATEWAY_APP_ID,
|
|
749
|
-
mode: 'gateway',
|
|
750
|
-
clientId,
|
|
751
|
-
clientSecret,
|
|
752
|
-
proxyUrl: KIMAKI_GATEWAY_PROXY_REST_BASE_URL,
|
|
753
|
-
})
|
|
754
|
-
|
|
755
|
-
return {
|
|
756
|
-
clientId,
|
|
757
|
-
clientSecret,
|
|
758
|
-
createdNow: true,
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
export async function printDiscordInstallUrlAndExit({
|
|
763
|
-
gateway,
|
|
764
|
-
gatewayCallbackUrl,
|
|
765
|
-
}: {
|
|
766
|
-
gateway?: boolean
|
|
767
|
-
gatewayCallbackUrl?: string
|
|
768
|
-
} = {}) {
|
|
769
|
-
await initDatabase()
|
|
770
|
-
|
|
771
|
-
if (gateway) {
|
|
772
|
-
const gatewayCredentials = await resolveGatewayInstallCredentials()
|
|
773
|
-
if (gatewayCredentials instanceof Error) {
|
|
774
|
-
cliLogger.error(`Failed to resolve gateway install URL: ${gatewayCredentials.message}`)
|
|
775
|
-
process.exit(EXIT_NO_RESTART)
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const installUrl = generateDiscordInstallUrlForBot({
|
|
779
|
-
appId: KIMAKI_GATEWAY_APP_ID,
|
|
780
|
-
mode: 'gateway',
|
|
781
|
-
clientId: gatewayCredentials.clientId,
|
|
782
|
-
clientSecret: gatewayCredentials.clientSecret,
|
|
783
|
-
gatewayCallbackUrl,
|
|
784
|
-
})
|
|
785
|
-
if (installUrl instanceof Error) {
|
|
786
|
-
cliLogger.error(`Failed to build install URL: ${installUrl.message}`)
|
|
787
|
-
process.exit(EXIT_NO_RESTART)
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
cliLogger.log(installUrl)
|
|
791
|
-
if (gatewayCredentials.createdNow) {
|
|
792
|
-
cliLogger.log('Generated and saved new local gateway client credentials.')
|
|
793
|
-
}
|
|
794
|
-
cliLogger.log(
|
|
795
|
-
'This gateway install URL contains your client credentials. Do not share it.',
|
|
796
|
-
)
|
|
797
|
-
process.exit(0)
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const existingBot = await getBotTokenWithMode()
|
|
801
|
-
|
|
802
|
-
if (!existingBot) {
|
|
803
|
-
cliLogger.error('No bot configured yet. Run `kimaki` first to set up.')
|
|
804
|
-
process.exit(EXIT_NO_RESTART)
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
const installUrl = generateDiscordInstallUrlForBot({
|
|
808
|
-
appId: existingBot.appId,
|
|
809
|
-
mode: existingBot.mode,
|
|
810
|
-
clientId: existingBot.clientId,
|
|
811
|
-
clientSecret: existingBot.clientSecret,
|
|
812
|
-
})
|
|
813
|
-
if (installUrl instanceof Error) {
|
|
814
|
-
cliLogger.error(`Failed to build install URL: ${installUrl.message}`)
|
|
815
|
-
process.exit(EXIT_NO_RESTART)
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
cliLogger.log(installUrl)
|
|
819
|
-
if (existingBot.mode === 'gateway') {
|
|
820
|
-
cliLogger.log(
|
|
821
|
-
'This gateway install URL contains your client credentials. Do not share it.',
|
|
822
|
-
)
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
process.exit(0)
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// Detect if a CLI tool is installed, prompt to install if missing.
|
|
829
|
-
// Uses official install scripts with platform-specific commands for Unix vs Windows.
|
|
830
|
-
// Sets process.env[envPathKey] to the found binary path for the current session.
|
|
831
|
-
// After install, re-checks PATH first, then falls back to common install locations.
|
|
832
|
-
export async function ensureCommandAvailable({
|
|
833
|
-
name,
|
|
834
|
-
envPathKey,
|
|
835
|
-
installUnix,
|
|
836
|
-
installWindows,
|
|
837
|
-
possiblePathsUnix,
|
|
838
|
-
possiblePathsWindows,
|
|
839
|
-
}: {
|
|
840
|
-
name: string
|
|
841
|
-
envPathKey: string
|
|
842
|
-
installUnix: string
|
|
843
|
-
installWindows: string
|
|
844
|
-
possiblePathsUnix: string[]
|
|
845
|
-
possiblePathsWindows: string[]
|
|
846
|
-
}): Promise<void> {
|
|
847
|
-
if (process.env[envPathKey]) {
|
|
848
|
-
return
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
const isWindows = process.platform === 'win32'
|
|
852
|
-
const whichCmd = isWindows ? 'where' : 'which'
|
|
853
|
-
const isInstalled = await execAsync(`${whichCmd} ${name}`, {
|
|
854
|
-
env: process.env,
|
|
855
|
-
}).then(
|
|
856
|
-
() => {
|
|
857
|
-
return true
|
|
858
|
-
},
|
|
859
|
-
() => {
|
|
860
|
-
return false
|
|
861
|
-
},
|
|
862
|
-
)
|
|
863
|
-
|
|
864
|
-
if (isInstalled) {
|
|
865
|
-
return
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
note(`${name} is required but not found in your PATH.`, `${name} Not Found`)
|
|
869
|
-
|
|
870
|
-
// In non-TTY (cloud sandbox, CI), auto-install without prompting.
|
|
871
|
-
// In interactive mode, ask the user first.
|
|
872
|
-
if (canUseInteractivePrompts()) {
|
|
873
|
-
const shouldInstall = await confirm({
|
|
874
|
-
message: `Would you like to install ${name} right now?`,
|
|
875
|
-
})
|
|
876
|
-
if (isCancel(shouldInstall) || !shouldInstall) {
|
|
877
|
-
cancel(`${name} is required to run this bot`)
|
|
878
|
-
process.exit(EXIT_NO_RESTART)
|
|
879
|
-
}
|
|
880
|
-
} else {
|
|
881
|
-
cliLogger.log(`Auto-installing ${name} (non-interactive mode)...`)
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
cliLogger.log(`Installing ${name}...`)
|
|
885
|
-
|
|
886
|
-
try {
|
|
887
|
-
// Use explicit shell invocation to avoid Node shell-mode quirks on Windows.
|
|
888
|
-
// PowerShell needs -NoProfile and -ExecutionPolicy Bypass for install scripts.
|
|
889
|
-
// Unix uses login shell (-l) so install scripts can update PATH in shell config.
|
|
890
|
-
const cmd = isWindows ? 'powershell.exe' : '/bin/bash'
|
|
891
|
-
const args = isWindows
|
|
892
|
-
? ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', installWindows]
|
|
893
|
-
: ['-lc', installUnix]
|
|
894
|
-
await new Promise<void>((resolve, reject) => {
|
|
895
|
-
const child = spawn(cmd, args, { stdio: 'inherit', env: process.env })
|
|
896
|
-
child.on('close', (code) => {
|
|
897
|
-
if (code === 0) {
|
|
898
|
-
resolve()
|
|
899
|
-
} else {
|
|
900
|
-
reject(new Error(`${name} install exited with code ${code}`))
|
|
901
|
-
}
|
|
902
|
-
})
|
|
903
|
-
child.on('error', reject)
|
|
904
|
-
})
|
|
905
|
-
cliLogger.log(`${name} installed successfully!`)
|
|
906
|
-
} catch (error) {
|
|
907
|
-
cliLogger.log(`Failed to install ${name}`)
|
|
908
|
-
cliLogger.error(
|
|
909
|
-
'Installation error:',
|
|
910
|
-
error instanceof Error ? error.stack : String(error),
|
|
911
|
-
)
|
|
912
|
-
process.exit(EXIT_NO_RESTART)
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// After install, re-check PATH first (install script may have added it)
|
|
916
|
-
const foundInPath = await execAsync(`${whichCmd} ${name}`, {
|
|
917
|
-
env: process.env,
|
|
918
|
-
}).then(
|
|
919
|
-
(result) => {
|
|
920
|
-
const resolved = selectResolvedCommand({
|
|
921
|
-
output: result.stdout,
|
|
922
|
-
isWindows,
|
|
923
|
-
})
|
|
924
|
-
return resolved || ''
|
|
925
|
-
},
|
|
926
|
-
() => {
|
|
927
|
-
return ''
|
|
928
|
-
},
|
|
929
|
-
)
|
|
930
|
-
if (foundInPath) {
|
|
931
|
-
process.env[envPathKey] = foundInPath
|
|
932
|
-
return
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// Fall back to probing common install locations
|
|
936
|
-
const home = process.env.HOME || process.env.USERPROFILE || ''
|
|
937
|
-
const accessFlag = isWindows ? fs.constants.F_OK : fs.constants.X_OK
|
|
938
|
-
const possiblePaths = (isWindows ? possiblePathsWindows : possiblePathsUnix)
|
|
939
|
-
.filter((p) => {
|
|
940
|
-
return !p.startsWith('~') || home
|
|
941
|
-
})
|
|
942
|
-
.map((p) => {
|
|
943
|
-
return p.replace('~', home)
|
|
944
|
-
})
|
|
945
|
-
|
|
946
|
-
const installedPath = possiblePaths.find((p) => {
|
|
947
|
-
try {
|
|
948
|
-
fs.accessSync(p, accessFlag)
|
|
949
|
-
return true
|
|
950
|
-
} catch {
|
|
951
|
-
return false
|
|
952
|
-
}
|
|
953
|
-
})
|
|
954
|
-
|
|
955
|
-
if (!installedPath) {
|
|
956
|
-
note(
|
|
957
|
-
`${name} was installed but may not be available in this session.\n` +
|
|
958
|
-
'Please restart your terminal and run this command again.',
|
|
959
|
-
'Restart Required',
|
|
960
|
-
)
|
|
961
|
-
process.exit(EXIT_NO_RESTART)
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
process.env[envPathKey] = installedPath
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// Run opencode upgrade in the background so the user always has the latest version.
|
|
968
|
-
|
|
969
|
-
// Spawn caffeinate on macOS to prevent system sleep while bot is running.
|
|
970
|
-
// Uses -s to also prevent sleep on lid close (AC power only, not battery).
|
|
971
|
-
// Uses -w to watch the parent PID so caffeinate self-terminates if kimaki
|
|
972
|
-
// exits for any reason (SIGTERM, crash, process.exit, supervisor stop).
|
|
973
|
-
export function startCaffeinate() {
|
|
974
|
-
if (process.platform !== 'darwin') {
|
|
975
|
-
return
|
|
976
|
-
}
|
|
977
|
-
try {
|
|
978
|
-
const proc = spawn('caffeinate', ['-s', '-w', String(process.pid)], {
|
|
979
|
-
stdio: 'ignore',
|
|
980
|
-
detached: false,
|
|
981
|
-
})
|
|
982
|
-
proc.unref()
|
|
983
|
-
proc.on('error', (err) => {
|
|
984
|
-
cliLogger.warn('Failed to start caffeinate:', err.message)
|
|
985
|
-
})
|
|
986
|
-
cliLogger.log('Started caffeinate to prevent system sleep')
|
|
987
|
-
} catch (err) {
|
|
988
|
-
cliLogger.warn(
|
|
989
|
-
'Failed to spawn caffeinate:',
|
|
990
|
-
err instanceof Error ? err.message : String(err),
|
|
991
|
-
)
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
// Result of credential resolution. `credentialSource` indicates how the
|
|
996
|
-
// credentials were obtained so downstream code can decide whether to show
|
|
997
|
-
// channel setup prompts (wizard) or skip them (env/saved/headless).
|
|
998
|
-
type CredentialResult = {
|
|
999
|
-
appId: string
|
|
1000
|
-
token: string
|
|
1001
|
-
credentialSource: 'env' | 'saved' | 'wizard'
|
|
1002
|
-
isGatewayMode: boolean
|
|
1003
|
-
installerDiscordUserId?: string
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
type CliOptions = {
|
|
1007
|
-
restartOnboarding?: boolean
|
|
1008
|
-
addChannels?: boolean
|
|
1009
|
-
dataDir?: string
|
|
1010
|
-
useWorktrees?: boolean
|
|
1011
|
-
enableVoiceChannels?: boolean
|
|
1012
|
-
gateway?: boolean
|
|
1013
|
-
gatewayCallbackUrl?: string
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
export async function collectKimakiChannels({
|
|
1018
|
-
guilds,
|
|
1019
|
-
}: {
|
|
1020
|
-
guilds: Guild[]
|
|
1021
|
-
}): Promise<{ guild: Guild; channels: ChannelWithTags[] }[]> {
|
|
1022
|
-
const guildResults = await Promise.all(
|
|
1023
|
-
guilds.map(async (guild) => {
|
|
1024
|
-
const channels = await getChannelsWithDescriptions(guild)
|
|
1025
|
-
const kimakiChans = channels.filter((ch) => ch.kimakiDirectory)
|
|
1026
|
-
|
|
1027
|
-
return { guild, channels: kimakiChans }
|
|
1028
|
-
}),
|
|
1029
|
-
)
|
|
1030
|
-
|
|
1031
|
-
return guildResults.filter((result) => {
|
|
1032
|
-
return result.channels.length > 0
|
|
1033
|
-
})
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
/**
|
|
1037
|
-
* Store channel-directory mappings in the database.
|
|
1038
|
-
* Called after Discord login to persist channel configurations.
|
|
1039
|
-
*/
|
|
1040
|
-
export async function storeChannelDirectories({
|
|
1041
|
-
kimakiChannels,
|
|
1042
|
-
}: {
|
|
1043
|
-
kimakiChannels: { guild: Guild; channels: ChannelWithTags[] }[]
|
|
1044
|
-
}): Promise<void> {
|
|
1045
|
-
for (const { guild, channels } of kimakiChannels) {
|
|
1046
|
-
for (const channel of channels) {
|
|
1047
|
-
if (channel.kimakiDirectory) {
|
|
1048
|
-
await setChannelDirectory({
|
|
1049
|
-
channelId: channel.id,
|
|
1050
|
-
directory: channel.kimakiDirectory,
|
|
1051
|
-
channelType: 'text',
|
|
1052
|
-
skipIfExists: true,
|
|
1053
|
-
})
|
|
1054
|
-
|
|
1055
|
-
const voiceChannel = guild.channels.cache.find(
|
|
1056
|
-
(ch) =>
|
|
1057
|
-
ch.type === ChannelType.GuildVoice && ch.name === channel.name,
|
|
1058
|
-
)
|
|
1059
|
-
|
|
1060
|
-
if (voiceChannel) {
|
|
1061
|
-
await setChannelDirectory({
|
|
1062
|
-
channelId: voiceChannel.id,
|
|
1063
|
-
directory: channel.kimakiDirectory,
|
|
1064
|
-
channelType: 'voice',
|
|
1065
|
-
skipIfExists: true,
|
|
1066
|
-
})
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
/**
|
|
1074
|
-
* Show the ready message with channel links.
|
|
1075
|
-
* Called at the end of startup to display available channels.
|
|
1076
|
-
*/
|
|
1077
|
-
export function showReadyMessage({
|
|
1078
|
-
kimakiChannels,
|
|
1079
|
-
createdChannels,
|
|
1080
|
-
}: {
|
|
1081
|
-
kimakiChannels: { guild: Guild; channels: ChannelWithTags[] }[]
|
|
1082
|
-
createdChannels: { name: string; id: string; guildId: string }[]
|
|
1083
|
-
}): void {
|
|
1084
|
-
const allChannels: {
|
|
1085
|
-
name: string
|
|
1086
|
-
id: string
|
|
1087
|
-
guildId: string
|
|
1088
|
-
directory?: string
|
|
1089
|
-
}[] = []
|
|
1090
|
-
|
|
1091
|
-
allChannels.push(...createdChannels)
|
|
1092
|
-
|
|
1093
|
-
kimakiChannels.forEach(({ guild, channels }) => {
|
|
1094
|
-
channels.forEach((ch) => {
|
|
1095
|
-
allChannels.push({
|
|
1096
|
-
name: ch.name,
|
|
1097
|
-
id: ch.id,
|
|
1098
|
-
guildId: guild.id,
|
|
1099
|
-
directory: ch.kimakiDirectory,
|
|
1100
|
-
})
|
|
1101
|
-
})
|
|
1102
|
-
})
|
|
1103
|
-
|
|
1104
|
-
if (allChannels.length > 0) {
|
|
1105
|
-
const channelLinks = allChannels
|
|
1106
|
-
.map(
|
|
1107
|
-
(ch) =>
|
|
1108
|
-
`• #${ch.name}: https://discord.com/channels/${ch.guildId}/${ch.id}`,
|
|
1109
|
-
)
|
|
1110
|
-
.join('\n')
|
|
1111
|
-
|
|
1112
|
-
note(
|
|
1113
|
-
`Your kimaki channels are ready! Click any link below to open in Discord:\n\n${channelLinks}\n\nSend a message in any channel to start using OpenCode!`,
|
|
1114
|
-
'🚀 Ready to Use',
|
|
1115
|
-
)
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
note(
|
|
1119
|
-
'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.',
|
|
1120
|
-
'⚠️ Keep Running',
|
|
1121
|
-
)
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
/**
|
|
1125
|
-
* Create default channels in locally configured guilds, or proxy-authorized gateway guilds.
|
|
1126
|
-
* Idempotent: skips guilds that already have the channel.
|
|
1127
|
-
* Extracted so both the interactive and headless startup paths share the same logic.
|
|
1128
|
-
*/
|
|
1129
|
-
export async function ensureDefaultChannelsWithWelcome({
|
|
1130
|
-
guilds,
|
|
1131
|
-
discordClient,
|
|
1132
|
-
appId,
|
|
1133
|
-
isGatewayMode,
|
|
1134
|
-
installerDiscordUserId,
|
|
1135
|
-
}: {
|
|
1136
|
-
guilds: Guild[]
|
|
1137
|
-
discordClient: import('discord.js').Client
|
|
1138
|
-
appId: string
|
|
1139
|
-
isGatewayMode: boolean
|
|
1140
|
-
installerDiscordUserId?: string
|
|
1141
|
-
}): Promise<{ name: string; id: string; guildId: string }[]> {
|
|
1142
|
-
if (process.env['KIMAKI_NO_DEFAULT_CHANNEL'] === '1') return []
|
|
1143
|
-
|
|
1144
|
-
const localMappings = isGatewayMode ? [] : await findChannelsByDirectory({})
|
|
1145
|
-
const created: { name: string; id: string; guildId: string }[] = []
|
|
1146
|
-
for (const guild of guilds) {
|
|
1147
|
-
try {
|
|
1148
|
-
if (!isGatewayMode) {
|
|
1149
|
-
// Match live channel IDs: older local mappings have no guild_id.
|
|
1150
|
-
const channels = await guild.channels.fetch()
|
|
1151
|
-
if (!localMappings.some((row) => channels.has(row.channel_id))) continue
|
|
1152
|
-
}
|
|
1153
|
-
const result = await createDefaultKimakiChannel({
|
|
1154
|
-
guild,
|
|
1155
|
-
botName: discordClient.user?.username,
|
|
1156
|
-
appId,
|
|
1157
|
-
isGatewayMode,
|
|
1158
|
-
})
|
|
1159
|
-
if (result) {
|
|
1160
|
-
created.push({
|
|
1161
|
-
name: result.channelName,
|
|
1162
|
-
id: result.textChannelId,
|
|
1163
|
-
guildId: guild.id,
|
|
1164
|
-
})
|
|
1165
|
-
|
|
1166
|
-
// Send welcome message to the newly created default channel.
|
|
1167
|
-
// Mention the installer so they get a notification.
|
|
1168
|
-
const mentionUserId = installerDiscordUserId || guild.ownerId
|
|
1169
|
-
await sendWelcomeMessage({
|
|
1170
|
-
channel: result.textChannel,
|
|
1171
|
-
mentionUserId,
|
|
1172
|
-
})
|
|
1173
|
-
}
|
|
1174
|
-
} catch (error) {
|
|
1175
|
-
cliLogger.warn(
|
|
1176
|
-
`Failed to create default kimaki channel in ${guild.name}: ${error instanceof Error ? error.stack : String(error)}`,
|
|
1177
|
-
)
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
return created
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
/**
|
|
1184
|
-
* Background initialization for quick start mode.
|
|
1185
|
-
* Starts OpenCode server and registers slash commands without blocking bot startup.
|
|
1186
|
-
*/
|
|
1187
|
-
export async function backgroundInit({
|
|
1188
|
-
currentDir,
|
|
1189
|
-
token,
|
|
1190
|
-
appId,
|
|
1191
|
-
guildIds,
|
|
1192
|
-
}: {
|
|
1193
|
-
currentDir: string
|
|
1194
|
-
token: string
|
|
1195
|
-
appId: string
|
|
1196
|
-
guildIds: string[]
|
|
1197
|
-
}): Promise<void> {
|
|
1198
|
-
try {
|
|
1199
|
-
const opencodeResult = await initializeOpencodeForDirectory(currentDir)
|
|
1200
|
-
if (opencodeResult instanceof Error) {
|
|
1201
|
-
cliLogger.warn('Background OpenCode init failed:', opencodeResult.message)
|
|
1202
|
-
// Still try to register basic commands without user commands/agents
|
|
1203
|
-
await registerCommands({
|
|
1204
|
-
token,
|
|
1205
|
-
appId,
|
|
1206
|
-
guildIds,
|
|
1207
|
-
userCommands: [],
|
|
1208
|
-
agents: [],
|
|
1209
|
-
})
|
|
1210
|
-
return
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
const getClient = opencodeResult
|
|
1214
|
-
|
|
1215
|
-
const [userCommands, agents] = await Promise.all([
|
|
1216
|
-
getClient()
|
|
1217
|
-
.command.list({ directory: currentDir })
|
|
1218
|
-
.then((r) => r.data || [])
|
|
1219
|
-
.catch((error) => {
|
|
1220
|
-
cliLogger.warn(
|
|
1221
|
-
'Failed to load user commands during background init:',
|
|
1222
|
-
error instanceof Error ? error.stack : String(error),
|
|
1223
|
-
)
|
|
1224
|
-
return []
|
|
1225
|
-
}),
|
|
1226
|
-
getClient()
|
|
1227
|
-
.app.agents({ directory: currentDir })
|
|
1228
|
-
.then((r) => r.data || [])
|
|
1229
|
-
.catch((error) => {
|
|
1230
|
-
cliLogger.warn(
|
|
1231
|
-
'Failed to load agents during background init:',
|
|
1232
|
-
error instanceof Error ? error.stack : String(error),
|
|
1233
|
-
)
|
|
1234
|
-
return []
|
|
1235
|
-
}),
|
|
1236
|
-
])
|
|
1237
|
-
|
|
1238
|
-
await registerCommands({ token, appId, guildIds, userCommands, agents })
|
|
1239
|
-
cliLogger.log('Slash commands registered!')
|
|
1240
|
-
} catch (error) {
|
|
1241
|
-
cliLogger.error(
|
|
1242
|
-
'Background init failed:',
|
|
1243
|
-
error instanceof Error ? error.stack : String(error),
|
|
1244
|
-
)
|
|
1245
|
-
void notifyError(error, 'Background init failed')
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
// Resolve bot credentials from (in priority order):
|
|
1250
|
-
// 1. KIMAKI_BOT_TOKEN env var (headless/CI deployments)
|
|
1251
|
-
// 2. Saved credentials in the database (self-hosted or gateway mode)
|
|
1252
|
-
// 3. Interactive wizard (gateway OAuth or self-hosted token entry)
|
|
1253
|
-
//
|
|
1254
|
-
// credentialSource tells the caller how creds were obtained:
|
|
1255
|
-
// 'env' — KIMAKI_BOT_TOKEN env var
|
|
1256
|
-
// 'saved' — reused from database
|
|
1257
|
-
// 'wizard' — user just completed onboarding (gateway OAuth or self-hosted)
|
|
1258
|
-
export async function resolveCredentials({
|
|
1259
|
-
forceRestartOnboarding,
|
|
1260
|
-
forceGateway,
|
|
1261
|
-
gatewayCallbackUrl,
|
|
1262
|
-
}: {
|
|
1263
|
-
forceRestartOnboarding: boolean
|
|
1264
|
-
forceGateway: boolean
|
|
1265
|
-
gatewayCallbackUrl?: string
|
|
1266
|
-
}): Promise<CredentialResult> {
|
|
1267
|
-
const envToken = process.env.KIMAKI_BOT_TOKEN
|
|
1268
|
-
const existingBot = await getBotTokenWithMode()
|
|
1269
|
-
// When --gateway is requested and the resolved bot is still self-hosted,
|
|
1270
|
-
// check if saved gateway credentials exist by looking up the gateway app_id
|
|
1271
|
-
// directly. This lets users switch back and forth between modes without
|
|
1272
|
-
// re-running the onboarding wizard each time.
|
|
1273
|
-
const hasGatewayCreds = (forceGateway && existingBot?.mode !== 'gateway')
|
|
1274
|
-
? await (await getDb()).query.bot_tokens.findFirst({
|
|
1275
|
-
where: { app_id: KIMAKI_GATEWAY_APP_ID },
|
|
1276
|
-
})
|
|
1277
|
-
: undefined
|
|
1278
|
-
|
|
1279
|
-
// 1. Env var takes precedence (headless deployments)
|
|
1280
|
-
if (envToken && !forceRestartOnboarding && !forceGateway) {
|
|
1281
|
-
const derivedAppId = appIdFromToken(envToken)
|
|
1282
|
-
if (!derivedAppId) {
|
|
1283
|
-
cliLogger.error(
|
|
1284
|
-
'Could not derive Application ID from KIMAKI_BOT_TOKEN. The token appears malformed.',
|
|
1285
|
-
)
|
|
1286
|
-
process.exit(EXIT_NO_RESTART)
|
|
1287
|
-
}
|
|
1288
|
-
await setBotToken(derivedAppId, envToken)
|
|
1289
|
-
cliLogger.log(`Using KIMAKI_BOT_TOKEN env var (App ID: ${derivedAppId})`)
|
|
1290
|
-
return { appId: derivedAppId, token: envToken, credentialSource: 'env', isGatewayMode: false }
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
// 2. Saved credentials in the database
|
|
1294
|
-
// Reuse saved creds unless: --restart-onboarding forces re-setup, or --gateway
|
|
1295
|
-
// overrides saved self-hosted creds (saved gateway creds are still used).
|
|
1296
|
-
const canReuseSavedCreds = existingBot && !forceRestartOnboarding
|
|
1297
|
-
&& !(forceGateway && existingBot.mode !== 'gateway')
|
|
1298
|
-
if (canReuseSavedCreds) {
|
|
1299
|
-
const modeLabel =
|
|
1300
|
-
existingBot.mode === 'gateway' ? ' (gateway mode)' : ''
|
|
1301
|
-
note(
|
|
1302
|
-
`Using saved bot credentials${modeLabel}:\nApp ID: ${existingBot.appId}\n\nTo use different credentials, run with --restart-onboarding`,
|
|
1303
|
-
'Existing Bot Found',
|
|
1304
|
-
)
|
|
1305
|
-
if (existingBot.mode !== 'gateway') {
|
|
1306
|
-
note(
|
|
1307
|
-
`Bot install URL (in case you need to add it to another server):\n${generateBotInstallUrl({ clientId: existingBot.appId })}`,
|
|
1308
|
-
'Install URL',
|
|
1309
|
-
)
|
|
1310
|
-
}
|
|
1311
|
-
return { appId: existingBot.appId, token: existingBot.token, credentialSource: 'saved', isGatewayMode: existingBot.mode === 'gateway' }
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
// 2b. Switching to gateway: saved gateway credentials exist from a previous
|
|
1315
|
-
// gateway setup. Reuse them without re-running the onboarding wizard.
|
|
1316
|
-
if (hasGatewayCreds && !forceRestartOnboarding) {
|
|
1317
|
-
const gatewayToken = (hasGatewayCreds.client_id && hasGatewayCreds.client_secret)
|
|
1318
|
-
? `${hasGatewayCreds.client_id}:${hasGatewayCreds.client_secret}`
|
|
1319
|
-
: hasGatewayCreds.token
|
|
1320
|
-
note(
|
|
1321
|
-
`Switching to saved gateway credentials:\nApp ID: ${hasGatewayCreds.app_id}`,
|
|
1322
|
-
'Mode Switch',
|
|
1323
|
-
)
|
|
1324
|
-
return {
|
|
1325
|
-
appId: hasGatewayCreds.app_id,
|
|
1326
|
-
token: gatewayToken,
|
|
1327
|
-
credentialSource: 'saved',
|
|
1328
|
-
isGatewayMode: true,
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
// 3. Interactive setup wizard (first-time users, --restart-onboarding, or --gateway override).
|
|
1333
|
-
// Non-TTY: gateway mode proceeds headlessly (JSON events on stdout),
|
|
1334
|
-
// self-hosted mode requires interactive prompts so we exit.
|
|
1335
|
-
if (!canUseInteractivePrompts() && !forceGateway) {
|
|
1336
|
-
exitNonInteractiveSetup()
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
if (existingBot && forceGateway && existingBot.mode !== 'gateway') {
|
|
1340
|
-
note(
|
|
1341
|
-
'Ignoring saved self-hosted credentials due to --gateway flag.\nSwitching to gateway mode.',
|
|
1342
|
-
'Gateway Mode',
|
|
1343
|
-
)
|
|
1344
|
-
} else if (forceRestartOnboarding && existingBot) {
|
|
1345
|
-
note('Ignoring saved credentials due to --restart-onboarding flag', 'Restart Onboarding')
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
// When --gateway is passed or we're in non-TTY mode, skip the mode selector.
|
|
1349
|
-
// Non-TTY without --gateway was already rejected above.
|
|
1350
|
-
const modeChoice: 'gateway' | 'self_hosted' = forceGateway
|
|
1351
|
-
? 'gateway'
|
|
1352
|
-
: await (async () => {
|
|
1353
|
-
const choice = await select({
|
|
1354
|
-
message:
|
|
1355
|
-
'How do you want to connect to Discord?\n\nGateway: uses Kimaki\'s pre-built bot — no setup, instant. Self-hosted: you create your own Discord bot at discord.com/developers.',
|
|
1356
|
-
options: [
|
|
1357
|
-
{
|
|
1358
|
-
value: 'gateway' as const,
|
|
1359
|
-
label: 'Gateway (pre-built Kimaki bot, no setup needed)',
|
|
1360
|
-
},
|
|
1361
|
-
{
|
|
1362
|
-
value: 'self_hosted' as const,
|
|
1363
|
-
label: 'Self-hosted (your own Discord bot, 5-10 min setup)',
|
|
1364
|
-
},
|
|
1365
|
-
],
|
|
1366
|
-
})
|
|
1367
|
-
if (isCancel(choice)) {
|
|
1368
|
-
cancel('Setup cancelled')
|
|
1369
|
-
process.exit(0)
|
|
1370
|
-
}
|
|
1371
|
-
return choice
|
|
1372
|
-
})()
|
|
1373
|
-
|
|
1374
|
-
// ── Gateway mode flow ──
|
|
1375
|
-
if (modeChoice === 'gateway') {
|
|
1376
|
-
if (!KIMAKI_GATEWAY_APP_ID) {
|
|
1377
|
-
cliLogger.error(
|
|
1378
|
-
'Gateway mode is not available yet. KIMAKI_GATEWAY_APP_ID is not configured.',
|
|
1379
|
-
)
|
|
1380
|
-
process.exit(EXIT_NO_RESTART)
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
const gatewayCredentials = await resolveGatewayInstallCredentials()
|
|
1384
|
-
if (gatewayCredentials instanceof Error) {
|
|
1385
|
-
throw gatewayCredentials
|
|
1386
|
-
}
|
|
1387
|
-
const { clientId, clientSecret } = gatewayCredentials
|
|
1388
|
-
|
|
1389
|
-
const oauthUrlResult = generateDiscordInstallUrlForBot({
|
|
1390
|
-
appId: KIMAKI_GATEWAY_APP_ID,
|
|
1391
|
-
mode: 'gateway',
|
|
1392
|
-
clientId,
|
|
1393
|
-
clientSecret,
|
|
1394
|
-
gatewayCallbackUrl,
|
|
1395
|
-
reachableUrl: getInternetReachableBaseUrl() || undefined,
|
|
1396
|
-
})
|
|
1397
|
-
if (oauthUrlResult instanceof Error) {
|
|
1398
|
-
throw oauthUrlResult
|
|
1399
|
-
}
|
|
1400
|
-
const oauthUrl = oauthUrlResult
|
|
1401
|
-
const isInteractive = canUseInteractivePrompts()
|
|
1402
|
-
|
|
1403
|
-
if (isInteractive) {
|
|
1404
|
-
note(
|
|
1405
|
-
`Open this URL to install the Kimaki bot in your Discord server:\n\n${oauthUrl}\n\nDo not share this URL with anyone — it contains your credentials.\n\nIf you don't have a server, create one first (+ button in the Discord sidebar).`,
|
|
1406
|
-
'Install Bot',
|
|
1407
|
-
)
|
|
1408
|
-
|
|
1409
|
-
// Open URL in default browser
|
|
1410
|
-
openUrlInDefaultBrowser(oauthUrl)
|
|
1411
|
-
} else {
|
|
1412
|
-
// Non-TTY: emit structured JSON so the host process can show the URL to the user.
|
|
1413
|
-
emitJsonEvent({ type: 'install_url', url: oauthUrl })
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
// Poll until the user installs the bot in a Discord server.
|
|
1417
|
-
// 100 attempts x 3s = 5 minutes timeout.
|
|
1418
|
-
const s = isInteractive ? spinner() : undefined
|
|
1419
|
-
s?.start('Waiting for a Discord server with the bot installed...')
|
|
1420
|
-
|
|
1421
|
-
const pollUrl = new URL('/api/onboarding/status', KIMAKI_WEBSITE_URL)
|
|
1422
|
-
pollUrl.searchParams.set('client_id', clientId)
|
|
1423
|
-
pollUrl.searchParams.set('secret', clientSecret)
|
|
1424
|
-
|
|
1425
|
-
let guildId: string | undefined
|
|
1426
|
-
let installerDiscordUserId: string | undefined
|
|
1427
|
-
let onboardingError: string | undefined
|
|
1428
|
-
for (let attempt = 0; attempt < 100; attempt++) {
|
|
1429
|
-
await new Promise((resolve) => {
|
|
1430
|
-
setTimeout(resolve, 3000)
|
|
1431
|
-
})
|
|
1432
|
-
|
|
1433
|
-
// Progressive hints for interactive users who may be stuck
|
|
1434
|
-
if (isInteractive) {
|
|
1435
|
-
if (attempt === 15) {
|
|
1436
|
-
s?.message(
|
|
1437
|
-
'Still waiting... Select a server in the Discord authorization page and click "Authorize"',
|
|
1438
|
-
)
|
|
1439
|
-
} else if (attempt === 45) {
|
|
1440
|
-
s?.message(
|
|
1441
|
-
`Still waiting... If you don't see any servers, create one first (+ button in Discord sidebar), then reopen the URL above`,
|
|
1442
|
-
)
|
|
1443
|
-
} else if (attempt === 75) {
|
|
1444
|
-
s?.message(
|
|
1445
|
-
`Still waiting... Reopen the install URL if you closed it:\n${oauthUrl}`,
|
|
1446
|
-
)
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
try {
|
|
1451
|
-
const resp = await fetch(pollUrl.toString())
|
|
1452
|
-
if (resp.ok) {
|
|
1453
|
-
const data = (await resp.json()) as {
|
|
1454
|
-
guild_id?: string
|
|
1455
|
-
discord_user_id?: string
|
|
1456
|
-
} | null
|
|
1457
|
-
if (data?.guild_id) {
|
|
1458
|
-
guildId = data.guild_id
|
|
1459
|
-
installerDiscordUserId = data.discord_user_id
|
|
1460
|
-
break
|
|
1461
|
-
}
|
|
1462
|
-
} else if (resp.status === 404) {
|
|
1463
|
-
// Check if the server returned a specific onboarding error
|
|
1464
|
-
// (e.g. guild_id missing from Discord callback)
|
|
1465
|
-
const data = (await resp.json().catch(() => null)) as {
|
|
1466
|
-
error?: string
|
|
1467
|
-
onboarding_error?: boolean
|
|
1468
|
-
} | null
|
|
1469
|
-
if (data?.onboarding_error && data.error) {
|
|
1470
|
-
onboardingError = data.error
|
|
1471
|
-
break
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
} catch {
|
|
1475
|
-
// Network error, retry
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
if (!guildId) {
|
|
1480
|
-
const errorMsg = onboardingError
|
|
1481
|
-
? `Authorization failed: ${onboardingError}`
|
|
1482
|
-
: 'Bot authorization timed out after 5 minutes. Please try again.'
|
|
1483
|
-
if (isInteractive) {
|
|
1484
|
-
s?.stop(onboardingError ? 'Authorization failed' : 'Authorization timed out')
|
|
1485
|
-
} else {
|
|
1486
|
-
emitJsonEvent({ type: 'error', message: errorMsg })
|
|
1487
|
-
}
|
|
1488
|
-
cliLogger.error(errorMsg)
|
|
1489
|
-
process.exit(EXIT_NO_RESTART)
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
if (isInteractive) {
|
|
1493
|
-
s?.stop('Bot authorized successfully!')
|
|
1494
|
-
const syncSpinner = spinner()
|
|
1495
|
-
syncSpinner.start('Waiting for gateway sync...')
|
|
1496
|
-
await new Promise((resolve) => {
|
|
1497
|
-
setTimeout(resolve, 2000)
|
|
1498
|
-
})
|
|
1499
|
-
syncSpinner.stop('Gateway sync completed')
|
|
1500
|
-
} else {
|
|
1501
|
-
emitJsonEvent({ type: 'authorized', guild_id: guildId })
|
|
1502
|
-
await new Promise((resolve) => {
|
|
1503
|
-
setTimeout(resolve, 2000)
|
|
1504
|
-
})
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
return {
|
|
1508
|
-
appId: KIMAKI_GATEWAY_APP_ID,
|
|
1509
|
-
token: `${clientId}:${clientSecret}`,
|
|
1510
|
-
credentialSource: 'wizard',
|
|
1511
|
-
isGatewayMode: true,
|
|
1512
|
-
installerDiscordUserId,
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
// ── Self-hosted mode flow (existing wizard) ──
|
|
1517
|
-
note(
|
|
1518
|
-
'1. Go to https://discord.com/developers/applications\n' +
|
|
1519
|
-
'2. Click "New Application"\n' +
|
|
1520
|
-
'3. Give your application a name',
|
|
1521
|
-
'Step 1: Create Discord Application',
|
|
1522
|
-
)
|
|
1523
|
-
|
|
1524
|
-
note(
|
|
1525
|
-
'1. Go to the "Bot" section in the left sidebar\n' +
|
|
1526
|
-
'2. Scroll down to "Privileged Gateway Intents"\n' +
|
|
1527
|
-
'3. Enable MESSAGE CONTENT INTENT by toggling it ON\n' +
|
|
1528
|
-
'4. Optional: enable SERVER MEMBERS INTENT only if you want name lookup in `kimaki user list` and `kimaki send --user Tommy`\n' +
|
|
1529
|
-
'5. Click "Save Changes" at the bottom',
|
|
1530
|
-
'Step 2: Enable Message Content Intent',
|
|
1531
|
-
)
|
|
1532
|
-
|
|
1533
|
-
const intentsConfirmed = await text({
|
|
1534
|
-
message: 'Press Enter after enabling Message Content Intent:',
|
|
1535
|
-
placeholder: 'Enter',
|
|
1536
|
-
})
|
|
1537
|
-
if (isCancel(intentsConfirmed)) {
|
|
1538
|
-
cancel('Setup cancelled')
|
|
1539
|
-
process.exit(0)
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
note(
|
|
1543
|
-
'1. Still in the "Bot" section\n' +
|
|
1544
|
-
'2. Click "Reset Token" to generate a new bot token (in case of errors try again)\n' +
|
|
1545
|
-
"3. Copy the token (you won't be able to see it again!)",
|
|
1546
|
-
'Step 3: Get Bot Token',
|
|
1547
|
-
)
|
|
1548
|
-
const tokenInput = await password({
|
|
1549
|
-
message:
|
|
1550
|
-
'Enter your Discord Bot Token (from "Bot" section - click "Reset Token" if needed):',
|
|
1551
|
-
validate(value) {
|
|
1552
|
-
const cleaned = stripBracketedPaste(value)
|
|
1553
|
-
if (!cleaned) {
|
|
1554
|
-
return 'Bot token is required'
|
|
1555
|
-
}
|
|
1556
|
-
if (cleaned.length < 50) {
|
|
1557
|
-
return 'Invalid token format (too short)'
|
|
1558
|
-
}
|
|
1559
|
-
},
|
|
1560
|
-
})
|
|
1561
|
-
if (isCancel(tokenInput)) {
|
|
1562
|
-
cancel('Setup cancelled')
|
|
1563
|
-
process.exit(0)
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
const wizardToken = stripBracketedPaste(tokenInput)
|
|
1567
|
-
const derivedAppId = appIdFromToken(wizardToken)
|
|
1568
|
-
if (!derivedAppId) {
|
|
1569
|
-
cliLogger.error(
|
|
1570
|
-
'Could not derive Application ID from the bot token. The token appears malformed.',
|
|
1571
|
-
)
|
|
1572
|
-
process.exit(EXIT_NO_RESTART)
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
await setBotToken(derivedAppId, wizardToken)
|
|
1576
|
-
|
|
1577
|
-
note(
|
|
1578
|
-
`Bot install URL:\n${generateBotInstallUrl({ clientId: derivedAppId })}\n\nYou MUST install the bot in your Discord server before continuing.`,
|
|
1579
|
-
'Step 4: Install Bot to Server',
|
|
1580
|
-
)
|
|
1581
|
-
const installed = await text({
|
|
1582
|
-
message: 'Press Enter AFTER you have installed the bot in your server:',
|
|
1583
|
-
placeholder: 'Enter',
|
|
1584
|
-
})
|
|
1585
|
-
if (isCancel(installed)) {
|
|
1586
|
-
cancel('Setup cancelled')
|
|
1587
|
-
process.exit(0)
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
return { appId: derivedAppId, token: wizardToken, credentialSource: 'wizard', isGatewayMode: false }
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
export async function run({
|
|
1594
|
-
restartOnboarding,
|
|
1595
|
-
addChannels,
|
|
1596
|
-
useWorktrees,
|
|
1597
|
-
enableVoiceChannels,
|
|
1598
|
-
gateway,
|
|
1599
|
-
gatewayCallbackUrl,
|
|
1600
|
-
}: CliOptions) {
|
|
1601
|
-
startCaffeinate()
|
|
1602
|
-
|
|
1603
|
-
const forceRestartOnboarding = Boolean(restartOnboarding)
|
|
1604
|
-
const forceGateway = Boolean(gateway)
|
|
1605
|
-
|
|
1606
|
-
// Step 0: Ensure opencode and bun are installed
|
|
1607
|
-
await Promise.all([
|
|
1608
|
-
ensureCommandAvailable({
|
|
1609
|
-
name: 'opencode',
|
|
1610
|
-
envPathKey: 'OPENCODE_PATH',
|
|
1611
|
-
installUnix: 'curl -fsSL https://opencode.ai/install | bash',
|
|
1612
|
-
installWindows: 'irm https://opencode.ai/install.ps1 | iex',
|
|
1613
|
-
possiblePathsUnix: [
|
|
1614
|
-
'~/.local/bin/opencode',
|
|
1615
|
-
'~/.opencode/bin/opencode',
|
|
1616
|
-
'/usr/local/bin/opencode',
|
|
1617
|
-
'/opt/opencode/bin/opencode',
|
|
1618
|
-
],
|
|
1619
|
-
possiblePathsWindows: [
|
|
1620
|
-
'~\\.local\\bin\\opencode.exe',
|
|
1621
|
-
'~\\AppData\\Local\\opencode\\opencode.exe',
|
|
1622
|
-
'~\\.opencode\\bin\\opencode.exe',
|
|
1623
|
-
],
|
|
1624
|
-
}),
|
|
1625
|
-
ensureCommandAvailable({
|
|
1626
|
-
name: 'bun',
|
|
1627
|
-
envPathKey: 'BUN_PATH',
|
|
1628
|
-
installUnix: 'curl -fsSL https://bun.sh/install | bash',
|
|
1629
|
-
installWindows: 'irm bun.sh/install.ps1 | iex',
|
|
1630
|
-
possiblePathsUnix: ['~/.bun/bin/bun', '/usr/local/bin/bun'],
|
|
1631
|
-
possiblePathsWindows: ['~\\.bun\\bin\\bun.exe'],
|
|
1632
|
-
}),
|
|
1633
|
-
])
|
|
1634
|
-
|
|
1635
|
-
const opencodeVersionCheck = await assertCompatibleOpencodeVersion()
|
|
1636
|
-
if (opencodeVersionCheck instanceof Error) {
|
|
1637
|
-
cliLogger.error(opencodeVersionCheck.message)
|
|
1638
|
-
process.exit(EXIT_NO_RESTART)
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
if (store.getState().autoUpgradeEnabled) {
|
|
1643
|
-
void backgroundUpgradeKimaki()
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
// Start in-process Hrana server before database init. Required for the bot
|
|
1647
|
-
// process because it serves as both the DB server and the single-instance
|
|
1648
|
-
// lock (binds the fixed lock port). Without it, IPC and lock enforcement
|
|
1649
|
-
// don't work. CLI subcommands skip the server and use file: directly.
|
|
1650
|
-
const hranaResult = await startHranaServer({
|
|
1651
|
-
dbPath: path.join(getDataDir(), 'discord-sessions.db'),
|
|
1652
|
-
bindAll: getInternetReachableBaseUrl() !== null,
|
|
1653
|
-
})
|
|
1654
|
-
if (hranaResult instanceof Error) {
|
|
1655
|
-
cliLogger.error('Failed to start hrana server:', hranaResult.message)
|
|
1656
|
-
process.exit(EXIT_NO_RESTART)
|
|
1657
|
-
}
|
|
1658
|
-
// We own the lock port from here on. Stop signals must release it in every
|
|
1659
|
-
// startup phase (Discord login, channel reconcile), not only after ready.
|
|
1660
|
-
registerBotLifecycleHandlers()
|
|
1661
|
-
|
|
1662
|
-
// Initialize database (connects to hrana server via HTTP)
|
|
1663
|
-
await initDatabase()
|
|
1664
|
-
|
|
1665
|
-
const { appId, token, credentialSource, isGatewayMode, installerDiscordUserId } = await resolveCredentials({
|
|
1666
|
-
forceRestartOnboarding,
|
|
1667
|
-
forceGateway,
|
|
1668
|
-
gatewayCallbackUrl,
|
|
1669
|
-
})
|
|
1670
|
-
|
|
1671
|
-
setAnalyticsBotMode(isGatewayMode ? 'gateway' : 'self_hosted')
|
|
1672
|
-
initAnalytics()
|
|
1673
|
-
|
|
1674
|
-
const gatewayToken = await ensureServiceAuthToken({
|
|
1675
|
-
appId,
|
|
1676
|
-
preferredGatewayToken: isGatewayMode ? token : undefined,
|
|
1677
|
-
})
|
|
1678
|
-
// Always set service auth token so local and internet control-plane paths
|
|
1679
|
-
// share one auth model (/kimaki/wake and future service endpoints).
|
|
1680
|
-
store.setState({ gatewayToken })
|
|
1681
|
-
|
|
1682
|
-
// In gateway mode, ensure REST calls route through the gateway proxy.
|
|
1683
|
-
// getBotTokenWithMode() sets this for saved-credential paths, but the fresh
|
|
1684
|
-
// onboarding path returns directly without going through getBotTokenWithMode(),
|
|
1685
|
-
// leaving store.discordBaseUrl at the default 'https://discord.com'.
|
|
1686
|
-
// Without this, discord.js sends the clientId:clientSecret token to Discord
|
|
1687
|
-
// directly, which rejects it with "An invalid token was provided".
|
|
1688
|
-
if (isGatewayMode) {
|
|
1689
|
-
store.setState({ discordBaseUrl: KIMAKI_GATEWAY_PROXY_REST_BASE_URL })
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
// When KIMAKI_INTERNET_REACHABLE_URL is set, the hrana server exposes
|
|
1693
|
-
// a /kimaki/wake endpoint for the gateway-proxy to wake this instance and
|
|
1694
|
-
// wait until discord.js is connected. Keep Discord traffic on the normal
|
|
1695
|
-
// configured base URL (gateway-proxy in gateway mode).
|
|
1696
|
-
if (getInternetReachableBaseUrl()) {
|
|
1697
|
-
cliLogger.log('Internet-reachable mode: enabling /kimaki/wake endpoint on hrana server')
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
// Start OpenCode server as early as possible — non-blocking.
|
|
1701
|
-
// All dependencies are met (dataDir, lockPort, gatewayToken, hranaUrl set).
|
|
1702
|
-
// Runs in parallel with last_used_at update, skipChannelSetup check, and
|
|
1703
|
-
// Discord Gateway login so cold start is not blocked by OpenCode spawn.
|
|
1704
|
-
const currentDir = process.cwd()
|
|
1705
|
-
cliLogger.log('Starting OpenCode server...')
|
|
1706
|
-
const opencodePromise = initializeOpencodeForDirectory(currentDir).then(
|
|
1707
|
-
(result) => {
|
|
1708
|
-
if (result instanceof Error) {
|
|
1709
|
-
throw new Error(result.message)
|
|
1710
|
-
}
|
|
1711
|
-
cliLogger.log('OpenCode server ready!')
|
|
1712
|
-
return result
|
|
1713
|
-
},
|
|
1714
|
-
)
|
|
1715
|
-
// Prevent unhandled rejection if OpenCode fails before backgroundInit
|
|
1716
|
-
// or the channel setup path awaits it. Errors are handled by the
|
|
1717
|
-
// respective consumers (backgroundInit catches, channel setup re-throws).
|
|
1718
|
-
opencodePromise.catch(() => {})
|
|
1719
|
-
|
|
1720
|
-
// Mark this bot as the most recently used so subcommands in separate
|
|
1721
|
-
// processes (send, upload-to-discord, project list) pick the correct bot.
|
|
1722
|
-
// getBotTokenWithMode() orders by last_used_at DESC as cross-process
|
|
1723
|
-
// source of truth.
|
|
1724
|
-
await (await getDb()).update(dbSchema.bot_tokens)
|
|
1725
|
-
.set({ last_used_at: new Date() })
|
|
1726
|
-
.where(orm.eq(dbSchema.bot_tokens.app_id, appId))
|
|
1727
|
-
|
|
1728
|
-
// skipChannelSetup: when true, skip interactive project/channel selection
|
|
1729
|
-
// and go straight to bot startup. Channel sync happens in the background.
|
|
1730
|
-
//
|
|
1731
|
-
// Skip when: creds came from env/saved (not first-time wizard), OR non-TTY
|
|
1732
|
-
// gateway (headless), OR user didn't pass --add-channels/--restart-onboarding.
|
|
1733
|
-
// Force channel setup when: first-time quick-start with no channels configured
|
|
1734
|
-
// and TTY is available, or user explicitly passed --add-channels.
|
|
1735
|
-
const isHeadlessGateway = isGatewayMode && !canUseInteractivePrompts()
|
|
1736
|
-
const hasConfiguredTextChannels = Boolean(
|
|
1737
|
-
await (await getDb()).query.channel_directories.findFirst({
|
|
1738
|
-
where: { channel_type: 'text' },
|
|
1739
|
-
columns: { channel_id: true },
|
|
1740
|
-
}),
|
|
1741
|
-
)
|
|
1742
|
-
const skipChannelSetup = isHeadlessGateway || (() => {
|
|
1743
|
-
// Wizard source always shows channel setup (user just completed onboarding)
|
|
1744
|
-
if (credentialSource === 'wizard') {
|
|
1745
|
-
return false
|
|
1746
|
-
}
|
|
1747
|
-
// Env/saved source: skip unless user explicitly asked for channels
|
|
1748
|
-
if (forceRestartOnboarding || Boolean(addChannels)) {
|
|
1749
|
-
return false
|
|
1750
|
-
}
|
|
1751
|
-
// First-time quick start with no channels: force setup if TTY is available
|
|
1752
|
-
if (!hasConfiguredTextChannels && canUseInteractivePrompts()) {
|
|
1753
|
-
return false
|
|
1754
|
-
}
|
|
1755
|
-
return true
|
|
1756
|
-
})()
|
|
1757
|
-
|
|
1758
|
-
cliLogger.log(`Connecting to ${getDiscordRestApiUrl()}...`)
|
|
1759
|
-
const discordClient = await createDiscordClient()
|
|
1760
|
-
|
|
1761
|
-
const guilds: Guild[] = []
|
|
1762
|
-
const kimakiChannels: { guild: Guild; channels: ChannelWithTags[] }[] = []
|
|
1763
|
-
const createdChannels: { name: string; id: string; guildId: string }[] = []
|
|
1764
|
-
|
|
1765
|
-
try {
|
|
1766
|
-
await new Promise((resolve, reject) => {
|
|
1767
|
-
discordClient.once(Events.ClientReady, async (c) => {
|
|
1768
|
-
// Guild discovery comes from the Gateway WebSocket READY payload, not
|
|
1769
|
-
// from a separate REST fetch. discord.js consumes READY and hydrates
|
|
1770
|
-
// client.guilds.cache from d.guilds. In gateway mode, gateway-proxy
|
|
1771
|
-
// already filters this list to authorized guilds for client_id:secret.
|
|
1772
|
-
// Example payload fragment received over WS:
|
|
1773
|
-
// {
|
|
1774
|
-
// "op": 0,
|
|
1775
|
-
// "t": "READY",
|
|
1776
|
-
// "d": {
|
|
1777
|
-
// "guilds": [
|
|
1778
|
-
// { "id": "123456789012345678", "unavailable": false }
|
|
1779
|
-
// ]
|
|
1780
|
-
// }
|
|
1781
|
-
// }
|
|
1782
|
-
guilds.push(...Array.from(c.guilds.cache.values()))
|
|
1783
|
-
|
|
1784
|
-
if (skipChannelSetup) {
|
|
1785
|
-
resolve(null)
|
|
1786
|
-
return
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
// Process guild metadata when setup flow needs channel prompts.
|
|
1790
|
-
const guildResults = await collectKimakiChannels({ guilds })
|
|
1791
|
-
|
|
1792
|
-
// Collect results
|
|
1793
|
-
for (const result of guildResults) {
|
|
1794
|
-
kimakiChannels.push(result)
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
resolve(null)
|
|
1798
|
-
})
|
|
1799
|
-
|
|
1800
|
-
discordClient.once(Events.Error, reject)
|
|
1801
|
-
|
|
1802
|
-
discordClient.login(token).catch(reject)
|
|
1803
|
-
})
|
|
1804
|
-
|
|
1805
|
-
cliLogger.log('Connected to Discord!')
|
|
1806
|
-
// Start IPC polling now that Discord client is ready.
|
|
1807
|
-
// Register cleanup on process exit since the shutdown handler lives in discord-bot.ts.
|
|
1808
|
-
await startIpcPolling({ discordClient })
|
|
1809
|
-
process.on('exit', stopIpcPolling)
|
|
1810
|
-
} catch (error) {
|
|
1811
|
-
cliLogger.log('Failed to connect to Discord', discordClient.ws.gateway)
|
|
1812
|
-
cliLogger.error(
|
|
1813
|
-
'Error: ' + (error instanceof Error ? error.stack : String(error)),
|
|
1814
|
-
)
|
|
1815
|
-
// Transient network errors (DNS down, gateway unreachable, connect
|
|
1816
|
-
// timeout) should allow the bin.ts wrapper to restart us after a delay.
|
|
1817
|
-
// EXIT_TEMPFAIL skips the crash-loop detector so a long outage cannot
|
|
1818
|
-
// permanently stop the bot. Only truly fatal errors (bad token, invalid
|
|
1819
|
-
// intent, etc.) should use EXIT_NO_RESTART.
|
|
1820
|
-
if (isTransientNetworkError(error)) {
|
|
1821
|
-
cliLogger.error('Transient network error, exiting for wrapper restart...')
|
|
1822
|
-
process.exit(EXIT_TEMPFAIL)
|
|
1823
|
-
}
|
|
1824
|
-
process.exit(EXIT_NO_RESTART)
|
|
1825
|
-
}
|
|
1826
|
-
// Stop signal during Discord login: shutdown owns the process now.
|
|
1827
|
-
if (global.shuttingDown) {
|
|
1828
|
-
return
|
|
1829
|
-
}
|
|
1830
|
-
await setBotToken(appId, token)
|
|
1831
|
-
|
|
1832
|
-
// In gateway mode the bot only sees guilds the user has installed
|
|
1833
|
-
// it in. Zero guilds means the install URL callback never completed or the
|
|
1834
|
-
// user removed the bot from all servers — there is nothing the bot can do.
|
|
1835
|
-
if (isGatewayMode && guilds.length === 0) {
|
|
1836
|
-
// Rebuild the install URL from the current credentials so the user can
|
|
1837
|
-
// add the bot to a server without going through the full --restart-onboarding flow.
|
|
1838
|
-
const [clientId, clientSecret] = token.split(':')
|
|
1839
|
-
if (!clientId || !clientSecret) {
|
|
1840
|
-
throw new Error('Malformed gateway token: expected clientId:clientSecret format')
|
|
1841
|
-
}
|
|
1842
|
-
const installUrlResult = generateDiscordInstallUrlForBot({
|
|
1843
|
-
appId: KIMAKI_GATEWAY_APP_ID,
|
|
1844
|
-
mode: 'gateway',
|
|
1845
|
-
clientId,
|
|
1846
|
-
clientSecret,
|
|
1847
|
-
})
|
|
1848
|
-
if (installUrlResult instanceof Error) {
|
|
1849
|
-
throw installUrlResult
|
|
1850
|
-
}
|
|
1851
|
-
const installUrl = installUrlResult
|
|
1852
|
-
if (!canUseInteractivePrompts()) {
|
|
1853
|
-
emitJsonEvent({ type: 'error', message: 'No Discord servers found', install_url: installUrl })
|
|
1854
|
-
}
|
|
1855
|
-
cliLogger.error(
|
|
1856
|
-
'No Discord servers found. The bot must be installed in at least one server.\n' +
|
|
1857
|
-
`Install URL: ${installUrl}\n` +
|
|
1858
|
-
'Do not share this URL with anyone — it contains your credentials.\n' +
|
|
1859
|
-
'Open the URL above to add the bot to a server, then run kimaki again.',
|
|
1860
|
-
)
|
|
1861
|
-
void discordClient.destroy()
|
|
1862
|
-
process.exit(EXIT_NO_RESTART)
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
if (skipChannelSetup) {
|
|
1866
|
-
// Start bot immediately — channel sync happens in the background.
|
|
1867
|
-
cliLogger.log('Starting Discord bot...')
|
|
1868
|
-
await startDiscordBot({ token, appId, discordClient, useWorktrees })
|
|
1869
|
-
if (global.shuttingDown) {
|
|
1870
|
-
return
|
|
1871
|
-
}
|
|
1872
|
-
cliLogger.log('Discord bot is running!')
|
|
1873
|
-
|
|
1874
|
-
// Background channel sync + role reconciliation + default channel creation.
|
|
1875
|
-
// Never blocks ready state.
|
|
1876
|
-
void (async () => {
|
|
1877
|
-
try {
|
|
1878
|
-
const backgroundChannels = await collectKimakiChannels({ guilds })
|
|
1879
|
-
await storeChannelDirectories({ kimakiChannels: backgroundChannels })
|
|
1880
|
-
cliLogger.log(
|
|
1881
|
-
`Background channel sync completed for ${backgroundChannels.length} guild(s)`,
|
|
1882
|
-
)
|
|
1883
|
-
} catch (error) {
|
|
1884
|
-
cliLogger.warn(
|
|
1885
|
-
'Background channel sync failed:',
|
|
1886
|
-
error instanceof Error ? error.stack : String(error),
|
|
1887
|
-
)
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
// Create default channels only in locally configured or gateway-authorized guilds.
|
|
1891
|
-
// Runs after channel sync so existing channels are detected correctly.
|
|
1892
|
-
try {
|
|
1893
|
-
await ensureDefaultChannelsWithWelcome({
|
|
1894
|
-
guilds,
|
|
1895
|
-
discordClient,
|
|
1896
|
-
appId,
|
|
1897
|
-
isGatewayMode,
|
|
1898
|
-
installerDiscordUserId,
|
|
1899
|
-
})
|
|
1900
|
-
} catch (error) {
|
|
1901
|
-
cliLogger.warn(
|
|
1902
|
-
'Background default channel creation failed:',
|
|
1903
|
-
error instanceof Error ? error.stack : String(error),
|
|
1904
|
-
)
|
|
1905
|
-
}
|
|
1906
|
-
})()
|
|
1907
|
-
|
|
1908
|
-
// Background: OpenCode init + slash command registration (non-blocking)
|
|
1909
|
-
void backgroundInit({
|
|
1910
|
-
currentDir,
|
|
1911
|
-
token,
|
|
1912
|
-
appId,
|
|
1913
|
-
guildIds: guilds.map((guild) => {
|
|
1914
|
-
return guild.id
|
|
1915
|
-
}),
|
|
1916
|
-
})
|
|
1917
|
-
} else {
|
|
1918
|
-
// ── Channel setup flow ──
|
|
1919
|
-
// Store channel-directory mappings discovered during Discord login.
|
|
1920
|
-
await storeChannelDirectories({ kimakiChannels })
|
|
1921
|
-
|
|
1922
|
-
if (!hasConfiguredTextChannels) {
|
|
1923
|
-
note(
|
|
1924
|
-
'No Kimaki project channels are configured yet. Opening project/channel setup.',
|
|
1925
|
-
'Channel Setup',
|
|
1926
|
-
)
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
if (kimakiChannels.length > 0) {
|
|
1930
|
-
const channelList = kimakiChannels
|
|
1931
|
-
.flatMap(({ guild, channels }) =>
|
|
1932
|
-
channels.map((ch) => {
|
|
1933
|
-
return `#${ch.name} in ${guild.name}: ${ch.kimakiDirectory}`
|
|
1934
|
-
}),
|
|
1935
|
-
)
|
|
1936
|
-
.join('\n')
|
|
1937
|
-
|
|
1938
|
-
note(channelList, 'Existing Kimaki Channels')
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
// Wait for OpenCode, fetch projects, show prompts, create channels if needed
|
|
1942
|
-
cliLogger.log('Waiting for OpenCode server...')
|
|
1943
|
-
const getClient = await opencodePromise
|
|
1944
|
-
|
|
1945
|
-
cliLogger.log('Fetching OpenCode data...')
|
|
1946
|
-
|
|
1947
|
-
// Fetch projects, commands, and agents in parallel
|
|
1948
|
-
const [projects, allUserCommands, allAgents] = await Promise.all([
|
|
1949
|
-
getClient()
|
|
1950
|
-
.project.list()
|
|
1951
|
-
.then((r) => r.data || [])
|
|
1952
|
-
.catch((error) => {
|
|
1953
|
-
cliLogger.log('Failed to fetch projects')
|
|
1954
|
-
cliLogger.error(
|
|
1955
|
-
'Error:',
|
|
1956
|
-
error instanceof Error ? error.stack : String(error),
|
|
1957
|
-
)
|
|
1958
|
-
void discordClient.destroy()
|
|
1959
|
-
process.exit(EXIT_NO_RESTART)
|
|
1960
|
-
}),
|
|
1961
|
-
getClient()
|
|
1962
|
-
.command.list({ directory: currentDir })
|
|
1963
|
-
.then((r) => r.data || [])
|
|
1964
|
-
.catch((error) => {
|
|
1965
|
-
cliLogger.warn(
|
|
1966
|
-
'Failed to load user commands during setup:',
|
|
1967
|
-
error instanceof Error ? error.stack : String(error),
|
|
1968
|
-
)
|
|
1969
|
-
return []
|
|
1970
|
-
}),
|
|
1971
|
-
getClient()
|
|
1972
|
-
.app.agents({ directory: currentDir })
|
|
1973
|
-
.then((r) => r.data || [])
|
|
1974
|
-
.catch((error) => {
|
|
1975
|
-
cliLogger.warn(
|
|
1976
|
-
'Failed to load agents during setup:',
|
|
1977
|
-
error instanceof Error ? error.stack : String(error),
|
|
1978
|
-
)
|
|
1979
|
-
return []
|
|
1980
|
-
}),
|
|
1981
|
-
])
|
|
1982
|
-
|
|
1983
|
-
cliLogger.log(`Found ${projects.length} OpenCode project(s)`)
|
|
1984
|
-
|
|
1985
|
-
const existingDirs = kimakiChannels.flatMap(({ channels }) =>
|
|
1986
|
-
channels
|
|
1987
|
-
.filter((ch) => ch.kimakiDirectory)
|
|
1988
|
-
.map((ch) => ch.kimakiDirectory)
|
|
1989
|
-
.filter(Boolean),
|
|
1990
|
-
)
|
|
1991
|
-
|
|
1992
|
-
const availableProjects = deduplicateByKey(
|
|
1993
|
-
projects.filter((project) => {
|
|
1994
|
-
if (existingDirs.includes(project.worktree)) {
|
|
1995
|
-
return false
|
|
1996
|
-
}
|
|
1997
|
-
if (path.basename(project.worktree).startsWith('opencode-test-')) {
|
|
1998
|
-
return false
|
|
1999
|
-
}
|
|
2000
|
-
return true
|
|
2001
|
-
}),
|
|
2002
|
-
(x) => x.worktree,
|
|
2003
|
-
)
|
|
2004
|
-
|
|
2005
|
-
if (availableProjects.length === 0) {
|
|
2006
|
-
note(
|
|
2007
|
-
'All OpenCode projects already have Discord channels',
|
|
2008
|
-
'No New Projects',
|
|
2009
|
-
)
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
if (availableProjects.length > 0) {
|
|
2013
|
-
if (!canUseInteractivePrompts()) {
|
|
2014
|
-
exitNonInteractiveSetup()
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
const selectedProjects = await multiselect({
|
|
2018
|
-
message: 'Select projects to create Discord channels for:',
|
|
2019
|
-
options: availableProjects.map((project) => ({
|
|
2020
|
-
value: project.id,
|
|
2021
|
-
label: `${path.basename(project.worktree)} (${abbreviatePath(project.worktree)})`,
|
|
2022
|
-
})),
|
|
2023
|
-
required: false,
|
|
2024
|
-
})
|
|
2025
|
-
|
|
2026
|
-
if (!isCancel(selectedProjects) && selectedProjects.length > 0) {
|
|
2027
|
-
let targetGuild: Guild
|
|
2028
|
-
if (guilds.length === 0) {
|
|
2029
|
-
cliLogger.error(
|
|
2030
|
-
'No Discord servers found! The bot must be installed in at least one server.',
|
|
2031
|
-
)
|
|
2032
|
-
process.exit(EXIT_NO_RESTART)
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
|
-
if (guilds.length === 1) {
|
|
2036
|
-
targetGuild = guilds[0]!
|
|
2037
|
-
note(`Using server: ${targetGuild.name}`, 'Server Selected')
|
|
2038
|
-
} else {
|
|
2039
|
-
const guildSelection = await multiselect({
|
|
2040
|
-
message: 'Select a Discord server to create channels in:',
|
|
2041
|
-
options: guilds.map((guild) => ({
|
|
2042
|
-
value: guild.id,
|
|
2043
|
-
label: `${guild.name} (${guild.memberCount} members)`,
|
|
2044
|
-
})),
|
|
2045
|
-
required: true,
|
|
2046
|
-
maxItems: 1,
|
|
2047
|
-
})
|
|
2048
|
-
|
|
2049
|
-
if (isCancel(guildSelection)) {
|
|
2050
|
-
cancel('Setup cancelled')
|
|
2051
|
-
process.exit(0)
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
targetGuild = guilds.find((g) => g.id === guildSelection[0])!
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
cliLogger.log('Creating Discord channels...')
|
|
2058
|
-
|
|
2059
|
-
for (const projectId of selectedProjects) {
|
|
2060
|
-
const project = projects.find((p) => p.id === projectId)
|
|
2061
|
-
if (!project) continue
|
|
2062
|
-
|
|
2063
|
-
try {
|
|
2064
|
-
const { textChannelId, channelName } = await createProjectChannels({
|
|
2065
|
-
guild: targetGuild,
|
|
2066
|
-
projectDirectory: project.worktree,
|
|
2067
|
-
botName: discordClient.user?.username,
|
|
2068
|
-
enableVoiceChannels,
|
|
2069
|
-
analyticsSource: 'onboarding',
|
|
2070
|
-
})
|
|
2071
|
-
|
|
2072
|
-
createdChannels.push({
|
|
2073
|
-
name: channelName,
|
|
2074
|
-
id: textChannelId,
|
|
2075
|
-
guildId: targetGuild.id,
|
|
2076
|
-
})
|
|
2077
|
-
} catch (error) {
|
|
2078
|
-
cliLogger.error(
|
|
2079
|
-
`Failed to create channels for ${path.basename(project.worktree)}:`,
|
|
2080
|
-
error,
|
|
2081
|
-
)
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
cliLogger.log(`Created ${createdChannels.length} channel(s)`)
|
|
2086
|
-
|
|
2087
|
-
if (createdChannels.length > 0) {
|
|
2088
|
-
note(
|
|
2089
|
-
createdChannels.map((ch) => `#${ch.name}`).join('\n'),
|
|
2090
|
-
'Created Channels',
|
|
2091
|
-
)
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
// Create default kimaki channel for general-purpose tasks.
|
|
2097
|
-
// Only locally configured or gateway-authorized guilds are eligible.
|
|
2098
|
-
const defaultChannelResults = await ensureDefaultChannelsWithWelcome({
|
|
2099
|
-
guilds,
|
|
2100
|
-
discordClient,
|
|
2101
|
-
appId,
|
|
2102
|
-
isGatewayMode,
|
|
2103
|
-
installerDiscordUserId,
|
|
2104
|
-
})
|
|
2105
|
-
createdChannels.push(...defaultChannelResults)
|
|
2106
|
-
|
|
2107
|
-
// Log available user commands
|
|
2108
|
-
const registrableCommands = allUserCommands.filter(
|
|
2109
|
-
(cmd) => !SKIP_USER_COMMANDS.includes(cmd.name),
|
|
2110
|
-
)
|
|
2111
|
-
|
|
2112
|
-
if (registrableCommands.length > 0) {
|
|
2113
|
-
note(
|
|
2114
|
-
`Found ${registrableCommands.length} user-defined command(s)`,
|
|
2115
|
-
'OpenCode Commands/Skills',
|
|
2116
|
-
)
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
cliLogger.log('Registering slash commands asynchronously...')
|
|
2120
|
-
void registerCommands({
|
|
2121
|
-
token,
|
|
2122
|
-
appId,
|
|
2123
|
-
guildIds: guilds.map((guild) => {
|
|
2124
|
-
return guild.id
|
|
2125
|
-
}),
|
|
2126
|
-
userCommands: allUserCommands,
|
|
2127
|
-
agents: allAgents,
|
|
2128
|
-
})
|
|
2129
|
-
.then(() => {
|
|
2130
|
-
cliLogger.log('Slash commands registered!')
|
|
2131
|
-
})
|
|
2132
|
-
.catch((error) => {
|
|
2133
|
-
cliLogger.error(
|
|
2134
|
-
'Failed to register slash commands:',
|
|
2135
|
-
error instanceof Error ? error.stack : String(error),
|
|
2136
|
-
)
|
|
2137
|
-
})
|
|
2138
|
-
|
|
2139
|
-
// Start bot after channel setup is complete so it doesn't handle
|
|
2140
|
-
// messages/interactions while the user is still going through prompts.
|
|
2141
|
-
cliLogger.log('Starting Discord bot...')
|
|
2142
|
-
await startDiscordBot({ token, appId, discordClient, useWorktrees })
|
|
2143
|
-
cliLogger.log('Discord bot is running!')
|
|
2144
|
-
}
|
|
2145
|
-
|
|
2146
|
-
// ── Ready ──
|
|
2147
|
-
if (!canUseInteractivePrompts()) {
|
|
2148
|
-
emitJsonEvent({
|
|
2149
|
-
type: 'ready',
|
|
2150
|
-
app_id: appId,
|
|
2151
|
-
guild_ids: guilds.map((g) => { return g.id }),
|
|
2152
|
-
})
|
|
2153
|
-
} else {
|
|
2154
|
-
showReadyMessage({ kimakiChannels, createdChannels })
|
|
2155
|
-
outro('✨ Bot ready! Listening for messages...')
|
|
2156
|
-
}
|
|
2157
|
-
}
|