@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,217 @@
1
+ // /fork command - Fork the session from a past user message.
2
+ import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, ThreadAutoArchiveDuration, MessageFlags, } from 'discord.js';
3
+ import { getThreadSession, setThreadSession, setPartMessagesBatch, } from '../database.js';
4
+ import { initializeOpencodeForDirectory } from '../opencode.js';
5
+ import { resolveWorkingDirectory, resolveTextChannel, sendThreadMessage, } from '../discord-utils.js';
6
+ import { collectLastAssistantParts } from '../message-formatting.js';
7
+ import { createLogger, LogPrefix } from '../logger.js';
8
+ import * as errore from 'errore';
9
+ const sessionLogger = createLogger(LogPrefix.SESSION);
10
+ const forkLogger = createLogger(LogPrefix.FORK);
11
+ export async function handleForkCommand(interaction) {
12
+ const channel = interaction.channel;
13
+ if (!channel) {
14
+ await interaction.reply({
15
+ content: 'This command can only be used in a channel',
16
+ flags: MessageFlags.Ephemeral,
17
+ });
18
+ return;
19
+ }
20
+ const isThread = [
21
+ ChannelType.PublicThread,
22
+ ChannelType.PrivateThread,
23
+ ChannelType.AnnouncementThread,
24
+ ].includes(channel.type);
25
+ if (!isThread) {
26
+ await interaction.reply({
27
+ content: 'This command can only be used in a thread with an active session',
28
+ flags: MessageFlags.Ephemeral,
29
+ });
30
+ return;
31
+ }
32
+ const resolved = await resolveWorkingDirectory({
33
+ channel: channel,
34
+ });
35
+ if (!resolved) {
36
+ await interaction.reply({
37
+ content: 'Could not determine project directory for this channel',
38
+ flags: MessageFlags.Ephemeral,
39
+ });
40
+ return;
41
+ }
42
+ const { projectDirectory } = resolved;
43
+ const sessionId = await getThreadSession(channel.id);
44
+ if (!sessionId) {
45
+ await interaction.reply({
46
+ content: 'No active session in this thread',
47
+ flags: MessageFlags.Ephemeral,
48
+ });
49
+ return;
50
+ }
51
+ // Defer reply before API calls to avoid 3-second timeout
52
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
53
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
54
+ if (getClient instanceof Error) {
55
+ await interaction.editReply({
56
+ content: `Failed to load messages: ${getClient.message}`,
57
+ });
58
+ return;
59
+ }
60
+ try {
61
+ const messagesResponse = await getClient().session.messages({
62
+ sessionID: sessionId,
63
+ });
64
+ if (!messagesResponse.data) {
65
+ await interaction.editReply({
66
+ content: 'Failed to fetch session messages',
67
+ });
68
+ return;
69
+ }
70
+ const userMessages = messagesResponse.data.filter((m) => m.info.role === 'user');
71
+ if (userMessages.length === 0) {
72
+ await interaction.editReply({
73
+ content: 'No user messages found in this session',
74
+ });
75
+ return;
76
+ }
77
+ const recentMessages = userMessages.slice(-25);
78
+ // Filter out synthetic parts (branch context, memory reminders, etc.)
79
+ // injected by the opencode plugin — they clutter the dropdown preview.
80
+ const options = recentMessages
81
+ .map((m, index) => {
82
+ const textPart = m.parts.find((p) => p.type === 'text' && !p.synthetic);
83
+ if (!textPart?.text) {
84
+ return null;
85
+ }
86
+ const preview = textPart.text.slice(0, 80);
87
+ const label = `${index + 1}. ${preview}${preview.length >= 80 ? '...' : ''}`;
88
+ return {
89
+ label: label.slice(0, 100),
90
+ value: m.info.id,
91
+ description: new Date(m.info.time.created)
92
+ .toLocaleString()
93
+ .slice(0, 50),
94
+ };
95
+ })
96
+ .filter((o) => o !== null);
97
+ const selectMenu = new StringSelectMenuBuilder()
98
+ // Discord component custom_id max length is 100 chars.
99
+ // Avoid embedding long directory paths (or base64 of them) in the custom ID.
100
+ // handleForkSelectMenu resolves the directory from the current thread instead.
101
+ .setCustomId(`fork_select:${sessionId}`)
102
+ .setPlaceholder('Select a message to fork from')
103
+ .addOptions(options);
104
+ const actionRow = new ActionRowBuilder().addComponents(selectMenu);
105
+ await interaction.editReply({
106
+ content: '**Fork Session**\nSelect the user message to fork from. The forked session will continue as if you had not sent that message:',
107
+ components: [actionRow],
108
+ });
109
+ }
110
+ catch (error) {
111
+ forkLogger.error('Error loading messages:', error);
112
+ await interaction.editReply({
113
+ content: `Failed to load messages: ${error instanceof Error ? error.message : 'Unknown error'}`,
114
+ });
115
+ }
116
+ }
117
+ export async function handleForkSelectMenu(interaction) {
118
+ const customId = interaction.customId;
119
+ if (!customId.startsWith('fork_select:')) {
120
+ return;
121
+ }
122
+ const [, sessionId] = customId.split(':');
123
+ if (!sessionId) {
124
+ await interaction.reply({
125
+ content: 'Invalid selection data',
126
+ flags: MessageFlags.Ephemeral,
127
+ });
128
+ return;
129
+ }
130
+ const selectedMessageId = interaction.values[0];
131
+ if (!selectedMessageId) {
132
+ await interaction.reply({
133
+ content: 'No message selected',
134
+ flags: MessageFlags.Ephemeral,
135
+ });
136
+ return;
137
+ }
138
+ await interaction.deferReply({ ephemeral: false });
139
+ const threadChannel = interaction.channel;
140
+ if (!threadChannel) {
141
+ await interaction.editReply('Could not access thread channel');
142
+ return;
143
+ }
144
+ const resolved = await resolveWorkingDirectory({
145
+ channel: threadChannel,
146
+ });
147
+ if (!resolved) {
148
+ await interaction.editReply('Could not determine project directory for this channel');
149
+ return;
150
+ }
151
+ const { projectDirectory } = resolved;
152
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
153
+ if (getClient instanceof Error) {
154
+ await interaction.editReply(`Failed to fork session: ${getClient.message}`);
155
+ return;
156
+ }
157
+ try {
158
+ const forkResponse = await getClient().session.fork({
159
+ sessionID: sessionId,
160
+ messageID: selectedMessageId,
161
+ });
162
+ if (!forkResponse.data) {
163
+ await interaction.editReply('Failed to fork session');
164
+ return;
165
+ }
166
+ const forkedSession = forkResponse.data;
167
+ const parentChannel = interaction.channel;
168
+ if (!parentChannel ||
169
+ ![
170
+ ChannelType.PublicThread,
171
+ ChannelType.PrivateThread,
172
+ ChannelType.AnnouncementThread,
173
+ ].includes(parentChannel.type)) {
174
+ await interaction.editReply('Could not access parent channel');
175
+ return;
176
+ }
177
+ const textChannel = await resolveTextChannel(parentChannel);
178
+ if (!textChannel) {
179
+ await interaction.editReply('Could not resolve parent text channel');
180
+ return;
181
+ }
182
+ const thread = await textChannel.threads.create({
183
+ name: `Fork: ${forkedSession.title}`.slice(0, 100),
184
+ autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
185
+ reason: `Forked from session ${sessionId}`,
186
+ });
187
+ // Add user to thread so it appears in their sidebar
188
+ await thread.members.add(interaction.user.id);
189
+ await setThreadSession(thread.id, forkedSession.id);
190
+ sessionLogger.log(`Created forked session ${forkedSession.id} in thread ${thread.id}`);
191
+ await sendThreadMessage(thread, `**Forked session created!**\nFrom: \`${sessionId}\`\nNew session: \`${forkedSession.id}\``);
192
+ // Fetch and display the last assistant messages from the forked session
193
+ const messagesResponse = await getClient().session.messages({
194
+ sessionID: forkedSession.id,
195
+ });
196
+ if (messagesResponse.data) {
197
+ const { partIds, content } = collectLastAssistantParts({
198
+ messages: messagesResponse.data,
199
+ });
200
+ if (content.trim()) {
201
+ const discordMessage = await sendThreadMessage(thread, content);
202
+ // Store part-message mappings atomically
203
+ await setPartMessagesBatch(partIds.map((partId) => ({
204
+ partId,
205
+ messageId: discordMessage.id,
206
+ threadId: thread.id,
207
+ })));
208
+ }
209
+ }
210
+ await sendThreadMessage(thread, `You can now continue the conversation from this point.`);
211
+ await interaction.editReply(`Session forked! Continue in ${thread.toString()}`);
212
+ }
213
+ catch (error) {
214
+ forkLogger.error('Error forking session:', error);
215
+ await interaction.editReply(`Failed to fork session: ${error instanceof Error ? error.message : 'Unknown error'}`);
216
+ }
217
+ }
@@ -0,0 +1,70 @@
1
+ // Transcription API key button, slash command, and modal handlers.
2
+ // Auto-detects provider from key prefix: sk-* = OpenAI, otherwise Gemini.
3
+ import { ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, MessageFlags, } from 'discord.js';
4
+ import { setGeminiApiKey, setOpenAIApiKey } from '../database.js';
5
+ function buildTranscriptionApiKeyModal(appId) {
6
+ const modal = new ModalBuilder()
7
+ .setCustomId(`transcription_apikey_modal:${appId}`)
8
+ .setTitle('Transcription API Key');
9
+ const apiKeyInput = new TextInputBuilder()
10
+ .setCustomId('apikey')
11
+ .setLabel('OpenAI or Gemini API Key')
12
+ .setPlaceholder('sk-... or AIza...')
13
+ .setStyle(TextInputStyle.Short)
14
+ .setRequired(true);
15
+ const actionRow = new ActionRowBuilder().addComponents(apiKeyInput);
16
+ modal.addComponents(actionRow);
17
+ return modal;
18
+ }
19
+ export async function handleTranscriptionApiKeyButton(interaction) {
20
+ if (!interaction.customId.startsWith('transcription_apikey:'))
21
+ return;
22
+ const appId = interaction.customId
23
+ .slice('transcription_apikey:'.length)
24
+ .trim();
25
+ if (!appId) {
26
+ await interaction.reply({
27
+ content: 'Missing app id for API key setup.',
28
+ flags: MessageFlags.Ephemeral,
29
+ });
30
+ return;
31
+ }
32
+ await interaction.showModal(buildTranscriptionApiKeyModal(appId));
33
+ }
34
+ export async function handleTranscriptionApiKeyCommand({ interaction, appId, }) {
35
+ await interaction.showModal(buildTranscriptionApiKeyModal(appId));
36
+ }
37
+ export async function handleTranscriptionApiKeyModalSubmit(interaction) {
38
+ if (!interaction.customId.startsWith('transcription_apikey_modal:'))
39
+ return;
40
+ const appId = interaction.customId
41
+ .slice('transcription_apikey_modal:'.length)
42
+ .trim();
43
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
44
+ if (!appId) {
45
+ await interaction.editReply({
46
+ content: 'Missing app id for API key setup.',
47
+ });
48
+ return;
49
+ }
50
+ const apiKey = interaction.fields.getTextInputValue('apikey').trim();
51
+ if (!apiKey) {
52
+ await interaction.editReply({
53
+ content: 'API key is required.',
54
+ });
55
+ return;
56
+ }
57
+ // Auto-detect provider from key prefix
58
+ if (apiKey.startsWith('sk-')) {
59
+ await setOpenAIApiKey(appId, apiKey);
60
+ await interaction.editReply({
61
+ content: 'OpenAI API key saved. Voice messages will be transcribed with OpenAI.',
62
+ });
63
+ }
64
+ else {
65
+ await setGeminiApiKey(appId, apiKey);
66
+ await interaction.editReply({
67
+ content: 'Gemini API key saved. Voice messages will be transcribed with Gemini.',
68
+ });
69
+ }
70
+ }