@otto-assistant/otto 0.1.2 → 0.7.15

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 (638) 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-account-identity.js +62 -0
  7. package/dist/anthropic-account-identity.test.js +38 -0
  8. package/dist/anthropic-auth-plugin.js +917 -0
  9. package/dist/anthropic-auth-state.js +303 -0
  10. package/dist/anthropic-auth-state.test.js +150 -0
  11. package/dist/bin.js +152 -0
  12. package/dist/btw-prefix-detection.js +17 -0
  13. package/dist/btw-prefix-detection.test.js +63 -0
  14. package/dist/channel-management.js +259 -0
  15. package/dist/cli-parsing.test.js +142 -0
  16. package/dist/cli-send-thread.e2e.test.js +353 -0
  17. package/dist/cli-telegram-options.test.js +99 -0
  18. package/dist/cli.js +4210 -568
  19. package/dist/commands/abort.js +65 -0
  20. package/dist/commands/action-buttons.js +245 -0
  21. package/dist/commands/add-dir.js +124 -0
  22. package/dist/commands/add-dir.test.js +126 -0
  23. package/dist/commands/add-project.js +113 -0
  24. package/dist/commands/agent.js +355 -0
  25. package/dist/commands/ask-question.js +320 -0
  26. package/dist/commands/ask-question.test.js +92 -0
  27. package/dist/commands/btw.js +121 -0
  28. package/dist/commands/cli-commands-group-a.test.js +728 -0
  29. package/dist/commands/cli-commands-group-b.test.js +695 -0
  30. package/dist/commands/compact.js +120 -0
  31. package/dist/commands/context-usage.js +140 -0
  32. package/dist/commands/create-new-project.js +130 -0
  33. package/dist/commands/diff.js +63 -0
  34. package/dist/commands/discord-commands-group-a.test.js +621 -0
  35. package/dist/commands/discord-commands-group-b.test.js +595 -0
  36. package/dist/commands/discord-commands-group-c.test.js +739 -0
  37. package/dist/commands/file-upload.js +275 -0
  38. package/dist/commands/fork-subagent.js +177 -0
  39. package/dist/commands/fork.js +262 -0
  40. package/dist/commands/gemini-apikey.js +70 -0
  41. package/dist/commands/login.js +887 -0
  42. package/dist/commands/mcp.js +239 -0
  43. package/dist/commands/memory-snapshot.js +24 -0
  44. package/dist/commands/mention-mode.js +44 -0
  45. package/dist/commands/merge-worktree.js +162 -0
  46. package/dist/commands/model-variant.js +366 -0
  47. package/dist/commands/model.js +794 -0
  48. package/dist/commands/new-worktree.js +465 -0
  49. package/dist/commands/paginated-select.js +57 -0
  50. package/dist/commands/permissions.js +274 -0
  51. package/dist/commands/queue.js +223 -0
  52. package/dist/commands/remove-project.js +115 -0
  53. package/dist/commands/restart-opencode-server.js +127 -0
  54. package/dist/commands/resume.js +149 -0
  55. package/dist/commands/run-command.js +79 -0
  56. package/dist/commands/screenshare.js +303 -0
  57. package/dist/commands/screenshare.test.js +20 -0
  58. package/dist/commands/session-id.js +78 -0
  59. package/dist/commands/session.js +176 -0
  60. package/dist/commands/share.js +80 -0
  61. package/dist/commands/tasks.js +205 -0
  62. package/dist/commands/thread-deletion-sync.js +50 -0
  63. package/dist/commands/types.js +2 -0
  64. package/dist/commands/undo-redo.js +305 -0
  65. package/dist/commands/unset-model.js +139 -0
  66. package/dist/commands/upgrade.js +48 -0
  67. package/dist/commands/user-command.js +155 -0
  68. package/dist/commands/verbosity.js +125 -0
  69. package/dist/commands/vscode.js +269 -0
  70. package/dist/commands/worktree-settings.js +43 -0
  71. package/dist/commands/worktrees.js +468 -0
  72. package/dist/condense-memory.js +33 -0
  73. package/dist/config.js +100 -255
  74. package/dist/context-awareness-plugin.js +340 -0
  75. package/dist/context-awareness-plugin.test.js +126 -0
  76. package/dist/critique-utils.js +95 -0
  77. package/dist/database.js +1355 -0
  78. package/dist/db.js +260 -0
  79. package/dist/db.test.js +138 -0
  80. package/dist/debounce-timeout.js +28 -0
  81. package/dist/debounced-process-flush.js +77 -0
  82. package/dist/discord-bot.js +1124 -0
  83. package/dist/discord-command-registration.js +567 -0
  84. package/dist/discord-urls.js +82 -0
  85. package/dist/discord-utils.js +616 -0
  86. package/dist/discord-utils.test.js +134 -0
  87. package/dist/errors.js +157 -0
  88. package/dist/escape-backticks.test.js +429 -0
  89. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  90. package/dist/eventsource-parser.test.js +327 -0
  91. package/dist/exec-async.js +26 -0
  92. package/dist/external-opencode-sync.js +480 -0
  93. package/dist/format-tables.js +491 -0
  94. package/dist/format-tables.test.js +478 -0
  95. package/dist/forum-sync/config.js +79 -0
  96. package/dist/forum-sync/discord-operations.js +154 -0
  97. package/dist/forum-sync/index.js +5 -0
  98. package/dist/forum-sync/markdown.js +113 -0
  99. package/dist/forum-sync/sync-to-discord.js +417 -0
  100. package/dist/forum-sync/sync-to-files.js +190 -0
  101. package/dist/forum-sync/types.js +53 -0
  102. package/dist/forum-sync/watchers.js +307 -0
  103. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  104. package/dist/gateway-proxy.e2e.test.js +485 -0
  105. package/dist/genai-worker-wrapper.js +111 -0
  106. package/dist/genai-worker.js +311 -0
  107. package/dist/genai.js +232 -0
  108. package/dist/generated/browser.js +17 -0
  109. package/dist/generated/client.js +37 -0
  110. package/dist/generated/commonInputTypes.js +10 -0
  111. package/dist/generated/enums.js +58 -0
  112. package/dist/generated/internal/class.js +49 -0
  113. package/dist/generated/internal/prismaNamespace.js +254 -0
  114. package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
  115. package/dist/generated/models/bot_api_keys.js +1 -0
  116. package/dist/generated/models/bot_tokens.js +1 -0
  117. package/dist/generated/models/channel_agents.js +1 -0
  118. package/dist/generated/models/channel_directories.js +1 -0
  119. package/dist/generated/models/channel_mention_mode.js +1 -0
  120. package/dist/generated/models/channel_models.js +1 -0
  121. package/dist/generated/models/channel_verbosity.js +1 -0
  122. package/dist/generated/models/channel_worktrees.js +1 -0
  123. package/dist/generated/models/forum_sync_configs.js +1 -0
  124. package/dist/generated/models/global_models.js +1 -0
  125. package/dist/generated/models/ipc_requests.js +1 -0
  126. package/dist/generated/models/part_messages.js +1 -0
  127. package/dist/generated/models/scheduled_tasks.js +1 -0
  128. package/dist/generated/models/session_agents.js +1 -0
  129. package/dist/generated/models/session_events.js +1 -0
  130. package/dist/generated/models/session_models.js +1 -0
  131. package/dist/generated/models/session_start_sources.js +1 -0
  132. package/dist/generated/models/thread_sessions.js +1 -0
  133. package/dist/generated/models/thread_worktrees.js +1 -0
  134. package/dist/generated/models.js +1 -0
  135. package/dist/heap-monitor.js +122 -0
  136. package/dist/hrana-server.js +251 -0
  137. package/dist/hrana-server.test.js +370 -0
  138. package/dist/html-actions.js +123 -0
  139. package/dist/html-actions.test.js +70 -0
  140. package/dist/html-components.js +117 -0
  141. package/dist/html-components.test.js +34 -0
  142. package/dist/image-optimizer-plugin.js +153 -0
  143. package/dist/image-utils.js +112 -0
  144. package/dist/interaction-handler.js +420 -0
  145. package/dist/ipc-polling.js +327 -0
  146. package/dist/ipc-tools-plugin.js +193 -0
  147. package/dist/ipc-utils.js +18 -0
  148. package/dist/limit-heading-depth.js +25 -0
  149. package/dist/limit-heading-depth.test.js +105 -0
  150. package/dist/logger.js +171 -0
  151. package/dist/markdown.js +342 -0
  152. package/dist/markdown.test.js +264 -0
  153. package/dist/memory-overview-plugin.js +128 -0
  154. package/dist/message-finish-field.e2e.test.js +168 -0
  155. package/dist/message-formatting.js +415 -0
  156. package/dist/message-formatting.test.js +115 -0
  157. package/dist/message-preprocessing.js +359 -0
  158. package/dist/onboarding-tutorial.js +163 -0
  159. package/dist/onboarding-welcome.js +37 -0
  160. package/dist/openai-realtime.js +224 -0
  161. package/dist/opencode-command-detection.js +65 -0
  162. package/dist/opencode-command-detection.test.js +240 -0
  163. package/dist/opencode-command.js +131 -0
  164. package/dist/opencode-command.test.js +48 -0
  165. package/dist/opencode-interrupt-plugin.js +388 -0
  166. package/dist/opencode-interrupt-plugin.test.js +463 -0
  167. package/dist/opencode.js +1117 -0
  168. package/dist/otto/branding.js +22 -0
  169. package/dist/otto/index.js +21 -0
  170. package/dist/otto-digital-twin.e2e.test.js +161 -0
  171. package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
  172. package/dist/otto-opencode-plugin.js +21 -0
  173. package/dist/otto-opencode-plugin.test.js +98 -0
  174. package/dist/parse-permission-rules.test.js +117 -0
  175. package/dist/patch-text-parser.js +97 -0
  176. package/dist/plugin-logger.js +68 -0
  177. package/dist/privacy-sanitizer.js +105 -0
  178. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  179. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  180. package/dist/queue-advanced-e2e-setup.js +790 -0
  181. package/dist/queue-advanced-footer.e2e.test.js +481 -0
  182. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  183. package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
  184. package/dist/queue-advanced-question.e2e.test.js +261 -0
  185. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  186. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  187. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  188. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  189. package/dist/queue-question-select-drain.e2e.test.js +256 -0
  190. package/dist/runtime-idle-sweeper.js +52 -0
  191. package/dist/runtime-lifecycle.e2e.test.js +514 -0
  192. package/dist/sentry.js +23 -0
  193. package/dist/session-handler/agent-utils.js +67 -0
  194. package/dist/session-handler/event-stream-state.js +475 -0
  195. package/dist/session-handler/event-stream-state.test.js +632 -0
  196. package/dist/session-handler/model-utils.js +147 -0
  197. package/dist/session-handler/opencode-session-event-log.js +94 -0
  198. package/dist/session-handler/thread-runtime-state.js +131 -0
  199. package/dist/session-handler/thread-session-runtime.js +3390 -0
  200. package/dist/session-handler.js +9 -0
  201. package/dist/session-search.js +100 -0
  202. package/dist/session-search.test.js +40 -0
  203. package/dist/session-title-rename.test.js +92 -0
  204. package/dist/skill-filter.js +31 -0
  205. package/dist/skill-filter.test.js +65 -0
  206. package/dist/startup-service.js +153 -0
  207. package/dist/startup-time.e2e.test.js +296 -0
  208. package/dist/store.js +19 -0
  209. package/dist/subagent-rate-limit-plugin.js +175 -0
  210. package/dist/system-message.js +702 -0
  211. package/dist/system-message.test.js +697 -0
  212. package/dist/task-runner.js +530 -0
  213. package/dist/task-schedule.js +213 -0
  214. package/dist/task-schedule.test.js +71 -0
  215. package/dist/test-utils.js +313 -0
  216. package/dist/thinking-utils.js +35 -0
  217. package/dist/thread-message-queue.e2e.test.js +1111 -0
  218. package/dist/tools.js +357 -0
  219. package/dist/undo-redo.e2e.test.js +161 -0
  220. package/dist/unnest-code-blocks.js +146 -0
  221. package/dist/unnest-code-blocks.test.js +673 -0
  222. package/dist/upgrade.js +156 -0
  223. package/dist/utils.js +172 -0
  224. package/dist/utils.test.js +130 -0
  225. package/dist/voice-attachment.js +34 -0
  226. package/dist/voice-handler.js +646 -0
  227. package/dist/voice-message.e2e.test.js +1021 -0
  228. package/dist/voice.js +456 -0
  229. package/dist/voice.test.js +235 -0
  230. package/dist/wait-session.js +171 -0
  231. package/dist/websockify.js +69 -0
  232. package/dist/worker-types.js +4 -0
  233. package/dist/worktree-lifecycle.e2e.test.js +311 -0
  234. package/dist/worktree-utils.js +3 -0
  235. package/dist/worktrees.js +991 -0
  236. package/dist/worktrees.test.js +415 -0
  237. package/dist/xml.js +92 -0
  238. package/dist/xml.test.js +32 -0
  239. package/package.json +90 -38
  240. package/schema.prisma +303 -0
  241. package/skills/batch/SKILL.md +87 -0
  242. package/skills/critique/SKILL.md +112 -0
  243. package/skills/egaki/SKILL.md +100 -0
  244. package/skills/errore/SKILL.md +647 -0
  245. package/skills/event-sourcing-state/SKILL.md +252 -0
  246. package/skills/goke/SKILL.md +38 -0
  247. package/skills/jitter/EDITOR.md +219 -0
  248. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  249. package/skills/jitter/SKILL.md +158 -0
  250. package/skills/jitter/jitter-clipboard.json +1042 -0
  251. package/skills/jitter/package.json +14 -0
  252. package/skills/jitter/tsconfig.json +15 -0
  253. package/skills/jitter/utils/actions.ts +212 -0
  254. package/skills/jitter/utils/export.ts +114 -0
  255. package/skills/jitter/utils/index.ts +141 -0
  256. package/skills/jitter/utils/snapshot.ts +154 -0
  257. package/skills/jitter/utils/traverse.ts +246 -0
  258. package/skills/jitter/utils/types.ts +279 -0
  259. package/skills/jitter/utils/wait.ts +133 -0
  260. package/skills/lintcn/SKILL.md +873 -0
  261. package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
  262. package/skills/new-skill/SKILL.md +237 -0
  263. package/skills/npm-package/SKILL.md +617 -0
  264. package/skills/opensrc/SKILL.md +78 -0
  265. package/skills/otto-publish/SKILL.md +61 -0
  266. package/skills/playwriter/SKILL.md +35 -0
  267. package/skills/profano/SKILL.md +16 -0
  268. package/skills/proxyman/SKILL.md +215 -0
  269. package/skills/security-review/SKILL.md +208 -0
  270. package/skills/sigillo/SKILL.md +101 -0
  271. package/skills/simplify/SKILL.md +58 -0
  272. package/skills/spiceflow/SKILL.md +28 -0
  273. package/skills/termcast/SKILL.md +945 -0
  274. package/skills/tuistory/SKILL.md +98 -0
  275. package/skills/usecomputer/SKILL.md +264 -0
  276. package/skills/x-articles/SKILL.md +554 -0
  277. package/skills/zele/SKILL.md +49 -0
  278. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  279. package/src/agent-model.e2e.test.ts +979 -0
  280. package/src/ai-tool-to-genai.test.ts +296 -0
  281. package/src/ai-tool-to-genai.ts +283 -0
  282. package/src/ai-tool.ts +39 -0
  283. package/src/anthropic-account-identity.test.ts +52 -0
  284. package/src/anthropic-account-identity.ts +77 -0
  285. package/src/anthropic-auth-plugin.ts +1139 -0
  286. package/src/anthropic-auth-state.test.ts +187 -0
  287. package/src/anthropic-auth-state.ts +386 -0
  288. package/src/bin.ts +182 -0
  289. package/src/btw-prefix-detection.test.ts +73 -0
  290. package/src/btw-prefix-detection.ts +23 -0
  291. package/src/channel-management.ts +376 -0
  292. package/src/cli-parsing.test.ts +197 -0
  293. package/src/cli-send-thread.e2e.test.ts +463 -0
  294. package/src/cli-telegram-options.test.ts +114 -0
  295. package/src/cli.ts +5718 -580
  296. package/src/commands/abort.ts +89 -0
  297. package/src/commands/action-buttons.ts +364 -0
  298. package/src/commands/add-dir.test.ts +154 -0
  299. package/src/commands/add-dir.ts +175 -0
  300. package/src/commands/add-project.ts +149 -0
  301. package/src/commands/agent.ts +496 -0
  302. package/src/commands/ask-question.test.ts +111 -0
  303. package/src/commands/ask-question.ts +455 -0
  304. package/src/commands/btw.ts +184 -0
  305. package/src/commands/cli-commands-group-a.test.ts +837 -0
  306. package/src/commands/cli-commands-group-b.test.ts +800 -0
  307. package/src/commands/compact.ts +157 -0
  308. package/src/commands/context-usage.ts +199 -0
  309. package/src/commands/create-new-project.ts +190 -0
  310. package/src/commands/diff.ts +91 -0
  311. package/src/commands/discord-commands-group-a.test.ts +751 -0
  312. package/src/commands/discord-commands-group-b.test.ts +648 -0
  313. package/src/commands/discord-commands-group-c.test.ts +882 -0
  314. package/src/commands/file-upload.ts +389 -0
  315. package/src/commands/fork-subagent.ts +263 -0
  316. package/src/commands/fork.ts +386 -0
  317. package/src/commands/gemini-apikey.ts +104 -0
  318. package/src/commands/login.ts +1175 -0
  319. package/src/commands/mcp.ts +307 -0
  320. package/src/commands/memory-snapshot.ts +30 -0
  321. package/src/commands/mention-mode.ts +68 -0
  322. package/src/commands/merge-worktree.ts +226 -0
  323. package/src/commands/model-variant.ts +485 -0
  324. package/src/commands/model.ts +1078 -0
  325. package/src/commands/new-worktree.ts +645 -0
  326. package/src/commands/paginated-select.ts +81 -0
  327. package/src/commands/permissions.ts +397 -0
  328. package/src/commands/queue.ts +293 -0
  329. package/src/commands/remove-project.ts +155 -0
  330. package/src/commands/restart-opencode-server.ts +162 -0
  331. package/src/commands/resume.ts +230 -0
  332. package/src/commands/run-command.ts +123 -0
  333. package/src/commands/screenshare.test.ts +30 -0
  334. package/src/commands/screenshare.ts +366 -0
  335. package/src/commands/session-id.ts +109 -0
  336. package/src/commands/session.ts +227 -0
  337. package/src/commands/share.ts +106 -0
  338. package/src/commands/tasks.ts +293 -0
  339. package/src/commands/thread-deletion-sync.ts +80 -0
  340. package/src/commands/types.ts +25 -0
  341. package/src/commands/undo-redo.ts +386 -0
  342. package/src/commands/unset-model.ts +174 -0
  343. package/src/commands/upgrade.ts +59 -0
  344. package/src/commands/user-command.ts +198 -0
  345. package/src/commands/verbosity.ts +173 -0
  346. package/src/commands/vscode.ts +342 -0
  347. package/src/commands/worktree-settings.ts +70 -0
  348. package/src/commands/worktrees.ts +645 -0
  349. package/src/condense-memory.ts +36 -0
  350. package/src/config.ts +103 -339
  351. package/src/context-awareness-plugin.test.ts +144 -0
  352. package/src/context-awareness-plugin.ts +469 -0
  353. package/src/critique-utils.ts +139 -0
  354. package/src/database.ts +1949 -0
  355. package/src/db.test.ts +162 -0
  356. package/src/db.ts +295 -0
  357. package/src/debounce-timeout.ts +43 -0
  358. package/src/debounced-process-flush.ts +104 -0
  359. package/src/discord-bot.ts +1505 -0
  360. package/src/discord-command-registration.ts +752 -0
  361. package/src/discord-urls.ts +89 -0
  362. package/src/discord-utils.test.ts +153 -0
  363. package/src/discord-utils.ts +846 -0
  364. package/src/errors.ts +201 -0
  365. package/src/escape-backticks.test.ts +469 -0
  366. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  367. package/src/eventsource-parser.test.ts +351 -0
  368. package/src/exec-async.ts +35 -0
  369. package/src/external-opencode-sync.ts +685 -0
  370. package/src/format-tables.test.ts +515 -0
  371. package/src/format-tables.ts +718 -0
  372. package/src/forum-sync/config.ts +92 -0
  373. package/src/forum-sync/discord-operations.ts +241 -0
  374. package/src/forum-sync/index.ts +9 -0
  375. package/src/forum-sync/markdown.ts +172 -0
  376. package/src/forum-sync/sync-to-discord.ts +595 -0
  377. package/src/forum-sync/sync-to-files.ts +294 -0
  378. package/src/forum-sync/types.ts +175 -0
  379. package/src/forum-sync/watchers.ts +454 -0
  380. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  381. package/src/gateway-proxy.e2e.test.ts +644 -0
  382. package/src/genai-worker-wrapper.ts +164 -0
  383. package/src/genai-worker.ts +386 -0
  384. package/src/genai.ts +321 -0
  385. package/src/generated/browser.ts +114 -0
  386. package/src/generated/client.ts +138 -0
  387. package/src/generated/commonInputTypes.ts +770 -0
  388. package/src/generated/enums.ts +98 -0
  389. package/src/generated/internal/class.ts +384 -0
  390. package/src/generated/internal/prismaNamespace.ts +2394 -0
  391. package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
  392. package/src/generated/models/bot_api_keys.ts +1288 -0
  393. package/src/generated/models/bot_tokens.ts +1700 -0
  394. package/src/generated/models/channel_agents.ts +1256 -0
  395. package/src/generated/models/channel_directories.ts +1859 -0
  396. package/src/generated/models/channel_mention_mode.ts +1300 -0
  397. package/src/generated/models/channel_models.ts +1288 -0
  398. package/src/generated/models/channel_verbosity.ts +1228 -0
  399. package/src/generated/models/channel_worktrees.ts +1300 -0
  400. package/src/generated/models/forum_sync_configs.ts +1452 -0
  401. package/src/generated/models/global_models.ts +1288 -0
  402. package/src/generated/models/ipc_requests.ts +1485 -0
  403. package/src/generated/models/part_messages.ts +1302 -0
  404. package/src/generated/models/scheduled_tasks.ts +2320 -0
  405. package/src/generated/models/session_agents.ts +1086 -0
  406. package/src/generated/models/session_events.ts +1439 -0
  407. package/src/generated/models/session_models.ts +1114 -0
  408. package/src/generated/models/session_start_sources.ts +1408 -0
  409. package/src/generated/models/thread_sessions.ts +1781 -0
  410. package/src/generated/models/thread_worktrees.ts +1356 -0
  411. package/src/generated/models.ts +30 -0
  412. package/src/heap-monitor.ts +152 -0
  413. package/src/hrana-server.test.ts +434 -0
  414. package/src/hrana-server.ts +299 -0
  415. package/src/html-actions.test.ts +87 -0
  416. package/src/html-actions.ts +174 -0
  417. package/src/html-components.test.ts +38 -0
  418. package/src/html-components.ts +181 -0
  419. package/src/image-optimizer-plugin.ts +194 -0
  420. package/src/image-utils.ts +149 -0
  421. package/src/interaction-handler.ts +610 -0
  422. package/src/ipc-polling.ts +427 -0
  423. package/src/ipc-tools-plugin.ts +236 -0
  424. package/src/ipc-utils.ts +29 -0
  425. package/src/limit-heading-depth.test.ts +116 -0
  426. package/src/limit-heading-depth.ts +26 -0
  427. package/src/logger.ts +215 -0
  428. package/src/markdown.test.ts +315 -0
  429. package/src/markdown.ts +410 -0
  430. package/src/memory-overview-plugin.ts +163 -0
  431. package/src/message-finish-field.e2e.test.ts +195 -0
  432. package/src/message-formatting.test.ts +126 -0
  433. package/src/message-formatting.ts +535 -0
  434. package/src/message-preprocessing.ts +488 -0
  435. package/src/onboarding-tutorial.ts +167 -0
  436. package/src/onboarding-welcome.ts +49 -0
  437. package/src/openai-realtime.ts +358 -0
  438. package/src/opencode-command-detection.test.ts +307 -0
  439. package/src/opencode-command-detection.ts +76 -0
  440. package/src/opencode-command.test.ts +70 -0
  441. package/src/opencode-command.ts +191 -0
  442. package/src/opencode-interrupt-plugin.test.ts +682 -0
  443. package/src/opencode-interrupt-plugin.ts +507 -0
  444. package/src/opencode.ts +1453 -0
  445. package/src/otto/branding.ts +23 -0
  446. package/src/otto/index.ts +22 -0
  447. package/src/otto-digital-twin.e2e.test.ts +199 -0
  448. package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
  449. package/src/otto-opencode-plugin.test.ts +108 -0
  450. package/src/otto-opencode-plugin.ts +22 -0
  451. package/src/parse-permission-rules.test.ts +127 -0
  452. package/src/patch-text-parser.ts +107 -0
  453. package/src/plugin-logger.ts +84 -0
  454. package/src/privacy-sanitizer.ts +142 -0
  455. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  456. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  457. package/src/queue-advanced-e2e-setup.ts +877 -0
  458. package/src/queue-advanced-footer.e2e.test.ts +591 -0
  459. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  460. package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
  461. package/src/queue-advanced-question.e2e.test.ts +316 -0
  462. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  463. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  464. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  465. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  466. package/src/queue-question-select-drain.e2e.test.ts +327 -0
  467. package/src/runtime-idle-sweeper.ts +76 -0
  468. package/src/runtime-lifecycle.e2e.test.ts +651 -0
  469. package/src/schema.sql +174 -0
  470. package/src/sentry.ts +26 -0
  471. package/src/session-handler/agent-utils.ts +99 -0
  472. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  473. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  474. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  475. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  476. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  477. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  478. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  479. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  480. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  481. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  482. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  483. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  484. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  485. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  486. package/src/session-handler/event-stream-state.test.ts +717 -0
  487. package/src/session-handler/event-stream-state.ts +706 -0
  488. package/src/session-handler/model-utils.ts +217 -0
  489. package/src/session-handler/opencode-session-event-log.ts +130 -0
  490. package/src/session-handler/thread-runtime-state.ts +247 -0
  491. package/src/session-handler/thread-session-runtime.ts +4440 -0
  492. package/src/session-handler.ts +15 -0
  493. package/src/session-search.test.ts +50 -0
  494. package/src/session-search.ts +148 -0
  495. package/src/session-title-rename.test.ts +130 -0
  496. package/src/skill-filter.test.ts +83 -0
  497. package/src/skill-filter.ts +42 -0
  498. package/src/startup-service.ts +200 -0
  499. package/src/startup-time.e2e.test.ts +373 -0
  500. package/src/store.ts +139 -0
  501. package/src/subagent-rate-limit-plugin.ts +218 -0
  502. package/src/system-message.test.ts +710 -0
  503. package/src/system-message.ts +814 -0
  504. package/src/task-runner.ts +725 -0
  505. package/src/task-schedule.test.ts +84 -0
  506. package/src/task-schedule.ts +317 -0
  507. package/src/test-utils.ts +451 -0
  508. package/src/thinking-utils.ts +61 -0
  509. package/src/thread-message-queue.e2e.test.ts +1350 -0
  510. package/src/tools.ts +430 -0
  511. package/src/undici.d.ts +12 -0
  512. package/src/undo-redo.e2e.test.ts +209 -0
  513. package/src/unnest-code-blocks.test.ts +713 -0
  514. package/src/unnest-code-blocks.ts +185 -0
  515. package/src/upgrade.ts +185 -0
  516. package/src/utils.test.ts +155 -0
  517. package/src/utils.ts +265 -0
  518. package/src/voice-attachment.ts +51 -0
  519. package/src/voice-handler.ts +908 -0
  520. package/src/voice-message.e2e.test.ts +1255 -0
  521. package/src/voice.test.ts +281 -0
  522. package/src/voice.ts +638 -0
  523. package/src/wait-session.ts +273 -0
  524. package/src/websockify.ts +101 -0
  525. package/src/worker-types.ts +64 -0
  526. package/src/worktree-lifecycle.e2e.test.ts +396 -0
  527. package/src/worktree-utils.ts +4 -0
  528. package/src/worktrees.test.ts +489 -0
  529. package/src/worktrees.ts +1370 -0
  530. package/src/xml.test.ts +38 -0
  531. package/src/xml.ts +121 -0
  532. package/README.md +0 -142
  533. package/dist/cli.d.ts +0 -3
  534. package/dist/cli.d.ts.map +0 -1
  535. package/dist/cli.js.map +0 -1
  536. package/dist/config.d.ts +0 -39
  537. package/dist/config.d.ts.map +0 -1
  538. package/dist/config.js.map +0 -1
  539. package/dist/config.test.d.ts +0 -2
  540. package/dist/config.test.d.ts.map +0 -1
  541. package/dist/config.test.js +0 -202
  542. package/dist/config.test.js.map +0 -1
  543. package/dist/detect.d.ts +0 -9
  544. package/dist/detect.d.ts.map +0 -1
  545. package/dist/detect.js +0 -40
  546. package/dist/detect.js.map +0 -1
  547. package/dist/detect.test.d.ts +0 -2
  548. package/dist/detect.test.d.ts.map +0 -1
  549. package/dist/detect.test.js +0 -26
  550. package/dist/detect.test.js.map +0 -1
  551. package/dist/docker.d.ts +0 -7
  552. package/dist/docker.d.ts.map +0 -1
  553. package/dist/docker.js +0 -17
  554. package/dist/docker.js.map +0 -1
  555. package/dist/docker.test.d.ts +0 -2
  556. package/dist/docker.test.d.ts.map +0 -1
  557. package/dist/docker.test.js +0 -12
  558. package/dist/docker.test.js.map +0 -1
  559. package/dist/health.d.ts +0 -31
  560. package/dist/health.d.ts.map +0 -1
  561. package/dist/health.js +0 -117
  562. package/dist/health.js.map +0 -1
  563. package/dist/health.test.d.ts +0 -2
  564. package/dist/health.test.d.ts.map +0 -1
  565. package/dist/health.test.js +0 -52
  566. package/dist/health.test.js.map +0 -1
  567. package/dist/index.d.ts +0 -20
  568. package/dist/index.d.ts.map +0 -1
  569. package/dist/index.js +0 -15
  570. package/dist/index.js.map +0 -1
  571. package/dist/index.test.d.ts +0 -2
  572. package/dist/index.test.d.ts.map +0 -1
  573. package/dist/index.test.js +0 -8
  574. package/dist/index.test.js.map +0 -1
  575. package/dist/installer.d.ts +0 -10
  576. package/dist/installer.d.ts.map +0 -1
  577. package/dist/installer.js +0 -50
  578. package/dist/installer.js.map +0 -1
  579. package/dist/installer.test.d.ts +0 -2
  580. package/dist/installer.test.d.ts.map +0 -1
  581. package/dist/installer.test.js +0 -43
  582. package/dist/installer.test.js.map +0 -1
  583. package/dist/lifecycle.d.ts +0 -10
  584. package/dist/lifecycle.d.ts.map +0 -1
  585. package/dist/lifecycle.js +0 -45
  586. package/dist/lifecycle.js.map +0 -1
  587. package/dist/lifecycle.test.d.ts +0 -2
  588. package/dist/lifecycle.test.d.ts.map +0 -1
  589. package/dist/lifecycle.test.js +0 -20
  590. package/dist/lifecycle.test.js.map +0 -1
  591. package/dist/manifest.d.ts +0 -18
  592. package/dist/manifest.d.ts.map +0 -1
  593. package/dist/manifest.js +0 -30
  594. package/dist/manifest.js.map +0 -1
  595. package/dist/skills-baseline.d.ts +0 -7
  596. package/dist/skills-baseline.d.ts.map +0 -1
  597. package/dist/skills-baseline.js +0 -9
  598. package/dist/skills-baseline.js.map +0 -1
  599. package/dist/skills.d.ts +0 -110
  600. package/dist/skills.d.ts.map +0 -1
  601. package/dist/skills.js +0 -429
  602. package/dist/skills.js.map +0 -1
  603. package/dist/skills.test.d.ts +0 -2
  604. package/dist/skills.test.d.ts.map +0 -1
  605. package/dist/skills.test.js +0 -416
  606. package/dist/skills.test.js.map +0 -1
  607. package/dist/sync.d.ts +0 -10
  608. package/dist/sync.d.ts.map +0 -1
  609. package/dist/sync.js +0 -39
  610. package/dist/sync.js.map +0 -1
  611. package/dist/tenant.d.ts +0 -13
  612. package/dist/tenant.d.ts.map +0 -1
  613. package/dist/tenant.js +0 -105
  614. package/dist/tenant.js.map +0 -1
  615. package/dist/tenant.test.d.ts +0 -2
  616. package/dist/tenant.test.d.ts.map +0 -1
  617. package/dist/tenant.test.js +0 -37
  618. package/dist/tenant.test.js.map +0 -1
  619. package/src/config.test.ts +0 -237
  620. package/src/detect.test.ts +0 -29
  621. package/src/detect.ts +0 -52
  622. package/src/docker.test.ts +0 -12
  623. package/src/docker.ts +0 -23
  624. package/src/health.test.ts +0 -61
  625. package/src/health.ts +0 -158
  626. package/src/index.test.ts +0 -8
  627. package/src/index.ts +0 -62
  628. package/src/installer.test.ts +0 -52
  629. package/src/installer.ts +0 -62
  630. package/src/lifecycle.test.ts +0 -23
  631. package/src/lifecycle.ts +0 -49
  632. package/src/manifest.ts +0 -42
  633. package/src/skills-baseline.ts +0 -14
  634. package/src/skills.test.ts +0 -503
  635. package/src/skills.ts +0 -512
  636. package/src/sync.ts +0 -53
  637. package/src/tenant.test.ts +0 -49
  638. package/src/tenant.ts +0 -120
