@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,404 @@
1
+ // /agent command - Set the preferred agent for this channel or session.
2
+ // Also provides quick agent commands like /plan-agent, /build-agent that switch instantly.
3
+
4
+ import {
5
+ ChatInputCommandInteraction,
6
+ StringSelectMenuInteraction,
7
+ StringSelectMenuBuilder,
8
+ ActionRowBuilder,
9
+ ChannelType,
10
+ type ThreadChannel,
11
+ type TextChannel,
12
+ MessageFlags,
13
+ } from 'discord.js'
14
+ import crypto from 'node:crypto'
15
+ import {
16
+ setChannelAgent,
17
+ setSessionAgent,
18
+ clearSessionModel,
19
+ getThreadSession,
20
+ getSessionAgent,
21
+ getChannelAgent,
22
+ } from '../database.js'
23
+ import { initializeOpencodeForDirectory } from '../opencode.js'
24
+ import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
25
+ import { createLogger, LogPrefix } from '../logger.js'
26
+
27
+ const agentLogger = createLogger(LogPrefix.AGENT)
28
+
29
+ const pendingAgentContexts = new Map<
30
+ string,
31
+ {
32
+ dir: string
33
+ channelId: string
34
+ sessionId?: string
35
+ isThread: boolean
36
+ }
37
+ >()
38
+
39
+ /**
40
+ * Context for agent commands, containing channel/session info.
41
+ */
42
+ export type AgentCommandContext = {
43
+ dir: string
44
+ channelId: string
45
+ sessionId?: string
46
+ isThread: boolean
47
+ }
48
+
49
+ export type CurrentAgentInfo =
50
+ | { type: 'session'; agent: string }
51
+ | { type: 'channel'; agent: string }
52
+ | { type: 'none' }
53
+
54
+ /**
55
+ * Get the current agent info for a channel/session, including where it comes from.
56
+ * Priority: session > channel > none
57
+ */
58
+ export async function getCurrentAgentInfo({
59
+ sessionId,
60
+ channelId,
61
+ }: {
62
+ sessionId?: string
63
+ channelId?: string
64
+ }): Promise<CurrentAgentInfo> {
65
+ if (sessionId) {
66
+ const sessionAgent = await getSessionAgent(sessionId)
67
+ if (sessionAgent) {
68
+ return { type: 'session', agent: sessionAgent }
69
+ }
70
+ }
71
+ if (channelId) {
72
+ const channelAgent = await getChannelAgent(channelId)
73
+ if (channelAgent) {
74
+ return { type: 'channel', agent: channelAgent }
75
+ }
76
+ }
77
+ return { type: 'none' }
78
+ }
79
+
80
+ /**
81
+ * Sanitize an agent name to be a valid Discord command name component.
82
+ * Lowercase, alphanumeric and hyphens only.
83
+ */
84
+ export function sanitizeAgentName(name: string): string {
85
+ return name
86
+ .toLowerCase()
87
+ .replace(/[^a-z0-9-]/g, '-')
88
+ .replace(/-+/g, '-')
89
+ .replace(/^-|-$/g, '')
90
+ }
91
+
92
+ /**
93
+ * Resolve the context for an agent command (directory, channel, session).
94
+ * Returns null if the command cannot be executed in this context.
95
+ */
96
+ export async function resolveAgentCommandContext({
97
+ interaction,
98
+ appId,
99
+ }: {
100
+ interaction: ChatInputCommandInteraction
101
+ appId: string
102
+ }): Promise<AgentCommandContext | null> {
103
+ const channel = interaction.channel
104
+
105
+ if (!channel) {
106
+ await interaction.editReply({
107
+ content: 'This command can only be used in a channel',
108
+ })
109
+ return null
110
+ }
111
+
112
+ const isThread = [
113
+ ChannelType.PublicThread,
114
+ ChannelType.PrivateThread,
115
+ ChannelType.AnnouncementThread,
116
+ ].includes(channel.type)
117
+
118
+ let projectDirectory: string | undefined
119
+ let channelAppId: string | undefined
120
+ let targetChannelId: string
121
+ let sessionId: string | undefined
122
+
123
+ if (isThread) {
124
+ const thread = channel as ThreadChannel
125
+ const textChannel = await resolveTextChannel(thread)
126
+ const metadata = await getKimakiMetadata(textChannel)
127
+ projectDirectory = metadata.projectDirectory
128
+ channelAppId = metadata.channelAppId
129
+ targetChannelId = textChannel?.id || channel.id
130
+
131
+ sessionId = await getThreadSession(thread.id)
132
+ } else if (channel.type === ChannelType.GuildText) {
133
+ const textChannel = channel as TextChannel
134
+ const metadata = await getKimakiMetadata(textChannel)
135
+ projectDirectory = metadata.projectDirectory
136
+ channelAppId = metadata.channelAppId
137
+ targetChannelId = channel.id
138
+ } else {
139
+ await interaction.editReply({
140
+ content: 'This command can only be used in text channels or threads',
141
+ })
142
+ return null
143
+ }
144
+
145
+ if (channelAppId && channelAppId !== appId) {
146
+ await interaction.editReply({
147
+ content: 'This channel is not configured for this bot',
148
+ })
149
+ return null
150
+ }
151
+
152
+ if (!projectDirectory) {
153
+ await interaction.editReply({
154
+ content: 'This channel is not configured with a project directory',
155
+ })
156
+ return null
157
+ }
158
+
159
+ return {
160
+ dir: projectDirectory,
161
+ channelId: targetChannelId,
162
+ sessionId,
163
+ isThread,
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Set the agent preference for a context (session or channel).
169
+ * When switching agents for a session, clears session model preference
170
+ * so the new agent's model takes effect (agent model > channel model).
171
+ */
172
+ export async function setAgentForContext({
173
+ context,
174
+ agentName,
175
+ }: {
176
+ context: AgentCommandContext
177
+ agentName: string
178
+ }): Promise<void> {
179
+ if (context.isThread && context.sessionId) {
180
+ await setSessionAgent(context.sessionId, agentName)
181
+ // Clear session model so the new agent's model takes effect
182
+ await clearSessionModel(context.sessionId)
183
+ agentLogger.log(
184
+ `Set agent ${agentName} for session ${context.sessionId} (cleared session model)`,
185
+ )
186
+ } else {
187
+ await setChannelAgent(context.channelId, agentName)
188
+ agentLogger.log(`Set agent ${agentName} for channel ${context.channelId}`)
189
+ }
190
+ }
191
+
192
+ export async function handleAgentCommand({
193
+ interaction,
194
+ appId,
195
+ }: {
196
+ interaction: ChatInputCommandInteraction
197
+ appId: string
198
+ }): Promise<void> {
199
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral })
200
+
201
+ const context = await resolveAgentCommandContext({ interaction, appId })
202
+ if (!context) {
203
+ return
204
+ }
205
+
206
+ try {
207
+ const getClient = await initializeOpencodeForDirectory(context.dir)
208
+ if (getClient instanceof Error) {
209
+ await interaction.editReply({ content: getClient.message })
210
+ return
211
+ }
212
+
213
+ const agentsResponse = await getClient().app.agents({
214
+ directory: context.dir,
215
+ })
216
+
217
+ if (!agentsResponse.data || agentsResponse.data.length === 0) {
218
+ await interaction.editReply({ content: 'No agents available' })
219
+ return
220
+ }
221
+
222
+ const agents = agentsResponse.data
223
+ .filter((agent) => {
224
+ const hidden = (agent as { hidden?: boolean }).hidden
225
+ return (agent.mode === 'primary' || agent.mode === 'all') && !hidden
226
+ })
227
+ .slice(0, 25)
228
+
229
+ if (agents.length === 0) {
230
+ await interaction.editReply({ content: 'No primary agents available' })
231
+ return
232
+ }
233
+
234
+ const currentAgentInfo = await getCurrentAgentInfo({
235
+ sessionId: context.sessionId,
236
+ channelId: context.channelId,
237
+ })
238
+
239
+ const currentAgentText = (() => {
240
+ switch (currentAgentInfo.type) {
241
+ case 'session':
242
+ return `**Current (session override):** \`${currentAgentInfo.agent}\``
243
+ case 'channel':
244
+ return `**Current (channel override):** \`${currentAgentInfo.agent}\``
245
+ case 'none':
246
+ return '**Current:** none'
247
+ }
248
+ })()
249
+
250
+ const contextHash = crypto.randomBytes(8).toString('hex')
251
+ pendingAgentContexts.set(contextHash, context)
252
+
253
+ const options = agents.map((agent) => ({
254
+ label: agent.name.slice(0, 100),
255
+ value: agent.name,
256
+ description: (agent.description || `${agent.mode} agent`).slice(0, 100),
257
+ }))
258
+
259
+ const selectMenu = new StringSelectMenuBuilder()
260
+ .setCustomId(`agent_select:${contextHash}`)
261
+ .setPlaceholder('Select an agent')
262
+ .addOptions(options)
263
+
264
+ const actionRow =
265
+ new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
266
+
267
+ await interaction.editReply({
268
+ content: `**Set Agent Preference**\n${currentAgentText}\nSelect an agent:`,
269
+ components: [actionRow],
270
+ })
271
+ } catch (error) {
272
+ agentLogger.error('Error loading agents:', error)
273
+ await interaction.editReply({
274
+ content: `Failed to load agents: ${error instanceof Error ? error.message : 'Unknown error'}`,
275
+ })
276
+ }
277
+ }
278
+
279
+ export async function handleAgentSelectMenu(
280
+ interaction: StringSelectMenuInteraction,
281
+ ): Promise<void> {
282
+ const customId = interaction.customId
283
+
284
+ if (!customId.startsWith('agent_select:')) {
285
+ return
286
+ }
287
+
288
+ await interaction.deferUpdate()
289
+
290
+ const contextHash = customId.replace('agent_select:', '')
291
+ const context = pendingAgentContexts.get(contextHash)
292
+
293
+ if (!context) {
294
+ await interaction.editReply({
295
+ content: 'Selection expired. Please run /agent again.',
296
+ components: [],
297
+ })
298
+ return
299
+ }
300
+
301
+ const selectedAgent = interaction.values[0]
302
+ if (!selectedAgent) {
303
+ await interaction.editReply({
304
+ content: 'No agent selected',
305
+ components: [],
306
+ })
307
+ return
308
+ }
309
+
310
+ try {
311
+ await setAgentForContext({ context, agentName: selectedAgent })
312
+
313
+ if (context.isThread && context.sessionId) {
314
+ await interaction.editReply({
315
+ content: `Agent preference set for this session: **${selectedAgent}**`,
316
+ components: [],
317
+ })
318
+ } else {
319
+ await interaction.editReply({
320
+ content: `Agent preference set for this channel: **${selectedAgent}**\nAll new sessions in this channel will use this agent.`,
321
+ components: [],
322
+ })
323
+ }
324
+
325
+ pendingAgentContexts.delete(contextHash)
326
+ } catch (error) {
327
+ agentLogger.error('Error saving agent preference:', error)
328
+ await interaction.editReply({
329
+ content: `Failed to save agent preference: ${error instanceof Error ? error.message : 'Unknown error'}`,
330
+ components: [],
331
+ })
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Handle quick agent commands like /plan-agent, /build-agent.
337
+ * These instantly switch to the specified agent without showing a dropdown.
338
+ *
339
+ * Optimized for speed: skips opencode server entirely.
340
+ * The agent name is already validated at command registration time (cli.ts registers
341
+ * commands from the agents list), so we trust the command name directly instead of
342
+ * calling initializeOpencodeForDirectory + app.agents() to re-validate.
343
+ * This eliminates two HTTP round-trips (~400-1000ms) that the old implementation had.
344
+ */
345
+ export async function handleQuickAgentCommand({
346
+ command,
347
+ appId,
348
+ }: {
349
+ command: ChatInputCommandInteraction
350
+ appId: string
351
+ }): Promise<void> {
352
+ // Extract agent name from command: "plan-agent" → "plan"
353
+ const sanitizedAgentName = command.commandName.replace(/-agent$/, '')
354
+
355
+ await command.deferReply({ flags: MessageFlags.Ephemeral })
356
+
357
+ const context = await resolveAgentCommandContext({
358
+ interaction: command,
359
+ appId,
360
+ })
361
+ if (!context) {
362
+ return
363
+ }
364
+
365
+ try {
366
+ // Check current agent and set new one.
367
+ // getCurrentAgentInfo is fast (DB only), use it for the "was X" text.
368
+ const previousAgent = await getCurrentAgentInfo({
369
+ sessionId: context.sessionId,
370
+ channelId: context.channelId,
371
+ })
372
+ const previousAgentName =
373
+ previousAgent.type !== 'none' ? previousAgent.agent : undefined
374
+
375
+ if (previousAgentName === sanitizedAgentName) {
376
+ await command.editReply({
377
+ content: `Already using **${sanitizedAgentName}** agent`,
378
+ })
379
+ return
380
+ }
381
+
382
+ // Set the agent preference (DB write only, no HTTP calls)
383
+ await setAgentForContext({ context, agentName: sanitizedAgentName })
384
+
385
+ const previousText = previousAgentName
386
+ ? ` (was **${previousAgentName}**)`
387
+ : ''
388
+
389
+ if (context.isThread && context.sessionId) {
390
+ await command.editReply({
391
+ content: `Switched to **${sanitizedAgentName}** agent for this session${previousText}`,
392
+ })
393
+ } else {
394
+ await command.editReply({
395
+ content: `Switched to **${sanitizedAgentName}** agent for this channel${previousText}\nAll new sessions will use this agent.`,
396
+ })
397
+ }
398
+ } catch (error) {
399
+ agentLogger.error('Error in quick agent command:', error)
400
+ await command.editReply({
401
+ content: `Failed to switch agent: ${error instanceof Error ? error.message : 'Unknown error'}`,
402
+ })
403
+ }
404
+ }