@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,162 @@
1
+ // /restart-opencode-server command - Restart the opencode server for the current channel.
2
+ // Used for resolving opencode state issues, internal bugs, refreshing auth state, plugins, etc.
3
+ // Aborts all in-progress sessions in this channel before restarting to avoid orphaned requests.
4
+
5
+ import {
6
+ ChannelType,
7
+ MessageFlags,
8
+ type ThreadChannel,
9
+ type TextChannel,
10
+ } from 'discord.js'
11
+ import type { CommandContext } from './types.js'
12
+ import {
13
+ initializeOpencodeForDirectory,
14
+ restartOpencodeServer,
15
+ } from '../opencode.js'
16
+ import {
17
+ resolveWorkingDirectory,
18
+ SILENT_MESSAGE_FLAGS,
19
+ } from '../discord-utils.js'
20
+ import { createLogger, LogPrefix } from '../logger.js'
21
+ import { getAllThreadSessionIds, getThreadIdBySessionId } from '../database.js'
22
+ import { abortControllers } from '../session-handler.js'
23
+ import { SessionAbortError } from '../errors.js'
24
+ import * as errore from 'errore'
25
+
26
+ const logger = createLogger(LogPrefix.OPENCODE)
27
+
28
+ export async function handleRestartOpencodeServerCommand({
29
+ command,
30
+ appId,
31
+ }: CommandContext): Promise<void> {
32
+ const channel = command.channel
33
+
34
+ if (!channel) {
35
+ await command.reply({
36
+ content: 'This command can only be used in a channel',
37
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
38
+ })
39
+ return
40
+ }
41
+
42
+ const isThread = [
43
+ ChannelType.PublicThread,
44
+ ChannelType.PrivateThread,
45
+ ChannelType.AnnouncementThread,
46
+ ].includes(channel.type)
47
+
48
+ const isTextChannel = channel.type === ChannelType.GuildText
49
+
50
+ if (!isThread && !isTextChannel) {
51
+ await command.reply({
52
+ content: 'This command can only be used in text channels or threads',
53
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
54
+ })
55
+ return
56
+ }
57
+
58
+ const resolved = await resolveWorkingDirectory({
59
+ channel: channel as TextChannel | ThreadChannel,
60
+ })
61
+
62
+ if (!resolved) {
63
+ await command.reply({
64
+ content: 'Could not determine project directory for this channel',
65
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
66
+ })
67
+ return
68
+ }
69
+
70
+ const { projectDirectory, channelAppId } = resolved
71
+
72
+ if (channelAppId && channelAppId !== appId) {
73
+ await command.reply({
74
+ content: 'This channel is not configured for this bot',
75
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
76
+ })
77
+ return
78
+ }
79
+
80
+ // Defer reply since restart may take a moment
81
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
82
+
83
+ // Abort all in-progress sessions in this channel before restarting.
84
+ // Find sessions with active abort controllers, check if their thread belongs
85
+ // to this channel (thread parentId matches, or command was run in the thread itself).
86
+ const parentChannelId = isThread
87
+ ? (channel as ThreadChannel).parentId
88
+ : channel.id
89
+ const activeSessionIds = [...abortControllers.keys()]
90
+ let abortedCount = 0
91
+
92
+ if (activeSessionIds.length > 0) {
93
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
94
+ const client = !(getClient instanceof Error) ? getClient : null
95
+
96
+ for (const sessionId of activeSessionIds) {
97
+ const threadId = await getThreadIdBySessionId(sessionId)
98
+ if (!threadId) {
99
+ continue
100
+ }
101
+ // Check if thread belongs to this channel: either the thread IS this channel,
102
+ // or the thread's parent matches the parent channel
103
+ const threadChannel = await errore.tryAsync(() => {
104
+ return command.client.channels.fetch(threadId)
105
+ })
106
+ if (threadChannel instanceof Error || !threadChannel) {
107
+ continue
108
+ }
109
+ const threadParentId =
110
+ 'parentId' in threadChannel ? threadChannel.parentId : null
111
+ if (threadId !== channel.id && threadParentId !== parentChannelId) {
112
+ continue
113
+ }
114
+
115
+ const controller = abortControllers.get(sessionId)
116
+ if (controller) {
117
+ logger.log(
118
+ `[RESTART] Aborting session ${sessionId} in thread ${threadId}`,
119
+ )
120
+ controller.abort(new SessionAbortError({ reason: 'server-restart' }))
121
+ abortControllers.delete(sessionId)
122
+ abortedCount++
123
+ }
124
+ if (client) {
125
+ await errore.tryAsync(() => {
126
+ return client().session.abort({ sessionID: sessionId })
127
+ })
128
+ }
129
+ }
130
+ }
131
+
132
+ if (abortedCount > 0) {
133
+ logger.log(
134
+ `[RESTART] Aborted ${abortedCount} active session(s) before restart`,
135
+ )
136
+ }
137
+
138
+ logger.log(
139
+ `[RESTART] Restarting opencode server for directory: ${projectDirectory}`,
140
+ )
141
+
142
+ const result = await restartOpencodeServer(projectDirectory)
143
+
144
+ if (result instanceof Error) {
145
+ logger.error('[RESTART] Failed:', result)
146
+ await command.editReply({
147
+ content: `Failed to restart opencode server: ${result.message}`,
148
+ })
149
+ return
150
+ }
151
+
152
+ const abortMsg =
153
+ abortedCount > 0
154
+ ? ` (aborted ${abortedCount} active session${abortedCount > 1 ? 's' : ''})`
155
+ : ''
156
+ await command.editReply({
157
+ content: `Opencode server **restarted** successfully${abortMsg}`,
158
+ })
159
+ logger.log(
160
+ `[RESTART] Opencode server restarted for directory: ${projectDirectory}`,
161
+ )
162
+ }
@@ -0,0 +1,242 @@
1
+ // /resume command - Resume an existing OpenCode session.
2
+
3
+ import {
4
+ ChannelType,
5
+ ThreadAutoArchiveDuration,
6
+ type TextChannel,
7
+ type ThreadChannel,
8
+ } from 'discord.js'
9
+ import fs from 'node:fs'
10
+ import type { CommandContext, AutocompleteContext } from './types.js'
11
+ import {
12
+ getChannelDirectory,
13
+ setThreadSession,
14
+ setPartMessagesBatch,
15
+ getAllThreadSessionIds,
16
+ } from '../database.js'
17
+ import { initializeOpencodeForDirectory } from '../opencode.js'
18
+ import { sendThreadMessage, resolveTextChannel } from '../discord-utils.js'
19
+ import { collectLastAssistantParts } from '../message-formatting.js'
20
+ import { createLogger, LogPrefix } from '../logger.js'
21
+ import * as errore from 'errore'
22
+
23
+ const logger = createLogger(LogPrefix.RESUME)
24
+
25
+ export async function handleResumeCommand({
26
+ command,
27
+ appId,
28
+ }: CommandContext): Promise<void> {
29
+ await command.deferReply({ ephemeral: false })
30
+
31
+ const sessionId = command.options.getString('session', true)
32
+ const channel = command.channel
33
+
34
+ const isThread =
35
+ channel &&
36
+ [
37
+ ChannelType.PublicThread,
38
+ ChannelType.PrivateThread,
39
+ ChannelType.AnnouncementThread,
40
+ ].includes(channel.type)
41
+
42
+ if (isThread) {
43
+ await command.editReply(
44
+ 'This command can only be used in project channels, not threads',
45
+ )
46
+ return
47
+ }
48
+
49
+ if (!channel || channel.type !== ChannelType.GuildText) {
50
+ await command.editReply('This command can only be used in text channels')
51
+ return
52
+ }
53
+
54
+ const textChannel = channel as TextChannel
55
+
56
+ const channelConfig = await getChannelDirectory(textChannel.id)
57
+ const projectDirectory = channelConfig?.directory
58
+ const channelAppId = channelConfig?.appId || undefined
59
+
60
+ if (channelAppId && channelAppId !== appId) {
61
+ await command.editReply('This channel is not configured for this bot')
62
+ return
63
+ }
64
+
65
+ if (!projectDirectory) {
66
+ await command.editReply(
67
+ 'This channel is not configured with a project directory',
68
+ )
69
+ return
70
+ }
71
+
72
+ if (!fs.existsSync(projectDirectory)) {
73
+ await command.editReply(`Directory does not exist: ${projectDirectory}`)
74
+ return
75
+ }
76
+
77
+ try {
78
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
79
+ if (getClient instanceof Error) {
80
+ await command.editReply(getClient.message)
81
+ return
82
+ }
83
+
84
+ const sessionResponse = await getClient().session.get({
85
+ sessionID: sessionId,
86
+ })
87
+
88
+ if (!sessionResponse.data) {
89
+ await command.editReply('Session not found')
90
+ return
91
+ }
92
+
93
+ const sessionTitle = sessionResponse.data.title
94
+
95
+ const thread = await textChannel.threads.create({
96
+ name: `Resume: ${sessionTitle}`.slice(0, 100),
97
+ autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
98
+ reason: `Resuming session ${sessionId}`,
99
+ })
100
+
101
+ // Add user to thread so it appears in their sidebar
102
+ await thread.members.add(command.user.id)
103
+
104
+ await setThreadSession(thread.id, sessionId)
105
+
106
+ logger.log(`[RESUME] Created thread ${thread.id} for session ${sessionId}`)
107
+
108
+ const messagesResponse = await getClient().session.messages({
109
+ sessionID: sessionId,
110
+ })
111
+
112
+ if (!messagesResponse.data) {
113
+ throw new Error('Failed to fetch session messages')
114
+ }
115
+
116
+ const messages = messagesResponse.data
117
+
118
+ await command.editReply(
119
+ `Resumed session "${sessionTitle}" in ${thread.toString()}`,
120
+ )
121
+
122
+ await sendThreadMessage(
123
+ thread,
124
+ `**Resumed session:** ${sessionTitle}\n**Created:** ${new Date(sessionResponse.data.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`,
125
+ )
126
+
127
+ try {
128
+ const { partIds, content, skippedCount } = collectLastAssistantParts({
129
+ messages,
130
+ })
131
+
132
+ if (skippedCount > 0) {
133
+ await sendThreadMessage(
134
+ thread,
135
+ `*Skipped ${skippedCount} older assistant parts...*`,
136
+ )
137
+ }
138
+
139
+ if (content.trim()) {
140
+ const discordMessage = await sendThreadMessage(thread, content)
141
+
142
+ // Store part-message mappings atomically
143
+ await setPartMessagesBatch(
144
+ partIds.map((partId) => ({
145
+ partId,
146
+ messageId: discordMessage.id,
147
+ threadId: thread.id,
148
+ })),
149
+ )
150
+ }
151
+
152
+ const messageCount = messages.length
153
+
154
+ await sendThreadMessage(
155
+ thread,
156
+ `**Session resumed!** Loaded ${messageCount} messages.\n\nYou can now continue the conversation by sending messages in this thread.`,
157
+ )
158
+ } catch (sendError) {
159
+ logger.error('[RESUME] Error sending messages to thread:', sendError)
160
+ await sendThreadMessage(
161
+ thread,
162
+ `Failed to load message history, but session is connected. You can still send new messages.`,
163
+ )
164
+ }
165
+ } catch (error) {
166
+ logger.error('[RESUME] Error:', error)
167
+ await command.editReply(
168
+ `Failed to resume session: ${error instanceof Error ? error.message : 'Unknown error'}`,
169
+ )
170
+ }
171
+ }
172
+
173
+ export async function handleResumeAutocomplete({
174
+ interaction,
175
+ appId,
176
+ }: AutocompleteContext): Promise<void> {
177
+ const focusedValue = interaction.options.getFocused()
178
+
179
+ let projectDirectory: string | undefined
180
+
181
+ if (interaction.channel) {
182
+ const textChannel = await resolveTextChannel(
183
+ interaction.channel as TextChannel | ThreadChannel | null,
184
+ )
185
+ if (textChannel) {
186
+ const channelConfig = await getChannelDirectory(textChannel.id)
187
+ if (channelConfig?.appId && channelConfig.appId !== appId) {
188
+ await interaction.respond([])
189
+ return
190
+ }
191
+ projectDirectory = channelConfig?.directory
192
+ }
193
+ }
194
+
195
+ if (!projectDirectory) {
196
+ await interaction.respond([])
197
+ return
198
+ }
199
+
200
+ try {
201
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
202
+ if (getClient instanceof Error) {
203
+ await interaction.respond([])
204
+ return
205
+ }
206
+
207
+ const sessionsResponse = await getClient().session.list()
208
+ if (!sessionsResponse.data) {
209
+ await interaction.respond([])
210
+ return
211
+ }
212
+
213
+ const existingSessionIds = new Set(await getAllThreadSessionIds())
214
+
215
+ const sessions = sessionsResponse.data
216
+ .filter((session) => !existingSessionIds.has(session.id))
217
+ .filter((session) =>
218
+ session.title.toLowerCase().includes(focusedValue.toLowerCase()),
219
+ )
220
+ .slice(0, 25)
221
+ .map((session) => {
222
+ const dateStr = new Date(session.time.updated).toLocaleString()
223
+ const suffix = ` (${dateStr})`
224
+ const maxTitleLength = 100 - suffix.length
225
+
226
+ let title = session.title
227
+ if (title.length > maxTitleLength) {
228
+ title = title.slice(0, Math.max(0, maxTitleLength - 1)) + '…'
229
+ }
230
+
231
+ return {
232
+ name: `${title}${suffix}`,
233
+ value: session.id,
234
+ }
235
+ })
236
+
237
+ await interaction.respond(sessions)
238
+ } catch (error) {
239
+ logger.error('[AUTOCOMPLETE] Error fetching sessions:', error)
240
+ await interaction.respond([])
241
+ }
242
+ }
@@ -0,0 +1,123 @@
1
+ // /run-shell-command command - Run an arbitrary shell command in the project directory.
2
+ // Resolves the project directory from the channel and executes the command with it as cwd.
3
+ // Also used by the ! prefix shortcut in discord messages (e.g. "!ls -la").
4
+ // Messages starting with ! are intercepted before session handling and routed here.
5
+
6
+ import {
7
+ ChannelType,
8
+ MessageFlags,
9
+ type TextChannel,
10
+ type ThreadChannel,
11
+ } from 'discord.js'
12
+ import type { CommandContext } from './types.js'
13
+ import {
14
+ resolveWorkingDirectory,
15
+ SILENT_MESSAGE_FLAGS,
16
+ } from '../discord-utils.js'
17
+ import { createLogger, LogPrefix } from '../logger.js'
18
+ import { execAsync } from '../worktree-utils.js'
19
+ import { stripAnsi } from '../utils.js'
20
+
21
+ const logger = createLogger(LogPrefix.INTERACTION)
22
+
23
+ const MAX_OUTPUT_CHARS = 1900
24
+
25
+ export async function runShellCommand({
26
+ command,
27
+ directory,
28
+ }: {
29
+ command: string
30
+ directory: string
31
+ }): Promise<string> {
32
+ try {
33
+ const { stdout, stderr } = await execAsync(command, { cwd: directory })
34
+ const output = stripAnsi([stdout, stderr].filter(Boolean).join('\n').trim())
35
+
36
+ const header = `\`${command}\` exited with 0`
37
+ if (!output) {
38
+ return header
39
+ }
40
+ return formatOutput(output, header)
41
+ } catch (error) {
42
+ const execError = error as {
43
+ stdout?: string
44
+ stderr?: string
45
+ message?: string
46
+ code?: number | string
47
+ }
48
+ const output = stripAnsi(
49
+ [execError.stdout, execError.stderr].filter(Boolean).join('\n').trim(),
50
+ )
51
+ const exitCode = execError.code ?? 1
52
+ logger.error(
53
+ `[RUN-COMMAND] Command "${command}" exited with ${exitCode}:`,
54
+ error,
55
+ )
56
+
57
+ const header = `\`${command}\` exited with ${exitCode}`
58
+ return formatOutput(output || execError.message || 'Unknown error', header)
59
+ }
60
+ }
61
+
62
+ export async function handleRunCommand({
63
+ command,
64
+ }: CommandContext): Promise<void> {
65
+ const channel = command.channel
66
+
67
+ if (!channel) {
68
+ await command.reply({
69
+ content: 'This command can only be used in a channel.',
70
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
71
+ })
72
+ return
73
+ }
74
+
75
+ const isThread = [
76
+ ChannelType.PublicThread,
77
+ ChannelType.PrivateThread,
78
+ ChannelType.AnnouncementThread,
79
+ ].includes(channel.type)
80
+
81
+ const isTextChannel = channel.type === ChannelType.GuildText
82
+
83
+ if (!isThread && !isTextChannel) {
84
+ await command.reply({
85
+ content: 'This command can only be used in a text channel or thread.',
86
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
87
+ })
88
+ return
89
+ }
90
+
91
+ const resolved = await resolveWorkingDirectory({
92
+ channel: channel as TextChannel | ThreadChannel,
93
+ })
94
+
95
+ if (!resolved) {
96
+ await command.reply({
97
+ content: 'Could not determine project directory for this channel.',
98
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
99
+ })
100
+ return
101
+ }
102
+
103
+ const input = command.options.getString('command', true)
104
+
105
+ await command.deferReply()
106
+
107
+ const result = await runShellCommand({
108
+ command: input,
109
+ directory: resolved.workingDirectory,
110
+ })
111
+ await command.editReply({ content: result })
112
+ }
113
+
114
+ function formatOutput(output: string, header: string): string {
115
+ // Reserve space for header + newline + code block delimiters (```\n...\n```)
116
+ const overhead = header.length + 1 + 3 + 1 + 1 + 3 // header\n```\n...\n```
117
+ const maxContent = MAX_OUTPUT_CHARS - overhead
118
+ const truncated =
119
+ output.length > maxContent
120
+ ? output.slice(0, maxContent - 14) + '\n... truncated'
121
+ : output
122
+ return `${header}\n\`\`\`\n${truncated}\n\`\`\``
123
+ }
@@ -0,0 +1,109 @@
1
+ // /session-id command - Show current session ID and an opencode attach command.
2
+
3
+ import {
4
+ ChannelType,
5
+ MessageFlags,
6
+ type TextChannel,
7
+ type ThreadChannel,
8
+ } from 'discord.js'
9
+ import type { CommandContext } from './types.js'
10
+ import { getThreadSession } from '../database.js'
11
+ import {
12
+ resolveWorkingDirectory,
13
+ SILENT_MESSAGE_FLAGS,
14
+ } from '../discord-utils.js'
15
+ import {
16
+ getOpencodeServerPort,
17
+ initializeOpencodeForDirectory,
18
+ } from '../opencode.js'
19
+ import { createLogger, LogPrefix } from '../logger.js'
20
+
21
+ const logger = createLogger(LogPrefix.SESSION)
22
+
23
+ function shellQuote(value: string): string {
24
+ if (!value) {
25
+ return "''"
26
+ }
27
+ return `'${value.replaceAll("'", `'"'"'`)}'`
28
+ }
29
+
30
+ export async function handleSessionIdCommand({
31
+ command,
32
+ }: CommandContext): Promise<void> {
33
+ const channel = command.channel
34
+
35
+ if (!channel) {
36
+ await command.reply({
37
+ content: 'This command can only be used in a channel',
38
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
39
+ })
40
+ return
41
+ }
42
+
43
+ const isThread = [
44
+ ChannelType.PublicThread,
45
+ ChannelType.PrivateThread,
46
+ ChannelType.AnnouncementThread,
47
+ ].includes(channel.type)
48
+
49
+ if (!isThread) {
50
+ await command.reply({
51
+ content:
52
+ 'This command can only be used in a thread with an active session',
53
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
54
+ })
55
+ return
56
+ }
57
+
58
+ const resolved = await resolveWorkingDirectory({
59
+ channel: channel as TextChannel | ThreadChannel,
60
+ })
61
+
62
+ if (!resolved) {
63
+ await command.reply({
64
+ content: 'Could not determine project directory for this channel',
65
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
66
+ })
67
+ return
68
+ }
69
+
70
+ const { projectDirectory, workingDirectory } = resolved
71
+ const sessionId = await getThreadSession(channel.id)
72
+
73
+ if (!sessionId) {
74
+ await command.reply({
75
+ content: 'No active session in this thread',
76
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
77
+ })
78
+ return
79
+ }
80
+
81
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
82
+
83
+ let port = getOpencodeServerPort(projectDirectory)
84
+ if (!port) {
85
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
86
+ if (getClient instanceof Error) {
87
+ await command.editReply({
88
+ content: `Session ID: \`${sessionId}\`\nFailed to resolve OpenCode server port: ${getClient.message}`,
89
+ })
90
+ return
91
+ }
92
+ port = getOpencodeServerPort(projectDirectory)
93
+ }
94
+
95
+ if (!port) {
96
+ await command.editReply({
97
+ content: `Session ID: \`${sessionId}\`\nCould not determine OpenCode server port`,
98
+ })
99
+ return
100
+ }
101
+
102
+ const attachUrl = `http://127.0.0.1:${port}`
103
+ const attachCommand = `opencode attach ${attachUrl} --session ${sessionId} --dir ${shellQuote(workingDirectory)}`
104
+
105
+ await command.editReply({
106
+ content: `**Session ID:** \`${sessionId}\`\n**Attach command:**\n\`\`\`bash\n${attachCommand}\n\`\`\``,
107
+ })
108
+ logger.log(`Session ID shown for thread ${channel.id}: ${sessionId}`)
109
+ }