@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,694 @@
1
+ // /model command - Set the preferred model for this channel or session.
2
+ import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, MessageFlags, } from 'discord.js';
3
+ import crypto from 'node:crypto';
4
+ import { setChannelModel, setSessionModel, setSessionAgent, getChannelModel, getSessionModel, getSessionAgent, getChannelAgent, getThreadSession, getGlobalModel, setGlobalModel, getVariantCascade, } from '../database.js';
5
+ import { initializeOpencodeForDirectory } from '../opencode.js';
6
+ import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
7
+ import { abortAndRetrySession, getDefaultModel } from '../session-handler.js';
8
+ import { getThinkingValuesForModel } from '../thinking-utils.js';
9
+ import { createLogger, LogPrefix } from '../logger.js';
10
+ import * as errore from 'errore';
11
+ const modelLogger = createLogger(LogPrefix.MODEL);
12
+ // Store context by hash to avoid customId length limits (Discord max: 100 chars)
13
+ const pendingModelContexts = new Map();
14
+ function parseModelId(modelString) {
15
+ const [providerID, ...modelParts] = modelString.split('/');
16
+ const modelID = modelParts.join('/');
17
+ if (providerID && modelID) {
18
+ return { providerID, modelID };
19
+ }
20
+ return undefined;
21
+ }
22
+ export async function ensureSessionPreferencesSnapshot({ sessionId, channelId, appId, getClient, agentOverride, modelOverride, force, }) {
23
+ const [sessionAgentPreference, sessionModelPreference] = await Promise.all([
24
+ getSessionAgent(sessionId),
25
+ getSessionModel(sessionId),
26
+ ]);
27
+ const shouldBootstrapSessionPreferences = force || (!sessionAgentPreference && !sessionModelPreference);
28
+ if (!shouldBootstrapSessionPreferences) {
29
+ return;
30
+ }
31
+ const bootstrappedAgent = agentOverride ||
32
+ sessionAgentPreference ||
33
+ (channelId ? await getChannelAgent(channelId) : undefined);
34
+ if (!sessionAgentPreference && bootstrappedAgent) {
35
+ await setSessionAgent(sessionId, bootstrappedAgent);
36
+ modelLogger.log(`[MODEL] Snapshotted session agent ${bootstrappedAgent} for session ${sessionId}`);
37
+ }
38
+ if (sessionModelPreference) {
39
+ return;
40
+ }
41
+ if (modelOverride) {
42
+ const parsedModelOverride = parseModelId(modelOverride);
43
+ if (parsedModelOverride) {
44
+ const bootstrappedVariant = await getVariantCascade({
45
+ sessionId,
46
+ channelId,
47
+ appId,
48
+ });
49
+ await setSessionModel({
50
+ sessionId,
51
+ modelId: modelOverride,
52
+ variant: bootstrappedVariant ?? null,
53
+ });
54
+ modelLogger.log(`[MODEL] Snapshotted explicit session model ${modelOverride} for session ${sessionId}`);
55
+ return;
56
+ }
57
+ modelLogger.warn(`[MODEL] Ignoring invalid explicit model override "${modelOverride}" for session ${sessionId}`);
58
+ }
59
+ const bootstrappedModel = await getCurrentModelInfo({
60
+ sessionId,
61
+ channelId,
62
+ appId,
63
+ agentPreference: bootstrappedAgent,
64
+ getClient,
65
+ });
66
+ if (bootstrappedModel.type === 'none') {
67
+ return;
68
+ }
69
+ const bootstrappedVariant = await getVariantCascade({
70
+ sessionId,
71
+ channelId,
72
+ appId,
73
+ });
74
+ await setSessionModel({
75
+ sessionId,
76
+ modelId: bootstrappedModel.model,
77
+ variant: bootstrappedVariant ?? null,
78
+ });
79
+ modelLogger.log(`[MODEL] Snapshotted session model ${bootstrappedModel.model} for session ${sessionId}`);
80
+ }
81
+ /**
82
+ * Get the current model info for a channel/session, including where it comes from.
83
+ * Priority: session > agent > channel > global > opencode default
84
+ */
85
+ export async function getCurrentModelInfo({ sessionId, channelId, appId, agentPreference, getClient, }) {
86
+ if (getClient instanceof Error) {
87
+ return { type: 'none' };
88
+ }
89
+ // 1. Check session model preference
90
+ if (sessionId) {
91
+ const sessionPref = await getSessionModel(sessionId);
92
+ if (sessionPref) {
93
+ const parsed = parseModelId(sessionPref.modelId);
94
+ if (parsed) {
95
+ return { type: 'session', model: sessionPref.modelId, ...parsed };
96
+ }
97
+ }
98
+ }
99
+ // 2. Check agent's configured model
100
+ const effectiveAgent = agentPreference ??
101
+ (sessionId
102
+ ? (await getSessionAgent(sessionId)) ||
103
+ (channelId ? await getChannelAgent(channelId) : undefined)
104
+ : channelId
105
+ ? await getChannelAgent(channelId)
106
+ : undefined);
107
+ if (effectiveAgent) {
108
+ const agentsResponse = await getClient().app.agents({});
109
+ if (agentsResponse.data) {
110
+ const agent = agentsResponse.data.find((a) => a.name === effectiveAgent);
111
+ if (agent?.model) {
112
+ const model = `${agent.model.providerID}/${agent.model.modelID}`;
113
+ return {
114
+ type: 'agent',
115
+ model,
116
+ providerID: agent.model.providerID,
117
+ modelID: agent.model.modelID,
118
+ agentName: effectiveAgent,
119
+ };
120
+ }
121
+ }
122
+ }
123
+ // 3. Check channel model preference
124
+ if (channelId) {
125
+ const channelPref = await getChannelModel(channelId);
126
+ if (channelPref) {
127
+ const parsed = parseModelId(channelPref.modelId);
128
+ if (parsed) {
129
+ return { type: 'channel', model: channelPref.modelId, ...parsed };
130
+ }
131
+ }
132
+ }
133
+ // 4. Check global model preference
134
+ if (appId) {
135
+ const globalPref = await getGlobalModel(appId);
136
+ if (globalPref) {
137
+ const parsed = parseModelId(globalPref.modelId);
138
+ if (parsed) {
139
+ return { type: 'global', model: globalPref.modelId, ...parsed };
140
+ }
141
+ }
142
+ }
143
+ // 5. Get opencode default (config > recent > provider default)
144
+ const defaultModel = await getDefaultModel({ getClient });
145
+ if (defaultModel) {
146
+ const model = `${defaultModel.providerID}/${defaultModel.modelID}`;
147
+ return {
148
+ type: defaultModel.source,
149
+ model,
150
+ providerID: defaultModel.providerID,
151
+ modelID: defaultModel.modelID,
152
+ };
153
+ }
154
+ return { type: 'none' };
155
+ }
156
+ /**
157
+ * Handle the /model slash command.
158
+ * Shows a select menu with available providers.
159
+ */
160
+ export async function handleModelCommand({ interaction, appId, }) {
161
+ modelLogger.log('[MODEL] handleModelCommand called');
162
+ // Defer reply immediately to avoid 3-second timeout
163
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
164
+ modelLogger.log('[MODEL] Deferred reply');
165
+ const channel = interaction.channel;
166
+ if (!channel) {
167
+ await interaction.editReply({
168
+ content: 'This command can only be used in a channel',
169
+ });
170
+ return;
171
+ }
172
+ // Determine if we're in a thread or text channel
173
+ const isThread = [
174
+ ChannelType.PublicThread,
175
+ ChannelType.PrivateThread,
176
+ ChannelType.AnnouncementThread,
177
+ ].includes(channel.type);
178
+ let projectDirectory;
179
+ let channelAppId;
180
+ let targetChannelId;
181
+ let sessionId;
182
+ if (isThread) {
183
+ const thread = channel;
184
+ // Parallelize: resolve metadata and session ID at the same time
185
+ const [textChannel, threadSessionId] = await Promise.all([
186
+ resolveTextChannel(thread),
187
+ getThreadSession(thread.id),
188
+ ]);
189
+ const metadata = await getKimakiMetadata(textChannel);
190
+ projectDirectory = metadata.projectDirectory;
191
+ channelAppId = metadata.channelAppId;
192
+ targetChannelId = textChannel?.id || channel.id;
193
+ sessionId = threadSessionId;
194
+ }
195
+ else if (channel.type === ChannelType.GuildText) {
196
+ const textChannel = channel;
197
+ const metadata = await getKimakiMetadata(textChannel);
198
+ projectDirectory = metadata.projectDirectory;
199
+ channelAppId = metadata.channelAppId;
200
+ targetChannelId = channel.id;
201
+ }
202
+ else {
203
+ await interaction.editReply({
204
+ content: 'This command can only be used in text channels or threads',
205
+ });
206
+ return;
207
+ }
208
+ if (channelAppId && channelAppId !== appId) {
209
+ await interaction.editReply({
210
+ content: 'This channel is not configured for this bot',
211
+ });
212
+ return;
213
+ }
214
+ if (!projectDirectory) {
215
+ await interaction.editReply({
216
+ content: 'This channel is not configured with a project directory',
217
+ });
218
+ return;
219
+ }
220
+ try {
221
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
222
+ if (getClient instanceof Error) {
223
+ await interaction.editReply({ content: getClient.message });
224
+ return;
225
+ }
226
+ const effectiveAppId = channelAppId || appId;
227
+ if (isThread && sessionId) {
228
+ await ensureSessionPreferencesSnapshot({
229
+ sessionId,
230
+ channelId: targetChannelId,
231
+ appId: effectiveAppId,
232
+ getClient,
233
+ });
234
+ }
235
+ // Parallelize: fetch providers, current model info, and variant cascade at the same time.
236
+ // getCurrentModelInfo does DB lookups first (fast) and only hits provider.list as fallback.
237
+ const [providersResponse, currentModelInfo, cascadeVariant] = await Promise.all([
238
+ getClient().provider.list({ directory: projectDirectory }),
239
+ getCurrentModelInfo({
240
+ sessionId,
241
+ channelId: targetChannelId,
242
+ appId: effectiveAppId,
243
+ getClient,
244
+ }),
245
+ getVariantCascade({
246
+ sessionId,
247
+ channelId: targetChannelId,
248
+ appId: effectiveAppId,
249
+ }),
250
+ ]);
251
+ if (!providersResponse.data) {
252
+ await interaction.editReply({
253
+ content: 'Failed to fetch providers',
254
+ });
255
+ return;
256
+ }
257
+ const { all: allProviders, connected } = providersResponse.data;
258
+ // Filter to only connected providers (have credentials)
259
+ const availableProviders = allProviders.filter((p) => {
260
+ return connected.includes(p.id);
261
+ });
262
+ if (availableProviders.length === 0) {
263
+ await interaction.editReply({
264
+ content: 'No providers with credentials found. Use `/login` to connect a provider and add credentials.',
265
+ });
266
+ return;
267
+ }
268
+ const currentModelText = (() => {
269
+ switch (currentModelInfo.type) {
270
+ case 'session':
271
+ return `**Current (this thread):** \`${currentModelInfo.model}\``;
272
+ case 'agent':
273
+ return `**Current (agent "${currentModelInfo.agentName}"):** \`${currentModelInfo.model}\``;
274
+ case 'channel':
275
+ return `**Current (channel override):** \`${currentModelInfo.model}\``;
276
+ case 'global':
277
+ return `**Current (global default):** \`${currentModelInfo.model}\``;
278
+ case 'opencode-config':
279
+ case 'opencode-recent':
280
+ case 'opencode-provider-default':
281
+ return `**Current (opencode default):** \`${currentModelInfo.model}\``;
282
+ case 'none':
283
+ return '**Current:** none';
284
+ }
285
+ })();
286
+ const variantText = (() => {
287
+ if (currentModelInfo.type === 'none' || !cascadeVariant) {
288
+ return '';
289
+ }
290
+ return `\n**Variant:** \`${cascadeVariant}\``;
291
+ })();
292
+ // Store context with a short hash key to avoid customId length limits
293
+ // Use bot's appId if channel doesn't have one stored (older channels or channels migrated before appId tracking)
294
+ const context = {
295
+ dir: projectDirectory,
296
+ channelId: targetChannelId,
297
+ sessionId: sessionId,
298
+ isThread: isThread,
299
+ thread: isThread ? channel : undefined,
300
+ appId: channelAppId || appId,
301
+ };
302
+ const contextHash = crypto.randomBytes(8).toString('hex');
303
+ pendingModelContexts.set(contextHash, context);
304
+ const options = [...availableProviders]
305
+ .sort((a, b) => a.name.localeCompare(b.name))
306
+ .slice(0, 25)
307
+ .map((provider) => {
308
+ const modelCount = Object.keys(provider.models || {}).length;
309
+ return {
310
+ label: provider.name.slice(0, 100),
311
+ value: provider.id,
312
+ description: `${modelCount} model${modelCount !== 1 ? 's' : ''} available`.slice(0, 100),
313
+ };
314
+ });
315
+ const selectMenu = new StringSelectMenuBuilder()
316
+ .setCustomId(`model_provider:${contextHash}`)
317
+ .setPlaceholder('Select a provider')
318
+ .addOptions(options);
319
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
320
+ await interaction.editReply({
321
+ content: `**Set Model Preference**\n${currentModelText}${variantText}\nSelect a provider:`,
322
+ components: [actionRow],
323
+ });
324
+ }
325
+ catch (error) {
326
+ modelLogger.error('Error loading providers:', error);
327
+ await interaction.editReply({
328
+ content: `Failed to load providers: ${error instanceof Error ? error.message : 'Unknown error'}`,
329
+ });
330
+ }
331
+ }
332
+ /**
333
+ * Handle the provider select menu interaction.
334
+ * Shows a second select menu with models for the chosen provider.
335
+ */
336
+ export async function handleProviderSelectMenu(interaction) {
337
+ const customId = interaction.customId;
338
+ if (!customId.startsWith('model_provider:')) {
339
+ return;
340
+ }
341
+ // Defer update immediately to avoid timeout
342
+ await interaction.deferUpdate();
343
+ const contextHash = customId.replace('model_provider:', '');
344
+ const context = pendingModelContexts.get(contextHash);
345
+ if (!context) {
346
+ await interaction.editReply({
347
+ content: 'Selection expired. Please run /model again.',
348
+ components: [],
349
+ });
350
+ return;
351
+ }
352
+ const selectedProviderId = interaction.values[0];
353
+ if (!selectedProviderId) {
354
+ await interaction.editReply({
355
+ content: 'No provider selected',
356
+ components: [],
357
+ });
358
+ return;
359
+ }
360
+ try {
361
+ const getClient = await initializeOpencodeForDirectory(context.dir);
362
+ if (getClient instanceof Error) {
363
+ await interaction.editReply({
364
+ content: getClient.message,
365
+ components: [],
366
+ });
367
+ return;
368
+ }
369
+ const providersResponse = await getClient().provider.list({
370
+ directory: context.dir,
371
+ });
372
+ if (!providersResponse.data) {
373
+ await interaction.editReply({
374
+ content: 'Failed to fetch providers',
375
+ components: [],
376
+ });
377
+ return;
378
+ }
379
+ const provider = providersResponse.data.all.find((p) => p.id === selectedProviderId);
380
+ if (!provider) {
381
+ await interaction.editReply({
382
+ content: 'Provider not found',
383
+ components: [],
384
+ });
385
+ return;
386
+ }
387
+ const models = Object.entries(provider.models || {})
388
+ .map(([modelId, model]) => ({
389
+ id: modelId,
390
+ name: model.name,
391
+ releaseDate: model.release_date,
392
+ }))
393
+ .sort((a, b) => a.name.localeCompare(b.name));
394
+ if (models.length === 0) {
395
+ await interaction.editReply({
396
+ content: `No models available for ${provider.name}`,
397
+ components: [],
398
+ });
399
+ return;
400
+ }
401
+ // Take first 25 models (most recent since sorted descending)
402
+ const recentModels = models.slice(0, 25);
403
+ // Update context with provider info and reuse the same hash
404
+ context.providerId = selectedProviderId;
405
+ context.providerName = provider.name;
406
+ pendingModelContexts.set(contextHash, context);
407
+ const options = recentModels.map((model) => {
408
+ const dateStr = model.releaseDate
409
+ ? new Date(model.releaseDate).toLocaleDateString()
410
+ : 'Unknown date';
411
+ return {
412
+ label: model.name.slice(0, 100),
413
+ value: model.id,
414
+ description: dateStr.slice(0, 100),
415
+ };
416
+ });
417
+ const selectMenu = new StringSelectMenuBuilder()
418
+ .setCustomId(`model_select:${contextHash}`)
419
+ .setPlaceholder('Select a model')
420
+ .addOptions(options);
421
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
422
+ await interaction.editReply({
423
+ content: `**Set Model Preference**\nProvider: **${provider.name}**\nSelect a model:`,
424
+ components: [actionRow],
425
+ });
426
+ }
427
+ catch (error) {
428
+ modelLogger.error('Error loading models:', error);
429
+ await interaction.editReply({
430
+ content: `Failed to load models: ${error instanceof Error ? error.message : 'Unknown error'}`,
431
+ components: [],
432
+ });
433
+ }
434
+ }
435
+ /**
436
+ * Handle the model select menu interaction.
437
+ * Stores the model preference in the database.
438
+ */
439
+ export async function handleModelSelectMenu(interaction) {
440
+ const customId = interaction.customId;
441
+ if (!customId.startsWith('model_select:')) {
442
+ return;
443
+ }
444
+ // Defer update immediately
445
+ await interaction.deferUpdate();
446
+ const contextHash = customId.replace('model_select:', '');
447
+ const context = pendingModelContexts.get(contextHash);
448
+ if (!context || !context.providerId || !context.providerName) {
449
+ await interaction.editReply({
450
+ content: 'Selection expired. Please run /model again.',
451
+ components: [],
452
+ });
453
+ return;
454
+ }
455
+ const selectedModelId = interaction.values[0];
456
+ if (!selectedModelId) {
457
+ await interaction.editReply({
458
+ content: 'No model selected',
459
+ components: [],
460
+ });
461
+ return;
462
+ }
463
+ // Build full model ID: provider_id/model_id
464
+ const fullModelId = `${context.providerId}/${selectedModelId}`;
465
+ try {
466
+ context.selectedModelId = fullModelId;
467
+ pendingModelContexts.set(contextHash, context);
468
+ // Check if model has variants (thinking levels) - if so, show variant picker first
469
+ const getClient = await initializeOpencodeForDirectory(context.dir);
470
+ if (!(getClient instanceof Error)) {
471
+ const providersResponse = await getClient().provider.list({
472
+ directory: context.dir,
473
+ });
474
+ if (providersResponse.data) {
475
+ const variants = getThinkingValuesForModel({
476
+ providers: providersResponse.data.all,
477
+ providerId: context.providerId,
478
+ modelId: selectedModelId,
479
+ });
480
+ if (variants.length > 0) {
481
+ context.availableVariants = variants;
482
+ pendingModelContexts.set(contextHash, context);
483
+ const variantOptions = [
484
+ {
485
+ label: 'None (default)',
486
+ value: '__none__',
487
+ description: 'Use the model without a specific thinking level',
488
+ },
489
+ ...variants.slice(0, 24).map((v) => ({
490
+ label: v.slice(0, 100),
491
+ value: v,
492
+ description: `Use ${v} thinking`.slice(0, 100),
493
+ })),
494
+ ];
495
+ const selectMenu = new StringSelectMenuBuilder()
496
+ .setCustomId(`model_variant:${contextHash}`)
497
+ .setPlaceholder('Select a thinking level')
498
+ .addOptions(variantOptions);
499
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
500
+ await interaction.editReply({
501
+ content: `**Set Model Preference**\nModel: **${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\nSelect a thinking level:`,
502
+ components: [actionRow],
503
+ });
504
+ return;
505
+ }
506
+ }
507
+ }
508
+ // No variants available - skip to scope
509
+ context.selectedVariant = null;
510
+ pendingModelContexts.set(contextHash, context);
511
+ await showScopeMenu({ interaction, contextHash, context });
512
+ }
513
+ catch (error) {
514
+ modelLogger.error('Error saving model preference:', error);
515
+ await interaction.editReply({
516
+ content: `Failed to save model preference: ${error instanceof Error ? error.message : 'Unknown error'}`,
517
+ components: [],
518
+ });
519
+ }
520
+ }
521
+ /**
522
+ * Handle the variant select menu interaction.
523
+ * Stores the selected variant and shows the scope menu.
524
+ */
525
+ export async function handleModelVariantSelectMenu(interaction) {
526
+ const customId = interaction.customId;
527
+ if (!customId.startsWith('model_variant:')) {
528
+ return;
529
+ }
530
+ await interaction.deferUpdate();
531
+ const contextHash = customId.replace('model_variant:', '');
532
+ const context = pendingModelContexts.get(contextHash);
533
+ if (!context || !context.selectedModelId) {
534
+ await interaction.editReply({
535
+ content: 'Selection expired. Please run /model again.',
536
+ components: [],
537
+ });
538
+ return;
539
+ }
540
+ const selectedValue = interaction.values[0];
541
+ if (!selectedValue) {
542
+ await interaction.editReply({
543
+ content: 'No variant selected',
544
+ components: [],
545
+ });
546
+ return;
547
+ }
548
+ context.selectedVariant = selectedValue === '__none__' ? null : selectedValue;
549
+ pendingModelContexts.set(contextHash, context);
550
+ await showScopeMenu({ interaction, contextHash, context });
551
+ }
552
+ async function showScopeMenu({ interaction, contextHash, context, }) {
553
+ const modelId = context.selectedModelId;
554
+ const modelDisplay = modelId.split('/')[1] || modelId;
555
+ const variantSuffix = context.selectedVariant
556
+ ? ` (${context.selectedVariant})`
557
+ : '';
558
+ const scopeOptions = [
559
+ ...(context.isThread && context.sessionId
560
+ ? [
561
+ {
562
+ label: 'This session only',
563
+ value: 'session',
564
+ description: 'Override for this session only',
565
+ },
566
+ ]
567
+ : []),
568
+ {
569
+ label: 'This channel only',
570
+ value: 'channel',
571
+ description: 'Override for this channel only',
572
+ },
573
+ {
574
+ label: 'Global default',
575
+ value: 'global',
576
+ description: 'Set for this channel and as default for all others',
577
+ },
578
+ ];
579
+ const selectMenu = new StringSelectMenuBuilder()
580
+ .setCustomId(`model_scope:${contextHash}`)
581
+ .setPlaceholder('Apply to...')
582
+ .addOptions(scopeOptions);
583
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
584
+ await interaction.editReply({
585
+ content: `**Set Model Preference**\nModel: **${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`\nApply to:`,
586
+ components: [actionRow],
587
+ });
588
+ }
589
+ /**
590
+ * Handle the scope select menu interaction.
591
+ * Applies the model to either the channel or globally.
592
+ */
593
+ export async function handleModelScopeSelectMenu(interaction) {
594
+ const customId = interaction.customId;
595
+ if (!customId.startsWith('model_scope:')) {
596
+ return;
597
+ }
598
+ // Defer update immediately
599
+ await interaction.deferUpdate();
600
+ const contextHash = customId.replace('model_scope:', '');
601
+ const context = pendingModelContexts.get(contextHash);
602
+ if (!context ||
603
+ !context.providerId ||
604
+ !context.providerName ||
605
+ !context.selectedModelId) {
606
+ await interaction.editReply({
607
+ content: 'Selection expired. Please run /model again.',
608
+ components: [],
609
+ });
610
+ return;
611
+ }
612
+ const selectedScope = interaction.values[0];
613
+ if (!selectedScope) {
614
+ await interaction.editReply({
615
+ content: 'No scope selected',
616
+ components: [],
617
+ });
618
+ return;
619
+ }
620
+ const modelId = context.selectedModelId;
621
+ const modelDisplay = modelId.split('/')[1] || modelId;
622
+ const variant = context.selectedVariant ?? null;
623
+ const variantSuffix = variant ? ` (${variant})` : '';
624
+ const agentTip = '\n_Tip: create [agent .md files](https://github.com/remorses/kimaki/blob/main/docs/model-switching.md) in .opencode/agent/ for one-command model switching_';
625
+ try {
626
+ if (selectedScope === 'session') {
627
+ if (!context.sessionId) {
628
+ pendingModelContexts.delete(contextHash);
629
+ await interaction.editReply({
630
+ content: 'No active session in this thread. Please run /model in a thread with a session.',
631
+ components: [],
632
+ });
633
+ return;
634
+ }
635
+ await setSessionModel({ sessionId: context.sessionId, modelId, variant });
636
+ modelLogger.log(`Set model ${modelId}${variantSuffix} for session ${context.sessionId}`);
637
+ let retried = false;
638
+ if (context.thread) {
639
+ retried = await abortAndRetrySession({
640
+ sessionId: context.sessionId,
641
+ thread: context.thread,
642
+ projectDirectory: context.dir,
643
+ appId: context.appId,
644
+ channelId: context.channelId,
645
+ });
646
+ }
647
+ const retryNote = retried
648
+ ? '\n_Retrying current request with new model..._'
649
+ : '';
650
+ await interaction.editReply({
651
+ content: `Model set for this session:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`${retryNote}${agentTip}`,
652
+ flags: MessageFlags.SuppressEmbeds,
653
+ components: [],
654
+ });
655
+ }
656
+ else if (selectedScope === 'global') {
657
+ if (!context.appId) {
658
+ pendingModelContexts.delete(contextHash);
659
+ await interaction.editReply({
660
+ content: 'Cannot set global model: channel is not linked to a bot',
661
+ components: [],
662
+ });
663
+ return;
664
+ }
665
+ await setGlobalModel({ appId: context.appId, modelId, variant });
666
+ await setChannelModel({ channelId: context.channelId, modelId, variant });
667
+ modelLogger.log(`Set global model ${modelId}${variantSuffix} for app ${context.appId} and channel ${context.channelId}`);
668
+ await interaction.editReply({
669
+ content: `Model set for this channel and as global default:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`\nAll channels will use this model (unless they have their own override).${agentTip}`,
670
+ flags: MessageFlags.SuppressEmbeds,
671
+ components: [],
672
+ });
673
+ }
674
+ else {
675
+ // channel scope
676
+ await setChannelModel({ channelId: context.channelId, modelId, variant });
677
+ modelLogger.log(`Set model ${modelId}${variantSuffix} for channel ${context.channelId}`);
678
+ await interaction.editReply({
679
+ content: `Model preference set for this channel:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`\nAll new sessions in this channel will use this model.${agentTip}`,
680
+ flags: MessageFlags.SuppressEmbeds,
681
+ components: [],
682
+ });
683
+ }
684
+ // Clean up the context from memory
685
+ pendingModelContexts.delete(contextHash);
686
+ }
687
+ catch (error) {
688
+ modelLogger.error('Error saving model preference:', error);
689
+ await interaction.editReply({
690
+ content: `Failed to save model preference: ${error instanceof Error ? error.message : 'Unknown error'}`,
691
+ components: [],
692
+ });
693
+ }
694
+ }