@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,15 @@
1
+ // Thin re-export shim for backward compatibility.
2
+ // Logic lives in:
3
+ // - session-handler/thread-session-runtime.ts (runtime class + registry)
4
+ // - session-handler/thread-runtime-state.ts (state transitions)
5
+ // - session-handler/model-utils.ts (getDefaultModel, types)
6
+ // - session-handler/agent-utils.ts (resolveValidatedAgentPreference)
7
+ // New code should import from the specific module directly.
8
+
9
+ export type { QueuedMessage } from './session-handler/thread-runtime-state.js'
10
+ export {
11
+ getDefaultModel,
12
+ type DefaultModelSource,
13
+ type SessionStartSourceContext,
14
+ } from './session-handler/model-utils.js'
15
+ export { resolveValidatedAgentPreference } from './session-handler/agent-utils.js'
@@ -0,0 +1,50 @@
1
+ // Tests for session search query parsing and snippet matching helpers.
2
+
3
+ import { describe, expect, test } from 'vitest'
4
+ import {
5
+ buildSessionSearchSnippet,
6
+ findFirstSessionSearchHit,
7
+ parseSessionSearchPattern,
8
+ } from './session-search.js'
9
+
10
+ describe('session search helpers', () => {
11
+ test('returns error for invalid regex query', () => {
12
+ const parsed = parseSessionSearchPattern('/(unclosed/')
13
+ expect(parsed).toBeInstanceOf(Error)
14
+ })
15
+
16
+ test('returns snippets that include the matched substring', () => {
17
+ const cases = [
18
+ {
19
+ query: 'panic',
20
+ text: 'There was a PANIC in production',
21
+ expectedSubstring: 'PANIC',
22
+ },
23
+ {
24
+ query: '/error\\s+42/i',
25
+ text: 'Request failed with ERROR 42 in worker',
26
+ expectedSubstring: 'ERROR 42',
27
+ },
28
+ ]
29
+
30
+ cases.forEach(({ query, text, expectedSubstring }) => {
31
+ const parsed = parseSessionSearchPattern(query)
32
+ if (parsed instanceof Error) {
33
+ throw parsed
34
+ }
35
+ const hit = findFirstSessionSearchHit({ text, searchPattern: parsed })
36
+ expect(hit).toBeDefined()
37
+ if (!hit) {
38
+ return
39
+ }
40
+
41
+ const snippet = buildSessionSearchSnippet({
42
+ text,
43
+ hit,
44
+ contextLength: 8,
45
+ })
46
+
47
+ expect(snippet.toUpperCase()).toContain(expectedSubstring.toUpperCase())
48
+ })
49
+ })
50
+ })
@@ -0,0 +1,148 @@
1
+ // Session search helpers for otto CLI commands.
2
+ // Parses string/regex queries and builds readable snippets from matched content.
3
+
4
+ import type { Part } from '@opencode-ai/sdk/v2'
5
+
6
+ export type SessionSearchPattern =
7
+ | {
8
+ mode: 'literal'
9
+ raw: string
10
+ normalizedNeedle: string
11
+ }
12
+ | {
13
+ mode: 'regex'
14
+ raw: string
15
+ regex: RegExp
16
+ }
17
+
18
+ export type SessionSearchHit = {
19
+ index: number
20
+ length: number
21
+ }
22
+
23
+ export function parseSessionSearchPattern(
24
+ query: string,
25
+ ): SessionSearchPattern | Error {
26
+ const trimmedQuery = query.trim()
27
+ if (!trimmedQuery) {
28
+ return new Error('Search query cannot be empty')
29
+ }
30
+
31
+ const regexMatch = trimmedQuery.match(/^\/([\s\S]+)\/([a-z]*)$/)
32
+ if (!regexMatch) {
33
+ return {
34
+ mode: 'literal',
35
+ raw: trimmedQuery,
36
+ normalizedNeedle: trimmedQuery.toLowerCase(),
37
+ }
38
+ }
39
+
40
+ const pattern = regexMatch[1] || ''
41
+ const flags = regexMatch[2] || ''
42
+
43
+ try {
44
+ return {
45
+ mode: 'regex',
46
+ raw: trimmedQuery,
47
+ regex: new RegExp(pattern, flags),
48
+ }
49
+ } catch (error) {
50
+ return new Error(
51
+ `Invalid regex query "${trimmedQuery}": ${error instanceof Error ? error.message : String(error)}`,
52
+ )
53
+ }
54
+ }
55
+
56
+ export function findFirstSessionSearchHit({
57
+ text,
58
+ searchPattern,
59
+ }: {
60
+ text: string
61
+ searchPattern: SessionSearchPattern
62
+ }): SessionSearchHit | undefined {
63
+ if (searchPattern.mode === 'literal') {
64
+ const index = text.toLowerCase().indexOf(searchPattern.normalizedNeedle)
65
+ if (index < 0) {
66
+ return undefined
67
+ }
68
+ return {
69
+ index,
70
+ length: searchPattern.raw.length,
71
+ }
72
+ }
73
+
74
+ searchPattern.regex.lastIndex = 0
75
+ const match = searchPattern.regex.exec(text)
76
+ if (!match || match.index < 0) {
77
+ return undefined
78
+ }
79
+
80
+ return {
81
+ index: match.index,
82
+ length: Math.max(match[0]?.length || 0, 1),
83
+ }
84
+ }
85
+
86
+ export function buildSessionSearchSnippet({
87
+ text,
88
+ hit,
89
+ contextLength = 90,
90
+ }: {
91
+ text: string
92
+ hit: SessionSearchHit
93
+ contextLength?: number
94
+ }): string {
95
+ const start = Math.max(0, hit.index - contextLength)
96
+ const end = Math.min(text.length, hit.index + hit.length + contextLength)
97
+
98
+ const prefix = start > 0 ? '...' : ''
99
+ const suffix = end < text.length ? '...' : ''
100
+ const body = text
101
+ .slice(start, end)
102
+ .replace(/[\r\n\t]+/g, ' ')
103
+ .replace(/\s+/g, ' ')
104
+ .trim()
105
+
106
+ return `${prefix}${body}${suffix}`
107
+ }
108
+
109
+ function stringifyUnknown(value: unknown): string {
110
+ if (value === undefined || value === null) {
111
+ return ''
112
+ }
113
+ if (typeof value === 'string') {
114
+ return value
115
+ }
116
+ try {
117
+ return JSON.stringify(value)
118
+ } catch {
119
+ return String(value)
120
+ }
121
+ }
122
+
123
+ export function getPartSearchTexts(part: Part): string[] {
124
+ switch (part.type) {
125
+ case 'text':
126
+ return part.text ? [part.text] : []
127
+ case 'reasoning':
128
+ return part.text ? [part.text] : []
129
+ case 'tool': {
130
+ const inputText = stringifyUnknown(part.state.input)
131
+ const outputText =
132
+ part.state.status === 'completed'
133
+ ? stringifyUnknown(part.state.output)
134
+ : part.state.status === 'error'
135
+ ? part.state.error || ''
136
+ : ''
137
+ return [`tool:${part.tool}`, inputText, outputText].filter((entry) => {
138
+ return entry.trim().length > 0
139
+ })
140
+ }
141
+ case 'file':
142
+ return [part.filename || '', part.url || ''].filter((entry) => {
143
+ return entry.trim().length > 0
144
+ })
145
+ default:
146
+ return []
147
+ }
148
+ }
@@ -0,0 +1,130 @@
1
+ // Unit tests for deriveThreadNameFromSessionTitle — the pure helper that
2
+ // decides whether (and how) to rename a Discord thread based on an
3
+ // OpenCode session title. Kept focused and deterministic; no Discord mocks.
4
+
5
+ import { describe, test, expect } from 'vitest'
6
+ import { deriveThreadNameFromSessionTitle } from './session-handler/thread-session-runtime.js'
7
+
8
+ describe('deriveThreadNameFromSessionTitle', () => {
9
+ test('returns trimmed title for plain thread', () => {
10
+ expect(
11
+ deriveThreadNameFromSessionTitle({
12
+ sessionTitle: ' Fix auth bug ',
13
+ currentName: 'fix the auth',
14
+ }),
15
+ ).toMatchInlineSnapshot(`"Fix auth bug"`)
16
+ })
17
+
18
+ test('preserves worktree prefix from current name', () => {
19
+ expect(
20
+ deriveThreadNameFromSessionTitle({
21
+ sessionTitle: 'Refactor queue',
22
+ currentName: '⬦ refactor queue old',
23
+ }),
24
+ ).toMatchInlineSnapshot(`"⬦ Refactor queue"`)
25
+ })
26
+
27
+ test('ignores placeholder "New Session -" titles', () => {
28
+ expect(
29
+ deriveThreadNameFromSessionTitle({
30
+ sessionTitle: 'New Session - 2025-01-02',
31
+ currentName: 'whatever',
32
+ }),
33
+ ).toMatchInlineSnapshot(`undefined`)
34
+ })
35
+
36
+ test('ignores case-insensitive placeholder titles', () => {
37
+ expect(
38
+ deriveThreadNameFromSessionTitle({
39
+ sessionTitle: 'new session -abc',
40
+ currentName: 'whatever',
41
+ }),
42
+ ).toMatchInlineSnapshot(`undefined`)
43
+ })
44
+
45
+ test('returns undefined when candidate already matches current name', () => {
46
+ expect(
47
+ deriveThreadNameFromSessionTitle({
48
+ sessionTitle: 'Fix auth bug',
49
+ currentName: 'Fix auth bug',
50
+ }),
51
+ ).toMatchInlineSnapshot(`undefined`)
52
+ })
53
+
54
+ test('returns undefined when candidate (with worktree prefix) already matches', () => {
55
+ expect(
56
+ deriveThreadNameFromSessionTitle({
57
+ sessionTitle: 'Refactor queue',
58
+ currentName: '⬦ Refactor queue',
59
+ }),
60
+ ).toMatchInlineSnapshot(`undefined`)
61
+ })
62
+
63
+ test('truncates to 100 chars including worktree prefix', () => {
64
+ const result = deriveThreadNameFromSessionTitle({
65
+ sessionTitle: 'x'.repeat(200),
66
+ currentName: '⬦ seed',
67
+ })
68
+ expect(result?.length).toMatchInlineSnapshot(`100`)
69
+ expect(result?.startsWith('⬦ ')).toMatchInlineSnapshot(`true`)
70
+ })
71
+
72
+ test('truncates to 100 chars without prefix', () => {
73
+ const result = deriveThreadNameFromSessionTitle({
74
+ sessionTitle: 'y'.repeat(200),
75
+ currentName: 'seed',
76
+ })
77
+ expect(result?.length).toMatchInlineSnapshot(`100`)
78
+ })
79
+
80
+ test('returns undefined for empty string', () => {
81
+ expect(
82
+ deriveThreadNameFromSessionTitle({
83
+ sessionTitle: '',
84
+ currentName: 'seed',
85
+ }),
86
+ ).toMatchInlineSnapshot(`undefined`)
87
+ })
88
+
89
+ test('returns undefined for whitespace-only title', () => {
90
+ expect(
91
+ deriveThreadNameFromSessionTitle({
92
+ sessionTitle: ' ',
93
+ currentName: 'seed',
94
+ }),
95
+ ).toMatchInlineSnapshot(`undefined`)
96
+ })
97
+
98
+ test('preserves btw: prefix from current name', () => {
99
+ expect(
100
+ deriveThreadNameFromSessionTitle({
101
+ sessionTitle: 'Side question about auth',
102
+ currentName: 'btw: why is auth broken',
103
+ }),
104
+ ).toMatchInlineSnapshot(`"btw: Side question about auth"`)
105
+ })
106
+
107
+ test('preserves Fork: prefix from current name', () => {
108
+ expect(
109
+ deriveThreadNameFromSessionTitle({
110
+ sessionTitle: 'Forked task title',
111
+ currentName: 'Fork: old session title',
112
+ }),
113
+ ).toMatchInlineSnapshot(`"Fork: Forked task title"`)
114
+ })
115
+
116
+ test('returns undefined for null/undefined title', () => {
117
+ expect(
118
+ deriveThreadNameFromSessionTitle({
119
+ sessionTitle: null,
120
+ currentName: 'seed',
121
+ }),
122
+ ).toMatchInlineSnapshot(`undefined`)
123
+ expect(
124
+ deriveThreadNameFromSessionTitle({
125
+ sessionTitle: undefined,
126
+ currentName: 'seed',
127
+ }),
128
+ ).toMatchInlineSnapshot(`undefined`)
129
+ })
130
+ })
@@ -0,0 +1,83 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { computeSkillPermission } from './skill-filter.js'
3
+
4
+ describe('computeSkillPermission', () => {
5
+ test('empty inputs returns undefined (no filtering)', () => {
6
+ expect(
7
+ computeSkillPermission({ enabledSkills: [], disabledSkills: [] }),
8
+ ).toMatchInlineSnapshot(`undefined`)
9
+ })
10
+
11
+ test('whitelist single skill', () => {
12
+ expect(
13
+ computeSkillPermission({
14
+ enabledSkills: ['npm-package'],
15
+ disabledSkills: [],
16
+ }),
17
+ ).toMatchInlineSnapshot(`
18
+ {
19
+ "*": "deny",
20
+ "npm-package": "allow",
21
+ }
22
+ `)
23
+ })
24
+
25
+ test('whitelist multiple skills', () => {
26
+ expect(
27
+ computeSkillPermission({
28
+ enabledSkills: ['npm-package', 'playwriter', 'errore'],
29
+ disabledSkills: [],
30
+ }),
31
+ ).toMatchInlineSnapshot(`
32
+ {
33
+ "*": "deny",
34
+ "errore": "allow",
35
+ "npm-package": "allow",
36
+ "playwriter": "allow",
37
+ }
38
+ `)
39
+ })
40
+
41
+ test('blacklist single skill', () => {
42
+ expect(
43
+ computeSkillPermission({
44
+ enabledSkills: [],
45
+ disabledSkills: ['jitter'],
46
+ }),
47
+ ).toMatchInlineSnapshot(`
48
+ {
49
+ "jitter": "deny",
50
+ }
51
+ `)
52
+ })
53
+
54
+ test('blacklist multiple skills', () => {
55
+ expect(
56
+ computeSkillPermission({
57
+ enabledSkills: [],
58
+ disabledSkills: ['jitter', 'termcast'],
59
+ }),
60
+ ).toMatchInlineSnapshot(`
61
+ {
62
+ "jitter": "deny",
63
+ "termcast": "deny",
64
+ }
65
+ `)
66
+ })
67
+
68
+ test('whitelist takes precedence when both are set (cli.ts is expected to reject this upstream)', () => {
69
+ // cli.ts validates mutual exclusion before reaching this helper. This
70
+ // test documents the defensive behavior if both arrays ever leak through.
71
+ expect(
72
+ computeSkillPermission({
73
+ enabledSkills: ['npm-package'],
74
+ disabledSkills: ['jitter'],
75
+ }),
76
+ ).toMatchInlineSnapshot(`
77
+ {
78
+ "*": "deny",
79
+ "npm-package": "allow",
80
+ }
81
+ `)
82
+ })
83
+ })
@@ -0,0 +1,42 @@
1
+ // Computes opencode permission.skill rules from otto's --enable-skill /
2
+ // --disable-skill CLI flags.
3
+ //
4
+ // OpenCode filters skills available to the model via
5
+ // Permission.evaluate("skill", skill.name, agent.permission). We inject a
6
+ // top-level permission.skill ruleset into the generated opencode-config.json
7
+ // so every agent inherits the same whitelist/blacklist via Permission.merge.
8
+ //
9
+ // Whitelist mode: { '*': 'deny', 'name': 'allow', ... }
10
+ // Blacklist mode: { 'name': 'deny', ... }
11
+ // Neither set: undefined (skills are unfiltered)
12
+ //
13
+ // cli.ts validates mutual exclusion of the two flags at startup, so this
14
+ // helper assumes at most one of the two arrays is non-empty.
15
+
16
+ type PermissionAction = 'ask' | 'allow' | 'deny'
17
+
18
+ export type SkillPermissionRule = Record<string, PermissionAction>
19
+
20
+ export function computeSkillPermission({
21
+ enabledSkills,
22
+ disabledSkills,
23
+ }: {
24
+ enabledSkills: string[]
25
+ disabledSkills: string[]
26
+ }): SkillPermissionRule | undefined {
27
+ if (enabledSkills.length > 0) {
28
+ const rules: SkillPermissionRule = { '*': 'deny' }
29
+ for (const name of enabledSkills) {
30
+ rules[name] = 'allow'
31
+ }
32
+ return rules
33
+ }
34
+ if (disabledSkills.length > 0) {
35
+ const rules: SkillPermissionRule = {}
36
+ for (const name of disabledSkills) {
37
+ rules[name] = 'deny'
38
+ }
39
+ return rules
40
+ }
41
+ return undefined
42
+ }
@@ -0,0 +1,200 @@
1
+ // Cross-platform startup service registration for otto daemon.
2
+ // Vendored from startup-run (MIT, github.com/vilicvane/startup-run) with
3
+ // significant simplifications: no abstract classes, no fs-extra, no winreg
4
+ // npm dep, no separate daemon process (otto's bin.ts already handles
5
+ // respawn/crash-loop). Just writes/deletes the platform service file.
6
+ //
7
+ // macOS: ~/Library/LaunchAgents/xyz.otto.plist (launchd)
8
+ // Linux: ~/.config/autostart/otto.desktop (XDG autostart)
9
+ // Windows: HKCU\Software\Microsoft\Windows\CurrentVersion\Run (registry)
10
+
11
+ import fs from 'node:fs'
12
+ import os from 'node:os'
13
+ import path from 'node:path'
14
+ import { execAsync } from './worktrees.js'
15
+
16
+ const SERVICE_NAME = 'xyz.otto'
17
+
18
+ function getServiceFilePath(): string {
19
+ switch (process.platform) {
20
+ case 'darwin':
21
+ return path.join(
22
+ os.homedir(),
23
+ 'Library',
24
+ 'LaunchAgents',
25
+ `${SERVICE_NAME}.plist`,
26
+ )
27
+ case 'linux':
28
+ return path.join(
29
+ os.homedir(),
30
+ '.config',
31
+ 'autostart',
32
+ 'otto.desktop',
33
+ )
34
+ case 'win32':
35
+ // No file — registry key, return a descriptive string for status display
36
+ return 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\otto'
37
+ default:
38
+ throw new Error(`Unsupported platform: ${process.platform}`)
39
+ }
40
+ }
41
+
42
+ function escapeXml(value: string): string {
43
+ return value
44
+ .replace(/&/g, '&amp;')
45
+ .replace(/</g, '&lt;')
46
+ .replace(/>/g, '&gt;')
47
+ }
48
+
49
+ // Shell-escape a string for use in a Linux .desktop Exec= line.
50
+ // Wraps in double quotes if it contains spaces or special chars.
51
+ function shellEscape(value: string): string {
52
+ if (/^[a-zA-Z0-9._/=-]+$/.test(value)) {
53
+ return value
54
+ }
55
+ return `"${value.replace(/"/g, '\\"')}"`
56
+ }
57
+
58
+ function buildMacOSPlist({
59
+ command,
60
+ args,
61
+ }: {
62
+ command: string
63
+ args: string[]
64
+ }): string {
65
+ const segments = [command, ...args]
66
+ return `<?xml version="1.0" encoding="UTF-8"?>
67
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
68
+ <plist version="1.0">
69
+ <dict>
70
+ <key>Label</key>
71
+ <string>${SERVICE_NAME}</string>
72
+ <key>ProgramArguments</key>
73
+ <array>
74
+ ${segments.map((s) => ` <string>${escapeXml(s)}</string>`).join('\n')}
75
+ </array>
76
+ <key>RunAtLoad</key>
77
+ <true/>
78
+ <key>KeepAlive</key>
79
+ <false/>
80
+ </dict>
81
+ </plist>
82
+ `
83
+ }
84
+
85
+ function buildLinuxDesktop({
86
+ command,
87
+ args,
88
+ }: {
89
+ command: string
90
+ args: string[]
91
+ }): string {
92
+ const execLine = [command, ...args].map(shellEscape).join(' ')
93
+ return `[Desktop Entry]
94
+ Type=Application
95
+ Version=1.0
96
+ Name=Otto
97
+ Comment=Otto Discord Bot Daemon
98
+ Exec=${execLine}
99
+ StartupNotify=false
100
+ Terminal=false
101
+ `
102
+ }
103
+
104
+ export type StartupServiceOptions = {
105
+ command: string
106
+ args: string[]
107
+ }
108
+
109
+ /**
110
+ * Register otto to start on user login.
111
+ * Writes the appropriate service file for the current platform.
112
+ */
113
+ export async function enableStartupService({
114
+ command,
115
+ args,
116
+ }: StartupServiceOptions): Promise<void> {
117
+ const platform = process.platform
118
+
119
+ if (platform === 'darwin') {
120
+ const filePath = getServiceFilePath()
121
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
122
+ fs.writeFileSync(filePath, buildMacOSPlist({ command, args }))
123
+ } else if (platform === 'linux') {
124
+ const filePath = getServiceFilePath()
125
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
126
+ fs.writeFileSync(filePath, buildLinuxDesktop({ command, args }))
127
+ } else if (platform === 'win32') {
128
+ const execLine = [command, ...args]
129
+ .map((s) => {
130
+ return s.includes(' ') ? `"${s}"` : s
131
+ })
132
+ .join(' ')
133
+ await execAsync(
134
+ `reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v otto /t REG_SZ /d "${execLine}" /f`,
135
+ )
136
+ } else {
137
+ throw new Error(`Unsupported platform: ${platform}`)
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Unregister otto from user login startup.
143
+ */
144
+ export async function disableStartupService(): Promise<void> {
145
+ const platform = process.platform
146
+
147
+ if (platform === 'darwin' || platform === 'linux') {
148
+ const filePath = getServiceFilePath()
149
+ if (fs.existsSync(filePath)) {
150
+ fs.unlinkSync(filePath)
151
+ }
152
+ } else if (platform === 'win32') {
153
+ await execAsync(
154
+ `reg delete "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v otto /f`,
155
+ ).catch(() => {
156
+ // Key may not exist, ignore
157
+ })
158
+ } else {
159
+ throw new Error(`Unsupported platform: ${platform}`)
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Check if otto is registered as a startup service.
165
+ */
166
+ export async function isStartupServiceEnabled(): Promise<boolean> {
167
+ const platform = process.platform
168
+
169
+ if (platform === 'darwin' || platform === 'linux') {
170
+ return fs.existsSync(getServiceFilePath())
171
+ }
172
+
173
+ if (platform === 'win32') {
174
+ const result = await execAsync(
175
+ `reg query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v otto`,
176
+ ).catch(() => {
177
+ return null
178
+ })
179
+ return result !== null
180
+ }
181
+
182
+ return false
183
+ }
184
+
185
+ /**
186
+ * Get a human-readable description of the service location for status display.
187
+ */
188
+ export function getServiceLocationDescription(): string {
189
+ const platform = process.platform
190
+ if (platform === 'darwin') {
191
+ return `launchd: ${getServiceFilePath()}`
192
+ }
193
+ if (platform === 'linux') {
194
+ return `XDG autostart: ${getServiceFilePath()}`
195
+ }
196
+ if (platform === 'win32') {
197
+ return `registry: ${getServiceFilePath()}`
198
+ }
199
+ return `unsupported platform: ${platform}`
200
+ }