@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.
- package/bin.js +2 -0
- package/dist/agent-model.e2e.test.js +755 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/anthropic-account-identity.js +62 -0
- package/dist/anthropic-account-identity.test.js +38 -0
- package/dist/anthropic-auth-plugin.js +917 -0
- package/dist/anthropic-auth-state.js +303 -0
- package/dist/anthropic-auth-state.test.js +150 -0
- package/dist/bin.js +152 -0
- package/dist/btw-prefix-detection.js +17 -0
- package/dist/btw-prefix-detection.test.js +63 -0
- package/dist/channel-management.js +259 -0
- package/dist/cli-parsing.test.js +142 -0
- package/dist/cli-send-thread.e2e.test.js +353 -0
- package/dist/cli-telegram-options.test.js +99 -0
- package/dist/cli.js +4210 -568
- package/dist/commands/abort.js +65 -0
- package/dist/commands/action-buttons.js +245 -0
- package/dist/commands/add-dir.js +124 -0
- package/dist/commands/add-dir.test.js +126 -0
- package/dist/commands/add-project.js +113 -0
- package/dist/commands/agent.js +355 -0
- package/dist/commands/ask-question.js +320 -0
- package/dist/commands/ask-question.test.js +92 -0
- package/dist/commands/btw.js +121 -0
- package/dist/commands/cli-commands-group-a.test.js +728 -0
- package/dist/commands/cli-commands-group-b.test.js +695 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +130 -0
- package/dist/commands/diff.js +63 -0
- package/dist/commands/discord-commands-group-a.test.js +621 -0
- package/dist/commands/discord-commands-group-b.test.js +595 -0
- package/dist/commands/discord-commands-group-c.test.js +739 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork-subagent.js +177 -0
- package/dist/commands/fork.js +262 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +887 -0
- package/dist/commands/mcp.js +239 -0
- package/dist/commands/memory-snapshot.js +24 -0
- package/dist/commands/mention-mode.js +44 -0
- package/dist/commands/merge-worktree.js +162 -0
- package/dist/commands/model-variant.js +366 -0
- package/dist/commands/model.js +794 -0
- package/dist/commands/new-worktree.js +465 -0
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/permissions.js +274 -0
- package/dist/commands/queue.js +223 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +127 -0
- package/dist/commands/resume.js +149 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/screenshare.js +303 -0
- package/dist/commands/screenshare.test.js +20 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +176 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/tasks.js +205 -0
- package/dist/commands/thread-deletion-sync.js +50 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +305 -0
- package/dist/commands/unset-model.js +139 -0
- package/dist/commands/upgrade.js +48 -0
- package/dist/commands/user-command.js +155 -0
- package/dist/commands/verbosity.js +125 -0
- package/dist/commands/vscode.js +269 -0
- package/dist/commands/worktree-settings.js +43 -0
- package/dist/commands/worktrees.js +468 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +100 -255
- package/dist/context-awareness-plugin.js +340 -0
- package/dist/context-awareness-plugin.test.js +126 -0
- package/dist/critique-utils.js +95 -0
- package/dist/database.js +1355 -0
- package/dist/db.js +260 -0
- package/dist/db.test.js +138 -0
- package/dist/debounce-timeout.js +28 -0
- package/dist/debounced-process-flush.js +77 -0
- package/dist/discord-bot.js +1124 -0
- package/dist/discord-command-registration.js +567 -0
- package/dist/discord-urls.js +82 -0
- package/dist/discord-utils.js +616 -0
- package/dist/discord-utils.test.js +134 -0
- package/dist/errors.js +157 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/event-stream-real-capture.e2e.test.js +533 -0
- package/dist/eventsource-parser.test.js +327 -0
- package/dist/exec-async.js +26 -0
- package/dist/external-opencode-sync.js +480 -0
- package/dist/format-tables.js +491 -0
- package/dist/format-tables.test.js +478 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +113 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
- package/dist/gateway-proxy.e2e.test.js +485 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +37 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +58 -0
- package/dist/generated/internal/class.js +49 -0
- package/dist/generated/internal/prismaNamespace.js +254 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_events.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +122 -0
- package/dist/hrana-server.js +251 -0
- package/dist/hrana-server.test.js +370 -0
- package/dist/html-actions.js +123 -0
- package/dist/html-actions.test.js +70 -0
- package/dist/html-components.js +117 -0
- package/dist/html-components.test.js +34 -0
- package/dist/image-optimizer-plugin.js +153 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +420 -0
- package/dist/ipc-polling.js +327 -0
- package/dist/ipc-tools-plugin.js +193 -0
- package/dist/ipc-utils.js +18 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +171 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +264 -0
- package/dist/memory-overview-plugin.js +128 -0
- package/dist/message-finish-field.e2e.test.js +168 -0
- package/dist/message-formatting.js +415 -0
- package/dist/message-formatting.test.js +115 -0
- package/dist/message-preprocessing.js +359 -0
- package/dist/onboarding-tutorial.js +163 -0
- package/dist/onboarding-welcome.js +37 -0
- package/dist/openai-realtime.js +224 -0
- package/dist/opencode-command-detection.js +65 -0
- package/dist/opencode-command-detection.test.js +240 -0
- package/dist/opencode-command.js +131 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +388 -0
- package/dist/opencode-interrupt-plugin.test.js +463 -0
- package/dist/opencode.js +1117 -0
- package/dist/otto/branding.js +22 -0
- package/dist/otto/index.js +21 -0
- package/dist/otto-digital-twin.e2e.test.js +161 -0
- package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
- package/dist/otto-opencode-plugin.js +21 -0
- package/dist/otto-opencode-plugin.test.js +98 -0
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/patch-text-parser.js +97 -0
- package/dist/plugin-logger.js +68 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/queue-advanced-abort.e2e.test.js +293 -0
- package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
- package/dist/queue-advanced-e2e-setup.js +790 -0
- package/dist/queue-advanced-footer.e2e.test.js +481 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
- package/dist/queue-advanced-question.e2e.test.js +261 -0
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
- package/dist/queue-advanced-typing.e2e.test.js +153 -0
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
- package/dist/queue-interrupt-drain.e2e.test.js +135 -0
- package/dist/queue-question-select-drain.e2e.test.js +256 -0
- package/dist/runtime-idle-sweeper.js +52 -0
- package/dist/runtime-lifecycle.e2e.test.js +514 -0
- package/dist/sentry.js +23 -0
- package/dist/session-handler/agent-utils.js +67 -0
- package/dist/session-handler/event-stream-state.js +475 -0
- package/dist/session-handler/event-stream-state.test.js +632 -0
- package/dist/session-handler/model-utils.js +147 -0
- package/dist/session-handler/opencode-session-event-log.js +94 -0
- package/dist/session-handler/thread-runtime-state.js +131 -0
- package/dist/session-handler/thread-session-runtime.js +3390 -0
- package/dist/session-handler.js +9 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/session-title-rename.test.js +92 -0
- package/dist/skill-filter.js +31 -0
- package/dist/skill-filter.test.js +65 -0
- package/dist/startup-service.js +153 -0
- package/dist/startup-time.e2e.test.js +296 -0
- package/dist/store.js +19 -0
- package/dist/subagent-rate-limit-plugin.js +175 -0
- package/dist/system-message.js +702 -0
- package/dist/system-message.test.js +697 -0
- package/dist/task-runner.js +530 -0
- package/dist/task-schedule.js +213 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/test-utils.js +313 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +1111 -0
- package/dist/tools.js +357 -0
- package/dist/undo-redo.e2e.test.js +161 -0
- package/dist/unnest-code-blocks.js +146 -0
- package/dist/unnest-code-blocks.test.js +673 -0
- package/dist/upgrade.js +156 -0
- package/dist/utils.js +172 -0
- package/dist/utils.test.js +130 -0
- package/dist/voice-attachment.js +34 -0
- package/dist/voice-handler.js +646 -0
- package/dist/voice-message.e2e.test.js +1021 -0
- package/dist/voice.js +456 -0
- package/dist/voice.test.js +235 -0
- package/dist/wait-session.js +171 -0
- package/dist/websockify.js +69 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-lifecycle.e2e.test.js +311 -0
- package/dist/worktree-utils.js +3 -0
- package/dist/worktrees.js +991 -0
- package/dist/worktrees.test.js +415 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +90 -38
- package/schema.prisma +303 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +112 -0
- package/skills/egaki/SKILL.md +100 -0
- package/skills/errore/SKILL.md +647 -0
- package/skills/event-sourcing-state/SKILL.md +252 -0
- package/skills/goke/SKILL.md +38 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/lintcn/SKILL.md +873 -0
- package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
- package/skills/new-skill/SKILL.md +237 -0
- package/skills/npm-package/SKILL.md +617 -0
- package/skills/opensrc/SKILL.md +78 -0
- package/skills/otto-publish/SKILL.md +61 -0
- package/skills/playwriter/SKILL.md +35 -0
- package/skills/profano/SKILL.md +16 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/sigillo/SKILL.md +101 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/spiceflow/SKILL.md +28 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +98 -0
- package/skills/usecomputer/SKILL.md +264 -0
- package/skills/x-articles/SKILL.md +554 -0
- package/skills/zele/SKILL.md +49 -0
- package/skills/zustand-centralized-state/SKILL.md +1004 -0
- package/src/agent-model.e2e.test.ts +979 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +283 -0
- package/src/ai-tool.ts +39 -0
- package/src/anthropic-account-identity.test.ts +52 -0
- package/src/anthropic-account-identity.ts +77 -0
- package/src/anthropic-auth-plugin.ts +1139 -0
- package/src/anthropic-auth-state.test.ts +187 -0
- package/src/anthropic-auth-state.ts +386 -0
- package/src/bin.ts +182 -0
- package/src/btw-prefix-detection.test.ts +73 -0
- package/src/btw-prefix-detection.ts +23 -0
- package/src/channel-management.ts +376 -0
- package/src/cli-parsing.test.ts +197 -0
- package/src/cli-send-thread.e2e.test.ts +463 -0
- package/src/cli-telegram-options.test.ts +114 -0
- package/src/cli.ts +5718 -580
- package/src/commands/abort.ts +89 -0
- package/src/commands/action-buttons.ts +364 -0
- package/src/commands/add-dir.test.ts +154 -0
- package/src/commands/add-dir.ts +175 -0
- package/src/commands/add-project.ts +149 -0
- package/src/commands/agent.ts +496 -0
- package/src/commands/ask-question.test.ts +111 -0
- package/src/commands/ask-question.ts +455 -0
- package/src/commands/btw.ts +184 -0
- package/src/commands/cli-commands-group-a.test.ts +837 -0
- package/src/commands/cli-commands-group-b.test.ts +800 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +190 -0
- package/src/commands/diff.ts +91 -0
- package/src/commands/discord-commands-group-a.test.ts +751 -0
- package/src/commands/discord-commands-group-b.test.ts +648 -0
- package/src/commands/discord-commands-group-c.test.ts +882 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork-subagent.ts +263 -0
- package/src/commands/fork.ts +386 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +1175 -0
- package/src/commands/mcp.ts +307 -0
- package/src/commands/memory-snapshot.ts +30 -0
- package/src/commands/mention-mode.ts +68 -0
- package/src/commands/merge-worktree.ts +226 -0
- package/src/commands/model-variant.ts +485 -0
- package/src/commands/model.ts +1078 -0
- package/src/commands/new-worktree.ts +645 -0
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/permissions.ts +397 -0
- package/src/commands/queue.ts +293 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +230 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/screenshare.test.ts +30 -0
- package/src/commands/screenshare.ts +366 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +227 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/tasks.ts +293 -0
- package/src/commands/thread-deletion-sync.ts +80 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +386 -0
- package/src/commands/unset-model.ts +174 -0
- package/src/commands/upgrade.ts +59 -0
- package/src/commands/user-command.ts +198 -0
- package/src/commands/verbosity.ts +173 -0
- package/src/commands/vscode.ts +342 -0
- package/src/commands/worktree-settings.ts +70 -0
- package/src/commands/worktrees.ts +645 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +103 -339
- package/src/context-awareness-plugin.test.ts +144 -0
- package/src/context-awareness-plugin.ts +469 -0
- package/src/critique-utils.ts +139 -0
- package/src/database.ts +1949 -0
- package/src/db.test.ts +162 -0
- package/src/db.ts +295 -0
- package/src/debounce-timeout.ts +43 -0
- package/src/debounced-process-flush.ts +104 -0
- package/src/discord-bot.ts +1505 -0
- package/src/discord-command-registration.ts +752 -0
- package/src/discord-urls.ts +89 -0
- package/src/discord-utils.test.ts +153 -0
- package/src/discord-utils.ts +846 -0
- package/src/errors.ts +201 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/event-stream-real-capture.e2e.test.ts +692 -0
- package/src/eventsource-parser.test.ts +351 -0
- package/src/exec-async.ts +35 -0
- package/src/external-opencode-sync.ts +685 -0
- package/src/format-tables.test.ts +515 -0
- package/src/format-tables.ts +718 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +172 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
- package/src/gateway-proxy.e2e.test.ts +644 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +114 -0
- package/src/generated/client.ts +138 -0
- package/src/generated/commonInputTypes.ts +770 -0
- package/src/generated/enums.ts +98 -0
- package/src/generated/internal/class.ts +384 -0
- package/src/generated/internal/prismaNamespace.ts +2394 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1700 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +1859 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1228 -0
- package/src/generated/models/channel_worktrees.ts +1300 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_events.ts +1439 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1781 -0
- package/src/generated/models/thread_worktrees.ts +1356 -0
- package/src/generated/models.ts +30 -0
- package/src/heap-monitor.ts +152 -0
- package/src/hrana-server.test.ts +434 -0
- package/src/hrana-server.ts +299 -0
- package/src/html-actions.test.ts +87 -0
- package/src/html-actions.ts +174 -0
- package/src/html-components.test.ts +38 -0
- package/src/html-components.ts +181 -0
- package/src/image-optimizer-plugin.ts +194 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +610 -0
- package/src/ipc-polling.ts +427 -0
- package/src/ipc-tools-plugin.ts +236 -0
- package/src/ipc-utils.ts +29 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +215 -0
- package/src/markdown.test.ts +315 -0
- package/src/markdown.ts +410 -0
- package/src/memory-overview-plugin.ts +163 -0
- package/src/message-finish-field.e2e.test.ts +195 -0
- package/src/message-formatting.test.ts +126 -0
- package/src/message-formatting.ts +535 -0
- package/src/message-preprocessing.ts +488 -0
- package/src/onboarding-tutorial.ts +167 -0
- package/src/onboarding-welcome.ts +49 -0
- package/src/openai-realtime.ts +358 -0
- package/src/opencode-command-detection.test.ts +307 -0
- package/src/opencode-command-detection.ts +76 -0
- package/src/opencode-command.test.ts +70 -0
- package/src/opencode-command.ts +191 -0
- package/src/opencode-interrupt-plugin.test.ts +682 -0
- package/src/opencode-interrupt-plugin.ts +507 -0
- package/src/opencode.ts +1453 -0
- package/src/otto/branding.ts +23 -0
- package/src/otto/index.ts +22 -0
- package/src/otto-digital-twin.e2e.test.ts +199 -0
- package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
- package/src/otto-opencode-plugin.test.ts +108 -0
- package/src/otto-opencode-plugin.ts +22 -0
- package/src/parse-permission-rules.test.ts +127 -0
- package/src/patch-text-parser.ts +107 -0
- package/src/plugin-logger.ts +84 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/queue-advanced-abort.e2e.test.ts +382 -0
- package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
- package/src/queue-advanced-e2e-setup.ts +877 -0
- package/src/queue-advanced-footer.e2e.test.ts +591 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
- package/src/queue-advanced-question.e2e.test.ts +316 -0
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
- package/src/queue-advanced-typing.e2e.test.ts +199 -0
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
- package/src/queue-interrupt-drain.e2e.test.ts +166 -0
- package/src/queue-question-select-drain.e2e.test.ts +327 -0
- package/src/runtime-idle-sweeper.ts +76 -0
- package/src/runtime-lifecycle.e2e.test.ts +651 -0
- package/src/schema.sql +174 -0
- package/src/sentry.ts +26 -0
- package/src/session-handler/agent-utils.ts +99 -0
- package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
- package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
- package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
- package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
- package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
- package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
- package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
- package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
- package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
- package/src/session-handler/event-stream-state.test.ts +717 -0
- package/src/session-handler/event-stream-state.ts +706 -0
- package/src/session-handler/model-utils.ts +217 -0
- package/src/session-handler/opencode-session-event-log.ts +130 -0
- package/src/session-handler/thread-runtime-state.ts +247 -0
- package/src/session-handler/thread-session-runtime.ts +4440 -0
- package/src/session-handler.ts +15 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/session-title-rename.test.ts +130 -0
- package/src/skill-filter.test.ts +83 -0
- package/src/skill-filter.ts +42 -0
- package/src/startup-service.ts +200 -0
- package/src/startup-time.e2e.test.ts +373 -0
- package/src/store.ts +139 -0
- package/src/subagent-rate-limit-plugin.ts +218 -0
- package/src/system-message.test.ts +710 -0
- package/src/system-message.ts +814 -0
- package/src/task-runner.ts +725 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +317 -0
- package/src/test-utils.ts +451 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +1350 -0
- package/src/tools.ts +430 -0
- package/src/undici.d.ts +12 -0
- package/src/undo-redo.e2e.test.ts +209 -0
- package/src/unnest-code-blocks.test.ts +713 -0
- package/src/unnest-code-blocks.ts +185 -0
- package/src/upgrade.ts +185 -0
- package/src/utils.test.ts +155 -0
- package/src/utils.ts +265 -0
- package/src/voice-attachment.ts +51 -0
- package/src/voice-handler.ts +908 -0
- package/src/voice-message.e2e.test.ts +1255 -0
- package/src/voice.test.ts +281 -0
- package/src/voice.ts +638 -0
- package/src/wait-session.ts +273 -0
- package/src/websockify.ts +101 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-lifecycle.e2e.test.ts +396 -0
- package/src/worktree-utils.ts +4 -0
- package/src/worktrees.test.ts +489 -0
- package/src/worktrees.ts +1370 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
- package/README.md +0 -142
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config.d.ts +0 -39
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -202
- package/dist/config.test.js.map +0 -1
- package/dist/detect.d.ts +0 -9
- package/dist/detect.d.ts.map +0 -1
- package/dist/detect.js +0 -40
- package/dist/detect.js.map +0 -1
- package/dist/detect.test.d.ts +0 -2
- package/dist/detect.test.d.ts.map +0 -1
- package/dist/detect.test.js +0 -26
- package/dist/detect.test.js.map +0 -1
- package/dist/docker.d.ts +0 -7
- package/dist/docker.d.ts.map +0 -1
- package/dist/docker.js +0 -17
- package/dist/docker.js.map +0 -1
- package/dist/docker.test.d.ts +0 -2
- package/dist/docker.test.d.ts.map +0 -1
- package/dist/docker.test.js +0 -12
- package/dist/docker.test.js.map +0 -1
- package/dist/health.d.ts +0 -31
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -117
- package/dist/health.js.map +0 -1
- package/dist/health.test.d.ts +0 -2
- package/dist/health.test.d.ts.map +0 -1
- package/dist/health.test.js +0 -52
- package/dist/health.test.js.map +0 -1
- package/dist/index.d.ts +0 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -15
- package/dist/index.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -8
- package/dist/index.test.js.map +0 -1
- package/dist/installer.d.ts +0 -10
- package/dist/installer.d.ts.map +0 -1
- package/dist/installer.js +0 -50
- package/dist/installer.js.map +0 -1
- package/dist/installer.test.d.ts +0 -2
- package/dist/installer.test.d.ts.map +0 -1
- package/dist/installer.test.js +0 -43
- package/dist/installer.test.js.map +0 -1
- package/dist/lifecycle.d.ts +0 -10
- package/dist/lifecycle.d.ts.map +0 -1
- package/dist/lifecycle.js +0 -45
- package/dist/lifecycle.js.map +0 -1
- package/dist/lifecycle.test.d.ts +0 -2
- package/dist/lifecycle.test.d.ts.map +0 -1
- package/dist/lifecycle.test.js +0 -20
- package/dist/lifecycle.test.js.map +0 -1
- package/dist/manifest.d.ts +0 -18
- package/dist/manifest.d.ts.map +0 -1
- package/dist/manifest.js +0 -30
- package/dist/manifest.js.map +0 -1
- package/dist/skills-baseline.d.ts +0 -7
- package/dist/skills-baseline.d.ts.map +0 -1
- package/dist/skills-baseline.js +0 -9
- package/dist/skills-baseline.js.map +0 -1
- package/dist/skills.d.ts +0 -110
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js +0 -429
- package/dist/skills.js.map +0 -1
- package/dist/skills.test.d.ts +0 -2
- package/dist/skills.test.d.ts.map +0 -1
- package/dist/skills.test.js +0 -416
- package/dist/skills.test.js.map +0 -1
- package/dist/sync.d.ts +0 -10
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -39
- package/dist/sync.js.map +0 -1
- package/dist/tenant.d.ts +0 -13
- package/dist/tenant.d.ts.map +0 -1
- package/dist/tenant.js +0 -105
- package/dist/tenant.js.map +0 -1
- package/dist/tenant.test.d.ts +0 -2
- package/dist/tenant.test.d.ts.map +0 -1
- package/dist/tenant.test.js +0 -37
- package/dist/tenant.test.js.map +0 -1
- package/src/config.test.ts +0 -237
- package/src/detect.test.ts +0 -29
- package/src/detect.ts +0 -52
- package/src/docker.test.ts +0 -12
- package/src/docker.ts +0 -23
- package/src/health.test.ts +0 -61
- package/src/health.ts +0 -158
- package/src/index.test.ts +0 -8
- package/src/index.ts +0 -62
- package/src/installer.test.ts +0 -52
- package/src/installer.ts +0 -62
- package/src/lifecycle.test.ts +0 -23
- package/src/lifecycle.ts +0 -49
- package/src/manifest.ts +0 -42
- package/src/skills-baseline.ts +0 -14
- package/src/skills.test.ts +0 -503
- package/src/skills.ts +0 -512
- package/src/sync.ts +0 -53
- package/src/tenant.test.ts +0 -49
- package/src/tenant.ts +0 -120
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// User-defined OpenCode command handler.
|
|
2
|
+
// Handles slash commands that map to user-configured commands in opencode.json.
|
|
3
|
+
|
|
4
|
+
import type { CommandContext, CommandHandler } from './types.js'
|
|
5
|
+
import {
|
|
6
|
+
ChannelType,
|
|
7
|
+
MessageFlags,
|
|
8
|
+
type TextChannel,
|
|
9
|
+
type ThreadChannel,
|
|
10
|
+
} from 'discord.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
|
+
import { getChannelDirectory, getThreadSession } from '../database.js'
|
|
15
|
+
import { store } from '../store.js'
|
|
16
|
+
import fs from 'node:fs'
|
|
17
|
+
|
|
18
|
+
const userCommandLogger = createLogger(LogPrefix.USER_CMD)
|
|
19
|
+
const DISCORD_MESSAGE_LIMIT = 2000
|
|
20
|
+
const DISCORD_THREAD_NAME_LIMIT = 100
|
|
21
|
+
|
|
22
|
+
export const handleUserCommand: CommandHandler = async ({
|
|
23
|
+
command,
|
|
24
|
+
appId,
|
|
25
|
+
}: CommandContext) => {
|
|
26
|
+
const discordCommandName = command.commandName
|
|
27
|
+
// Look up the original OpenCode command name from the mapping populated at registration.
|
|
28
|
+
// The sanitized Discord name is lossy (e.g. foo:bar → foo-bar), so resolving from
|
|
29
|
+
// the exact registered slash command name avoids collisions.
|
|
30
|
+
const registered = store.getState().registeredUserCommands.find(
|
|
31
|
+
(c) => c.discordCommandName === discordCommandName,
|
|
32
|
+
)
|
|
33
|
+
const fallbackBase = discordCommandName.replace(/-(cmd|skill|mcp-prompt)$/, '')
|
|
34
|
+
const commandName = registered?.name || fallbackBase
|
|
35
|
+
const args = command.options.getString('arguments') || ''
|
|
36
|
+
const commandInvocation = args ? `/${commandName} ${args}` : `/${commandName}`
|
|
37
|
+
const threadOpeningMessage =
|
|
38
|
+
commandInvocation.length <= DISCORD_MESSAGE_LIMIT
|
|
39
|
+
? commandInvocation
|
|
40
|
+
: `${commandInvocation.slice(0, DISCORD_MESSAGE_LIMIT - 14)}... truncated`
|
|
41
|
+
|
|
42
|
+
userCommandLogger.log(
|
|
43
|
+
`Executing /${commandName} (from /${discordCommandName}) argsLength=${args.length}`,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const channel = command.channel
|
|
47
|
+
|
|
48
|
+
userCommandLogger.log(
|
|
49
|
+
`Channel info: type=${channel?.type}, id=${channel?.id}, isNull=${channel === null}`,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const isThread =
|
|
53
|
+
channel &&
|
|
54
|
+
[
|
|
55
|
+
ChannelType.PublicThread,
|
|
56
|
+
ChannelType.PrivateThread,
|
|
57
|
+
ChannelType.AnnouncementThread,
|
|
58
|
+
].includes(channel.type)
|
|
59
|
+
|
|
60
|
+
const isTextChannel = channel?.type === ChannelType.GuildText
|
|
61
|
+
|
|
62
|
+
if (!channel || (!isTextChannel && !isThread)) {
|
|
63
|
+
await command.reply({
|
|
64
|
+
content: 'This command can only be used in text channels or threads',
|
|
65
|
+
flags: MessageFlags.Ephemeral,
|
|
66
|
+
})
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let projectDirectory: string | undefined
|
|
71
|
+
let textChannel: TextChannel | null = null
|
|
72
|
+
let thread: ThreadChannel | null = null
|
|
73
|
+
|
|
74
|
+
if (isThread) {
|
|
75
|
+
// Running in an existing thread - get project directory from parent channel
|
|
76
|
+
thread = channel as ThreadChannel
|
|
77
|
+
textChannel = thread.parent as TextChannel | null
|
|
78
|
+
|
|
79
|
+
// Verify this thread has an existing session
|
|
80
|
+
const sessionId = await getThreadSession(thread.id)
|
|
81
|
+
|
|
82
|
+
if (!sessionId) {
|
|
83
|
+
await command.reply({
|
|
84
|
+
content:
|
|
85
|
+
'This thread does not have an active session. Use this command in a project channel to create a new thread.',
|
|
86
|
+
flags: MessageFlags.Ephemeral,
|
|
87
|
+
})
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (textChannel) {
|
|
92
|
+
const channelConfig = await getChannelDirectory(textChannel.id)
|
|
93
|
+
projectDirectory = channelConfig?.directory
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// Running in a text channel - will create a new thread
|
|
97
|
+
textChannel = channel as TextChannel
|
|
98
|
+
|
|
99
|
+
const channelConfig = await getChannelDirectory(textChannel.id)
|
|
100
|
+
projectDirectory = channelConfig?.directory
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!projectDirectory) {
|
|
104
|
+
await command.reply({
|
|
105
|
+
content: 'This channel is not configured with a project directory',
|
|
106
|
+
flags: MessageFlags.Ephemeral,
|
|
107
|
+
})
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(projectDirectory)) {
|
|
112
|
+
await command.reply({
|
|
113
|
+
content: `Directory does not exist: ${projectDirectory}`,
|
|
114
|
+
flags: MessageFlags.Ephemeral,
|
|
115
|
+
})
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await command.deferReply()
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Use the dedicated session.command API instead of formatting as text prompt
|
|
123
|
+
const commandPayload = { name: commandName, arguments: args }
|
|
124
|
+
|
|
125
|
+
if (isThread && thread) {
|
|
126
|
+
// Running in existing thread - just send the command
|
|
127
|
+
await command.editReply(`Running ${commandInvocation}...`)
|
|
128
|
+
|
|
129
|
+
const runtime = getOrCreateRuntime({
|
|
130
|
+
threadId: thread.id,
|
|
131
|
+
thread,
|
|
132
|
+
projectDirectory,
|
|
133
|
+
sdkDirectory: projectDirectory,
|
|
134
|
+
channelId: textChannel?.id,
|
|
135
|
+
appId,
|
|
136
|
+
})
|
|
137
|
+
await runtime.enqueueIncoming({
|
|
138
|
+
prompt: '',
|
|
139
|
+
userId: command.user.id,
|
|
140
|
+
username: command.user.displayName,
|
|
141
|
+
command: commandPayload,
|
|
142
|
+
appId,
|
|
143
|
+
mode: 'local-queue',
|
|
144
|
+
})
|
|
145
|
+
} else if (textChannel) {
|
|
146
|
+
// Running in text channel - create a new thread
|
|
147
|
+
const starterMessage = await textChannel.send({
|
|
148
|
+
content: threadOpeningMessage,
|
|
149
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const newThread = await starterMessage.startThread({
|
|
153
|
+
name: commandInvocation.slice(0, DISCORD_THREAD_NAME_LIMIT),
|
|
154
|
+
autoArchiveDuration: 1440,
|
|
155
|
+
reason: `OpenCode command: ${commandName}`,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Add user to thread so it appears in their sidebar
|
|
159
|
+
await newThread.members.add(command.user.id)
|
|
160
|
+
|
|
161
|
+
await command.editReply(
|
|
162
|
+
`Started /${commandName} in ${newThread.toString()}`,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
const runtime = getOrCreateRuntime({
|
|
166
|
+
threadId: newThread.id,
|
|
167
|
+
thread: newThread,
|
|
168
|
+
projectDirectory,
|
|
169
|
+
sdkDirectory: projectDirectory,
|
|
170
|
+
channelId: textChannel.id,
|
|
171
|
+
appId,
|
|
172
|
+
})
|
|
173
|
+
await runtime.enqueueIncoming({
|
|
174
|
+
prompt: '',
|
|
175
|
+
userId: command.user.id,
|
|
176
|
+
username: command.user.displayName,
|
|
177
|
+
command: commandPayload,
|
|
178
|
+
appId,
|
|
179
|
+
mode: 'local-queue',
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
userCommandLogger.error(`Error executing /${commandName}:`, error)
|
|
184
|
+
|
|
185
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
186
|
+
|
|
187
|
+
if (command.deferred) {
|
|
188
|
+
await command.editReply({
|
|
189
|
+
content: `Failed to execute /${commandName}: ${errorMessage}`,
|
|
190
|
+
})
|
|
191
|
+
} else {
|
|
192
|
+
await command.reply({
|
|
193
|
+
content: `Failed to execute /${commandName}: ${errorMessage}`,
|
|
194
|
+
flags: MessageFlags.Ephemeral,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// /verbosity command.
|
|
2
|
+
// Shows a dropdown to set output verbosity level for sessions in a channel.
|
|
3
|
+
// 'text_and_essential_tools' (default): shows text and essential tools (edits, custom MCP tools)
|
|
4
|
+
// 'tools_and_text': shows all output including tool executions
|
|
5
|
+
// 'text_only': only shows text responses
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ChatInputCommandInteraction,
|
|
9
|
+
StringSelectMenuInteraction,
|
|
10
|
+
StringSelectMenuBuilder,
|
|
11
|
+
ActionRowBuilder,
|
|
12
|
+
MessageFlags,
|
|
13
|
+
ChannelType,
|
|
14
|
+
type ThreadChannel,
|
|
15
|
+
} from 'discord.js'
|
|
16
|
+
import {
|
|
17
|
+
getChannelVerbosity,
|
|
18
|
+
setChannelVerbosity,
|
|
19
|
+
type VerbosityLevel,
|
|
20
|
+
} from '../database.js'
|
|
21
|
+
import { getPrisma } from '../db.js'
|
|
22
|
+
import { store } from '../store.js'
|
|
23
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
24
|
+
|
|
25
|
+
const verbosityLogger = createLogger(LogPrefix.VERBOSITY)
|
|
26
|
+
|
|
27
|
+
const VERBOSITY_OPTIONS: Array<{
|
|
28
|
+
value: VerbosityLevel
|
|
29
|
+
label: string
|
|
30
|
+
description: string
|
|
31
|
+
}> = [
|
|
32
|
+
{
|
|
33
|
+
value: 'tools_and_text',
|
|
34
|
+
label: 'Tools and text',
|
|
35
|
+
description: 'All output including tool executions and status messages',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
value: 'text_and_essential_tools',
|
|
39
|
+
label: 'Text and essential tools',
|
|
40
|
+
description: 'Text + essential tools (edits, custom MCP). Hides read/search.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
value: 'text_only',
|
|
44
|
+
label: 'Text only',
|
|
45
|
+
description: 'Only text responses. Hides all tools and status messages.',
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
function resolveChannelId(channel: ChatInputCommandInteraction['channel']): string | null {
|
|
50
|
+
if (!channel) {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
if (channel.type === ChannelType.GuildText) {
|
|
54
|
+
return channel.id
|
|
55
|
+
}
|
|
56
|
+
if (
|
|
57
|
+
channel.type === ChannelType.PublicThread ||
|
|
58
|
+
channel.type === ChannelType.PrivateThread ||
|
|
59
|
+
channel.type === ChannelType.AnnouncementThread
|
|
60
|
+
) {
|
|
61
|
+
return (channel as ThreadChannel).parentId || channel.id
|
|
62
|
+
}
|
|
63
|
+
return channel.id
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if there is a per-channel verbosity override in the DB.
|
|
68
|
+
* Returns the override value if it exists, null otherwise.
|
|
69
|
+
*/
|
|
70
|
+
async function getChannelVerbosityOverride(
|
|
71
|
+
channelId: string,
|
|
72
|
+
): Promise<VerbosityLevel | null> {
|
|
73
|
+
const prisma = await getPrisma()
|
|
74
|
+
const row = await prisma.channel_verbosity.findUnique({
|
|
75
|
+
where: { channel_id: channelId },
|
|
76
|
+
})
|
|
77
|
+
if (row?.verbosity) {
|
|
78
|
+
return row.verbosity as VerbosityLevel
|
|
79
|
+
}
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Handle the /verbosity slash command.
|
|
85
|
+
* Shows a dropdown with the current verbosity level and available options.
|
|
86
|
+
*/
|
|
87
|
+
export async function handleVerbosityCommand({
|
|
88
|
+
command,
|
|
89
|
+
}: {
|
|
90
|
+
command: ChatInputCommandInteraction
|
|
91
|
+
appId: string
|
|
92
|
+
}): Promise<void> {
|
|
93
|
+
verbosityLogger.log('[VERBOSITY] Command called')
|
|
94
|
+
|
|
95
|
+
const channelId = resolveChannelId(command.channel)
|
|
96
|
+
if (!channelId) {
|
|
97
|
+
await command.reply({
|
|
98
|
+
content: 'Could not determine channel.',
|
|
99
|
+
flags: MessageFlags.Ephemeral,
|
|
100
|
+
})
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const override = await getChannelVerbosityOverride(channelId)
|
|
105
|
+
const currentLevel = override || store.getState().defaultVerbosity
|
|
106
|
+
const source = override ? 'channel override' : 'global default'
|
|
107
|
+
|
|
108
|
+
const options = VERBOSITY_OPTIONS.map((opt) => ({
|
|
109
|
+
label: opt.label,
|
|
110
|
+
value: opt.value,
|
|
111
|
+
description: opt.description,
|
|
112
|
+
default: opt.value === currentLevel,
|
|
113
|
+
}))
|
|
114
|
+
|
|
115
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
116
|
+
.setCustomId(`verbosity_select:${channelId}`)
|
|
117
|
+
.setPlaceholder('Select verbosity level')
|
|
118
|
+
.addOptions(options)
|
|
119
|
+
|
|
120
|
+
const actionRow =
|
|
121
|
+
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
|
|
122
|
+
|
|
123
|
+
await command.reply({
|
|
124
|
+
content: `**Verbosity**\nCurrent: \`${currentLevel}\` (${source})`,
|
|
125
|
+
components: [actionRow],
|
|
126
|
+
flags: MessageFlags.Ephemeral,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Handle the verbosity select menu interaction.
|
|
132
|
+
* Sets the selected verbosity level for the channel.
|
|
133
|
+
*/
|
|
134
|
+
export async function handleVerbositySelectMenu(
|
|
135
|
+
interaction: StringSelectMenuInteraction,
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
const customId = interaction.customId
|
|
138
|
+
if (!customId.startsWith('verbosity_select:')) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await interaction.deferUpdate()
|
|
143
|
+
|
|
144
|
+
const channelId = customId.replace('verbosity_select:', '')
|
|
145
|
+
const level = interaction.values[0] as VerbosityLevel | undefined
|
|
146
|
+
|
|
147
|
+
if (!level) {
|
|
148
|
+
await interaction.editReply({
|
|
149
|
+
content: 'No level selected.',
|
|
150
|
+
components: [],
|
|
151
|
+
})
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const currentLevel = await getChannelVerbosity(channelId)
|
|
156
|
+
if (currentLevel === level) {
|
|
157
|
+
await interaction.editReply({
|
|
158
|
+
content: `Verbosity is already \`${level}\` for this channel.`,
|
|
159
|
+
components: [],
|
|
160
|
+
})
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await setChannelVerbosity(channelId, level)
|
|
165
|
+
verbosityLogger.log(`[VERBOSITY] Set channel ${channelId} to ${level}`)
|
|
166
|
+
|
|
167
|
+
const description = VERBOSITY_OPTIONS.find((o) => o.value === level)?.description || ''
|
|
168
|
+
|
|
169
|
+
await interaction.editReply({
|
|
170
|
+
content: `Verbosity set to \`${level}\` for this channel.\n${description}\nApplies immediately, including active sessions.`,
|
|
171
|
+
components: [],
|
|
172
|
+
})
|
|
173
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
3
|
+
import net from 'node:net'
|
|
4
|
+
import {
|
|
5
|
+
ChannelType,
|
|
6
|
+
MessageFlags,
|
|
7
|
+
type TextChannel,
|
|
8
|
+
type ThreadChannel,
|
|
9
|
+
} from 'discord.js'
|
|
10
|
+
import { TunnelClient } from 'traforo/client'
|
|
11
|
+
import type { CommandContext } from './types.js'
|
|
12
|
+
import {
|
|
13
|
+
resolveWorkingDirectory,
|
|
14
|
+
SILENT_MESSAGE_FLAGS,
|
|
15
|
+
} from '../discord-utils.js'
|
|
16
|
+
import { createLogger } from '../logger.js'
|
|
17
|
+
|
|
18
|
+
const logger = createLogger('VSCODE')
|
|
19
|
+
const SECURE_REPLY_FLAGS = MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS
|
|
20
|
+
const MAX_SESSION_MINUTES = 30
|
|
21
|
+
const MAX_SESSION_MS = MAX_SESSION_MINUTES * 60 * 1000
|
|
22
|
+
const TUNNEL_BASE_DOMAIN = 'otto.dev'
|
|
23
|
+
const TUNNEL_ID_BYTES = 16
|
|
24
|
+
const READY_TIMEOUT_MS = 60_000
|
|
25
|
+
const LOCAL_HOST = '127.0.0.1'
|
|
26
|
+
|
|
27
|
+
export type VscodeSession = {
|
|
28
|
+
coderaftProcess: ChildProcess
|
|
29
|
+
tunnelClient: TunnelClient
|
|
30
|
+
url: string
|
|
31
|
+
workingDirectory: string
|
|
32
|
+
startedBy: string
|
|
33
|
+
startedAt: number
|
|
34
|
+
timeoutTimer: ReturnType<typeof setTimeout>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const activeSessions = new Map<string, VscodeSession>()
|
|
38
|
+
|
|
39
|
+
export function createVscodeTunnelId(): string {
|
|
40
|
+
return crypto.randomBytes(TUNNEL_ID_BYTES).toString('hex')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildCoderaftArgs({
|
|
44
|
+
port,
|
|
45
|
+
workingDirectory,
|
|
46
|
+
}: {
|
|
47
|
+
port: number
|
|
48
|
+
workingDirectory: string
|
|
49
|
+
}): string[] {
|
|
50
|
+
return [
|
|
51
|
+
'coderaft',
|
|
52
|
+
'--port',
|
|
53
|
+
String(port),
|
|
54
|
+
'--host',
|
|
55
|
+
LOCAL_HOST,
|
|
56
|
+
'--without-connection-token',
|
|
57
|
+
'--disable-workspace-trust',
|
|
58
|
+
'--default-folder',
|
|
59
|
+
workingDirectory,
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createPortWaiter({
|
|
64
|
+
port,
|
|
65
|
+
process: proc,
|
|
66
|
+
timeoutMs,
|
|
67
|
+
}: {
|
|
68
|
+
port: number
|
|
69
|
+
process: ChildProcess
|
|
70
|
+
timeoutMs: number
|
|
71
|
+
}): Promise<void> {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const maxAttempts = Math.ceil(timeoutMs / 100)
|
|
74
|
+
let attempts = 0
|
|
75
|
+
|
|
76
|
+
const check = (): void => {
|
|
77
|
+
if (proc.exitCode !== null) {
|
|
78
|
+
reject(new Error(`coderaft exited with code ${proc.exitCode} before becoming ready`))
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const socket = net.createConnection(port, LOCAL_HOST)
|
|
83
|
+
socket.on('connect', () => {
|
|
84
|
+
socket.destroy()
|
|
85
|
+
resolve()
|
|
86
|
+
})
|
|
87
|
+
socket.on('error', () => {
|
|
88
|
+
socket.destroy()
|
|
89
|
+
attempts += 1
|
|
90
|
+
if (attempts >= maxAttempts) {
|
|
91
|
+
reject(new Error(`Port ${port} not reachable after ${timeoutMs}ms`))
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
setTimeout(check, 100)
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
check()
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getAvailablePort(): Promise<number> {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
const server = net.createServer()
|
|
105
|
+
server.on('error', reject)
|
|
106
|
+
server.listen(0, LOCAL_HOST, () => {
|
|
107
|
+
const address = server.address()
|
|
108
|
+
if (!address || typeof address === 'string') {
|
|
109
|
+
server.close(() => {
|
|
110
|
+
reject(new Error('Failed to resolve an available port'))
|
|
111
|
+
})
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
const port = address.port
|
|
115
|
+
server.close((error) => {
|
|
116
|
+
if (error) {
|
|
117
|
+
reject(error)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
resolve(port)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cleanupSession(session: VscodeSession): void {
|
|
127
|
+
clearTimeout(session.timeoutTimer)
|
|
128
|
+
try {
|
|
129
|
+
session.tunnelClient.close()
|
|
130
|
+
} catch {}
|
|
131
|
+
if (session.coderaftProcess.exitCode === null) {
|
|
132
|
+
try {
|
|
133
|
+
session.coderaftProcess.kill('SIGTERM')
|
|
134
|
+
} catch {}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getActiveVscodeSession({ sessionKey }: { sessionKey: string }): VscodeSession | undefined {
|
|
139
|
+
return activeSessions.get(sessionKey)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function stopVscode({ sessionKey }: { sessionKey: string }): boolean {
|
|
143
|
+
const session = activeSessions.get(sessionKey)
|
|
144
|
+
if (!session) {
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
activeSessions.delete(sessionKey)
|
|
149
|
+
cleanupSession(session)
|
|
150
|
+
logger.log(`VS Code stopped (key: ${sessionKey})`)
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function startVscode({
|
|
155
|
+
sessionKey,
|
|
156
|
+
startedBy,
|
|
157
|
+
workingDirectory,
|
|
158
|
+
}: {
|
|
159
|
+
sessionKey: string
|
|
160
|
+
startedBy: string
|
|
161
|
+
workingDirectory: string
|
|
162
|
+
}): Promise<VscodeSession> {
|
|
163
|
+
const existing = activeSessions.get(sessionKey)
|
|
164
|
+
if (existing) {
|
|
165
|
+
return existing
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const port = await getAvailablePort()
|
|
169
|
+
const tunnelId = createVscodeTunnelId()
|
|
170
|
+
const args = buildCoderaftArgs({
|
|
171
|
+
port,
|
|
172
|
+
workingDirectory,
|
|
173
|
+
})
|
|
174
|
+
const coderaftProcess = spawn('bunx', args, {
|
|
175
|
+
cwd: workingDirectory,
|
|
176
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
177
|
+
env: {
|
|
178
|
+
...process.env,
|
|
179
|
+
PORT: String(port),
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
coderaftProcess.stdout?.on('data', (data: Buffer) => {
|
|
184
|
+
logger.log(data.toString().trim())
|
|
185
|
+
})
|
|
186
|
+
coderaftProcess.stderr?.on('data', (data: Buffer) => {
|
|
187
|
+
logger.error(data.toString().trim())
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await createPortWaiter({
|
|
192
|
+
port,
|
|
193
|
+
process: coderaftProcess,
|
|
194
|
+
timeoutMs: READY_TIMEOUT_MS,
|
|
195
|
+
})
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (coderaftProcess.exitCode === null) {
|
|
198
|
+
coderaftProcess.kill('SIGTERM')
|
|
199
|
+
}
|
|
200
|
+
throw error
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const tunnelClient = new TunnelClient({
|
|
204
|
+
localPort: port,
|
|
205
|
+
localHost: LOCAL_HOST,
|
|
206
|
+
tunnelId,
|
|
207
|
+
baseDomain: TUNNEL_BASE_DOMAIN,
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
await Promise.race([
|
|
212
|
+
tunnelClient.connect(),
|
|
213
|
+
new Promise<never>((_, reject) => {
|
|
214
|
+
setTimeout(() => {
|
|
215
|
+
reject(new Error('Tunnel connection timed out after 15s'))
|
|
216
|
+
}, 15_000)
|
|
217
|
+
}),
|
|
218
|
+
])
|
|
219
|
+
} catch (error) {
|
|
220
|
+
tunnelClient.close()
|
|
221
|
+
if (coderaftProcess.exitCode === null) {
|
|
222
|
+
coderaftProcess.kill('SIGTERM')
|
|
223
|
+
}
|
|
224
|
+
throw error
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const url = tunnelClient.url
|
|
228
|
+
|
|
229
|
+
const timeoutTimer = setTimeout(() => {
|
|
230
|
+
logger.log(`VS Code auto-stopped after ${MAX_SESSION_MINUTES} minutes (key: ${sessionKey})`)
|
|
231
|
+
stopVscode({ sessionKey })
|
|
232
|
+
}, MAX_SESSION_MS)
|
|
233
|
+
timeoutTimer.unref()
|
|
234
|
+
|
|
235
|
+
const session: VscodeSession = {
|
|
236
|
+
coderaftProcess,
|
|
237
|
+
tunnelClient,
|
|
238
|
+
url,
|
|
239
|
+
workingDirectory,
|
|
240
|
+
startedBy,
|
|
241
|
+
startedAt: Date.now(),
|
|
242
|
+
timeoutTimer,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
coderaftProcess.once('exit', (code, signal) => {
|
|
246
|
+
const current = activeSessions.get(sessionKey)
|
|
247
|
+
if (current !== session) {
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
logger.log(`VS Code process exited (key: ${sessionKey}, code: ${code}, signal: ${signal ?? 'none'})`)
|
|
251
|
+
stopVscode({ sessionKey })
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
activeSessions.set(sessionKey, session)
|
|
255
|
+
logger.log(`VS Code started by ${startedBy}: ${url}`)
|
|
256
|
+
return session
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function handleVscodeCommand({
|
|
260
|
+
command,
|
|
261
|
+
}: CommandContext): Promise<void> {
|
|
262
|
+
const channel = command.channel
|
|
263
|
+
if (!channel) {
|
|
264
|
+
await command.reply({
|
|
265
|
+
content: 'This command can only be used in a channel.',
|
|
266
|
+
flags: SECURE_REPLY_FLAGS,
|
|
267
|
+
})
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const isThread = [
|
|
272
|
+
ChannelType.PublicThread,
|
|
273
|
+
ChannelType.PrivateThread,
|
|
274
|
+
ChannelType.AnnouncementThread,
|
|
275
|
+
].includes(channel.type)
|
|
276
|
+
const isTextChannel = channel.type === ChannelType.GuildText
|
|
277
|
+
if (!isThread && !isTextChannel) {
|
|
278
|
+
await command.reply({
|
|
279
|
+
content: 'This command can only be used in a text channel or thread.',
|
|
280
|
+
flags: SECURE_REPLY_FLAGS,
|
|
281
|
+
})
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const resolved = await resolveWorkingDirectory({
|
|
286
|
+
channel: channel as TextChannel | ThreadChannel,
|
|
287
|
+
})
|
|
288
|
+
if (!resolved) {
|
|
289
|
+
await command.reply({
|
|
290
|
+
content: 'Could not determine project directory for this channel.',
|
|
291
|
+
flags: SECURE_REPLY_FLAGS,
|
|
292
|
+
})
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await command.deferReply({ flags: SECURE_REPLY_FLAGS })
|
|
297
|
+
|
|
298
|
+
const sessionKey = channel.id
|
|
299
|
+
const existing = getActiveVscodeSession({ sessionKey })
|
|
300
|
+
if (existing) {
|
|
301
|
+
await command.editReply({
|
|
302
|
+
content:
|
|
303
|
+
`VS Code is already running for this thread. ` +
|
|
304
|
+
`This unique tunnel auto-stops after ${MAX_SESSION_MINUTES} minutes from startup.\n` +
|
|
305
|
+
`${existing.url}`,
|
|
306
|
+
})
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const session = await startVscode({
|
|
312
|
+
sessionKey,
|
|
313
|
+
startedBy: command.user.tag,
|
|
314
|
+
workingDirectory: resolved.workingDirectory,
|
|
315
|
+
})
|
|
316
|
+
await command.editReply({
|
|
317
|
+
content:
|
|
318
|
+
`VS Code started for \`${session.workingDirectory}\`. ` +
|
|
319
|
+
`This unique tunnel auto-stops after ${MAX_SESSION_MINUTES} minutes, so open it before it expires.\n` +
|
|
320
|
+
`${session.url}`,
|
|
321
|
+
})
|
|
322
|
+
} catch (error) {
|
|
323
|
+
logger.error('Failed to start VS Code:', error)
|
|
324
|
+
await command.editReply({
|
|
325
|
+
content: `Failed to start VS Code: ${error instanceof Error ? error.message : String(error)}`,
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function cleanupAllVscodeSessions(): void {
|
|
331
|
+
for (const sessionKey of activeSessions.keys()) {
|
|
332
|
+
stopVscode({ sessionKey })
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function onProcessExit(): void {
|
|
337
|
+
cleanupAllVscodeSessions()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
process.on('SIGINT', onProcessExit)
|
|
341
|
+
process.on('SIGTERM', onProcessExit)
|
|
342
|
+
process.on('exit', onProcessExit)
|