@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,645 @@
|
|
|
1
|
+
// Worktree management command: /new-worktree
|
|
2
|
+
// Uses OpenCode SDK v2 to create worktrees with otto- prefix
|
|
3
|
+
// Creates thread immediately, then worktree in background so user can type
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ChannelType,
|
|
7
|
+
REST,
|
|
8
|
+
type TextChannel,
|
|
9
|
+
type ThreadChannel,
|
|
10
|
+
type Message,
|
|
11
|
+
} from 'discord.js'
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import type { CommandContext } from './types.js'
|
|
14
|
+
import {
|
|
15
|
+
createPendingWorktree,
|
|
16
|
+
setWorktreeReady,
|
|
17
|
+
setWorktreeError,
|
|
18
|
+
getChannelDirectory,
|
|
19
|
+
getThreadWorktree,
|
|
20
|
+
getThreadSession,
|
|
21
|
+
} from '../database.js'
|
|
22
|
+
import {
|
|
23
|
+
SILENT_MESSAGE_FLAGS,
|
|
24
|
+
reactToThread,
|
|
25
|
+
resolveProjectDirectoryFromAutocomplete,
|
|
26
|
+
} from '../discord-utils.js'
|
|
27
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
28
|
+
import { notifyError } from '../sentry.js'
|
|
29
|
+
import {
|
|
30
|
+
createWorktreeWithSubmodules,
|
|
31
|
+
execAsync,
|
|
32
|
+
listBranchesByLastCommit,
|
|
33
|
+
validateBranchRef,
|
|
34
|
+
} from '../worktrees.js'
|
|
35
|
+
import {
|
|
36
|
+
buildExternalDirectoryPermissionRules,
|
|
37
|
+
getOpencodeClient,
|
|
38
|
+
initializeOpencodeForDirectory,
|
|
39
|
+
} from '../opencode.js'
|
|
40
|
+
import { WORKTREE_PREFIX } from './merge-worktree.js'
|
|
41
|
+
import type { AutocompleteContext } from './types.js'
|
|
42
|
+
import * as errore from 'errore'
|
|
43
|
+
|
|
44
|
+
const logger = createLogger(LogPrefix.WORKTREE)
|
|
45
|
+
const DEFAULT_WORKTREE_BASE_REF = 'HEAD'
|
|
46
|
+
|
|
47
|
+
async function resolveRequestedWorktreeBaseRef({
|
|
48
|
+
projectDirectory,
|
|
49
|
+
rawBaseBranch,
|
|
50
|
+
}: {
|
|
51
|
+
projectDirectory: string
|
|
52
|
+
rawBaseBranch?: string
|
|
53
|
+
}): Promise<string | Error> {
|
|
54
|
+
if (!rawBaseBranch) {
|
|
55
|
+
// Default to the current local HEAD so worktrees can branch from
|
|
56
|
+
// unpublished commits in the main checkout.
|
|
57
|
+
return DEFAULT_WORKTREE_BASE_REF
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return validateBranchRef({
|
|
61
|
+
directory: projectDirectory,
|
|
62
|
+
ref: rawBaseBranch,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Status message shown while a worktree is being created. */
|
|
67
|
+
export function worktreeCreatingMessage(worktreeName: string): string {
|
|
68
|
+
return `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class WorktreeError extends Error {
|
|
72
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
73
|
+
super(message, options)
|
|
74
|
+
this.name = 'WorktreeError'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Lowercase, collapse whitespace to dashes, drop non-[a-z0-9-] chars.
|
|
80
|
+
* Does NOT add the `opencode/otto-` prefix — callers do that so they can
|
|
81
|
+
* optionally compress the slug first for auto-derived names.
|
|
82
|
+
*/
|
|
83
|
+
export function slugifyWorktreeName(name: string): string {
|
|
84
|
+
return name
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.trim()
|
|
87
|
+
.replace(/\s+/g, '-')
|
|
88
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Compress a slug by stripping vowels from each dash-separated word, but
|
|
93
|
+
* keeping the first character so the word stays recognizable.
|
|
94
|
+
* Only applied to slugs longer than 20 chars — short names are left alone.
|
|
95
|
+
*
|
|
96
|
+
* "configurable-sidebar-width-by-component" → "cnfgrbl-sdbr-wdth-by-cmpnnt"
|
|
97
|
+
*
|
|
98
|
+
* Used ONLY for auto-derived worktree names (thread name, prompt slug)
|
|
99
|
+
* so long Discord titles don't produce 80-char folder paths that make
|
|
100
|
+
* the agent lazy and reuse the previous worktree. User-provided names
|
|
101
|
+
* via `--worktree <name>` or `/new-worktree name:` are never compressed.
|
|
102
|
+
*/
|
|
103
|
+
export function shortenWorktreeSlug(slug: string): string {
|
|
104
|
+
if (slug.length <= 20) {
|
|
105
|
+
return slug
|
|
106
|
+
}
|
|
107
|
+
const shortened = slug
|
|
108
|
+
.split('-')
|
|
109
|
+
.map((word) => {
|
|
110
|
+
if (!word) {
|
|
111
|
+
return word
|
|
112
|
+
}
|
|
113
|
+
const first = word[0]
|
|
114
|
+
const rest = word.slice(1).replace(/[aeiou]/g, '')
|
|
115
|
+
return first + rest
|
|
116
|
+
})
|
|
117
|
+
.join('-')
|
|
118
|
+
return shortened || slug
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Format worktree name: lowercase, spaces to dashes, remove special chars, add opencode/otto- prefix.
|
|
123
|
+
* "My Feature" → "opencode/otto-my-feature"
|
|
124
|
+
* Returns empty string if no valid name can be extracted.
|
|
125
|
+
*
|
|
126
|
+
* This is the "explicit" path used when the user provides a specific name.
|
|
127
|
+
* The slug is NOT compressed — if you ask for `my-long-explicit-branch-name`
|
|
128
|
+
* you get `opencode/otto-my-long-explicit-branch-name` verbatim.
|
|
129
|
+
*/
|
|
130
|
+
export function formatWorktreeName(name: string): string {
|
|
131
|
+
const slug = slugifyWorktreeName(name)
|
|
132
|
+
if (!slug) {
|
|
133
|
+
return ''
|
|
134
|
+
}
|
|
135
|
+
return `opencode/otto-${slug}`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Format an auto-derived worktree name (from a Discord thread title or a
|
|
140
|
+
* prompt). Same as formatWorktreeName but compresses slugs longer than 20
|
|
141
|
+
* chars by stripping vowels so the on-disk folder name stays short.
|
|
142
|
+
*/
|
|
143
|
+
export function formatAutoWorktreeName(name: string): string {
|
|
144
|
+
const slug = slugifyWorktreeName(name)
|
|
145
|
+
if (!slug) {
|
|
146
|
+
return ''
|
|
147
|
+
}
|
|
148
|
+
return `opencode/otto-${shortenWorktreeSlug(slug)}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Derive worktree name from thread name.
|
|
153
|
+
* Handles existing "⬦ worktree: opencode/otto-name" (or legacy opencode/otto-name) format,
|
|
154
|
+
* or uses thread name directly. Uses formatAutoWorktreeName so long thread titles get vowel-compressed.
|
|
155
|
+
*/
|
|
156
|
+
function deriveWorktreeNameFromThread(threadName: string): string {
|
|
157
|
+
// Handle existing "⬦ worktree: opencode/otto-name" (or legacy otto-) format
|
|
158
|
+
const worktreeMatch = threadName.match(/worktree:\s*(.+)$/i)
|
|
159
|
+
const extractedName = worktreeMatch?.[1]?.trim()
|
|
160
|
+
if (extractedName) {
|
|
161
|
+
// If already has opencode/otto- or legacy opencode/otto- prefix, return as is
|
|
162
|
+
if (extractedName.startsWith('opencode/otto-') || extractedName.startsWith('opencode/otto-')) {
|
|
163
|
+
return extractedName
|
|
164
|
+
}
|
|
165
|
+
return formatAutoWorktreeName(extractedName)
|
|
166
|
+
}
|
|
167
|
+
// Use thread name directly (compressed if > 20 chars)
|
|
168
|
+
return formatAutoWorktreeName(threadName)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get project directory from database.
|
|
173
|
+
*/
|
|
174
|
+
async function getProjectDirectoryFromChannel(
|
|
175
|
+
channel: TextChannel,
|
|
176
|
+
): Promise<string | WorktreeError> {
|
|
177
|
+
const channelConfig = await getChannelDirectory(channel.id)
|
|
178
|
+
|
|
179
|
+
if (!channelConfig) {
|
|
180
|
+
return new WorktreeError(
|
|
181
|
+
'This channel is not configured with a project directory',
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!fs.existsSync(channelConfig.directory)) {
|
|
186
|
+
return new WorktreeError(
|
|
187
|
+
`Directory does not exist: ${channelConfig.directory}`,
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return channelConfig.directory
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Create worktree and update the status message when done.
|
|
196
|
+
* Handles the full lifecycle: pending DB entry, git creation, DB ready/error,
|
|
197
|
+
* tree emoji reaction, and editing the status message.
|
|
198
|
+
*
|
|
199
|
+
* starterMessage is optional — if omitted, status edits are skipped (creation
|
|
200
|
+
* still proceeds). This keeps worktree creation independent of Discord message
|
|
201
|
+
* delivery, so a transient send failure never silently skips the worktree.
|
|
202
|
+
*
|
|
203
|
+
* Returns the worktree directory on success, or an Error on failure.
|
|
204
|
+
* Never throws — all internal errors are caught and returned as Error values.
|
|
205
|
+
*/
|
|
206
|
+
export async function createWorktreeInBackground({
|
|
207
|
+
thread,
|
|
208
|
+
starterMessage,
|
|
209
|
+
worktreeName,
|
|
210
|
+
projectDirectory,
|
|
211
|
+
baseBranch,
|
|
212
|
+
rest,
|
|
213
|
+
}: {
|
|
214
|
+
thread: ThreadChannel
|
|
215
|
+
starterMessage?: Message
|
|
216
|
+
worktreeName: string
|
|
217
|
+
projectDirectory: string
|
|
218
|
+
baseBranch?: string
|
|
219
|
+
rest: REST
|
|
220
|
+
}): Promise<string | Error> {
|
|
221
|
+
return errore.tryAsync({
|
|
222
|
+
try: async () => {
|
|
223
|
+
logger.log(
|
|
224
|
+
`Creating worktree "${worktreeName}" for project ${projectDirectory}${baseBranch ? ` from ${baseBranch}` : ''}`,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
await createPendingWorktree({
|
|
228
|
+
threadId: thread.id,
|
|
229
|
+
worktreeName,
|
|
230
|
+
projectDirectory,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// Serialize status message edits so onProgress can't overwrite the
|
|
234
|
+
// final success/error edit even if Discord's API is slow.
|
|
235
|
+
let editChain: Promise<void> = Promise.resolve()
|
|
236
|
+
const editStatus = (content: string) => {
|
|
237
|
+
editChain = editChain
|
|
238
|
+
.then(async () => {
|
|
239
|
+
await starterMessage?.edit(content)
|
|
240
|
+
})
|
|
241
|
+
.catch(() => {})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const worktreeResult = await createWorktreeWithSubmodules({
|
|
245
|
+
directory: projectDirectory,
|
|
246
|
+
name: worktreeName,
|
|
247
|
+
baseBranch,
|
|
248
|
+
onProgress: (phase) => {
|
|
249
|
+
editStatus(`🌳 **Worktree: ${worktreeName}**\n${phase}`)
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
if (worktreeResult instanceof Error) {
|
|
254
|
+
const errorMsg = worktreeResult.message
|
|
255
|
+
logger.error('[WORKTREE] Creation failed:', worktreeResult)
|
|
256
|
+
await setWorktreeError({ threadId: thread.id, errorMessage: errorMsg })
|
|
257
|
+
editStatus(`🌳 **Worktree: ${worktreeName}**\n❌ ${errorMsg}`)
|
|
258
|
+
await editChain
|
|
259
|
+
return worktreeResult
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Success - update database and edit starter message
|
|
263
|
+
await setWorktreeReady({
|
|
264
|
+
threadId: thread.id,
|
|
265
|
+
worktreeDirectory: worktreeResult.directory,
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
await denyPreviousCheckoutForExistingSession({
|
|
269
|
+
threadId: thread.id,
|
|
270
|
+
projectDirectory,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// React with tree emoji to mark as worktree thread
|
|
274
|
+
await reactToThread({
|
|
275
|
+
rest,
|
|
276
|
+
threadId: thread.id,
|
|
277
|
+
channelId: thread.parentId || undefined,
|
|
278
|
+
emoji: '🌳',
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
editStatus(
|
|
282
|
+
`🌳 **Worktree: ${worktreeName}**\n` +
|
|
283
|
+
`📁 \`${worktreeResult.directory}\`\n` +
|
|
284
|
+
`🌿 Branch: \`${worktreeResult.branch}\``,
|
|
285
|
+
)
|
|
286
|
+
await editChain
|
|
287
|
+
|
|
288
|
+
return worktreeResult.directory
|
|
289
|
+
},
|
|
290
|
+
catch: (e) => {
|
|
291
|
+
logger.error('[WORKTREE] Unexpected error in createWorktreeInBackground:', e)
|
|
292
|
+
return new Error(`Worktree creation failed: ${e instanceof Error ? e.message : String(e)}`, { cause: e })
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function denyPreviousCheckoutForExistingSession({
|
|
298
|
+
threadId,
|
|
299
|
+
projectDirectory,
|
|
300
|
+
}: {
|
|
301
|
+
threadId: string
|
|
302
|
+
projectDirectory: string
|
|
303
|
+
}): Promise<void> {
|
|
304
|
+
const sessionId = await getThreadSession(threadId)
|
|
305
|
+
if (!sessionId) {
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const initializeResult = await initializeOpencodeForDirectory(projectDirectory)
|
|
310
|
+
if (initializeResult instanceof Error) {
|
|
311
|
+
logger.warn(
|
|
312
|
+
`[WORKTREE] Failed to initialize OpenCode before denying previous checkout for thread ${threadId}: ${initializeResult.message}`,
|
|
313
|
+
)
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const client = getOpencodeClient(projectDirectory)
|
|
318
|
+
if (!client) {
|
|
319
|
+
logger.warn(
|
|
320
|
+
`[WORKTREE] Missing OpenCode client for previous checkout deny update in thread ${threadId}`,
|
|
321
|
+
)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const updateResult = await errore.tryAsync({
|
|
326
|
+
try: async () => {
|
|
327
|
+
// SDK types don't include 'permission' yet — upstream added this API
|
|
328
|
+
// for session permission updates (worktree isolation). Cast to bypass.
|
|
329
|
+
await client.session.update({
|
|
330
|
+
sessionID: sessionId,
|
|
331
|
+
permission: buildExternalDirectoryPermissionRules({
|
|
332
|
+
resolvedPattern: projectDirectory.replaceAll('\\', '/'),
|
|
333
|
+
action: 'deny',
|
|
334
|
+
}),
|
|
335
|
+
} as Parameters<typeof client.session.update>[0])
|
|
336
|
+
},
|
|
337
|
+
catch: (e) =>
|
|
338
|
+
new Error('Failed to deny previous checkout for existing session', {
|
|
339
|
+
cause: e,
|
|
340
|
+
}),
|
|
341
|
+
})
|
|
342
|
+
if (updateResult instanceof Error) {
|
|
343
|
+
logger.warn(
|
|
344
|
+
`[WORKTREE] Failed to deny previous checkout for existing session in thread ${threadId}: ${updateResult.message}`,
|
|
345
|
+
)
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
logger.log(
|
|
350
|
+
`[WORKTREE] Denied previous checkout for existing session ${sessionId} in thread ${threadId}`,
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function findExistingWorktreePath({
|
|
355
|
+
projectDirectory,
|
|
356
|
+
worktreeName,
|
|
357
|
+
}: {
|
|
358
|
+
projectDirectory: string
|
|
359
|
+
worktreeName: string
|
|
360
|
+
}): Promise<string | undefined | Error> {
|
|
361
|
+
const listResult = await errore.tryAsync({
|
|
362
|
+
try: () =>
|
|
363
|
+
execAsync('git worktree list --porcelain', { cwd: projectDirectory }),
|
|
364
|
+
catch: (e) => new WorktreeError('Failed to list worktrees', { cause: e }),
|
|
365
|
+
})
|
|
366
|
+
if (errore.isError(listResult)) {
|
|
367
|
+
return listResult
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const lines = listResult.stdout.split('\n')
|
|
371
|
+
let currentPath = ''
|
|
372
|
+
const branchRef = `refs/heads/${worktreeName}`
|
|
373
|
+
|
|
374
|
+
for (const line of lines) {
|
|
375
|
+
if (line.startsWith('worktree ')) {
|
|
376
|
+
currentPath = line.slice('worktree '.length)
|
|
377
|
+
continue
|
|
378
|
+
}
|
|
379
|
+
if (
|
|
380
|
+
line.startsWith('branch ') &&
|
|
381
|
+
line.slice('branch '.length) === branchRef
|
|
382
|
+
) {
|
|
383
|
+
return currentPath || undefined
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return undefined
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export async function handleNewWorktreeCommand({
|
|
391
|
+
command,
|
|
392
|
+
}: CommandContext): Promise<void> {
|
|
393
|
+
await command.deferReply()
|
|
394
|
+
|
|
395
|
+
const channel = command.channel
|
|
396
|
+
if (!channel) {
|
|
397
|
+
await command.editReply('Cannot determine channel')
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Handle command in existing thread - attach worktree to this thread
|
|
402
|
+
if (
|
|
403
|
+
channel.type === ChannelType.PublicThread ||
|
|
404
|
+
channel.type === ChannelType.PrivateThread
|
|
405
|
+
) {
|
|
406
|
+
await handleWorktreeInThread({
|
|
407
|
+
command,
|
|
408
|
+
thread: channel,
|
|
409
|
+
})
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Handle command in text channel - create new thread with worktree (existing behavior)
|
|
414
|
+
if (channel.type !== ChannelType.GuildText) {
|
|
415
|
+
await command.editReply(
|
|
416
|
+
'This command can only be used in text channels or threads',
|
|
417
|
+
)
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const rawName = command.options.getString('name')
|
|
422
|
+
const rawBaseBranch = command.options.getString('base-branch') || undefined
|
|
423
|
+
if (!rawName) {
|
|
424
|
+
await command.editReply(
|
|
425
|
+
'Name is required when creating a worktree from a text channel. Use `/new-worktree name:my-feature`',
|
|
426
|
+
)
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const worktreeName = formatWorktreeName(rawName)
|
|
431
|
+
if (!worktreeName) {
|
|
432
|
+
await command.editReply(
|
|
433
|
+
'Invalid worktree name. Please use letters, numbers, and spaces.',
|
|
434
|
+
)
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const textChannel = channel
|
|
439
|
+
|
|
440
|
+
const projectDirectory = await getProjectDirectoryFromChannel(
|
|
441
|
+
textChannel,
|
|
442
|
+
)
|
|
443
|
+
if (errore.isError(projectDirectory)) {
|
|
444
|
+
await command.editReply(projectDirectory.message)
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const baseBranch = await resolveRequestedWorktreeBaseRef({
|
|
449
|
+
projectDirectory,
|
|
450
|
+
rawBaseBranch,
|
|
451
|
+
})
|
|
452
|
+
if (baseBranch instanceof Error) {
|
|
453
|
+
await command.editReply(`Invalid base branch: \`${rawBaseBranch}\``)
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const existingWorktree = await findExistingWorktreePath({
|
|
458
|
+
projectDirectory,
|
|
459
|
+
worktreeName,
|
|
460
|
+
})
|
|
461
|
+
if (errore.isError(existingWorktree)) {
|
|
462
|
+
await command.editReply(existingWorktree.message)
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
if (existingWorktree) {
|
|
466
|
+
await command.editReply(
|
|
467
|
+
`Worktree \`${worktreeName}\` already exists at \`${existingWorktree}\``,
|
|
468
|
+
)
|
|
469
|
+
return
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Create thread immediately so user can start typing
|
|
473
|
+
const result = await errore.tryAsync({
|
|
474
|
+
try: async () => {
|
|
475
|
+
const starterMessage = await textChannel.send({
|
|
476
|
+
content: worktreeCreatingMessage(worktreeName),
|
|
477
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
const thread = await starterMessage.startThread({
|
|
481
|
+
name: `${WORKTREE_PREFIX}worktree: ${worktreeName}`,
|
|
482
|
+
autoArchiveDuration: 1440,
|
|
483
|
+
reason: 'Worktree session',
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
// Add user to thread so it appears in their sidebar
|
|
487
|
+
await thread.members.add(command.user.id)
|
|
488
|
+
|
|
489
|
+
return { thread, starterMessage }
|
|
490
|
+
},
|
|
491
|
+
catch: (e) => new WorktreeError('Failed to create thread', { cause: e }),
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
if (errore.isError(result)) {
|
|
495
|
+
logger.error('[NEW-WORKTREE] Error:', result.cause)
|
|
496
|
+
await command.editReply(result.message)
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const { thread, starterMessage } = result
|
|
501
|
+
|
|
502
|
+
await command.editReply(`Creating worktree in ${thread.toString()}`)
|
|
503
|
+
|
|
504
|
+
// Create worktree in background (don't await)
|
|
505
|
+
createWorktreeInBackground({
|
|
506
|
+
thread,
|
|
507
|
+
starterMessage,
|
|
508
|
+
worktreeName,
|
|
509
|
+
projectDirectory,
|
|
510
|
+
baseBranch,
|
|
511
|
+
rest: command.client.rest,
|
|
512
|
+
}).catch((e) => {
|
|
513
|
+
logger.error('[NEW-WORKTREE] Background error:', e)
|
|
514
|
+
void notifyError(e, 'Background worktree creation failed')
|
|
515
|
+
})
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Handle /new-worktree when called inside an existing thread.
|
|
520
|
+
* Attaches a worktree to the current thread, using thread name if no name provided.
|
|
521
|
+
*/
|
|
522
|
+
async function handleWorktreeInThread({
|
|
523
|
+
command,
|
|
524
|
+
thread,
|
|
525
|
+
}: {
|
|
526
|
+
command: CommandContext['command']
|
|
527
|
+
thread: ThreadChannel
|
|
528
|
+
}): Promise<void> {
|
|
529
|
+
// Error if thread already has a worktree
|
|
530
|
+
if (await getThreadWorktree(thread.id)) {
|
|
531
|
+
await command.editReply('This thread already has a worktree attached.')
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Get worktree name from parameter or derive from thread name
|
|
536
|
+
const rawName = command.options.getString('name')
|
|
537
|
+
const rawBaseBranch = command.options.getString('base-branch') || undefined
|
|
538
|
+
const worktreeName = rawName
|
|
539
|
+
? formatWorktreeName(rawName)
|
|
540
|
+
: deriveWorktreeNameFromThread(thread.name)
|
|
541
|
+
|
|
542
|
+
if (!worktreeName) {
|
|
543
|
+
await command.editReply(
|
|
544
|
+
'Invalid worktree name. Please provide a name or rename the thread.',
|
|
545
|
+
)
|
|
546
|
+
return
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Get parent channel for project directory
|
|
550
|
+
const parent = thread.parent
|
|
551
|
+
if (!parent || parent.type !== ChannelType.GuildText) {
|
|
552
|
+
await command.editReply('Cannot determine parent channel')
|
|
553
|
+
return
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const projectDirectory = await getProjectDirectoryFromChannel(
|
|
557
|
+
parent,
|
|
558
|
+
)
|
|
559
|
+
if (errore.isError(projectDirectory)) {
|
|
560
|
+
await command.editReply(projectDirectory.message)
|
|
561
|
+
return
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const baseBranch = await resolveRequestedWorktreeBaseRef({
|
|
565
|
+
projectDirectory,
|
|
566
|
+
rawBaseBranch,
|
|
567
|
+
})
|
|
568
|
+
if (baseBranch instanceof Error) {
|
|
569
|
+
await command.editReply(`Invalid base branch: \`${rawBaseBranch}\``)
|
|
570
|
+
return
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const existingWorktreePath = await findExistingWorktreePath({
|
|
574
|
+
projectDirectory,
|
|
575
|
+
worktreeName,
|
|
576
|
+
})
|
|
577
|
+
if (errore.isError(existingWorktreePath)) {
|
|
578
|
+
await command.editReply(existingWorktreePath.message)
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
if (existingWorktreePath) {
|
|
582
|
+
await command.editReply(
|
|
583
|
+
`Worktree \`${worktreeName}\` already exists at \`${existingWorktreePath}\``,
|
|
584
|
+
)
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Send status message in thread
|
|
589
|
+
const statusMessage = await thread.send({
|
|
590
|
+
content: worktreeCreatingMessage(worktreeName),
|
|
591
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
await command.editReply(
|
|
595
|
+
`Creating worktree \`${worktreeName}\` for this thread...`,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
createWorktreeInBackground({
|
|
599
|
+
thread,
|
|
600
|
+
starterMessage: statusMessage,
|
|
601
|
+
worktreeName,
|
|
602
|
+
projectDirectory,
|
|
603
|
+
baseBranch,
|
|
604
|
+
rest: command.client.rest,
|
|
605
|
+
}).catch((e) => {
|
|
606
|
+
logger.error('[NEW-WORKTREE] Background error:', e)
|
|
607
|
+
void notifyError(e, 'Background worktree creation failed (in-thread)')
|
|
608
|
+
})
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Autocomplete handler for /new-worktree base-branch option.
|
|
613
|
+
* Lists local + remote branches sorted by most recent commit date.
|
|
614
|
+
*/
|
|
615
|
+
export async function handleNewWorktreeAutocomplete({
|
|
616
|
+
interaction,
|
|
617
|
+
}: AutocompleteContext): Promise<void> {
|
|
618
|
+
try {
|
|
619
|
+
const focusedValue = interaction.options.getFocused()
|
|
620
|
+
|
|
621
|
+
// interaction.channel can be null when the channel isn't cached
|
|
622
|
+
// (common with gateway-proxy). Use channelId which is always available
|
|
623
|
+
// from the raw interaction payload.
|
|
624
|
+
const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction)
|
|
625
|
+
|
|
626
|
+
if (!projectDirectory) {
|
|
627
|
+
await interaction.respond([])
|
|
628
|
+
return
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const branches = await listBranchesByLastCommit({
|
|
632
|
+
directory: projectDirectory,
|
|
633
|
+
query: focusedValue,
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
await interaction.respond(
|
|
637
|
+
branches.map((name) => {
|
|
638
|
+
return { name, value: name }
|
|
639
|
+
}),
|
|
640
|
+
)
|
|
641
|
+
} catch (e) {
|
|
642
|
+
logger.error('[NEW-WORKTREE] Autocomplete error:', e)
|
|
643
|
+
await interaction.respond([]).catch(() => {})
|
|
644
|
+
}
|
|
645
|
+
}
|