@brianli/kimaki 0.4.72-brianli.1

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.
Files changed (328) hide show
  1. package/bin.js +2 -0
  2. package/dist/ai-tool-to-genai.js +233 -0
  3. package/dist/ai-tool-to-genai.test.js +267 -0
  4. package/dist/ai-tool.js +6 -0
  5. package/dist/bin.js +87 -0
  6. package/dist/bot-token.js +121 -0
  7. package/dist/bot-token.test.js +134 -0
  8. package/dist/channel-management.js +101 -0
  9. package/dist/cli-parsing.test.js +89 -0
  10. package/dist/cli.js +2529 -0
  11. package/dist/commands/abort.js +82 -0
  12. package/dist/commands/action-buttons.js +257 -0
  13. package/dist/commands/add-project.js +114 -0
  14. package/dist/commands/agent.js +291 -0
  15. package/dist/commands/ask-question.js +223 -0
  16. package/dist/commands/compact.js +120 -0
  17. package/dist/commands/context-usage.js +140 -0
  18. package/dist/commands/create-new-project.js +118 -0
  19. package/dist/commands/diff.js +128 -0
  20. package/dist/commands/file-upload.js +275 -0
  21. package/dist/commands/fork.js +217 -0
  22. package/dist/commands/gemini-apikey.js +70 -0
  23. package/dist/commands/login.js +490 -0
  24. package/dist/commands/mention-mode.js +51 -0
  25. package/dist/commands/merge-worktree.js +124 -0
  26. package/dist/commands/model.js +694 -0
  27. package/dist/commands/permissions.js +163 -0
  28. package/dist/commands/queue.js +217 -0
  29. package/dist/commands/remove-project.js +115 -0
  30. package/dist/commands/restart-opencode-server.js +116 -0
  31. package/dist/commands/resume.js +159 -0
  32. package/dist/commands/run-command.js +79 -0
  33. package/dist/commands/session-id.js +78 -0
  34. package/dist/commands/session.js +192 -0
  35. package/dist/commands/share.js +80 -0
  36. package/dist/commands/types.js +2 -0
  37. package/dist/commands/undo-redo.js +159 -0
  38. package/dist/commands/unset-model.js +152 -0
  39. package/dist/commands/upgrade.js +42 -0
  40. package/dist/commands/user-command.js +148 -0
  41. package/dist/commands/verbosity.js +60 -0
  42. package/dist/commands/worktree-settings.js +50 -0
  43. package/dist/commands/worktree.js +299 -0
  44. package/dist/condense-memory.js +33 -0
  45. package/dist/config.js +110 -0
  46. package/dist/database.js +1050 -0
  47. package/dist/db.js +159 -0
  48. package/dist/db.test.js +49 -0
  49. package/dist/discord-api.js +28 -0
  50. package/dist/discord-auth.js +231 -0
  51. package/dist/discord-auth.test.js +80 -0
  52. package/dist/discord-bot.js +997 -0
  53. package/dist/discord-utils.js +560 -0
  54. package/dist/discord-utils.test.js +115 -0
  55. package/dist/errors.js +167 -0
  56. package/dist/escape-backticks.test.js +429 -0
  57. package/dist/format-tables.js +122 -0
  58. package/dist/format-tables.test.js +199 -0
  59. package/dist/forum-sync/config.js +79 -0
  60. package/dist/forum-sync/discord-operations.js +154 -0
  61. package/dist/forum-sync/index.js +5 -0
  62. package/dist/forum-sync/markdown.js +117 -0
  63. package/dist/forum-sync/sync-to-discord.js +417 -0
  64. package/dist/forum-sync/sync-to-files.js +190 -0
  65. package/dist/forum-sync/types.js +53 -0
  66. package/dist/forum-sync/watchers.js +307 -0
  67. package/dist/gateway-consumer.js +232 -0
  68. package/dist/gateway-consumer.test.js +18 -0
  69. package/dist/genai-worker-wrapper.js +111 -0
  70. package/dist/genai-worker.js +311 -0
  71. package/dist/genai.js +232 -0
  72. package/dist/generated/browser.js +17 -0
  73. package/dist/generated/client.js +35 -0
  74. package/dist/generated/commonInputTypes.js +10 -0
  75. package/dist/generated/enums.js +30 -0
  76. package/dist/generated/internal/class.js +41 -0
  77. package/dist/generated/internal/prismaNamespace.js +239 -0
  78. package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
  79. package/dist/generated/models/bot_api_keys.js +1 -0
  80. package/dist/generated/models/bot_tokens.js +1 -0
  81. package/dist/generated/models/channel_agents.js +1 -0
  82. package/dist/generated/models/channel_directories.js +1 -0
  83. package/dist/generated/models/channel_mention_mode.js +1 -0
  84. package/dist/generated/models/channel_models.js +1 -0
  85. package/dist/generated/models/channel_verbosity.js +1 -0
  86. package/dist/generated/models/channel_worktrees.js +1 -0
  87. package/dist/generated/models/forum_sync_configs.js +1 -0
  88. package/dist/generated/models/global_models.js +1 -0
  89. package/dist/generated/models/ipc_requests.js +1 -0
  90. package/dist/generated/models/part_messages.js +1 -0
  91. package/dist/generated/models/scheduled_tasks.js +1 -0
  92. package/dist/generated/models/session_agents.js +1 -0
  93. package/dist/generated/models/session_models.js +1 -0
  94. package/dist/generated/models/session_start_sources.js +1 -0
  95. package/dist/generated/models/thread_sessions.js +1 -0
  96. package/dist/generated/models/thread_worktrees.js +1 -0
  97. package/dist/generated/models.js +1 -0
  98. package/dist/heap-monitor.js +95 -0
  99. package/dist/hrana-server.js +416 -0
  100. package/dist/hrana-server.test.js +368 -0
  101. package/dist/image-utils.js +112 -0
  102. package/dist/interaction-handler.js +327 -0
  103. package/dist/ipc-polling.js +251 -0
  104. package/dist/kimaki-digital-twin.e2e.test.js +165 -0
  105. package/dist/limit-heading-depth.js +25 -0
  106. package/dist/limit-heading-depth.test.js +105 -0
  107. package/dist/logger.js +160 -0
  108. package/dist/markdown.js +342 -0
  109. package/dist/markdown.test.js +253 -0
  110. package/dist/message-formatting.js +433 -0
  111. package/dist/message-formatting.test.js +73 -0
  112. package/dist/openai-realtime.js +228 -0
  113. package/dist/opencode-plugin-loading.e2e.test.js +91 -0
  114. package/dist/opencode-plugin.js +536 -0
  115. package/dist/opencode-plugin.test.js +98 -0
  116. package/dist/opencode.js +409 -0
  117. package/dist/privacy-sanitizer.js +105 -0
  118. package/dist/runtime-mode.js +51 -0
  119. package/dist/runtime-mode.test.js +115 -0
  120. package/dist/sentry.js +127 -0
  121. package/dist/session-handler/state.js +151 -0
  122. package/dist/session-handler.js +1874 -0
  123. package/dist/session-search.js +100 -0
  124. package/dist/session-search.test.js +40 -0
  125. package/dist/startup-service.js +153 -0
  126. package/dist/system-message.js +499 -0
  127. package/dist/task-runner.js +282 -0
  128. package/dist/task-schedule.js +191 -0
  129. package/dist/task-schedule.test.js +71 -0
  130. package/dist/thinking-utils.js +35 -0
  131. package/dist/thread-message-queue.e2e.test.js +781 -0
  132. package/dist/tools.js +359 -0
  133. package/dist/unnest-code-blocks.js +136 -0
  134. package/dist/unnest-code-blocks.test.js +641 -0
  135. package/dist/upgrade.js +114 -0
  136. package/dist/utils.js +109 -0
  137. package/dist/voice-handler.js +606 -0
  138. package/dist/voice.js +304 -0
  139. package/dist/voice.test.js +187 -0
  140. package/dist/wait-session.js +94 -0
  141. package/dist/worker-types.js +4 -0
  142. package/dist/worktree-utils.js +727 -0
  143. package/dist/xml.js +92 -0
  144. package/dist/xml.test.js +32 -0
  145. package/package.json +82 -0
  146. package/schema.prisma +246 -0
  147. package/skills/batch/SKILL.md +87 -0
  148. package/skills/critique/SKILL.md +129 -0
  149. package/skills/errore/SKILL.md +589 -0
  150. package/skills/goke/.prettierrc +5 -0
  151. package/skills/goke/CHANGELOG.md +40 -0
  152. package/skills/goke/LICENSE +21 -0
  153. package/skills/goke/README.md +666 -0
  154. package/skills/goke/SKILL.md +458 -0
  155. package/skills/goke/package.json +43 -0
  156. package/skills/goke/src/__test__/coerce.test.ts +411 -0
  157. package/skills/goke/src/__test__/index.test.ts +1798 -0
  158. package/skills/goke/src/__test__/types.test-d.ts +111 -0
  159. package/skills/goke/src/coerce.ts +547 -0
  160. package/skills/goke/src/goke.ts +1362 -0
  161. package/skills/goke/src/index.ts +16 -0
  162. package/skills/goke/src/mri.ts +164 -0
  163. package/skills/goke/tsconfig.json +15 -0
  164. package/skills/jitter/EDITOR.md +219 -0
  165. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  166. package/skills/jitter/SKILL.md +158 -0
  167. package/skills/jitter/jitter-clipboard.json +1042 -0
  168. package/skills/jitter/package.json +14 -0
  169. package/skills/jitter/tsconfig.json +15 -0
  170. package/skills/jitter/utils/actions.ts +212 -0
  171. package/skills/jitter/utils/export.ts +114 -0
  172. package/skills/jitter/utils/index.ts +141 -0
  173. package/skills/jitter/utils/snapshot.ts +154 -0
  174. package/skills/jitter/utils/traverse.ts +246 -0
  175. package/skills/jitter/utils/types.ts +279 -0
  176. package/skills/jitter/utils/wait.ts +133 -0
  177. package/skills/playwriter/SKILL.md +31 -0
  178. package/skills/security-review/SKILL.md +208 -0
  179. package/skills/simplify/SKILL.md +58 -0
  180. package/skills/termcast/SKILL.md +945 -0
  181. package/skills/tuistory/SKILL.md +250 -0
  182. package/skills/zustand-centralized-state/SKILL.md +582 -0
  183. package/src/__snapshots__/compact-session-context-no-system.md +35 -0
  184. package/src/__snapshots__/compact-session-context.md +41 -0
  185. package/src/__snapshots__/first-session-no-info.md +17 -0
  186. package/src/__snapshots__/first-session-with-info.md +23 -0
  187. package/src/__snapshots__/session-1.md +17 -0
  188. package/src/__snapshots__/session-2.md +5871 -0
  189. package/src/__snapshots__/session-3.md +17 -0
  190. package/src/__snapshots__/session-with-tools.md +5871 -0
  191. package/src/ai-tool-to-genai.test.ts +296 -0
  192. package/src/ai-tool-to-genai.ts +282 -0
  193. package/src/ai-tool.ts +39 -0
  194. package/src/bin.ts +108 -0
  195. package/src/bot-token.test.ts +171 -0
  196. package/src/bot-token.ts +159 -0
  197. package/src/channel-management.ts +172 -0
  198. package/src/cli-parsing.test.ts +132 -0
  199. package/src/cli.ts +3605 -0
  200. package/src/commands/abort.ts +112 -0
  201. package/src/commands/action-buttons.ts +376 -0
  202. package/src/commands/add-project.ts +152 -0
  203. package/src/commands/agent.ts +404 -0
  204. package/src/commands/ask-question.ts +330 -0
  205. package/src/commands/compact.ts +157 -0
  206. package/src/commands/context-usage.ts +199 -0
  207. package/src/commands/create-new-project.ts +179 -0
  208. package/src/commands/diff.ts +165 -0
  209. package/src/commands/file-upload.ts +389 -0
  210. package/src/commands/fork.ts +320 -0
  211. package/src/commands/gemini-apikey.ts +104 -0
  212. package/src/commands/login.ts +634 -0
  213. package/src/commands/mention-mode.ts +77 -0
  214. package/src/commands/merge-worktree.ts +177 -0
  215. package/src/commands/model.ts +961 -0
  216. package/src/commands/permissions.ts +261 -0
  217. package/src/commands/queue.ts +296 -0
  218. package/src/commands/remove-project.ts +155 -0
  219. package/src/commands/restart-opencode-server.ts +162 -0
  220. package/src/commands/resume.ts +242 -0
  221. package/src/commands/run-command.ts +123 -0
  222. package/src/commands/session-id.ts +109 -0
  223. package/src/commands/session.ts +250 -0
  224. package/src/commands/share.ts +106 -0
  225. package/src/commands/types.ts +25 -0
  226. package/src/commands/undo-redo.ts +221 -0
  227. package/src/commands/unset-model.ts +189 -0
  228. package/src/commands/upgrade.ts +52 -0
  229. package/src/commands/user-command.ts +193 -0
  230. package/src/commands/verbosity.ts +88 -0
  231. package/src/commands/worktree-settings.ts +79 -0
  232. package/src/commands/worktree.ts +431 -0
  233. package/src/condense-memory.ts +36 -0
  234. package/src/config.ts +148 -0
  235. package/src/database.ts +1530 -0
  236. package/src/db.test.ts +60 -0
  237. package/src/db.ts +190 -0
  238. package/src/discord-api.ts +35 -0
  239. package/src/discord-bot.ts +1316 -0
  240. package/src/discord-utils.test.ts +132 -0
  241. package/src/discord-utils.ts +767 -0
  242. package/src/errors.ts +213 -0
  243. package/src/escape-backticks.test.ts +469 -0
  244. package/src/format-tables.test.ts +223 -0
  245. package/src/format-tables.ts +145 -0
  246. package/src/forum-sync/config.ts +92 -0
  247. package/src/forum-sync/discord-operations.ts +241 -0
  248. package/src/forum-sync/index.ts +9 -0
  249. package/src/forum-sync/markdown.ts +176 -0
  250. package/src/forum-sync/sync-to-discord.ts +595 -0
  251. package/src/forum-sync/sync-to-files.ts +294 -0
  252. package/src/forum-sync/types.ts +175 -0
  253. package/src/forum-sync/watchers.ts +454 -0
  254. package/src/genai-worker-wrapper.ts +164 -0
  255. package/src/genai-worker.ts +386 -0
  256. package/src/genai.ts +321 -0
  257. package/src/generated/browser.ts +109 -0
  258. package/src/generated/client.ts +131 -0
  259. package/src/generated/commonInputTypes.ts +512 -0
  260. package/src/generated/enums.ts +46 -0
  261. package/src/generated/internal/class.ts +362 -0
  262. package/src/generated/internal/prismaNamespace.ts +2251 -0
  263. package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
  264. package/src/generated/models/bot_api_keys.ts +1288 -0
  265. package/src/generated/models/bot_tokens.ts +1577 -0
  266. package/src/generated/models/channel_agents.ts +1256 -0
  267. package/src/generated/models/channel_directories.ts +2104 -0
  268. package/src/generated/models/channel_mention_mode.ts +1300 -0
  269. package/src/generated/models/channel_models.ts +1288 -0
  270. package/src/generated/models/channel_verbosity.ts +1224 -0
  271. package/src/generated/models/channel_worktrees.ts +1308 -0
  272. package/src/generated/models/forum_sync_configs.ts +1452 -0
  273. package/src/generated/models/global_models.ts +1288 -0
  274. package/src/generated/models/ipc_requests.ts +1485 -0
  275. package/src/generated/models/part_messages.ts +1302 -0
  276. package/src/generated/models/scheduled_tasks.ts +2320 -0
  277. package/src/generated/models/session_agents.ts +1086 -0
  278. package/src/generated/models/session_models.ts +1114 -0
  279. package/src/generated/models/session_start_sources.ts +1408 -0
  280. package/src/generated/models/thread_sessions.ts +1599 -0
  281. package/src/generated/models/thread_worktrees.ts +1352 -0
  282. package/src/generated/models.ts +29 -0
  283. package/src/heap-monitor.ts +121 -0
  284. package/src/hrana-server.test.ts +428 -0
  285. package/src/hrana-server.ts +547 -0
  286. package/src/image-utils.ts +149 -0
  287. package/src/interaction-handler.ts +461 -0
  288. package/src/ipc-polling.ts +325 -0
  289. package/src/kimaki-digital-twin.e2e.test.ts +201 -0
  290. package/src/limit-heading-depth.test.ts +116 -0
  291. package/src/limit-heading-depth.ts +26 -0
  292. package/src/logger.ts +203 -0
  293. package/src/markdown.test.ts +360 -0
  294. package/src/markdown.ts +410 -0
  295. package/src/message-formatting.test.ts +81 -0
  296. package/src/message-formatting.ts +549 -0
  297. package/src/openai-realtime.ts +362 -0
  298. package/src/opencode-plugin-loading.e2e.test.ts +112 -0
  299. package/src/opencode-plugin.test.ts +108 -0
  300. package/src/opencode-plugin.ts +652 -0
  301. package/src/opencode.ts +554 -0
  302. package/src/privacy-sanitizer.ts +142 -0
  303. package/src/schema.sql +158 -0
  304. package/src/sentry.ts +137 -0
  305. package/src/session-handler/state.ts +232 -0
  306. package/src/session-handler.ts +2668 -0
  307. package/src/session-search.test.ts +50 -0
  308. package/src/session-search.ts +148 -0
  309. package/src/startup-service.ts +200 -0
  310. package/src/system-message.ts +568 -0
  311. package/src/task-runner.ts +425 -0
  312. package/src/task-schedule.test.ts +84 -0
  313. package/src/task-schedule.ts +287 -0
  314. package/src/thinking-utils.ts +61 -0
  315. package/src/thread-message-queue.e2e.test.ts +997 -0
  316. package/src/tools.ts +432 -0
  317. package/src/unnest-code-blocks.test.ts +679 -0
  318. package/src/unnest-code-blocks.ts +168 -0
  319. package/src/upgrade.ts +127 -0
  320. package/src/utils.ts +145 -0
  321. package/src/voice-handler.ts +852 -0
  322. package/src/voice.test.ts +219 -0
  323. package/src/voice.ts +444 -0
  324. package/src/wait-session.ts +147 -0
  325. package/src/worker-types.ts +64 -0
  326. package/src/worktree-utils.ts +988 -0
  327. package/src/xml.test.ts +38 -0
  328. package/src/xml.ts +121 -0
