@otto-assistant/otto 0.1.2 → 0.7.16

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 +655 -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 +893 -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 +369 -0
  47. package/dist/commands/model.js +798 -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 +179 -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 +1124 -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 +789 -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 +1181 -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 +488 -0
  324. package/src/commands/model.ts +1082 -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 +1507 -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 +232 -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 +1462 -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,157 @@
1
+ // /compact command - Trigger context compaction (summarization) for the current session.
2
+
3
+ import {
4
+ ChannelType,
5
+ MessageFlags,
6
+ type TextChannel,
7
+ type ThreadChannel,
8
+ } from 'discord.js'
9
+ import type { CommandContext } from './types.js'
10
+ import { getThreadSession } from '../database.js'
11
+ import {
12
+ initializeOpencodeForDirectory,
13
+ getOpencodeClient,
14
+ } from '../opencode.js'
15
+ import {
16
+ resolveWorkingDirectory,
17
+ SILENT_MESSAGE_FLAGS,
18
+ } from '../discord-utils.js'
19
+ import { createLogger, LogPrefix } from '../logger.js'
20
+
21
+ const logger = createLogger(LogPrefix.COMPACT)
22
+
23
+ export async function handleCompactCommand({
24
+ command,
25
+ }: CommandContext): Promise<void> {
26
+ const channel = command.channel
27
+
28
+ if (!channel) {
29
+ await command.reply({
30
+ content: 'This command can only be used in a channel',
31
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
32
+ })
33
+ return
34
+ }
35
+
36
+ const isThread = [
37
+ ChannelType.PublicThread,
38
+ ChannelType.PrivateThread,
39
+ ChannelType.AnnouncementThread,
40
+ ].includes(channel.type)
41
+
42
+ if (!isThread) {
43
+ await command.reply({
44
+ content:
45
+ 'This command can only be used in a thread with an active session',
46
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
47
+ })
48
+ return
49
+ }
50
+
51
+ const resolved = await resolveWorkingDirectory({
52
+ channel: channel as TextChannel | ThreadChannel,
53
+ })
54
+
55
+ if (!resolved) {
56
+ await command.reply({
57
+ content: 'Could not determine project directory for this channel',
58
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
59
+ })
60
+ return
61
+ }
62
+
63
+ const { projectDirectory, workingDirectory } = resolved
64
+
65
+ const sessionId = await getThreadSession(channel.id)
66
+
67
+ if (!sessionId) {
68
+ await command.reply({
69
+ content: 'No active session in this thread',
70
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
71
+ })
72
+ return
73
+ }
74
+
75
+ // Ensure server is running for the base project directory
76
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
77
+ if (getClient instanceof Error) {
78
+ await command.reply({
79
+ content: `Failed to compact: ${getClient.message}`,
80
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
81
+ })
82
+ return
83
+ }
84
+
85
+ const client = getOpencodeClient(projectDirectory)
86
+ if (!client) {
87
+ await command.reply({
88
+ content: 'Failed to get OpenCode client',
89
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
90
+ })
91
+ return
92
+ }
93
+
94
+ // Defer reply since compaction may take a moment
95
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
96
+
97
+ try {
98
+ // Get session messages to find the model from the last user message
99
+ const messagesResult = await client.session.messages({
100
+ sessionID: sessionId,
101
+ directory: workingDirectory,
102
+ })
103
+
104
+ if (messagesResult.error || !messagesResult.data) {
105
+ logger.error('[COMPACT] Failed to get messages:', messagesResult.error)
106
+ await command.editReply({
107
+ content: 'Failed to compact: Could not retrieve session messages',
108
+ })
109
+ return
110
+ }
111
+
112
+ // Find the last user message to get the model
113
+ const lastUserMessage = [...messagesResult.data]
114
+ .reverse()
115
+ .find((msg) => msg.info.role === 'user')
116
+
117
+ if (!lastUserMessage || lastUserMessage.info.role !== 'user') {
118
+ await command.editReply({
119
+ content: 'Failed to compact: No user message found in session',
120
+ })
121
+ return
122
+ }
123
+
124
+ const { providerID, modelID } = lastUserMessage.info.model
125
+
126
+ const result = await client.session.summarize({
127
+ sessionID: sessionId,
128
+ directory: workingDirectory,
129
+ providerID,
130
+ modelID,
131
+ auto: false,
132
+ })
133
+
134
+ if (result.error) {
135
+ logger.error('[COMPACT] Error:', result.error)
136
+ const errorMessage =
137
+ 'data' in result.error && result.error.data
138
+ ? (result.error.data as { message?: string }).message ||
139
+ 'Unknown error'
140
+ : 'Unknown error'
141
+ await command.editReply({
142
+ content: `Failed to compact: ${errorMessage}`,
143
+ })
144
+ return
145
+ }
146
+
147
+ await command.editReply({
148
+ content: `šŸ“¦ Session **compacted** successfully`,
149
+ })
150
+ logger.log(`Session ${sessionId} compacted by user`)
151
+ } catch (error) {
152
+ logger.error('[COMPACT] Error:', error)
153
+ await command.editReply({
154
+ content: `Failed to compact: ${error instanceof Error ? error.message : 'Unknown error'}`,
155
+ })
156
+ }
157
+ }
@@ -0,0 +1,199 @@
1
+ // /context-usage command - Show token usage and context window percentage for the current session.
2
+
3
+ import {
4
+ ChannelType,
5
+ MessageFlags,
6
+ type TextChannel,
7
+ type ThreadChannel,
8
+ } from 'discord.js'
9
+ import type { CommandContext } from './types.js'
10
+ import { getThreadSession } from '../database.js'
11
+ import { initializeOpencodeForDirectory } from '../opencode.js'
12
+ import {
13
+ resolveWorkingDirectory,
14
+ SILENT_MESSAGE_FLAGS,
15
+ } from '../discord-utils.js'
16
+ import { createLogger, LogPrefix } from '../logger.js'
17
+ import * as errore from 'errore'
18
+
19
+ const logger = createLogger(LogPrefix.SESSION)
20
+
21
+ function getTokenTotal({
22
+ input,
23
+ output,
24
+ reasoning,
25
+ cache,
26
+ }: {
27
+ input: number
28
+ output: number
29
+ reasoning: number
30
+ cache: { read: number; write: number }
31
+ }): number {
32
+ return input + output + reasoning + cache.read + cache.write
33
+ }
34
+
35
+ export async function handleContextUsageCommand({
36
+ command,
37
+ }: CommandContext): Promise<void> {
38
+ const channel = command.channel
39
+
40
+ if (!channel) {
41
+ await command.reply({
42
+ content: 'This command can only be used in a channel',
43
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
44
+ })
45
+ return
46
+ }
47
+
48
+ const isThread = [
49
+ ChannelType.PublicThread,
50
+ ChannelType.PrivateThread,
51
+ ChannelType.AnnouncementThread,
52
+ ].includes(channel.type)
53
+
54
+ if (!isThread) {
55
+ await command.reply({
56
+ content:
57
+ 'This command can only be used in a thread with an active session',
58
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
59
+ })
60
+ return
61
+ }
62
+
63
+ const resolved = await resolveWorkingDirectory({
64
+ channel: channel as TextChannel | ThreadChannel,
65
+ })
66
+
67
+ if (!resolved) {
68
+ await command.reply({
69
+ content: 'Could not determine project directory for this channel',
70
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
71
+ })
72
+ return
73
+ }
74
+
75
+ const { projectDirectory, workingDirectory } = resolved
76
+
77
+ const sessionId = await getThreadSession(channel.id)
78
+
79
+ if (!sessionId) {
80
+ await command.reply({
81
+ content: 'No active session in this thread',
82
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
83
+ })
84
+ return
85
+ }
86
+
87
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
88
+ if (getClient instanceof Error) {
89
+ await command.reply({
90
+ content: `Failed to get context usage: ${getClient.message}`,
91
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
92
+ })
93
+ return
94
+ }
95
+
96
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
97
+
98
+ try {
99
+ const messagesResponse = await getClient().session.messages({
100
+ sessionID: sessionId,
101
+ directory: workingDirectory,
102
+ })
103
+
104
+ const messages = messagesResponse.data || []
105
+ const assistantMessages = messages.filter(
106
+ (m) => m.info.role === 'assistant',
107
+ )
108
+
109
+ if (assistantMessages.length === 0) {
110
+ await command.editReply({
111
+ content: 'No assistant messages in this session yet',
112
+ })
113
+ return
114
+ }
115
+
116
+ const lastAssistant = [...assistantMessages].reverse().find((m) => {
117
+ if (m.info.role !== 'assistant') {
118
+ return false
119
+ }
120
+ if (!m.info.tokens) {
121
+ return false
122
+ }
123
+ return getTokenTotal(m.info.tokens) > 0
124
+ })
125
+
126
+ if (!lastAssistant || lastAssistant.info.role !== 'assistant') {
127
+ await command.editReply({
128
+ content: 'Token usage not available for this session yet',
129
+ })
130
+ return
131
+ }
132
+
133
+ const { tokens, modelID, providerID } = lastAssistant.info
134
+ const totalTokens = getTokenTotal(tokens)
135
+
136
+ // Sum cost across all assistant messages for accurate session total
137
+ // (AssistantMessage.cost is per-message, not cumulative)
138
+ const totalCost = assistantMessages.reduce((sum, m) => {
139
+ if (m.info.role === 'assistant' && 'cost' in m.info) {
140
+ return sum + (m.info.cost || 0)
141
+ }
142
+ return sum
143
+ }, 0)
144
+
145
+ // Fetch model context limit from provider API
146
+ let contextLimit: number | undefined
147
+ const providersResult = await errore.tryAsync(() => {
148
+ return getClient().provider.list({ directory: workingDirectory })
149
+ })
150
+ if (providersResult instanceof Error) {
151
+ logger.error(
152
+ '[CONTEXT-USAGE] Failed to fetch provider info:',
153
+ providersResult,
154
+ )
155
+ } else {
156
+ const provider = providersResult.data?.all?.find(
157
+ (p) => p.id === providerID,
158
+ )
159
+ const model = provider?.models?.[modelID]
160
+ if (model?.limit?.context) {
161
+ contextLimit = model.limit.context
162
+ }
163
+ }
164
+
165
+ const formattedTokens = totalTokens.toLocaleString('en-US')
166
+ const formattedCost = totalCost > 0 ? `$${totalCost.toFixed(4)}` : '$0.00'
167
+
168
+ const lines: string[] = []
169
+
170
+ if (contextLimit) {
171
+ const percentage = Math.round((totalTokens / contextLimit) * 100)
172
+ const formattedLimit = contextLimit.toLocaleString('en-US')
173
+ lines.push(
174
+ `**Context usage:** ${percentage}%, ${formattedTokens} / ${formattedLimit} tokens`,
175
+ )
176
+ } else {
177
+ lines.push(
178
+ `**Context usage:** ${formattedTokens} tokens (context limit unavailable)`,
179
+ )
180
+ }
181
+
182
+ if (modelID) {
183
+ lines.push(`**Model:** ${modelID}`)
184
+ }
185
+ if (totalCost > 0) {
186
+ lines.push(`**Session cost:** ${formattedCost}`)
187
+ }
188
+
189
+ await command.editReply({ content: lines.join('\n') })
190
+ logger.log(
191
+ `Context usage shown for session ${sessionId}: ${totalTokens} tokens`,
192
+ )
193
+ } catch (error) {
194
+ logger.error('[CONTEXT-USAGE] Error:', error)
195
+ await command.editReply({
196
+ content: `Failed to get context usage: ${error instanceof Error ? error.message : 'Unknown error'}`,
197
+ })
198
+ }
199
+ }
@@ -0,0 +1,190 @@
1
+ // /create-new-project command - Create a new project folder, initialize git, and start a session.
2
+ // Also exports createNewProject() for reuse during onboarding (welcome channel creation).
3
+
4
+ import { ChannelType, type Guild, type TextChannel } from 'discord.js'
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+ import { execAsync } from '../worktrees.js'
8
+ import type { CommandContext } from './types.js'
9
+ import { getProjectsDir } from '../config.js'
10
+ import { createProjectChannels } from '../channel-management.js'
11
+ import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js'
12
+ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
13
+ import { createLogger, LogPrefix } from '../logger.js'
14
+
15
+ const logger = createLogger(LogPrefix.CREATE_PROJECT)
16
+
17
+ /**
18
+ * Core project creation logic: creates directory, inits git, creates Discord channels.
19
+ * Reused by the slash command handler and by onboarding (welcome channel).
20
+ * Returns null if the project directory already exists.
21
+ */
22
+ export async function createNewProject({
23
+ guild,
24
+ projectName,
25
+ botName,
26
+ }: {
27
+ guild: Guild
28
+ projectName: string
29
+ botName?: string
30
+ }): Promise<{
31
+ textChannelId: string
32
+ voiceChannelId: string | null
33
+ channelName: string
34
+ projectDirectory: string
35
+ sanitizedName: string
36
+ } | null> {
37
+ const sanitizedName = projectName
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9-]/g, '-')
40
+ .replace(/-+/g, '-')
41
+ .replace(/^-|-$/g, '')
42
+ .slice(0, 100)
43
+
44
+ if (!sanitizedName) {
45
+ return null
46
+ }
47
+
48
+ const projectsDir = getProjectsDir()
49
+ const projectDirectory = path.join(projectsDir, sanitizedName)
50
+
51
+ if (!fs.existsSync(projectsDir)) {
52
+ fs.mkdirSync(projectsDir, { recursive: true })
53
+ logger.log(`Created projects directory: ${projectsDir}`)
54
+ }
55
+
56
+ if (fs.existsSync(projectDirectory)) {
57
+ return null
58
+ }
59
+
60
+ fs.mkdirSync(projectDirectory, { recursive: true })
61
+ logger.log(`Created project directory: ${projectDirectory}`)
62
+
63
+ // Git init — gracefully skip if git is not installed
64
+ try {
65
+ await execAsync('git init', { cwd: projectDirectory, timeout: 10_000 })
66
+ logger.log(`Initialized git in: ${projectDirectory}`)
67
+ } catch (error) {
68
+ logger.warn(
69
+ `Could not initialize git in ${projectDirectory}: ${error instanceof Error ? error.message : String(error)}`,
70
+ )
71
+ }
72
+
73
+ const { textChannelId, voiceChannelId, channelName } =
74
+ await createProjectChannels({
75
+ guild,
76
+ projectDirectory,
77
+ botName,
78
+ })
79
+
80
+ return {
81
+ textChannelId,
82
+ voiceChannelId,
83
+ channelName,
84
+ projectDirectory,
85
+ sanitizedName,
86
+ }
87
+ }
88
+
89
+ export async function handleCreateNewProjectCommand({
90
+ command,
91
+ appId,
92
+ }: CommandContext): Promise<void> {
93
+ await command.deferReply()
94
+
95
+ const projectName = command.options.getString('name', true)
96
+ const guild = command.guild
97
+ const channel = command.channel
98
+
99
+ if (!guild) {
100
+ await command.editReply('This command can only be used in a guild')
101
+ return
102
+ }
103
+
104
+ if (!channel || channel.type !== ChannelType.GuildText) {
105
+ await command.editReply('This command can only be used in a text channel')
106
+ return
107
+ }
108
+
109
+ try {
110
+ const result = await createNewProject({
111
+ guild,
112
+ projectName,
113
+ botName: command.client.user?.username,
114
+ })
115
+
116
+ if (!result) {
117
+ const sanitizedName = projectName
118
+ .toLowerCase()
119
+ .replace(/[^a-z0-9-]/g, '-')
120
+ .replace(/-+/g, '-')
121
+ .replace(/^-|-$/g, '')
122
+ .slice(0, 100)
123
+
124
+ if (!sanitizedName) {
125
+ await command.editReply('Invalid project name')
126
+ return
127
+ }
128
+
129
+ const projectDirectory = path.join(getProjectsDir(), sanitizedName)
130
+ await command.editReply(
131
+ `Project directory already exists: ${projectDirectory}`,
132
+ )
133
+ return
134
+ }
135
+
136
+ const {
137
+ textChannelId,
138
+ voiceChannelId,
139
+ channelName,
140
+ projectDirectory,
141
+ sanitizedName,
142
+ } = result
143
+ const textChannel = (await guild.channels.fetch(
144
+ textChannelId,
145
+ )) as TextChannel
146
+
147
+ const voiceInfo = voiceChannelId ? `\nšŸ”Š Voice: <#${voiceChannelId}>` : ''
148
+ await command.editReply(
149
+ `āœ… Created new project **${sanitizedName}**\nšŸ“ Directory: \`${projectDirectory}\`\nšŸ“ Text: <#${textChannelId}>${voiceInfo}\n_Starting session..._`,
150
+ )
151
+
152
+ const starterMessage = await textChannel.send({
153
+ content: `šŸš€ **New project initialized**\nšŸ“ \`${projectDirectory}\``,
154
+ flags: SILENT_MESSAGE_FLAGS,
155
+ })
156
+
157
+ const thread = await starterMessage.startThread({
158
+ name: `Init: ${sanitizedName}`,
159
+ autoArchiveDuration: 1440,
160
+ reason: 'New project session',
161
+ })
162
+
163
+ // Add user to thread so it appears in their sidebar
164
+ await thread.members.add(command.user.id)
165
+
166
+ const runtime = getOrCreateRuntime({
167
+ threadId: thread.id,
168
+ thread,
169
+ projectDirectory,
170
+ sdkDirectory: projectDirectory,
171
+ channelId: textChannel.id,
172
+ appId,
173
+ })
174
+ await runtime.enqueueIncoming({
175
+ prompt:
176
+ 'The project was just initialized. Say hi and ask what the user wants to build.',
177
+ userId: command.user.id,
178
+ username: command.user.displayName,
179
+ appId,
180
+ mode: 'opencode',
181
+ })
182
+
183
+ logger.log(`Created new project ${channelName} at ${projectDirectory}`)
184
+ } catch (error) {
185
+ logger.error('[CREATE-NEW-PROJECT] Error:', error)
186
+ await command.editReply(
187
+ `Failed to create new project: ${error instanceof Error ? error.message : 'Unknown error'}`,
188
+ )
189
+ }
190
+ }
@@ -0,0 +1,91 @@
1
+ // /diff command - Show git diff as a shareable URL.
2
+
3
+ import {
4
+ ChannelType,
5
+ EmbedBuilder,
6
+ MessageFlags,
7
+ type TextChannel,
8
+ type ThreadChannel,
9
+ } from 'discord.js'
10
+ import path from 'node:path'
11
+ import type { CommandContext } from './types.js'
12
+ import {
13
+ resolveWorkingDirectory,
14
+ SILENT_MESSAGE_FLAGS,
15
+ } from '../discord-utils.js'
16
+ import { createLogger, LogPrefix } from '../logger.js'
17
+ import { uploadGitDiffViaCritique } from '../critique-utils.js'
18
+
19
+ const logger = createLogger(LogPrefix.DIFF)
20
+
21
+ export async function handleDiffCommand({
22
+ command,
23
+ }: CommandContext): Promise<void> {
24
+ const channel = command.channel
25
+
26
+ if (!channel) {
27
+ await command.reply({
28
+ content: 'This command can only be used in a channel',
29
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
30
+ })
31
+ return
32
+ }
33
+
34
+ const isThread = [
35
+ ChannelType.PublicThread,
36
+ ChannelType.PrivateThread,
37
+ ChannelType.AnnouncementThread,
38
+ ].includes(channel.type)
39
+
40
+ const isTextChannel = channel.type === ChannelType.GuildText
41
+
42
+ if (!isThread && !isTextChannel) {
43
+ await command.reply({
44
+ content: 'This command can only be used in a text channel or thread',
45
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
46
+ })
47
+ return
48
+ }
49
+
50
+ const resolved = await resolveWorkingDirectory({
51
+ channel: channel as TextChannel | ThreadChannel,
52
+ })
53
+
54
+ if (!resolved) {
55
+ await command.reply({
56
+ content: 'Could not determine project directory for this channel',
57
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
58
+ })
59
+ return
60
+ }
61
+
62
+ const { workingDirectory } = resolved
63
+
64
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
65
+
66
+ const projectName = path.basename(workingDirectory)
67
+ const title = `${projectName}: Discord /diff`
68
+ const result = await uploadGitDiffViaCritique({
69
+ title,
70
+ cwd: workingDirectory,
71
+ })
72
+
73
+ if (!result) {
74
+ await command.editReply({ content: 'No changes to show' })
75
+ return
76
+ }
77
+
78
+ if (result.error || !result.url) {
79
+ await command.editReply({ content: result.error || 'No changes to show' })
80
+ return
81
+ }
82
+
83
+ const imageUrl = `https://critique.work/og/${result.id}.png`
84
+ const embed = new EmbedBuilder()
85
+ .setTitle(title)
86
+ .setURL(result.url)
87
+ .setImage(imageUrl)
88
+
89
+ await command.editReply({ embeds: [embed] })
90
+ logger.log(`Diff shared: ${result.url}`)
91
+ }