@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,431 @@
1
+ // Worktree management command: /new-worktree
2
+ // Uses OpenCode SDK v2 to create worktrees with kimaki- prefix
3
+ // Creates thread immediately, then worktree in background so user can type
4
+
5
+ import {
6
+ ChannelType,
7
+ REST,
8
+ type TextChannel,
9
+ type ThreadChannel,
10
+ type Message,
11
+ } from 'discord.js'
12
+ import fs from 'node:fs'
13
+ import type { CommandContext } from './types.js'
14
+ import {
15
+ createPendingWorktree,
16
+ setWorktreeReady,
17
+ setWorktreeError,
18
+ getChannelDirectory,
19
+ getThreadWorktree,
20
+ } from '../database.js'
21
+ import { SILENT_MESSAGE_FLAGS, reactToThread } from '../discord-utils.js'
22
+ import { createLogger, LogPrefix } from '../logger.js'
23
+ import { notifyError } from '../sentry.js'
24
+ import {
25
+ createWorktreeWithSubmodules,
26
+ captureGitDiff,
27
+ execAsync,
28
+ type CapturedDiff,
29
+ } from '../worktree-utils.js'
30
+ import { WORKTREE_PREFIX } from './merge-worktree.js'
31
+ import * as errore from 'errore'
32
+
33
+ const logger = createLogger(LogPrefix.WORKTREE)
34
+
35
+ class WorktreeError extends Error {
36
+ constructor(message: string, options?: { cause?: unknown }) {
37
+ super(message, options)
38
+ this.name = 'WorktreeError'
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Format worktree name: lowercase, spaces to dashes, remove special chars, add opencode/kimaki- prefix.
44
+ * "My Feature" → "opencode/kimaki-my-feature"
45
+ * Returns empty string if no valid name can be extracted.
46
+ */
47
+ export function formatWorktreeName(name: string): string {
48
+ const formatted = name
49
+ .toLowerCase()
50
+ .trim()
51
+ .replace(/\s+/g, '-')
52
+ .replace(/[^a-z0-9-]/g, '')
53
+
54
+ if (!formatted) {
55
+ return ''
56
+ }
57
+ return `opencode/kimaki-${formatted}`
58
+ }
59
+
60
+ /**
61
+ * Derive worktree name from thread name.
62
+ * Handles existing "⬦ worktree: opencode/kimaki-name" format or uses thread name directly.
63
+ */
64
+ function deriveWorktreeNameFromThread(threadName: string): string {
65
+ // Handle existing "⬦ worktree: opencode/kimaki-name" format
66
+ const worktreeMatch = threadName.match(/worktree:\s*(.+)$/i)
67
+ const extractedName = worktreeMatch?.[1]?.trim()
68
+ if (extractedName) {
69
+ // If already has opencode/kimaki- prefix, return as is
70
+ if (extractedName.startsWith('opencode/kimaki-')) {
71
+ return extractedName
72
+ }
73
+ return formatWorktreeName(extractedName)
74
+ }
75
+ // Use thread name directly
76
+ return formatWorktreeName(threadName)
77
+ }
78
+
79
+ /**
80
+ * Get project directory from database.
81
+ */
82
+ async function getProjectDirectoryFromChannel(
83
+ channel: TextChannel,
84
+ appId: string,
85
+ ): Promise<string | WorktreeError> {
86
+ const channelConfig = await getChannelDirectory(channel.id)
87
+
88
+ if (!channelConfig) {
89
+ return new WorktreeError(
90
+ 'This channel is not configured with a project directory',
91
+ )
92
+ }
93
+
94
+ if (channelConfig.appId && channelConfig.appId !== appId) {
95
+ return new WorktreeError('This channel is not configured for this bot')
96
+ }
97
+
98
+ if (!fs.existsSync(channelConfig.directory)) {
99
+ return new WorktreeError(
100
+ `Directory does not exist: ${channelConfig.directory}`,
101
+ )
102
+ }
103
+
104
+ return channelConfig.directory
105
+ }
106
+
107
+ /**
108
+ * Create worktree in background and update starter message when done.
109
+ * If diff is provided, it's applied during worktree creation (before submodule init).
110
+ */
111
+ async function createWorktreeInBackground({
112
+ thread,
113
+ starterMessage,
114
+ worktreeName,
115
+ projectDirectory,
116
+ diff,
117
+ rest,
118
+ }: {
119
+ thread: ThreadChannel
120
+ starterMessage: Message
121
+ worktreeName: string
122
+ projectDirectory: string
123
+ diff?: CapturedDiff | null
124
+ rest: REST
125
+ }): Promise<void> {
126
+ // Create worktree using git, apply diff, then init submodules
127
+ logger.log(
128
+ `Creating worktree "${worktreeName}" for project ${projectDirectory}`,
129
+ )
130
+ const worktreeResult = await createWorktreeWithSubmodules({
131
+ directory: projectDirectory,
132
+ name: worktreeName,
133
+ diff,
134
+ })
135
+
136
+ if (worktreeResult instanceof Error) {
137
+ const errorMsg = worktreeResult.message
138
+ logger.error('[NEW-WORKTREE] Error:', worktreeResult)
139
+ await setWorktreeError({ threadId: thread.id, errorMessage: errorMsg })
140
+ await starterMessage.edit(
141
+ `🌳 **Worktree: ${worktreeName}**\n❌ ${errorMsg}`,
142
+ )
143
+ return
144
+ }
145
+
146
+ // Success - update database and edit starter message
147
+ await setWorktreeReady({
148
+ threadId: thread.id,
149
+ worktreeDirectory: worktreeResult.directory,
150
+ })
151
+
152
+ // React with tree emoji to mark as worktree thread
153
+ await reactToThread({
154
+ rest,
155
+ threadId: thread.id,
156
+ channelId: thread.parentId || undefined,
157
+ emoji: '🌳',
158
+ })
159
+
160
+ const diffStatus = diff
161
+ ? worktreeResult.diffApplied
162
+ ? '\n✅ Changes applied'
163
+ : '\n⚠️ Failed to apply changes'
164
+ : ''
165
+ await starterMessage.edit(
166
+ `🌳 **Worktree: ${worktreeName}**\n` +
167
+ `📁 \`${worktreeResult.directory}\`\n` +
168
+ `🌿 Branch: \`${worktreeResult.branch}\`` +
169
+ diffStatus,
170
+ )
171
+ }
172
+
173
+ async function findExistingWorktreePath({
174
+ projectDirectory,
175
+ worktreeName,
176
+ }: {
177
+ projectDirectory: string
178
+ worktreeName: string
179
+ }): Promise<string | undefined | Error> {
180
+ const listResult = await errore.tryAsync({
181
+ try: () =>
182
+ execAsync('git worktree list --porcelain', { cwd: projectDirectory }),
183
+ catch: (e) => new WorktreeError('Failed to list worktrees', { cause: e }),
184
+ })
185
+ if (errore.isError(listResult)) {
186
+ return listResult
187
+ }
188
+
189
+ const lines = listResult.stdout.split('\n')
190
+ let currentPath = ''
191
+ const branchRef = `refs/heads/${worktreeName}`
192
+
193
+ for (const line of lines) {
194
+ if (line.startsWith('worktree ')) {
195
+ currentPath = line.slice('worktree '.length)
196
+ continue
197
+ }
198
+ if (
199
+ line.startsWith('branch ') &&
200
+ line.slice('branch '.length) === branchRef
201
+ ) {
202
+ return currentPath || undefined
203
+ }
204
+ }
205
+
206
+ return undefined
207
+ }
208
+
209
+ export async function handleNewWorktreeCommand({
210
+ command,
211
+ appId,
212
+ }: CommandContext): Promise<void> {
213
+ await command.deferReply({ ephemeral: false })
214
+
215
+ const channel = command.channel
216
+ if (!channel) {
217
+ await command.editReply('Cannot determine channel')
218
+ return
219
+ }
220
+
221
+ const isThread =
222
+ channel.type === ChannelType.PublicThread ||
223
+ channel.type === ChannelType.PrivateThread
224
+
225
+ // Handle command in existing thread - attach worktree to this thread
226
+ if (isThread) {
227
+ await handleWorktreeInThread({
228
+ command,
229
+ appId,
230
+ thread: channel as ThreadChannel,
231
+ })
232
+ return
233
+ }
234
+
235
+ // Handle command in text channel - create new thread with worktree (existing behavior)
236
+ if (channel.type !== ChannelType.GuildText) {
237
+ await command.editReply(
238
+ 'This command can only be used in text channels or threads',
239
+ )
240
+ return
241
+ }
242
+
243
+ const rawName = command.options.getString('name')
244
+ if (!rawName) {
245
+ await command.editReply(
246
+ 'Name is required when creating a worktree from a text channel. Use `/new-worktree name:my-feature`',
247
+ )
248
+ return
249
+ }
250
+
251
+ const worktreeName = formatWorktreeName(rawName)
252
+ if (!worktreeName) {
253
+ await command.editReply(
254
+ 'Invalid worktree name. Please use letters, numbers, and spaces.',
255
+ )
256
+ return
257
+ }
258
+
259
+ const textChannel = channel as TextChannel
260
+
261
+ const projectDirectory = await getProjectDirectoryFromChannel(
262
+ textChannel,
263
+ appId,
264
+ )
265
+ if (errore.isError(projectDirectory)) {
266
+ await command.editReply(projectDirectory.message)
267
+ return
268
+ }
269
+
270
+ const existingWorktree = await findExistingWorktreePath({
271
+ projectDirectory,
272
+ worktreeName,
273
+ })
274
+ if (errore.isError(existingWorktree)) {
275
+ await command.editReply(existingWorktree.message)
276
+ return
277
+ }
278
+ if (existingWorktree) {
279
+ await command.editReply(
280
+ `Worktree \`${worktreeName}\` already exists at \`${existingWorktree}\``,
281
+ )
282
+ return
283
+ }
284
+
285
+ // Create thread immediately so user can start typing
286
+ const result = await errore.tryAsync({
287
+ try: async () => {
288
+ const starterMessage = await textChannel.send({
289
+ content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...`,
290
+ flags: SILENT_MESSAGE_FLAGS,
291
+ })
292
+
293
+ const thread = await starterMessage.startThread({
294
+ name: `${WORKTREE_PREFIX}worktree: ${worktreeName}`,
295
+ autoArchiveDuration: 1440,
296
+ reason: 'Worktree session',
297
+ })
298
+
299
+ // Add user to thread so it appears in their sidebar
300
+ await thread.members.add(command.user.id)
301
+
302
+ return { thread, starterMessage }
303
+ },
304
+ catch: (e) => new WorktreeError('Failed to create thread', { cause: e }),
305
+ })
306
+
307
+ if (errore.isError(result)) {
308
+ logger.error('[NEW-WORKTREE] Error:', result.cause)
309
+ await command.editReply(result.message)
310
+ return
311
+ }
312
+
313
+ const { thread, starterMessage } = result
314
+
315
+ // Store pending worktree in database
316
+ await createPendingWorktree({
317
+ threadId: thread.id,
318
+ worktreeName,
319
+ projectDirectory,
320
+ })
321
+
322
+ await command.editReply(`Creating worktree in ${thread.toString()}`)
323
+
324
+ // Create worktree in background (don't await)
325
+ createWorktreeInBackground({
326
+ thread,
327
+ starterMessage,
328
+ worktreeName,
329
+ projectDirectory,
330
+ rest: command.client.rest,
331
+ }).catch((e) => {
332
+ logger.error('[NEW-WORKTREE] Background error:', e)
333
+ void notifyError(e, 'Background worktree creation failed')
334
+ })
335
+ }
336
+
337
+ /**
338
+ * Handle /new-worktree when called inside an existing thread.
339
+ * Attaches a worktree to the current thread, using thread name if no name provided.
340
+ */
341
+ async function handleWorktreeInThread({
342
+ command,
343
+ appId,
344
+ thread,
345
+ }: CommandContext & { thread: ThreadChannel }): Promise<void> {
346
+ // Error if thread already has a worktree
347
+ if (await getThreadWorktree(thread.id)) {
348
+ await command.editReply('This thread already has a worktree attached.')
349
+ return
350
+ }
351
+
352
+ // Get worktree name from parameter or derive from thread name
353
+ const rawName = command.options.getString('name')
354
+ const worktreeName = rawName
355
+ ? formatWorktreeName(rawName)
356
+ : deriveWorktreeNameFromThread(thread.name)
357
+
358
+ if (!worktreeName) {
359
+ await command.editReply(
360
+ 'Invalid worktree name. Please provide a name or rename the thread.',
361
+ )
362
+ return
363
+ }
364
+
365
+ // Get parent channel for project directory
366
+ const parent = thread.parent
367
+ if (!parent || parent.type !== ChannelType.GuildText) {
368
+ await command.editReply('Cannot determine parent channel')
369
+ return
370
+ }
371
+
372
+ const projectDirectory = await getProjectDirectoryFromChannel(
373
+ parent as TextChannel,
374
+ appId,
375
+ )
376
+ if (errore.isError(projectDirectory)) {
377
+ await command.editReply(projectDirectory.message)
378
+ return
379
+ }
380
+
381
+ const existingWorktreePath = await findExistingWorktreePath({
382
+ projectDirectory,
383
+ worktreeName,
384
+ })
385
+ if (errore.isError(existingWorktreePath)) {
386
+ await command.editReply(existingWorktreePath.message)
387
+ return
388
+ }
389
+ if (existingWorktreePath) {
390
+ await command.editReply(
391
+ `Worktree \`${worktreeName}\` already exists at \`${existingWorktreePath}\``,
392
+ )
393
+ return
394
+ }
395
+
396
+ // Capture git diff from project directory before creating worktree.
397
+ // This allows transferring uncommitted changes to the new worktree.
398
+ const diff = await captureGitDiff(projectDirectory)
399
+ const hasDiff = diff && (diff.staged || diff.unstaged)
400
+
401
+ // Store pending worktree in database for this existing thread
402
+ await createPendingWorktree({
403
+ threadId: thread.id,
404
+ worktreeName,
405
+ projectDirectory,
406
+ })
407
+
408
+ // Send status message in thread
409
+ const diffNote = hasDiff ? '\n📋 Will transfer uncommitted changes' : ''
410
+ const statusMessage = await thread.send({
411
+ content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...${diffNote}`,
412
+ flags: SILENT_MESSAGE_FLAGS,
413
+ })
414
+
415
+ await command.editReply(
416
+ `Creating worktree \`${worktreeName}\` for this thread...`,
417
+ )
418
+
419
+ // Create worktree in background, passing diff to apply after creation
420
+ createWorktreeInBackground({
421
+ thread,
422
+ starterMessage: statusMessage,
423
+ worktreeName,
424
+ projectDirectory,
425
+ diff,
426
+ rest: command.client.rest,
427
+ }).catch((e) => {
428
+ logger.error('[NEW-WORKTREE] Background error:', e)
429
+ void notifyError(e, 'Background worktree creation failed (in-thread)')
430
+ })
431
+ }
@@ -0,0 +1,36 @@
1
+ // Utility to condense MEMORY.md into a line-numbered table of contents.
2
+ // Separated from opencode-plugin.ts because OpenCode's plugin loader calls
3
+ // every exported function in the module as a plugin initializer — exporting
4
+ // this utility from the plugin entry file caused it to be invoked with a
5
+ // PluginInput object instead of a string, crashing inside marked's Lexer.
6
+
7
+ import { Lexer } from 'marked'
8
+
9
+ /**
10
+ * Condense MEMORY.md into a line-numbered table of contents.
11
+ * Parses markdown AST with marked's Lexer, emits each heading prefixed by
12
+ * its source line number, and collapses non-heading content to `...`.
13
+ * The agent can then use Read with offset/limit to read specific sections.
14
+ */
15
+ export function condenseMemoryMd(content: string): string {
16
+ const tokens = new Lexer().lex(content)
17
+ const lines: string[] = []
18
+ let charOffset = 0
19
+ let lastWasEllipsis = false
20
+
21
+ for (const token of tokens) {
22
+ // Compute 1-based line number from character offset
23
+ const lineNumber = content.slice(0, charOffset).split('\n').length
24
+ if (token.type === 'heading') {
25
+ const prefix = '#'.repeat(token.depth)
26
+ lines.push(`${lineNumber}: ${prefix} ${token.text}`)
27
+ lastWasEllipsis = false
28
+ } else if (!lastWasEllipsis) {
29
+ lines.push('...')
30
+ lastWasEllipsis = true
31
+ }
32
+ charOffset += token.raw.length
33
+ }
34
+
35
+ return lines.join('\n')
36
+ }
package/src/config.ts ADDED
@@ -0,0 +1,148 @@
1
+ // Runtime configuration for Kimaki bot.
2
+ // Stores data directory path and provides accessors for other modules.
3
+ // Must be initialized before database or other path-dependent modules are used.
4
+
5
+ import fs from 'node:fs'
6
+ import os from 'node:os'
7
+ import path from 'node:path'
8
+
9
+ const DEFAULT_DATA_DIR = path.join(os.homedir(), '.kimaki')
10
+
11
+ let dataDir: string | null = null
12
+
13
+ /**
14
+ * Get the data directory path.
15
+ * Falls back to ~/.kimaki if not explicitly set.
16
+ * Under vitest (KIMAKI_VITEST env var), auto-creates an isolated temp dir so
17
+ * tests never touch the real ~/.kimaki/ database. Tests that need a specific
18
+ * dir can still call setDataDir() before any DB access to override this.
19
+ */
20
+ export function getDataDir(): string {
21
+ if (!dataDir) {
22
+ if (process.env.KIMAKI_VITEST) {
23
+ dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kimaki-test-'))
24
+ } else {
25
+ dataDir = DEFAULT_DATA_DIR
26
+ }
27
+ }
28
+ return dataDir
29
+ }
30
+
31
+ /**
32
+ * Set the data directory path.
33
+ * Creates the directory if it doesn't exist.
34
+ * Must be called before any database or path-dependent operations.
35
+ */
36
+ export function setDataDir(dir: string): void {
37
+ const resolvedDir = path.resolve(dir)
38
+
39
+ if (!fs.existsSync(resolvedDir)) {
40
+ fs.mkdirSync(resolvedDir, { recursive: true })
41
+ }
42
+
43
+ dataDir = resolvedDir
44
+ }
45
+
46
+ /**
47
+ * Get the projects directory path (for /create-new-project command).
48
+ * Returns <dataDir>/projects
49
+ */
50
+ export function getProjectsDir(): string {
51
+ return path.join(getDataDir(), 'projects')
52
+ }
53
+
54
+ // Default verbosity for channels that haven't set a per-channel override.
55
+ // Set via --verbosity CLI flag at startup.
56
+ import type { VerbosityLevel } from './database.js'
57
+
58
+ let defaultVerbosity: VerbosityLevel = 'text-and-essential-tools'
59
+
60
+ export function getDefaultVerbosity(): VerbosityLevel {
61
+ return defaultVerbosity
62
+ }
63
+
64
+ export function setDefaultVerbosity(level: VerbosityLevel): void {
65
+ defaultVerbosity = level
66
+ }
67
+
68
+ // Default mention mode for channels that haven't set a per-channel override.
69
+ // Set via --mention-mode CLI flag at startup.
70
+ let defaultMentionMode = false
71
+
72
+ export function getDefaultMentionMode(): boolean {
73
+ return defaultMentionMode
74
+ }
75
+
76
+ export function setDefaultMentionMode(enabled: boolean): void {
77
+ defaultMentionMode = enabled
78
+ }
79
+
80
+ // Whether critique (diff upload to critique.work) is enabled in system prompts.
81
+ // Enabled by default, disabled via --no-critique CLI flag.
82
+ let critiqueEnabled = true
83
+
84
+ export function getCritiqueEnabled(): boolean {
85
+ return critiqueEnabled
86
+ }
87
+
88
+ export function setCritiqueEnabled(enabled: boolean): void {
89
+ critiqueEnabled = enabled
90
+ }
91
+
92
+ // Whether to forward OpenCode server stdout/stderr to kimaki.log.
93
+ // Disabled by default, enabled via --verbose-opencode-server CLI flag.
94
+ let verboseOpencodeServer = false
95
+
96
+ export function getVerboseOpencodeServer(): boolean {
97
+ return verboseOpencodeServer
98
+ }
99
+
100
+ export function setVerboseOpencodeServer(enabled: boolean): void {
101
+ verboseOpencodeServer = enabled
102
+ }
103
+
104
+ // Registered user commands, populated by registerCommands() in cli.ts.
105
+ // Stored here (not cli.ts) to avoid circular imports since commands/ modules need this.
106
+ // discordName is the sanitized Discord slash command name (without -cmd suffix),
107
+ // name is the original OpenCode command name (may contain :, /, etc).
108
+ export type RegisteredUserCommand = {
109
+ name: string
110
+ discordName: string
111
+ description: string
112
+ }
113
+ export const registeredUserCommands: RegisteredUserCommand[] = []
114
+
115
+ const DEFAULT_LOCK_PORT = 29988
116
+
117
+ /**
118
+ * Derive a lock port from the data directory path.
119
+ * If KIMAKI_LOCK_PORT is set to a valid TCP port, it takes precedence.
120
+ * Returns 29988 for the default ~/.kimaki directory (backwards compatible).
121
+ * For custom data dirs, uses a hash to generate a port in the range 30000-39999.
122
+ */
123
+ export function getLockPort(): number {
124
+ const envPortRaw = process.env['KIMAKI_LOCK_PORT']
125
+ if (envPortRaw) {
126
+ const envPort = Number.parseInt(envPortRaw, 10)
127
+ if (Number.isInteger(envPort) && envPort >= 1 && envPort <= 65535) {
128
+ return envPort
129
+ }
130
+ }
131
+
132
+ const dir = getDataDir()
133
+
134
+ // Use original port for default data dir (backwards compatible)
135
+ if (dir === DEFAULT_DATA_DIR) {
136
+ return DEFAULT_LOCK_PORT
137
+ }
138
+
139
+ // Hash-based port for custom data dirs
140
+ let hash = 0
141
+ for (let i = 0; i < dir.length; i++) {
142
+ const char = dir.charCodeAt(i)
143
+ hash = (hash << 5) - hash + char
144
+ hash = hash & hash // Convert to 32bit integer
145
+ }
146
+ // Map to port range 30000-39999
147
+ return 30000 + (Math.abs(hash) % 10000)
148
+ }