@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,549 @@
1
+ // OpenCode message part formatting for Discord.
2
+ // Converts SDK message parts (text, tools, reasoning) to Discord-friendly format,
3
+ // handles file attachments, and provides tool summary generation.
4
+
5
+ import type { Part, FilePartInput } from '@opencode-ai/sdk/v2'
6
+ import type { Message, TextChannel } from 'discord.js'
7
+
8
+ // Extended FilePartInput with original Discord URL for reference in prompts
9
+ export type DiscordFileAttachment = FilePartInput & {
10
+ sourceUrl?: string
11
+ }
12
+ import * as errore from 'errore'
13
+ import { createLogger, LogPrefix } from './logger.js'
14
+ import { FetchError } from './errors.js'
15
+ import { processImage } from './image-utils.js'
16
+
17
+ // Generic message type compatible with both v1 and v2 SDK
18
+ type GenericSessionMessage = {
19
+ info: { role: string; id?: string }
20
+ parts: Part[]
21
+ }
22
+
23
+ const logger = createLogger(LogPrefix.FORMATTING)
24
+
25
+ /**
26
+ * Resolves Discord mentions in message content to human-readable names.
27
+ * Replaces <@userId> with @displayName, <@&roleId> with @roleName, <#channelId> with #channelName.
28
+ */
29
+ export function resolveMentions(message: Message): string {
30
+ let content = message.content || ''
31
+
32
+ // Replace user mentions <@userId> or <@!userId> with @displayName
33
+ for (const [userId, user] of message.mentions.users) {
34
+ const member = message.guild?.members.cache.get(userId)
35
+ const displayName = member?.displayName || user.displayName || user.username
36
+ content = content.replace(
37
+ new RegExp(`<@!?${userId}>`, 'g'),
38
+ `@${displayName}`,
39
+ )
40
+ }
41
+
42
+ // Replace role mentions <@&roleId> with @roleName
43
+ for (const [roleId, role] of message.mentions.roles) {
44
+ content = content.replace(new RegExp(`<@&${roleId}>`, 'g'), `@${role.name}`)
45
+ }
46
+
47
+ // Replace channel mentions <#channelId> with #channelName
48
+ for (const [channelId, channel] of message.mentions.channels) {
49
+ const name = 'name' in channel ? (channel as TextChannel).name : channelId
50
+ content = content.replace(new RegExp(`<#${channelId}>`, 'g'), `#${name}`)
51
+ }
52
+
53
+ return content
54
+ }
55
+
56
+ /**
57
+ * Escapes Discord inline markdown characters so dynamic content
58
+ * doesn't break formatting when wrapped in *, _, **, etc.
59
+ */
60
+ function escapeInlineMarkdown(text: string): string {
61
+ return text.replace(/([*_~|`\\])/g, '\\$1')
62
+ }
63
+
64
+ /**
65
+ * Parses a patchText string (apply_patch format) and counts additions/deletions per file.
66
+ * Patch format uses `*** Add File:`, `*** Update File:`, `*** Delete File:` headers,
67
+ * with diff lines prefixed by `+` (addition) or `-` (deletion) inside `@@` hunks.
68
+ */
69
+ function parsePatchCounts(
70
+ patchText: string,
71
+ ): Map<string, { additions: number; deletions: number }> {
72
+ const counts = new Map<string, { additions: number; deletions: number }>()
73
+ const lines = patchText.split('\n')
74
+ let currentFile = ''
75
+ let currentType = ''
76
+ let inHunk = false
77
+
78
+ for (const line of lines) {
79
+ const addMatch = line.match(/^\*\*\* Add File:\s*(.+)/)
80
+ const updateMatch = line.match(/^\*\*\* Update File:\s*(.+)/)
81
+ const deleteMatch = line.match(/^\*\*\* Delete File:\s*(.+)/)
82
+
83
+ if (addMatch || updateMatch || deleteMatch) {
84
+ const match = addMatch || updateMatch || deleteMatch
85
+ currentFile = (match?.[1] ?? '').trim()
86
+ currentType = addMatch ? 'add' : updateMatch ? 'update' : 'delete'
87
+ counts.set(currentFile, { additions: 0, deletions: 0 })
88
+ inHunk = false
89
+ continue
90
+ }
91
+
92
+ if (line.startsWith('@@')) {
93
+ inHunk = true
94
+ continue
95
+ }
96
+
97
+ if (line.startsWith('*** ')) {
98
+ inHunk = false
99
+ continue
100
+ }
101
+
102
+ if (!currentFile) {
103
+ continue
104
+ }
105
+
106
+ const entry = counts.get(currentFile)
107
+ if (!entry) {
108
+ continue
109
+ }
110
+
111
+ if (currentType === 'add') {
112
+ // all content lines in Add File are additions
113
+ if (line.length > 0 && !line.startsWith('*** ')) {
114
+ entry.additions++
115
+ }
116
+ } else if (currentType === 'delete') {
117
+ // all content lines in Delete File are deletions
118
+ if (line.length > 0 && !line.startsWith('*** ')) {
119
+ entry.deletions++
120
+ }
121
+ } else if (inHunk) {
122
+ if (line.startsWith('+')) {
123
+ entry.additions++
124
+ } else if (line.startsWith('-')) {
125
+ entry.deletions++
126
+ }
127
+ }
128
+ }
129
+ return counts
130
+ }
131
+
132
+ /**
133
+ * Normalize whitespace: convert newlines to spaces and collapse consecutive spaces.
134
+ */
135
+ function normalizeWhitespace(text: string): string {
136
+ return text.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ')
137
+ }
138
+
139
+ /**
140
+ * Collects and formats the last N assistant parts from session messages.
141
+ * Used by both /resume and /fork to show recent assistant context.
142
+ */
143
+ export function collectLastAssistantParts({
144
+ messages,
145
+ limit = 30,
146
+ }: {
147
+ messages: GenericSessionMessage[]
148
+ limit?: number
149
+ }): { partIds: string[]; content: string; skippedCount: number } {
150
+ const allAssistantParts: { id: string; content: string }[] = []
151
+
152
+ for (const message of messages) {
153
+ if (message.info.role === 'assistant') {
154
+ for (const part of message.parts) {
155
+ const content = formatPart(part)
156
+ if (content.trim()) {
157
+ allAssistantParts.push({ id: part.id, content: content.trimEnd() })
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ const partsToRender = allAssistantParts.slice(-limit)
164
+ const partIds = partsToRender.map((p) => p.id)
165
+ const content = partsToRender.map((p) => p.content).join('\n')
166
+ const skippedCount = allAssistantParts.length - partsToRender.length
167
+
168
+ return { partIds, content, skippedCount }
169
+ }
170
+
171
+ export const TEXT_MIME_TYPES = [
172
+ 'text/',
173
+ 'application/json',
174
+ 'application/xml',
175
+ 'application/javascript',
176
+ 'application/typescript',
177
+ 'application/x-yaml',
178
+ 'application/toml',
179
+ ]
180
+
181
+ export function isTextMimeType(contentType: string | null): boolean {
182
+ if (!contentType) {
183
+ return false
184
+ }
185
+ return TEXT_MIME_TYPES.some((prefix) => contentType.startsWith(prefix))
186
+ }
187
+
188
+ export async function getTextAttachments(message: Message): Promise<string> {
189
+ const textAttachments = Array.from(message.attachments.values()).filter(
190
+ (attachment) => isTextMimeType(attachment.contentType),
191
+ )
192
+
193
+ if (textAttachments.length === 0) {
194
+ return ''
195
+ }
196
+
197
+ const textContents = await Promise.all(
198
+ textAttachments.map(async (attachment) => {
199
+ const response = await errore.tryAsync({
200
+ try: () => fetch(attachment.url),
201
+ catch: (e) => new FetchError({ url: attachment.url, cause: e }),
202
+ })
203
+ if (response instanceof Error) {
204
+ return `<attachment filename="${attachment.name}" error="${response.message}" />`
205
+ }
206
+ if (!response.ok) {
207
+ return `<attachment filename="${attachment.name}" error="Failed to fetch: ${response.status}" />`
208
+ }
209
+ const text = await response.text()
210
+ return `<attachment filename="${attachment.name}" mime="${attachment.contentType}">\n${text}\n</attachment>`
211
+ }),
212
+ )
213
+
214
+ return textContents.join('\n\n')
215
+ }
216
+
217
+ export async function getFileAttachments(
218
+ message: Message,
219
+ ): Promise<DiscordFileAttachment[]> {
220
+ const fileAttachments = Array.from(message.attachments.values()).filter(
221
+ (attachment) => {
222
+ const contentType = attachment.contentType || ''
223
+ return (
224
+ contentType.startsWith('image/') || contentType === 'application/pdf'
225
+ )
226
+ },
227
+ )
228
+
229
+ if (fileAttachments.length === 0) {
230
+ return []
231
+ }
232
+
233
+ const results = await Promise.all(
234
+ fileAttachments.map(async (attachment) => {
235
+ const response = await errore.tryAsync({
236
+ try: () => fetch(attachment.url),
237
+ catch: (e) => new FetchError({ url: attachment.url, cause: e }),
238
+ })
239
+ if (response instanceof Error) {
240
+ logger.error(
241
+ `Error downloading attachment ${attachment.name}:`,
242
+ response.message,
243
+ )
244
+ return null
245
+ }
246
+ if (!response.ok) {
247
+ logger.error(
248
+ `Failed to fetch attachment ${attachment.name}: ${response.status}`,
249
+ )
250
+ return null
251
+ }
252
+
253
+ const rawBuffer = Buffer.from(await response.arrayBuffer())
254
+ const originalMime = attachment.contentType || 'application/octet-stream'
255
+
256
+ // Process image (resize if needed, convert to JPEG)
257
+ const { buffer, mime } = await processImage(rawBuffer, originalMime)
258
+
259
+ const base64 = buffer.toString('base64')
260
+ const dataUrl = `data:${mime};base64,${base64}`
261
+
262
+ logger.log(
263
+ `Attachment ${attachment.name}: ${rawBuffer.length} → ${buffer.length} bytes, ${mime}`,
264
+ )
265
+
266
+ return {
267
+ type: 'file' as const,
268
+ mime,
269
+ filename: attachment.name,
270
+ url: dataUrl,
271
+ sourceUrl: attachment.url,
272
+ }
273
+ }),
274
+ )
275
+
276
+ return results.filter((r) => r !== null) as DiscordFileAttachment[]
277
+ }
278
+
279
+ const MAX_BASH_COMMAND_INLINE_LENGTH = 100
280
+
281
+ export function getToolSummaryText(part: Part): string {
282
+ if (part.type !== 'tool') return ''
283
+
284
+ if (part.tool === 'edit') {
285
+ const filePath = (part.state.input?.filePath as string) || ''
286
+ const newString = (part.state.input?.newString as string) || ''
287
+ const oldString = (part.state.input?.oldString as string) || ''
288
+ const added = newString.split('\n').length
289
+ const removed = oldString.split('\n').length
290
+ const fileName = filePath.split('/').pop() || ''
291
+ return fileName
292
+ ? `*${escapeInlineMarkdown(fileName)}* (+${added}-${removed})`
293
+ : `(+${added}-${removed})`
294
+ }
295
+
296
+ if (part.tool === 'apply_patch') {
297
+ // Only inputs are available when parts are sent during streaming (output/metadata not yet populated)
298
+ const patchText = (part.state.input?.patchText as string) || ''
299
+ if (!patchText) {
300
+ return ''
301
+ }
302
+ const patchCounts = parsePatchCounts(patchText)
303
+ return [...patchCounts.entries()]
304
+ .map(([filePath, { additions, deletions }]) => {
305
+ const fileName = filePath.split('/').pop() || ''
306
+ return fileName
307
+ ? `*${escapeInlineMarkdown(fileName)}* (+${additions}-${deletions})`
308
+ : `(+${additions}-${deletions})`
309
+ })
310
+ .join(', ')
311
+ }
312
+
313
+ if (part.tool === 'write') {
314
+ const filePath = (part.state.input?.filePath as string) || ''
315
+ const content = (part.state.input?.content as string) || ''
316
+ const lines = content.split('\n').length
317
+ const fileName = filePath.split('/').pop() || ''
318
+ return fileName
319
+ ? `*${escapeInlineMarkdown(fileName)}* (${lines} line${lines === 1 ? '' : 's'})`
320
+ : `(${lines} line${lines === 1 ? '' : 's'})`
321
+ }
322
+
323
+ if (part.tool === 'webfetch') {
324
+ const url = (part.state.input?.url as string) || ''
325
+ const urlWithoutProtocol = url.replace(/^https?:\/\//, '')
326
+ return urlWithoutProtocol
327
+ ? `*${escapeInlineMarkdown(urlWithoutProtocol)}*`
328
+ : ''
329
+ }
330
+
331
+ if (part.tool === 'read') {
332
+ const filePath = (part.state.input?.filePath as string) || ''
333
+ const fileName = filePath.split('/').pop() || ''
334
+ return fileName ? `*${escapeInlineMarkdown(fileName)}*` : ''
335
+ }
336
+
337
+ if (part.tool === 'list') {
338
+ const path = (part.state.input?.path as string) || ''
339
+ const dirName = path.split('/').pop() || path
340
+ return dirName ? `*${escapeInlineMarkdown(dirName)}*` : ''
341
+ }
342
+
343
+ if (part.tool === 'glob') {
344
+ const pattern = (part.state.input?.pattern as string) || ''
345
+ return pattern ? `*${escapeInlineMarkdown(pattern)}*` : ''
346
+ }
347
+
348
+ if (part.tool === 'grep') {
349
+ const pattern = (part.state.input?.pattern as string) || ''
350
+ return pattern ? `*${escapeInlineMarkdown(pattern)}*` : ''
351
+ }
352
+
353
+ if (
354
+ part.tool === 'bash' ||
355
+ part.tool === 'todoread' ||
356
+ part.tool === 'todowrite'
357
+ ) {
358
+ return ''
359
+ }
360
+
361
+ // Task tool display is handled via subtask part in session-handler (shows name + agent)
362
+ if (part.tool === 'task') {
363
+ return ''
364
+ }
365
+
366
+ if (part.tool === 'skill') {
367
+ const name = (part.state.input?.name as string) || ''
368
+ return name ? `_${escapeInlineMarkdown(name)}_` : ''
369
+ }
370
+
371
+ // File upload tool - show the prompt
372
+ if (part.tool.endsWith('kimaki_file_upload')) {
373
+ const prompt = (part.state.input?.prompt as string) || ''
374
+ return prompt ? `*${escapeInlineMarkdown(prompt.slice(0, 60))}*` : ''
375
+ }
376
+
377
+ if (!part.state.input) return ''
378
+
379
+ const inputFields = Object.entries(part.state.input)
380
+ .map(([key, value]) => {
381
+ if (value === null || value === undefined) return null
382
+ const stringValue =
383
+ typeof value === 'string' ? value : JSON.stringify(value)
384
+ const normalized = normalizeWhitespace(stringValue)
385
+ const truncatedValue =
386
+ normalized.length > 50 ? normalized.slice(0, 50) + '…' : normalized
387
+ return `${key}: ${truncatedValue}`
388
+ })
389
+ .filter(Boolean)
390
+
391
+ if (inputFields.length === 0) return ''
392
+
393
+ return `(${inputFields.join(', ')})`
394
+ }
395
+
396
+ export function formatTodoList(part: Part): string {
397
+ if (part.type !== 'tool' || part.tool !== 'todowrite') return ''
398
+ const todos =
399
+ (part.state.input?.todos as {
400
+ content: string
401
+ status: 'pending' | 'in_progress' | 'completed' | 'cancelled'
402
+ }[]) || []
403
+ const activeIndex = todos.findIndex((todo) => {
404
+ return todo.status === 'in_progress'
405
+ })
406
+ const activeTodo = todos[activeIndex]
407
+ if (activeIndex === -1 || !activeTodo) return ''
408
+ // digit-with-period ⒈-⒛ for 1-20, fallback to regular number for 21+
409
+ const digitWithPeriod = '⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛'
410
+ const todoNumber = activeIndex + 1
411
+ const num =
412
+ todoNumber <= 20 ? digitWithPeriod[todoNumber - 1] : `${todoNumber}.`
413
+ const content =
414
+ activeTodo.content.charAt(0).toLowerCase() + activeTodo.content.slice(1)
415
+ return `${num} **${escapeInlineMarkdown(content)}**`
416
+ }
417
+
418
+ export function formatPart(part: Part, prefix?: string): string {
419
+ const pfx = prefix ? `${prefix} ⋅ ` : ''
420
+
421
+ if (part.type === 'text') {
422
+ const text = part.text?.trim()
423
+ if (!text) return ''
424
+ // For subtask text, always use bullet with prefix
425
+ if (prefix) {
426
+ return `⬥ ${pfx}${text}`
427
+ }
428
+ const firstChar = text[0] || ''
429
+ const markdownStarters = ['#', '*', '_', '-', '>', '`', '[', '|']
430
+ const startsWithMarkdown =
431
+ markdownStarters.includes(firstChar) || /^\d+\./.test(text)
432
+ if (startsWithMarkdown) {
433
+ return `\n${text}`
434
+ }
435
+ return `⬥ ${text}`
436
+ }
437
+
438
+ if (part.type === 'reasoning') {
439
+ if (!part.text?.trim()) return ''
440
+ return `┣ ${pfx}thinking`
441
+ }
442
+
443
+ if (part.type === 'file') {
444
+ return prefix
445
+ ? `📄 ${pfx}${part.filename || 'File'}`
446
+ : `📄 ${part.filename || 'File'}`
447
+ }
448
+
449
+ if (
450
+ part.type === 'step-start' ||
451
+ part.type === 'step-finish' ||
452
+ part.type === 'patch'
453
+ ) {
454
+ return ''
455
+ }
456
+
457
+ if (part.type === 'agent') {
458
+ return `┣ ${pfx}agent ${part.id}`
459
+ }
460
+
461
+ if (part.type === 'snapshot') {
462
+ return `┣ ${pfx}snapshot ${part.snapshot}`
463
+ }
464
+
465
+ if (part.type === 'tool') {
466
+ if (part.tool === 'todowrite') {
467
+ const formatted = formatTodoList(part)
468
+ return prefix && formatted ? `┣ ${pfx}${formatted}` : formatted
469
+ }
470
+
471
+ // Question tool is handled via Discord dropdowns, not text
472
+ if (part.tool === 'question') {
473
+ return ''
474
+ }
475
+
476
+ // File upload tool is handled via Discord button + modal, not text
477
+ if (part.tool.endsWith('kimaki_file_upload')) {
478
+ return ''
479
+ }
480
+
481
+ // Action buttons tool is handled via Discord buttons, not text
482
+ if (part.tool.endsWith('kimaki_action_buttons')) {
483
+ return ''
484
+ }
485
+
486
+ // Task tool display is handled in session-handler with proper label
487
+ if (part.tool === 'task') {
488
+ return ''
489
+ }
490
+
491
+ if (part.state.status === 'pending') {
492
+ if (part.tool !== 'bash') {
493
+ return ''
494
+ }
495
+ const command = (part.state.input?.command as string) || ''
496
+ const description = (part.state.input?.description as string) || ''
497
+ const isSingleLine = !command.includes('\n')
498
+ const toolTitle =
499
+ isSingleLine && command.length <= MAX_BASH_COMMAND_INLINE_LENGTH
500
+ ? ` _${escapeInlineMarkdown(command)}_`
501
+ : description
502
+ ? ` _${escapeInlineMarkdown(description)}_`
503
+ : ''
504
+ return `┣ ${pfx}bash${toolTitle}`
505
+ }
506
+
507
+ const summaryText = getToolSummaryText(part)
508
+ const stateTitle = 'title' in part.state ? part.state.title : undefined
509
+
510
+ let toolTitle = ''
511
+ if (part.state.status === 'error') {
512
+ toolTitle = part.state.error || 'error'
513
+ } else if (part.tool === 'bash') {
514
+ const command = (part.state.input?.command as string) || ''
515
+ const description = (part.state.input?.description as string) || ''
516
+ const isSingleLine = !command.includes('\n')
517
+ if (isSingleLine && command.length <= MAX_BASH_COMMAND_INLINE_LENGTH) {
518
+ toolTitle = `_${escapeInlineMarkdown(command)}_`
519
+ } else if (description) {
520
+ toolTitle = `_${escapeInlineMarkdown(description)}_`
521
+ } else if (stateTitle) {
522
+ toolTitle = `_${escapeInlineMarkdown(stateTitle)}_`
523
+ }
524
+ } else if (stateTitle) {
525
+ toolTitle = `_${escapeInlineMarkdown(stateTitle)}_`
526
+ }
527
+
528
+ const icon = (() => {
529
+ if (part.state.status === 'error') {
530
+ return '⨯'
531
+ }
532
+ if (
533
+ part.tool === 'edit' ||
534
+ part.tool === 'write' ||
535
+ part.tool === 'apply_patch'
536
+ ) {
537
+ return '◼︎'
538
+ }
539
+ return '┣'
540
+ })()
541
+ const toolParts = [part.tool, toolTitle, summaryText]
542
+ .filter(Boolean)
543
+ .join(' ')
544
+ return `${icon} ${pfx}${toolParts}`
545
+ }
546
+
547
+ logger.warn('Unknown part type:', part)
548
+ return ''
549
+ }