@otto-assistant/otto 0.1.2 → 0.7.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin.js +2 -0
- package/dist/agent-model.e2e.test.js +755 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/anthropic-account-identity.js +62 -0
- package/dist/anthropic-account-identity.test.js +38 -0
- package/dist/anthropic-auth-plugin.js +917 -0
- package/dist/anthropic-auth-state.js +303 -0
- package/dist/anthropic-auth-state.test.js +150 -0
- package/dist/bin.js +152 -0
- package/dist/btw-prefix-detection.js +17 -0
- package/dist/btw-prefix-detection.test.js +63 -0
- package/dist/channel-management.js +259 -0
- package/dist/cli-parsing.test.js +142 -0
- package/dist/cli-send-thread.e2e.test.js +353 -0
- package/dist/cli-telegram-options.test.js +99 -0
- package/dist/cli.js +4210 -568
- package/dist/commands/abort.js +65 -0
- package/dist/commands/action-buttons.js +245 -0
- package/dist/commands/add-dir.js +124 -0
- package/dist/commands/add-dir.test.js +126 -0
- package/dist/commands/add-project.js +113 -0
- package/dist/commands/agent.js +355 -0
- package/dist/commands/ask-question.js +320 -0
- package/dist/commands/ask-question.test.js +92 -0
- package/dist/commands/btw.js +121 -0
- package/dist/commands/cli-commands-group-a.test.js +728 -0
- package/dist/commands/cli-commands-group-b.test.js +695 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +130 -0
- package/dist/commands/diff.js +63 -0
- package/dist/commands/discord-commands-group-a.test.js +621 -0
- package/dist/commands/discord-commands-group-b.test.js +595 -0
- package/dist/commands/discord-commands-group-c.test.js +739 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork-subagent.js +177 -0
- package/dist/commands/fork.js +262 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +887 -0
- package/dist/commands/mcp.js +239 -0
- package/dist/commands/memory-snapshot.js +24 -0
- package/dist/commands/mention-mode.js +44 -0
- package/dist/commands/merge-worktree.js +162 -0
- package/dist/commands/model-variant.js +366 -0
- package/dist/commands/model.js +794 -0
- package/dist/commands/new-worktree.js +465 -0
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/permissions.js +274 -0
- package/dist/commands/queue.js +223 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +127 -0
- package/dist/commands/resume.js +149 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/screenshare.js +303 -0
- package/dist/commands/screenshare.test.js +20 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +176 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/tasks.js +205 -0
- package/dist/commands/thread-deletion-sync.js +50 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +305 -0
- package/dist/commands/unset-model.js +139 -0
- package/dist/commands/upgrade.js +48 -0
- package/dist/commands/user-command.js +155 -0
- package/dist/commands/verbosity.js +125 -0
- package/dist/commands/vscode.js +269 -0
- package/dist/commands/worktree-settings.js +43 -0
- package/dist/commands/worktrees.js +468 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +100 -255
- package/dist/context-awareness-plugin.js +340 -0
- package/dist/context-awareness-plugin.test.js +126 -0
- package/dist/critique-utils.js +95 -0
- package/dist/database.js +1355 -0
- package/dist/db.js +260 -0
- package/dist/db.test.js +138 -0
- package/dist/debounce-timeout.js +28 -0
- package/dist/debounced-process-flush.js +77 -0
- package/dist/discord-bot.js +1124 -0
- package/dist/discord-command-registration.js +567 -0
- package/dist/discord-urls.js +82 -0
- package/dist/discord-utils.js +616 -0
- package/dist/discord-utils.test.js +134 -0
- package/dist/errors.js +157 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/event-stream-real-capture.e2e.test.js +533 -0
- package/dist/eventsource-parser.test.js +327 -0
- package/dist/exec-async.js +26 -0
- package/dist/external-opencode-sync.js +480 -0
- package/dist/format-tables.js +491 -0
- package/dist/format-tables.test.js +478 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +113 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
- package/dist/gateway-proxy.e2e.test.js +485 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +37 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +58 -0
- package/dist/generated/internal/class.js +49 -0
- package/dist/generated/internal/prismaNamespace.js +254 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_events.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +122 -0
- package/dist/hrana-server.js +251 -0
- package/dist/hrana-server.test.js +370 -0
- package/dist/html-actions.js +123 -0
- package/dist/html-actions.test.js +70 -0
- package/dist/html-components.js +117 -0
- package/dist/html-components.test.js +34 -0
- package/dist/image-optimizer-plugin.js +153 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +420 -0
- package/dist/ipc-polling.js +327 -0
- package/dist/ipc-tools-plugin.js +193 -0
- package/dist/ipc-utils.js +18 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +171 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +264 -0
- package/dist/memory-overview-plugin.js +128 -0
- package/dist/message-finish-field.e2e.test.js +168 -0
- package/dist/message-formatting.js +415 -0
- package/dist/message-formatting.test.js +115 -0
- package/dist/message-preprocessing.js +359 -0
- package/dist/onboarding-tutorial.js +163 -0
- package/dist/onboarding-welcome.js +37 -0
- package/dist/openai-realtime.js +224 -0
- package/dist/opencode-command-detection.js +65 -0
- package/dist/opencode-command-detection.test.js +240 -0
- package/dist/opencode-command.js +131 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +388 -0
- package/dist/opencode-interrupt-plugin.test.js +463 -0
- package/dist/opencode.js +1117 -0
- package/dist/otto/branding.js +22 -0
- package/dist/otto/index.js +21 -0
- package/dist/otto-digital-twin.e2e.test.js +161 -0
- package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
- package/dist/otto-opencode-plugin.js +21 -0
- package/dist/otto-opencode-plugin.test.js +98 -0
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/patch-text-parser.js +97 -0
- package/dist/plugin-logger.js +68 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/queue-advanced-abort.e2e.test.js +293 -0
- package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
- package/dist/queue-advanced-e2e-setup.js +790 -0
- package/dist/queue-advanced-footer.e2e.test.js +481 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
- package/dist/queue-advanced-question.e2e.test.js +261 -0
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
- package/dist/queue-advanced-typing.e2e.test.js +153 -0
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
- package/dist/queue-interrupt-drain.e2e.test.js +135 -0
- package/dist/queue-question-select-drain.e2e.test.js +256 -0
- package/dist/runtime-idle-sweeper.js +52 -0
- package/dist/runtime-lifecycle.e2e.test.js +514 -0
- package/dist/sentry.js +23 -0
- package/dist/session-handler/agent-utils.js +67 -0
- package/dist/session-handler/event-stream-state.js +475 -0
- package/dist/session-handler/event-stream-state.test.js +632 -0
- package/dist/session-handler/model-utils.js +147 -0
- package/dist/session-handler/opencode-session-event-log.js +94 -0
- package/dist/session-handler/thread-runtime-state.js +131 -0
- package/dist/session-handler/thread-session-runtime.js +3390 -0
- package/dist/session-handler.js +9 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/session-title-rename.test.js +92 -0
- package/dist/skill-filter.js +31 -0
- package/dist/skill-filter.test.js +65 -0
- package/dist/startup-service.js +153 -0
- package/dist/startup-time.e2e.test.js +296 -0
- package/dist/store.js +19 -0
- package/dist/subagent-rate-limit-plugin.js +175 -0
- package/dist/system-message.js +702 -0
- package/dist/system-message.test.js +697 -0
- package/dist/task-runner.js +530 -0
- package/dist/task-schedule.js +213 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/test-utils.js +313 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +1111 -0
- package/dist/tools.js +357 -0
- package/dist/undo-redo.e2e.test.js +161 -0
- package/dist/unnest-code-blocks.js +146 -0
- package/dist/unnest-code-blocks.test.js +673 -0
- package/dist/upgrade.js +156 -0
- package/dist/utils.js +172 -0
- package/dist/utils.test.js +130 -0
- package/dist/voice-attachment.js +34 -0
- package/dist/voice-handler.js +646 -0
- package/dist/voice-message.e2e.test.js +1021 -0
- package/dist/voice.js +456 -0
- package/dist/voice.test.js +235 -0
- package/dist/wait-session.js +171 -0
- package/dist/websockify.js +69 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-lifecycle.e2e.test.js +311 -0
- package/dist/worktree-utils.js +3 -0
- package/dist/worktrees.js +991 -0
- package/dist/worktrees.test.js +415 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +90 -38
- package/schema.prisma +303 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +112 -0
- package/skills/egaki/SKILL.md +100 -0
- package/skills/errore/SKILL.md +647 -0
- package/skills/event-sourcing-state/SKILL.md +252 -0
- package/skills/goke/SKILL.md +38 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/lintcn/SKILL.md +873 -0
- package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
- package/skills/new-skill/SKILL.md +237 -0
- package/skills/npm-package/SKILL.md +617 -0
- package/skills/opensrc/SKILL.md +78 -0
- package/skills/otto-publish/SKILL.md +61 -0
- package/skills/playwriter/SKILL.md +35 -0
- package/skills/profano/SKILL.md +16 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/sigillo/SKILL.md +101 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/spiceflow/SKILL.md +28 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +98 -0
- package/skills/usecomputer/SKILL.md +264 -0
- package/skills/x-articles/SKILL.md +554 -0
- package/skills/zele/SKILL.md +49 -0
- package/skills/zustand-centralized-state/SKILL.md +1004 -0
- package/src/agent-model.e2e.test.ts +979 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +283 -0
- package/src/ai-tool.ts +39 -0
- package/src/anthropic-account-identity.test.ts +52 -0
- package/src/anthropic-account-identity.ts +77 -0
- package/src/anthropic-auth-plugin.ts +1139 -0
- package/src/anthropic-auth-state.test.ts +187 -0
- package/src/anthropic-auth-state.ts +386 -0
- package/src/bin.ts +182 -0
- package/src/btw-prefix-detection.test.ts +73 -0
- package/src/btw-prefix-detection.ts +23 -0
- package/src/channel-management.ts +376 -0
- package/src/cli-parsing.test.ts +197 -0
- package/src/cli-send-thread.e2e.test.ts +463 -0
- package/src/cli-telegram-options.test.ts +114 -0
- package/src/cli.ts +5718 -580
- package/src/commands/abort.ts +89 -0
- package/src/commands/action-buttons.ts +364 -0
- package/src/commands/add-dir.test.ts +154 -0
- package/src/commands/add-dir.ts +175 -0
- package/src/commands/add-project.ts +149 -0
- package/src/commands/agent.ts +496 -0
- package/src/commands/ask-question.test.ts +111 -0
- package/src/commands/ask-question.ts +455 -0
- package/src/commands/btw.ts +184 -0
- package/src/commands/cli-commands-group-a.test.ts +837 -0
- package/src/commands/cli-commands-group-b.test.ts +800 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +190 -0
- package/src/commands/diff.ts +91 -0
- package/src/commands/discord-commands-group-a.test.ts +751 -0
- package/src/commands/discord-commands-group-b.test.ts +648 -0
- package/src/commands/discord-commands-group-c.test.ts +882 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork-subagent.ts +263 -0
- package/src/commands/fork.ts +386 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +1175 -0
- package/src/commands/mcp.ts +307 -0
- package/src/commands/memory-snapshot.ts +30 -0
- package/src/commands/mention-mode.ts +68 -0
- package/src/commands/merge-worktree.ts +226 -0
- package/src/commands/model-variant.ts +485 -0
- package/src/commands/model.ts +1078 -0
- package/src/commands/new-worktree.ts +645 -0
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/permissions.ts +397 -0
- package/src/commands/queue.ts +293 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +230 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/screenshare.test.ts +30 -0
- package/src/commands/screenshare.ts +366 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +227 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/tasks.ts +293 -0
- package/src/commands/thread-deletion-sync.ts +80 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +386 -0
- package/src/commands/unset-model.ts +174 -0
- package/src/commands/upgrade.ts +59 -0
- package/src/commands/user-command.ts +198 -0
- package/src/commands/verbosity.ts +173 -0
- package/src/commands/vscode.ts +342 -0
- package/src/commands/worktree-settings.ts +70 -0
- package/src/commands/worktrees.ts +645 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +103 -339
- package/src/context-awareness-plugin.test.ts +144 -0
- package/src/context-awareness-plugin.ts +469 -0
- package/src/critique-utils.ts +139 -0
- package/src/database.ts +1949 -0
- package/src/db.test.ts +162 -0
- package/src/db.ts +295 -0
- package/src/debounce-timeout.ts +43 -0
- package/src/debounced-process-flush.ts +104 -0
- package/src/discord-bot.ts +1505 -0
- package/src/discord-command-registration.ts +752 -0
- package/src/discord-urls.ts +89 -0
- package/src/discord-utils.test.ts +153 -0
- package/src/discord-utils.ts +846 -0
- package/src/errors.ts +201 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/event-stream-real-capture.e2e.test.ts +692 -0
- package/src/eventsource-parser.test.ts +351 -0
- package/src/exec-async.ts +35 -0
- package/src/external-opencode-sync.ts +685 -0
- package/src/format-tables.test.ts +515 -0
- package/src/format-tables.ts +718 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +172 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
- package/src/gateway-proxy.e2e.test.ts +644 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +114 -0
- package/src/generated/client.ts +138 -0
- package/src/generated/commonInputTypes.ts +770 -0
- package/src/generated/enums.ts +98 -0
- package/src/generated/internal/class.ts +384 -0
- package/src/generated/internal/prismaNamespace.ts +2394 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1700 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +1859 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1228 -0
- package/src/generated/models/channel_worktrees.ts +1300 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_events.ts +1439 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1781 -0
- package/src/generated/models/thread_worktrees.ts +1356 -0
- package/src/generated/models.ts +30 -0
- package/src/heap-monitor.ts +152 -0
- package/src/hrana-server.test.ts +434 -0
- package/src/hrana-server.ts +299 -0
- package/src/html-actions.test.ts +87 -0
- package/src/html-actions.ts +174 -0
- package/src/html-components.test.ts +38 -0
- package/src/html-components.ts +181 -0
- package/src/image-optimizer-plugin.ts +194 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +610 -0
- package/src/ipc-polling.ts +427 -0
- package/src/ipc-tools-plugin.ts +236 -0
- package/src/ipc-utils.ts +29 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +215 -0
- package/src/markdown.test.ts +315 -0
- package/src/markdown.ts +410 -0
- package/src/memory-overview-plugin.ts +163 -0
- package/src/message-finish-field.e2e.test.ts +195 -0
- package/src/message-formatting.test.ts +126 -0
- package/src/message-formatting.ts +535 -0
- package/src/message-preprocessing.ts +488 -0
- package/src/onboarding-tutorial.ts +167 -0
- package/src/onboarding-welcome.ts +49 -0
- package/src/openai-realtime.ts +358 -0
- package/src/opencode-command-detection.test.ts +307 -0
- package/src/opencode-command-detection.ts +76 -0
- package/src/opencode-command.test.ts +70 -0
- package/src/opencode-command.ts +191 -0
- package/src/opencode-interrupt-plugin.test.ts +682 -0
- package/src/opencode-interrupt-plugin.ts +507 -0
- package/src/opencode.ts +1453 -0
- package/src/otto/branding.ts +23 -0
- package/src/otto/index.ts +22 -0
- package/src/otto-digital-twin.e2e.test.ts +199 -0
- package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
- package/src/otto-opencode-plugin.test.ts +108 -0
- package/src/otto-opencode-plugin.ts +22 -0
- package/src/parse-permission-rules.test.ts +127 -0
- package/src/patch-text-parser.ts +107 -0
- package/src/plugin-logger.ts +84 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/queue-advanced-abort.e2e.test.ts +382 -0
- package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
- package/src/queue-advanced-e2e-setup.ts +877 -0
- package/src/queue-advanced-footer.e2e.test.ts +591 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
- package/src/queue-advanced-question.e2e.test.ts +316 -0
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
- package/src/queue-advanced-typing.e2e.test.ts +199 -0
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
- package/src/queue-interrupt-drain.e2e.test.ts +166 -0
- package/src/queue-question-select-drain.e2e.test.ts +327 -0
- package/src/runtime-idle-sweeper.ts +76 -0
- package/src/runtime-lifecycle.e2e.test.ts +651 -0
- package/src/schema.sql +174 -0
- package/src/sentry.ts +26 -0
- package/src/session-handler/agent-utils.ts +99 -0
- package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
- package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
- package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
- package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
- package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
- package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
- package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
- package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
- package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
- package/src/session-handler/event-stream-state.test.ts +717 -0
- package/src/session-handler/event-stream-state.ts +706 -0
- package/src/session-handler/model-utils.ts +217 -0
- package/src/session-handler/opencode-session-event-log.ts +130 -0
- package/src/session-handler/thread-runtime-state.ts +247 -0
- package/src/session-handler/thread-session-runtime.ts +4440 -0
- package/src/session-handler.ts +15 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/session-title-rename.test.ts +130 -0
- package/src/skill-filter.test.ts +83 -0
- package/src/skill-filter.ts +42 -0
- package/src/startup-service.ts +200 -0
- package/src/startup-time.e2e.test.ts +373 -0
- package/src/store.ts +139 -0
- package/src/subagent-rate-limit-plugin.ts +218 -0
- package/src/system-message.test.ts +710 -0
- package/src/system-message.ts +814 -0
- package/src/task-runner.ts +725 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +317 -0
- package/src/test-utils.ts +451 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +1350 -0
- package/src/tools.ts +430 -0
- package/src/undici.d.ts +12 -0
- package/src/undo-redo.e2e.test.ts +209 -0
- package/src/unnest-code-blocks.test.ts +713 -0
- package/src/unnest-code-blocks.ts +185 -0
- package/src/upgrade.ts +185 -0
- package/src/utils.test.ts +155 -0
- package/src/utils.ts +265 -0
- package/src/voice-attachment.ts +51 -0
- package/src/voice-handler.ts +908 -0
- package/src/voice-message.e2e.test.ts +1255 -0
- package/src/voice.test.ts +281 -0
- package/src/voice.ts +638 -0
- package/src/wait-session.ts +273 -0
- package/src/websockify.ts +101 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-lifecycle.e2e.test.ts +396 -0
- package/src/worktree-utils.ts +4 -0
- package/src/worktrees.test.ts +489 -0
- package/src/worktrees.ts +1370 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
- package/README.md +0 -142
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config.d.ts +0 -39
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -202
- package/dist/config.test.js.map +0 -1
- package/dist/detect.d.ts +0 -9
- package/dist/detect.d.ts.map +0 -1
- package/dist/detect.js +0 -40
- package/dist/detect.js.map +0 -1
- package/dist/detect.test.d.ts +0 -2
- package/dist/detect.test.d.ts.map +0 -1
- package/dist/detect.test.js +0 -26
- package/dist/detect.test.js.map +0 -1
- package/dist/docker.d.ts +0 -7
- package/dist/docker.d.ts.map +0 -1
- package/dist/docker.js +0 -17
- package/dist/docker.js.map +0 -1
- package/dist/docker.test.d.ts +0 -2
- package/dist/docker.test.d.ts.map +0 -1
- package/dist/docker.test.js +0 -12
- package/dist/docker.test.js.map +0 -1
- package/dist/health.d.ts +0 -31
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -117
- package/dist/health.js.map +0 -1
- package/dist/health.test.d.ts +0 -2
- package/dist/health.test.d.ts.map +0 -1
- package/dist/health.test.js +0 -52
- package/dist/health.test.js.map +0 -1
- package/dist/index.d.ts +0 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -15
- package/dist/index.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -8
- package/dist/index.test.js.map +0 -1
- package/dist/installer.d.ts +0 -10
- package/dist/installer.d.ts.map +0 -1
- package/dist/installer.js +0 -50
- package/dist/installer.js.map +0 -1
- package/dist/installer.test.d.ts +0 -2
- package/dist/installer.test.d.ts.map +0 -1
- package/dist/installer.test.js +0 -43
- package/dist/installer.test.js.map +0 -1
- package/dist/lifecycle.d.ts +0 -10
- package/dist/lifecycle.d.ts.map +0 -1
- package/dist/lifecycle.js +0 -45
- package/dist/lifecycle.js.map +0 -1
- package/dist/lifecycle.test.d.ts +0 -2
- package/dist/lifecycle.test.d.ts.map +0 -1
- package/dist/lifecycle.test.js +0 -20
- package/dist/lifecycle.test.js.map +0 -1
- package/dist/manifest.d.ts +0 -18
- package/dist/manifest.d.ts.map +0 -1
- package/dist/manifest.js +0 -30
- package/dist/manifest.js.map +0 -1
- package/dist/skills-baseline.d.ts +0 -7
- package/dist/skills-baseline.d.ts.map +0 -1
- package/dist/skills-baseline.js +0 -9
- package/dist/skills-baseline.js.map +0 -1
- package/dist/skills.d.ts +0 -110
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js +0 -429
- package/dist/skills.js.map +0 -1
- package/dist/skills.test.d.ts +0 -2
- package/dist/skills.test.d.ts.map +0 -1
- package/dist/skills.test.js +0 -416
- package/dist/skills.test.js.map +0 -1
- package/dist/sync.d.ts +0 -10
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -39
- package/dist/sync.js.map +0 -1
- package/dist/tenant.d.ts +0 -13
- package/dist/tenant.d.ts.map +0 -1
- package/dist/tenant.js +0 -105
- package/dist/tenant.js.map +0 -1
- package/dist/tenant.test.d.ts +0 -2
- package/dist/tenant.test.d.ts.map +0 -1
- package/dist/tenant.test.js +0 -37
- package/dist/tenant.test.js.map +0 -1
- package/src/config.test.ts +0 -237
- package/src/detect.test.ts +0 -29
- package/src/detect.ts +0 -52
- package/src/docker.test.ts +0 -12
- package/src/docker.ts +0 -23
- package/src/health.test.ts +0 -61
- package/src/health.ts +0 -158
- package/src/index.test.ts +0 -8
- package/src/index.ts +0 -62
- package/src/installer.test.ts +0 -52
- package/src/installer.ts +0 -62
- package/src/lifecycle.test.ts +0 -23
- package/src/lifecycle.ts +0 -49
- package/src/manifest.ts +0 -42
- package/src/skills-baseline.ts +0 -14
- package/src/skills.test.ts +0 -503
- package/src/skills.ts +0 -512
- package/src/sync.ts +0 -53
- package/src/tenant.test.ts +0 -49
- package/src/tenant.ts +0 -120
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic OAuth authentication plugin for OpenCode.
|
|
3
|
+
*
|
|
4
|
+
* If you're copy-pasting this plugin into your OpenCode config folder,
|
|
5
|
+
* you need to install the runtime dependencies first:
|
|
6
|
+
*
|
|
7
|
+
* cd ~/.config/opencode
|
|
8
|
+
* bun init -y
|
|
9
|
+
* bun add proper-lockfile
|
|
10
|
+
*
|
|
11
|
+
* Handles three concerns:
|
|
12
|
+
* 1. OAuth login + token refresh (PKCE flow against claude.ai)
|
|
13
|
+
* 2. Request/response rewriting (tool names, system prompt, beta headers)
|
|
14
|
+
* so the Anthropic API treats requests as Claude Code CLI requests.
|
|
15
|
+
* 3. Multi-account OAuth rotation after Anthropic rate-limit/auth failures.
|
|
16
|
+
*
|
|
17
|
+
* Login mode is chosen from environment:
|
|
18
|
+
* - `OTTO` (or `OTTO`) set: remote-first pasted callback URL/raw code flow
|
|
19
|
+
* - otherwise: standard localhost auto-complete flow
|
|
20
|
+
*
|
|
21
|
+
* Source references:
|
|
22
|
+
* - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/anthropic.ts
|
|
23
|
+
* - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts
|
|
24
|
+
*/
|
|
25
|
+
import { appendToastSessionMarker } from "./plugin-logger.js";
|
|
26
|
+
import { loadAccountStore, rememberAnthropicOAuth, rotateAnthropicAccount, saveAccountStore, setAnthropicAuth, shouldRotateAuth, upsertAccount, withAuthStateLock, } from "./anthropic-auth-state.js";
|
|
27
|
+
import { extractAnthropicAccountIdentity, } from "./anthropic-account-identity.js";
|
|
28
|
+
// PKCE (Proof Key for Code Exchange) using Web Crypto API.
|
|
29
|
+
// Reference: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/pkce.ts
|
|
30
|
+
function base64urlEncode(bytes) {
|
|
31
|
+
let binary = "";
|
|
32
|
+
for (const byte of bytes) {
|
|
33
|
+
binary += String.fromCharCode(byte);
|
|
34
|
+
}
|
|
35
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
36
|
+
}
|
|
37
|
+
async function generatePKCE() {
|
|
38
|
+
const verifierBytes = new Uint8Array(32);
|
|
39
|
+
crypto.getRandomValues(verifierBytes);
|
|
40
|
+
const verifier = base64urlEncode(verifierBytes);
|
|
41
|
+
const data = new TextEncoder().encode(verifier);
|
|
42
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
43
|
+
const challenge = base64urlEncode(new Uint8Array(hashBuffer));
|
|
44
|
+
return { verifier, challenge };
|
|
45
|
+
}
|
|
46
|
+
import { spawn } from "node:child_process";
|
|
47
|
+
import { createServer } from "node:http";
|
|
48
|
+
// --- Constants ---
|
|
49
|
+
const CLIENT_ID = (() => {
|
|
50
|
+
const encoded = "OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl";
|
|
51
|
+
return typeof atob === "function"
|
|
52
|
+
? atob(encoded)
|
|
53
|
+
: Buffer.from(encoded, "base64").toString("utf8");
|
|
54
|
+
})();
|
|
55
|
+
const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
56
|
+
const CREATE_API_KEY_URL = "https://api.anthropic.com/api/oauth/claude_cli/create_api_key";
|
|
57
|
+
const CLIENT_DATA_URL = "https://api.anthropic.com/api/oauth/claude_cli/client_data";
|
|
58
|
+
const PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
59
|
+
const CALLBACK_PORT = 53692;
|
|
60
|
+
const CALLBACK_PATH = "/callback";
|
|
61
|
+
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
62
|
+
const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
63
|
+
const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
64
|
+
const CLAUDE_CODE_VERSION = "2.1.75";
|
|
65
|
+
const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
66
|
+
const OPENCODE_IDENTITY = "You are OpenCode, the best coding agent on the planet.";
|
|
67
|
+
const ANTHROPIC_PROMPT_MARKER = "Skills provide specialized instructions";
|
|
68
|
+
// Subagent prompts don't contain OPENCODE_IDENTITY; opencode appends this
|
|
69
|
+
// line + an <env> block instead. We strip from here to </env> inclusive.
|
|
70
|
+
const SUBAGENT_MODEL_IDENTITY = "You are powered by the model named";
|
|
71
|
+
const CLAUDE_CODE_BETA = "claude-code-20250219";
|
|
72
|
+
const OAUTH_BETA = "oauth-2025-04-20";
|
|
73
|
+
const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
|
|
74
|
+
const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
|
|
75
|
+
const TOAST_SESSION_HEADER = "x-otto-session-id";
|
|
76
|
+
const ANTHROPIC_HOSTS = new Set([
|
|
77
|
+
"api.anthropic.com",
|
|
78
|
+
"claude.ai",
|
|
79
|
+
"console.anthropic.com",
|
|
80
|
+
"platform.claude.com",
|
|
81
|
+
]);
|
|
82
|
+
const OPENCODE_TO_CLAUDE_CODE_TOOL_NAME = {
|
|
83
|
+
bash: "Bash",
|
|
84
|
+
edit: "Edit",
|
|
85
|
+
glob: "Glob",
|
|
86
|
+
grep: "Grep",
|
|
87
|
+
question: "AskUserQuestion",
|
|
88
|
+
read: "Read",
|
|
89
|
+
skill: "Skill",
|
|
90
|
+
task: "Task",
|
|
91
|
+
todowrite: "TodoWrite",
|
|
92
|
+
webfetch: "WebFetch",
|
|
93
|
+
websearch: "WebSearch",
|
|
94
|
+
write: "Write",
|
|
95
|
+
};
|
|
96
|
+
// --- HTTP helpers ---
|
|
97
|
+
// Claude OAuth token exchange can 429 when this runs inside the opencode auth
|
|
98
|
+
// process, even with the same payload that succeeds in a plain Node process.
|
|
99
|
+
// Run these OAuth-only HTTP calls in an isolated Node child to avoid whatever
|
|
100
|
+
// parent-process runtime state is affecting the in-process requests.
|
|
101
|
+
async function requestText(urlString, options) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const payload = JSON.stringify({
|
|
104
|
+
body: options.body,
|
|
105
|
+
headers: options.headers,
|
|
106
|
+
method: options.method,
|
|
107
|
+
url: urlString,
|
|
108
|
+
});
|
|
109
|
+
const child = spawn("node", [
|
|
110
|
+
"-e",
|
|
111
|
+
`
|
|
112
|
+
const input = JSON.parse(process.argv[1]);
|
|
113
|
+
(async () => {
|
|
114
|
+
const response = await fetch(input.url, {
|
|
115
|
+
method: input.method,
|
|
116
|
+
headers: input.headers,
|
|
117
|
+
body: input.body,
|
|
118
|
+
});
|
|
119
|
+
const text = await response.text();
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
console.error(JSON.stringify({ status: response.status, body: text }));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
process.stdout.write(text);
|
|
125
|
+
})().catch((error) => {
|
|
126
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|
|
129
|
+
`.trim(),
|
|
130
|
+
payload,
|
|
131
|
+
], {
|
|
132
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
133
|
+
});
|
|
134
|
+
let stdout = "";
|
|
135
|
+
let stderr = "";
|
|
136
|
+
const timeout = setTimeout(() => {
|
|
137
|
+
child.kill();
|
|
138
|
+
reject(new Error(`Request timed out. url=${urlString}`));
|
|
139
|
+
}, 30_000);
|
|
140
|
+
child.stdout.on("data", (chunk) => {
|
|
141
|
+
stdout += String(chunk);
|
|
142
|
+
});
|
|
143
|
+
child.stderr.on("data", (chunk) => {
|
|
144
|
+
stderr += String(chunk);
|
|
145
|
+
});
|
|
146
|
+
child.on("error", (error) => {
|
|
147
|
+
clearTimeout(timeout);
|
|
148
|
+
reject(error);
|
|
149
|
+
});
|
|
150
|
+
child.on("close", (code) => {
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
if (code !== 0) {
|
|
153
|
+
let details = stderr.trim();
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(details);
|
|
156
|
+
if (typeof parsed.status === "number") {
|
|
157
|
+
reject(new Error(`HTTP ${parsed.status} from ${urlString}: ${parsed.body ?? ""}`));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// fall back to raw stderr
|
|
163
|
+
}
|
|
164
|
+
reject(new Error(details || `Node helper exited with code ${code}`));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
resolve(stdout);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async function postJson(url, body) {
|
|
172
|
+
const requestBody = JSON.stringify(body);
|
|
173
|
+
const responseText = await requestText(url, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: {
|
|
176
|
+
Accept: "application/json",
|
|
177
|
+
"Content-Length": String(Buffer.byteLength(requestBody)),
|
|
178
|
+
"Content-Type": "application/json",
|
|
179
|
+
},
|
|
180
|
+
body: requestBody,
|
|
181
|
+
});
|
|
182
|
+
return JSON.parse(responseText);
|
|
183
|
+
}
|
|
184
|
+
const pendingRefresh = new Map();
|
|
185
|
+
// --- OAuth token exchange & refresh ---
|
|
186
|
+
function parseTokenResponse(json) {
|
|
187
|
+
const data = json;
|
|
188
|
+
if (!data.access_token || !data.refresh_token) {
|
|
189
|
+
throw new Error(`Invalid token response: ${JSON.stringify(json)}`);
|
|
190
|
+
}
|
|
191
|
+
return data;
|
|
192
|
+
}
|
|
193
|
+
function tokenExpiry(expiresIn) {
|
|
194
|
+
return Date.now() + expiresIn * 1000 - 5 * 60 * 1000;
|
|
195
|
+
}
|
|
196
|
+
async function exchangeAuthorizationCode(code, state, verifier, redirectUri) {
|
|
197
|
+
const json = await postJson(TOKEN_URL, {
|
|
198
|
+
grant_type: "authorization_code",
|
|
199
|
+
client_id: CLIENT_ID,
|
|
200
|
+
code,
|
|
201
|
+
state,
|
|
202
|
+
redirect_uri: redirectUri,
|
|
203
|
+
code_verifier: verifier,
|
|
204
|
+
});
|
|
205
|
+
const data = parseTokenResponse(json);
|
|
206
|
+
return {
|
|
207
|
+
type: "success",
|
|
208
|
+
refresh: data.refresh_token,
|
|
209
|
+
access: data.access_token,
|
|
210
|
+
expires: tokenExpiry(data.expires_in),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async function refreshAnthropicToken(refreshToken) {
|
|
214
|
+
const json = await postJson(TOKEN_URL, {
|
|
215
|
+
grant_type: "refresh_token",
|
|
216
|
+
client_id: CLIENT_ID,
|
|
217
|
+
refresh_token: refreshToken,
|
|
218
|
+
});
|
|
219
|
+
const data = parseTokenResponse(json);
|
|
220
|
+
return {
|
|
221
|
+
type: "oauth",
|
|
222
|
+
refresh: data.refresh_token,
|
|
223
|
+
access: data.access_token,
|
|
224
|
+
expires: tokenExpiry(data.expires_in),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function createApiKey(accessToken) {
|
|
228
|
+
const responseText = await requestText(CREATE_API_KEY_URL, {
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers: {
|
|
231
|
+
Accept: "application/json",
|
|
232
|
+
authorization: `Bearer ${accessToken}`,
|
|
233
|
+
"Content-Type": "application/json",
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
const json = JSON.parse(responseText);
|
|
237
|
+
return { type: "success", key: json.raw_key };
|
|
238
|
+
}
|
|
239
|
+
async function fetchAnthropicAccountIdentity(accessToken) {
|
|
240
|
+
const urls = [CLIENT_DATA_URL, PROFILE_URL];
|
|
241
|
+
for (const url of urls) {
|
|
242
|
+
const responseText = await requestText(url, {
|
|
243
|
+
method: "GET",
|
|
244
|
+
headers: {
|
|
245
|
+
Accept: "application/json",
|
|
246
|
+
authorization: `Bearer ${accessToken}`,
|
|
247
|
+
"user-agent": process.env.OPENCODE_ANTHROPIC_USER_AGENT ||
|
|
248
|
+
`claude-cli/${CLAUDE_CODE_VERSION}`,
|
|
249
|
+
"x-app": "cli",
|
|
250
|
+
},
|
|
251
|
+
}).catch(() => {
|
|
252
|
+
return undefined;
|
|
253
|
+
});
|
|
254
|
+
if (!responseText)
|
|
255
|
+
continue;
|
|
256
|
+
const parsed = JSON.parse(responseText);
|
|
257
|
+
const identity = extractAnthropicAccountIdentity(parsed);
|
|
258
|
+
if (identity)
|
|
259
|
+
return identity;
|
|
260
|
+
}
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
async function startCallbackServer(expectedState) {
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
let settle;
|
|
266
|
+
let settled = false;
|
|
267
|
+
const waitPromise = new Promise((res) => {
|
|
268
|
+
settle = (v) => {
|
|
269
|
+
if (settled)
|
|
270
|
+
return;
|
|
271
|
+
settled = true;
|
|
272
|
+
res(v);
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
const server = createServer((req, res) => {
|
|
276
|
+
try {
|
|
277
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
278
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
279
|
+
res.writeHead(404).end("Not found");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const code = url.searchParams.get("code");
|
|
283
|
+
const state = url.searchParams.get("state");
|
|
284
|
+
const error = url.searchParams.get("error");
|
|
285
|
+
if (error || !code || !state || state !== expectedState) {
|
|
286
|
+
res
|
|
287
|
+
.writeHead(400)
|
|
288
|
+
.end("Authentication failed: " + (error || "missing code/state"));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
res
|
|
292
|
+
.writeHead(200, { "Content-Type": "text/plain" })
|
|
293
|
+
.end("Authentication successful. You can close this window.");
|
|
294
|
+
settle?.({ code, state });
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
res.writeHead(500).end("Internal error");
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
server.once("error", reject);
|
|
301
|
+
server.listen(CALLBACK_PORT, "127.0.0.1", () => {
|
|
302
|
+
resolve({
|
|
303
|
+
server,
|
|
304
|
+
cancelWait: () => {
|
|
305
|
+
settle?.(null);
|
|
306
|
+
},
|
|
307
|
+
waitForCode: () => waitPromise,
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function closeServer(server) {
|
|
313
|
+
return new Promise((resolve) => {
|
|
314
|
+
server.close(() => {
|
|
315
|
+
resolve();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
// --- Authorization flow ---
|
|
320
|
+
// Unified flow: beginAuthorizationFlow starts PKCE + callback server,
|
|
321
|
+
// then waitForCallback handles both auto (localhost) and manual (pasted code) paths.
|
|
322
|
+
async function beginAuthorizationFlow() {
|
|
323
|
+
const pkce = await generatePKCE();
|
|
324
|
+
const callbackServer = await startCallbackServer(pkce.verifier);
|
|
325
|
+
const authParams = new URLSearchParams({
|
|
326
|
+
code: "true",
|
|
327
|
+
client_id: CLIENT_ID,
|
|
328
|
+
response_type: "code",
|
|
329
|
+
redirect_uri: REDIRECT_URI,
|
|
330
|
+
scope: SCOPES,
|
|
331
|
+
code_challenge: pkce.challenge,
|
|
332
|
+
code_challenge_method: "S256",
|
|
333
|
+
state: pkce.verifier,
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
url: `https://claude.ai/oauth/authorize?${authParams.toString()}`,
|
|
337
|
+
verifier: pkce.verifier,
|
|
338
|
+
callbackServer,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
async function waitForCallback(callbackServer, manualInput) {
|
|
342
|
+
try {
|
|
343
|
+
// Try localhost callback first (instant check)
|
|
344
|
+
const quick = await Promise.race([
|
|
345
|
+
callbackServer.waitForCode(),
|
|
346
|
+
new Promise((r) => {
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
r(null);
|
|
349
|
+
}, 50);
|
|
350
|
+
}),
|
|
351
|
+
]);
|
|
352
|
+
if (quick?.code)
|
|
353
|
+
return quick;
|
|
354
|
+
// If manual input was provided, parse it
|
|
355
|
+
const trimmed = manualInput?.trim();
|
|
356
|
+
if (trimmed) {
|
|
357
|
+
return parseManualInput(trimmed);
|
|
358
|
+
}
|
|
359
|
+
// Wait for localhost callback with timeout
|
|
360
|
+
const result = await Promise.race([
|
|
361
|
+
callbackServer.waitForCode(),
|
|
362
|
+
new Promise((r) => {
|
|
363
|
+
setTimeout(() => {
|
|
364
|
+
r(null);
|
|
365
|
+
}, OAUTH_TIMEOUT_MS);
|
|
366
|
+
}),
|
|
367
|
+
]);
|
|
368
|
+
if (!result?.code) {
|
|
369
|
+
throw new Error("Timed out waiting for OAuth callback");
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
callbackServer.cancelWait();
|
|
375
|
+
await closeServer(callbackServer.server);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function parseManualInput(input) {
|
|
379
|
+
try {
|
|
380
|
+
const url = new URL(input);
|
|
381
|
+
const code = url.searchParams.get("code");
|
|
382
|
+
const state = url.searchParams.get("state");
|
|
383
|
+
if (code)
|
|
384
|
+
return { code, state: state || "" };
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// not a URL
|
|
388
|
+
}
|
|
389
|
+
if (input.includes("#")) {
|
|
390
|
+
const [code = "", state = ""] = input.split("#", 2);
|
|
391
|
+
return { code, state };
|
|
392
|
+
}
|
|
393
|
+
if (input.includes("code=")) {
|
|
394
|
+
const params = new URLSearchParams(input);
|
|
395
|
+
const code = params.get("code");
|
|
396
|
+
if (code)
|
|
397
|
+
return { code, state: params.get("state") || "" };
|
|
398
|
+
}
|
|
399
|
+
return { code: input, state: "" };
|
|
400
|
+
}
|
|
401
|
+
// Unified authorize handler: returns either OAuth tokens or an API key,
|
|
402
|
+
// for both auto and remote-first modes.
|
|
403
|
+
function buildAuthorizeHandler(mode) {
|
|
404
|
+
return async () => {
|
|
405
|
+
const auth = await beginAuthorizationFlow();
|
|
406
|
+
const isRemote = Boolean(process.env.OTTO || process.env.OTTO);
|
|
407
|
+
let pendingAuthResult;
|
|
408
|
+
const finalize = async (result) => {
|
|
409
|
+
const verifier = auth.verifier;
|
|
410
|
+
const creds = await exchangeAuthorizationCode(result.code, result.state || verifier, verifier, REDIRECT_URI);
|
|
411
|
+
if (mode === "apikey") {
|
|
412
|
+
return createApiKey(creds.access);
|
|
413
|
+
}
|
|
414
|
+
const identity = await fetchAnthropicAccountIdentity(creds.access);
|
|
415
|
+
await rememberAnthropicOAuth({
|
|
416
|
+
type: "oauth",
|
|
417
|
+
refresh: creds.refresh,
|
|
418
|
+
access: creds.access,
|
|
419
|
+
expires: creds.expires,
|
|
420
|
+
}, identity);
|
|
421
|
+
return creds;
|
|
422
|
+
};
|
|
423
|
+
if (!isRemote) {
|
|
424
|
+
return {
|
|
425
|
+
url: auth.url,
|
|
426
|
+
instructions: "Complete login in your browser on this machine. OpenCode will catch the localhost callback automatically.",
|
|
427
|
+
method: "auto",
|
|
428
|
+
callback: async () => {
|
|
429
|
+
pendingAuthResult ??= (async () => {
|
|
430
|
+
try {
|
|
431
|
+
const result = await waitForCallback(auth.callbackServer);
|
|
432
|
+
return await finalize(result);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return { type: "failed" };
|
|
436
|
+
}
|
|
437
|
+
})();
|
|
438
|
+
return pendingAuthResult;
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
url: auth.url,
|
|
444
|
+
instructions: "Complete login in your browser, then paste the final redirect URL from the address bar here. Pasting just the authorization code also works.",
|
|
445
|
+
method: "code",
|
|
446
|
+
callback: async (input) => {
|
|
447
|
+
pendingAuthResult ??= (async () => {
|
|
448
|
+
try {
|
|
449
|
+
const result = await waitForCallback(auth.callbackServer, input);
|
|
450
|
+
return await finalize(result);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return { type: "failed" };
|
|
454
|
+
}
|
|
455
|
+
})();
|
|
456
|
+
return pendingAuthResult;
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
// --- Request/response rewriting ---
|
|
462
|
+
// Renames opencode tool names to Claude Code tool names in requests,
|
|
463
|
+
// and reverses the mapping in streamed responses.
|
|
464
|
+
function toClaudeCodeToolName(name) {
|
|
465
|
+
return OPENCODE_TO_CLAUDE_CODE_TOOL_NAME[name.toLowerCase()] ?? name;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Strips the OpenCode identity block (from "You are OpenCode…" up to the
|
|
469
|
+
* Anthropic prompt marker "Skills provide specialized instructions") and
|
|
470
|
+
* re-injects essential environment context as a small XML tag.
|
|
471
|
+
*
|
|
472
|
+
* The original OpenCode prompt between those markers contains the current
|
|
473
|
+
* working directory and other runtime context. Stripping it wholesale loses
|
|
474
|
+
* that info, so we add back what the model needs (cwd) in a compact form.
|
|
475
|
+
*
|
|
476
|
+
* Original OpenCode Anthropic prompt structure (for reference):
|
|
477
|
+
* "You are OpenCode, the best coding agent on the planet."
|
|
478
|
+
* + environment block (cwd, OS, shell, date, etc.)
|
|
479
|
+
* + "Skills provide specialized instructions …"
|
|
480
|
+
*/
|
|
481
|
+
function sanitizeAnthropicSystemText(text, onError) {
|
|
482
|
+
const startIdx = text.indexOf(OPENCODE_IDENTITY);
|
|
483
|
+
if (startIdx !== -1) {
|
|
484
|
+
// Main session path: strip from OpenCode identity to the Anthropic prompt marker.
|
|
485
|
+
// Keep the marker aligned with the current OpenCode Anthropic prompt.
|
|
486
|
+
const endIdx = text.indexOf(ANTHROPIC_PROMPT_MARKER, startIdx);
|
|
487
|
+
if (endIdx === -1) {
|
|
488
|
+
onError?.("sanitizeAnthropicSystemText: could not find Anthropic prompt marker after OpenCode identity");
|
|
489
|
+
return text;
|
|
490
|
+
}
|
|
491
|
+
return replaceBlockWithCompactEnv(text, startIdx, endIdx);
|
|
492
|
+
}
|
|
493
|
+
// Subagent path: opencode appends "You are powered by the model named ..."
|
|
494
|
+
// followed by an <env> block. Strip from that line through </env>.
|
|
495
|
+
const subagentIdx = text.indexOf(SUBAGENT_MODEL_IDENTITY);
|
|
496
|
+
if (subagentIdx !== -1) {
|
|
497
|
+
const envCloseTag = "</env>";
|
|
498
|
+
const envCloseIdx = text.indexOf(envCloseTag, subagentIdx);
|
|
499
|
+
if (envCloseIdx === -1) {
|
|
500
|
+
onError?.("sanitizeAnthropicSystemText: could not find </env> after subagent model identity");
|
|
501
|
+
return text;
|
|
502
|
+
}
|
|
503
|
+
const endIdx = envCloseIdx + envCloseTag.length;
|
|
504
|
+
// Skip trailing newline so the join is clean
|
|
505
|
+
const afterEnd = text[endIdx] === "\n" ? endIdx + 1 : endIdx;
|
|
506
|
+
return replaceBlockWithCompactEnv(text, subagentIdx, afterEnd);
|
|
507
|
+
}
|
|
508
|
+
return text;
|
|
509
|
+
}
|
|
510
|
+
// Extract cwd from the block being stripped and replace it with a compact
|
|
511
|
+
// <environment> tag. Shared by both main-session and subagent paths.
|
|
512
|
+
// Source: anomalyco/opencode packages/opencode/src/session/system.ts
|
|
513
|
+
// OpenCode's system prompt format (as of 2025):
|
|
514
|
+
// <env>
|
|
515
|
+
// Working directory: ${Instance.directory}
|
|
516
|
+
// Workspace root folder: ${Instance.worktree}
|
|
517
|
+
// Is directory a git repo: yes/no
|
|
518
|
+
// Platform: ${process.platform}
|
|
519
|
+
// Today's date: ${new Date().toDateString()}
|
|
520
|
+
// </env>
|
|
521
|
+
// Older format used <environment><cwd>/path</cwd></environment>.
|
|
522
|
+
// We try both patterns to stay compatible across opencode versions.
|
|
523
|
+
// We preserve the per-session directory instead of falling back to
|
|
524
|
+
// process.cwd() which is the opencode server's cwd and wrong for
|
|
525
|
+
// multi-session/worktree setups where each session has a different directory.
|
|
526
|
+
function replaceBlockWithCompactEnv(text, startIdx, endIdx) {
|
|
527
|
+
const strippedBlock = text.slice(startIdx, endIdx);
|
|
528
|
+
const cwdMatch = strippedBlock.match(/Working directory:\s*(.+)/)?.[1]?.trim() ||
|
|
529
|
+
strippedBlock.match(/<cwd>([^<]+)<\/cwd>/)?.[1];
|
|
530
|
+
const cwd = cwdMatch || process.cwd();
|
|
531
|
+
const envContext = `\n<environment>\n<cwd>${cwd}</cwd>\n</environment>\n` +
|
|
532
|
+
`Read, write, and edit files under ${cwd}.\n\n`;
|
|
533
|
+
return (text.slice(0, startIdx) +
|
|
534
|
+
envContext +
|
|
535
|
+
text.slice(endIdx));
|
|
536
|
+
}
|
|
537
|
+
function mapSystemTextPart(part, onError) {
|
|
538
|
+
if (typeof part === "string") {
|
|
539
|
+
return { type: "text", text: sanitizeAnthropicSystemText(part, onError) };
|
|
540
|
+
}
|
|
541
|
+
if (part &&
|
|
542
|
+
typeof part === "object" &&
|
|
543
|
+
"type" in part &&
|
|
544
|
+
part.type === "text" &&
|
|
545
|
+
"text" in part &&
|
|
546
|
+
typeof part.text === "string") {
|
|
547
|
+
return {
|
|
548
|
+
...part,
|
|
549
|
+
text: sanitizeAnthropicSystemText(part.text, onError),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return part;
|
|
553
|
+
}
|
|
554
|
+
function prependClaudeCodeIdentity(system, onError) {
|
|
555
|
+
const identityBlock = {
|
|
556
|
+
type: "text",
|
|
557
|
+
text: CLAUDE_CODE_IDENTITY,
|
|
558
|
+
};
|
|
559
|
+
if (typeof system === "undefined")
|
|
560
|
+
return [identityBlock];
|
|
561
|
+
if (typeof system === "string") {
|
|
562
|
+
const sanitized = sanitizeAnthropicSystemText(system, onError);
|
|
563
|
+
if (sanitized === CLAUDE_CODE_IDENTITY)
|
|
564
|
+
return [identityBlock];
|
|
565
|
+
return [identityBlock, { type: "text", text: sanitized }];
|
|
566
|
+
}
|
|
567
|
+
if (!Array.isArray(system))
|
|
568
|
+
return [identityBlock, system];
|
|
569
|
+
const sanitized = system.map((item) => {
|
|
570
|
+
return mapSystemTextPart(item, onError);
|
|
571
|
+
});
|
|
572
|
+
const first = sanitized[0];
|
|
573
|
+
if (first &&
|
|
574
|
+
typeof first === "object" &&
|
|
575
|
+
"type" in first &&
|
|
576
|
+
first.type === "text" &&
|
|
577
|
+
"text" in first &&
|
|
578
|
+
first.text === CLAUDE_CODE_IDENTITY) {
|
|
579
|
+
return sanitized;
|
|
580
|
+
}
|
|
581
|
+
return [identityBlock, ...sanitized];
|
|
582
|
+
}
|
|
583
|
+
function rewriteRequestPayload(body, onError) {
|
|
584
|
+
if (!body)
|
|
585
|
+
return {
|
|
586
|
+
body,
|
|
587
|
+
modelId: undefined,
|
|
588
|
+
reverseToolNameMap: new Map(),
|
|
589
|
+
};
|
|
590
|
+
try {
|
|
591
|
+
const payload = JSON.parse(body);
|
|
592
|
+
const reverseToolNameMap = new Map();
|
|
593
|
+
const modelId = typeof payload.model === "string" ? payload.model : undefined;
|
|
594
|
+
// Build reverse map and rename tools
|
|
595
|
+
if (Array.isArray(payload.tools)) {
|
|
596
|
+
payload.tools = payload.tools.map((tool) => {
|
|
597
|
+
if (!tool || typeof tool !== "object")
|
|
598
|
+
return tool;
|
|
599
|
+
const name = tool.name;
|
|
600
|
+
if (typeof name !== "string")
|
|
601
|
+
return tool;
|
|
602
|
+
const mapped = toClaudeCodeToolName(name);
|
|
603
|
+
reverseToolNameMap.set(mapped, name);
|
|
604
|
+
return { ...tool, name: mapped };
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
// Rename system prompt
|
|
608
|
+
payload.system = prependClaudeCodeIdentity(payload.system, onError);
|
|
609
|
+
// Rename tool_choice
|
|
610
|
+
if (payload.tool_choice &&
|
|
611
|
+
typeof payload.tool_choice === "object" &&
|
|
612
|
+
payload.tool_choice.type === "tool") {
|
|
613
|
+
const name = payload.tool_choice.name;
|
|
614
|
+
if (typeof name === "string") {
|
|
615
|
+
payload.tool_choice = {
|
|
616
|
+
...payload.tool_choice,
|
|
617
|
+
name: toClaudeCodeToolName(name),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// Rename tool_use blocks in messages
|
|
622
|
+
if (Array.isArray(payload.messages)) {
|
|
623
|
+
payload.messages = payload.messages.map((message) => {
|
|
624
|
+
if (!message || typeof message !== "object")
|
|
625
|
+
return message;
|
|
626
|
+
const content = message.content;
|
|
627
|
+
if (!Array.isArray(content))
|
|
628
|
+
return message;
|
|
629
|
+
return {
|
|
630
|
+
...message,
|
|
631
|
+
content: content.map((block) => {
|
|
632
|
+
if (!block || typeof block !== "object")
|
|
633
|
+
return block;
|
|
634
|
+
const b = block;
|
|
635
|
+
if (b.type !== "tool_use" || typeof b.name !== "string")
|
|
636
|
+
return block;
|
|
637
|
+
return {
|
|
638
|
+
...block,
|
|
639
|
+
name: toClaudeCodeToolName(b.name),
|
|
640
|
+
};
|
|
641
|
+
}),
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
return { body: JSON.stringify(payload), modelId, reverseToolNameMap };
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
return {
|
|
649
|
+
body,
|
|
650
|
+
modelId: undefined,
|
|
651
|
+
reverseToolNameMap: new Map(),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function wrapResponseStream(response, reverseToolNameMap) {
|
|
656
|
+
if (!response.body || reverseToolNameMap.size === 0)
|
|
657
|
+
return response;
|
|
658
|
+
const reader = response.body.getReader();
|
|
659
|
+
const decoder = new TextDecoder();
|
|
660
|
+
const encoder = new TextEncoder();
|
|
661
|
+
let carry = "";
|
|
662
|
+
const transform = (text) => {
|
|
663
|
+
return text.replace(/"name"\s*:\s*"([^"]+)"/g, (full, name) => {
|
|
664
|
+
const original = reverseToolNameMap.get(name);
|
|
665
|
+
return original ? full.replace(`"${name}"`, `"${original}"`) : full;
|
|
666
|
+
});
|
|
667
|
+
};
|
|
668
|
+
const stream = new ReadableStream({
|
|
669
|
+
async pull(controller) {
|
|
670
|
+
const { done, value } = await reader.read();
|
|
671
|
+
if (done) {
|
|
672
|
+
const finalText = carry + decoder.decode();
|
|
673
|
+
if (finalText)
|
|
674
|
+
controller.enqueue(encoder.encode(transform(finalText)));
|
|
675
|
+
controller.close();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
carry += decoder.decode(value, { stream: true });
|
|
679
|
+
// Buffer 256 chars to avoid splitting JSON keys across chunks
|
|
680
|
+
if (carry.length <= 256)
|
|
681
|
+
return;
|
|
682
|
+
const output = carry.slice(0, -256);
|
|
683
|
+
carry = carry.slice(-256);
|
|
684
|
+
controller.enqueue(encoder.encode(transform(output)));
|
|
685
|
+
},
|
|
686
|
+
async cancel(reason) {
|
|
687
|
+
await reader.cancel(reason);
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
return new Response(stream, {
|
|
691
|
+
status: response.status,
|
|
692
|
+
statusText: response.statusText,
|
|
693
|
+
headers: response.headers,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
// --- Beta headers ---
|
|
697
|
+
function getRequiredBetas(modelId) {
|
|
698
|
+
const betas = [
|
|
699
|
+
CLAUDE_CODE_BETA,
|
|
700
|
+
OAUTH_BETA,
|
|
701
|
+
FINE_GRAINED_TOOL_STREAMING_BETA,
|
|
702
|
+
];
|
|
703
|
+
const isAdaptive = modelId?.includes("opus-4-6") ||
|
|
704
|
+
modelId?.includes("opus-4.6") ||
|
|
705
|
+
modelId?.includes("sonnet-4-6") ||
|
|
706
|
+
modelId?.includes("sonnet-4.6");
|
|
707
|
+
if (!isAdaptive)
|
|
708
|
+
betas.push(INTERLEAVED_THINKING_BETA);
|
|
709
|
+
return betas;
|
|
710
|
+
}
|
|
711
|
+
function mergeBetas(existing, required) {
|
|
712
|
+
return [
|
|
713
|
+
...new Set([
|
|
714
|
+
...required,
|
|
715
|
+
...(existing || "")
|
|
716
|
+
.split(",")
|
|
717
|
+
.map((s) => s.trim())
|
|
718
|
+
.filter(Boolean),
|
|
719
|
+
]),
|
|
720
|
+
].join(",");
|
|
721
|
+
}
|
|
722
|
+
// --- Token refresh with dedup ---
|
|
723
|
+
function isOAuthStored(auth) {
|
|
724
|
+
return auth.type === "oauth";
|
|
725
|
+
}
|
|
726
|
+
async function getFreshOAuth(getAuth, client) {
|
|
727
|
+
const auth = await getAuth();
|
|
728
|
+
if (!isOAuthStored(auth))
|
|
729
|
+
return undefined;
|
|
730
|
+
if (auth.access && auth.expires > Date.now())
|
|
731
|
+
return auth;
|
|
732
|
+
const pending = pendingRefresh.get(auth.refresh);
|
|
733
|
+
if (pending) {
|
|
734
|
+
return pending;
|
|
735
|
+
}
|
|
736
|
+
const refreshPromise = withAuthStateLock(async () => {
|
|
737
|
+
const latest = await getAuth();
|
|
738
|
+
if (!isOAuthStored(latest)) {
|
|
739
|
+
throw new Error("Anthropic OAuth credentials disappeared during refresh");
|
|
740
|
+
}
|
|
741
|
+
if (latest.access && latest.expires > Date.now())
|
|
742
|
+
return latest;
|
|
743
|
+
const refreshed = await refreshAnthropicToken(latest.refresh);
|
|
744
|
+
await setAnthropicAuth(refreshed, client);
|
|
745
|
+
const store = await loadAccountStore();
|
|
746
|
+
if (store.accounts.length > 0) {
|
|
747
|
+
const identity = (() => {
|
|
748
|
+
const currentIndex = store.accounts.findIndex((account) => {
|
|
749
|
+
return (account.refresh === latest.refresh ||
|
|
750
|
+
account.access === latest.access);
|
|
751
|
+
});
|
|
752
|
+
const current = currentIndex >= 0 ? store.accounts[currentIndex] : undefined;
|
|
753
|
+
if (!current)
|
|
754
|
+
return undefined;
|
|
755
|
+
return {
|
|
756
|
+
...(current.email ? { email: current.email } : {}),
|
|
757
|
+
...(current.accountId ? { accountId: current.accountId } : {}),
|
|
758
|
+
};
|
|
759
|
+
})();
|
|
760
|
+
upsertAccount(store, { ...refreshed, ...identity });
|
|
761
|
+
await saveAccountStore(store);
|
|
762
|
+
}
|
|
763
|
+
return refreshed;
|
|
764
|
+
});
|
|
765
|
+
pendingRefresh.set(auth.refresh, refreshPromise);
|
|
766
|
+
return refreshPromise.finally(() => {
|
|
767
|
+
pendingRefresh.delete(auth.refresh);
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
const AnthropicAuthPlugin = async ({ client }) => {
|
|
771
|
+
return {
|
|
772
|
+
"chat.headers": async (input, output) => {
|
|
773
|
+
if (input.model.providerID !== "anthropic") {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
output.headers[TOAST_SESSION_HEADER] = input.sessionID;
|
|
777
|
+
},
|
|
778
|
+
auth: {
|
|
779
|
+
provider: "anthropic",
|
|
780
|
+
async loader(getAuth, provider) {
|
|
781
|
+
const auth = await getAuth();
|
|
782
|
+
if (auth.type !== "oauth")
|
|
783
|
+
return {};
|
|
784
|
+
// Zero out costs for OAuth users (Claude Pro/Max subscription)
|
|
785
|
+
for (const model of Object.values(provider.models)) {
|
|
786
|
+
model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } };
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
apiKey: "",
|
|
790
|
+
async fetch(input, init) {
|
|
791
|
+
const url = (() => {
|
|
792
|
+
try {
|
|
793
|
+
return new URL(input instanceof Request ? input.url : input.toString());
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
})();
|
|
799
|
+
if (!url || !ANTHROPIC_HOSTS.has(url.hostname))
|
|
800
|
+
return fetch(input, init);
|
|
801
|
+
const originalBody = typeof init?.body === "string"
|
|
802
|
+
? init.body
|
|
803
|
+
: input instanceof Request
|
|
804
|
+
? await input
|
|
805
|
+
.clone()
|
|
806
|
+
.text()
|
|
807
|
+
.catch(() => undefined)
|
|
808
|
+
: undefined;
|
|
809
|
+
const headers = new Headers(init?.headers);
|
|
810
|
+
if (input instanceof Request) {
|
|
811
|
+
input.headers.forEach((v, k) => {
|
|
812
|
+
if (!headers.has(k))
|
|
813
|
+
headers.set(k, v);
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
const sessionId = headers.get(TOAST_SESSION_HEADER) ?? undefined;
|
|
817
|
+
const rewritten = rewriteRequestPayload(originalBody, (msg) => {
|
|
818
|
+
client.tui
|
|
819
|
+
.showToast({
|
|
820
|
+
body: {
|
|
821
|
+
message: appendToastSessionMarker({
|
|
822
|
+
message: msg,
|
|
823
|
+
sessionId,
|
|
824
|
+
}),
|
|
825
|
+
variant: "error",
|
|
826
|
+
},
|
|
827
|
+
})
|
|
828
|
+
.catch(() => { });
|
|
829
|
+
});
|
|
830
|
+
const betas = getRequiredBetas(rewritten.modelId);
|
|
831
|
+
const runRequest = async (auth) => {
|
|
832
|
+
const requestHeaders = new Headers(headers);
|
|
833
|
+
requestHeaders.delete(TOAST_SESSION_HEADER);
|
|
834
|
+
requestHeaders.set("accept", "application/json");
|
|
835
|
+
requestHeaders.set("anthropic-beta", mergeBetas(requestHeaders.get("anthropic-beta"), betas));
|
|
836
|
+
requestHeaders.set("anthropic-dangerous-direct-browser-access", "true");
|
|
837
|
+
requestHeaders.set("authorization", `Bearer ${auth.access}`);
|
|
838
|
+
requestHeaders.set("user-agent", process.env.OPENCODE_ANTHROPIC_USER_AGENT ||
|
|
839
|
+
`claude-cli/${CLAUDE_CODE_VERSION}`);
|
|
840
|
+
requestHeaders.set("x-app", "cli");
|
|
841
|
+
requestHeaders.delete("x-api-key");
|
|
842
|
+
return fetch(input, {
|
|
843
|
+
...(init ?? {}),
|
|
844
|
+
body: rewritten.body,
|
|
845
|
+
headers: requestHeaders,
|
|
846
|
+
});
|
|
847
|
+
};
|
|
848
|
+
const freshAuth = await getFreshOAuth(getAuth, client);
|
|
849
|
+
if (!freshAuth)
|
|
850
|
+
return fetch(input, init);
|
|
851
|
+
let response = await runRequest(freshAuth);
|
|
852
|
+
if (!response.ok) {
|
|
853
|
+
const bodyText = await response
|
|
854
|
+
.clone()
|
|
855
|
+
.text()
|
|
856
|
+
.catch(() => "");
|
|
857
|
+
if (shouldRotateAuth(response.status, bodyText)) {
|
|
858
|
+
const rotated = await rotateAnthropicAccount(freshAuth, client);
|
|
859
|
+
if (rotated) {
|
|
860
|
+
// Show toast notification so Discord thread shows the rotation
|
|
861
|
+
client.tui
|
|
862
|
+
.showToast({
|
|
863
|
+
body: {
|
|
864
|
+
message: appendToastSessionMarker({
|
|
865
|
+
message: `Switching from account ${rotated.fromLabel} to account ${rotated.toLabel}`,
|
|
866
|
+
sessionId,
|
|
867
|
+
}),
|
|
868
|
+
variant: "info",
|
|
869
|
+
},
|
|
870
|
+
})
|
|
871
|
+
.catch(() => { });
|
|
872
|
+
const retryAuth = await getFreshOAuth(getAuth, client);
|
|
873
|
+
if (retryAuth) {
|
|
874
|
+
response = await runRequest(retryAuth);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return wrapResponseStream(response, rewritten.reverseToolNameMap);
|
|
880
|
+
},
|
|
881
|
+
};
|
|
882
|
+
},
|
|
883
|
+
methods: [
|
|
884
|
+
{
|
|
885
|
+
label: "Claude Pro/Max",
|
|
886
|
+
type: "oauth",
|
|
887
|
+
authorize: buildAuthorizeHandler("oauth"),
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
label: "Create an API Key",
|
|
891
|
+
type: "oauth",
|
|
892
|
+
authorize: buildAuthorizeHandler("apikey"),
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
provider: "anthropic",
|
|
896
|
+
label: "Manually enter API Key",
|
|
897
|
+
type: "api",
|
|
898
|
+
},
|
|
899
|
+
],
|
|
900
|
+
},
|
|
901
|
+
};
|
|
902
|
+
};
|
|
903
|
+
const replacer = async () => {
|
|
904
|
+
return {
|
|
905
|
+
"experimental.chat.system.transform": (async (input, output) => {
|
|
906
|
+
if (input.model.providerID !== "anthropic")
|
|
907
|
+
return;
|
|
908
|
+
const textIndex = output.system.findIndex((x) => x.includes(OPENCODE_IDENTITY));
|
|
909
|
+
const text = output.system[textIndex];
|
|
910
|
+
if (!text) {
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
output.system[textIndex] = sanitizeAnthropicSystemText(text);
|
|
914
|
+
}),
|
|
915
|
+
};
|
|
916
|
+
};
|
|
917
|
+
export { replacer, AnthropicAuthPlugin as anthropicAuthPlugin };
|