@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,168 @@
1
+ // Unnest code blocks from list items for Discord.
2
+ // Discord doesn't render code blocks inside lists, so this hoists them
3
+ // to root level while preserving list structure.
4
+
5
+ import { Lexer, type Token, type Tokens } from 'marked'
6
+
7
+ type Segment =
8
+ | { type: 'list-item'; prefix: string; content: string }
9
+ | { type: 'code'; content: string }
10
+
11
+ export function unnestCodeBlocksFromLists(markdown: string): string {
12
+ const lexer = new Lexer()
13
+ const tokens = lexer.lex(markdown)
14
+
15
+ const result: string[] = []
16
+
17
+ for (let i = 0; i < tokens.length; i++) {
18
+ const token = tokens[i]!
19
+ const next = tokens[i + 1]
20
+
21
+ const chunk = (() => {
22
+ if (token.type === 'list') {
23
+ const segments = processListToken(token as Tokens.List)
24
+ return renderSegments(segments)
25
+ }
26
+ return token.raw
27
+ })()
28
+
29
+ if (!chunk) {
30
+ continue
31
+ }
32
+
33
+ const nextRaw = next?.raw ?? ''
34
+ const needsNewline =
35
+ nextRaw &&
36
+ !chunk.endsWith('\n') &&
37
+ typeof nextRaw === 'string' &&
38
+ !nextRaw.startsWith('\n')
39
+
40
+ result.push(needsNewline ? chunk + '\n' : chunk)
41
+ }
42
+ return result.join('')
43
+ }
44
+
45
+ function processListToken(list: Tokens.List): Segment[] {
46
+ const segments: Segment[] = []
47
+ const start =
48
+ typeof list.start === 'number' ? list.start : parseInt(list.start, 10) || 1
49
+ const prefix = list.ordered ? (i: number) => `${start + i}. ` : () => '- '
50
+
51
+ for (let i = 0; i < list.items.length; i++) {
52
+ const item = list.items[i]!
53
+ const itemSegments = processListItem(item, prefix(i))
54
+ segments.push(...itemSegments)
55
+ }
56
+
57
+ return segments
58
+ }
59
+
60
+ function processListItem(item: Tokens.ListItem, prefix: string): Segment[] {
61
+ const segments: Segment[] = []
62
+ let currentText: string[] = []
63
+ // Track if we've seen a code block - text after code uses continuation prefix
64
+ let seenCodeBlock = false
65
+
66
+ const taskMarker = item.task ? (item.checked ? '[x] ' : '[ ] ') : ''
67
+ let wroteFirstListItem = false
68
+
69
+ const flushText = (): void => {
70
+ const rawText = currentText.join('')
71
+ const text = rawText.trimEnd()
72
+ if (text.trim()) {
73
+ // After a code block, use '-' as continuation prefix to avoid repeating numbers
74
+ const effectivePrefix = seenCodeBlock ? '- ' : prefix
75
+ const marker = !wroteFirstListItem ? taskMarker : ''
76
+ const normalizedText = text.replace(/^\s+/, '')
77
+ segments.push({
78
+ type: 'list-item',
79
+ prefix: effectivePrefix,
80
+ content: marker + normalizedText,
81
+ })
82
+ wroteFirstListItem = true
83
+ }
84
+ currentText = []
85
+ }
86
+
87
+ for (const token of item.tokens) {
88
+ if (token.type === 'code') {
89
+ flushText()
90
+ const codeToken = token as Tokens.Code
91
+ const lang = codeToken.lang || ''
92
+ segments.push({
93
+ type: 'code',
94
+ content: '```' + lang + '\n' + codeToken.text + '\n```\n',
95
+ })
96
+ seenCodeBlock = true
97
+ continue
98
+ }
99
+
100
+ if (token.type === 'list') {
101
+ flushText()
102
+ // Recursively process nested list - segments bubble up
103
+ const nestedSegments = processListToken(token as Tokens.List)
104
+ segments.push(...nestedSegments)
105
+ continue
106
+ }
107
+
108
+ currentText.push(extractText(token))
109
+ }
110
+
111
+ flushText()
112
+
113
+ // If no segments were created (empty item), return empty
114
+ if (segments.length === 0) {
115
+ return []
116
+ }
117
+
118
+ // If item had no code blocks (all segments are list-items from this level),
119
+ // return original raw to preserve formatting
120
+ const hasCode = segments.some((s) => s.type === 'code')
121
+ if (!hasCode) {
122
+ return [{ type: 'list-item', prefix: '', content: item.raw }]
123
+ }
124
+
125
+ return segments
126
+ }
127
+
128
+ function extractText(token: Token): string {
129
+ // Prefer raw to preserve newlines and markdown markers.
130
+ if ('raw' in token && typeof token.raw === 'string') {
131
+ return token.raw
132
+ }
133
+
134
+ if (token.type === 'text') {
135
+ return (token as Tokens.Text).text
136
+ }
137
+
138
+ return ''
139
+ }
140
+
141
+ function renderSegments(segments: Segment[]): string {
142
+ const result: string[] = []
143
+
144
+ for (let i = 0; i < segments.length; i++) {
145
+ const segment = segments[i]!
146
+ const prev = segments[i - 1]
147
+
148
+ if (segment.type === 'code') {
149
+ // Add newline before code if previous was a list item
150
+ if (prev && prev.type === 'list-item') {
151
+ result.push('\n')
152
+ }
153
+ result.push(segment.content)
154
+ } else {
155
+ // list-item
156
+ if (segment.prefix) {
157
+ result.push(segment.prefix + segment.content + '\n')
158
+ } else {
159
+ // Raw content (no prefix means it's original raw)
160
+ // Ensure raw ends with newline for proper separation from next segment
161
+ const raw = segment.content.trimEnd()
162
+ result.push(raw + '\n')
163
+ }
164
+ }
165
+ }
166
+
167
+ return result.join('').trimEnd()
168
+ }
package/src/upgrade.ts ADDED
@@ -0,0 +1,127 @@
1
+ // Kimaki self-upgrade utilities.
2
+ // Detects the package manager used to install kimaki, checks npm for newer versions,
3
+ // and runs the global upgrade command. Used by both CLI `kimaki upgrade` and
4
+ // the Discord `/upgrade-and-restart` command, plus background auto-upgrade on startup.
5
+
6
+ import fs from 'node:fs'
7
+ import { createRequire } from 'node:module'
8
+ import { createLogger, LogPrefix } from './logger.js'
9
+ import { execAsync } from './worktree-utils.js'
10
+
11
+ const logger = createLogger(LogPrefix.CLI)
12
+
13
+ type Pm = 'bun' | 'pnpm' | 'npm'
14
+
15
+ // Detects which package manager globally installed kimaki, used to run the
16
+ // correct `<pm> i -g kimaki@latest` upgrade command.
17
+ //
18
+ // Detection order:
19
+ // 1. npm_config_user_agent — set by npx/bunx/pnpm dlx, reliable for those cases
20
+ // 2. Realpath of the running script — resolve symlinks and check if the path
21
+ // lives under a known PM global directory (e.g. ~/.bun, ~/Library/pnpm,
22
+ // /usr/local/lib/node_modules). Inspired by sindresorhus/global-directory.
23
+ // 3. process.versions.bun — if the runtime itself is Bun, likely bun ecosystem
24
+ // 4. Default to npm — safest fallback since npm is the most common global installer
25
+ export function detectPm(): Pm {
26
+ const ua = process.env.npm_config_user_agent
27
+ if (ua?.startsWith('bun/')) {
28
+ return 'bun'
29
+ }
30
+ if (ua?.startsWith('pnpm/')) {
31
+ return 'pnpm'
32
+ }
33
+ if (ua?.startsWith('npm/')) {
34
+ return 'npm'
35
+ }
36
+
37
+ const scriptPath = resolveScriptRealpath()
38
+ if (scriptPath) {
39
+ const p = scriptPath.toLowerCase()
40
+ // bun global installs live under ~/.bun or $BUN_INSTALL
41
+ if (p.includes('.bun/') || p.includes('/bun/install/')) {
42
+ return 'bun'
43
+ }
44
+ // pnpm global installs live under ~/Library/pnpm, ~/.local/share/pnpm, or $PNPM_HOME
45
+ if (p.includes('/pnpm/')) {
46
+ return 'pnpm'
47
+ }
48
+ // npm global installs typically live under lib/node_modules/kimaki without
49
+ // any pnpm or bun path segments, so if we reach here it's likely npm
50
+ }
51
+
52
+ if (process.versions.bun) {
53
+ return 'bun'
54
+ }
55
+
56
+ return 'npm'
57
+ }
58
+
59
+ function resolveScriptRealpath(): string | null {
60
+ try {
61
+ const script = process.argv[1]
62
+ if (!script) {
63
+ return null
64
+ }
65
+ return fs.realpathSync(script)
66
+ } catch {
67
+ return null
68
+ }
69
+ }
70
+
71
+ export function getCurrentVersion(): string {
72
+ const require = createRequire(import.meta.url)
73
+ const pkg = require('../package.json') as { version: string }
74
+ return pkg.version
75
+ }
76
+
77
+ export async function getLatestNpmVersion(): Promise<string | null> {
78
+ try {
79
+ const res = await fetch('https://registry.npmjs.org/kimaki/latest', {
80
+ signal: AbortSignal.timeout(15_000),
81
+ })
82
+ if (!res.ok) {
83
+ return null
84
+ }
85
+ const data = (await res.json()) as { version: string }
86
+ return data.version
87
+ } catch {
88
+ return null
89
+ }
90
+ }
91
+
92
+ // Returns the new version string if upgraded, null if already up to date.
93
+ export async function upgrade(): Promise<string | null> {
94
+ const current = getCurrentVersion()
95
+ const latest = await getLatestNpmVersion()
96
+ if (!latest) {
97
+ throw new Error('Failed to check latest version from npm')
98
+ }
99
+ if (current === latest) {
100
+ return null
101
+ }
102
+
103
+ const pm = detectPm()
104
+ logger.log(`Upgrading kimaki from v${current} to v${latest} using ${pm}...`)
105
+ await execAsync(`${pm} i -g kimaki@latest`, { timeout: 120_000 })
106
+
107
+ return latest
108
+ }
109
+
110
+ // Fire-and-forget background upgrade check on bot startup.
111
+ // Only upgrades if a newer version is available. Errors are silently ignored.
112
+ export async function backgroundUpgradeKimaki(): Promise<void> {
113
+ try {
114
+ const current = getCurrentVersion()
115
+ const latest = await getLatestNpmVersion()
116
+ if (!latest || current === latest) {
117
+ return
118
+ }
119
+
120
+ const pm = detectPm()
121
+ logger.debug(`Background kimaki upgrade started: v${current} -> v${latest}`)
122
+ await execAsync(`${pm} i -g kimaki@latest`, { timeout: 120_000 })
123
+ logger.debug(`Background kimaki upgrade completed: v${latest}`)
124
+ } catch {
125
+ // silently ignored, non-critical
126
+ }
127
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,145 @@
1
+ // General utility functions for the bot.
2
+ // Includes Discord OAuth URL generation, array deduplication,
3
+ // abort error detection, and date/time formatting helpers.
4
+
5
+ import os from 'node:os'
6
+ import { PermissionsBitField } from 'discord.js'
7
+
8
+ type GenerateInstallUrlOptions = {
9
+ clientId: string
10
+ permissions?: bigint[]
11
+ scopes?: string[]
12
+ guildId?: string
13
+ disableGuildSelect?: boolean
14
+ }
15
+
16
+ export function generateBotInstallUrl({
17
+ clientId,
18
+ permissions = [
19
+ PermissionsBitField.Flags.ViewChannel,
20
+ PermissionsBitField.Flags.ManageChannels,
21
+ PermissionsBitField.Flags.SendMessages,
22
+ PermissionsBitField.Flags.SendMessagesInThreads,
23
+ PermissionsBitField.Flags.CreatePublicThreads,
24
+ PermissionsBitField.Flags.ManageThreads,
25
+ PermissionsBitField.Flags.ReadMessageHistory,
26
+ PermissionsBitField.Flags.AddReactions,
27
+ PermissionsBitField.Flags.ManageMessages,
28
+ PermissionsBitField.Flags.UseExternalEmojis,
29
+ PermissionsBitField.Flags.AttachFiles,
30
+ PermissionsBitField.Flags.Connect,
31
+ PermissionsBitField.Flags.Speak,
32
+ PermissionsBitField.Flags.ManageRoles,
33
+ PermissionsBitField.Flags.ManageEvents,
34
+ PermissionsBitField.Flags.CreateEvents,
35
+ ],
36
+ scopes = ['bot'],
37
+ guildId,
38
+ disableGuildSelect = false,
39
+ }: GenerateInstallUrlOptions): string {
40
+ const permissionsBitField = new PermissionsBitField(permissions)
41
+ const permissionsValue = permissionsBitField.bitfield.toString()
42
+
43
+ const url = new URL('https://discord.com/api/oauth2/authorize')
44
+ url.searchParams.set('client_id', clientId)
45
+ url.searchParams.set('permissions', permissionsValue)
46
+ url.searchParams.set('scope', scopes.join(' '))
47
+
48
+ if (guildId) {
49
+ url.searchParams.set('guild_id', guildId)
50
+ }
51
+
52
+ if (disableGuildSelect) {
53
+ url.searchParams.set('disable_guild_select', 'true')
54
+ }
55
+
56
+ return url.toString()
57
+ }
58
+
59
+ export function deduplicateByKey<T, K>(arr: T[], keyFn: (item: T) => K): T[] {
60
+ const seen = new Set<K>()
61
+ return arr.filter((item) => {
62
+ const key = keyFn(item)
63
+ if (seen.has(key)) {
64
+ return false
65
+ }
66
+ seen.add(key)
67
+ return true
68
+ })
69
+ }
70
+
71
+ import * as errore from 'errore'
72
+
73
+ // Delegates to errore.isAbortError (walks cause chain for AbortError instances),
74
+ // then falls back to opencode server-specific abort patterns that aren't
75
+ // errore.AbortError but still represent aborted operations.
76
+ export function isAbortError(error: unknown): error is Error {
77
+ if (errore.isAbortError(error)) return true
78
+ if (!(error instanceof Error)) return false
79
+ return (
80
+ error.name === 'MessageAbortedError' ||
81
+ error.message?.includes('aborted') === true
82
+ )
83
+ }
84
+
85
+ const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
86
+
87
+ const TIME_DIVISIONS: Array<{
88
+ amount: number
89
+ name: Intl.RelativeTimeFormatUnit
90
+ }> = [
91
+ { amount: 60, name: 'seconds' },
92
+ { amount: 60, name: 'minutes' },
93
+ { amount: 24, name: 'hours' },
94
+ { amount: 7, name: 'days' },
95
+ { amount: 4.34524, name: 'weeks' },
96
+ { amount: 12, name: 'months' },
97
+ { amount: Number.POSITIVE_INFINITY, name: 'years' },
98
+ ]
99
+
100
+ export function formatDistanceToNow(date: Date): string {
101
+ let duration = (date.getTime() - Date.now()) / 1000
102
+
103
+ for (const division of TIME_DIVISIONS) {
104
+ if (Math.abs(duration) < division.amount) {
105
+ return rtf.format(Math.round(duration), division.name)
106
+ }
107
+ duration /= division.amount
108
+ }
109
+ return rtf.format(Math.round(duration), 'years')
110
+ }
111
+
112
+ const dtf = new Intl.DateTimeFormat('en-US', {
113
+ month: 'short',
114
+ day: 'numeric',
115
+ year: 'numeric',
116
+ hour: 'numeric',
117
+ minute: '2-digit',
118
+ hour12: true,
119
+ })
120
+
121
+ export function formatDateTime(date: Date): string {
122
+ return dtf.format(date)
123
+ }
124
+
125
+ // Comprehensive ANSI escape sequence regex covering CSI, OSC, and related sequences.
126
+ // Valid string terminator sequences are BEL, ESC\, and 0x9c.
127
+ const ANSI_REGEX = (() => {
128
+ const ST = '(?:\\u0007|\\u001B\\u005C|\\u009C)'
129
+ const osc = `(?:\\u001B\\][\\s\\S]*?${ST})`
130
+ const csi =
131
+ '[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]'
132
+ return new RegExp(`${osc}|${csi}`, 'g')
133
+ })()
134
+
135
+ export function stripAnsi(str: string): string {
136
+ return str.replace(ANSI_REGEX, '')
137
+ }
138
+
139
+ export function abbreviatePath(fullPath: string): string {
140
+ const home = os.homedir()
141
+ if (fullPath.startsWith(home)) {
142
+ return '~' + fullPath.slice(home.length)
143
+ }
144
+ return fullPath
145
+ }