@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
+ // /resume command - Resume an existing OpenCode session.
2
+ import { ChannelType, ThreadAutoArchiveDuration, } from 'discord.js';
3
+ import fs from 'node:fs';
4
+ import { getChannelDirectory, setThreadSession, setPartMessagesBatch, getAllThreadSessionIds, } from '../database.js';
5
+ import { initializeOpencodeForDirectory } from '../opencode.js';
6
+ import { sendThreadMessage, resolveTextChannel } from '../discord-utils.js';
7
+ import { collectLastAssistantParts } from '../message-formatting.js';
8
+ import { createLogger, LogPrefix } from '../logger.js';
9
+ import * as errore from 'errore';
10
+ const logger = createLogger(LogPrefix.RESUME);
11
+ export async function handleResumeCommand({ command, appId, }) {
12
+ await command.deferReply({ ephemeral: false });
13
+ const sessionId = command.options.getString('session', true);
14
+ const channel = command.channel;
15
+ const isThread = channel &&
16
+ [
17
+ ChannelType.PublicThread,
18
+ ChannelType.PrivateThread,
19
+ ChannelType.AnnouncementThread,
20
+ ].includes(channel.type);
21
+ if (isThread) {
22
+ await command.editReply('This command can only be used in project channels, not threads');
23
+ return;
24
+ }
25
+ if (!channel || channel.type !== ChannelType.GuildText) {
26
+ await command.editReply('This command can only be used in text channels');
27
+ return;
28
+ }
29
+ const textChannel = channel;
30
+ const channelConfig = await getChannelDirectory(textChannel.id);
31
+ const projectDirectory = channelConfig?.directory;
32
+ const channelAppId = channelConfig?.appId || undefined;
33
+ if (channelAppId && channelAppId !== appId) {
34
+ await command.editReply('This channel is not configured for this bot');
35
+ return;
36
+ }
37
+ if (!projectDirectory) {
38
+ await command.editReply('This channel is not configured with a project directory');
39
+ return;
40
+ }
41
+ if (!fs.existsSync(projectDirectory)) {
42
+ await command.editReply(`Directory does not exist: ${projectDirectory}`);
43
+ return;
44
+ }
45
+ try {
46
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
47
+ if (getClient instanceof Error) {
48
+ await command.editReply(getClient.message);
49
+ return;
50
+ }
51
+ const sessionResponse = await getClient().session.get({
52
+ sessionID: sessionId,
53
+ });
54
+ if (!sessionResponse.data) {
55
+ await command.editReply('Session not found');
56
+ return;
57
+ }
58
+ const sessionTitle = sessionResponse.data.title;
59
+ const thread = await textChannel.threads.create({
60
+ name: `Resume: ${sessionTitle}`.slice(0, 100),
61
+ autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
62
+ reason: `Resuming session ${sessionId}`,
63
+ });
64
+ // Add user to thread so it appears in their sidebar
65
+ await thread.members.add(command.user.id);
66
+ await setThreadSession(thread.id, sessionId);
67
+ logger.log(`[RESUME] Created thread ${thread.id} for session ${sessionId}`);
68
+ const messagesResponse = await getClient().session.messages({
69
+ sessionID: sessionId,
70
+ });
71
+ if (!messagesResponse.data) {
72
+ throw new Error('Failed to fetch session messages');
73
+ }
74
+ const messages = messagesResponse.data;
75
+ await command.editReply(`Resumed session "${sessionTitle}" in ${thread.toString()}`);
76
+ await sendThreadMessage(thread, `**Resumed session:** ${sessionTitle}\n**Created:** ${new Date(sessionResponse.data.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`);
77
+ try {
78
+ const { partIds, content, skippedCount } = collectLastAssistantParts({
79
+ messages,
80
+ });
81
+ if (skippedCount > 0) {
82
+ await sendThreadMessage(thread, `*Skipped ${skippedCount} older assistant parts...*`);
83
+ }
84
+ if (content.trim()) {
85
+ const discordMessage = await sendThreadMessage(thread, content);
86
+ // Store part-message mappings atomically
87
+ await setPartMessagesBatch(partIds.map((partId) => ({
88
+ partId,
89
+ messageId: discordMessage.id,
90
+ threadId: thread.id,
91
+ })));
92
+ }
93
+ const messageCount = messages.length;
94
+ await sendThreadMessage(thread, `**Session resumed!** Loaded ${messageCount} messages.\n\nYou can now continue the conversation by sending messages in this thread.`);
95
+ }
96
+ catch (sendError) {
97
+ logger.error('[RESUME] Error sending messages to thread:', sendError);
98
+ await sendThreadMessage(thread, `Failed to load message history, but session is connected. You can still send new messages.`);
99
+ }
100
+ }
101
+ catch (error) {
102
+ logger.error('[RESUME] Error:', error);
103
+ await command.editReply(`Failed to resume session: ${error instanceof Error ? error.message : 'Unknown error'}`);
104
+ }
105
+ }
106
+ export async function handleResumeAutocomplete({ interaction, appId, }) {
107
+ const focusedValue = interaction.options.getFocused();
108
+ let projectDirectory;
109
+ if (interaction.channel) {
110
+ const textChannel = await resolveTextChannel(interaction.channel);
111
+ if (textChannel) {
112
+ const channelConfig = await getChannelDirectory(textChannel.id);
113
+ if (channelConfig?.appId && channelConfig.appId !== appId) {
114
+ await interaction.respond([]);
115
+ return;
116
+ }
117
+ projectDirectory = channelConfig?.directory;
118
+ }
119
+ }
120
+ if (!projectDirectory) {
121
+ await interaction.respond([]);
122
+ return;
123
+ }
124
+ try {
125
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
126
+ if (getClient instanceof Error) {
127
+ await interaction.respond([]);
128
+ return;
129
+ }
130
+ const sessionsResponse = await getClient().session.list();
131
+ if (!sessionsResponse.data) {
132
+ await interaction.respond([]);
133
+ return;
134
+ }
135
+ const existingSessionIds = new Set(await getAllThreadSessionIds());
136
+ const sessions = sessionsResponse.data
137
+ .filter((session) => !existingSessionIds.has(session.id))
138
+ .filter((session) => session.title.toLowerCase().includes(focusedValue.toLowerCase()))
139
+ .slice(0, 25)
140
+ .map((session) => {
141
+ const dateStr = new Date(session.time.updated).toLocaleString();
142
+ const suffix = ` (${dateStr})`;
143
+ const maxTitleLength = 100 - suffix.length;
144
+ let title = session.title;
145
+ if (title.length > maxTitleLength) {
146
+ title = title.slice(0, Math.max(0, maxTitleLength - 1)) + '…';
147
+ }
148
+ return {
149
+ name: `${title}${suffix}`,
150
+ value: session.id,
151
+ };
152
+ });
153
+ await interaction.respond(sessions);
154
+ }
155
+ catch (error) {
156
+ logger.error('[AUTOCOMPLETE] Error fetching sessions:', error);
157
+ await interaction.respond([]);
158
+ }
159
+ }
@@ -0,0 +1,79 @@
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
+ import { ChannelType, MessageFlags, } from 'discord.js';
6
+ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
7
+ import { createLogger, LogPrefix } from '../logger.js';
8
+ import { execAsync } from '../worktree-utils.js';
9
+ import { stripAnsi } from '../utils.js';
10
+ const logger = createLogger(LogPrefix.INTERACTION);
11
+ const MAX_OUTPUT_CHARS = 1900;
12
+ export async function runShellCommand({ command, directory, }) {
13
+ try {
14
+ const { stdout, stderr } = await execAsync(command, { cwd: directory });
15
+ const output = stripAnsi([stdout, stderr].filter(Boolean).join('\n').trim());
16
+ const header = `\`${command}\` exited with 0`;
17
+ if (!output) {
18
+ return header;
19
+ }
20
+ return formatOutput(output, header);
21
+ }
22
+ catch (error) {
23
+ const execError = error;
24
+ const output = stripAnsi([execError.stdout, execError.stderr].filter(Boolean).join('\n').trim());
25
+ const exitCode = execError.code ?? 1;
26
+ logger.error(`[RUN-COMMAND] Command "${command}" exited with ${exitCode}:`, error);
27
+ const header = `\`${command}\` exited with ${exitCode}`;
28
+ return formatOutput(output || execError.message || 'Unknown error', header);
29
+ }
30
+ }
31
+ export async function handleRunCommand({ command, }) {
32
+ const channel = command.channel;
33
+ if (!channel) {
34
+ await command.reply({
35
+ content: 'This command can only be used in a channel.',
36
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
37
+ });
38
+ return;
39
+ }
40
+ const isThread = [
41
+ ChannelType.PublicThread,
42
+ ChannelType.PrivateThread,
43
+ ChannelType.AnnouncementThread,
44
+ ].includes(channel.type);
45
+ const isTextChannel = channel.type === ChannelType.GuildText;
46
+ if (!isThread && !isTextChannel) {
47
+ await command.reply({
48
+ content: 'This command can only be used in a text channel or thread.',
49
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
50
+ });
51
+ return;
52
+ }
53
+ const resolved = await resolveWorkingDirectory({
54
+ channel: channel,
55
+ });
56
+ if (!resolved) {
57
+ await command.reply({
58
+ content: 'Could not determine project directory for this channel.',
59
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
60
+ });
61
+ return;
62
+ }
63
+ const input = command.options.getString('command', true);
64
+ await command.deferReply();
65
+ const result = await runShellCommand({
66
+ command: input,
67
+ directory: resolved.workingDirectory,
68
+ });
69
+ await command.editReply({ content: result });
70
+ }
71
+ function formatOutput(output, header) {
72
+ // Reserve space for header + newline + code block delimiters (```\n...\n```)
73
+ const overhead = header.length + 1 + 3 + 1 + 1 + 3; // header\n```\n...\n```
74
+ const maxContent = MAX_OUTPUT_CHARS - overhead;
75
+ const truncated = output.length > maxContent
76
+ ? output.slice(0, maxContent - 14) + '\n... truncated'
77
+ : output;
78
+ return `${header}\n\`\`\`\n${truncated}\n\`\`\``;
79
+ }
@@ -0,0 +1,78 @@
1
+ // /session-id command - Show current session ID and an opencode attach command.
2
+ import { ChannelType, MessageFlags, } from 'discord.js';
3
+ import { getThreadSession } from '../database.js';
4
+ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
5
+ import { getOpencodeServerPort, initializeOpencodeForDirectory, } from '../opencode.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ const logger = createLogger(LogPrefix.SESSION);
8
+ function shellQuote(value) {
9
+ if (!value) {
10
+ return "''";
11
+ }
12
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
13
+ }
14
+ export async function handleSessionIdCommand({ command, }) {
15
+ const channel = command.channel;
16
+ if (!channel) {
17
+ await command.reply({
18
+ content: 'This command can only be used in a channel',
19
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
20
+ });
21
+ return;
22
+ }
23
+ const isThread = [
24
+ ChannelType.PublicThread,
25
+ ChannelType.PrivateThread,
26
+ ChannelType.AnnouncementThread,
27
+ ].includes(channel.type);
28
+ if (!isThread) {
29
+ await command.reply({
30
+ content: 'This command can only be used in a thread with an active session',
31
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
32
+ });
33
+ return;
34
+ }
35
+ const resolved = await resolveWorkingDirectory({
36
+ channel: channel,
37
+ });
38
+ if (!resolved) {
39
+ await command.reply({
40
+ content: 'Could not determine project directory for this channel',
41
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
42
+ });
43
+ return;
44
+ }
45
+ const { projectDirectory, workingDirectory } = resolved;
46
+ const sessionId = await getThreadSession(channel.id);
47
+ if (!sessionId) {
48
+ await command.reply({
49
+ content: 'No active session in this thread',
50
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
51
+ });
52
+ return;
53
+ }
54
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
55
+ let port = getOpencodeServerPort(projectDirectory);
56
+ if (!port) {
57
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
58
+ if (getClient instanceof Error) {
59
+ await command.editReply({
60
+ content: `Session ID: \`${sessionId}\`\nFailed to resolve OpenCode server port: ${getClient.message}`,
61
+ });
62
+ return;
63
+ }
64
+ port = getOpencodeServerPort(projectDirectory);
65
+ }
66
+ if (!port) {
67
+ await command.editReply({
68
+ content: `Session ID: \`${sessionId}\`\nCould not determine OpenCode server port`,
69
+ });
70
+ return;
71
+ }
72
+ const attachUrl = `http://127.0.0.1:${port}`;
73
+ const attachCommand = `opencode attach ${attachUrl} --session ${sessionId} --dir ${shellQuote(workingDirectory)}`;
74
+ await command.editReply({
75
+ content: `**Session ID:** \`${sessionId}\`\n**Attach command:**\n\`\`\`bash\n${attachCommand}\n\`\`\``,
76
+ });
77
+ logger.log(`Session ID shown for thread ${channel.id}: ${sessionId}`);
78
+ }
@@ -0,0 +1,192 @@
1
+ // /new-session command - Start a new OpenCode session.
2
+ import { ChannelType } from 'discord.js';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { getChannelDirectory } from '../database.js';
6
+ import { initializeOpencodeForDirectory } from '../opencode.js';
7
+ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
8
+ import { handleOpencodeSession } from '../session-handler.js';
9
+ import { createLogger, LogPrefix } from '../logger.js';
10
+ import * as errore from 'errore';
11
+ const logger = createLogger(LogPrefix.SESSION);
12
+ export async function handleSessionCommand({ command, appId, }) {
13
+ await command.deferReply({ ephemeral: false });
14
+ const prompt = command.options.getString('prompt', true);
15
+ const filesString = command.options.getString('files') || '';
16
+ const agent = command.options.getString('agent') || undefined;
17
+ const channel = command.channel;
18
+ if (!channel || channel.type !== ChannelType.GuildText) {
19
+ await command.editReply('This command can only be used in text channels');
20
+ return;
21
+ }
22
+ const textChannel = channel;
23
+ const channelConfig = await getChannelDirectory(textChannel.id);
24
+ const projectDirectory = channelConfig?.directory;
25
+ const channelAppId = channelConfig?.appId || undefined;
26
+ if (channelAppId && channelAppId !== appId) {
27
+ await command.editReply('This channel is not configured for this bot');
28
+ return;
29
+ }
30
+ if (!projectDirectory) {
31
+ await command.editReply('This channel is not configured with a project directory');
32
+ return;
33
+ }
34
+ if (!fs.existsSync(projectDirectory)) {
35
+ await command.editReply(`Directory does not exist: ${projectDirectory}`);
36
+ return;
37
+ }
38
+ try {
39
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
40
+ if (getClient instanceof Error) {
41
+ await command.editReply(getClient.message);
42
+ return;
43
+ }
44
+ const files = filesString
45
+ .split(',')
46
+ .map((f) => f.trim())
47
+ .filter((f) => f);
48
+ let fullPrompt = prompt;
49
+ if (files.length > 0) {
50
+ fullPrompt = `${prompt}\n\n@${files.join(' @')}`;
51
+ }
52
+ const starterMessage = await textChannel.send({
53
+ content: `🚀 **Starting OpenCode session**\n📝 ${prompt}${files.length > 0 ? `\n📎 Files: ${files.join(', ')}` : ''}`,
54
+ flags: SILENT_MESSAGE_FLAGS,
55
+ });
56
+ const thread = await starterMessage.startThread({
57
+ name: prompt.slice(0, 100),
58
+ autoArchiveDuration: 1440,
59
+ reason: 'OpenCode session',
60
+ });
61
+ // Add user to thread so it appears in their sidebar
62
+ await thread.members.add(command.user.id);
63
+ await command.editReply(`Created new session in ${thread.toString()}`);
64
+ await handleOpencodeSession({
65
+ prompt: fullPrompt,
66
+ thread,
67
+ projectDirectory,
68
+ channelId: textChannel.id,
69
+ agent,
70
+ appId,
71
+ });
72
+ }
73
+ catch (error) {
74
+ logger.error('[SESSION] Error:', error);
75
+ await command.editReply(`Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}`);
76
+ }
77
+ }
78
+ async function handleAgentAutocomplete({ interaction, appId, }) {
79
+ const focusedValue = interaction.options.getFocused();
80
+ let projectDirectory;
81
+ if (interaction.channel &&
82
+ interaction.channel.type === ChannelType.GuildText) {
83
+ const channelConfig = await getChannelDirectory(interaction.channel.id);
84
+ if (channelConfig) {
85
+ if (channelConfig.appId && channelConfig.appId !== appId) {
86
+ await interaction.respond([]);
87
+ return;
88
+ }
89
+ projectDirectory = channelConfig.directory;
90
+ }
91
+ }
92
+ if (!projectDirectory) {
93
+ await interaction.respond([]);
94
+ return;
95
+ }
96
+ try {
97
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
98
+ if (getClient instanceof Error) {
99
+ await interaction.respond([]);
100
+ return;
101
+ }
102
+ const agentsResponse = await getClient().app.agents({
103
+ directory: projectDirectory,
104
+ });
105
+ if (!agentsResponse.data || agentsResponse.data.length === 0) {
106
+ await interaction.respond([]);
107
+ return;
108
+ }
109
+ const agents = agentsResponse.data
110
+ .filter((a) => {
111
+ const hidden = a.hidden;
112
+ return (a.mode === 'primary' || a.mode === 'all') && !hidden;
113
+ })
114
+ .filter((a) => a.name.toLowerCase().includes(focusedValue.toLowerCase()))
115
+ .slice(0, 25);
116
+ const choices = agents.map((agent) => ({
117
+ name: agent.name.slice(0, 100),
118
+ value: agent.name,
119
+ }));
120
+ await interaction.respond(choices);
121
+ }
122
+ catch (error) {
123
+ logger.error('[AUTOCOMPLETE] Error fetching agents:', error);
124
+ await interaction.respond([]);
125
+ }
126
+ }
127
+ export async function handleSessionAutocomplete({ interaction, appId, }) {
128
+ const focusedOption = interaction.options.getFocused(true);
129
+ if (focusedOption.name === 'agent') {
130
+ await handleAgentAutocomplete({ interaction, appId });
131
+ return;
132
+ }
133
+ if (focusedOption.name !== 'files') {
134
+ return;
135
+ }
136
+ const focusedValue = focusedOption.value;
137
+ const parts = focusedValue.split(',');
138
+ const previousFiles = parts
139
+ .slice(0, -1)
140
+ .map((f) => f.trim())
141
+ .filter((f) => f);
142
+ const currentQuery = (parts[parts.length - 1] || '').trim();
143
+ let projectDirectory;
144
+ if (interaction.channel &&
145
+ interaction.channel.type === ChannelType.GuildText) {
146
+ const channelConfig = await getChannelDirectory(interaction.channel.id);
147
+ if (channelConfig) {
148
+ if (channelConfig.appId && channelConfig.appId !== appId) {
149
+ await interaction.respond([]);
150
+ return;
151
+ }
152
+ projectDirectory = channelConfig.directory;
153
+ }
154
+ }
155
+ if (!projectDirectory) {
156
+ await interaction.respond([]);
157
+ return;
158
+ }
159
+ try {
160
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
161
+ if (getClient instanceof Error) {
162
+ await interaction.respond([]);
163
+ return;
164
+ }
165
+ const response = await getClient().find.files({
166
+ query: currentQuery || '',
167
+ });
168
+ const files = response.data || [];
169
+ const prefix = previousFiles.length > 0 ? previousFiles.join(', ') + ', ' : '';
170
+ const choices = files
171
+ .map((file) => {
172
+ const fullValue = prefix + file;
173
+ const allFiles = [...previousFiles, file];
174
+ const allBasenames = allFiles.map((f) => f.split('/').pop() || f);
175
+ let displayName = allBasenames.join(', ');
176
+ if (displayName.length > 100) {
177
+ displayName = '…' + displayName.slice(-97);
178
+ }
179
+ return {
180
+ name: displayName,
181
+ value: fullValue,
182
+ };
183
+ })
184
+ .filter((choice) => choice.value.length <= 100)
185
+ .slice(0, 25);
186
+ await interaction.respond(choices);
187
+ }
188
+ catch (error) {
189
+ logger.error('[AUTOCOMPLETE] Error fetching files:', error);
190
+ await interaction.respond([]);
191
+ }
192
+ }
@@ -0,0 +1,80 @@
1
+ // /share command - Share the current session as a public URL.
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.SHARE);
8
+ export async function handleShareCommand({ 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
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
49
+ if (getClient instanceof Error) {
50
+ await command.reply({
51
+ content: `Failed to share session: ${getClient.message}`,
52
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
53
+ });
54
+ return;
55
+ }
56
+ try {
57
+ const response = await getClient().session.share({
58
+ sessionID: sessionId,
59
+ });
60
+ if (!response.data?.share?.url) {
61
+ await command.reply({
62
+ content: 'Failed to generate share URL',
63
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
64
+ });
65
+ return;
66
+ }
67
+ await command.reply({
68
+ content: `🔗 **Session shared:** ${response.data.share.url}`,
69
+ flags: SILENT_MESSAGE_FLAGS,
70
+ });
71
+ logger.log(`Session ${sessionId} shared: ${response.data.share.url}`);
72
+ }
73
+ catch (error) {
74
+ logger.error('[SHARE] Error:', error);
75
+ await command.reply({
76
+ content: `Failed to share session: ${error instanceof Error ? error.message : 'Unknown error'}`,
77
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
78
+ });
79
+ }
80
+ }
@@ -0,0 +1,2 @@
1
+ // Shared types for command handlers.
2
+ export {};