@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,390 @@
|
|
|
1
|
+
// AskUserQuestion tool handler - Shows Discord dropdowns for AI questions.
|
|
2
|
+
// When the AI uses the AskUserQuestion tool, this module renders dropdowns
|
|
3
|
+
// for each question and collects user responses.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
StringSelectMenuBuilder,
|
|
7
|
+
StringSelectMenuInteraction,
|
|
8
|
+
ActionRowBuilder,
|
|
9
|
+
type ThreadChannel,
|
|
10
|
+
MessageFlags,
|
|
11
|
+
} from 'discord.js'
|
|
12
|
+
import crypto from 'node:crypto'
|
|
13
|
+
import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
14
|
+
import { getOpencodeClient } from '../opencode.js'
|
|
15
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
16
|
+
|
|
17
|
+
const logger = createLogger(LogPrefix.ASK_QUESTION)
|
|
18
|
+
|
|
19
|
+
// Schema matching the question tool input
|
|
20
|
+
export type AskUserQuestionInput = {
|
|
21
|
+
questions: Array<{
|
|
22
|
+
question: string
|
|
23
|
+
header: string // max 12 chars
|
|
24
|
+
options: Array<{
|
|
25
|
+
label: string
|
|
26
|
+
description: string
|
|
27
|
+
}>
|
|
28
|
+
multiple?: boolean // optional, defaults to false
|
|
29
|
+
}>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type CancelQuestionResult = 'no-pending' | 'replied' | 'reply-failed'
|
|
33
|
+
|
|
34
|
+
type PendingQuestionContext = {
|
|
35
|
+
sessionId: string
|
|
36
|
+
directory: string
|
|
37
|
+
thread: ThreadChannel
|
|
38
|
+
requestId: string // OpenCode question request ID for replying
|
|
39
|
+
questions: AskUserQuestionInput['questions']
|
|
40
|
+
answers: Record<number, string[]> // questionIndex -> selected labels
|
|
41
|
+
totalQuestions: number
|
|
42
|
+
answeredCount: number
|
|
43
|
+
contextHash: string
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Store pending question contexts by hash.
|
|
48
|
+
// TTL prevents unbounded growth if user never answers a question.
|
|
49
|
+
const QUESTION_CONTEXT_TTL_MS = 10 * 60 * 1000
|
|
50
|
+
export const pendingQuestionContexts = new Map<string, PendingQuestionContext>()
|
|
51
|
+
|
|
52
|
+
export function hasPendingQuestionForThread(threadId: string): boolean {
|
|
53
|
+
return [...pendingQuestionContexts.values()].some((ctx) => {
|
|
54
|
+
return ctx.thread.id === threadId
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Show dropdown menus for question tool input.
|
|
60
|
+
* Sends one message per question with the dropdown directly under the question text.
|
|
61
|
+
*/
|
|
62
|
+
export async function showAskUserQuestionDropdowns({
|
|
63
|
+
thread,
|
|
64
|
+
sessionId,
|
|
65
|
+
directory,
|
|
66
|
+
requestId,
|
|
67
|
+
input,
|
|
68
|
+
silent,
|
|
69
|
+
}: {
|
|
70
|
+
thread: ThreadChannel
|
|
71
|
+
sessionId: string
|
|
72
|
+
directory: string
|
|
73
|
+
requestId: string // OpenCode question request ID
|
|
74
|
+
input: AskUserQuestionInput
|
|
75
|
+
/** Suppress notification when queue has pending items */
|
|
76
|
+
silent?: boolean
|
|
77
|
+
}): Promise<void> {
|
|
78
|
+
const contextHash = crypto.randomBytes(8).toString('hex')
|
|
79
|
+
|
|
80
|
+
const context: PendingQuestionContext = {
|
|
81
|
+
sessionId,
|
|
82
|
+
directory,
|
|
83
|
+
thread,
|
|
84
|
+
requestId,
|
|
85
|
+
questions: input.questions,
|
|
86
|
+
answers: {},
|
|
87
|
+
totalQuestions: input.questions.length,
|
|
88
|
+
answeredCount: 0,
|
|
89
|
+
contextHash,
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pendingQuestionContexts.set(contextHash, context)
|
|
94
|
+
// On TTL expiry: hide the dropdown UI and abort the session so OpenCode
|
|
95
|
+
// unblocks. We intentionally do NOT call question.reply() — sending 'Other'
|
|
96
|
+
// made the model think the user chose an option when they didn't.
|
|
97
|
+
setTimeout(async () => {
|
|
98
|
+
const ctx = pendingQuestionContexts.get(contextHash)
|
|
99
|
+
if (!ctx) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
// Delete context first so the dropdown becomes inert immediately.
|
|
103
|
+
// Without this, a user clicking during the abort() await would still
|
|
104
|
+
// be accepted by handleAskQuestionSelectMenu, then abort() would
|
|
105
|
+
// kill that valid run.
|
|
106
|
+
pendingQuestionContexts.delete(contextHash)
|
|
107
|
+
// Abort the session so OpenCode isn't stuck waiting for a reply
|
|
108
|
+
const client = getOpencodeClient(ctx.directory)
|
|
109
|
+
if (client) {
|
|
110
|
+
await client.session.abort({
|
|
111
|
+
sessionID: ctx.sessionId,
|
|
112
|
+
}).catch((error) => {
|
|
113
|
+
logger.error('Failed to abort session after question expiry:', error)
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}, QUESTION_CONTEXT_TTL_MS).unref()
|
|
117
|
+
|
|
118
|
+
// Send one message per question with its dropdown directly underneath
|
|
119
|
+
for (let i = 0; i < input.questions.length; i++) {
|
|
120
|
+
const q = input.questions[i]!
|
|
121
|
+
|
|
122
|
+
// Map options to Discord select menu options
|
|
123
|
+
// Discord max: 25 options per select menu
|
|
124
|
+
const options = [
|
|
125
|
+
...q.options.slice(0, 24).map((opt, optIdx) => ({
|
|
126
|
+
label: opt.label.slice(0, 100),
|
|
127
|
+
value: `${optIdx}`,
|
|
128
|
+
description: opt.description.slice(0, 100),
|
|
129
|
+
})),
|
|
130
|
+
{
|
|
131
|
+
label: 'Other',
|
|
132
|
+
value: 'other',
|
|
133
|
+
description: 'Provide a custom answer in chat',
|
|
134
|
+
},
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
const placeholder =
|
|
138
|
+
options.find((x) => x.label)?.label || 'Select an option'
|
|
139
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
140
|
+
.setCustomId(`ask_question:${contextHash}:${i}`)
|
|
141
|
+
.setPlaceholder(placeholder)
|
|
142
|
+
.addOptions(options)
|
|
143
|
+
|
|
144
|
+
// Enable multi-select if the question supports it
|
|
145
|
+
if (q.multiple) {
|
|
146
|
+
selectMenu.setMinValues(1)
|
|
147
|
+
selectMenu.setMaxValues(options.length)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const actionRow =
|
|
151
|
+
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectMenu)
|
|
152
|
+
|
|
153
|
+
await thread.send({
|
|
154
|
+
content: `**${(q.header || '').slice(0, 200)}**\n${q.question.slice(0, 1700)}`,
|
|
155
|
+
components: [actionRow],
|
|
156
|
+
flags: silent ? SILENT_MESSAGE_FLAGS : NOTIFY_MESSAGE_FLAGS,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
logger.log(
|
|
161
|
+
`Showed ${input.questions.length} question dropdown(s) for session ${sessionId}`,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Handle dropdown selection for AskUserQuestion.
|
|
167
|
+
*/
|
|
168
|
+
export async function handleAskQuestionSelectMenu(
|
|
169
|
+
interaction: StringSelectMenuInteraction,
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
const customId = interaction.customId
|
|
172
|
+
|
|
173
|
+
if (!customId.startsWith('ask_question:')) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const parts = customId.split(':')
|
|
178
|
+
const contextHash = parts[1]
|
|
179
|
+
const questionIndex = parseInt(parts[2]!, 10)
|
|
180
|
+
|
|
181
|
+
if (!contextHash) {
|
|
182
|
+
await interaction.reply({
|
|
183
|
+
content: 'Invalid selection.',
|
|
184
|
+
flags: MessageFlags.Ephemeral,
|
|
185
|
+
})
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const context = pendingQuestionContexts.get(contextHash)
|
|
190
|
+
|
|
191
|
+
if (!context) {
|
|
192
|
+
await interaction.reply({
|
|
193
|
+
content: 'This question has expired. Please ask the AI again.',
|
|
194
|
+
flags: MessageFlags.Ephemeral,
|
|
195
|
+
})
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await interaction.deferUpdate()
|
|
200
|
+
|
|
201
|
+
const selectedValues = interaction.values
|
|
202
|
+
const question = context.questions[questionIndex]
|
|
203
|
+
|
|
204
|
+
if (!question) {
|
|
205
|
+
logger.error(`Question index ${questionIndex} not found in context`)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check if "other" was selected
|
|
210
|
+
if (selectedValues.includes('other')) {
|
|
211
|
+
// User wants to provide custom answer
|
|
212
|
+
// For now, mark as "Other" - they can type in chat
|
|
213
|
+
context.answers[questionIndex] = ['Other (please type your answer in chat)']
|
|
214
|
+
} else {
|
|
215
|
+
// Map value indices back to option labels
|
|
216
|
+
context.answers[questionIndex] = selectedValues.map((v) => {
|
|
217
|
+
const optIdx = parseInt(v, 10)
|
|
218
|
+
return question.options[optIdx]?.label || `Option ${optIdx + 1}`
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
context.answeredCount++
|
|
223
|
+
|
|
224
|
+
// Update this question's message: show answer and remove dropdown
|
|
225
|
+
const answeredText = context.answers[questionIndex]!.join(', ')
|
|
226
|
+
await interaction.editReply({
|
|
227
|
+
content: `**${question.header}**\n${question.question}\n✓ _${answeredText}_`,
|
|
228
|
+
components: [], // Remove the dropdown
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// Check if all questions are answered
|
|
232
|
+
if (context.answeredCount >= context.totalQuestions) {
|
|
233
|
+
// All questions answered - send result back to session
|
|
234
|
+
await submitQuestionAnswers(context)
|
|
235
|
+
pendingQuestionContexts.delete(contextHash)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Submit all collected answers back to the OpenCode session.
|
|
241
|
+
* Uses the question.reply API to provide answers to the waiting tool.
|
|
242
|
+
*/
|
|
243
|
+
async function submitQuestionAnswers(
|
|
244
|
+
context: PendingQuestionContext,
|
|
245
|
+
): Promise<void> {
|
|
246
|
+
try {
|
|
247
|
+
const client = getOpencodeClient(context.directory)
|
|
248
|
+
if (!client) {
|
|
249
|
+
throw new Error('OpenCode server not found for directory')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Build answers array: each element is an array of selected labels for that question
|
|
253
|
+
const answers = context.questions.map((_, i) => {
|
|
254
|
+
return context.answers[i] || []
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
await client.question.reply({
|
|
258
|
+
requestID: context.requestId,
|
|
259
|
+
directory: context.directory,
|
|
260
|
+
answers,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
logger.log(
|
|
264
|
+
`Submitted answers for question ${context.requestId} in session ${context.sessionId}`,
|
|
265
|
+
)
|
|
266
|
+
} catch (error) {
|
|
267
|
+
logger.error('Failed to submit answers:', error)
|
|
268
|
+
await sendThreadMessage(
|
|
269
|
+
context.thread,
|
|
270
|
+
`✗ Failed to submit answers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Check if a tool part is an AskUserQuestion tool.
|
|
277
|
+
* Returns the parsed input if valid, null otherwise.
|
|
278
|
+
*/
|
|
279
|
+
export function parseAskUserQuestionTool(part: {
|
|
280
|
+
type: string
|
|
281
|
+
tool?: string
|
|
282
|
+
state?: { input?: unknown }
|
|
283
|
+
}): AskUserQuestionInput | null {
|
|
284
|
+
if (part.type !== 'tool') {
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Check for the tool name (case-insensitive)
|
|
289
|
+
const toolName = part.tool?.toLowerCase()
|
|
290
|
+
if (toolName !== 'question') {
|
|
291
|
+
return null
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const input = part.state?.input as AskUserQuestionInput | undefined
|
|
295
|
+
|
|
296
|
+
if (
|
|
297
|
+
!input?.questions ||
|
|
298
|
+
!Array.isArray(input.questions) ||
|
|
299
|
+
input.questions.length === 0
|
|
300
|
+
) {
|
|
301
|
+
return null
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Validate structure
|
|
305
|
+
for (const q of input.questions) {
|
|
306
|
+
if (
|
|
307
|
+
typeof q.question !== 'string' ||
|
|
308
|
+
typeof q.header !== 'string' ||
|
|
309
|
+
!Array.isArray(q.options) ||
|
|
310
|
+
q.options.length < 2
|
|
311
|
+
) {
|
|
312
|
+
return null
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return input
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Cancel a pending question for a thread.
|
|
321
|
+
*
|
|
322
|
+
* Two modes depending on whether `userMessage` is provided:
|
|
323
|
+
*
|
|
324
|
+
* - `cancelPendingQuestion(threadId)` — cleanup only. Removes the context
|
|
325
|
+
* without replying to OpenCode. Use when aborting the blocked session
|
|
326
|
+
* separately (e.g. voice/attachment messages whose content needs
|
|
327
|
+
* transcription first). Returns 'no-pending' in both "found+cleaned" and
|
|
328
|
+
* "nothing found" cases.
|
|
329
|
+
*
|
|
330
|
+
* - `cancelPendingQuestion(threadId, text)` — reply path. Sends the text as
|
|
331
|
+
* the tool answer so the model sees the user's response. The caller should
|
|
332
|
+
* NOT also enqueue the message as a new prompt.
|
|
333
|
+
* Returns 'replied' on success, 'reply-failed' if the reply call fails
|
|
334
|
+
* (context kept pending so TTL can retry).
|
|
335
|
+
*/
|
|
336
|
+
export async function cancelPendingQuestion(
|
|
337
|
+
threadId: string,
|
|
338
|
+
userMessage?: string,
|
|
339
|
+
): Promise<CancelQuestionResult> {
|
|
340
|
+
// Find pending question for this thread
|
|
341
|
+
let contextHash: string | undefined
|
|
342
|
+
let context: PendingQuestionContext | undefined
|
|
343
|
+
for (const [hash, ctx] of pendingQuestionContexts) {
|
|
344
|
+
if (ctx.thread.id === threadId) {
|
|
345
|
+
contextHash = hash
|
|
346
|
+
context = ctx
|
|
347
|
+
break
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!contextHash || !context) {
|
|
352
|
+
return 'no-pending'
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// undefined means teardown/cleanup — just remove context, don't reply.
|
|
356
|
+
// The session is already being torn down or the caller wants to dismiss
|
|
357
|
+
// the question without providing an answer (e.g. voice/attachment-only
|
|
358
|
+
// messages where content needs transcription before it can be an answer).
|
|
359
|
+
if (userMessage === undefined) {
|
|
360
|
+
pendingQuestionContexts.delete(contextHash)
|
|
361
|
+
return 'no-pending'
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const client = getOpencodeClient(context.directory)
|
|
366
|
+
if (!client) {
|
|
367
|
+
throw new Error('OpenCode server not found for directory')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const answers = context.questions.map((_, i) => {
|
|
371
|
+
return context.answers[i] || [userMessage]
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
await client.question.reply({
|
|
375
|
+
requestID: context.requestId,
|
|
376
|
+
directory: context.directory,
|
|
377
|
+
answers,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
logger.log(`Answered question ${context.requestId} with user message`)
|
|
381
|
+
} catch (error) {
|
|
382
|
+
logger.error('Failed to answer question:', error)
|
|
383
|
+
// Keep context pending so TTL can still fire.
|
|
384
|
+
// Caller should not consume the user message since reply failed.
|
|
385
|
+
return 'reply-failed'
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
pendingQuestionContexts.delete(contextHash)
|
|
389
|
+
return 'replied'
|
|
390
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// /btw command - Fork the current session with full context and send a new prompt.
|
|
2
|
+
// Unlike /fork, this does not replay past messages in Discord. It just creates
|
|
3
|
+
// a new thread, forks the entire session (no messageID), and immediately
|
|
4
|
+
// dispatches the user's prompt so the forked session starts working right away.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ChannelType,
|
|
8
|
+
ThreadAutoArchiveDuration,
|
|
9
|
+
type ThreadChannel,
|
|
10
|
+
MessageFlags,
|
|
11
|
+
} from 'discord.js'
|
|
12
|
+
import { getThreadSession, setThreadSession } from '../database.js'
|
|
13
|
+
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
14
|
+
import {
|
|
15
|
+
resolveWorkingDirectory,
|
|
16
|
+
resolveTextChannel,
|
|
17
|
+
sendThreadMessage,
|
|
18
|
+
} from '../discord-utils.js'
|
|
19
|
+
import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js'
|
|
20
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
21
|
+
import type { CommandContext } from './types.js'
|
|
22
|
+
|
|
23
|
+
const logger = createLogger(LogPrefix.FORK)
|
|
24
|
+
|
|
25
|
+
export async function handleBtwCommand({
|
|
26
|
+
command,
|
|
27
|
+
appId,
|
|
28
|
+
}: CommandContext): Promise<void> {
|
|
29
|
+
const channel = command.channel
|
|
30
|
+
|
|
31
|
+
if (!channel) {
|
|
32
|
+
await command.reply({
|
|
33
|
+
content: 'This command can only be used in a channel',
|
|
34
|
+
flags: MessageFlags.Ephemeral,
|
|
35
|
+
})
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isThread = [
|
|
40
|
+
ChannelType.PublicThread,
|
|
41
|
+
ChannelType.PrivateThread,
|
|
42
|
+
ChannelType.AnnouncementThread,
|
|
43
|
+
].includes(channel.type)
|
|
44
|
+
|
|
45
|
+
if (!isThread) {
|
|
46
|
+
await command.reply({
|
|
47
|
+
content:
|
|
48
|
+
'This command can only be used in a thread with an active session',
|
|
49
|
+
flags: MessageFlags.Ephemeral,
|
|
50
|
+
})
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const prompt = command.options.getString('prompt', true)
|
|
55
|
+
|
|
56
|
+
const resolved = await resolveWorkingDirectory({
|
|
57
|
+
channel: channel as ThreadChannel,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (!resolved) {
|
|
61
|
+
await command.reply({
|
|
62
|
+
content: 'Could not determine project directory for this channel',
|
|
63
|
+
flags: MessageFlags.Ephemeral,
|
|
64
|
+
})
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { projectDirectory } = resolved
|
|
69
|
+
|
|
70
|
+
const sessionId = await getThreadSession(channel.id)
|
|
71
|
+
|
|
72
|
+
if (!sessionId) {
|
|
73
|
+
await command.reply({
|
|
74
|
+
content: 'No active session in this thread',
|
|
75
|
+
flags: MessageFlags.Ephemeral,
|
|
76
|
+
})
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await command.deferReply({ flags: MessageFlags.Ephemeral })
|
|
81
|
+
|
|
82
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
83
|
+
if (getClient instanceof Error) {
|
|
84
|
+
await command.editReply({
|
|
85
|
+
content: `Failed to fork session: ${getClient.message}`,
|
|
86
|
+
})
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Fork the entire session (no messageID = fork at the latest point)
|
|
92
|
+
const forkResponse = await getClient().session.fork({
|
|
93
|
+
sessionID: sessionId,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (!forkResponse.data) {
|
|
97
|
+
await command.editReply('Failed to fork session')
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const forkedSession = forkResponse.data
|
|
102
|
+
|
|
103
|
+
const textChannel = await resolveTextChannel(channel as ThreadChannel)
|
|
104
|
+
if (!textChannel) {
|
|
105
|
+
await command.editReply('Could not resolve parent text channel')
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const threadName = `btw: ${prompt}`.slice(0, 100)
|
|
110
|
+
const thread = await textChannel.threads.create({
|
|
111
|
+
name: threadName,
|
|
112
|
+
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
113
|
+
reason: `btw fork from session ${sessionId}`,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Claim the forked session immediately so external polling does not race
|
|
117
|
+
await setThreadSession(thread.id, forkedSession.id)
|
|
118
|
+
|
|
119
|
+
await thread.members.add(command.user.id)
|
|
120
|
+
|
|
121
|
+
logger.log(
|
|
122
|
+
`Created btw fork session ${forkedSession.id} in thread ${thread.id} from ${sessionId}`,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// Short status message with prompt instead of replaying past messages
|
|
126
|
+
const sourceThreadLink = `<#${channel.id}>`
|
|
127
|
+
await sendThreadMessage(
|
|
128
|
+
thread,
|
|
129
|
+
`Reusing context from ${sourceThreadLink} to answer prompt...\n${prompt}`,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const wrappedPrompt = [
|
|
133
|
+
`The user asked a side question while you were working on another task.`,
|
|
134
|
+
`This is a forked session whose ONLY goal is to answer this question.`,
|
|
135
|
+
`Do NOT continue, resume, or reference the previous task. Only answer the question below.\n`,
|
|
136
|
+
prompt,
|
|
137
|
+
].join('\n')
|
|
138
|
+
|
|
139
|
+
const runtime = getOrCreateRuntime({
|
|
140
|
+
threadId: thread.id,
|
|
141
|
+
thread,
|
|
142
|
+
projectDirectory,
|
|
143
|
+
sdkDirectory: projectDirectory,
|
|
144
|
+
channelId: textChannel.id,
|
|
145
|
+
appId,
|
|
146
|
+
})
|
|
147
|
+
await runtime.enqueueIncoming({
|
|
148
|
+
prompt: wrappedPrompt,
|
|
149
|
+
userId: command.user.id,
|
|
150
|
+
username: command.user.displayName,
|
|
151
|
+
appId,
|
|
152
|
+
mode: 'opencode',
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
await command.editReply(
|
|
156
|
+
`Session forked! Continue in ${thread.toString()}`,
|
|
157
|
+
)
|
|
158
|
+
} catch (error) {
|
|
159
|
+
logger.error('Error in /btw:', error)
|
|
160
|
+
await command.editReply(
|
|
161
|
+
`Failed to fork session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// /compact command - Trigger context compaction (summarization) for the current session.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ChannelType,
|
|
5
|
+
MessageFlags,
|
|
6
|
+
type TextChannel,
|
|
7
|
+
type ThreadChannel,
|
|
8
|
+
} from 'discord.js'
|
|
9
|
+
import type { CommandContext } from './types.js'
|
|
10
|
+
import { getThreadSession } from '../database.js'
|
|
11
|
+
import {
|
|
12
|
+
initializeOpencodeForDirectory,
|
|
13
|
+
getOpencodeClient,
|
|
14
|
+
} from '../opencode.js'
|
|
15
|
+
import {
|
|
16
|
+
resolveWorkingDirectory,
|
|
17
|
+
SILENT_MESSAGE_FLAGS,
|
|
18
|
+
} from '../discord-utils.js'
|
|
19
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
20
|
+
|
|
21
|
+
const logger = createLogger(LogPrefix.COMPACT)
|
|
22
|
+
|
|
23
|
+
export async function handleCompactCommand({
|
|
24
|
+
command,
|
|
25
|
+
}: CommandContext): Promise<void> {
|
|
26
|
+
const channel = command.channel
|
|
27
|
+
|
|
28
|
+
if (!channel) {
|
|
29
|
+
await command.reply({
|
|
30
|
+
content: 'This command can only be used in a channel',
|
|
31
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
32
|
+
})
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isThread = [
|
|
37
|
+
ChannelType.PublicThread,
|
|
38
|
+
ChannelType.PrivateThread,
|
|
39
|
+
ChannelType.AnnouncementThread,
|
|
40
|
+
].includes(channel.type)
|
|
41
|
+
|
|
42
|
+
if (!isThread) {
|
|
43
|
+
await command.reply({
|
|
44
|
+
content:
|
|
45
|
+
'This command can only be used in a thread with an active session',
|
|
46
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
47
|
+
})
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const resolved = await resolveWorkingDirectory({
|
|
52
|
+
channel: channel as TextChannel | ThreadChannel,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (!resolved) {
|
|
56
|
+
await command.reply({
|
|
57
|
+
content: 'Could not determine project directory for this channel',
|
|
58
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
59
|
+
})
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { projectDirectory, workingDirectory } = resolved
|
|
64
|
+
|
|
65
|
+
const sessionId = await getThreadSession(channel.id)
|
|
66
|
+
|
|
67
|
+
if (!sessionId) {
|
|
68
|
+
await command.reply({
|
|
69
|
+
content: 'No active session in this thread',
|
|
70
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
71
|
+
})
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Ensure server is running for the base project directory
|
|
76
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
77
|
+
if (getClient instanceof Error) {
|
|
78
|
+
await command.reply({
|
|
79
|
+
content: `Failed to compact: ${getClient.message}`,
|
|
80
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
81
|
+
})
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const client = getOpencodeClient(projectDirectory)
|
|
86
|
+
if (!client) {
|
|
87
|
+
await command.reply({
|
|
88
|
+
content: 'Failed to get OpenCode client',
|
|
89
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
90
|
+
})
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Defer reply since compaction may take a moment
|
|
95
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Get session messages to find the model from the last user message
|
|
99
|
+
const messagesResult = await client.session.messages({
|
|
100
|
+
sessionID: sessionId,
|
|
101
|
+
directory: workingDirectory,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
if (messagesResult.error || !messagesResult.data) {
|
|
105
|
+
logger.error('[COMPACT] Failed to get messages:', messagesResult.error)
|
|
106
|
+
await command.editReply({
|
|
107
|
+
content: 'Failed to compact: Could not retrieve session messages',
|
|
108
|
+
})
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Find the last user message to get the model
|
|
113
|
+
const lastUserMessage = [...messagesResult.data]
|
|
114
|
+
.reverse()
|
|
115
|
+
.find((msg) => msg.info.role === 'user')
|
|
116
|
+
|
|
117
|
+
if (!lastUserMessage || lastUserMessage.info.role !== 'user') {
|
|
118
|
+
await command.editReply({
|
|
119
|
+
content: 'Failed to compact: No user message found in session',
|
|
120
|
+
})
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { providerID, modelID } = lastUserMessage.info.model
|
|
125
|
+
|
|
126
|
+
const result = await client.session.summarize({
|
|
127
|
+
sessionID: sessionId,
|
|
128
|
+
directory: workingDirectory,
|
|
129
|
+
providerID,
|
|
130
|
+
modelID,
|
|
131
|
+
auto: false,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if (result.error) {
|
|
135
|
+
logger.error('[COMPACT] Error:', result.error)
|
|
136
|
+
const errorMessage =
|
|
137
|
+
'data' in result.error && result.error.data
|
|
138
|
+
? (result.error.data as { message?: string }).message ||
|
|
139
|
+
'Unknown error'
|
|
140
|
+
: 'Unknown error'
|
|
141
|
+
await command.editReply({
|
|
142
|
+
content: `Failed to compact: ${errorMessage}`,
|
|
143
|
+
})
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await command.editReply({
|
|
148
|
+
content: `📦 Session **compacted** successfully`,
|
|
149
|
+
})
|
|
150
|
+
logger.log(`Session ${sessionId} compacted by user`)
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logger.error('[COMPACT] Error:', error)
|
|
153
|
+
await command.editReply({
|
|
154
|
+
content: `Failed to compact: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}
|