@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,885 @@
|
|
|
1
|
+
// /login command — authenticate with AI providers (OAuth or API key).
|
|
2
|
+
//
|
|
3
|
+
// Uses a unified select handler (`login_select:<hash>`) for all sequential
|
|
4
|
+
// select menus (provider → method → plugin prompts). The context tracks a
|
|
5
|
+
// `step` field so one handler drives the whole flow.
|
|
6
|
+
//
|
|
7
|
+
// CustomId patterns:
|
|
8
|
+
// login_select:<hash> — all select menus (provider, method, prompts)
|
|
9
|
+
// login_apikey:<hash> — API key modal submission
|
|
10
|
+
// login_text:<hash> — text prompt modal submission
|
|
11
|
+
import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ModalSubmitInteraction, ButtonBuilder, ButtonStyle, ChannelType, MessageFlags, } from 'discord.js';
|
|
12
|
+
import crypto from 'node:crypto';
|
|
13
|
+
import { initializeOpencodeForDirectory, getOpencodeServerPort, } from '../opencode.js';
|
|
14
|
+
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
15
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
16
|
+
import { buildPaginatedOptions, parsePaginationValue } from './paginated-select.js';
|
|
17
|
+
const loginLogger = createLogger(LogPrefix.LOGIN);
|
|
18
|
+
// ── Context store ───────────────────────────────────────────────
|
|
19
|
+
// Keyed by random hash to stay under Discord's 100-char customId limit.
|
|
20
|
+
// TTL prevents unbounded growth when users open /login and never interact.
|
|
21
|
+
const LOGIN_CONTEXT_TTL_MS = 10 * 60 * 1000;
|
|
22
|
+
const pendingLoginContexts = new Map();
|
|
23
|
+
function createContextHash(context) {
|
|
24
|
+
const hash = crypto.randomBytes(8).toString('hex');
|
|
25
|
+
pendingLoginContexts.set(hash, context);
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
pendingLoginContexts.delete(hash);
|
|
28
|
+
}, LOGIN_CONTEXT_TTL_MS).unref();
|
|
29
|
+
return hash;
|
|
30
|
+
}
|
|
31
|
+
// ── Provider popularity order ───────────────────────────────────
|
|
32
|
+
// Discord select menus cap at 25 options, so we show popular ones first.
|
|
33
|
+
// IDs sourced from opencode's provider.list() API (scripts/list-providers.ts).
|
|
34
|
+
const PROVIDER_POPULARITY_ORDER = [
|
|
35
|
+
'anthropic',
|
|
36
|
+
'openai',
|
|
37
|
+
'google',
|
|
38
|
+
'github-copilot',
|
|
39
|
+
'xai',
|
|
40
|
+
'groq',
|
|
41
|
+
'deepseek',
|
|
42
|
+
'mistral',
|
|
43
|
+
'openrouter',
|
|
44
|
+
'fireworks-ai',
|
|
45
|
+
'togetherai',
|
|
46
|
+
'amazon-bedrock',
|
|
47
|
+
'azure',
|
|
48
|
+
'google-vertex',
|
|
49
|
+
'google-vertex-anthropic',
|
|
50
|
+
'cohere',
|
|
51
|
+
'cerebras',
|
|
52
|
+
'perplexity',
|
|
53
|
+
'cloudflare-workers-ai',
|
|
54
|
+
'novita-ai',
|
|
55
|
+
'huggingface',
|
|
56
|
+
'deepinfra',
|
|
57
|
+
'github-models',
|
|
58
|
+
'lmstudio',
|
|
59
|
+
'llama',
|
|
60
|
+
];
|
|
61
|
+
// ── Helpers ─────────────────────────────────────────────────────
|
|
62
|
+
function extractErrorMessage({ error, fallback, }) {
|
|
63
|
+
if (!error || typeof error !== 'object') {
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
const parsed = error;
|
|
67
|
+
return parsed.data?.message || parsed.message || fallback;
|
|
68
|
+
}
|
|
69
|
+
function shouldShowPrompt(prompt, inputs) {
|
|
70
|
+
if (!prompt.when) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
const value = inputs[prompt.when.key];
|
|
74
|
+
if (prompt.when.op === 'eq') {
|
|
75
|
+
return value === prompt.when.value;
|
|
76
|
+
}
|
|
77
|
+
if (prompt.when.op === 'neq') {
|
|
78
|
+
return value !== prompt.when.value;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
function buildSelectMenu({ customId, placeholder, options, }) {
|
|
83
|
+
const menu = new StringSelectMenuBuilder()
|
|
84
|
+
.setCustomId(customId)
|
|
85
|
+
.setPlaceholder(placeholder)
|
|
86
|
+
.addOptions(options);
|
|
87
|
+
return new ActionRowBuilder().addComponents(menu);
|
|
88
|
+
}
|
|
89
|
+
// ── /login command ──────────────────────────────────────────────
|
|
90
|
+
export async function handleLoginCommand({ interaction, }) {
|
|
91
|
+
loginLogger.log('[LOGIN] handleLoginCommand called');
|
|
92
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
93
|
+
const channel = interaction.channel;
|
|
94
|
+
if (!channel) {
|
|
95
|
+
await interaction.editReply({
|
|
96
|
+
content: 'This command can only be used in a channel',
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const isThread = [
|
|
101
|
+
ChannelType.PublicThread,
|
|
102
|
+
ChannelType.PrivateThread,
|
|
103
|
+
ChannelType.AnnouncementThread,
|
|
104
|
+
].includes(channel.type);
|
|
105
|
+
let projectDirectory;
|
|
106
|
+
let targetChannelId;
|
|
107
|
+
if (isThread) {
|
|
108
|
+
const thread = channel;
|
|
109
|
+
const textChannel = await resolveTextChannel(thread);
|
|
110
|
+
const metadata = await getKimakiMetadata(textChannel);
|
|
111
|
+
projectDirectory = metadata.projectDirectory;
|
|
112
|
+
targetChannelId = textChannel?.id || channel.id;
|
|
113
|
+
}
|
|
114
|
+
else if (channel.type === ChannelType.GuildText) {
|
|
115
|
+
const textChannel = channel;
|
|
116
|
+
const metadata = await getKimakiMetadata(textChannel);
|
|
117
|
+
projectDirectory = metadata.projectDirectory;
|
|
118
|
+
targetChannelId = channel.id;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
await interaction.editReply({
|
|
122
|
+
content: 'This command can only be used in text channels or threads',
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!projectDirectory) {
|
|
127
|
+
await interaction.editReply({
|
|
128
|
+
content: 'This channel is not configured with a project directory',
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
134
|
+
if (getClient instanceof Error) {
|
|
135
|
+
await interaction.editReply({ content: getClient.message });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const providersResponse = await getClient().provider.list({
|
|
139
|
+
directory: projectDirectory,
|
|
140
|
+
});
|
|
141
|
+
if (!providersResponse.data) {
|
|
142
|
+
await interaction.editReply({ content: 'Failed to fetch providers' });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const { all: allProviders, connected } = providersResponse.data;
|
|
146
|
+
if (allProviders.length === 0) {
|
|
147
|
+
await interaction.editReply({ content: 'No providers available.' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const allProviderOptions = [...allProviders]
|
|
151
|
+
.sort((a, b) => {
|
|
152
|
+
const rankA = PROVIDER_POPULARITY_ORDER.indexOf(a.id);
|
|
153
|
+
const rankB = PROVIDER_POPULARITY_ORDER.indexOf(b.id);
|
|
154
|
+
const posA = rankA === -1 ? Infinity : rankA;
|
|
155
|
+
const posB = rankB === -1 ? Infinity : rankB;
|
|
156
|
+
if (posA !== posB) {
|
|
157
|
+
return posA - posB;
|
|
158
|
+
}
|
|
159
|
+
return a.name.localeCompare(b.name);
|
|
160
|
+
})
|
|
161
|
+
.map((provider) => {
|
|
162
|
+
const isConnected = connected.includes(provider.id);
|
|
163
|
+
return {
|
|
164
|
+
label: `${provider.name}${isConnected ? ' ✓' : ''}`.slice(0, 100),
|
|
165
|
+
value: provider.id,
|
|
166
|
+
description: isConnected
|
|
167
|
+
? 'Connected - select to re-authenticate'
|
|
168
|
+
: 'Not connected',
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
const { options } = buildPaginatedOptions({
|
|
172
|
+
allOptions: allProviderOptions,
|
|
173
|
+
page: 0,
|
|
174
|
+
});
|
|
175
|
+
const context = {
|
|
176
|
+
dir: projectDirectory,
|
|
177
|
+
channelId: targetChannelId,
|
|
178
|
+
steps: [{ type: 'provider' }],
|
|
179
|
+
stepIndex: 0,
|
|
180
|
+
inputs: {},
|
|
181
|
+
};
|
|
182
|
+
const hash = createContextHash(context);
|
|
183
|
+
await interaction.editReply({
|
|
184
|
+
content: '**Authenticate with Provider**\nSelect a provider:',
|
|
185
|
+
components: [
|
|
186
|
+
buildSelectMenu({
|
|
187
|
+
customId: `login_select:${hash}`,
|
|
188
|
+
placeholder: 'Select a provider to authenticate',
|
|
189
|
+
options,
|
|
190
|
+
}),
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
loginLogger.error('Error loading providers:', error);
|
|
196
|
+
await interaction.editReply({
|
|
197
|
+
content: `Failed to load providers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// ── Unified select handler ──────────────────────────────────────
|
|
202
|
+
// Handles all select menu interactions for the login flow.
|
|
203
|
+
// Reads the current step from context, processes the answer,
|
|
204
|
+
// then either shows the next step or proceeds to authorize/API key.
|
|
205
|
+
export async function handleLoginSelect(interaction) {
|
|
206
|
+
if (!interaction.customId.startsWith('login_select:')) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const hash = interaction.customId.replace('login_select:', '');
|
|
210
|
+
const ctx = pendingLoginContexts.get(hash);
|
|
211
|
+
if (!ctx) {
|
|
212
|
+
await interaction.deferUpdate();
|
|
213
|
+
await interaction.editReply({
|
|
214
|
+
content: 'Selection expired. Please run /login again.',
|
|
215
|
+
components: [],
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const value = interaction.values[0];
|
|
220
|
+
if (!value) {
|
|
221
|
+
await interaction.deferUpdate();
|
|
222
|
+
await interaction.editReply({
|
|
223
|
+
content: 'No option selected.',
|
|
224
|
+
components: [],
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const step = ctx.steps[ctx.stepIndex];
|
|
229
|
+
if (!step) {
|
|
230
|
+
await interaction.deferUpdate();
|
|
231
|
+
await interaction.editReply({
|
|
232
|
+
content: 'Invalid state. Please run /login again.',
|
|
233
|
+
components: [],
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
if (step.type === 'provider') {
|
|
239
|
+
await handleProviderStep(interaction, ctx, hash, value);
|
|
240
|
+
}
|
|
241
|
+
else if (step.type === 'method') {
|
|
242
|
+
await handleMethodStep(interaction, ctx, hash, value, step);
|
|
243
|
+
}
|
|
244
|
+
else if (step.type === 'prompt') {
|
|
245
|
+
await handlePromptStep(interaction, ctx, hash, value, step);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
loginLogger.error('Error in login select:', error);
|
|
250
|
+
if (!interaction.deferred && !interaction.replied) {
|
|
251
|
+
await interaction.deferUpdate();
|
|
252
|
+
}
|
|
253
|
+
await interaction.editReply({
|
|
254
|
+
content: `Login error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
255
|
+
components: [],
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// ── Step handlers ───────────────────────────────────────────────
|
|
260
|
+
async function handleProviderStep(interaction, ctx, hash, providerId) {
|
|
261
|
+
// Handle pagination nav — re-render the same provider select with new page
|
|
262
|
+
const navPage = parsePaginationValue(providerId);
|
|
263
|
+
if (navPage !== undefined) {
|
|
264
|
+
await interaction.deferUpdate();
|
|
265
|
+
ctx.providerPage = navPage;
|
|
266
|
+
const getClient = await initializeOpencodeForDirectory(ctx.dir);
|
|
267
|
+
if (getClient instanceof Error) {
|
|
268
|
+
await interaction.editReply({ content: getClient.message, components: [] });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const providersResponse = await getClient().provider.list({ directory: ctx.dir });
|
|
272
|
+
if (!providersResponse.data) {
|
|
273
|
+
await interaction.editReply({ content: 'Failed to fetch providers', components: [] });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const { all: allProviders, connected } = providersResponse.data;
|
|
277
|
+
const allProviderOptions = [...allProviders]
|
|
278
|
+
.sort((a, b) => {
|
|
279
|
+
const rankA = PROVIDER_POPULARITY_ORDER.indexOf(a.id);
|
|
280
|
+
const rankB = PROVIDER_POPULARITY_ORDER.indexOf(b.id);
|
|
281
|
+
const posA = rankA === -1 ? Infinity : rankA;
|
|
282
|
+
const posB = rankB === -1 ? Infinity : rankB;
|
|
283
|
+
if (posA !== posB) {
|
|
284
|
+
return posA - posB;
|
|
285
|
+
}
|
|
286
|
+
return a.name.localeCompare(b.name);
|
|
287
|
+
})
|
|
288
|
+
.map((p) => {
|
|
289
|
+
const isConnected = connected.includes(p.id);
|
|
290
|
+
return {
|
|
291
|
+
label: `${p.name}${isConnected ? ' ✓' : ''}`.slice(0, 100),
|
|
292
|
+
value: p.id,
|
|
293
|
+
description: isConnected ? 'Connected - select to re-authenticate' : 'Not connected',
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
const { options } = buildPaginatedOptions({ allOptions: allProviderOptions, page: navPage });
|
|
297
|
+
await interaction.editReply({
|
|
298
|
+
content: '**Authenticate with Provider**\nSelect a provider:',
|
|
299
|
+
components: [
|
|
300
|
+
buildSelectMenu({
|
|
301
|
+
customId: `login_select:${hash}`,
|
|
302
|
+
placeholder: 'Select a provider to authenticate',
|
|
303
|
+
options,
|
|
304
|
+
}),
|
|
305
|
+
],
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const getClient = await initializeOpencodeForDirectory(ctx.dir);
|
|
310
|
+
if (getClient instanceof Error) {
|
|
311
|
+
await interaction.deferUpdate();
|
|
312
|
+
await interaction.editReply({ content: getClient.message, components: [] });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const providersResponse = await getClient().provider.list({
|
|
316
|
+
directory: ctx.dir,
|
|
317
|
+
});
|
|
318
|
+
const provider = providersResponse.data?.all.find((p) => p.id === providerId);
|
|
319
|
+
const providerName = provider?.name || providerId;
|
|
320
|
+
const authResponse = await getClient().provider.auth({ directory: ctx.dir });
|
|
321
|
+
if (!authResponse.data) {
|
|
322
|
+
await interaction.deferUpdate();
|
|
323
|
+
await interaction.editReply({
|
|
324
|
+
content: 'Failed to fetch authentication methods',
|
|
325
|
+
components: [],
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// The server returns prompts in the auth response when the opencode
|
|
330
|
+
// version supports it (dev branch, not yet released as of v1.2.27).
|
|
331
|
+
// Once released, plugin-defined prompts will be collected and passed
|
|
332
|
+
// as inputs to the authorize call automatically.
|
|
333
|
+
const methods = authResponse.data[providerId] || [
|
|
334
|
+
{ type: 'api', label: 'API Key' },
|
|
335
|
+
];
|
|
336
|
+
if (methods.length === 0) {
|
|
337
|
+
await interaction.deferUpdate();
|
|
338
|
+
await interaction.editReply({
|
|
339
|
+
content: `No authentication methods available for ${providerName}`,
|
|
340
|
+
components: [],
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
ctx.providerId = providerId;
|
|
345
|
+
ctx.providerName = providerName;
|
|
346
|
+
if (methods.length === 1) {
|
|
347
|
+
// Single method — skip method select, go straight to prompts or action
|
|
348
|
+
const method = methods[0];
|
|
349
|
+
ctx.methodIndex = 0;
|
|
350
|
+
ctx.methodType = method.type;
|
|
351
|
+
const promptSteps = buildPromptSteps(method);
|
|
352
|
+
if (promptSteps.length > 0) {
|
|
353
|
+
// Has prompts — defer and show first prompt
|
|
354
|
+
ctx.steps = promptSteps;
|
|
355
|
+
ctx.stepIndex = 0;
|
|
356
|
+
await interaction.deferUpdate();
|
|
357
|
+
await showNextStep(interaction, ctx, hash);
|
|
358
|
+
}
|
|
359
|
+
else if (method.type === 'api') {
|
|
360
|
+
// API key with no prompts — show modal directly (don't defer)
|
|
361
|
+
await showApiKeyModal(interaction, hash, providerName);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
// OAuth with no prompts — defer and authorize
|
|
365
|
+
await interaction.deferUpdate();
|
|
366
|
+
await startOAuthFlow(interaction, ctx, hash);
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// Multiple methods — show method select
|
|
371
|
+
ctx.steps = [
|
|
372
|
+
{ type: 'method', methods },
|
|
373
|
+
];
|
|
374
|
+
ctx.stepIndex = 0;
|
|
375
|
+
await interaction.deferUpdate();
|
|
376
|
+
await showNextStep(interaction, ctx, hash);
|
|
377
|
+
}
|
|
378
|
+
async function handleMethodStep(interaction, ctx, hash, value, step) {
|
|
379
|
+
const methodIndex = parseInt(value, 10);
|
|
380
|
+
const method = step.methods[methodIndex];
|
|
381
|
+
if (!method) {
|
|
382
|
+
await interaction.deferUpdate();
|
|
383
|
+
await interaction.editReply({
|
|
384
|
+
content: 'Invalid method selected.',
|
|
385
|
+
components: [],
|
|
386
|
+
});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
ctx.methodIndex = methodIndex;
|
|
390
|
+
ctx.methodType = method.type;
|
|
391
|
+
const promptSteps = buildPromptSteps(method);
|
|
392
|
+
if (promptSteps.length > 0) {
|
|
393
|
+
// Replace remaining steps with prompt steps
|
|
394
|
+
ctx.steps = promptSteps;
|
|
395
|
+
ctx.stepIndex = 0;
|
|
396
|
+
await interaction.deferUpdate();
|
|
397
|
+
await showNextStep(interaction, ctx, hash);
|
|
398
|
+
}
|
|
399
|
+
else if (method.type === 'api') {
|
|
400
|
+
// API key with no prompts — show modal directly (don't defer)
|
|
401
|
+
await showApiKeyModal(interaction, hash, ctx.providerName || '');
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
// OAuth with no prompts
|
|
405
|
+
await interaction.deferUpdate();
|
|
406
|
+
await startOAuthFlow(interaction, ctx, hash);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async function handlePromptStep(interaction, ctx, hash, value, step) {
|
|
410
|
+
// Store the answer
|
|
411
|
+
ctx.inputs[step.prompt.key] = value;
|
|
412
|
+
ctx.stepIndex++;
|
|
413
|
+
// Find the next prompt step that passes its `when` condition
|
|
414
|
+
await interaction.deferUpdate();
|
|
415
|
+
await showNextStep(interaction, ctx, hash);
|
|
416
|
+
}
|
|
417
|
+
// ── Step rendering ──────────────────────────────────────────────
|
|
418
|
+
// Advances through steps, skipping prompts whose `when` condition
|
|
419
|
+
// fails, until it finds one to show or reaches the end.
|
|
420
|
+
async function showNextStep(interaction, ctx, hash) {
|
|
421
|
+
// Skip prompts whose `when` condition doesn't match
|
|
422
|
+
while (ctx.stepIndex < ctx.steps.length) {
|
|
423
|
+
const step = ctx.steps[ctx.stepIndex];
|
|
424
|
+
if (step.type === 'prompt' && !shouldShowPrompt(step.prompt, ctx.inputs)) {
|
|
425
|
+
ctx.stepIndex++;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
if (ctx.stepIndex >= ctx.steps.length) {
|
|
431
|
+
// All steps done — proceed to action
|
|
432
|
+
if (ctx.methodType === 'api') {
|
|
433
|
+
// We're deferred, so show a button that opens the API key modal
|
|
434
|
+
const button = new ButtonBuilder()
|
|
435
|
+
.setCustomId(`login_apikey_btn:${hash}`)
|
|
436
|
+
.setLabel('Enter API Key')
|
|
437
|
+
.setStyle(ButtonStyle.Primary);
|
|
438
|
+
await interaction.editReply({
|
|
439
|
+
content: `**Authenticate with ${ctx.providerName}**\nClick to enter your API key.`,
|
|
440
|
+
components: [
|
|
441
|
+
new ActionRowBuilder().addComponents(button),
|
|
442
|
+
],
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
await startOAuthFlow(interaction, ctx, hash);
|
|
447
|
+
}
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const step = ctx.steps[ctx.stepIndex];
|
|
451
|
+
pendingLoginContexts.set(hash, ctx);
|
|
452
|
+
if (step.type === 'method') {
|
|
453
|
+
const options = step.methods.slice(0, 25).map((method, index) => ({
|
|
454
|
+
label: method.label.slice(0, 100),
|
|
455
|
+
value: String(index),
|
|
456
|
+
description: method.type === 'oauth'
|
|
457
|
+
? 'OAuth authentication'
|
|
458
|
+
: 'Enter API key manually',
|
|
459
|
+
}));
|
|
460
|
+
await interaction.editReply({
|
|
461
|
+
content: `**Authenticate with ${ctx.providerName}**\nSelect authentication method:`,
|
|
462
|
+
components: [
|
|
463
|
+
buildSelectMenu({
|
|
464
|
+
customId: `login_select:${hash}`,
|
|
465
|
+
placeholder: 'Select authentication method',
|
|
466
|
+
options,
|
|
467
|
+
}),
|
|
468
|
+
],
|
|
469
|
+
});
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (step.type === 'prompt') {
|
|
473
|
+
const prompt = step.prompt;
|
|
474
|
+
if (prompt.type === 'select') {
|
|
475
|
+
const options = prompt.options.slice(0, 25).map((opt) => ({
|
|
476
|
+
label: opt.label.slice(0, 100),
|
|
477
|
+
value: opt.value,
|
|
478
|
+
description: opt.hint?.slice(0, 100),
|
|
479
|
+
}));
|
|
480
|
+
await interaction.editReply({
|
|
481
|
+
content: `**Authenticate with ${ctx.providerName}**\n${prompt.message}`,
|
|
482
|
+
components: [
|
|
483
|
+
buildSelectMenu({
|
|
484
|
+
customId: `login_select:${hash}`,
|
|
485
|
+
placeholder: prompt.message.slice(0, 150),
|
|
486
|
+
options,
|
|
487
|
+
}),
|
|
488
|
+
],
|
|
489
|
+
});
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (prompt.type === 'text') {
|
|
493
|
+
// Text prompts need a modal, but we're deferred. Show a button.
|
|
494
|
+
const button = new ButtonBuilder()
|
|
495
|
+
.setCustomId(`login_text_btn:${hash}`)
|
|
496
|
+
.setLabel(prompt.message.slice(0, 80))
|
|
497
|
+
.setStyle(ButtonStyle.Primary);
|
|
498
|
+
await interaction.editReply({
|
|
499
|
+
content: `**Authenticate with ${ctx.providerName}**\n${prompt.message}`,
|
|
500
|
+
components: [
|
|
501
|
+
new ActionRowBuilder().addComponents(button),
|
|
502
|
+
],
|
|
503
|
+
});
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function buildPromptSteps(method) {
|
|
509
|
+
return (method.prompts || []).map((prompt) => ({
|
|
510
|
+
type: 'prompt',
|
|
511
|
+
prompt,
|
|
512
|
+
}));
|
|
513
|
+
}
|
|
514
|
+
// ── Text prompt button + modal ──────────────────────────────────
|
|
515
|
+
// When a text prompt needs to be shown but we're in a deferred state,
|
|
516
|
+
// we show a button. Clicking it opens a modal for text input.
|
|
517
|
+
export async function handleLoginTextButton(interaction) {
|
|
518
|
+
if (!interaction.customId.startsWith('login_text_btn:')) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const hash = interaction.customId.replace('login_text_btn:', '');
|
|
522
|
+
const ctx = pendingLoginContexts.get(hash);
|
|
523
|
+
if (!ctx) {
|
|
524
|
+
await interaction.reply({
|
|
525
|
+
content: 'Selection expired. Please run /login again.',
|
|
526
|
+
flags: MessageFlags.Ephemeral,
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const step = ctx.steps[ctx.stepIndex];
|
|
531
|
+
if (!step || step.type !== 'prompt' || step.prompt.type !== 'text') {
|
|
532
|
+
await interaction.reply({
|
|
533
|
+
content: 'Invalid state. Please run /login again.',
|
|
534
|
+
flags: MessageFlags.Ephemeral,
|
|
535
|
+
});
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const modal = new ModalBuilder()
|
|
539
|
+
.setCustomId(`login_text:${hash}`)
|
|
540
|
+
.setTitle(`${ctx.providerName || 'Provider'} Login`.slice(0, 45));
|
|
541
|
+
const textInput = new TextInputBuilder()
|
|
542
|
+
.setCustomId('prompt_value')
|
|
543
|
+
.setLabel(step.prompt.message.slice(0, 45))
|
|
544
|
+
.setPlaceholder(step.prompt.type === 'text' ? (step.prompt.placeholder || '') : '')
|
|
545
|
+
.setStyle(TextInputStyle.Short)
|
|
546
|
+
.setRequired(true);
|
|
547
|
+
modal.addComponents(new ActionRowBuilder().addComponents(textInput));
|
|
548
|
+
await interaction.showModal(modal);
|
|
549
|
+
}
|
|
550
|
+
export async function handleLoginTextModalSubmit(interaction) {
|
|
551
|
+
if (!interaction.customId.startsWith('login_text:')) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
await interaction.deferUpdate();
|
|
555
|
+
const hash = interaction.customId.replace('login_text:', '');
|
|
556
|
+
const ctx = pendingLoginContexts.get(hash);
|
|
557
|
+
if (!ctx) {
|
|
558
|
+
await interaction.editReply({
|
|
559
|
+
content: 'Selection expired. Please run /login again.',
|
|
560
|
+
components: [],
|
|
561
|
+
});
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const step = ctx.steps[ctx.stepIndex];
|
|
565
|
+
if (!step || step.type !== 'prompt' || step.prompt.type !== 'text') {
|
|
566
|
+
await interaction.editReply({
|
|
567
|
+
content: 'Invalid state. Please run /login again.',
|
|
568
|
+
components: [],
|
|
569
|
+
});
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const value = interaction.fields.getTextInputValue('prompt_value');
|
|
573
|
+
if (!value?.trim()) {
|
|
574
|
+
await interaction.editReply({
|
|
575
|
+
content: 'A value is required.',
|
|
576
|
+
components: [],
|
|
577
|
+
});
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
ctx.inputs[step.prompt.key] = value.trim();
|
|
581
|
+
ctx.stepIndex++;
|
|
582
|
+
await showNextStep(interaction, ctx, hash);
|
|
583
|
+
}
|
|
584
|
+
// ── API key button + modal ──────────────────────────────────────
|
|
585
|
+
// When we're deferred and need an API key modal, show a button first.
|
|
586
|
+
export async function handleLoginApiKeyButton(interaction) {
|
|
587
|
+
if (!interaction.customId.startsWith('login_apikey_btn:')) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const hash = interaction.customId.replace('login_apikey_btn:', '');
|
|
591
|
+
const ctx = pendingLoginContexts.get(hash);
|
|
592
|
+
if (!ctx || !ctx.providerName) {
|
|
593
|
+
await interaction.reply({
|
|
594
|
+
content: 'Selection expired. Please run /login again.',
|
|
595
|
+
flags: MessageFlags.Ephemeral,
|
|
596
|
+
});
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
await showApiKeyModal(interaction, hash, ctx.providerName);
|
|
600
|
+
}
|
|
601
|
+
async function showApiKeyModal(interaction, hash, providerName) {
|
|
602
|
+
const modal = new ModalBuilder()
|
|
603
|
+
.setCustomId(`login_apikey:${hash}`)
|
|
604
|
+
.setTitle(`${providerName} API Key`.slice(0, 45));
|
|
605
|
+
const apiKeyInput = new TextInputBuilder()
|
|
606
|
+
.setCustomId('apikey')
|
|
607
|
+
.setLabel('API Key')
|
|
608
|
+
.setPlaceholder('sk-...')
|
|
609
|
+
.setStyle(TextInputStyle.Short)
|
|
610
|
+
.setRequired(true);
|
|
611
|
+
modal.addComponents(new ActionRowBuilder().addComponents(apiKeyInput));
|
|
612
|
+
await interaction.showModal(modal);
|
|
613
|
+
}
|
|
614
|
+
// ── OAuth code submission (code mode) ───────────────────────────
|
|
615
|
+
// When the OAuth flow returns method="code", the user completes login
|
|
616
|
+
// in a browser (possibly on a different machine) and pastes the final
|
|
617
|
+
// callback URL or authorization code here.
|
|
618
|
+
export async function handleOAuthCodeButton(interaction) {
|
|
619
|
+
if (!interaction.customId.startsWith('login_oauth_code_btn:')) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const hash = interaction.customId.replace('login_oauth_code_btn:', '');
|
|
623
|
+
const ctx = pendingLoginContexts.get(hash);
|
|
624
|
+
if (!ctx || !ctx.providerId || !ctx.providerName) {
|
|
625
|
+
await interaction.reply({
|
|
626
|
+
content: 'Selection expired. Please run /login again.',
|
|
627
|
+
flags: MessageFlags.Ephemeral,
|
|
628
|
+
});
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const modal = new ModalBuilder()
|
|
632
|
+
.setCustomId(`login_oauth_code:${hash}`)
|
|
633
|
+
.setTitle(`${ctx.providerName} Authorization`.slice(0, 45));
|
|
634
|
+
const codeInput = new TextInputBuilder()
|
|
635
|
+
.setCustomId('oauth_code')
|
|
636
|
+
.setLabel('Authorization code or callback URL')
|
|
637
|
+
.setPlaceholder('Paste the code or full callback URL')
|
|
638
|
+
.setStyle(TextInputStyle.Paragraph)
|
|
639
|
+
.setRequired(true);
|
|
640
|
+
modal.addComponents(new ActionRowBuilder().addComponents(codeInput));
|
|
641
|
+
await interaction.showModal(modal);
|
|
642
|
+
}
|
|
643
|
+
export async function handleOAuthCodeModalSubmit(interaction) {
|
|
644
|
+
if (!interaction.customId.startsWith('login_oauth_code:')) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
await interaction.deferUpdate();
|
|
648
|
+
const hash = interaction.customId.replace('login_oauth_code:', '');
|
|
649
|
+
const ctx = pendingLoginContexts.get(hash);
|
|
650
|
+
if (!ctx || !ctx.providerId || !ctx.providerName || ctx.methodIndex === undefined) {
|
|
651
|
+
await interaction.editReply({
|
|
652
|
+
content: 'Session expired. Please run /login again.',
|
|
653
|
+
components: [],
|
|
654
|
+
});
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const code = interaction.fields.getTextInputValue('oauth_code')?.trim();
|
|
658
|
+
if (!code) {
|
|
659
|
+
await interaction.editReply({
|
|
660
|
+
content: 'Authorization code is required.',
|
|
661
|
+
components: [],
|
|
662
|
+
});
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
const getClient = await initializeOpencodeForDirectory(ctx.dir);
|
|
667
|
+
if (getClient instanceof Error) {
|
|
668
|
+
await interaction.editReply({
|
|
669
|
+
content: getClient.message,
|
|
670
|
+
components: [],
|
|
671
|
+
});
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
await interaction.editReply({
|
|
675
|
+
content: `**Authenticating with ${ctx.providerName}**\nVerifying authorization...`,
|
|
676
|
+
components: [],
|
|
677
|
+
});
|
|
678
|
+
const callbackResponse = await getClient().provider.oauth.callback({
|
|
679
|
+
providerID: ctx.providerId,
|
|
680
|
+
method: ctx.methodIndex,
|
|
681
|
+
code,
|
|
682
|
+
directory: ctx.dir,
|
|
683
|
+
});
|
|
684
|
+
if (callbackResponse.error) {
|
|
685
|
+
pendingLoginContexts.delete(hash);
|
|
686
|
+
await interaction.editReply({
|
|
687
|
+
content: `**Authentication Failed**\n${extractErrorMessage({ error: callbackResponse.error, fallback: 'Authorization code was invalid or expired' })}`,
|
|
688
|
+
components: [],
|
|
689
|
+
});
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
await getClient().instance.dispose({ directory: ctx.dir });
|
|
693
|
+
pendingLoginContexts.delete(hash);
|
|
694
|
+
await interaction.editReply({
|
|
695
|
+
content: `✅ **Successfully authenticated with ${ctx.providerName}!**\n\nYou can now use models from this provider.`,
|
|
696
|
+
components: [],
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
loginLogger.error('OAuth code submission error:', error);
|
|
701
|
+
pendingLoginContexts.delete(hash);
|
|
702
|
+
await interaction.editReply({
|
|
703
|
+
content: `**Authentication Failed**\n${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
704
|
+
components: [],
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
export async function handleApiKeyModalSubmit(interaction) {
|
|
709
|
+
if (!interaction.customId.startsWith('login_apikey:')) {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
713
|
+
const hash = interaction.customId.replace('login_apikey:', '');
|
|
714
|
+
const ctx = pendingLoginContexts.get(hash);
|
|
715
|
+
if (!ctx || !ctx.providerId || !ctx.providerName) {
|
|
716
|
+
await interaction.editReply({
|
|
717
|
+
content: 'Session expired. Please run /login again.',
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const apiKey = interaction.fields.getTextInputValue('apikey');
|
|
722
|
+
if (!apiKey?.trim()) {
|
|
723
|
+
await interaction.editReply({ content: 'API key is required.' });
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
const getClient = await initializeOpencodeForDirectory(ctx.dir);
|
|
728
|
+
if (getClient instanceof Error) {
|
|
729
|
+
await interaction.editReply({ content: getClient.message });
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
await getClient().auth.set({
|
|
733
|
+
providerID: ctx.providerId,
|
|
734
|
+
auth: { type: 'api', key: apiKey.trim() },
|
|
735
|
+
});
|
|
736
|
+
// Dispose to refresh provider state so new credentials are recognized
|
|
737
|
+
await getClient().instance.dispose({ directory: ctx.dir });
|
|
738
|
+
await interaction.editReply({
|
|
739
|
+
content: `✅ **Successfully authenticated with ${ctx.providerName}!**\n\nYou can now use models from this provider.`,
|
|
740
|
+
});
|
|
741
|
+
pendingLoginContexts.delete(hash);
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
loginLogger.error('API key save error:', error);
|
|
745
|
+
await interaction.editReply({
|
|
746
|
+
content: `**Failed to save API key**\n${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// ── OAuth flow ──────────────────────────────────────────────────
|
|
751
|
+
async function startOAuthFlow(interaction, ctx, hash) {
|
|
752
|
+
if (!ctx.providerId || ctx.methodIndex === undefined) {
|
|
753
|
+
await interaction.editReply({
|
|
754
|
+
content: 'Invalid context for OAuth flow',
|
|
755
|
+
components: [],
|
|
756
|
+
});
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
const getClient = await initializeOpencodeForDirectory(ctx.dir);
|
|
761
|
+
if (getClient instanceof Error) {
|
|
762
|
+
await interaction.editReply({
|
|
763
|
+
content: getClient.message,
|
|
764
|
+
components: [],
|
|
765
|
+
});
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
await interaction.editReply({
|
|
769
|
+
content: `**Authenticating with ${ctx.providerName}**\nStarting authorization...`,
|
|
770
|
+
components: [],
|
|
771
|
+
});
|
|
772
|
+
// Direct fetch to the server because the SDK's buildClientParams drops
|
|
773
|
+
// unknown keys — `inputs` would be silently stripped. The server accepts
|
|
774
|
+
// `inputs` in the body (see opencode server/routes/provider.ts).
|
|
775
|
+
const port = getOpencodeServerPort();
|
|
776
|
+
if (!port) {
|
|
777
|
+
await interaction.editReply({
|
|
778
|
+
content: 'OpenCode server is not running. Please try again.',
|
|
779
|
+
components: [],
|
|
780
|
+
});
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const hasInputs = Object.keys(ctx.inputs).length > 0;
|
|
784
|
+
const authorizeUrl = new URL(`/provider/${encodeURIComponent(ctx.providerId)}/oauth/authorize`, `http://127.0.0.1:${port}`);
|
|
785
|
+
authorizeUrl.searchParams.set('directory', ctx.dir);
|
|
786
|
+
// Include basic auth if OPENCODE_SERVER_PASSWORD is set,
|
|
787
|
+
// matching the opencode server's optional basicAuth middleware.
|
|
788
|
+
const fetchHeaders = {
|
|
789
|
+
'Content-Type': 'application/json',
|
|
790
|
+
'x-opencode-directory': ctx.dir,
|
|
791
|
+
};
|
|
792
|
+
const serverPassword = process.env.OPENCODE_SERVER_PASSWORD;
|
|
793
|
+
if (serverPassword) {
|
|
794
|
+
const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode';
|
|
795
|
+
fetchHeaders['Authorization'] =
|
|
796
|
+
`Basic ${Buffer.from(`${username}:${serverPassword}`).toString('base64')}`;
|
|
797
|
+
}
|
|
798
|
+
const authorizeRes = await fetch(authorizeUrl, {
|
|
799
|
+
method: 'POST',
|
|
800
|
+
headers: fetchHeaders,
|
|
801
|
+
body: JSON.stringify({
|
|
802
|
+
method: ctx.methodIndex,
|
|
803
|
+
...(hasInputs ? { inputs: ctx.inputs } : {}),
|
|
804
|
+
}),
|
|
805
|
+
});
|
|
806
|
+
if (!authorizeRes.ok) {
|
|
807
|
+
const errorText = await authorizeRes.text().catch(() => '');
|
|
808
|
+
let errorMessage = 'Unknown error';
|
|
809
|
+
try {
|
|
810
|
+
const parsed = JSON.parse(errorText);
|
|
811
|
+
errorMessage = parsed?.data?.message || parsed?.message || errorMessage;
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
errorMessage = errorText || errorMessage;
|
|
815
|
+
}
|
|
816
|
+
await interaction.editReply({
|
|
817
|
+
content: `Failed to start authorization: ${errorMessage}`,
|
|
818
|
+
components: [],
|
|
819
|
+
});
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const { url, method, instructions } = (await authorizeRes.json());
|
|
823
|
+
let message = `**Authenticating with ${ctx.providerName}**\n\n`;
|
|
824
|
+
message += `Open this URL to authorize:\n${url}\n\n`;
|
|
825
|
+
if (instructions) {
|
|
826
|
+
// Match "code: ABC-123" or "code: WXYZ1234" but not natural language
|
|
827
|
+
// like "code will". Require a colon separator and uppercase alphanum code.
|
|
828
|
+
const codeMatch = instructions.match(/code:\s*([A-Z0-9][A-Z0-9-]+)/);
|
|
829
|
+
if (codeMatch) {
|
|
830
|
+
message += `**Code:** \`${codeMatch[1]}\`\n\n`;
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
message += `${instructions}\n\n`;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (method === 'auto') {
|
|
837
|
+
message += '_Waiting for authorization to complete..._';
|
|
838
|
+
}
|
|
839
|
+
if (method === 'code') {
|
|
840
|
+
// Code mode: show a button to paste the auth code/URL after
|
|
841
|
+
// completing login in a browser (possibly on a different machine).
|
|
842
|
+
const button = new ButtonBuilder()
|
|
843
|
+
.setCustomId(`login_oauth_code_btn:${hash}`)
|
|
844
|
+
.setLabel('Paste authorization code')
|
|
845
|
+
.setStyle(ButtonStyle.Primary);
|
|
846
|
+
await interaction.editReply({
|
|
847
|
+
content: message,
|
|
848
|
+
components: [
|
|
849
|
+
new ActionRowBuilder().addComponents(button),
|
|
850
|
+
],
|
|
851
|
+
});
|
|
852
|
+
// Don't delete context — we need it for the code submission
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
await interaction.editReply({ content: message, components: [] });
|
|
856
|
+
// Auto mode: poll for completion (device flow / localhost callback)
|
|
857
|
+
const callbackResponse = await getClient().provider.oauth.callback({
|
|
858
|
+
providerID: ctx.providerId,
|
|
859
|
+
method: ctx.methodIndex,
|
|
860
|
+
directory: ctx.dir,
|
|
861
|
+
});
|
|
862
|
+
if (callbackResponse.error) {
|
|
863
|
+
pendingLoginContexts.delete(hash);
|
|
864
|
+
await interaction.editReply({
|
|
865
|
+
content: `**Authentication Failed**\n${extractErrorMessage({ error: callbackResponse.error, fallback: 'Authorization was not completed' })}`,
|
|
866
|
+
components: [],
|
|
867
|
+
});
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
await getClient().instance.dispose({ directory: ctx.dir });
|
|
871
|
+
pendingLoginContexts.delete(hash);
|
|
872
|
+
await interaction.editReply({
|
|
873
|
+
content: `✅ **Successfully authenticated with ${ctx.providerName}!**\n\nYou can now use models from this provider.`,
|
|
874
|
+
components: [],
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
loginLogger.error('OAuth flow error:', error);
|
|
879
|
+
pendingLoginContexts.delete(hash);
|
|
880
|
+
await interaction.editReply({
|
|
881
|
+
content: `**Authentication Failed**\n${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
882
|
+
components: [],
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|