@otto-assistant/bridge 0.4.92

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 (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,410 @@
1
+ // /worktrees command — list worktree sessions for the current channel's project.
2
+ // Renders a markdown table that the CV2 pipeline auto-formats for Discord,
3
+ // including HTML-backed action buttons for deletable worktrees.
4
+ import { ButtonInteraction, ChatInputCommandInteraction, ChannelType, ComponentType, MessageFlags, } from 'discord.js';
5
+ import { deleteThreadWorktree, getThreadWorktree, } from '../database.js';
6
+ import { getPrisma } from '../db.js';
7
+ import { splitTablesFromMarkdown } from '../format-tables.js';
8
+ import { buildHtmlActionCustomId, cancelHtmlActionsForOwner, registerHtmlAction, } from '../html-actions.js';
9
+ import * as errore from 'errore';
10
+ import { GitCommandError } from '../errors.js';
11
+ import { resolveWorkingDirectory } from '../discord-utils.js';
12
+ import { deleteWorktree, git, getDefaultBranch } from '../worktrees.js';
13
+ // Extracts the git stderr from a deleteWorktree error via errore.findCause.
14
+ // Chain: Error { cause: GitCommandError { cause: CommandError { stderr } } }.
15
+ export function extractGitStderr(error) {
16
+ const gitErr = errore.findCause(error, GitCommandError);
17
+ const stderr = gitErr?.cause?.stderr?.trim();
18
+ if (stderr && stderr.length > 0) {
19
+ return stderr;
20
+ }
21
+ return undefined;
22
+ }
23
+ export function formatTimeAgo(date) {
24
+ const diffMs = Date.now() - date.getTime();
25
+ if (diffMs < 0) {
26
+ return 'just now';
27
+ }
28
+ const totalSeconds = Math.floor(diffMs / 1000);
29
+ if (totalSeconds < 60) {
30
+ return `${totalSeconds}s ago`;
31
+ }
32
+ const totalMinutes = Math.floor(totalSeconds / 60);
33
+ if (totalMinutes < 60) {
34
+ return `${totalMinutes}m ago`;
35
+ }
36
+ const hours = Math.floor(totalMinutes / 60);
37
+ const minutes = totalMinutes % 60;
38
+ if (hours < 24) {
39
+ return minutes > 0 ? `${hours}h ${minutes}m ago` : `${hours}h ago`;
40
+ }
41
+ const days = Math.floor(hours / 24);
42
+ const remainingHours = hours % 24;
43
+ return remainingHours > 0 ? `${days}d ${remainingHours}h ago` : `${days}d ago`;
44
+ }
45
+ function statusLabel(wt) {
46
+ if (wt.status === 'ready') {
47
+ return 'ready';
48
+ }
49
+ if (wt.status === 'error') {
50
+ return 'error';
51
+ }
52
+ return 'pending';
53
+ }
54
+ // 5s timeout per git call — prevents hangs from deleted dirs, git locks, slow disks.
55
+ // Returns null on timeout/error so the table shows "unknown" for that worktree.
56
+ const GIT_CMD_TIMEOUT = 5_000;
57
+ const GLOBAL_TIMEOUT = 10_000;
58
+ // Checks dirty state and commits ahead of default branch in parallel.
59
+ // Returns null for worktrees that aren't ready or when the directory is
60
+ // missing / git commands fail / timeout (e.g. deleted worktree folder).
61
+ async function getWorktreeGitStatus({ wt, defaultBranch, }) {
62
+ if (wt.status !== 'ready' || !wt.worktree_directory) {
63
+ return null;
64
+ }
65
+ try {
66
+ const dir = wt.worktree_directory;
67
+ // Use raw git calls so errors/timeouts are visible — isDirty() swallows
68
+ // errors and returns false, which would render "merged" instead of "unknown".
69
+ const [statusResult, aheadResult] = await Promise.all([
70
+ git(dir, 'status --porcelain', { timeout: GIT_CMD_TIMEOUT }),
71
+ git(dir, `rev-list --count "${defaultBranch}..HEAD"`, {
72
+ timeout: GIT_CMD_TIMEOUT,
73
+ }),
74
+ ]);
75
+ if (statusResult instanceof Error || aheadResult instanceof Error) {
76
+ return null;
77
+ }
78
+ const aheadCount = parseInt(aheadResult, 10);
79
+ if (!Number.isFinite(aheadCount)) {
80
+ return null;
81
+ }
82
+ return { dirty: statusResult.length > 0, aheadCount };
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ function buildWorktreeTable({ worktrees, gitStatuses, guildId, }) {
89
+ const header = '| Thread | Name | Status | Created | Folder | Action |';
90
+ const separator = '|---|---|---|---|---|---|';
91
+ const rows = worktrees.map((wt, i) => {
92
+ const threadLink = `[thread](https://discord.com/channels/${guildId}/${wt.thread_id})`;
93
+ const name = wt.worktree_name;
94
+ const gs = gitStatuses[i] ?? null;
95
+ const status = (() => {
96
+ if (wt.status !== 'ready') {
97
+ return statusLabel(wt);
98
+ }
99
+ if (!gs) {
100
+ return 'unknown';
101
+ }
102
+ const parts = [];
103
+ if (gs.dirty) {
104
+ parts.push('dirty');
105
+ }
106
+ if (gs.aheadCount > 0) {
107
+ parts.push(`${gs.aheadCount} ahead`);
108
+ }
109
+ else {
110
+ parts.push('merged');
111
+ }
112
+ return parts.join(', ');
113
+ })();
114
+ const created = wt.created_at ? formatTimeAgo(wt.created_at) : 'unknown';
115
+ const folder = wt.worktree_directory ?? wt.project_directory;
116
+ const action = buildActionCell({ wt, gitStatus: gs });
117
+ return `| ${threadLink} | ${name} | ${status} | ${created} | ${folder} | ${action} |`;
118
+ });
119
+ return [header, separator, ...rows].join('\n');
120
+ }
121
+ function buildActionCell({ wt, gitStatus, }) {
122
+ if (!canDeleteWorktree({ wt, gitStatus })) {
123
+ return '-';
124
+ }
125
+ return buildDeleteButtonHtml({
126
+ buttonId: `delete-worktree-${wt.thread_id}`,
127
+ });
128
+ }
129
+ function buildDeleteButtonHtml({ buttonId, }) {
130
+ return `<button id="${buttonId}" variant="secondary">Delete</button>`;
131
+ }
132
+ function canDeleteWorktree({ wt, gitStatus, }) {
133
+ if (wt.status !== 'ready' || !wt.worktree_directory) {
134
+ return false;
135
+ }
136
+ if (!gitStatus) {
137
+ return false;
138
+ }
139
+ if (gitStatus.dirty) {
140
+ return false;
141
+ }
142
+ return gitStatus.aheadCount === 0;
143
+ }
144
+ // Resolves git statuses for all worktrees within a single global deadline.
145
+ // Caches getDefaultBranch per project_directory to avoid redundant spawns.
146
+ // Returns null for any worktree whose git calls fail, timeout, or exceed
147
+ // the global deadline — the table renders those as "unknown".
148
+ async function resolveGitStatuses({ worktrees, timeout, }) {
149
+ const nullFallback = worktrees.map(() => null);
150
+ let timer;
151
+ const deadline = new Promise((resolve) => {
152
+ timer = setTimeout(() => {
153
+ resolve(nullFallback);
154
+ }, timeout);
155
+ });
156
+ const work = (async () => {
157
+ // Resolve default branch once per unique project directory (avoids
158
+ // redundant git subprocess spawns when multiple worktrees share a project).
159
+ const uniqueProjectDirs = [
160
+ ...new Set(worktrees.map((wt) => wt.project_directory)),
161
+ ];
162
+ const defaultBranchEntries = await Promise.all(uniqueProjectDirs.map(async (dir) => {
163
+ const branch = await getDefaultBranch(dir, { timeout: GIT_CMD_TIMEOUT });
164
+ return [dir, branch];
165
+ }));
166
+ const defaultBranchByProject = new Map(defaultBranchEntries);
167
+ return Promise.all(worktrees.map((wt) => {
168
+ const defaultBranch = defaultBranchByProject.get(wt.project_directory) ?? 'main';
169
+ return getWorktreeGitStatus({ wt, defaultBranch });
170
+ }));
171
+ })();
172
+ try {
173
+ return await Promise.race([work, deadline]);
174
+ }
175
+ finally {
176
+ clearTimeout(timer);
177
+ }
178
+ }
179
+ async function getRecentWorktrees({ projectDirectory, }) {
180
+ const prisma = await getPrisma();
181
+ return await prisma.thread_worktrees.findMany({
182
+ where: {
183
+ project_directory: projectDirectory,
184
+ },
185
+ orderBy: { created_at: 'desc' },
186
+ take: 10,
187
+ });
188
+ }
189
+ function getWorktreesActionOwnerKey({ userId, channelId, }) {
190
+ return `worktrees:${userId}:${channelId}`;
191
+ }
192
+ function isProjectChannel(channel) {
193
+ if (!channel) {
194
+ return false;
195
+ }
196
+ return [
197
+ ChannelType.GuildText,
198
+ ChannelType.PublicThread,
199
+ ChannelType.PrivateThread,
200
+ ChannelType.AnnouncementThread,
201
+ ].includes(channel.type);
202
+ }
203
+ async function renderWorktreesReply({ guildId, userId, channelId, projectDirectory, notice, editReply, }) {
204
+ const ownerKey = getWorktreesActionOwnerKey({ userId, channelId });
205
+ cancelHtmlActionsForOwner(ownerKey);
206
+ const worktrees = await getRecentWorktrees({ projectDirectory });
207
+ if (worktrees.length === 0) {
208
+ const message = notice ? `${notice}\n\nNo worktrees found.` : 'No worktrees found.';
209
+ const textDisplay = {
210
+ type: ComponentType.TextDisplay,
211
+ content: message,
212
+ };
213
+ await editReply({
214
+ components: [textDisplay],
215
+ flags: MessageFlags.IsComponentsV2,
216
+ });
217
+ return;
218
+ }
219
+ const gitStatuses = await resolveGitStatuses({
220
+ worktrees,
221
+ timeout: GLOBAL_TIMEOUT,
222
+ });
223
+ const deletableWorktreesByButtonId = new Map();
224
+ worktrees.forEach((wt, index) => {
225
+ const gitStatus = gitStatuses[index] ?? null;
226
+ if (!canDeleteWorktree({ wt, gitStatus })) {
227
+ return;
228
+ }
229
+ deletableWorktreesByButtonId.set(`delete-worktree-${wt.thread_id}`, wt);
230
+ });
231
+ const tableMarkdown = buildWorktreeTable({
232
+ worktrees,
233
+ gitStatuses,
234
+ guildId,
235
+ });
236
+ const markdown = notice ? `${notice}\n\n${tableMarkdown}` : tableMarkdown;
237
+ const segments = splitTablesFromMarkdown(markdown, {
238
+ resolveButtonCustomId: ({ button }) => {
239
+ const worktree = deletableWorktreesByButtonId.get(button.id);
240
+ if (!worktree) {
241
+ return new Error(`No worktree registered for button ${button.id}`);
242
+ }
243
+ const actionId = registerHtmlAction({
244
+ ownerKey,
245
+ threadId: worktree.thread_id,
246
+ run: async ({ interaction }) => {
247
+ await handleDeleteWorktreeAction({
248
+ interaction,
249
+ threadId: worktree.thread_id,
250
+ });
251
+ },
252
+ });
253
+ return buildHtmlActionCustomId(actionId);
254
+ },
255
+ });
256
+ const components = segments.flatMap((segment) => {
257
+ if (segment.type === 'components') {
258
+ return segment.components;
259
+ }
260
+ const textDisplay = {
261
+ type: ComponentType.TextDisplay,
262
+ content: segment.text,
263
+ };
264
+ return [textDisplay];
265
+ });
266
+ await editReply({
267
+ components,
268
+ flags: MessageFlags.IsComponentsV2,
269
+ });
270
+ }
271
+ async function handleDeleteWorktreeAction({ interaction, threadId, }) {
272
+ const guildId = interaction.guildId;
273
+ if (!guildId) {
274
+ await interaction.editReply({
275
+ components: [
276
+ {
277
+ type: ComponentType.TextDisplay,
278
+ content: 'This action can only be used in a server.',
279
+ },
280
+ ],
281
+ flags: MessageFlags.IsComponentsV2,
282
+ });
283
+ return;
284
+ }
285
+ const worktree = await getThreadWorktree(threadId);
286
+ if (!worktree) {
287
+ if (!isProjectChannel(interaction.channel)) {
288
+ await interaction.editReply({
289
+ components: [
290
+ {
291
+ type: ComponentType.TextDisplay,
292
+ content: 'This action can only be used in a project channel or thread.',
293
+ },
294
+ ],
295
+ flags: MessageFlags.IsComponentsV2,
296
+ });
297
+ return;
298
+ }
299
+ const resolved = await resolveWorkingDirectory({
300
+ channel: interaction.channel,
301
+ });
302
+ if (!resolved) {
303
+ await interaction.editReply({
304
+ components: [
305
+ {
306
+ type: ComponentType.TextDisplay,
307
+ content: 'Could not determine the project folder for this channel.',
308
+ },
309
+ ],
310
+ flags: MessageFlags.IsComponentsV2,
311
+ });
312
+ return;
313
+ }
314
+ await renderWorktreesReply({
315
+ guildId,
316
+ userId: interaction.user.id,
317
+ channelId: interaction.channelId,
318
+ projectDirectory: resolved.projectDirectory,
319
+ notice: 'Worktree was already removed.',
320
+ editReply: (options) => {
321
+ return interaction.editReply(options);
322
+ },
323
+ });
324
+ return;
325
+ }
326
+ if (worktree.status !== 'ready' || !worktree.worktree_directory) {
327
+ await renderWorktreesReply({
328
+ guildId,
329
+ userId: interaction.user.id,
330
+ channelId: interaction.channelId,
331
+ projectDirectory: worktree.project_directory,
332
+ notice: `Cannot delete \`${worktree.worktree_name}\` because it is ${worktree.status}.`,
333
+ editReply: (options) => {
334
+ return interaction.editReply(options);
335
+ },
336
+ });
337
+ return;
338
+ }
339
+ const deleteResult = await deleteWorktree({
340
+ projectDirectory: worktree.project_directory,
341
+ worktreeDirectory: worktree.worktree_directory,
342
+ worktreeName: worktree.worktree_name,
343
+ });
344
+ if (deleteResult instanceof Error) {
345
+ // Send error as a separate ephemeral follow-up so the table stays intact.
346
+ // Dig into cause chain to surface the actual git stderr when available.
347
+ const gitStderr = extractGitStderr(deleteResult);
348
+ const detail = gitStderr
349
+ ? `\`\`\`\n${gitStderr}\n\`\`\``
350
+ : deleteResult.message;
351
+ await interaction
352
+ .followUp({
353
+ content: `Failed to delete \`${worktree.worktree_name}\`\n${detail}`,
354
+ flags: MessageFlags.Ephemeral,
355
+ })
356
+ .catch(() => {
357
+ return undefined;
358
+ });
359
+ return;
360
+ }
361
+ await deleteThreadWorktree(threadId);
362
+ await renderWorktreesReply({
363
+ guildId,
364
+ userId: interaction.user.id,
365
+ channelId: interaction.channelId,
366
+ projectDirectory: worktree.project_directory,
367
+ notice: `Deleted \`${worktree.worktree_name}\`.`,
368
+ editReply: (options) => {
369
+ return interaction.editReply(options);
370
+ },
371
+ });
372
+ }
373
+ export async function handleWorktreesCommand({ command, }) {
374
+ const channel = command.channel;
375
+ const guildId = command.guildId;
376
+ if (!guildId || !channel) {
377
+ await command.reply({
378
+ content: 'This command can only be used in a server channel.',
379
+ flags: MessageFlags.Ephemeral,
380
+ });
381
+ return;
382
+ }
383
+ if (!isProjectChannel(channel)) {
384
+ await command.reply({
385
+ content: 'This command can only be used in a project channel or thread.',
386
+ flags: MessageFlags.Ephemeral,
387
+ });
388
+ return;
389
+ }
390
+ const resolved = await resolveWorkingDirectory({
391
+ channel: channel,
392
+ });
393
+ if (!resolved) {
394
+ await command.reply({
395
+ content: 'Could not determine the project folder for this channel.',
396
+ flags: MessageFlags.Ephemeral,
397
+ });
398
+ return;
399
+ }
400
+ await command.deferReply({ flags: MessageFlags.Ephemeral });
401
+ await renderWorktreesReply({
402
+ guildId,
403
+ userId: command.user.id,
404
+ channelId: command.channelId,
405
+ projectDirectory: resolved.projectDirectory,
406
+ editReply: (options) => {
407
+ return command.editReply(options);
408
+ },
409
+ });
410
+ }
@@ -0,0 +1,33 @@
1
+ // Utility to condense MEMORY.md into a line-numbered table of contents.
2
+ // Separated from kimaki-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
+ import { Lexer } from 'marked';
7
+ /**
8
+ * Condense MEMORY.md into a line-numbered table of contents.
9
+ * Parses markdown AST with marked's Lexer, emits each heading prefixed by
10
+ * its source line number, and collapses non-heading content to `...`.
11
+ * The agent can then use Read with offset/limit to read specific sections.
12
+ */
13
+ export function condenseMemoryMd(content) {
14
+ const tokens = new Lexer().lex(content);
15
+ const lines = [];
16
+ let charOffset = 0;
17
+ let lastWasEllipsis = false;
18
+ for (const token of tokens) {
19
+ // Compute 1-based line number from character offset
20
+ const lineNumber = content.slice(0, charOffset).split('\n').length;
21
+ if (token.type === 'heading') {
22
+ const prefix = '#'.repeat(token.depth);
23
+ lines.push(`${lineNumber}: ${prefix} ${token.text}`);
24
+ lastWasEllipsis = false;
25
+ }
26
+ else if (!lastWasEllipsis) {
27
+ lines.push('...');
28
+ lastWasEllipsis = true;
29
+ }
30
+ charOffset += token.raw.length;
31
+ }
32
+ return lines.join('\n');
33
+ }
package/dist/config.js ADDED
@@ -0,0 +1,94 @@
1
+ // Runtime configuration for Kimaki bot.
2
+ // Thin re-export layer over the centralized zustand store (store.ts).
3
+ // Getter/setter functions are kept for backwards compatibility so existing
4
+ // import sites don't need to change. They delegate to store.getState() and
5
+ // store.setState() under the hood.
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { store } from './store.js';
10
+ const DEFAULT_DATA_DIR = path.join(os.homedir(), '.kimaki');
11
+ /**
12
+ * Get the data directory path.
13
+ * Falls back to ~/.kimaki if not explicitly set.
14
+ * Under vitest (KIMAKI_VITEST env var), auto-creates an isolated temp dir so
15
+ * tests never touch the real ~/.kimaki/ database. Tests that need a specific
16
+ * dir can still call setDataDir() before any DB access to override this.
17
+ */
18
+ export function getDataDir() {
19
+ const current = store.getState().dataDir;
20
+ if (current) {
21
+ return current;
22
+ }
23
+ if (process.env.KIMAKI_VITEST) {
24
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kimaki-test-'));
25
+ store.setState({ dataDir: tmpDir });
26
+ return tmpDir;
27
+ }
28
+ store.setState({ dataDir: DEFAULT_DATA_DIR });
29
+ return DEFAULT_DATA_DIR;
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) {
37
+ const resolvedDir = path.resolve(dir);
38
+ if (!fs.existsSync(resolvedDir)) {
39
+ fs.mkdirSync(resolvedDir, { recursive: true });
40
+ }
41
+ store.setState({ dataDir: resolvedDir });
42
+ }
43
+ /**
44
+ * Get the projects directory path (for /create-new-project command).
45
+ * Returns the custom --projects-dir if set, otherwise <dataDir>/projects.
46
+ */
47
+ export function getProjectsDir() {
48
+ const custom = store.getState().projectsDir;
49
+ if (custom) {
50
+ return custom;
51
+ }
52
+ return path.join(getDataDir(), 'projects');
53
+ }
54
+ /**
55
+ * Set a custom projects directory path (from --projects-dir CLI flag).
56
+ * Creates the directory if it doesn't exist.
57
+ */
58
+ export function setProjectsDir(dir) {
59
+ const resolvedDir = path.resolve(dir);
60
+ if (!fs.existsSync(resolvedDir)) {
61
+ fs.mkdirSync(resolvedDir, { recursive: true });
62
+ }
63
+ store.setState({ projectsDir: resolvedDir });
64
+ }
65
+ const DEFAULT_LOCK_PORT = 29988;
66
+ /**
67
+ * Derive a lock port from the data directory path.
68
+ * If KIMAKI_LOCK_PORT is set to a valid TCP port, it takes precedence.
69
+ * Returns 29988 for the default ~/.kimaki directory (backwards compatible).
70
+ * For custom data dirs, uses a hash to generate a port in the range 30000-39999.
71
+ */
72
+ export function getLockPort() {
73
+ const envPortRaw = process.env['KIMAKI_LOCK_PORT'];
74
+ if (envPortRaw) {
75
+ const envPort = Number.parseInt(envPortRaw, 10);
76
+ if (Number.isInteger(envPort) && envPort >= 1 && envPort <= 65535) {
77
+ return envPort;
78
+ }
79
+ }
80
+ const dir = getDataDir();
81
+ // Use original port for default data dir (backwards compatible)
82
+ if (dir === DEFAULT_DATA_DIR) {
83
+ return DEFAULT_LOCK_PORT;
84
+ }
85
+ // Hash-based port for custom data dirs
86
+ let hash = 0;
87
+ for (let i = 0; i < dir.length; i++) {
88
+ const char = dir.charCodeAt(i);
89
+ hash = (hash << 5) - hash + char;
90
+ hash = hash & hash; // Convert to 32bit integer
91
+ }
92
+ // Map to port range 30000-39999
93
+ return 30000 + (Math.abs(hash) % 10000);
94
+ }