@@ -0,0 +1,36 @@
1
+ // Utility to condense MEMORY.md into a line-numbered table of contents.
2
+ // Separated from otto-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 CHANGED
@@ -1,362 +1,126 @@
1
- import fs from "node:fs"
2
- import path from "node:path"
3
- import { OPENCODE_CONFIG_DIR } from "./manifest.js"
4
-
5
- export interface OttoSubagentThreads {
6
- enabled?: boolean
7
- askBeforeDelete?: boolean
8
- autoDeleteOnComplete?: boolean
1
+ // Runtime configuration for Otto bot (formerly Kimaki).
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
+ // Default data directory: ~/.otto for new installs, falls back to ~/.otto
13
+ // for existing users to preserve their database and configuration.
14
+ const DEFAULT_DATA_DIR = (() => {
15
+ const home = os.homedir()
16
+ const ottoDirr = path.join(home, '.otto')
17
+ const ottoDir = path.join(home, '.otto')
18
+ // If ~/.otto already exists, use it. If ~/.otto exists (legacy), use it.
19
+ // Otherwise default to ~/.otto for fresh installs.
20
+ if (fs.existsSync(ottoDirr)) {
21
+ return ottoDirr
22
+ }
23
+ if (fs.existsSync(ottoDir)) {
24
+ return ottoDir
25
+ }
26
+ return ottoDirr
27
+ })()
28
+
29
+ /**
30
+ * Get the data directory path.
31
+ * Falls back to ~/.otto (or ~/.otto for legacy installs) if not explicitly set.
32
+ * Under vitest (OTTO_VITEST / OTTO_VITEST env var), auto-creates an isolated
33
+ * temp dir so tests never touch the real data directory. Tests that need a
34
+ * specific dir can still call setDataDir() before any DB access to override.
35
+ */
36
+ export function getDataDir(): string {
37
+ const current = store.getState().dataDir
38
+ if (current) {
39
+ return current
40
+ }
41
+ if (process.env.OTTO_VITEST || process.env.OTTO_VITEST) {
42
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'otto-test-'))
43
+ store.setState({ dataDir: tmpDir })
44
+ return tmpDir
45
+ }
46
+ store.setState({ dataDir: DEFAULT_DATA_DIR })
47
+ return DEFAULT_DATA_DIR
9
48
  }
