@otto-assistant/bridge 0.4.92
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-auth-plugin.js +728 -0
- package/dist/anthropic-auth-plugin.test.js +125 -0
- package/dist/anthropic-auth-state.js +231 -0
- package/dist/bin.js +90 -0
- package/dist/channel-management.js +227 -0
- package/dist/cli-parsing.test.js +137 -0
- package/dist/cli-send-thread.e2e.test.js +356 -0
- package/dist/cli.js +3276 -0
- package/dist/commands/abort.js +65 -0
- package/dist/commands/action-buttons.js +245 -0
- package/dist/commands/add-project.js +113 -0
- package/dist/commands/agent.js +335 -0
- package/dist/commands/ask-question.js +274 -0
- package/dist/commands/btw.js +116 -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/file-upload.js +275 -0
- package/dist/commands/fork.js +220 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +885 -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 +159 -0
- package/dist/commands/model-variant.js +364 -0
- package/dist/commands/model.js +776 -0
- package/dist/commands/new-worktree.js +366 -0
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/permissions.js +274 -0
- package/dist/commands/queue.js +206 -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/types.js +2 -0
- package/dist/commands/undo-redo.js +305 -0
- package/dist/commands/unset-model.js +138 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +155 -0
- package/dist/commands/verbosity.js +125 -0
- package/dist/commands/worktree-settings.js +43 -0
- package/dist/commands/worktrees.js +410 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +94 -0
- package/dist/context-awareness-plugin.js +363 -0
- package/dist/context-awareness-plugin.test.js +124 -0
- package/dist/critique-utils.js +95 -0
- package/dist/database.js +1310 -0
- package/dist/db.js +251 -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 +1008 -0
- package/dist/discord-command-registration.js +524 -0
- package/dist/discord-urls.js +81 -0
- package/dist/discord-utils.js +591 -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 +302 -0
- package/dist/format-tables.test.js +308 -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 +483 -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 +52 -0
- package/dist/generated/internal/class.js +49 -0
- package/dist/generated/internal/prismaNamespace.js +253 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +223 -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 +263 -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 +397 -0
- package/dist/ipc-polling.js +252 -0
- package/dist/ipc-tools-plugin.js +193 -0
- package/dist/kimaki-digital-twin.e2e.test.js +161 -0
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
- package/dist/kimaki-opencode-plugin.js +17 -0
- package/dist/kimaki-opencode-plugin.test.js +98 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +165 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +257 -0
- package/dist/message-finish-field.e2e.test.js +165 -0
- package/dist/message-formatting.js +413 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/message-preprocessing.js +330 -0
- package/dist/onboarding-tutorial.js +172 -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 +129 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +361 -0
- package/dist/opencode-interrupt-plugin.test.js +458 -0
- package/dist/opencode.js +861 -0
- package/dist/otto/branding.js +22 -0
- package/dist/otto/index.js +21 -0
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/patch-text-parser.js +97 -0
- package/dist/plugin-logger.js +59 -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 +786 -0
- package/dist/queue-advanced-footer.e2e.test.js +472 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -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 +120 -0
- package/dist/runtime-idle-sweeper.js +52 -0
- package/dist/runtime-lifecycle.e2e.test.js +508 -0
- package/dist/sentry.js +23 -0
- package/dist/session-handler/agent-utils.js +67 -0
- package/dist/session-handler/event-stream-state.js +420 -0
- package/dist/session-handler/event-stream-state.test.js +563 -0
- package/dist/session-handler/model-utils.js +124 -0
- package/dist/session-handler/opencode-session-event-log.js +94 -0
- package/dist/session-handler/thread-runtime-state.js +104 -0
- package/dist/session-handler/thread-session-runtime.js +3258 -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 +80 -0
- package/dist/startup-service.js +153 -0
- package/dist/startup-time.e2e.test.js +296 -0
- package/dist/store.js +17 -0
- package/dist/system-message.js +613 -0
- package/dist/system-message.test.js +602 -0
- package/dist/task-runner.js +295 -0
- package/dist/task-schedule.js +209 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/test-utils.js +299 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +999 -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 +114 -0
- package/dist/utils.js +144 -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 +447 -0
- package/dist/voice.test.js +235 -0
- package/dist/wait-session.js +94 -0
- package/dist/websockify.js +69 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-lifecycle.e2e.test.js +308 -0
- package/dist/worktree-utils.js +3 -0
- package/dist/worktrees.js +929 -0
- package/dist/worktrees.test.js +189 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +98 -0
- package/schema.prisma +295 -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/gitchamber/SKILL.md +93 -0
- package/skills/goke/SKILL.md +644 -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/new-skill/SKILL.md +211 -0
- package/skills/npm-package/SKILL.md +239 -0
- package/skills/playwriter/SKILL.md +35 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/spiceflow/SKILL.md +14 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/usecomputer/SKILL.md +264 -0
- package/skills/x-articles/SKILL.md +554 -0
- package/skills/zele/SKILL.md +112 -0
- package/skills/zustand-centralized-state/SKILL.md +1004 -0
- package/src/agent-model.e2e.test.ts +976 -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-auth-plugin.test.ts +159 -0
- package/src/anthropic-auth-plugin.ts +861 -0
- package/src/anthropic-auth-state.ts +282 -0
- package/src/bin.ts +111 -0
- package/src/channel-management.ts +334 -0
- package/src/cli-parsing.test.ts +195 -0
- package/src/cli-send-thread.e2e.test.ts +464 -0
- package/src/cli.ts +4581 -0
- package/src/commands/abort.ts +89 -0
- package/src/commands/action-buttons.ts +364 -0
- package/src/commands/add-project.ts +149 -0
- package/src/commands/agent.ts +473 -0
- package/src/commands/ask-question.ts +390 -0
- package/src/commands/btw.ts +164 -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/file-upload.ts +389 -0
- package/src/commands/fork.ts +321 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +1173 -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 +223 -0
- package/src/commands/model-variant.ts +483 -0
- package/src/commands/model.ts +1053 -0
- package/src/commands/new-worktree.ts +510 -0
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/permissions.ts +397 -0
- package/src/commands/queue.ts +271 -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/types.ts +25 -0
- package/src/commands/undo-redo.ts +386 -0
- package/src/commands/unset-model.ts +173 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +198 -0
- package/src/commands/verbosity.ts +173 -0
- package/src/commands/worktree-settings.ts +70 -0
- package/src/commands/worktrees.ts +552 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +111 -0
- package/src/context-awareness-plugin.test.ts +142 -0
- package/src/context-awareness-plugin.ts +510 -0
- package/src/critique-utils.ts +139 -0
- package/src/database.ts +1876 -0
- package/src/db.test.ts +162 -0
- package/src/db.ts +286 -0
- package/src/debounce-timeout.ts +43 -0
- package/src/debounced-process-flush.ts +104 -0
- package/src/discord-bot.ts +1330 -0
- package/src/discord-command-registration.ts +693 -0
- package/src/discord-urls.ts +88 -0
- package/src/discord-utils.test.ts +153 -0
- package/src/discord-utils.ts +800 -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 +335 -0
- package/src/format-tables.ts +445 -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 +640 -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 +736 -0
- package/src/generated/enums.ts +88 -0
- package/src/generated/internal/class.ts +384 -0
- package/src/generated/internal/prismaNamespace.ts +2386 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1656 -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 +314 -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 +576 -0
- package/src/ipc-polling.ts +326 -0
- package/src/ipc-tools-plugin.ts +236 -0
- package/src/kimaki-digital-twin.e2e.test.ts +199 -0
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
- package/src/kimaki-opencode-plugin.test.ts +108 -0
- package/src/kimaki-opencode-plugin.ts +18 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +208 -0
- package/src/markdown.test.ts +308 -0
- package/src/markdown.ts +410 -0
- package/src/message-finish-field.e2e.test.ts +192 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +533 -0
- package/src/message-preprocessing.ts +455 -0
- package/src/onboarding-tutorial.ts +176 -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 +188 -0
- package/src/opencode-interrupt-plugin.test.ts +677 -0
- package/src/opencode-interrupt-plugin.ts +477 -0
- package/src/opencode.ts +1110 -0
- package/src/otto/branding.ts +23 -0
- package/src/otto/index.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 +68 -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 +873 -0
- package/src/queue-advanced-footer.e2e.test.ts +576 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -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 +152 -0
- package/src/runtime-idle-sweeper.ts +76 -0
- package/src/runtime-lifecycle.e2e.test.ts +641 -0
- package/src/schema.sql +173 -0
- package/src/sentry.ts +26 -0
- package/src/session-handler/agent-utils.ts +97 -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 +645 -0
- package/src/session-handler/event-stream-state.ts +608 -0
- package/src/session-handler/model-utils.ts +183 -0
- package/src/session-handler/opencode-session-event-log.ts +130 -0
- package/src/session-handler/thread-runtime-state.ts +212 -0
- package/src/session-handler/thread-session-runtime.ts +4281 -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 +112 -0
- package/src/startup-service.ts +200 -0
- package/src/startup-time.e2e.test.ts +373 -0
- package/src/store.ts +122 -0
- package/src/system-message.test.ts +612 -0
- package/src/system-message.ts +723 -0
- package/src/task-runner.ts +421 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +311 -0
- package/src/test-utils.ts +435 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +1219 -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 +127 -0
- package/src/utils.ts +212 -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 +627 -0
- package/src/wait-session.ts +147 -0
- package/src/websockify.ts +101 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-lifecycle.e2e.test.ts +391 -0
- package/src/worktree-utils.ts +4 -0
- package/src/worktrees.test.ts +223 -0
- package/src/worktrees.ts +1294 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// /mcp command - List and toggle MCP servers for the current project.
|
|
2
|
+
// Uses OpenCode SDK mcp.status/connect/disconnect to manage servers.
|
|
3
|
+
// MCP state is project-scoped (per channel), not per thread or session.
|
|
4
|
+
// No database storage needed — state lives in OpenCode's config.
|
|
5
|
+
|
|
6
|
+
import crypto from 'node:crypto'
|
|
7
|
+
import {
|
|
8
|
+
MessageFlags,
|
|
9
|
+
StringSelectMenuBuilder,
|
|
10
|
+
ActionRowBuilder,
|
|
11
|
+
ChannelType,
|
|
12
|
+
type StringSelectMenuInteraction,
|
|
13
|
+
type TextChannel,
|
|
14
|
+
type ThreadChannel,
|
|
15
|
+
} from 'discord.js'
|
|
16
|
+
import type { McpStatus } from '@opencode-ai/sdk/v2'
|
|
17
|
+
import type { CommandContext } from './types.js'
|
|
18
|
+
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
19
|
+
import {
|
|
20
|
+
resolveWorkingDirectory,
|
|
21
|
+
SILENT_MESSAGE_FLAGS,
|
|
22
|
+
} from '../discord-utils.js'
|
|
23
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
24
|
+
|
|
25
|
+
const logger = createLogger(LogPrefix.MCP)
|
|
26
|
+
|
|
27
|
+
// Short-lived context map: contextHash → projectDirectory.
|
|
28
|
+
// Avoids embedding long directory paths in Discord customId (100 char limit).
|
|
29
|
+
// Entries auto-expire after 5 minutes to prevent unbounded growth from
|
|
30
|
+
// abandoned menus (user runs /mcp but never clicks the select menu).
|
|
31
|
+
const MCP_CONTEXT_TTL_MS = 5 * 60_000
|
|
32
|
+
const pendingMcpContexts = new Map<string, string>()
|
|
33
|
+
|
|
34
|
+
const STATUS_LABELS: Record<string, string> = {
|
|
35
|
+
connected: 'connected',
|
|
36
|
+
disabled: 'disabled',
|
|
37
|
+
failed: 'failed',
|
|
38
|
+
needs_auth: 'needs auth',
|
|
39
|
+
needs_client_registration: 'needs registration',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatStatusLabel(status: string): string {
|
|
43
|
+
return STATUS_LABELS[status] || status
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Extract error string from McpStatus using discriminated union narrowing. */
|
|
47
|
+
function getStatusError(info: McpStatus): string | undefined {
|
|
48
|
+
if (info.status === 'failed') {
|
|
49
|
+
return info.error
|
|
50
|
+
}
|
|
51
|
+
if (info.status === 'needs_client_registration') {
|
|
52
|
+
return info.error
|
|
53
|
+
}
|
|
54
|
+
return undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Build a one-line description for a server entry in the list. */
|
|
58
|
+
export function formatServerLine({
|
|
59
|
+
name,
|
|
60
|
+
status,
|
|
61
|
+
error,
|
|
62
|
+
}: {
|
|
63
|
+
name: string
|
|
64
|
+
status: string
|
|
65
|
+
error?: string
|
|
66
|
+
}): string {
|
|
67
|
+
const label = formatStatusLabel(status)
|
|
68
|
+
const errorSuffix = error ? ` — ${error}` : ''
|
|
69
|
+
return `\`${label}\` **${name}**${errorSuffix}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Determine the select menu option label for toggling a server. */
|
|
73
|
+
export function toggleActionLabel(status: string): string {
|
|
74
|
+
if (status === 'connected') {
|
|
75
|
+
return 'disconnect'
|
|
76
|
+
}
|
|
77
|
+
if (status === 'failed') {
|
|
78
|
+
return 'reconnect'
|
|
79
|
+
}
|
|
80
|
+
return 'connect'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function handleMcpCommand({
|
|
84
|
+
command,
|
|
85
|
+
}: CommandContext): Promise<void> {
|
|
86
|
+
const channel = command.channel
|
|
87
|
+
if (!channel) {
|
|
88
|
+
await command.reply({
|
|
89
|
+
content: 'This command can only be used in a channel.',
|
|
90
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
91
|
+
})
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const isThread = [
|
|
96
|
+
ChannelType.PublicThread,
|
|
97
|
+
ChannelType.PrivateThread,
|
|
98
|
+
ChannelType.AnnouncementThread,
|
|
99
|
+
].includes(channel.type)
|
|
100
|
+
const isTextChannel = channel.type === ChannelType.GuildText
|
|
101
|
+
|
|
102
|
+
if (!isThread && !isTextChannel) {
|
|
103
|
+
await command.reply({
|
|
104
|
+
content: 'This command can only be used in text channels or threads.',
|
|
105
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
106
|
+
})
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const resolved = await resolveWorkingDirectory({
|
|
111
|
+
channel: channel as TextChannel | ThreadChannel,
|
|
112
|
+
})
|
|
113
|
+
if (!resolved) {
|
|
114
|
+
await command.reply({
|
|
115
|
+
content: 'Could not determine project directory for this channel.',
|
|
116
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
117
|
+
})
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { projectDirectory } = resolved
|
|
122
|
+
|
|
123
|
+
await command.deferReply({ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS })
|
|
124
|
+
|
|
125
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
126
|
+
if (getClient instanceof Error) {
|
|
127
|
+
await command.editReply({
|
|
128
|
+
content: `Failed to connect to OpenCode server: ${getClient.message}`,
|
|
129
|
+
})
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const client = getClient()
|
|
134
|
+
const { data, error } = await client.mcp.status({
|
|
135
|
+
directory: projectDirectory,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
if (error || !data) {
|
|
139
|
+
await command.editReply({
|
|
140
|
+
content: 'Failed to fetch MCP server status.',
|
|
141
|
+
})
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const servers = Object.entries(data)
|
|
146
|
+
if (servers.length === 0) {
|
|
147
|
+
await command.editReply({
|
|
148
|
+
content:
|
|
149
|
+
'No MCP servers configured for this project.\nAdd MCP servers in your project\'s `opencode.json` configuration.',
|
|
150
|
+
})
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const lines = servers.map(([name, info]) => {
|
|
155
|
+
return formatServerLine({ name, status: info.status, error: getStatusError(info) })
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const content = `**MCP Servers** (project-wide)\n${lines.join('\n')}`
|
|
159
|
+
|
|
160
|
+
const contextHash = crypto.randomBytes(8).toString('hex')
|
|
161
|
+
pendingMcpContexts.set(contextHash, projectDirectory)
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
pendingMcpContexts.delete(contextHash)
|
|
164
|
+
}, MCP_CONTEXT_TTL_MS)
|
|
165
|
+
|
|
166
|
+
// Discord select option limits: label max 100 chars, description max 100 chars
|
|
167
|
+
const options = servers.map(([name, info]) => ({
|
|
168
|
+
label: name.slice(0, 100),
|
|
169
|
+
value: name.slice(0, 100),
|
|
170
|
+
description: `${formatStatusLabel(info.status)} — click to ${toggleActionLabel(info.status)}`.slice(0, 100),
|
|
171
|
+
}))
|
|
172
|
+
|
|
173
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
174
|
+
.setCustomId(`mcp_toggle:${contextHash}`)
|
|
175
|
+
.setPlaceholder('Select MCP server to toggle')
|
|
176
|
+
.addOptions(options.slice(0, 25)) // Discord max 25 options
|
|
177
|
+
|
|
178
|
+
const actionRow =
|
|
179
|
+
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
|
|
180
|
+
|
|
181
|
+
await command.editReply({
|
|
182
|
+
content,
|
|
183
|
+
components: [actionRow],
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function handleMcpSelectMenu(
|
|
188
|
+
interaction: StringSelectMenuInteraction,
|
|
189
|
+
): Promise<void> {
|
|
190
|
+
const customId = interaction.customId
|
|
191
|
+
if (!customId.startsWith('mcp_toggle:')) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await interaction.deferUpdate()
|
|
196
|
+
|
|
197
|
+
const contextHash = customId.slice('mcp_toggle:'.length)
|
|
198
|
+
const projectDirectory = pendingMcpContexts.get(contextHash)
|
|
199
|
+
|
|
200
|
+
if (!projectDirectory) {
|
|
201
|
+
await interaction.editReply({
|
|
202
|
+
content: 'Session expired. Run `/mcp` again.',
|
|
203
|
+
components: [],
|
|
204
|
+
})
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const serverName = interaction.values[0]
|
|
209
|
+
if (!serverName) {
|
|
210
|
+
await interaction.editReply({
|
|
211
|
+
content: 'No server selected.',
|
|
212
|
+
components: [],
|
|
213
|
+
})
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
pendingMcpContexts.delete(contextHash)
|
|
218
|
+
|
|
219
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
220
|
+
if (getClient instanceof Error) {
|
|
221
|
+
await interaction.editReply({
|
|
222
|
+
content: `Failed to connect to OpenCode server: ${getClient.message}`,
|
|
223
|
+
components: [],
|
|
224
|
+
})
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const client = getClient()
|
|
229
|
+
|
|
230
|
+
const { data: statusData, error: statusError } = await client.mcp.status({
|
|
231
|
+
directory: projectDirectory,
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
if (statusError || !statusData) {
|
|
235
|
+
await interaction.editReply({
|
|
236
|
+
content: 'Failed to refresh MCP server status.',
|
|
237
|
+
components: [],
|
|
238
|
+
})
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!statusData[serverName]) {
|
|
243
|
+
await interaction.editReply({
|
|
244
|
+
content: `Server **${serverName}** not found.`,
|
|
245
|
+
components: [],
|
|
246
|
+
})
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const serverInfo = statusData[serverName]
|
|
251
|
+
|
|
252
|
+
if (serverInfo.status === 'connected') {
|
|
253
|
+
const { error } = await client.mcp.disconnect({
|
|
254
|
+
name: serverName,
|
|
255
|
+
directory: projectDirectory,
|
|
256
|
+
})
|
|
257
|
+
if (error) {
|
|
258
|
+
logger.error(`[MCP] Failed to disconnect ${serverName}:`, error)
|
|
259
|
+
await interaction.editReply({
|
|
260
|
+
content: `Failed to disconnect **${serverName}**.`,
|
|
261
|
+
components: [],
|
|
262
|
+
})
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
logger.log(`[MCP] Disconnected server: ${serverName}`)
|
|
266
|
+
await interaction.editReply({
|
|
267
|
+
content: `**${serverName}** disconnected`,
|
|
268
|
+
components: [],
|
|
269
|
+
})
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (serverInfo.status === 'needs_auth') {
|
|
274
|
+
await interaction.editReply({
|
|
275
|
+
content: `**${serverName}** needs authentication.\nRun \`opencode\` in the project directory to complete the OAuth flow.`,
|
|
276
|
+
components: [],
|
|
277
|
+
})
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (serverInfo.status === 'needs_client_registration') {
|
|
282
|
+
await interaction.editReply({
|
|
283
|
+
content: `**${serverName}** needs client registration.${serverInfo.error ? `\n${serverInfo.error}` : ''}`,
|
|
284
|
+
components: [],
|
|
285
|
+
})
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Connect (handles disabled and failed)
|
|
290
|
+
const { error } = await client.mcp.connect({
|
|
291
|
+
name: serverName,
|
|
292
|
+
directory: projectDirectory,
|
|
293
|
+
})
|
|
294
|
+
if (error) {
|
|
295
|
+
logger.error(`[MCP] Failed to connect ${serverName}:`, error)
|
|
296
|
+
await interaction.editReply({
|
|
297
|
+
content: `Failed to connect **${serverName}**.`,
|
|
298
|
+
components: [],
|
|
299
|
+
})
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
logger.log(`[MCP] Connected server: ${serverName}`)
|
|
303
|
+
await interaction.editReply({
|
|
304
|
+
content: `**${serverName}** connected`,
|
|
305
|
+
components: [],
|
|
306
|
+
})
|
|
307
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// /memory-snapshot command - Write a V8 heap snapshot and show the file path.
|
|
2
|
+
// Reuses writeHeapSnapshot() from heap-monitor.ts which writes gzip-compressed
|
|
3
|
+
// .heapsnapshot.gz files to ~/.kimaki/heap-snapshots/.
|
|
4
|
+
|
|
5
|
+
import { MessageFlags } from 'discord.js'
|
|
6
|
+
import type { CommandContext } from './types.js'
|
|
7
|
+
import { writeHeapSnapshot } from '../heap-monitor.js'
|
|
8
|
+
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
9
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
10
|
+
|
|
11
|
+
const logger = createLogger(LogPrefix.HEAP)
|
|
12
|
+
|
|
13
|
+
export async function handleMemorySnapshotCommand({
|
|
14
|
+
command,
|
|
15
|
+
}: CommandContext): Promise<void> {
|
|
16
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const filepath = await writeHeapSnapshot()
|
|
20
|
+
await command.editReply({
|
|
21
|
+
content: `Heap snapshot written:\n\`${filepath}\``,
|
|
22
|
+
})
|
|
23
|
+
logger.log(`Memory snapshot requested via /memory-snapshot: ${filepath}`)
|
|
24
|
+
} catch (e) {
|
|
25
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
26
|
+
await command.editReply({
|
|
27
|
+
content: `Failed to write heap snapshot: ${msg}`,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// /toggle-mention-mode command.
|
|
2
|
+
// Toggles mention-only mode for a channel.
|
|
3
|
+
// When enabled, bot only responds to messages that @mention it.
|
|
4
|
+
// Messages in threads are not affected - they always work without mentions.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ChatInputCommandInteraction,
|
|
8
|
+
MessageFlags,
|
|
9
|
+
ChannelType,
|
|
10
|
+
type TextChannel,
|
|
11
|
+
} from 'discord.js'
|
|
12
|
+
import { getChannelMentionMode, setChannelMentionMode } from '../database.js'
|
|
13
|
+
import { getKimakiMetadata } from '../discord-utils.js'
|
|
14
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
15
|
+
|
|
16
|
+
const mentionModeLogger = createLogger(LogPrefix.CLI)
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Handle the /toggle-mention-mode slash command.
|
|
20
|
+
* Toggles whether the bot only responds when @mentioned in this channel.
|
|
21
|
+
*/
|
|
22
|
+
export async function handleToggleMentionModeCommand({
|
|
23
|
+
command,
|
|
24
|
+
}: {
|
|
25
|
+
command: ChatInputCommandInteraction
|
|
26
|
+
appId: string
|
|
27
|
+
}): Promise<void> {
|
|
28
|
+
mentionModeLogger.log('[TOGGLE_MENTION_MODE] Command called')
|
|
29
|
+
|
|
30
|
+
const channel = command.channel
|
|
31
|
+
|
|
32
|
+
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
33
|
+
await command.reply({
|
|
34
|
+
content: 'This command can only be used in text channels (not threads).',
|
|
35
|
+
flags: MessageFlags.Ephemeral,
|
|
36
|
+
})
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const textChannel = channel as TextChannel
|
|
41
|
+
const metadata = await getKimakiMetadata(textChannel)
|
|
42
|
+
|
|
43
|
+
if (!metadata.projectDirectory) {
|
|
44
|
+
await command.reply({
|
|
45
|
+
content:
|
|
46
|
+
'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
|
|
47
|
+
flags: MessageFlags.Ephemeral,
|
|
48
|
+
})
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const wasEnabled = await getChannelMentionMode(textChannel.id)
|
|
53
|
+
const nextEnabled = !wasEnabled
|
|
54
|
+
await setChannelMentionMode(textChannel.id, nextEnabled)
|
|
55
|
+
|
|
56
|
+
const nextLabel = nextEnabled ? 'enabled' : 'disabled'
|
|
57
|
+
|
|
58
|
+
mentionModeLogger.log(
|
|
59
|
+
`[TOGGLE_MENTION_MODE] ${nextLabel.toUpperCase()} for channel ${textChannel.id}`,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
await command.reply({
|
|
63
|
+
content: nextEnabled
|
|
64
|
+
? `Mention mode **enabled** for this channel.\nThe bot will only start new sessions when @mentioned.\nMessages in existing threads are not affected.`
|
|
65
|
+
: `Mention mode **disabled** for this channel.\nThe bot will respond to all messages in **#${textChannel.name}**.`,
|
|
66
|
+
flags: MessageFlags.Ephemeral,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// /merge-worktree command - Merge worktree commits into default branch.
|
|
2
|
+
// Pipeline: rebase worktree commits onto target -> local fast-forward push.
|
|
3
|
+
// Preserves all commits (no squash). On rebase conflicts, asks the AI model
|
|
4
|
+
// in the thread to resolve them.
|
|
5
|
+
|
|
6
|
+
import { type TextChannel, type ThreadChannel } from 'discord.js'
|
|
7
|
+
import type { AutocompleteContext, CommandContext } from './types.js'
|
|
8
|
+
import {
|
|
9
|
+
getThreadWorktree,
|
|
10
|
+
getThreadSession,
|
|
11
|
+
getChannelDirectory,
|
|
12
|
+
} from '../database.js'
|
|
13
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
14
|
+
import { notifyError } from '../sentry.js'
|
|
15
|
+
import { mergeWorktree, listBranchesByLastCommit, validateBranchRef } from '../worktrees.js'
|
|
16
|
+
import {
|
|
17
|
+
sendThreadMessage,
|
|
18
|
+
resolveWorkingDirectory,
|
|
19
|
+
resolveProjectDirectoryFromAutocomplete,
|
|
20
|
+
} from '../discord-utils.js'
|
|
21
|
+
import {
|
|
22
|
+
getOrCreateRuntime,
|
|
23
|
+
} from '../session-handler/thread-session-runtime.js'
|
|
24
|
+
import { RebaseConflictError, DirtyWorktreeError } from '../errors.js'
|
|
25
|
+
|
|
26
|
+
const logger = createLogger(LogPrefix.WORKTREE)
|
|
27
|
+
|
|
28
|
+
/** Worktree thread title prefix - indicates unmerged worktree */
|
|
29
|
+
export const WORKTREE_PREFIX = '⬦ '
|
|
30
|
+
|
|
31
|
+
async function removeWorktreePrefixFromTitle(
|
|
32
|
+
thread: ThreadChannel,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
if (!thread.name.startsWith(WORKTREE_PREFIX)) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
const newName = thread.name.slice(WORKTREE_PREFIX.length)
|
|
38
|
+
const timeoutMs = 5000
|
|
39
|
+
await Promise.race([
|
|
40
|
+
thread.setName(newName).catch((e) => {
|
|
41
|
+
logger.warn(
|
|
42
|
+
`Failed to update thread title: ${e instanceof Error ? e.message : String(e)}`,
|
|
43
|
+
)
|
|
44
|
+
}),
|
|
45
|
+
new Promise<void>((resolve) => {
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
logger.warn(`Thread title update timed out after ${timeoutMs}ms`)
|
|
48
|
+
resolve()
|
|
49
|
+
}, timeoutMs)
|
|
50
|
+
}),
|
|
51
|
+
])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Send a prompt to the AI model in the thread.
|
|
56
|
+
* If a session is actively streaming, queues it. Otherwise sends directly.
|
|
57
|
+
* Routes through ThreadSessionRuntime.
|
|
58
|
+
*/
|
|
59
|
+
async function sendPromptToModel({
|
|
60
|
+
prompt,
|
|
61
|
+
thread,
|
|
62
|
+
projectDirectory,
|
|
63
|
+
command,
|
|
64
|
+
appId,
|
|
65
|
+
}: {
|
|
66
|
+
prompt: string
|
|
67
|
+
thread: ThreadChannel
|
|
68
|
+
projectDirectory: string
|
|
69
|
+
command: CommandContext['command']
|
|
70
|
+
appId?: string
|
|
71
|
+
}): Promise<void> {
|
|
72
|
+
const resolved = await resolveWorkingDirectory({ channel: thread })
|
|
73
|
+
|
|
74
|
+
// Merge prompts use opencode queue mode.
|
|
75
|
+
const runtime = getOrCreateRuntime({
|
|
76
|
+
threadId: thread.id,
|
|
77
|
+
thread,
|
|
78
|
+
projectDirectory: resolved?.projectDirectory || projectDirectory,
|
|
79
|
+
sdkDirectory: resolved?.workingDirectory || projectDirectory,
|
|
80
|
+
channelId: thread.parentId || thread.id,
|
|
81
|
+
appId,
|
|
82
|
+
})
|
|
83
|
+
await runtime.enqueueIncoming({
|
|
84
|
+
prompt,
|
|
85
|
+
userId: command.user.id,
|
|
86
|
+
username: command.user.displayName,
|
|
87
|
+
appId,
|
|
88
|
+
mode: 'opencode',
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function handleMergeWorktreeCommand({
|
|
93
|
+
command,
|
|
94
|
+
appId,
|
|
95
|
+
}: CommandContext): Promise<void> {
|
|
96
|
+
await command.deferReply()
|
|
97
|
+
|
|
98
|
+
const channel = command.channel
|
|
99
|
+
if (!channel || !channel.isThread()) {
|
|
100
|
+
await command.editReply('This command can only be used in a thread')
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const thread = channel as ThreadChannel
|
|
105
|
+
const worktreeInfo = await getThreadWorktree(thread.id)
|
|
106
|
+
if (!worktreeInfo) {
|
|
107
|
+
await command.editReply('This thread is not associated with a worktree')
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (worktreeInfo.status !== 'ready' || !worktreeInfo.worktree_directory) {
|
|
112
|
+
await command.editReply(
|
|
113
|
+
`Worktree is not ready (status: ${worktreeInfo.status})${worktreeInfo.error_message ? `: ${worktreeInfo.error_message}` : ''}`,
|
|
114
|
+
)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
const rawTargetBranch = command.options.getString('target-branch') || undefined
|
|
121
|
+
let targetBranch = rawTargetBranch
|
|
122
|
+
if (targetBranch) {
|
|
123
|
+
const validated = await validateBranchRef({
|
|
124
|
+
directory: worktreeInfo.project_directory,
|
|
125
|
+
ref: targetBranch,
|
|
126
|
+
})
|
|
127
|
+
if (validated instanceof Error) {
|
|
128
|
+
await command.editReply(`Invalid target branch: \`${targetBranch}\``)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
targetBranch = validated
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result = await mergeWorktree({
|
|
135
|
+
worktreeDir: worktreeInfo.worktree_directory,
|
|
136
|
+
mainRepoDir: worktreeInfo.project_directory,
|
|
137
|
+
worktreeName: worktreeInfo.worktree_name,
|
|
138
|
+
targetBranch,
|
|
139
|
+
onProgress: (msg) => {
|
|
140
|
+
logger.log(`[merge] ${msg}`)
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
if (result instanceof Error) {
|
|
145
|
+
if (result instanceof DirtyWorktreeError) {
|
|
146
|
+
await command.editReply(
|
|
147
|
+
'Merge failed: uncommitted changes in the worktree. Commit changes first, then run `/merge-worktree` again.',
|
|
148
|
+
)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (result instanceof RebaseConflictError) {
|
|
153
|
+
await command.editReply(
|
|
154
|
+
'Rebase conflict detected. Asking the model to resolve...',
|
|
155
|
+
)
|
|
156
|
+
await sendPromptToModel({
|
|
157
|
+
prompt: [
|
|
158
|
+
'A rebase conflict occurred while merging this worktree into the default branch.',
|
|
159
|
+
'Rebasing multiple commits can pause on each commit that conflicts, so you may need to repeat the resolve/continue loop several times.',
|
|
160
|
+
'Please resolve the rebase conflicts:',
|
|
161
|
+
'1. Check `git status` to see which files have conflicts',
|
|
162
|
+
'2. Edit the conflicted files to resolve the merge markers',
|
|
163
|
+
'3. Stage resolved files with `git add`',
|
|
164
|
+
'4. Continue the rebase with `git rebase --continue`',
|
|
165
|
+
'5. If git reports more conflicts, repeat steps 1-4 until the rebase finishes (no more MERGE markers, `git status` shows no rebase in progress)',
|
|
166
|
+
'6. Once the rebase is fully complete, tell me so I can run `/merge-worktree` again',
|
|
167
|
+
].join('\n'),
|
|
168
|
+
thread,
|
|
169
|
+
projectDirectory: worktreeInfo.project_directory,
|
|
170
|
+
command,
|
|
171
|
+
appId,
|
|
172
|
+
})
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await command.editReply(`Merge failed: ${result.message}`)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
void removeWorktreePrefixFromTitle(thread)
|
|
181
|
+
await command.editReply(
|
|
182
|
+
`Merged \`${result.branchName}\` into \`${result.defaultBranch}\` @ ${result.shortSha} (${result.commitCount} commit${result.commitCount === 1 ? '' : 's'})\nWorktree now at detached HEAD.`,
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Autocomplete handler for /merge-worktree target-branch option.
|
|
188
|
+
* Lists local branches only (no remotes) sorted by most recent commit date.
|
|
189
|
+
* Resolves directory from the thread's worktree info or parent channel.
|
|
190
|
+
*/
|
|
191
|
+
export async function handleMergeWorktreeAutocomplete({
|
|
192
|
+
interaction,
|
|
193
|
+
}: AutocompleteContext): Promise<void> {
|
|
194
|
+
try {
|
|
195
|
+
const focusedValue = interaction.options.getFocused()
|
|
196
|
+
|
|
197
|
+
// interaction.channel can be null when the channel isn't cached
|
|
198
|
+
// (common with gateway-proxy). Use channelId which is always available
|
|
199
|
+
// from the raw interaction payload.
|
|
200
|
+
const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction)
|
|
201
|
+
|
|
202
|
+
if (!projectDirectory) {
|
|
203
|
+
await interaction.respond([])
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Local branches only — merge targets must be local refs
|
|
208
|
+
const branches = await listBranchesByLastCommit({
|
|
209
|
+
directory: projectDirectory,
|
|
210
|
+
query: focusedValue,
|
|
211
|
+
includeRemote: false,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
await interaction.respond(
|
|
215
|
+
branches.map((name) => {
|
|
216
|
+
return { name, value: name }
|
|
217
|
+
}),
|
|
218
|
+
)
|
|
219
|
+
} catch (e) {
|
|
220
|
+
logger.error('[MERGE-WORKTREE] Autocomplete error:', e)
|
|
221
|
+
await interaction.respond([]).catch(() => {})
|
|
222
|
+
}
|
|
223
|
+
}
|