@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,127 @@
|
|
|
1
|
+
// /restart-opencode-server command - Restart the single shared opencode server
|
|
2
|
+
// and re-register Discord slash commands.
|
|
3
|
+
// Used for resolving opencode state issues, internal bugs, refreshing auth state,
|
|
4
|
+
// plugins, and picking up new/changed slash commands or agents. Aborts in-progress
|
|
5
|
+
// sessions in this channel before restarting. Note: since there is one shared server,
|
|
6
|
+
// this restart affects all projects. Other runtimes reconnect through their listener
|
|
7
|
+
// backoff loop once the shared server comes back.
|
|
8
|
+
import { ChannelType, MessageFlags, } from 'discord.js';
|
|
9
|
+
import { initializeOpencodeForDirectory, restartOpencodeServer } from '../opencode.js';
|
|
10
|
+
import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
11
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
12
|
+
import { disposeRuntimesForDirectory } from '../session-handler/thread-session-runtime.js';
|
|
13
|
+
import { registerCommands } from '../discord-command-registration.js';
|
|
14
|
+
const logger = createLogger(LogPrefix.OPENCODE);
|
|
15
|
+
export async function handleRestartOpencodeServerCommand({ command, appId, }) {
|
|
16
|
+
const channel = command.channel;
|
|
17
|
+
if (!channel) {
|
|
18
|
+
await command.reply({
|
|
19
|
+
content: 'This command can only be used in a channel',
|
|
20
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const isThread = [
|
|
25
|
+
ChannelType.PublicThread,
|
|
26
|
+
ChannelType.PrivateThread,
|
|
27
|
+
ChannelType.AnnouncementThread,
|
|
28
|
+
].includes(channel.type);
|
|
29
|
+
const isTextChannel = channel.type === ChannelType.GuildText;
|
|
30
|
+
if (!isThread && !isTextChannel) {
|
|
31
|
+
await command.reply({
|
|
32
|
+
content: 'This command can only be used in text channels or threads',
|
|
33
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const resolved = await resolveWorkingDirectory({
|
|
38
|
+
channel: channel,
|
|
39
|
+
});
|
|
40
|
+
if (!resolved) {
|
|
41
|
+
await command.reply({
|
|
42
|
+
content: 'Could not determine project directory for this channel',
|
|
43
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const { projectDirectory } = resolved;
|
|
48
|
+
// Defer reply since restart may take a moment
|
|
49
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
50
|
+
// Dispose all runtimes for this directory/channel scope.
|
|
51
|
+
// disposeRuntimesForDirectory aborts active runs, kills listeners, and
|
|
52
|
+
// removes runtimes from the registry. Scoped by channelId so runtimes
|
|
53
|
+
// in other channels sharing the same project directory are not affected.
|
|
54
|
+
const parentChannelId = isThread
|
|
55
|
+
? channel.parentId
|
|
56
|
+
: channel.id;
|
|
57
|
+
const abortedCount = disposeRuntimesForDirectory({
|
|
58
|
+
directory: projectDirectory,
|
|
59
|
+
channelId: parentChannelId || undefined,
|
|
60
|
+
});
|
|
61
|
+
logger.log(`[RESTART] Restarting shared opencode server`);
|
|
62
|
+
const result = await restartOpencodeServer();
|
|
63
|
+
if (result instanceof Error) {
|
|
64
|
+
logger.error('[RESTART] Failed:', result);
|
|
65
|
+
await command.editReply({
|
|
66
|
+
content: `Failed to restart opencode server: ${result.message}`,
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const abortMsg = abortedCount > 0
|
|
71
|
+
? ` (aborted ${abortedCount} active session${abortedCount > 1 ? 's' : ''})`
|
|
72
|
+
: '';
|
|
73
|
+
await command.editReply({
|
|
74
|
+
content: `Opencode server **restarted** successfully${abortMsg}. Re-registering slash commands...`,
|
|
75
|
+
});
|
|
76
|
+
logger.log('[RESTART] Shared opencode server restarted');
|
|
77
|
+
// Re-register Discord slash commands after restart so new/changed
|
|
78
|
+
// commands, agents, and plugins are picked up immediately.
|
|
79
|
+
const token = command.client.token;
|
|
80
|
+
if (!token) {
|
|
81
|
+
logger.error('[RESTART] No bot token available, skipping command registration');
|
|
82
|
+
await command.editReply({
|
|
83
|
+
content: `Opencode server **restarted**${abortMsg}, but slash command re-registration skipped (no bot token)`,
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const guildIds = [...command.client.guilds.cache.keys()];
|
|
88
|
+
const opencodeResult = await initializeOpencodeForDirectory(projectDirectory);
|
|
89
|
+
const [userCommands, agents] = await (async () => {
|
|
90
|
+
if (opencodeResult instanceof Error) {
|
|
91
|
+
logger.warn('[RESTART] OpenCode init failed, registering without user commands:', opencodeResult.message);
|
|
92
|
+
return [[], []];
|
|
93
|
+
}
|
|
94
|
+
const getClient = opencodeResult;
|
|
95
|
+
const [cmds, ags] = await Promise.all([
|
|
96
|
+
getClient()
|
|
97
|
+
.command.list({ directory: projectDirectory })
|
|
98
|
+
.then((r) => r.data || [])
|
|
99
|
+
.catch((e) => {
|
|
100
|
+
logger.warn('[RESTART] Failed to load user commands:', e instanceof Error ? e.stack : String(e));
|
|
101
|
+
return [];
|
|
102
|
+
}),
|
|
103
|
+
getClient()
|
|
104
|
+
.app.agents({ directory: projectDirectory })
|
|
105
|
+
.then((r) => r.data || [])
|
|
106
|
+
.catch((e) => {
|
|
107
|
+
logger.warn('[RESTART] Failed to load agents:', e instanceof Error ? e.stack : String(e));
|
|
108
|
+
return [];
|
|
109
|
+
}),
|
|
110
|
+
]);
|
|
111
|
+
return [cmds, ags];
|
|
112
|
+
})();
|
|
113
|
+
const registerResult = await registerCommands({ token, appId, guildIds, userCommands, agents })
|
|
114
|
+
.then(() => null)
|
|
115
|
+
.catch((e) => (e instanceof Error ? e : new Error(String(e))));
|
|
116
|
+
if (registerResult instanceof Error) {
|
|
117
|
+
logger.error('[RESTART] Failed to re-register commands:', registerResult.message);
|
|
118
|
+
await command.editReply({
|
|
119
|
+
content: `Opencode server **restarted**${abortMsg}, but slash command re-registration failed: ${registerResult.message}`,
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
logger.log('[RESTART] Slash commands re-registered');
|
|
124
|
+
await command.editReply({
|
|
125
|
+
content: `Opencode server **restarted** and slash commands **re-registered**${abortMsg}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// /resume command - Resume an existing OpenCode session.
|
|
2
|
+
import { ChannelType, ThreadAutoArchiveDuration, } from 'discord.js';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { getChannelDirectory, setThreadSession, setPartMessagesBatch, getAllThreadSessionIds, } from '../database.js';
|
|
5
|
+
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
|
+
import { sendThreadMessage, resolveProjectDirectoryFromAutocomplete, NOTIFY_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
7
|
+
import { collectSessionChunks, batchChunksForDiscord } from '../message-formatting.js';
|
|
8
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
|
+
import * as errore from 'errore';
|
|
10
|
+
const logger = createLogger(LogPrefix.RESUME);
|
|
11
|
+
export async function handleResumeCommand({ command, }) {
|
|
12
|
+
await command.deferReply();
|
|
13
|
+
const sessionId = command.options.getString('session', true);
|
|
14
|
+
const channel = command.channel;
|
|
15
|
+
const isThread = channel &&
|
|
16
|
+
[
|
|
17
|
+
ChannelType.PublicThread,
|
|
18
|
+
ChannelType.PrivateThread,
|
|
19
|
+
ChannelType.AnnouncementThread,
|
|
20
|
+
].includes(channel.type);
|
|
21
|
+
if (isThread) {
|
|
22
|
+
await command.editReply('This command can only be used in project channels, not threads');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
26
|
+
await command.editReply('This command can only be used in text channels');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const textChannel = channel;
|
|
30
|
+
const channelConfig = await getChannelDirectory(textChannel.id);
|
|
31
|
+
const projectDirectory = channelConfig?.directory;
|
|
32
|
+
if (!projectDirectory) {
|
|
33
|
+
await command.editReply('This channel is not configured with a project directory');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!fs.existsSync(projectDirectory)) {
|
|
37
|
+
await command.editReply(`Directory does not exist: ${projectDirectory}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
42
|
+
if (getClient instanceof Error) {
|
|
43
|
+
await command.editReply(getClient.message);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const sessionResponse = await getClient().session.get({
|
|
47
|
+
sessionID: sessionId,
|
|
48
|
+
});
|
|
49
|
+
if (!sessionResponse.data) {
|
|
50
|
+
await command.editReply('Session not found');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const sessionTitle = sessionResponse.data.title;
|
|
54
|
+
const thread = await textChannel.threads.create({
|
|
55
|
+
name: `Resume: ${sessionTitle}`.slice(0, 100),
|
|
56
|
+
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
57
|
+
reason: `Resuming session ${sessionId}`,
|
|
58
|
+
});
|
|
59
|
+
// Claim the resumed session immediately so external polling does not race
|
|
60
|
+
// and create a duplicate Sync thread before the rest of this setup runs.
|
|
61
|
+
await setThreadSession(thread.id, sessionId);
|
|
62
|
+
// Add user to thread so it appears in their sidebar
|
|
63
|
+
await thread.members.add(command.user.id);
|
|
64
|
+
logger.log(`[RESUME] Created thread ${thread.id} for session ${sessionId}`);
|
|
65
|
+
const messagesResponse = await getClient().session.messages({
|
|
66
|
+
sessionID: sessionId,
|
|
67
|
+
});
|
|
68
|
+
if (!messagesResponse.data) {
|
|
69
|
+
throw new Error('Failed to fetch session messages');
|
|
70
|
+
}
|
|
71
|
+
const messages = messagesResponse.data;
|
|
72
|
+
await command.editReply(`Resumed session "${sessionTitle}" in ${thread.toString()}`);
|
|
73
|
+
await sendThreadMessage(thread, `**Resumed session:** ${sessionTitle}\n**Created:** ${new Date(sessionResponse.data.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`);
|
|
74
|
+
try {
|
|
75
|
+
const { chunks, skippedCount } = collectSessionChunks({
|
|
76
|
+
messages,
|
|
77
|
+
limit: 30,
|
|
78
|
+
});
|
|
79
|
+
if (skippedCount > 0) {
|
|
80
|
+
await sendThreadMessage(thread, `*Skipped ${skippedCount} older assistant parts...*`);
|
|
81
|
+
}
|
|
82
|
+
const batched = batchChunksForDiscord(chunks);
|
|
83
|
+
for (const batch of batched) {
|
|
84
|
+
const discordMessage = await sendThreadMessage(thread, batch.content);
|
|
85
|
+
await setPartMessagesBatch(batch.partIds.map((partId) => ({
|
|
86
|
+
partId,
|
|
87
|
+
messageId: discordMessage.id,
|
|
88
|
+
threadId: thread.id,
|
|
89
|
+
})));
|
|
90
|
+
}
|
|
91
|
+
const messageCount = messages.length;
|
|
92
|
+
await sendThreadMessage(thread, `**Session resumed!** Loaded ${messageCount} messages.\n\nYou can now continue the conversation by sending messages in this thread.`);
|
|
93
|
+
}
|
|
94
|
+
catch (sendError) {
|
|
95
|
+
logger.error('[RESUME] Error sending messages to thread:', sendError);
|
|
96
|
+
await sendThreadMessage(thread, `Failed to load message history, but session is connected. You can still send new messages.`, { flags: NOTIFY_MESSAGE_FLAGS });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger.error('[RESUME] Error:', error);
|
|
101
|
+
await command.editReply(`Failed to resume session: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export async function handleResumeAutocomplete({ interaction, }) {
|
|
105
|
+
const focusedValue = interaction.options.getFocused();
|
|
106
|
+
// interaction.channel can be null when the channel isn't cached
|
|
107
|
+
// (common with gateway-proxy). Use channelId which is always available
|
|
108
|
+
// from the raw interaction payload.
|
|
109
|
+
const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction);
|
|
110
|
+
if (!projectDirectory) {
|
|
111
|
+
await interaction.respond([]);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
116
|
+
if (getClient instanceof Error) {
|
|
117
|
+
await interaction.respond([]);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const sessionsResponse = await getClient().session.list();
|
|
121
|
+
if (!sessionsResponse.data) {
|
|
122
|
+
await interaction.respond([]);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const existingSessionIds = new Set(await getAllThreadSessionIds());
|
|
126
|
+
const sessions = sessionsResponse.data
|
|
127
|
+
.filter((session) => !existingSessionIds.has(session.id))
|
|
128
|
+
.filter((session) => session.title.toLowerCase().includes(focusedValue.toLowerCase()))
|
|
129
|
+
.slice(0, 25)
|
|
130
|
+
.map((session) => {
|
|
131
|
+
const dateStr = new Date(session.time.updated).toLocaleString();
|
|
132
|
+
const suffix = ` (${dateStr})`;
|
|
133
|
+
const maxTitleLength = 100 - suffix.length;
|
|
134
|
+
let title = session.title;
|
|
135
|
+
if (title.length > maxTitleLength) {
|
|
136
|
+
title = title.slice(0, Math.max(0, maxTitleLength - 1)) + '…';
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
name: `${title}${suffix}`,
|
|
140
|
+
value: session.id,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
await interaction.respond(sessions);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
logger.error('[AUTOCOMPLETE] Error fetching sessions:', error);
|
|
147
|
+
await interaction.respond([]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// /run-shell-command command - Run an arbitrary shell command in the project directory.
|
|
2
|
+
// Resolves the project directory from the channel and executes the command with it as cwd.
|
|
3
|
+
// Also used by the ! prefix shortcut in discord messages (e.g. "!ls -la").
|
|
4
|
+
// Messages starting with ! are intercepted before session handling and routed here.
|
|
5
|
+
import { ChannelType, MessageFlags, } from 'discord.js';
|
|
6
|
+
import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
7
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
8
|
+
import { execAsync } from '../worktrees.js';
|
|
9
|
+
import { stripAnsi } from '../utils.js';
|
|
10
|
+
const logger = createLogger(LogPrefix.INTERACTION);
|
|
11
|
+
const MAX_OUTPUT_CHARS = 1900;
|
|
12
|
+
export async function runShellCommand({ command, directory, }) {
|
|
13
|
+
try {
|
|
14
|
+
const { stdout, stderr } = await execAsync(command, { cwd: directory });
|
|
15
|
+
const output = stripAnsi([stdout, stderr].filter(Boolean).join('\n').trim());
|
|
16
|
+
const header = `\`${command}\` exited with 0`;
|
|
17
|
+
if (!output) {
|
|
18
|
+
return header;
|
|
19
|
+
}
|
|
20
|
+
return formatOutput(output, header);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const execError = error;
|
|
24
|
+
const output = stripAnsi([execError.stdout, execError.stderr].filter(Boolean).join('\n').trim());
|
|
25
|
+
const exitCode = execError.code ?? 1;
|
|
26
|
+
logger.error(`[RUN-COMMAND] Command "${command}" exited with ${exitCode}:`, error);
|
|
27
|
+
const header = `\`${command}\` exited with ${exitCode}`;
|
|
28
|
+
return formatOutput(output || execError.message || 'Unknown error', header);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function handleRunCommand({ command, }) {
|
|
32
|
+
const channel = command.channel;
|
|
33
|
+
if (!channel) {
|
|
34
|
+
await command.reply({
|
|
35
|
+
content: 'This command can only be used in a channel.',
|
|
36
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const isThread = [
|
|
41
|
+
ChannelType.PublicThread,
|
|
42
|
+
ChannelType.PrivateThread,
|
|
43
|
+
ChannelType.AnnouncementThread,
|
|
44
|
+
].includes(channel.type);
|
|
45
|
+
const isTextChannel = channel.type === ChannelType.GuildText;
|
|
46
|
+
if (!isThread && !isTextChannel) {
|
|
47
|
+
await command.reply({
|
|
48
|
+
content: 'This command can only be used in a text channel or thread.',
|
|
49
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const resolved = await resolveWorkingDirectory({
|
|
54
|
+
channel: channel,
|
|
55
|
+
});
|
|
56
|
+
if (!resolved) {
|
|
57
|
+
await command.reply({
|
|
58
|
+
content: 'Could not determine project directory for this channel.',
|
|
59
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const input = command.options.getString('command', true);
|
|
64
|
+
await command.deferReply();
|
|
65
|
+
const result = await runShellCommand({
|
|
66
|
+
command: input,
|
|
67
|
+
directory: resolved.workingDirectory,
|
|
68
|
+
});
|
|
69
|
+
await command.editReply({ content: result });
|
|
70
|
+
}
|
|
71
|
+
function formatOutput(output, header) {
|
|
72
|
+
// Reserve space for header + newline + code block delimiters (```\n...\n```)
|
|
73
|
+
const overhead = header.length + 1 + 3 + 1 + 1 + 3; // header\n```\n...\n```
|
|
74
|
+
const maxContent = MAX_OUTPUT_CHARS - overhead;
|
|
75
|
+
const truncated = output.length > maxContent
|
|
76
|
+
? output.slice(0, maxContent - 14) + '\n... truncated'
|
|
77
|
+
: output;
|
|
78
|
+
return `${header}\n\`\`\`\n${truncated}\n\`\`\``;
|
|
79
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// /screenshare command - Start screen sharing via VNC + WebSocket bridge + otto tunnel.
|
|
2
|
+
// On macOS: uses built-in Screen Sharing (port 5900).
|
|
3
|
+
// On Linux: spawns x11vnc against the current $DISPLAY.
|
|
4
|
+
// Exposes the VNC stream via an in-process websockify bridge and a traforo tunnel,
|
|
5
|
+
// then sends the user a noVNC URL they can open in a browser.
|
|
6
|
+
//
|
|
7
|
+
// /screenshare-stop command - Stops the active screen share for this guild.
|
|
8
|
+
import { MessageFlags } from 'discord.js';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import net from 'node:net';
|
|
12
|
+
import { TunnelClient } from 'traforo/client';
|
|
13
|
+
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
14
|
+
import { startWebsockify } from '../websockify.js';
|
|
15
|
+
import { createLogger } from '../logger.js';
|
|
16
|
+
import { execAsync } from '../worktrees.js';
|
|
17
|
+
const logger = createLogger('SCREEN');
|
|
18
|
+
const SECURE_REPLY_FLAGS = MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS;
|
|
19
|
+
/** One active screenshare per guild (Discord) or per machine (CLI) */
|
|
20
|
+
const activeSessions = new Map();
|
|
21
|
+
const VNC_PORT = 5900;
|
|
22
|
+
const MAX_SESSION_MINUTES = 30;
|
|
23
|
+
const MAX_SESSION_MS = MAX_SESSION_MINUTES * 60 * 1000;
|
|
24
|
+
const TUNNEL_BASE_DOMAIN = 'otto.dev';
|
|
25
|
+
const SCREENSHARE_TUNNEL_ID_BYTES = 16;
|
|
26
|
+
// Public noVNC client — we point it at our tunnel URL
|
|
27
|
+
export function buildNoVncUrl({ tunnelHost }) {
|
|
28
|
+
const params = new URLSearchParams({
|
|
29
|
+
autoconnect: 'true',
|
|
30
|
+
host: tunnelHost,
|
|
31
|
+
port: '443',
|
|
32
|
+
encrypt: '1',
|
|
33
|
+
resize: 'scale',
|
|
34
|
+
view_only: 'false',
|
|
35
|
+
});
|
|
36
|
+
return `https://novnc.com/noVNC/vnc.html?${params.toString()}`;
|
|
37
|
+
}
|
|
38
|
+
export function createScreenshareTunnelId() {
|
|
39
|
+
return crypto.randomBytes(SCREENSHARE_TUNNEL_ID_BYTES).toString('hex');
|
|
40
|
+
}
|
|
41
|
+
// macOS has two separate services:
|
|
42
|
+
// - "Screen Sharing" = view-only VNC (com.apple.screensharing)
|
|
43
|
+
// - "Remote Management" = full control VNC with mouse/keyboard (ARDAgent)
|
|
44
|
+
// We need Remote Management for interactive control, not just Screen Sharing.
|
|
45
|
+
export async function ensureMacRemoteManagement() {
|
|
46
|
+
// Check if port 5900 is listening via netstat (no sudo needed).
|
|
47
|
+
// lsof and launchctl list both require sudo for system daemons.
|
|
48
|
+
try {
|
|
49
|
+
const { stdout } = await execAsync('netstat -an | grep "\\.5900 " | grep LISTEN', { timeout: 5000 });
|
|
50
|
+
if (stdout.trim()) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// not listening
|
|
56
|
+
}
|
|
57
|
+
throw new Error('macOS Remote Management is not enabled.\n' +
|
|
58
|
+
'Enable it: **System Settings > General > Sharing > Remote Management**\n' +
|
|
59
|
+
'Make sure "VNC viewers may control screen with password" is enabled.\n' +
|
|
60
|
+
'Or via terminal:\n' +
|
|
61
|
+
'```\nsudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \\\n' +
|
|
62
|
+
' -activate -configure -allowAccessFor -allUsers -privs -all \\\n' +
|
|
63
|
+
' -clientopts -setvnclegacy -vnclegacy yes \\\n' +
|
|
64
|
+
' -restart -agent -console\n```');
|
|
65
|
+
}
|
|
66
|
+
export function spawnX11Vnc() {
|
|
67
|
+
const display = process.env['DISPLAY'] || ':0';
|
|
68
|
+
const child = spawn('x11vnc', [
|
|
69
|
+
'-display', display,
|
|
70
|
+
'-nopw',
|
|
71
|
+
'-localhost',
|
|
72
|
+
'-rfbport', String(VNC_PORT),
|
|
73
|
+
'-shared',
|
|
74
|
+
'-forever',
|
|
75
|
+
], {
|
|
76
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
77
|
+
});
|
|
78
|
+
child.stdout?.on('data', (data) => {
|
|
79
|
+
logger.log(`x11vnc: ${data.toString().trim()}`);
|
|
80
|
+
});
|
|
81
|
+
child.stderr?.on('data', (data) => {
|
|
82
|
+
logger.error(`x11vnc: ${data.toString().trim()}`);
|
|
83
|
+
});
|
|
84
|
+
return child;
|
|
85
|
+
}
|
|
86
|
+
function waitForPort({ port, process: proc, timeoutMs, }) {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const maxAttempts = Math.ceil(timeoutMs / 100);
|
|
89
|
+
let attempts = 0;
|
|
90
|
+
const check = () => {
|
|
91
|
+
if (proc.exitCode !== null) {
|
|
92
|
+
reject(new Error(`x11vnc exited with code ${proc.exitCode} before becoming ready`));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const sock = net.createConnection(port, 'localhost');
|
|
96
|
+
sock.on('connect', () => {
|
|
97
|
+
sock.destroy();
|
|
98
|
+
resolve();
|
|
99
|
+
});
|
|
100
|
+
sock.on('error', () => {
|
|
101
|
+
sock.destroy();
|
|
102
|
+
if (++attempts >= maxAttempts) {
|
|
103
|
+
reject(new Error(`Port ${port} not reachable after ${timeoutMs}ms`));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
setTimeout(check, 100);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
check();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
export function cleanupSession(session) {
|
|
114
|
+
clearTimeout(session.timeoutTimer);
|
|
115
|
+
try {
|
|
116
|
+
session.tunnelClient.close();
|
|
117
|
+
}
|
|
118
|
+
catch { }
|
|
119
|
+
try {
|
|
120
|
+
session.wss.close();
|
|
121
|
+
}
|
|
122
|
+
catch { }
|
|
123
|
+
if (session.vncProcess) {
|
|
124
|
+
try {
|
|
125
|
+
session.vncProcess.kill();
|
|
126
|
+
}
|
|
127
|
+
catch { }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Core screenshare start logic, reused by both Discord command and CLI.
|
|
132
|
+
* Returns the session or throws on failure.
|
|
133
|
+
*/
|
|
134
|
+
export async function startScreenshare({ sessionKey, startedBy, }) {
|
|
135
|
+
const existing = activeSessions.get(sessionKey);
|
|
136
|
+
if (existing) {
|
|
137
|
+
throw new Error(`Screen sharing is already active: ${existing.noVncUrl}`);
|
|
138
|
+
}
|
|
139
|
+
const platform = process.platform;
|
|
140
|
+
let vncProcess;
|
|
141
|
+
// Step 1: ensure VNC server is running
|
|
142
|
+
if (platform === 'darwin') {
|
|
143
|
+
await ensureMacRemoteManagement();
|
|
144
|
+
}
|
|
145
|
+
else if (platform === 'linux') {
|
|
146
|
+
if (!process.env['DISPLAY']) {
|
|
147
|
+
throw new Error('No $DISPLAY found. Screen sharing requires a running X11 display.');
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await execAsync('which x11vnc', { timeout: 3000 });
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new Error('x11vnc is not installed. Install it with: sudo apt install x11vnc');
|
|
154
|
+
}
|
|
155
|
+
vncProcess = spawnX11Vnc();
|
|
156
|
+
// Wait for x11vnc to actually be ready (port 5900 accepting connections)
|
|
157
|
+
// instead of a blind 1s sleep. Polls every 100ms, fails if process exits first.
|
|
158
|
+
await waitForPort({ port: VNC_PORT, process: vncProcess, timeoutMs: 3000 });
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
throw new Error(`Screen sharing is not supported on ${platform}. Only macOS and Linux are supported.`);
|
|
162
|
+
}
|
|
163
|
+
// Step 2: start in-process websockify bridge
|
|
164
|
+
let wsInstance;
|
|
165
|
+
try {
|
|
166
|
+
wsInstance = await startWebsockify({
|
|
167
|
+
wsPort: 0,
|
|
168
|
+
tcpHost: 'localhost',
|
|
169
|
+
tcpPort: VNC_PORT,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
if (vncProcess) {
|
|
174
|
+
vncProcess.kill();
|
|
175
|
+
}
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
// Step 3: create tunnel
|
|
179
|
+
const tunnelId = createScreenshareTunnelId();
|
|
180
|
+
const tunnelClient = new TunnelClient({
|
|
181
|
+
localPort: wsInstance.port,
|
|
182
|
+
tunnelId,
|
|
183
|
+
baseDomain: TUNNEL_BASE_DOMAIN,
|
|
184
|
+
});
|
|
185
|
+
try {
|
|
186
|
+
await Promise.race([
|
|
187
|
+
tunnelClient.connect(),
|
|
188
|
+
new Promise((_, reject) => {
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
reject(new Error('Tunnel connection timed out after 15s'));
|
|
191
|
+
}, 15000);
|
|
192
|
+
}),
|
|
193
|
+
]);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
tunnelClient.close();
|
|
197
|
+
wsInstance.close();
|
|
198
|
+
if (vncProcess) {
|
|
199
|
+
vncProcess.kill();
|
|
200
|
+
}
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
const tunnelHost = `${tunnelId}-tunnel.${TUNNEL_BASE_DOMAIN}`;
|
|
204
|
+
const tunnelUrl = `https://${tunnelHost}`;
|
|
205
|
+
const noVncUrl = buildNoVncUrl({ tunnelHost });
|
|
206
|
+
// Auto-kill after a short session so a leaked URL does not stay usable all day.
|
|
207
|
+
const timeoutTimer = setTimeout(() => {
|
|
208
|
+
logger.log(`Screen share auto-stopped after ${MAX_SESSION_MINUTES} minutes (key: ${sessionKey})`);
|
|
209
|
+
stopScreenshare({ sessionKey });
|
|
210
|
+
}, MAX_SESSION_MS);
|
|
211
|
+
// Don't keep the process alive just for this timer
|
|
212
|
+
timeoutTimer.unref();
|
|
213
|
+
const session = {
|
|
214
|
+
tunnelClient,
|
|
215
|
+
wss: wsInstance.wss,
|
|
216
|
+
vncProcess,
|
|
217
|
+
url: tunnelUrl,
|
|
218
|
+
noVncUrl,
|
|
219
|
+
startedBy,
|
|
220
|
+
startedAt: Date.now(),
|
|
221
|
+
timeoutTimer,
|
|
222
|
+
};
|
|
223
|
+
activeSessions.set(sessionKey, session);
|
|
224
|
+
logger.log(`Screen share started by ${startedBy}: ${tunnelUrl}`);
|
|
225
|
+
return session;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Core screenshare stop logic, reused by both Discord command and CLI.
|
|
229
|
+
*/
|
|
230
|
+
export function stopScreenshare({ sessionKey }) {
|
|
231
|
+
const session = activeSessions.get(sessionKey);
|
|
232
|
+
if (!session) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
cleanupSession(session);
|
|
236
|
+
activeSessions.delete(sessionKey);
|
|
237
|
+
logger.log(`Screen share stopped (key: ${sessionKey})`);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
export async function handleScreenshareCommand({ command, }) {
|
|
241
|
+
const guildId = command.guildId;
|
|
242
|
+
if (!guildId) {
|
|
243
|
+
await command.reply({
|
|
244
|
+
content: 'This command can only be used in a server',
|
|
245
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
await command.deferReply({ flags: SECURE_REPLY_FLAGS });
|
|
250
|
+
try {
|
|
251
|
+
const session = await startScreenshare({
|
|
252
|
+
sessionKey: guildId,
|
|
253
|
+
startedBy: command.user.tag,
|
|
254
|
+
});
|
|
255
|
+
await command.editReply({
|
|
256
|
+
content: `Screen sharing started. This reply is private and the URL uses a high-entropy tunnel id. ` +
|
|
257
|
+
`It will auto-stop after ${MAX_SESSION_MINUTES} minutes. Use /screenshare-stop to stop sooner.\n` +
|
|
258
|
+
`${session.noVncUrl}`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
logger.error('Failed to start screen share:', err);
|
|
263
|
+
await command.editReply({
|
|
264
|
+
content: `Failed to start screen share: ${err instanceof Error ? err.message : String(err)}`,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
export async function handleScreenshareStopCommand({ command, }) {
|
|
269
|
+
const guildId = command.guildId;
|
|
270
|
+
if (!guildId) {
|
|
271
|
+
await command.reply({
|
|
272
|
+
content: 'This command can only be used in a server',
|
|
273
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const stopped = stopScreenshare({ sessionKey: guildId });
|
|
278
|
+
if (!stopped) {
|
|
279
|
+
await command.reply({
|
|
280
|
+
content: 'No active screen share to stop',
|
|
281
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
await command.reply({
|
|
286
|
+
content: 'Screen sharing stopped',
|
|
287
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/** Cleanup all sessions on bot shutdown */
|
|
291
|
+
export function cleanupAllScreenshares() {
|
|
292
|
+
for (const [guildId, session] of activeSessions) {
|
|
293
|
+
cleanupSession(session);
|
|
294
|
+
activeSessions.delete(guildId);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Kill all screenshares when the process exits (Ctrl+C, SIGTERM, etc.)
|
|
298
|
+
function onProcessExit() {
|
|
299
|
+
cleanupAllScreenshares();
|
|
300
|
+
}
|
|
301
|
+
process.on('SIGINT', onProcessExit);
|
|
302
|
+
process.on('SIGTERM', onProcessExit);
|
|
303
|
+
process.on('exit', onProcessExit);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { buildNoVncUrl, createScreenshareTunnelId } from './screenshare.js';
|
|
3
|
+
describe('screenshare security defaults', () => {
|
|
4
|
+
test('generates a 128-bit tunnel id', () => {
|
|
5
|
+
const ids = new Set(Array.from({ length: 32 }, () => {
|
|
6
|
+
return createScreenshareTunnelId();
|
|
7
|
+
}));
|
|
8
|
+
expect(ids.size).toBe(32);
|
|
9
|
+
for (const id of ids) {
|
|
10
|
+
expect(id).toMatch(/^[0-9a-f]{32}$/);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
test('builds a secure noVNC URL', () => {
|
|
14
|
+
const url = new URL(buildNoVncUrl({ tunnelHost: '0123456789abcdef-tunnel.otto.dev' }));
|
|
15
|
+
expect(url.origin).toBe('https://novnc.com');
|
|
16
|
+
expect(url.searchParams.get('host')).toBe('0123456789abcdef-tunnel.otto.dev');
|
|
17
|
+
expect(url.searchParams.get('port')).toBe('443');
|
|
18
|
+
expect(url.searchParams.get('encrypt')).toBe('1');
|
|
19
|
+
});
|
|
20
|
+
});
|