10
49
 
11
- export interface OttoConfig {
12
- subagentThreads: {
13
- enabled: boolean
14
- askBeforeDelete: boolean
15
- autoDeleteOnComplete: boolean
16
- }
17
- }
18
-
19
- /** Defaults used when otto.json is missing or partial. */
20
- export const OTTO_DEFAULTS: OttoConfig = {
21
- subagentThreads: {
22
- enabled: true,
23
- askBeforeDelete: true,
24
- autoDeleteOnComplete: false,
25
- },
26
- }
50
+ /**
51
+ * Set the data directory path.
52
+ * Creates the directory if it doesn't exist.
53
+ * Must be called before any database or path-dependent operations.
54
+ */
55
+ export function setDataDir(dir: string): void {
56
+ const resolvedDir = path.resolve(dir)
27
57
 
28
- export interface OpenCodeConfig {
29
- $schema?: string
30
- model?: string
31
- plugin?: string[]
32
- provider?: Record<string, unknown>
33
- agent?: Record<string, unknown>
34
- [key: string]: unknown
35
- }
36
-
37
- export function mergePlugins(config: OpenCodeConfig, pluginToAdd: string): OpenCodeConfig {
38
- const plugins = config.plugin ?? []
39
- if (plugins.includes(pluginToAdd)) {
40
- return config
58
+ if (!fs.existsSync(resolvedDir)) {
59
+ fs.mkdirSync(resolvedDir, { recursive: true })
41
60
  }
42
- return { ...config, plugin: [...plugins, pluginToAdd] }
43
- }
44
-
45
- // ---------------------------------------------------------------------------
46
- // Otto's own config (stored in ~/.config/opencode/otto.json, NOT opencode.json)
47
- // opencode rejects unknown keys in its config — Otto must not pollute it.
48
- // ---------------------------------------------------------------------------
49
61
 
50
- export type OttoConfigReadStatus = "missing" | "ok" | "invalid"
51
-
52
- export function readOttoConfig(dir?: string): OttoConfig {
53
- const configDir = dir ?? OPENCODE_CONFIG_DIR()
54
- const configPath = path.join(configDir, "otto.json")
55
- try {
56
- const raw = fs.readFileSync(configPath, "utf-8")
57
- const parsed = JSON.parse(raw) as Partial<OttoConfig>
58
- return {
59
- subagentThreads: {
60
- enabled: parsed.subagentThreads?.enabled ?? OTTO_DEFAULTS.subagentThreads.enabled,
61
- askBeforeDelete: parsed.subagentThreads?.askBeforeDelete ?? OTTO_DEFAULTS.subagentThreads.askBeforeDelete,
62
- autoDeleteOnComplete: parsed.subagentThreads?.autoDeleteOnComplete ?? OTTO_DEFAULTS.subagentThreads.autoDeleteOnComplete,
63
- },
64
- }
65
- } catch {
66
- return { ...OTTO_DEFAULTS }
67
- }
62
+ store.setState({ dataDir: resolvedDir })
68
63
  }
69
64
 
70
- export function writeOttoConfig(config: OttoConfig, dir?: string): boolean {
71
- const configDir = dir ?? OPENCODE_CONFIG_DIR()
72
- const configPath = path.join(configDir, "otto.json")
73
-
74
- let existing: string | null = null
75
- try {
76
- existing = fs.readFileSync(configPath, "utf-8")
77
- } catch {
78
- // file doesn't exist
65
+ /**
66
+ * Get the projects directory path (for /create-new-project command).
67
+ * Returns the custom --projects-dir if set, otherwise <dataDir>/projects.
68
+ */
69
+ export function getProjectsDir(): string {
70
+ const custom = store.getState().projectsDir
71
+ if (custom) {
72
+ return custom
79
73
  }
80
-
81
- const newContent = JSON.stringify(config, null, 2) + "\n"
82
- if (existing === newContent) {
83
- return false
84
- }
85
-
86
- fs.mkdirSync(configDir, { recursive: true })
87
- fs.writeFileSync(configPath, newContent, "utf-8")
88
- return true
74
+ return path.join(getDataDir(), 'projects')
89
75
  }
90
76
 
91
- export function buildSubagentThreadPolicy(ottoConfig: OttoConfig): string {
92
- const policy = ottoConfig.subagentThreads
93
- const enabled = policy.enabled
94
- const askBeforeDelete = policy.askBeforeDelete
95
- const autoDeleteOnComplete = policy.autoDeleteOnComplete
96
-
97
- const lines = [
98
- "Otto subagent policy (must follow):",
99
- `- subagent_threads_enabled: ${enabled}`,
100
- `- subagent_threads_ask_before_delete: ${askBeforeDelete}`,
101
- `- subagent_threads_auto_delete_on_complete: ${autoDeleteOnComplete}`,
102
- ]
103
-
104
- if (!enabled) {
105
- lines.push(
106
- "- subagent_threads_enabled is false — you may spawn subagents normally (Task tool or otherwise) without creating Discord threads.",
107
- )
108
- return lines.join("\n")
109
- }
110
-
111
- lines.push(
112
- "",
113
- "## Spawning subagents with Discord threads",
114
- "",
115
- "When you need to spawn a subagent (parallel task, background investigation, etc.), do NOT use the Task tool directly.",
116
- "Instead, create a visible Discord thread using `kimaki send` via Bash:",
117
- "",
118
- "```bash",
119
- 'kimaki send --channel <channel_id> --prompt "detailed task description" --user "<username>"',
120
- "```",
121
- "",
122
- "The channel_id and username are in your system prompt (\"Your current Discord channel ID\" / \"Discord\" user field).",
123
- "",
124
- "For sequential tasks where you need the result before continuing, use --wait (set Bash timeout to 1_500_000+):",
125
- "```bash",
126
- 'kimaki send --channel <channel_id> --prompt "task" --user "<username>" --wait',
127
- "```",
128
- "",
129
- "## After subagent completes",
130
- "",
131
- )
77
+ /**
78
+ * Set a custom projects directory path (from --projects-dir CLI flag).
79
+ * Creates the directory if it doesn't exist.
80
+ */
81
+ export function setProjectsDir(dir: string): void {
82
+ const resolvedDir = path.resolve(dir)
132
83
 
133
- if (autoDeleteOnComplete) {
134
- lines.push(
135
- "- subagent_threads_auto_delete_on_complete is true → automatically archive the thread after the subagent finishes:",
136
- " ```bash",
137
- ' kimaki session archive --session <session_id>',
138
- " ```",
139
- )
140
- } else if (askBeforeDelete) {
141
- lines.push(
142
- "- subagent_threads_ask_before_delete is true → after the subagent finishes, ask the user:",
143
- ' "Subagent thread created. Should I archive it or keep it open?"',
144
- " If user says archive → `kimaki session archive --session <session_id>`",
145
- )
146
- } else {
147
- lines.push(
148
- "- Keep the subagent thread open after completion (no auto-archive, no prompt).",
149
- )
84
+ if (!fs.existsSync(resolvedDir)) {
85
+ fs.mkdirSync(resolvedDir, { recursive: true })
150
86
  }
151
87
 
152
- lines.push(
153
- "",
154
- "## Required: load skills before subagent execution",
155
- "",
156
- "Before dispatching any subagent, load the skill: `otto-subagent-threads`.",
157
- "This ensures the subagent follows the same thread creation rules if it spawns further subagents.",
158
- )
159
-
160
- return lines.join("\n")
88
+ store.setState({ projectsDir: resolvedDir })
161
89
  }
162
90
 
163
- export function mergeAgentPrompts(config: OpenCodeConfig, policyText: string): OpenCodeConfig {
164
- const marker = "Otto subagent policy (must follow):"
165
- const agent = (config.agent ?? {}) as Record<string, unknown>
166
- const entries = Object.entries(agent)
167
-
168
- if (entries.length === 0) {
169
- return {
170
- ...config,
171
- agent: {
172
- build: { prompt: policyText },
173
- },
91
+ export type { RegisteredUserCommand } from './store.js'
92
+
93
+ const DEFAULT_LOCK_PORT = 29988
94
+
95
+ /**
96
+ * Derive a lock port from the data directory path.
97
+ * Reads OTTO_LOCK_PORT or OTTO_LOCK_PORT (in that order of preference).
98
+ * Returns a stable port for the default data directories.
99
+ * For custom data dirs, uses a hash to generate a port in the range 30000-39999.
100
+ */
101
+ export function getLockPort(): number {
102
+ const envPortRaw = process.env['OTTO_LOCK_PORT'] || process.env['OTTO_LOCK_PORT']
103
+ if (envPortRaw) {
104
+ const envPort = Number.parseInt(envPortRaw, 10)
105
+ if (Number.isInteger(envPort) && envPort >= 1 && envPort <= 65535) {
106
+ return envPort
174
107
  }
175
108
  }
176
109
 
177
- const nextAgent: Record<string, unknown> = {}
178
- for (const [name, value] of entries) {
179
- if (value && typeof value === "object" && !Array.isArray(value)) {
180
- const typed = value as Record<string, unknown>
181
- const existingPrompt = typeof typed.prompt === "string" ? typed.prompt : ""
182
- const hasPolicy = existingPrompt.includes(marker)
183
- nextAgent[name] = {
184
- ...typed,
185
- prompt: hasPolicy
186
- ? existingPrompt
187
- : (existingPrompt.length > 0 ? `${existingPrompt}\n\n${policyText}` : policyText),
188
- }
189
- } else {
190
- nextAgent[name] = value
191
- }
192
- }
193
-
194
- return { ...config, agent: nextAgent }
195
- }
196
-
197
- export type OpenCodeConfigReadStatus = "missing" | "ok" | "invalid"
198
-
199
- /** Distinguishes a missing file from invalid JSON (both previously surfaced as `{}`). */
200
- export function readOpenCodeConfigState(dir?: string): {
201
- config: OpenCodeConfig
202
- status: OpenCodeConfigReadStatus
203
- } {
204
- const configDir = dir ?? OPENCODE_CONFIG_DIR()
205
- const configPath = path.join(configDir, "opencode.json")
206
- if (!fs.existsSync(configPath)) {
207
- return { config: {}, status: "missing" }
208
- }
209
- try {
210
- const raw = fs.readFileSync(configPath, "utf-8")
211
- return { config: JSON.parse(raw) as OpenCodeConfig, status: "ok" }
212
- } catch {
213
- return { config: {}, status: "invalid" }
214
- }
215
- }
216
-
217
- export function readOpenCodeConfig(dir?: string): OpenCodeConfig {
218
- return readOpenCodeConfigState(dir).config
219
- }
220
-
221
- export function writeOpenCodeConfig(config: OpenCodeConfig, dir?: string): boolean {
222
- const configDir = dir ?? OPENCODE_CONFIG_DIR()
223
- const configPath = path.join(configDir, "opencode.json")
110
+ const dir = getDataDir()
224
111
 
225
- let existing: string | null = null
226
- try {
227
- existing = fs.readFileSync(configPath, "utf-8")
228
- } catch {
229
- // file doesn't exist
112
+ // Use original port for default data dir (backwards compatible)
113
+ if (dir === DEFAULT_DATA_DIR) {
114
+ return DEFAULT_LOCK_PORT
230
115
  }
231
116
 
232
- const newContent = JSON.stringify(config, null, 2) + "\n"
233
- if (existing === newContent) {
234
- return false
117
+ // Hash-based port for custom data dirs
118
+ let hash = 0
119
+ for (let i = 0; i < dir.length; i++) {
120
+ const char = dir.charCodeAt(i)
121
+ hash = (hash << 5) - hash + char
122
+ hash = hash & hash // Convert to 32bit integer
235
123
  }
236
-
237
- fs.mkdirSync(configDir, { recursive: true })
238
- fs.writeFileSync(configPath, newContent, "utf-8")
239
- return true
240
- }
241
-
242
- export function ensureAgentMemoryConfig(dir?: string): boolean {
243
- const configDir = dir ?? OPENCODE_CONFIG_DIR()
244
- const configPath = path.join(configDir, "agent-memory.json")
245
-
246
- try {
247
- fs.readFileSync(configPath, "utf-8")
248
- return false
249
- } catch {
250
- // create with defaults
251
- }
252
-
253
- const defaults = {
254
- journal: {
255
- enabled: true,
256
- tags: [
257
- { name: "infra", description: "Infrastructure changes, host/service topology, runtime environment" },
258
- { name: "debugging", description: "Bug investigations, root-cause analysis, and troubleshooting" },
259
- { name: "decision", description: "Architecture and implementation decisions with rationale" },
260
- { name: "incident", description: "Service failures, outages, and recovery actions" },
261
- { name: "automation", description: "Agent workflows, scripts, and repeatable operational routines" },
262
- ],
263
- },
264
- }
265
-
266
- fs.mkdirSync(configDir, { recursive: true })
267
- fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2) + "\n", "utf-8")
268
- return true
269
- }
270
-
271
- // ---------------------------------------------------------------------------
272
- // Skill file: otto-subagent-threads
273
- // ---------------------------------------------------------------------------
274
-
275
- const SKILL_NAME = "otto-subagent-threads"
276
-
277
- const SKILL_MD = `---
278
- name: otto-subagent-threads
279
- description: Enforce visible Discord threads for every subagent spawn. Use before dispatching any subagent via kimaki send.
280
- ---
281
-
282
- # Otto Subagent Threads
283
-
284
- ## When to Use
285
-
286
- Use this skill EVERY TIME you are about to spawn a subagent (parallel task, background investigation, code review, etc.).
287
-
288
- ## Rules
289
-
290
- ### 1. NEVER use Task tool directly for subagents
291
-
292
- The Task tool spawns a subagent in "shadow mode" — invisible to the user in Discord.
293
- Instead, create a visible Discord thread.
294
-
295
- ### 2. Create a Discord thread with kimaki send
296
-
297
- \`\`\`bash
298
- kimaki send --channel <channel_id> --prompt "detailed task description" --user "<username>"
299
- \`\`\`
300
-
301
- Get channel_id and username from your system prompt:
302
- - "Your current Discord channel ID is: ..."
303
- - Discord user info section
304
-
305
- ### 3. Wait for result if needed
306
-
307
- If you need the subagent's output before continuing, add \`--wait\` and set Bash timeout to 1_500_000+:
308
-
309
- \`\`\`bash
310
- kimaki send --channel <channel_id> --prompt "task" --user "<username>" --wait
311
- \`\`\`
312
-
313
- ### 4. After subagent completes
314
-
315
- Check the otto.subagentThreads config:
316
- - **autoDeleteOnComplete = true** → archive the thread:
317
- \`\`\`bash
318
- kimaki session archive --session <session_id>
319
- \`\`\`
320
- - **askBeforeDelete = true** → ask the user:
321
- "Subagent finished. Should I archive the thread or keep it open?"
322
- - **both false** → keep the thread open.
323
-
324
- ### 5. Nested subagents
325
-
326
- If your subagent itself needs to spawn further subagents, include these same rules in its prompt so it also creates visible threads.
327
-
328
- ## Quick Reference
329
-
330
- | Scenario | Command |
331
- |----------|---------|
332
- | Parallel task (fire & forget) | \`kimaki send -c <ch> -p "task" -u <user>\` |
333
- | Sequential task (need result) | \`kimaki send -c <ch> -p "task" -u <user> --wait\` |
334
- | Archive thread | \`kimaki session archive -s <session_id>\` |
335
- | List sessions | \`kimaki session list\` |
336
- | Read session output | \`kimaki session read <session_id> > ./tmp/session.md 2>/dev/null\` |
337
-
338
- ## Anti-patterns
339
-
340
- - **DO NOT** use the Task tool to spawn subagents (creates invisible shadow sessions).
341
- - **DO NOT** create worktrees (\`--worktree\`) unless the user explicitly asks.
342
- - **DO NOT** forget to include your channel_id in the kimaki send command.
343
- `
344
-
345
- export function ensureSubagentThreadSkill(dir?: string): boolean {
346
- const configDir = dir ?? OPENCODE_CONFIG_DIR()
347
- const skillDir = path.join(configDir, "skills", SKILL_NAME)
348
- const skillPath = path.join(skillDir, "SKILL.md")
349
-
350
- try {
351
- const existing = fs.readFileSync(skillPath, "utf-8")
352
- if (existing === SKILL_MD) {
353
- return false // already up to date
354
- }
355
- } catch {
356
- // file doesn't exist — will create
357
- }
358
-
359
- fs.mkdirSync(skillDir, { recursive: true })
360
- fs.writeFileSync(skillPath, SKILL_MD, "utf-8")
361
- return true
124
+ // Map to port range 30000-39999
125
+ return 30000 + (Math.abs(hash) % 10000)
362
126
  }
@@ -0,0 +1,144 @@
1
+ // Tests for context-awareness directory switch reminders.
2
+
3
+ import { describe, expect, test } from 'vitest'
4
+ import {
5
+ shouldInjectPwd,
6
+ shouldInjectMemoryReminderFromLatestAssistant,
7
+ } from './context-awareness-plugin.js'
8
+
9
+ describe('shouldInjectPwd', () => {
10
+ test('does not inject when current directory matches announced directory', () => {
11
+ const result = shouldInjectPwd({
12
+ currentDir: '/repo/worktree',
13
+ previousDir: '/repo/main',
14
+ announcedDir: '/repo/worktree',
15
+ })
16
+
17
+ expect(result).toMatchInlineSnapshot(`
18
+ {
19
+ "inject": false,
20
+ }
21
+ `)
22
+ })
23
+
24
+ test('does not inject without a previous directory to warn about', () => {
25
+ const result = shouldInjectPwd({
26
+ currentDir: '/repo/worktree',
27
+ previousDir: undefined,
28
+ announcedDir: undefined,
29
+ })
30
+
31
+ expect(result).toMatchInlineSnapshot(`
32
+ {
33
+ "inject": false,
34
+ }
35
+ `)
36
+ })
37
+
38
+ test('names previous and current directories in the correct order', () => {
39
+ const result = shouldInjectPwd({
40
+ currentDir: '/repo/worktree',
41
+ previousDir: '/repo/main',
42
+ announcedDir: undefined,
43
+ })
44
+
45
+ expect(result).toMatchInlineSnapshot(`
46
+ {
47
+ "inject": true,
48
+ "text": "
49
+ [working directory changed (cwd / pwd has changed). The user expects you to edit files in the new cwd. Previous folder (DO NOT TOUCH): /repo/main. New folder (new cwd / pwd, edit files here): /repo/worktree. You MUST read, write, and edit files only under the new folder /repo/worktree. You MUST NOT read, write, or edit any files under the previous folder /repo/main — that folder is a separate checkout and the user or another agent may be actively working there, so writing to it would override their unrelated changes.]
50
+ ",
51
+ }
52
+ `)
53
+ })
54
+
55
+ test('prefers the last announced directory as the previous directory', () => {
56
+ const result = shouldInjectPwd({
57
+ currentDir: '/repo/worktree-b',
58
+ previousDir: '/repo/main',
59
+ announcedDir: '/repo/worktree-a',
60
+ })
61
+
62
+ expect(result).toMatchInlineSnapshot(`
63
+ {
64
+ "inject": true,
65
+ "text": "
66
+ [working directory changed (cwd / pwd has changed). The user expects you to edit files in the new cwd. Previous folder (DO NOT TOUCH): /repo/worktree-a. New folder (new cwd / pwd, edit files here): /repo/worktree-b. You MUST read, write, and edit files only under the new folder /repo/worktree-b. You MUST NOT read, write, or edit any files under the previous folder /repo/worktree-a — that folder is a separate checkout and the user or another agent may be actively working there, so writing to it would override their unrelated changes.]
67
+ ",
68
+ }
69
+ `)
70
+ })
71
+ })
72
+
73
+ describe('shouldInjectMemoryReminderFromLatestAssistant', () => {
74
+ test('does not trigger before threshold', () => {
75
+ const result = shouldInjectMemoryReminderFromLatestAssistant({
76
+ latestAssistantMessage: {
77
+ id: 'msg_asst_1',
78
+ role: 'assistant',
79
+ time: { completed: 1 },
80
+ tokens: {
81
+ input: 1_000,
82
+ output: 3_000,
83
+ reasoning: 500,
84
+ cache: { read: 0, write: 0 },
85
+ },
86
+ },
87
+ threshold: 10_000,
88
+ })
89
+
90
+ expect(result).toMatchInlineSnapshot(`
91
+ {
92
+ "inject": false,
93
+ }
94
+ `)
95
+ })
96
+
97
+ test('triggers when latest assistant message exceeds threshold', () => {
98
+ const result = shouldInjectMemoryReminderFromLatestAssistant({
99
+ latestAssistantMessage: {
100
+ id: 'msg_asst_2',
101
+ role: 'assistant',
102
+ time: { completed: 2 },
103
+ tokens: {
104
+ input: 2_000,
105
+ output: 2_200,
106
+ reasoning: 400,
107
+ cache: { read: 0, write: 0 },
108
+ },
109
+ },
110
+ threshold: 2_000,
111
+ })
112
+
113
+ expect(result).toMatchInlineSnapshot(`
114
+ {
115
+ "assistantMessageId": "msg_asst_2",
116
+ "inject": true,
117
+ }
118
+ `)
119
+ })
120
+
121
+ test('does not trigger again for the same reminded assistant message', () => {
122
+ const result = shouldInjectMemoryReminderFromLatestAssistant({
123
+ lastMemoryReminderAssistantMessageId: 'msg_asst_3',
124
+ latestAssistantMessage: {
125
+ id: 'msg_asst_3',
126
+ role: 'assistant',
127
+ time: { completed: 3 },
128
+ tokens: {
129
+ input: 2_000,
130
+ output: 2_200,
131
+ reasoning: 400,
132
+ cache: { read: 0, write: 0 },
133
+ },
134
+ },
135
+ threshold: 10_000,
136
+ })
137
+
138
+ expect(result).toMatchInlineSnapshot(`
139
+ {
140
+ "inject": false,
141
+ }
142
+ `)
143
+ })
144
+ })