@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,159 @@
1
+ // Undo/Redo commands - /undo, /redo
2
+ import { ChannelType, MessageFlags, } from 'discord.js';
3
+ import { getThreadSession } from '../database.js';
4
+ import { initializeOpencodeForDirectory } from '../opencode.js';
5
+ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ const logger = createLogger(LogPrefix.UNDO_REDO);
8
+ export async function handleUndoCommand({ command, }) {
9
+ const channel = command.channel;
10
+ if (!channel) {
11
+ await command.reply({
12
+ content: 'This command can only be used in a channel',
13
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
14
+ });
15
+ return;
16
+ }
17
+ const isThread = [
18
+ ChannelType.PublicThread,
19
+ ChannelType.PrivateThread,
20
+ ChannelType.AnnouncementThread,
21
+ ].includes(channel.type);
22
+ if (!isThread) {
23
+ await command.reply({
24
+ content: 'This command can only be used in a thread with an active session',
25
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
26
+ });
27
+ return;
28
+ }
29
+ const resolved = await resolveWorkingDirectory({
30
+ channel: channel,
31
+ });
32
+ if (!resolved) {
33
+ await command.reply({
34
+ content: 'Could not determine project directory for this channel',
35
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
36
+ });
37
+ return;
38
+ }
39
+ const { projectDirectory } = resolved;
40
+ const sessionId = await getThreadSession(channel.id);
41
+ if (!sessionId) {
42
+ await command.reply({
43
+ content: 'No active session in this thread',
44
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
45
+ });
46
+ return;
47
+ }
48
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
49
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
50
+ if (getClient instanceof Error) {
51
+ await command.editReply(`Failed to undo: ${getClient.message}`);
52
+ return;
53
+ }
54
+ try {
55
+ // Fetch messages to find the last assistant message
56
+ const messagesResponse = await getClient().session.messages({
57
+ sessionID: sessionId,
58
+ });
59
+ if (!messagesResponse.data || messagesResponse.data.length === 0) {
60
+ await command.editReply('No messages to undo');
61
+ return;
62
+ }
63
+ // Find the last assistant message
64
+ const lastAssistantMessage = [...messagesResponse.data]
65
+ .reverse()
66
+ .find((m) => m.info.role === 'assistant');
67
+ if (!lastAssistantMessage) {
68
+ await command.editReply('No assistant message to undo');
69
+ return;
70
+ }
71
+ const response = await getClient().session.revert({
72
+ sessionID: sessionId,
73
+ messageID: lastAssistantMessage.info.id,
74
+ });
75
+ if (response.error) {
76
+ await command.editReply(`Failed to undo: ${JSON.stringify(response.error)}`);
77
+ return;
78
+ }
79
+ const diffInfo = response.data?.revert?.diff
80
+ ? `\n\`\`\`diff\n${response.data.revert.diff.slice(0, 1500)}\n\`\`\``
81
+ : '';
82
+ await command.editReply(`⏪ **Undone** - reverted last assistant message${diffInfo}`);
83
+ logger.log(`Session ${sessionId} reverted message ${lastAssistantMessage.info.id}`);
84
+ }
85
+ catch (error) {
86
+ logger.error('[UNDO] Error:', error);
87
+ await command.editReply(`Failed to undo: ${error instanceof Error ? error.message : 'Unknown error'}`);
88
+ }
89
+ }
90
+ export async function handleRedoCommand({ command, }) {
91
+ const channel = command.channel;
92
+ if (!channel) {
93
+ await command.reply({
94
+ content: 'This command can only be used in a channel',
95
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
96
+ });
97
+ return;
98
+ }
99
+ const isThread = [
100
+ ChannelType.PublicThread,
101
+ ChannelType.PrivateThread,
102
+ ChannelType.AnnouncementThread,
103
+ ].includes(channel.type);
104
+ if (!isThread) {
105
+ await command.reply({
106
+ content: 'This command can only be used in a thread with an active session',
107
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
108
+ });
109
+ return;
110
+ }
111
+ const resolved = await resolveWorkingDirectory({
112
+ channel: channel,
113
+ });
114
+ if (!resolved) {
115
+ await command.reply({
116
+ content: 'Could not determine project directory for this channel',
117
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
118
+ });
119
+ return;
120
+ }
121
+ const { projectDirectory } = resolved;
122
+ const sessionId = await getThreadSession(channel.id);
123
+ if (!sessionId) {
124
+ await command.reply({
125
+ content: 'No active session in this thread',
126
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
127
+ });
128
+ return;
129
+ }
130
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
131
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
132
+ if (getClient instanceof Error) {
133
+ await command.editReply(`Failed to redo: ${getClient.message}`);
134
+ return;
135
+ }
136
+ try {
137
+ // Check if session has reverted state
138
+ const sessionResponse = await getClient().session.get({
139
+ sessionID: sessionId,
140
+ });
141
+ if (!sessionResponse.data?.revert) {
142
+ await command.editReply('Nothing to redo - no previous undo found');
143
+ return;
144
+ }
145
+ const response = await getClient().session.unrevert({
146
+ sessionID: sessionId,
147
+ });
148
+ if (response.error) {
149
+ await command.editReply(`Failed to redo: ${JSON.stringify(response.error)}`);
150
+ return;
151
+ }
152
+ await command.editReply(`⏩ **Restored** - session back to previous state`);
153
+ logger.log(`Session ${sessionId} unrevert completed`);
154
+ }
155
+ catch (error) {
156
+ logger.error('[REDO] Error:', error);
157
+ await command.editReply(`Failed to redo: ${error instanceof Error ? error.message : 'Unknown error'}`);
158
+ }
159
+ }
@@ -0,0 +1,152 @@
1
+ // /unset-model-override command - Remove model overrides and use default instead.
2
+ import { ChatInputCommandInteraction, ChannelType, MessageFlags, } from 'discord.js';
3
+ import { getChannelModel, getSessionModel, getThreadSession, clearSessionModel, } from '../database.js';
4
+ import { getPrisma } from '../db.js';
5
+ import { initializeOpencodeForDirectory } from '../opencode.js';
6
+ import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
7
+ import { abortAndRetrySession } from '../session-handler.js';
8
+ import { getCurrentModelInfo } from './model.js';
9
+ import { createLogger, LogPrefix } from '../logger.js';
10
+ const unsetModelLogger = createLogger(LogPrefix.MODEL);
11
+ function formatModelSource(type, agentName) {
12
+ switch (type) {
13
+ case 'session':
14
+ return 'session override';
15
+ case 'agent':
16
+ return `agent "${agentName}"`;
17
+ case 'channel':
18
+ return 'channel override';
19
+ case 'global':
20
+ return 'global default';
21
+ case 'opencode-config':
22
+ case 'opencode-recent':
23
+ case 'opencode-provider-default':
24
+ return 'opencode default';
25
+ default:
26
+ return 'none';
27
+ }
28
+ }
29
+ /**
30
+ * Handle the /unset-model-override slash command.
31
+ * In thread: clears session override if exists, otherwise channel override.
32
+ * In channel: clears channel override.
33
+ */
34
+ export async function handleUnsetModelCommand({ interaction, appId, }) {
35
+ unsetModelLogger.log('[UNSET-MODEL] handleUnsetModelCommand called');
36
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
37
+ const channel = interaction.channel;
38
+ if (!channel) {
39
+ await interaction.editReply({
40
+ content: 'This command can only be used in a channel',
41
+ });
42
+ return;
43
+ }
44
+ const isThread = [
45
+ ChannelType.PublicThread,
46
+ ChannelType.PrivateThread,
47
+ ChannelType.AnnouncementThread,
48
+ ].includes(channel.type);
49
+ let projectDirectory;
50
+ let channelAppId;
51
+ let targetChannelId;
52
+ let sessionId;
53
+ if (isThread) {
54
+ const thread = channel;
55
+ const textChannel = await resolveTextChannel(thread);
56
+ const metadata = await getKimakiMetadata(textChannel);
57
+ projectDirectory = metadata.projectDirectory;
58
+ channelAppId = metadata.channelAppId;
59
+ targetChannelId = textChannel?.id || channel.id;
60
+ sessionId = await getThreadSession(thread.id);
61
+ }
62
+ else if (channel.type === ChannelType.GuildText) {
63
+ const textChannel = channel;
64
+ const metadata = await getKimakiMetadata(textChannel);
65
+ projectDirectory = metadata.projectDirectory;
66
+ channelAppId = metadata.channelAppId;
67
+ targetChannelId = channel.id;
68
+ }
69
+ else {
70
+ await interaction.editReply({
71
+ content: 'This command can only be used in text channels or threads',
72
+ });
73
+ return;
74
+ }
75
+ if (channelAppId && channelAppId !== appId) {
76
+ await interaction.editReply({
77
+ content: 'This channel is not configured for this bot',
78
+ });
79
+ return;
80
+ }
81
+ if (!projectDirectory) {
82
+ await interaction.editReply({
83
+ content: 'This channel is not configured with a project directory',
84
+ });
85
+ return;
86
+ }
87
+ const effectiveAppId = channelAppId || appId;
88
+ // Check what overrides exist
89
+ const [sessionPref, channelPref] = await Promise.all([
90
+ sessionId ? getSessionModel(sessionId) : Promise.resolve(undefined),
91
+ getChannelModel(targetChannelId),
92
+ ]);
93
+ let clearedType = null;
94
+ let clearedModel;
95
+ if (isThread && sessionId && sessionPref) {
96
+ // In thread with session override: clear session
97
+ await clearSessionModel(sessionId);
98
+ clearedType = 'session';
99
+ clearedModel = sessionPref.modelId;
100
+ unsetModelLogger.log(`[UNSET-MODEL] Cleared session model for ${sessionId}`);
101
+ }
102
+ else if (channelPref) {
103
+ // Clear channel override
104
+ const prisma = await getPrisma();
105
+ await prisma.channel_models.deleteMany({
106
+ where: { channel_id: targetChannelId },
107
+ });
108
+ clearedType = 'channel';
109
+ clearedModel = channelPref.modelId;
110
+ unsetModelLogger.log(`[UNSET-MODEL] Cleared channel model for ${targetChannelId}`);
111
+ }
112
+ else {
113
+ await interaction.editReply({
114
+ content: 'No model override to clear.',
115
+ });
116
+ return;
117
+ }
118
+ // Get the new model that will be used
119
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
120
+ let newModelText = 'unknown';
121
+ if (!(getClient instanceof Error)) {
122
+ const newModelInfo = await getCurrentModelInfo({
123
+ sessionId,
124
+ channelId: targetChannelId,
125
+ appId: effectiveAppId,
126
+ getClient,
127
+ });
128
+ newModelText =
129
+ newModelInfo.type === 'none'
130
+ ? 'none'
131
+ : `\`${newModelInfo.model}\` (${formatModelSource(newModelInfo.type, 'agentName' in newModelInfo ? newModelInfo.agentName : undefined)})`;
132
+ }
133
+ // Check if there's a running request and abort+retry with new model (only for session changes in threads)
134
+ let retried = false;
135
+ if (isThread && clearedType === 'session' && sessionId) {
136
+ const thread = channel;
137
+ retried = await abortAndRetrySession({
138
+ sessionId,
139
+ thread,
140
+ projectDirectory,
141
+ appId: effectiveAppId,
142
+ channelId: targetChannelId,
143
+ });
144
+ }
145
+ const clearedTypeText = clearedType === 'session' ? 'Session' : 'Channel';
146
+ const retriedText = retried
147
+ ? '\n_Retrying current request with new model..._'
148
+ : '';
149
+ await interaction.editReply({
150
+ content: `${clearedTypeText} model override removed.\n**Was:** \`${clearedModel}\`\n**Now using:** ${newModelText}${retriedText}`,
151
+ });
152
+ }
@@ -0,0 +1,42 @@
1
+ // /upgrade-and-restart command - Upgrade kimaki to the latest version and restart the bot.
2
+ // Checks npm for a newer version, installs it globally, then spawns a new kimaki process.
3
+ // The new process kills the old one on startup (kimaki's single-instance lock).
4
+ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
5
+ import { createLogger, LogPrefix } from '../logger.js';
6
+ import { getCurrentVersion, upgrade } from '../upgrade.js';
7
+ import { spawn } from 'node:child_process';
8
+ const logger = createLogger(LogPrefix.CLI);
9
+ export async function handleUpgradeAndRestartCommand({ command, }) {
10
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
11
+ logger.log('[UPGRADE] /upgrade-and-restart triggered');
12
+ try {
13
+ const currentVersion = getCurrentVersion();
14
+ const newVersion = await upgrade();
15
+ if (!newVersion) {
16
+ await command.editReply({
17
+ content: `Already on latest version: **v${currentVersion}**`,
18
+ });
19
+ return;
20
+ }
21
+ await command.editReply({
22
+ content: `Upgraded kimaki **v${currentVersion}** -> **v${newVersion}**. Restarting bot...`,
23
+ });
24
+ // Spawning bare `kimaki` works even if the user originally ran via npx/bunx:
25
+ // `npm i -g kimaki@latest` creates a global bin link, and npx resolves
26
+ // local -> global -> cache -> registry, so it prefers the global install.
27
+ // bunx shares the same global cache, so it also picks up the new version.
28
+ const child = spawn('kimaki', process.argv.slice(2), {
29
+ shell: true,
30
+ stdio: 'ignore',
31
+ detached: true,
32
+ });
33
+ child.unref();
34
+ logger.debug('Started new background kimaki');
35
+ }
36
+ catch (error) {
37
+ logger.error('[UPGRADE] Failed:', error);
38
+ await command.editReply({
39
+ content: `Upgrade failed: ${error instanceof Error ? error.message : String(error)}`,
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,148 @@
1
+ // User-defined OpenCode command handler.
2
+ // Handles slash commands that map to user-configured commands in opencode.json.
3
+ import { ChannelType, MessageFlags, } from 'discord.js';
4
+ import { handleOpencodeSession } from '../session-handler.js';
5
+ import { sendThreadMessage, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ import { getChannelDirectory, getThreadSession } from '../database.js';
8
+ import { registeredUserCommands } from '../config.js';
9
+ import fs from 'node:fs';
10
+ const userCommandLogger = createLogger(LogPrefix.USER_CMD);
11
+ export const handleUserCommand = async ({ command, appId, }) => {
12
+ const discordCommandName = command.commandName;
13
+ // Look up the original OpenCode command name from the mapping populated at registration.
14
+ // The sanitized Discord name is lossy (e.g. foo:bar → foo-bar), so stripping -cmd
15
+ // would give the wrong name for commands with special characters.
16
+ const sanitizedBase = discordCommandName.replace(/-cmd$/, '');
17
+ const registered = registeredUserCommands.find((c) => c.discordName === sanitizedBase);
18
+ const commandName = registered?.name || sanitizedBase;
19
+ const args = command.options.getString('arguments') || '';
20
+ userCommandLogger.log(`Executing /${commandName} (from /${discordCommandName}) argsLength=${args.length}`);
21
+ const channel = command.channel;
22
+ userCommandLogger.log(`Channel info: type=${channel?.type}, id=${channel?.id}, isNull=${channel === null}`);
23
+ const isThread = channel &&
24
+ [
25
+ ChannelType.PublicThread,
26
+ ChannelType.PrivateThread,
27
+ ChannelType.AnnouncementThread,
28
+ ].includes(channel.type);
29
+ const isTextChannel = channel?.type === ChannelType.GuildText;
30
+ if (!channel || (!isTextChannel && !isThread)) {
31
+ await command.reply({
32
+ content: 'This command can only be used in text channels or threads',
33
+ flags: MessageFlags.Ephemeral,
34
+ });
35
+ return;
36
+ }
37
+ let projectDirectory;
38
+ let channelAppId;
39
+ let textChannel = null;
40
+ let thread = null;
41
+ if (isThread) {
42
+ // Running in an existing thread - get project directory from parent channel
43
+ thread = channel;
44
+ textChannel = thread.parent;
45
+ // Verify this thread has an existing session
46
+ const sessionId = await getThreadSession(thread.id);
47
+ if (!sessionId) {
48
+ await command.reply({
49
+ content: 'This thread does not have an active session. Use this command in a project channel to create a new thread.',
50
+ flags: MessageFlags.Ephemeral,
51
+ });
52
+ return;
53
+ }
54
+ if (textChannel) {
55
+ const channelConfig = await getChannelDirectory(textChannel.id);
56
+ projectDirectory = channelConfig?.directory;
57
+ channelAppId = channelConfig?.appId || undefined;
58
+ }
59
+ }
60
+ else {
61
+ // Running in a text channel - will create a new thread
62
+ textChannel = channel;
63
+ const channelConfig = await getChannelDirectory(textChannel.id);
64
+ projectDirectory = channelConfig?.directory;
65
+ channelAppId = channelConfig?.appId || undefined;
66
+ }
67
+ if (channelAppId && channelAppId !== appId) {
68
+ await command.reply({
69
+ content: 'This channel is not configured for this bot',
70
+ flags: MessageFlags.Ephemeral,
71
+ });
72
+ return;
73
+ }
74
+ if (!projectDirectory) {
75
+ await command.reply({
76
+ content: 'This channel is not configured with a project directory',
77
+ flags: MessageFlags.Ephemeral,
78
+ });
79
+ return;
80
+ }
81
+ if (!fs.existsSync(projectDirectory)) {
82
+ await command.reply({
83
+ content: `Directory does not exist: ${projectDirectory}`,
84
+ flags: MessageFlags.Ephemeral,
85
+ });
86
+ return;
87
+ }
88
+ await command.deferReply({ ephemeral: false });
89
+ try {
90
+ // Use the dedicated session.command API instead of formatting as text prompt
91
+ const commandPayload = { name: commandName, arguments: args };
92
+ if (isThread && thread) {
93
+ // Running in existing thread - just send the command
94
+ await command.editReply(`Running /${commandName}...`);
95
+ await handleOpencodeSession({
96
+ prompt: '', // Not used when command is set
97
+ thread,
98
+ projectDirectory,
99
+ channelId: textChannel?.id,
100
+ command: commandPayload,
101
+ appId,
102
+ });
103
+ }
104
+ else if (textChannel) {
105
+ // Running in text channel - create a new thread
106
+ const starterMessage = await textChannel.send({
107
+ content: `**/${commandName}**`,
108
+ flags: SILENT_MESSAGE_FLAGS,
109
+ });
110
+ const threadName = `/${commandName}`;
111
+ const newThread = await starterMessage.startThread({
112
+ name: threadName.slice(0, 100),
113
+ autoArchiveDuration: 1440,
114
+ reason: `OpenCode command: ${commandName}`,
115
+ });
116
+ // Add user to thread so it appears in their sidebar
117
+ await newThread.members.add(command.user.id);
118
+ if (args) {
119
+ const argsPreview = args.length > 1800 ? `${args.slice(0, 1800)}\n... truncated` : args;
120
+ await sendThreadMessage(newThread, `Args: ${argsPreview}`);
121
+ }
122
+ await command.editReply(`Started /${commandName} in ${newThread.toString()}`);
123
+ await handleOpencodeSession({
124
+ prompt: '', // Not used when command is set
125
+ thread: newThread,
126
+ projectDirectory,
127
+ channelId: textChannel.id,
128
+ command: commandPayload,
129
+ appId,
130
+ });
131
+ }
132
+ }
133
+ catch (error) {
134
+ userCommandLogger.error(`Error executing /${commandName}:`, error);
135
+ const errorMessage = error instanceof Error ? error.message : String(error);
136
+ if (command.deferred) {
137
+ await command.editReply({
138
+ content: `Failed to execute /${commandName}: ${errorMessage}`,
139
+ });
140
+ }
141
+ else {
142
+ await command.reply({
143
+ content: `Failed to execute /${commandName}: ${errorMessage}`,
144
+ flags: MessageFlags.Ephemeral,
145
+ });
146
+ }
147
+ }
148
+ };
@@ -0,0 +1,60 @@
1
+ // /verbosity command.
2
+ // Sets the output verbosity level for sessions in a channel.
3
+ // 'text-and-essential-tools' (default): shows text and essential tools (edits, custom MCP tools)
4
+ // 'tools-and-text': shows all output including tool executions
5
+ // 'text-only': only shows text responses (⬥ diamond parts)
6
+ import { ChatInputCommandInteraction, MessageFlags, ChannelType, } from 'discord.js';
7
+ import { getChannelVerbosity, setChannelVerbosity, } from '../database.js';
8
+ import { createLogger, LogPrefix } from '../logger.js';
9
+ const verbosityLogger = createLogger(LogPrefix.VERBOSITY);
10
+ /**
11
+ * Handle the /verbosity slash command.
12
+ * Sets output verbosity for the channel (applies immediately, even mid-session).
13
+ */
14
+ export async function handleVerbosityCommand({ command, appId, }) {
15
+ verbosityLogger.log('[VERBOSITY] Command called');
16
+ const channel = command.channel;
17
+ if (!channel) {
18
+ await command.reply({
19
+ content: 'Could not determine channel.',
20
+ flags: MessageFlags.Ephemeral,
21
+ });
22
+ return;
23
+ }
24
+ // Get the parent channel ID (for threads, use parent; for text channels, use self)
25
+ const channelId = (() => {
26
+ if (channel.type === ChannelType.GuildText) {
27
+ return channel.id;
28
+ }
29
+ if (channel.type === ChannelType.PublicThread ||
30
+ channel.type === ChannelType.PrivateThread ||
31
+ channel.type === ChannelType.AnnouncementThread) {
32
+ return channel.parentId || channel.id;
33
+ }
34
+ return channel.id;
35
+ })();
36
+ const level = command.options.getString('level', true);
37
+ const currentLevel = await getChannelVerbosity(channelId);
38
+ if (currentLevel === level) {
39
+ await command.reply({
40
+ content: `Verbosity is already set to **${level}** for this channel.`,
41
+ flags: MessageFlags.Ephemeral,
42
+ });
43
+ return;
44
+ }
45
+ await setChannelVerbosity(channelId, level);
46
+ verbosityLogger.log(`[VERBOSITY] Set channel ${channelId} to ${level}`);
47
+ const description = (() => {
48
+ if (level === 'text-only') {
49
+ return 'Only text responses will be shown. Tool executions, status messages, and thinking will be hidden.';
50
+ }
51
+ if (level === 'text-and-essential-tools') {
52
+ return 'Text responses and essential tools (edits, custom MCP tools) will be shown. Read, search, and navigation tools will be hidden.';
53
+ }
54
+ return 'All output will be shown, including tool executions and status messages.';
55
+ })();
56
+ await command.reply({
57
+ content: `Verbosity set to **${level}** for this channel.\n${description}\nThis is a per-channel setting and applies immediately, including any active sessions.`,
58
+ flags: MessageFlags.Ephemeral,
59
+ });
60
+ }
@@ -0,0 +1,50 @@
1
+ // /toggle-worktrees command.
2
+ // Allows per-channel opt-in for automatic worktree creation,
3
+ // as an alternative to the global --use-worktrees CLI flag.
4
+ import { ChatInputCommandInteraction, MessageFlags, ChannelType, } from 'discord.js';
5
+ import { getChannelWorktreesEnabled, setChannelWorktreesEnabled, } from '../database.js';
6
+ import { getKimakiMetadata } from '../discord-utils.js';
7
+ import { createLogger, LogPrefix } from '../logger.js';
8
+ const worktreeSettingsLogger = createLogger(LogPrefix.WORKTREE);
9
+ /**
10
+ * Handle the /toggle-worktrees slash command.
11
+ * Toggles automatic worktree creation for new sessions in this channel.
12
+ */
13
+ export async function handleToggleWorktreesCommand({ command, appId, }) {
14
+ worktreeSettingsLogger.log('[TOGGLE_WORKTREES] Command called');
15
+ const channel = command.channel;
16
+ if (!channel || channel.type !== ChannelType.GuildText) {
17
+ await command.reply({
18
+ content: 'This command can only be used in text channels (not threads).',
19
+ flags: MessageFlags.Ephemeral,
20
+ });
21
+ return;
22
+ }
23
+ const textChannel = channel;
24
+ const metadata = await getKimakiMetadata(textChannel);
25
+ if (metadata.channelAppId && metadata.channelAppId !== appId) {
26
+ await command.reply({
27
+ content: 'This channel is configured for a different bot.',
28
+ flags: MessageFlags.Ephemeral,
29
+ });
30
+ return;
31
+ }
32
+ if (!metadata.projectDirectory) {
33
+ await command.reply({
34
+ content: 'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
35
+ flags: MessageFlags.Ephemeral,
36
+ });
37
+ return;
38
+ }
39
+ const wasEnabled = await getChannelWorktreesEnabled(textChannel.id);
40
+ const nextEnabled = !wasEnabled;
41
+ await setChannelWorktreesEnabled(textChannel.id, nextEnabled);
42
+ const nextLabel = nextEnabled ? 'enabled' : 'disabled';
43
+ worktreeSettingsLogger.log(`[TOGGLE_WORKTREES] ${nextLabel.toUpperCase()} for channel ${textChannel.id}`);
44
+ await command.reply({
45
+ content: nextEnabled
46
+ ? `Worktrees **enabled** for this channel.\n\nNew sessions started from messages in **#${textChannel.name}** will now automatically create git worktrees.\n\nNew setting for **#${textChannel.name}**: **enabled**.`
47
+ : `Worktrees **disabled** for this channel.\n\nNew sessions started from messages in **#${textChannel.name}** will use the main project directory.\n\nNew setting for **#${textChannel.name}**: **disabled**.`,
48
+ flags: MessageFlags.Ephemeral,
49
+ });
50
+ }