@@ -0,0 +1,634 @@
1
+ // /login command - Authenticate with AI providers (OAuth or API key).
2
+ // Supports GitHub Copilot (device flow), OpenAI Codex (device flow), and API keys.
3
+
4
+ import {
5
+ ChatInputCommandInteraction,
6
+ StringSelectMenuInteraction,
7
+ StringSelectMenuBuilder,
8
+ ActionRowBuilder,
9
+ ModalBuilder,
10
+ TextInputBuilder,
11
+ TextInputStyle,
12
+ ModalSubmitInteraction,
13
+ ChannelType,
14
+ type ThreadChannel,
15
+ type TextChannel,
16
+ MessageFlags,
17
+ } from 'discord.js'
18
+ import crypto from 'node:crypto'
19
+ import { initializeOpencodeForDirectory } from '../opencode.js'
20
+ import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
21
+ import { createLogger, LogPrefix } from '../logger.js'
22
+
23
+ const loginLogger = createLogger(LogPrefix.LOGIN)
24
+
25
+ // Store context by hash to avoid customId length limits (Discord max: 100 chars)
26
+ const pendingLoginContexts = new Map<
27
+ string,
28
+ {
29
+ dir: string
30
+ channelId: string
31
+ providerId?: string
32
+ providerName?: string
33
+ methodIndex?: number
34
+ methodType?: 'oauth' | 'api'
35
+ methodLabel?: string
36
+ }
37
+ >()
38
+
39
+ export type ProviderAuthMethod = {
40
+ type: 'oauth' | 'api'
41
+ label: string
42
+ }
43
+
44
+ /**
45
+ * Handle the /login slash command.
46
+ * Shows a select menu with available providers.
47
+ */
48
+ export async function handleLoginCommand({
49
+ interaction,
50
+ appId,
51
+ }: {
52
+ interaction: ChatInputCommandInteraction
53
+ appId: string
54
+ }): Promise<void> {
55
+ loginLogger.log('[LOGIN] handleLoginCommand called')
56
+
57
+ // Defer reply immediately to avoid 3-second timeout
58
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral })
59
+ loginLogger.log('[LOGIN] Deferred reply')
60
+
61
+ const channel = interaction.channel
62
+
63
+ if (!channel) {
64
+ await interaction.editReply({
65
+ content: 'This command can only be used in a channel',
66
+ })
67
+ return
68
+ }
69
+
70
+ // Determine if we're in a thread or text channel
71
+ const isThread = [
72
+ ChannelType.PublicThread,
73
+ ChannelType.PrivateThread,
74
+ ChannelType.AnnouncementThread,
75
+ ].includes(channel.type)
76
+
77
+ let projectDirectory: string | undefined
78
+ let channelAppId: string | undefined
79
+ let targetChannelId: string
80
+
81
+ if (isThread) {
82
+ const thread = channel as ThreadChannel
83
+ const textChannel = await resolveTextChannel(thread)
84
+ const metadata = await getKimakiMetadata(textChannel)
85
+ projectDirectory = metadata.projectDirectory
86
+ channelAppId = metadata.channelAppId
87
+ targetChannelId = textChannel?.id || channel.id
88
+ } else if (channel.type === ChannelType.GuildText) {
89
+ const textChannel = channel as TextChannel
90
+ const metadata = await getKimakiMetadata(textChannel)
91
+ projectDirectory = metadata.projectDirectory
92
+ channelAppId = metadata.channelAppId
93
+ targetChannelId = channel.id
94
+ } else {
95
+ await interaction.editReply({
96
+ content: 'This command can only be used in text channels or threads',
97
+ })
98
+ return
99
+ }
100
+
101
+ if (channelAppId && channelAppId !== appId) {
102
+ await interaction.editReply({
103
+ content: 'This channel is not configured for this bot',
104
+ })
105
+ return
106
+ }
107
+
108
+ if (!projectDirectory) {
109
+ await interaction.editReply({
110
+ content: 'This channel is not configured with a project directory',
111
+ })
112
+ return
113
+ }
114
+
115
+ try {
116
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
117
+ if (getClient instanceof Error) {
118
+ await interaction.editReply({ content: getClient.message })
119
+ return
120
+ }
121
+
122
+ const providersResponse = await getClient().provider.list({
123
+ directory: projectDirectory,
124
+ })
125
+
126
+ if (!providersResponse.data) {
127
+ await interaction.editReply({
128
+ content: 'Failed to fetch providers',
129
+ })
130
+ return
131
+ }
132
+
133
+ const { all: allProviders, connected } = providersResponse.data
134
+
135
+ if (allProviders.length === 0) {
136
+ await interaction.editReply({
137
+ content: 'No providers available.',
138
+ })
139
+ return
140
+ }
141
+
142
+ // Store context with a short hash key to avoid customId length limits
143
+ const context = {
144
+ dir: projectDirectory,
145
+ channelId: targetChannelId,
146
+ }
147
+ const contextHash = crypto.randomBytes(8).toString('hex')
148
+ pendingLoginContexts.set(contextHash, context)
149
+
150
+ const options = [...allProviders]
151
+ .sort((a, b) => a.name.localeCompare(b.name))
152
+ .slice(0, 25)
153
+ .map((provider) => {
154
+ const isConnected = connected.includes(provider.id)
155
+ return {
156
+ label: `${provider.name}${isConnected ? ' ✓' : ''}`.slice(0, 100),
157
+ value: provider.id,
158
+ description: isConnected
159
+ ? 'Connected - select to re-authenticate'
160
+ : 'Not connected',
161
+ }
162
+ })
163
+
164
+ const selectMenu = new StringSelectMenuBuilder()
165
+ .setCustomId(`login_provider:${contextHash}`)
166
+ .setPlaceholder('Select a provider to authenticate')
167
+ .addOptions(options)
168
+
169
+ const actionRow =
170
+ new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
171
+
172
+ await interaction.editReply({
173
+ content: '**Authenticate with Provider**\nSelect a provider:',
174
+ components: [actionRow],
175
+ })
176
+ } catch (error) {
177
+ loginLogger.error('Error loading providers:', error)
178
+ await interaction.editReply({
179
+ content: `Failed to load providers: ${error instanceof Error ? error.message : 'Unknown error'}`,
180
+ })
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Handle the provider select menu interaction.
186
+ * Shows a second select menu with auth methods for the chosen provider.
187
+ */
188
+ export async function handleLoginProviderSelectMenu(
189
+ interaction: StringSelectMenuInteraction,
190
+ ): Promise<void> {
191
+ const customId = interaction.customId
192
+
193
+ if (!customId.startsWith('login_provider:')) {
194
+ return
195
+ }
196
+
197
+ const contextHash = customId.replace('login_provider:', '')
198
+ const context = pendingLoginContexts.get(contextHash)
199
+
200
+ if (!context) {
201
+ await interaction.deferUpdate()
202
+ await interaction.editReply({
203
+ content: 'Selection expired. Please run /login again.',
204
+ components: [],
205
+ })
206
+ return
207
+ }
208
+
209
+ const selectedProviderId = interaction.values[0]
210
+ if (!selectedProviderId) {
211
+ await interaction.deferUpdate()
212
+ await interaction.editReply({
213
+ content: 'No provider selected',
214
+ components: [],
215
+ })
216
+ return
217
+ }
218
+
219
+ try {
220
+ const getClient = await initializeOpencodeForDirectory(context.dir)
221
+ if (getClient instanceof Error) {
222
+ await interaction.deferUpdate()
223
+ await interaction.editReply({
224
+ content: getClient.message,
225
+ components: [],
226
+ })
227
+ return
228
+ }
229
+
230
+ // Get provider info for display
231
+ const providersResponse = await getClient().provider.list({
232
+ directory: context.dir,
233
+ })
234
+
235
+ const provider = providersResponse.data?.all.find(
236
+ (p) => p.id === selectedProviderId,
237
+ )
238
+ const providerName = provider?.name || selectedProviderId
239
+
240
+ // Get auth methods for all providers
241
+ const authMethodsResponse = await getClient().provider.auth({
242
+ directory: context.dir,
243
+ })
244
+
245
+ if (!authMethodsResponse.data) {
246
+ await interaction.deferUpdate()
247
+ await interaction.editReply({
248
+ content: 'Failed to fetch authentication methods',
249
+ components: [],
250
+ })
251
+ return
252
+ }
253
+
254
+ // Get methods for this specific provider, default to API key if none defined
255
+ const methods: ProviderAuthMethod[] = authMethodsResponse.data[
256
+ selectedProviderId
257
+ ] || [{ type: 'api', label: 'API Key' }]
258
+
259
+ if (methods.length === 0) {
260
+ await interaction.deferUpdate()
261
+ await interaction.editReply({
262
+ content: `No authentication methods available for ${providerName}`,
263
+ components: [],
264
+ })
265
+ return
266
+ }
267
+
268
+ // Update context with provider info
269
+ context.providerId = selectedProviderId
270
+ context.providerName = providerName
271
+ pendingLoginContexts.set(contextHash, context)
272
+
273
+ // If only one method and it's API, show modal directly (no defer)
274
+ if (methods.length === 1 && methods[0]!.type === 'api') {
275
+ const method = methods[0]!
276
+ context.methodIndex = 0
277
+ context.methodType = method.type
278
+ context.methodLabel = method.label
279
+ pendingLoginContexts.set(contextHash, context)
280
+ await showApiKeyModal(interaction, contextHash, providerName)
281
+ return
282
+ }
283
+
284
+ // For OAuth or multiple methods, defer and continue
285
+ await interaction.deferUpdate()
286
+
287
+ // If only one method and it's OAuth, start flow directly
288
+ if (methods.length === 1) {
289
+ const method = methods[0]!
290
+ context.methodIndex = 0
291
+ context.methodType = method.type
292
+ context.methodLabel = method.label
293
+ pendingLoginContexts.set(contextHash, context)
294
+ await startOAuthFlow(interaction, context, contextHash)
295
+ return
296
+ }
297
+
298
+ // Multiple methods - show selection menu
299
+ const options = methods.slice(0, 25).map((method, index) => ({
300
+ label: method.label.slice(0, 100),
301
+ value: String(index),
302
+ description:
303
+ method.type === 'oauth'
304
+ ? 'OAuth authentication'
305
+ : 'Enter API key manually',
306
+ }))
307
+
308
+ const selectMenu = new StringSelectMenuBuilder()
309
+ .setCustomId(`login_method:${contextHash}`)
310
+ .setPlaceholder('Select authentication method')
311
+ .addOptions(options)
312
+
313
+ const actionRow =
314
+ new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
315
+
316
+ await interaction.editReply({
317
+ content: `**Authenticate with ${providerName}**\nSelect authentication method:`,
318
+ components: [actionRow],
319
+ })
320
+ } catch (error) {
321
+ loginLogger.error('Error loading auth methods:', error)
322
+ if (!interaction.deferred && !interaction.replied) {
323
+ await interaction.deferUpdate()
324
+ }
325
+ await interaction.editReply({
326
+ content: `Failed to load auth methods: ${error instanceof Error ? error.message : 'Unknown error'}`,
327
+ components: [],
328
+ })
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Handle the auth method select menu interaction.
334
+ * Starts OAuth flow or shows API key modal.
335
+ */
336
+ export async function handleLoginMethodSelectMenu(
337
+ interaction: StringSelectMenuInteraction,
338
+ ): Promise<void> {
339
+ const customId = interaction.customId
340
+
341
+ if (!customId.startsWith('login_method:')) {
342
+ return
343
+ }
344
+
345
+ const contextHash = customId.replace('login_method:', '')
346
+ const context = pendingLoginContexts.get(contextHash)
347
+
348
+ if (!context || !context.providerId || !context.providerName) {
349
+ await interaction.deferUpdate()
350
+ await interaction.editReply({
351
+ content: 'Selection expired. Please run /login again.',
352
+ components: [],
353
+ })
354
+ return
355
+ }
356
+
357
+ const selectedMethodIndex = parseInt(interaction.values[0] || '0', 10)
358
+
359
+ try {
360
+ const getClient = await initializeOpencodeForDirectory(context.dir)
361
+ if (getClient instanceof Error) {
362
+ await interaction.deferUpdate()
363
+ await interaction.editReply({
364
+ content: getClient.message,
365
+ components: [],
366
+ })
367
+ return
368
+ }
369
+
370
+ // Get auth methods again to get the selected one
371
+ const authMethodsResponse = await getClient().provider.auth({
372
+ directory: context.dir,
373
+ })
374
+
375
+ const methods: ProviderAuthMethod[] = authMethodsResponse.data?.[
376
+ context.providerId
377
+ ] || [{ type: 'api', label: 'API Key' }]
378
+
379
+ const selectedMethod = methods[selectedMethodIndex]
380
+ if (!selectedMethod) {
381
+ await interaction.deferUpdate()
382
+ await interaction.editReply({
383
+ content: 'Invalid method selected',
384
+ components: [],
385
+ })
386
+ return
387
+ }
388
+
389
+ // Update context
390
+ context.methodIndex = selectedMethodIndex
391
+ context.methodType = selectedMethod.type
392
+ context.methodLabel = selectedMethod.label
393
+ pendingLoginContexts.set(contextHash, context)
394
+
395
+ if (selectedMethod.type === 'api') {
396
+ // Show API key modal (don't defer for modals)
397
+ await showApiKeyModal(interaction, contextHash, context.providerName)
398
+ } else {
399
+ // Start OAuth flow
400
+ await interaction.deferUpdate()
401
+ await startOAuthFlow(interaction, context, contextHash)
402
+ }
403
+ } catch (error) {
404
+ loginLogger.error('Error processing auth method:', error)
405
+ try {
406
+ if (!interaction.deferred && !interaction.replied) {
407
+ await interaction.deferUpdate()
408
+ }
409
+ await interaction.editReply({
410
+ content: `Failed to process auth method: ${error instanceof Error ? error.message : 'Unknown error'}`,
411
+ components: [],
412
+ })
413
+ } catch {
414
+ // Ignore follow-up errors
415
+ }
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Show API key input modal.
421
+ */
422
+ async function showApiKeyModal(
423
+ interaction: StringSelectMenuInteraction,
424
+ contextHash: string,
425
+ providerName: string,
426
+ ): Promise<void> {
427
+ const modal = new ModalBuilder()
428
+ .setCustomId(`login_apikey:${contextHash}`)
429
+ .setTitle(`${providerName} API Key`.slice(0, 45))
430
+
431
+ const apiKeyInput = new TextInputBuilder()
432
+ .setCustomId('apikey')
433
+ .setLabel('API Key')
434
+ .setPlaceholder('sk-...')
435
+ .setStyle(TextInputStyle.Short)
436
+ .setRequired(true)
437
+
438
+ const actionRow = new ActionRowBuilder<TextInputBuilder>().addComponents(
439
+ apiKeyInput,
440
+ )
441
+ modal.addComponents(actionRow)
442
+
443
+ await interaction.showModal(modal)
444
+ }
445
+
446
+ /**
447
+ * Start OAuth authorization flow.
448
+ */
449
+ async function startOAuthFlow(
450
+ interaction: StringSelectMenuInteraction,
451
+ context: {
452
+ dir: string
453
+ providerId?: string
454
+ providerName?: string
455
+ methodIndex?: number
456
+ methodLabel?: string
457
+ },
458
+ contextHash: string,
459
+ ): Promise<void> {
460
+ if (!context.providerId || context.methodIndex === undefined) {
461
+ await interaction.editReply({
462
+ content: 'Invalid context for OAuth flow',
463
+ components: [],
464
+ })
465
+ return
466
+ }
467
+
468
+ try {
469
+ const getClient = await initializeOpencodeForDirectory(context.dir)
470
+ if (getClient instanceof Error) {
471
+ await interaction.editReply({
472
+ content: getClient.message,
473
+ components: [],
474
+ })
475
+ return
476
+ }
477
+
478
+ await interaction.editReply({
479
+ content: `**Authenticating with ${context.providerName}**\nStarting authorization...`,
480
+ components: [],
481
+ })
482
+
483
+ // Start OAuth authorization
484
+ const authorizeResponse = await getClient().provider.oauth.authorize({
485
+ providerID: context.providerId,
486
+ method: context.methodIndex,
487
+ directory: context.dir,
488
+ })
489
+
490
+ if (!authorizeResponse.data) {
491
+ const errorData = authorizeResponse.error as
492
+ | { data?: { message?: string } }
493
+ | undefined
494
+ await interaction.editReply({
495
+ content: `Failed to start authorization: ${errorData?.data?.message || 'Unknown error'}`,
496
+ components: [],
497
+ })
498
+ return
499
+ }
500
+
501
+ const { url, method, instructions } = authorizeResponse.data
502
+
503
+ // Show authorization URL and instructions
504
+ let message = `**Authenticating with ${context.providerName}**\n\n`
505
+ message += `Open this URL to authorize:\n${url}\n\n`
506
+
507
+ if (instructions) {
508
+ // Extract code from instructions like "Enter code: ABC-123"
509
+ const codeMatch = instructions.match(/code[:\s]+([A-Z0-9-]+)/i)
510
+ if (codeMatch) {
511
+ message += `**Code:** \`${codeMatch[1]}\`\n\n`
512
+ } else {
513
+ message += `${instructions}\n\n`
514
+ }
515
+ }
516
+
517
+ if (method === 'auto') {
518
+ message += '_Waiting for authorization to complete..._'
519
+ }
520
+
521
+ await interaction.editReply({
522
+ content: message,
523
+ components: [],
524
+ })
525
+
526
+ if (method === 'auto') {
527
+ // Poll for completion (device flow)
528
+ const callbackResponse = await getClient().provider.oauth.callback({
529
+ providerID: context.providerId,
530
+ method: context.methodIndex,
531
+ directory: context.dir,
532
+ })
533
+
534
+ if (callbackResponse.error) {
535
+ const errorData = callbackResponse.error as
536
+ | { data?: { message?: string } }
537
+ | undefined
538
+ await interaction.editReply({
539
+ content: `**Authentication Failed**\n${errorData?.data?.message || 'Authorization was not completed'}`,
540
+ components: [],
541
+ })
542
+ return
543
+ }
544
+
545
+ // Dispose to refresh provider state so new credentials are recognized
546
+ await getClient().instance.dispose({ directory: context.dir })
547
+
548
+ await interaction.editReply({
549
+ content: `✅ **Successfully authenticated with ${context.providerName}!**\n\nYou can now use models from this provider.`,
550
+ components: [],
551
+ })
552
+ }
553
+ // For 'code' method, we would need to prompt for code input
554
+ // But Discord modals can't be shown after deferUpdate, so we'd need a different flow
555
+ // For now, most providers use 'auto' (device flow) which works well for Discord
556
+
557
+ // Clean up context
558
+ pendingLoginContexts.delete(contextHash)
559
+ } catch (error) {
560
+ loginLogger.error('OAuth flow error:', error)
561
+ await interaction.editReply({
562
+ content: `**Authentication Failed**\n${error instanceof Error ? error.message : 'Unknown error'}`,
563
+ components: [],
564
+ })
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Handle API key modal submission.
570
+ */
571
+ export async function handleApiKeyModalSubmit(
572
+ interaction: ModalSubmitInteraction,
573
+ ): Promise<void> {
574
+ const customId = interaction.customId
575
+
576
+ if (!customId.startsWith('login_apikey:')) {
577
+ return
578
+ }
579
+
580
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral })
581
+
582
+ const contextHash = customId.replace('login_apikey:', '')
583
+ const context = pendingLoginContexts.get(contextHash)
584
+
585
+ if (!context || !context.providerId || !context.providerName) {
586
+ await interaction.editReply({
587
+ content: 'Session expired. Please run /login again.',
588
+ })
589
+ return
590
+ }
591
+
592
+ const apiKey = interaction.fields.getTextInputValue('apikey')
593
+
594
+ if (!apiKey?.trim()) {
595
+ await interaction.editReply({
596
+ content: 'API key is required.',
597
+ })
598
+ return
599
+ }
600
+
601
+ try {
602
+ const getClient = await initializeOpencodeForDirectory(context.dir)
603
+ if (getClient instanceof Error) {
604
+ await interaction.editReply({
605
+ content: getClient.message,
606
+ })
607
+ return
608
+ }
609
+
610
+ // Set the API key
611
+ await getClient().auth.set({
612
+ providerID: context.providerId,
613
+ auth: {
614
+ type: 'api',
615
+ key: apiKey.trim(),
616
+ },
617
+ })
618
+
619
+ // Dispose to refresh provider state so new credentials are recognized
620
+ await getClient().instance.dispose({ directory: context.dir })
621
+
622
+ await interaction.editReply({
623
+ content: `✅ **Successfully authenticated with ${context.providerName}!**\n\nYou can now use models from this provider.`,
624
+ })
625
+
626
+ // Clean up context
627
+ pendingLoginContexts.delete(contextHash)
628
+ } catch (error) {
629
+ loginLogger.error('API key save error:', error)
630
+ await interaction.editReply({
631
+ content: `**Failed to save API key**\n${error instanceof Error ? error.message : 'Unknown error'}`,
632
+ })
633
+ }
634
+ }
@@ -0,0 +1,77 @@
1
+ // /toggle-mention-mode command.
2
+ // Toggles mention-only mode for a channel.
3
+ // When enabled, bot only responds to messages that @mention it.
4
+ // Messages in threads are not affected - they always work without mentions.
5
+
6
+ import {
7
+ ChatInputCommandInteraction,
8
+ MessageFlags,
9
+ ChannelType,
10
+ type TextChannel,
11
+ } from 'discord.js'
12
+ import { getChannelMentionMode, setChannelMentionMode } from '../database.js'
13
+ import { getKimakiMetadata } from '../discord-utils.js'
14
+ import { createLogger, LogPrefix } from '../logger.js'
15
+
16
+ const mentionModeLogger = createLogger(LogPrefix.CLI)
17
+
18
+ /**
19
+ * Handle the /toggle-mention-mode slash command.
20
+ * Toggles whether the bot only responds when @mentioned in this channel.
21
+ */
22
+ export async function handleToggleMentionModeCommand({
23
+ command,
24
+ appId,
25
+ }: {
26
+ command: ChatInputCommandInteraction
27
+ appId: string
28
+ }): Promise<void> {
29
+ mentionModeLogger.log('[TOGGLE_MENTION_MODE] Command called')
30
+
31
+ const channel = command.channel
32
+
33
+ if (!channel || channel.type !== ChannelType.GuildText) {
34
+ await command.reply({
35
+ content: 'This command can only be used in text channels (not threads).',
36
+ flags: MessageFlags.Ephemeral,
37
+ })
38
+ return
39
+ }
40
+
41
+ const textChannel = channel as TextChannel
42
+ const metadata = await getKimakiMetadata(textChannel)
43
+
44
+ if (metadata.channelAppId && metadata.channelAppId !== appId) {
45
+ await command.reply({
46
+ content: 'This channel is configured for a different bot.',
47
+ flags: MessageFlags.Ephemeral,
48
+ })
49
+ return
50
+ }
51
+
52
+ if (!metadata.projectDirectory) {
53
+ await command.reply({
54
+ content:
55
+ 'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
56
+ flags: MessageFlags.Ephemeral,
57
+ })
58
+ return
59
+ }
60
+
61
+ const wasEnabled = await getChannelMentionMode(textChannel.id)
62
+ const nextEnabled = !wasEnabled
63
+ await setChannelMentionMode(textChannel.id, nextEnabled)
64
+
65
+ const nextLabel = nextEnabled ? 'enabled' : 'disabled'
66
+
67
+ mentionModeLogger.log(
68
+ `[TOGGLE_MENTION_MODE] ${nextLabel.toUpperCase()} for channel ${textChannel.id}`,
69
+ )
70
+
71
+ await command.reply({
72
+ content: nextEnabled
73
+ ? `Mention mode **enabled** for this channel.\nThe bot will only start new sessions when @mentioned.\nMessages in existing threads are not affected.`
74
+ : `Mention mode **disabled** for this channel.\nThe bot will respond to all messages in **#${textChannel.name}**.`,
75
+ flags: MessageFlags.Ephemeral,
76
+ })
77
+ }