@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,552 @@
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
+
5
+ import {
6
+ ButtonInteraction,
7
+ ChatInputCommandInteraction,
8
+ ChannelType,
9
+ ComponentType,
10
+ MessageFlags,
11
+ type TextChannel,
12
+ type ThreadChannel,
13
+ type APIMessageTopLevelComponent,
14
+ type APITextDisplayComponent,
15
+ type InteractionEditReplyOptions,
16
+ } from 'discord.js'
17
+ import {
18
+ deleteThreadWorktree,
19
+ getThreadWorktree,
20
+ type ThreadWorktree,
21
+ } from '../database.js'
22
+ import { getPrisma } from '../db.js'
23
+ import { splitTablesFromMarkdown } from '../format-tables.js'
24
+ import {
25
+ buildHtmlActionCustomId,
26
+ cancelHtmlActionsForOwner,
27
+ registerHtmlAction,
28
+ } from '../html-actions.js'
29
+ import * as errore from 'errore'
30
+ import { GitCommandError } from '../errors.js'
31
+ import { resolveWorkingDirectory } from '../discord-utils.js'
32
+ import { deleteWorktree, git, getDefaultBranch } from '../worktrees.js'
33
+
34
+ // Extracts the git stderr from a deleteWorktree error via errore.findCause.
35
+ // Chain: Error { cause: GitCommandError { cause: CommandError { stderr } } }.
36
+ export function extractGitStderr(error: Error): string | undefined {
37
+ const gitErr = errore.findCause(error, GitCommandError)
38
+ const stderr = (gitErr?.cause as { stderr?: string } | undefined)?.stderr?.trim()
39
+ if (stderr && stderr.length > 0) {
40
+ return stderr
41
+ }
42
+ return undefined
43
+ }
44
+
45
+ export function formatTimeAgo(date: Date): string {
46
+ const diffMs = Date.now() - date.getTime()
47
+ if (diffMs < 0) {
48
+ return 'just now'
49
+ }
50
+ const totalSeconds = Math.floor(diffMs / 1000)
51
+ if (totalSeconds < 60) {
52
+ return `${totalSeconds}s ago`
53
+ }
54
+ const totalMinutes = Math.floor(totalSeconds / 60)
55
+ if (totalMinutes < 60) {
56
+ return `${totalMinutes}m ago`
57
+ }
58
+ const hours = Math.floor(totalMinutes / 60)
59
+ const minutes = totalMinutes % 60
60
+ if (hours < 24) {
61
+ return minutes > 0 ? `${hours}h ${minutes}m ago` : `${hours}h ago`
62
+ }
63
+ const days = Math.floor(hours / 24)
64
+ const remainingHours = hours % 24
65
+ return remainingHours > 0 ? `${days}d ${remainingHours}h ago` : `${days}d ago`
66
+ }
67
+
68
+ function statusLabel(wt: ThreadWorktree): string {
69
+ if (wt.status === 'ready') {
70
+ return 'ready'
71
+ }
72
+ if (wt.status === 'error') {
73
+ return 'error'
74
+ }
75
+ return 'pending'
76
+ }
77
+
78
+ type WorktreeGitStatus = {
79
+ dirty: boolean
80
+ aheadCount: number
81
+ }
82
+
83
+ type WorktreesReplyTarget = {
84
+ guildId: string
85
+ userId: string
86
+ channelId: string
87
+ projectDirectory: string
88
+ notice?: string
89
+ editReply: (
90
+ options: string | InteractionEditReplyOptions,
91
+ ) => Promise<unknown>
92
+ }
93
+
94
+ // 5s timeout per git call — prevents hangs from deleted dirs, git locks, slow disks.
95
+ // Returns null on timeout/error so the table shows "unknown" for that worktree.
96
+ const GIT_CMD_TIMEOUT = 5_000
97
+ const GLOBAL_TIMEOUT = 10_000
98
+
99
+ // Checks dirty state and commits ahead of default branch in parallel.
100
+ // Returns null for worktrees that aren't ready or when the directory is
101
+ // missing / git commands fail / timeout (e.g. deleted worktree folder).
102
+ async function getWorktreeGitStatus({
103
+ wt,
104
+ defaultBranch,
105
+ }: {
106
+ wt: ThreadWorktree
107
+ defaultBranch: string
108
+ }): Promise<WorktreeGitStatus | null> {
109
+ if (wt.status !== 'ready' || !wt.worktree_directory) {
110
+ return null
111
+ }
112
+ try {
113
+ const dir = wt.worktree_directory
114
+ // Use raw git calls so errors/timeouts are visible — isDirty() swallows
115
+ // errors and returns false, which would render "merged" instead of "unknown".
116
+ const [statusResult, aheadResult] = await Promise.all([
117
+ git(dir, 'status --porcelain', { timeout: GIT_CMD_TIMEOUT }),
118
+ git(dir, `rev-list --count "${defaultBranch}..HEAD"`, {
119
+ timeout: GIT_CMD_TIMEOUT,
120
+ }),
121
+ ])
122
+ if (statusResult instanceof Error || aheadResult instanceof Error) {
123
+ return null
124
+ }
125
+ const aheadCount = parseInt(aheadResult, 10)
126
+ if (!Number.isFinite(aheadCount)) {
127
+ return null
128
+ }
129
+ return { dirty: statusResult.length > 0, aheadCount }
130
+ } catch {
131
+ return null
132
+ }
133
+ }
134
+
135
+ function buildWorktreeTable({
136
+ worktrees,
137
+ gitStatuses,
138
+ guildId,
139
+ }: {
140
+ worktrees: ThreadWorktree[]
141
+ gitStatuses: (WorktreeGitStatus | null)[]
142
+ guildId: string
143
+ }): string {
144
+ const header = '| Thread | Name | Status | Created | Folder | Action |'
145
+ const separator = '|---|---|---|---|---|---|'
146
+ const rows = worktrees.map((wt, i) => {
147
+ const threadLink = `[thread](https://discord.com/channels/${guildId}/${wt.thread_id})`
148
+ const name = wt.worktree_name
149
+ const gs = gitStatuses[i] ?? null
150
+ const status = (() => {
151
+ if (wt.status !== 'ready') {
152
+ return statusLabel(wt)
153
+ }
154
+ if (!gs) {
155
+ return 'unknown'
156
+ }
157
+ const parts: string[] = []
158
+ if (gs.dirty) {
159
+ parts.push('dirty')
160
+ }
161
+ if (gs.aheadCount > 0) {
162
+ parts.push(`${gs.aheadCount} ahead`)
163
+ } else {
164
+ parts.push('merged')
165
+ }
166
+ return parts.join(', ')
167
+ })()
168
+ const created = wt.created_at ? formatTimeAgo(wt.created_at) : 'unknown'
169
+ const folder = wt.worktree_directory ?? wt.project_directory
170
+ const action = buildActionCell({ wt, gitStatus: gs })
171
+ return `| ${threadLink} | ${name} | ${status} | ${created} | ${folder} | ${action} |`
172
+ })
173
+ return [header, separator, ...rows].join('\n')
174
+ }
175
+
176
+ function buildActionCell({
177
+ wt,
178
+ gitStatus,
179
+ }: {
180
+ wt: ThreadWorktree
181
+ gitStatus: WorktreeGitStatus | null
182
+ }): string {
183
+ if (!canDeleteWorktree({ wt, gitStatus })) {
184
+ return '-'
185
+ }
186
+
187
+ return buildDeleteButtonHtml({
188
+ buttonId: `delete-worktree-${wt.thread_id}`,
189
+ })
190
+ }
191
+
192
+ function buildDeleteButtonHtml({
193
+ buttonId,
194
+ }: {
195
+ buttonId: string
196
+ }): string {
197
+ return `<button id="${buttonId}" variant="secondary">Delete</button>`
198
+ }
199
+
200
+ function canDeleteWorktree({
201
+ wt,
202
+ gitStatus,
203
+ }: {
204
+ wt: ThreadWorktree
205
+ gitStatus: WorktreeGitStatus | null
206
+ }): boolean {
207
+ if (wt.status !== 'ready' || !wt.worktree_directory) {
208
+ return false
209
+ }
210
+ if (!gitStatus) {
211
+ return false
212
+ }
213
+ if (gitStatus.dirty) {
214
+ return false
215
+ }
216
+ return gitStatus.aheadCount === 0
217
+ }
218
+
219
+ // Resolves git statuses for all worktrees within a single global deadline.
220
+ // Caches getDefaultBranch per project_directory to avoid redundant spawns.
221
+ // Returns null for any worktree whose git calls fail, timeout, or exceed
222
+ // the global deadline — the table renders those as "unknown".
223
+ async function resolveGitStatuses({
224
+ worktrees,
225
+ timeout,
226
+ }: {
227
+ worktrees: ThreadWorktree[]
228
+ timeout: number
229
+ }): Promise<(WorktreeGitStatus | null)[]> {
230
+ const nullFallback = worktrees.map(() => null)
231
+
232
+ let timer: ReturnType<typeof setTimeout> | undefined
233
+ const deadline = new Promise<(WorktreeGitStatus | null)[]>((resolve) => {
234
+ timer = setTimeout(() => {
235
+ resolve(nullFallback)
236
+ }, timeout)
237
+ })
238
+
239
+ const work = (async () => {
240
+ // Resolve default branch once per unique project directory (avoids
241
+ // redundant git subprocess spawns when multiple worktrees share a project).
242
+ const uniqueProjectDirs = [
243
+ ...new Set(worktrees.map((wt) => wt.project_directory)),
244
+ ]
245
+ const defaultBranchEntries = await Promise.all(
246
+ uniqueProjectDirs.map(async (dir) => {
247
+ const branch = await getDefaultBranch(dir, { timeout: GIT_CMD_TIMEOUT })
248
+ return [dir, branch] as const
249
+ }),
250
+ )
251
+ const defaultBranchByProject = new Map(defaultBranchEntries)
252
+
253
+ return Promise.all(
254
+ worktrees.map((wt) => {
255
+ const defaultBranch =
256
+ defaultBranchByProject.get(wt.project_directory) ?? 'main'
257
+ return getWorktreeGitStatus({ wt, defaultBranch })
258
+ }),
259
+ )
260
+ })()
261
+
262
+ try {
263
+ return await Promise.race([work, deadline])
264
+ } finally {
265
+ clearTimeout(timer)
266
+ }
267
+ }
268
+
269
+ async function getRecentWorktrees({
270
+ projectDirectory,
271
+ }: {
272
+ projectDirectory: string
273
+ }): Promise<ThreadWorktree[]> {
274
+ const prisma = await getPrisma()
275
+ return await prisma.thread_worktrees.findMany({
276
+ where: {
277
+ project_directory: projectDirectory,
278
+ },
279
+ orderBy: { created_at: 'desc' },
280
+ take: 10,
281
+ })
282
+ }
283
+
284
+ function getWorktreesActionOwnerKey({
285
+ userId,
286
+ channelId,
287
+ }: {
288
+ userId: string
289
+ channelId: string
290
+ }): string {
291
+ return `worktrees:${userId}:${channelId}`
292
+ }
293
+
294
+ function isProjectChannel(
295
+ channel: ChatInputCommandInteraction['channel'] | ButtonInteraction['channel'],
296
+ ): boolean {
297
+ if (!channel) {
298
+ return false
299
+ }
300
+
301
+ return [
302
+ ChannelType.GuildText,
303
+ ChannelType.PublicThread,
304
+ ChannelType.PrivateThread,
305
+ ChannelType.AnnouncementThread,
306
+ ].includes(channel.type)
307
+ }
308
+
309
+ async function renderWorktreesReply({
310
+ guildId,
311
+ userId,
312
+ channelId,
313
+ projectDirectory,
314
+ notice,
315
+ editReply,
316
+ }: WorktreesReplyTarget): Promise<void> {
317
+ const ownerKey = getWorktreesActionOwnerKey({ userId, channelId })
318
+ cancelHtmlActionsForOwner(ownerKey)
319
+
320
+ const worktrees = await getRecentWorktrees({ projectDirectory })
321
+ if (worktrees.length === 0) {
322
+ const message = notice ? `${notice}\n\nNo worktrees found.` : 'No worktrees found.'
323
+ const textDisplay: APITextDisplayComponent = {
324
+ type: ComponentType.TextDisplay,
325
+ content: message,
326
+ }
327
+ await editReply({
328
+ components: [textDisplay],
329
+ flags: MessageFlags.IsComponentsV2,
330
+ })
331
+ return
332
+ }
333
+
334
+ const gitStatuses = await resolveGitStatuses({
335
+ worktrees,
336
+ timeout: GLOBAL_TIMEOUT,
337
+ })
338
+ const deletableWorktreesByButtonId = new Map<string, ThreadWorktree>()
339
+ worktrees.forEach((wt, index) => {
340
+ const gitStatus = gitStatuses[index] ?? null
341
+ if (!canDeleteWorktree({ wt, gitStatus })) {
342
+ return
343
+ }
344
+ deletableWorktreesByButtonId.set(`delete-worktree-${wt.thread_id}`, wt)
345
+ })
346
+
347
+ const tableMarkdown = buildWorktreeTable({
348
+ worktrees,
349
+ gitStatuses,
350
+ guildId,
351
+ })
352
+ const markdown = notice ? `${notice}\n\n${tableMarkdown}` : tableMarkdown
353
+ const segments = splitTablesFromMarkdown(markdown, {
354
+ resolveButtonCustomId: ({ button }) => {
355
+ const worktree = deletableWorktreesByButtonId.get(button.id)
356
+ if (!worktree) {
357
+ return new Error(`No worktree registered for button ${button.id}`)
358
+ }
359
+
360
+ const actionId = registerHtmlAction({
361
+ ownerKey,
362
+ threadId: worktree.thread_id,
363
+ run: async ({ interaction }) => {
364
+ await handleDeleteWorktreeAction({
365
+ interaction,
366
+ threadId: worktree.thread_id,
367
+ })
368
+ },
369
+ })
370
+ return buildHtmlActionCustomId(actionId)
371
+ },
372
+ })
373
+
374
+ const components: APIMessageTopLevelComponent[] = segments.flatMap((segment) => {
375
+ if (segment.type === 'components') {
376
+ return segment.components
377
+ }
378
+
379
+ const textDisplay: APITextDisplayComponent = {
380
+ type: ComponentType.TextDisplay,
381
+ content: segment.text,
382
+ }
383
+ return [textDisplay]
384
+ })
385
+
386
+ await editReply({
387
+ components,
388
+ flags: MessageFlags.IsComponentsV2,
389
+ })
390
+ }
391
+
392
+ async function handleDeleteWorktreeAction({
393
+ interaction,
394
+ threadId,
395
+ }: {
396
+ interaction: ButtonInteraction
397
+ threadId: string
398
+ }): Promise<void> {
399
+ const guildId = interaction.guildId
400
+ if (!guildId) {
401
+ await interaction.editReply({
402
+ components: [
403
+ {
404
+ type: ComponentType.TextDisplay,
405
+ content: 'This action can only be used in a server.',
406
+ },
407
+ ],
408
+ flags: MessageFlags.IsComponentsV2,
409
+ })
410
+ return
411
+ }
412
+
413
+ const worktree = await getThreadWorktree(threadId)
414
+ if (!worktree) {
415
+ if (!isProjectChannel(interaction.channel)) {
416
+ await interaction.editReply({
417
+ components: [
418
+ {
419
+ type: ComponentType.TextDisplay,
420
+ content: 'This action can only be used in a project channel or thread.',
421
+ },
422
+ ],
423
+ flags: MessageFlags.IsComponentsV2,
424
+ })
425
+ return
426
+ }
427
+
428
+ const resolved = await resolveWorkingDirectory({
429
+ channel: interaction.channel as TextChannel | ThreadChannel,
430
+ })
431
+ if (!resolved) {
432
+ await interaction.editReply({
433
+ components: [
434
+ {
435
+ type: ComponentType.TextDisplay,
436
+ content: 'Could not determine the project folder for this channel.',
437
+ },
438
+ ],
439
+ flags: MessageFlags.IsComponentsV2,
440
+ })
441
+ return
442
+ }
443
+
444
+ await renderWorktreesReply({
445
+ guildId,
446
+ userId: interaction.user.id,
447
+ channelId: interaction.channelId,
448
+ projectDirectory: resolved.projectDirectory,
449
+ notice: 'Worktree was already removed.',
450
+ editReply: (options) => {
451
+ return interaction.editReply(options)
452
+ },
453
+ })
454
+ return
455
+ }
456
+
457
+ if (worktree.status !== 'ready' || !worktree.worktree_directory) {
458
+ await renderWorktreesReply({
459
+ guildId,
460
+ userId: interaction.user.id,
461
+ channelId: interaction.channelId,
462
+ projectDirectory: worktree.project_directory,
463
+ notice: `Cannot delete \`${worktree.worktree_name}\` because it is ${worktree.status}.`,
464
+ editReply: (options) => {
465
+ return interaction.editReply(options)
466
+ },
467
+ })
468
+ return
469
+ }
470
+
471
+ const deleteResult = await deleteWorktree({
472
+ projectDirectory: worktree.project_directory,
473
+ worktreeDirectory: worktree.worktree_directory,
474
+ worktreeName: worktree.worktree_name,
475
+ })
476
+ if (deleteResult instanceof Error) {
477
+ // Send error as a separate ephemeral follow-up so the table stays intact.
478
+ // Dig into cause chain to surface the actual git stderr when available.
479
+ const gitStderr = extractGitStderr(deleteResult)
480
+ const detail = gitStderr
481
+ ? `\`\`\`\n${gitStderr}\n\`\`\``
482
+ : deleteResult.message
483
+ await interaction
484
+ .followUp({
485
+ content: `Failed to delete \`${worktree.worktree_name}\`\n${detail}`,
486
+ flags: MessageFlags.Ephemeral,
487
+ })
488
+ .catch(() => {
489
+ return undefined
490
+ })
491
+ return
492
+ }
493
+
494
+ await deleteThreadWorktree(threadId)
495
+ await renderWorktreesReply({
496
+ guildId,
497
+ userId: interaction.user.id,
498
+ channelId: interaction.channelId,
499
+ projectDirectory: worktree.project_directory,
500
+ notice: `Deleted \`${worktree.worktree_name}\`.`,
501
+ editReply: (options) => {
502
+ return interaction.editReply(options)
503
+ },
504
+ })
505
+ }
506
+
507
+ export async function handleWorktreesCommand({
508
+ command,
509
+ }: {
510
+ command: ChatInputCommandInteraction
511
+ appId: string
512
+ }): Promise<void> {
513
+ const channel = command.channel
514
+ const guildId = command.guildId
515
+ if (!guildId || !channel) {
516
+ await command.reply({
517
+ content: 'This command can only be used in a server channel.',
518
+ flags: MessageFlags.Ephemeral,
519
+ })
520
+ return
521
+ }
522
+
523
+ if (!isProjectChannel(channel)) {
524
+ await command.reply({
525
+ content: 'This command can only be used in a project channel or thread.',
526
+ flags: MessageFlags.Ephemeral,
527
+ })
528
+ return
529
+ }
530
+
531
+ const resolved = await resolveWorkingDirectory({
532
+ channel: channel as TextChannel | ThreadChannel,
533
+ })
534
+ if (!resolved) {
535
+ await command.reply({
536
+ content: 'Could not determine the project folder for this channel.',
537
+ flags: MessageFlags.Ephemeral,
538
+ })
539
+ return
540
+ }
541
+
542
+ await command.deferReply({ flags: MessageFlags.Ephemeral })
543
+ await renderWorktreesReply({
544
+ guildId,
545
+ userId: command.user.id,
546
+ channelId: command.channelId,
547
+ projectDirectory: resolved.projectDirectory,
548
+ editReply: (options) => {
549
+ return command.editReply(options)
550
+ },
551
+ })
552
+ }
@@ -0,0 +1,36 @@
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
+
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,111 @@
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
+
7
+ import fs from 'node:fs'
8
+ import os from 'node:os'
9
+ import path from 'node:path'
10
+ import { store } from './store.js'
11
+
12
+ const DEFAULT_DATA_DIR = path.join(os.homedir(), '.kimaki')
13
+
14
+ /**
15
+ * Get the data directory path.
16
+ * Falls back to ~/.kimaki if not explicitly set.
17
+ * Under vitest (KIMAKI_VITEST env var), auto-creates an isolated temp dir so
18
+ * tests never touch the real ~/.kimaki/ database. Tests that need a specific
19
+ * dir can still call setDataDir() before any DB access to override this.
20
+ */
21
+ export function getDataDir(): string {
22
+ const current = store.getState().dataDir
23
+ if (current) {
24
+ return current
25
+ }
26
+ if (process.env.KIMAKI_VITEST) {
27
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kimaki-test-'))
28
+ store.setState({ dataDir: tmpDir })
29
+ return tmpDir
30
+ }
31
+ store.setState({ dataDir: DEFAULT_DATA_DIR })
32
+ return DEFAULT_DATA_DIR
33
+ }
34
+
35
+ /**
36
+ * Set the data directory path.
37
+ * Creates the directory if it doesn't exist.
38
+ * Must be called before any database or path-dependent operations.
39
+ */
40
+ export function setDataDir(dir: string): void {
41
+ const resolvedDir = path.resolve(dir)
42
+
43
+ if (!fs.existsSync(resolvedDir)) {
44
+ fs.mkdirSync(resolvedDir, { recursive: true })
45
+ }
46
+
47
+ store.setState({ dataDir: resolvedDir })
48
+ }
49
+
50
+ /**
51
+ * Get the projects directory path (for /create-new-project command).
52
+ * Returns the custom --projects-dir if set, otherwise <dataDir>/projects.
53
+ */
54
+ export function getProjectsDir(): string {
55
+ const custom = store.getState().projectsDir
56
+ if (custom) {
57
+ return custom
58
+ }
59
+ return path.join(getDataDir(), 'projects')
60
+ }
61
+
62
+ /**
63
+ * Set a custom projects directory path (from --projects-dir CLI flag).
64
+ * Creates the directory if it doesn't exist.
65
+ */
66
+ export function setProjectsDir(dir: string): void {
67
+ const resolvedDir = path.resolve(dir)
68
+
69
+ if (!fs.existsSync(resolvedDir)) {
70
+ fs.mkdirSync(resolvedDir, { recursive: true })
71
+ }
72
+
73
+ store.setState({ projectsDir: resolvedDir })
74
+ }
75
+
76
+ export type { RegisteredUserCommand } from './store.js'
77
+
78
+ const DEFAULT_LOCK_PORT = 29988
79
+
80
+ /**
81
+ * Derive a lock port from the data directory path.
82
+ * If KIMAKI_LOCK_PORT is set to a valid TCP port, it takes precedence.
83
+ * Returns 29988 for the default ~/.kimaki directory (backwards compatible).
84
+ * For custom data dirs, uses a hash to generate a port in the range 30000-39999.
85
+ */
86
+ export function getLockPort(): number {
87
+ const envPortRaw = process.env['KIMAKI_LOCK_PORT']
88
+ if (envPortRaw) {
89
+ const envPort = Number.parseInt(envPortRaw, 10)
90
+ if (Number.isInteger(envPort) && envPort >= 1 && envPort <= 65535) {
91
+ return envPort
92
+ }
93
+ }
94
+
95
+ const dir = getDataDir()
96
+
97
+ // Use original port for default data dir (backwards compatible)
98
+ if (dir === DEFAULT_DATA_DIR) {
99
+ return DEFAULT_LOCK_PORT
100
+ }
101
+
102
+ // Hash-based port for custom data dirs
103
+ let hash = 0
104
+ for (let i = 0; i < dir.length; i++) {
105
+ const char = dir.charCodeAt(i)
106
+ hash = (hash << 5) - hash + char
107
+ hash = hash & hash // Convert to 32bit integer
108
+ }
109
+ // Map to port range 30000-39999
110
+ return 30000 + (Math.abs(hash) % 10000)
111
+ }