mixdog 0.9.3 → 0.9.5
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/README.md +112 -38
- package/package.json +10 -3
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/r5-orchestrated-task.json +7 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r13-clientmeta.json +464 -0
- package/scripts/bench/round-r14-betafeatures.json +466 -0
- package/scripts/bench/round-r15-fulldefault.json +462 -0
- package/scripts/bench/round-r16-sessionid.json +466 -0
- package/scripts/bench/round-r17-wirebytes.json +456 -0
- package/scripts/bench/round-r18-prewarm.json +468 -0
- package/scripts/bench/round-r19-clean.json +472 -0
- package/scripts/bench/round-r20-prewarm-clean.json +475 -0
- package/scripts/bench/round-r21-delta-retry.json +473 -0
- package/scripts/bench/round-r22-full-probe.json +693 -0
- package/scripts/bench/round-r23-itemprobe.json +701 -0
- package/scripts/bench/round-r24-shapefix.json +677 -0
- package/scripts/bench/round-r25-serial.json +464 -0
- package/scripts/bench/round-r26-parallel3.json +671 -0
- package/scripts/bench/round-r27-parallel10.json +894 -0
- package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
- package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
- package/scripts/bench/round-r30-instid.json +253 -0
- package/scripts/bench/round-r31-upgradeprobe.json +256 -0
- package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
- package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
- package/scripts/bench/round-r34-orchestrated.json +120 -0
- package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
- package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +251 -32
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +3 -4
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/model-catalog-audit.mjs +209 -0
- package/scripts/model-list-sanitize-test.mjs +37 -0
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-bench.mjs +76 -13
- package/scripts/recall-quality-cases.json +12 -0
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +25 -65
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +4 -1
- package/src/agents/heavy-worker/AGENT.md +9 -8
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/scheduler-task/AGENT.md +2 -3
- package/src/agents/webhook-handler/AGENT.md +2 -3
- package/src/agents/worker/AGENT.md +10 -7
- package/src/app.mjs +12 -1
- package/src/headless-role.mjs +7 -1
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +647 -2056
- package/src/output-styles/default.md +30 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +8 -6
- package/src/output-styles/simple.md +21 -7
- package/src/rules/agent/00-common.md +6 -3
- package/src/rules/agent/30-explorer.md +16 -5
- package/src/rules/lead/01-general.md +5 -5
- package/src/rules/lead/lead-brief.md +15 -0
- package/src/rules/lead/lead-tool.md +6 -15
- package/src/rules/shared/01-tool.md +17 -21
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +27 -318
- package/src/runtime/channels/backends/telegram.mjs +8 -12
- package/src/runtime/channels/index.mjs +247 -701
- package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
- package/src/runtime/channels/lib/config.mjs +37 -149
- package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
- package/src/runtime/channels/lib/event-queue.mjs +78 -13
- package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
- package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/scheduler.mjs +27 -113
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +43 -616
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +210 -1948
- package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/ko-morph.mjs +195 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
- package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +802 -0
- package/src/runtime/memory/lib/recall-format.mjs +55 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +5 -13
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/config.mjs +14 -4
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
- package/src/runtime/shared/schedules-store.mjs +13 -3
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +23 -0
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +119 -2
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +11 -9
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +205 -0
- package/src/session-runtime/quick-search-models.mjs +47 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +352 -0
- package/src/session-runtime/tool-catalog.mjs +29 -29
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/session-runtime/workflow.mjs +1 -1
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +138 -669
- package/src/standalone/channel-admin.mjs +102 -90
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +64 -14
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +77 -870
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +5 -1
- package/src/standalone/provider-admin.mjs +1 -16
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +1059 -8110
- package/src/tui/app/app-format.mjs +213 -0
- package/src/tui/app/channel-pickers.mjs +508 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +356 -0
- package/src/tui/app/model-options.mjs +334 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +489 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +677 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +512 -0
- package/src/tui/app/use-transcript-window.mjs +607 -0
- package/src/tui/components/ConfirmBar.jsx +10 -7
- package/src/tui/components/Picker.jsx +64 -15
- package/src/tui/components/PromptInput.jsx +33 -102
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +69 -15
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +52 -594
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +13553 -12384
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine.mjs +170 -574
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +65 -1
- package/src/tui/input-editing.mjs +2 -2
- package/src/tui/markdown/format-token.mjs +4 -1
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +77 -501
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +16 -18
- package/src/workflows/sequential/WORKFLOW.md +16 -18
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +112 -7
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/channels/lib/holidays.mjs +0 -138
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/input-editing.selection.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/paste-fix.test.mjs +0 -119
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic OAuth credential store + PKCE login flow.
|
|
3
|
+
*
|
|
4
|
+
* Split out of anthropic-oauth.mjs (section-scoped extraction). Owns the
|
|
5
|
+
* on-disk credentials file (load/save/refresh/forget) and the loopback
|
|
6
|
+
* PKCE login flow. anthropic-oauth.mjs imports these back and re-exports
|
|
7
|
+
* the public functions so external callers keep their existing import path.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { createServer } from 'http';
|
|
12
|
+
import { randomBytes, createHash } from 'crypto';
|
|
13
|
+
import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
14
|
+
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
15
|
+
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
16
|
+
|
|
17
|
+
// SSRF guard for the OAuth token endpoint override. Env-supplied URLs must be
|
|
18
|
+
// https with a valid http(s) URL shape; reject file:/data:/ftp:/etc. and any
|
|
19
|
+
// http override so a hostile env cannot redirect refresh-token requests.
|
|
20
|
+
function assertSafeTokenURL(rawURL) {
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = new URL(String(rawURL));
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error(`[anthropic-oauth] invalid ANTHROPIC_OAUTH_TOKEN_URL: ${rawURL}`);
|
|
26
|
+
}
|
|
27
|
+
if (parsed.protocol.toLowerCase() !== 'https:') {
|
|
28
|
+
throw new Error(`[anthropic-oauth] ANTHROPIC_OAUTH_TOKEN_URL must use https (got ${parsed.protocol})`);
|
|
29
|
+
}
|
|
30
|
+
return rawURL;
|
|
31
|
+
}
|
|
32
|
+
export const TOKEN_URL = assertSafeTokenURL(process.env.ANTHROPIC_OAUTH_TOKEN_URL || 'https://platform.claude.com/v1/oauth/token');
|
|
33
|
+
export const DEFAULT_CREDENTIALS_PATH = join(resolvePluginData(), 'anthropic-oauth-credentials.json');
|
|
34
|
+
export const CLAUDE_CODE_CLIENT_ID = process.env.ANTHROPIC_OAUTH_CLIENT_ID || '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
35
|
+
export const TOKEN_REFRESH_SKEW_MS = 5 * 60_000;
|
|
36
|
+
const CLAUDE_AI_AUTHORIZE_URL = 'https://claude.com/cai/oauth/authorize';
|
|
37
|
+
const ALL_OAUTH_SCOPES = [
|
|
38
|
+
'org:create_api_key',
|
|
39
|
+
'user:profile',
|
|
40
|
+
'user:inference',
|
|
41
|
+
'user:sessions:claude_code',
|
|
42
|
+
'user:mcp_servers',
|
|
43
|
+
'user:file_upload',
|
|
44
|
+
];
|
|
45
|
+
const OAUTH_LOGIN_SCOPE = ALL_OAUTH_SCOPES.join(' ');
|
|
46
|
+
const OAUTH_CALLBACK_HOST = 'localhost';
|
|
47
|
+
const OAUTH_CALLBACK_PORT = 54545;
|
|
48
|
+
const OAUTH_CALLBACK_PATH = '/callback';
|
|
49
|
+
const OAUTH_REDIRECT_URI = `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`;
|
|
50
|
+
const OAUTH_MANUAL_REDIRECT_URI = process.env.ANTHROPIC_OAUTH_MANUAL_REDIRECT_URI || 'https://platform.claude.com/oauth/code/callback';
|
|
51
|
+
const OAUTH_SUCCESS_REDIRECT_URL = process.env.ANTHROPIC_OAUTH_SUCCESS_REDIRECT_URL || 'https://platform.claude.com/oauth/code/success?app=claude-code';
|
|
52
|
+
const OAUTH_LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
53
|
+
const OAUTH_TOKEN_TIMEOUT_MS = 30_000;
|
|
54
|
+
|
|
55
|
+
export const DEFAULT_CLI_VERSION = '2.1.77';
|
|
56
|
+
|
|
57
|
+
export function resolveCliVersion() {
|
|
58
|
+
return process.env.MIXDOG_CLI_VERSION
|
|
59
|
+
|| DEFAULT_CLI_VERSION;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- Credential helpers ---
|
|
63
|
+
|
|
64
|
+
function _pushUnique(list, value) {
|
|
65
|
+
if (!value || typeof value !== 'string') return;
|
|
66
|
+
if (!list.includes(value)) list.push(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function credentialCandidates() {
|
|
70
|
+
const paths = [];
|
|
71
|
+
_pushUnique(paths, process.env.ANTHROPIC_OAUTH_CREDENTIALS_PATH);
|
|
72
|
+
_pushUnique(paths, DEFAULT_CREDENTIALS_PATH);
|
|
73
|
+
return paths;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fallback expiry from the access_token's JWT `exp` claim (epoch ms) when the
|
|
77
|
+
// credentials file carries no explicit expiresAt — without it expiresAt stays 0,
|
|
78
|
+
// which ensureAuth reads as "never expires", disabling proactive refresh. Claude
|
|
79
|
+
// OAuth tokens are opaque so this returns 0 and the file's expiresAt governs.
|
|
80
|
+
// JWT `exp` is epoch SECONDS (RFC 7519).
|
|
81
|
+
function _expiryFromAccessToken(token) {
|
|
82
|
+
try {
|
|
83
|
+
const parts = String(token || '').split('.');
|
|
84
|
+
if (parts.length !== 3) return 0;
|
|
85
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));
|
|
86
|
+
const exp = Number(payload?.exp);
|
|
87
|
+
return Number.isFinite(exp) && exp > 0 ? exp * 1000 : 0;
|
|
88
|
+
} catch { return 0; }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _loadCredentialsFile(path) {
|
|
92
|
+
if (!existsSync(path)) return null;
|
|
93
|
+
try {
|
|
94
|
+
const stat = statSync(path);
|
|
95
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
96
|
+
const oauth = raw?.claudeAiOauth;
|
|
97
|
+
if (!oauth?.accessToken) return null;
|
|
98
|
+
return {
|
|
99
|
+
path,
|
|
100
|
+
mtimeMs: stat.mtimeMs,
|
|
101
|
+
accessToken: oauth.accessToken,
|
|
102
|
+
refreshToken: oauth.refreshToken || null,
|
|
103
|
+
expiresAt: _normalizeExpiresAt(oauth.expiresAt ?? oauth.expires_at) || _expiryFromAccessToken(oauth.accessToken),
|
|
104
|
+
scopes: Array.isArray(oauth.scopes) ? oauth.scopes : [],
|
|
105
|
+
subscriptionType: oauth.subscriptionType || null,
|
|
106
|
+
};
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Cross-process safe credential save. Lockfile (O_EXCL) prevents two Mixdog
|
|
113
|
+
// refreshers from clobbering each other; atomic rename guarantees readers see
|
|
114
|
+
// either the old or new file, never a half-written one. Used so refresh_token
|
|
115
|
+
// rotation propagates to other Mixdog readers of the same credentials file
|
|
116
|
+
// instead of leaving them stuck on the previous refresh_token.
|
|
117
|
+
export function _saveCredentialsFile(path, raw) {
|
|
118
|
+
// Secret file, not parent-dir ACL mutation. `secret: true` clamps the file
|
|
119
|
+
// itself on Windows; it deliberately leaves the data dir inheritance alone.
|
|
120
|
+
writeJsonAtomicSync(path, raw, { lock: true, fsyncDir: true, mode: 0o600, secret: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Cheap stat-only probe so ensureAuth can detect Mixdog-updated credentials
|
|
124
|
+
// without paying a full JSON read every call.
|
|
125
|
+
export function _credentialsMaxMtime() {
|
|
126
|
+
let max = 0;
|
|
127
|
+
for (const p of credentialCandidates()) {
|
|
128
|
+
try {
|
|
129
|
+
const s = statSync(p);
|
|
130
|
+
if (s.mtimeMs > max) max = s.mtimeMs;
|
|
131
|
+
} catch { /* not present — skip */ }
|
|
132
|
+
}
|
|
133
|
+
return max;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function loadCredentials() {
|
|
137
|
+
const loaded = credentialCandidates()
|
|
138
|
+
.map(_loadCredentialsFile)
|
|
139
|
+
.filter(Boolean);
|
|
140
|
+
if (!loaded.length) return null;
|
|
141
|
+
loaded.sort((a, b) => (Number(b.expiresAt) || 0) - (Number(a.expiresAt) || 0));
|
|
142
|
+
return loaded[0];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Public predicate used by config.buildDefaultConfig — provider is enabled
|
|
146
|
+
// when on-disk credentials exist AND carry the inference scope. Single
|
|
147
|
+
// truth: same loader the runtime uses, no parallel hard-coded path probe.
|
|
148
|
+
export function hasAnthropicOAuthCredentials() {
|
|
149
|
+
const creds = loadCredentials();
|
|
150
|
+
if (!creds?.accessToken) return false;
|
|
151
|
+
return Array.isArray(creds.scopes) && creds.scopes.includes('user:inference');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function describeAnthropicOAuthCredentials() {
|
|
155
|
+
try {
|
|
156
|
+
const creds = loadCredentials();
|
|
157
|
+
if (!creds?.accessToken) {
|
|
158
|
+
return { authenticated: false, status: 'Not Set', detail: 'Mixdog OAuth credentials' };
|
|
159
|
+
}
|
|
160
|
+
const hasInferenceScope = Array.isArray(creds.scopes) && creds.scopes.includes('user:inference');
|
|
161
|
+
const hasRefresh = Boolean(creds.refreshToken);
|
|
162
|
+
const expiresAt = _normalizeExpiresAt(creds.expiresAt);
|
|
163
|
+
const expiring = expiresAt > 0 && expiresAt < Date.now() + TOKEN_REFRESH_SKEW_MS;
|
|
164
|
+
const expired = expiresAt > 0 && expiresAt <= Date.now();
|
|
165
|
+
const detail = creds.path || DEFAULT_CREDENTIALS_PATH;
|
|
166
|
+
if (!hasInferenceScope) {
|
|
167
|
+
return { authenticated: false, status: 'Missing Scope', detail, expiresAt };
|
|
168
|
+
}
|
|
169
|
+
if (!hasRefresh) {
|
|
170
|
+
return {
|
|
171
|
+
authenticated: expiresAt === 0 || !expired,
|
|
172
|
+
status: expired ? 'Reauth Required' : 'Access Only',
|
|
173
|
+
detail: `${detail}; no refresh token`,
|
|
174
|
+
expiresAt,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (expired) return { authenticated: true, status: 'Refresh Required', detail, expiresAt };
|
|
178
|
+
if (expiring) return { authenticated: true, status: 'Refresh Soon', detail, expiresAt };
|
|
179
|
+
return { authenticated: true, status: 'Valid', detail, expiresAt };
|
|
180
|
+
} catch (err) {
|
|
181
|
+
return { authenticated: false, status: 'Error', detail: String(err?.message || err).slice(0, 200) };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function forgetAnthropicOAuthCredentials() {
|
|
186
|
+
let removed = false;
|
|
187
|
+
for (const path of credentialCandidates()) {
|
|
188
|
+
if (!existsSync(path)) continue;
|
|
189
|
+
try {
|
|
190
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
191
|
+
if (raw?.claudeAiOauth) {
|
|
192
|
+
delete raw.claudeAiOauth;
|
|
193
|
+
_saveCredentialsFile(path, raw);
|
|
194
|
+
removed = true;
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
throw new Error(`Anthropic OAuth reset failed for ${path}: ${err?.message || err}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { removed };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function _normalizeExpiresAt(value) {
|
|
204
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return 0;
|
|
205
|
+
return value < 1e12 ? value * 1000 : value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function _scrubTokens(text) {
|
|
209
|
+
return String(text || '')
|
|
210
|
+
.replace(/Bearer [A-Za-z0-9._\-]+/g, 'Bearer [REDACTED]')
|
|
211
|
+
.replace(/sk-ant-[A-Za-z0-9._\-]+/g, '[REDACTED]')
|
|
212
|
+
.replace(/"access[Tt]oken"\s*:\s*"[^"]+"/g, '"accessToken":"[REDACTED]"')
|
|
213
|
+
.replace(/"refresh[Tt]oken"\s*:\s*"[^"]+"/g, '"refreshToken":"[REDACTED]"')
|
|
214
|
+
.replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token":"[REDACTED]"')
|
|
215
|
+
.replace(/"refresh_token"\s*:\s*"[^"]+"/g, '"refresh_token":"[REDACTED]"');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function refreshOAuthCredentials(creds) {
|
|
219
|
+
if (!creds?.refreshToken) {
|
|
220
|
+
throw new Error('Anthropic OAuth refresh token not available. Open /providers in mixdog to sign in again.');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const controller = new AbortController();
|
|
224
|
+
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch(TOKEN_URL, {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: {
|
|
229
|
+
'Content-Type': 'application/json',
|
|
230
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
231
|
+
'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
|
|
232
|
+
},
|
|
233
|
+
body: JSON.stringify({
|
|
234
|
+
grant_type: 'refresh_token',
|
|
235
|
+
refresh_token: creds.refreshToken,
|
|
236
|
+
client_id: CLAUDE_CODE_CLIENT_ID,
|
|
237
|
+
}),
|
|
238
|
+
// Never follow a redirect on a secret-bearing request: a token
|
|
239
|
+
// endpoint that 307/308-redirects would replay the refresh_token to
|
|
240
|
+
// the redirect target. Fail loud instead.
|
|
241
|
+
redirect: 'error',
|
|
242
|
+
signal: controller.signal,
|
|
243
|
+
dispatcher: getLlmDispatcher(),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const text = await res.text();
|
|
247
|
+
let json = null;
|
|
248
|
+
try { json = text ? JSON.parse(text) : null; } catch { /* handled below */ }
|
|
249
|
+
if (!res.ok) {
|
|
250
|
+
const isInvalidGrant = text.includes('invalid_grant') || json?.error === 'invalid_grant';
|
|
251
|
+
throw Object.assign(new Error(`token refresh ${res.status}: ${_scrubTokens(text).slice(0, 200)}`), { isInvalidGrant });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const accessToken = json?.access_token || json?.accessToken;
|
|
255
|
+
if (!accessToken) throw new Error('token refresh returned no access token');
|
|
256
|
+
const expiresAt = _normalizeExpiresAt(json?.expires_at ?? json?.expiresAt)
|
|
257
|
+
|| (typeof json?.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0);
|
|
258
|
+
const refreshed = {
|
|
259
|
+
path: creds.path,
|
|
260
|
+
accessToken,
|
|
261
|
+
refreshToken: json?.refresh_token || json?.refreshToken || creds.refreshToken,
|
|
262
|
+
expiresAt,
|
|
263
|
+
scopes: Array.isArray(json?.scope) ? json.scope : creds.scopes,
|
|
264
|
+
subscriptionType: creds.subscriptionType,
|
|
265
|
+
};
|
|
266
|
+
// Persist rotated tokens back so any other Mixdog reader of the same
|
|
267
|
+
// credentials file picks up the new refresh_token. Without this, a
|
|
268
|
+
// later process can replay an old single-use refresh token and loop on
|
|
269
|
+
// invalid_grant.
|
|
270
|
+
if (creds.path && existsSync(creds.path)) {
|
|
271
|
+
try {
|
|
272
|
+
const raw = JSON.parse(readFileSync(creds.path, 'utf-8'));
|
|
273
|
+
raw.claudeAiOauth = {
|
|
274
|
+
...(raw.claudeAiOauth || {}),
|
|
275
|
+
accessToken: refreshed.accessToken,
|
|
276
|
+
refreshToken: refreshed.refreshToken,
|
|
277
|
+
expiresAt: refreshed.expiresAt,
|
|
278
|
+
scopes: refreshed.scopes,
|
|
279
|
+
};
|
|
280
|
+
_saveCredentialsFile(creds.path, raw);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
process.stderr.write(`[anthropic-oauth] credential save failed: ${_scrubTokens(err?.message || String(err)).slice(0, 200)}\n`);
|
|
283
|
+
throw new Error(`[oauth] credentials save failed: ${err?.message ?? String(err)}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return refreshed;
|
|
287
|
+
} catch (err) {
|
|
288
|
+
if (err?.name === 'AbortError') {
|
|
289
|
+
throw new Error('Anthropic OAuth token refresh timed out after 30000ms');
|
|
290
|
+
}
|
|
291
|
+
throw err;
|
|
292
|
+
} finally {
|
|
293
|
+
clearTimeout(timeout);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- Login flow (PKCE loopback, export for setup UI / CLI) ---
|
|
298
|
+
|
|
299
|
+
function _oauthGeneratePKCE() {
|
|
300
|
+
const verifier = randomBytes(32).toString('base64url');
|
|
301
|
+
const challenge = createHash('sha256').update(verifier).digest('base64url');
|
|
302
|
+
return { verifier, challenge };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function _oauthCredentialsWritePath() {
|
|
306
|
+
for (const p of credentialCandidates()) {
|
|
307
|
+
if (existsSync(p)) return p;
|
|
308
|
+
}
|
|
309
|
+
return DEFAULT_CREDENTIALS_PATH;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function _oauthParseScopeField(scope) {
|
|
313
|
+
if (Array.isArray(scope)) return scope;
|
|
314
|
+
return String(scope || '').split(' ').filter(Boolean);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function _parseOAuthCodeInput(input) {
|
|
318
|
+
const value = String(input || '').trim();
|
|
319
|
+
if (!value) return { code: '', state: '' };
|
|
320
|
+
try {
|
|
321
|
+
const url = new URL(value);
|
|
322
|
+
const code = url.searchParams.get('code') || '';
|
|
323
|
+
const state = url.searchParams.get('state') || '';
|
|
324
|
+
if (code || state) return { code, state, redirectUri: `${url.origin}${url.pathname}` };
|
|
325
|
+
} catch { /* not a URL */ }
|
|
326
|
+
if (value.includes('#')) {
|
|
327
|
+
const [code, state] = value.split('#', 2);
|
|
328
|
+
return { code: String(code || '').trim(), state: String(state || '').trim() };
|
|
329
|
+
}
|
|
330
|
+
if (value.includes('code=')) {
|
|
331
|
+
const params = new URLSearchParams(value.startsWith('?') ? value.slice(1) : value);
|
|
332
|
+
return { code: params.get('code') || '', state: params.get('state') || '' };
|
|
333
|
+
}
|
|
334
|
+
return { code: value, state: '' };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function exchangeAuthorizationCode({ pkce, code, state, redirectUri }) {
|
|
338
|
+
const cleanCode = String(code || '').trim();
|
|
339
|
+
if (!cleanCode) throw new Error('[anthropic-oauth] authorization code is required');
|
|
340
|
+
const tokenRes = await fetch(TOKEN_URL, {
|
|
341
|
+
method: 'POST',
|
|
342
|
+
headers: {
|
|
343
|
+
'Content-Type': 'application/json',
|
|
344
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
345
|
+
'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
|
|
346
|
+
},
|
|
347
|
+
body: JSON.stringify({
|
|
348
|
+
grant_type: 'authorization_code',
|
|
349
|
+
code: cleanCode,
|
|
350
|
+
redirect_uri: redirectUri,
|
|
351
|
+
client_id: CLAUDE_CODE_CLIENT_ID,
|
|
352
|
+
code_verifier: pkce.verifier,
|
|
353
|
+
state,
|
|
354
|
+
}),
|
|
355
|
+
redirect: 'error',
|
|
356
|
+
signal: AbortSignal.timeout(OAUTH_TOKEN_TIMEOUT_MS),
|
|
357
|
+
dispatcher: getLlmDispatcher(),
|
|
358
|
+
});
|
|
359
|
+
if (!tokenRes.ok) {
|
|
360
|
+
const text = await tokenRes.text().catch(() => '');
|
|
361
|
+
throw new Error(`[anthropic-oauth] token exchange ${tokenRes.status}: ${_scrubTokens(text).slice(0, 500)}`);
|
|
362
|
+
}
|
|
363
|
+
const json = await tokenRes.json();
|
|
364
|
+
const accessToken = json?.access_token || json?.accessToken;
|
|
365
|
+
const refreshToken = json?.refresh_token || json?.refreshToken;
|
|
366
|
+
if (!accessToken || !refreshToken) {
|
|
367
|
+
throw new Error('[anthropic-oauth] token exchange response missing access_token or refresh_token');
|
|
368
|
+
}
|
|
369
|
+
const expiresAt = _normalizeExpiresAt(json?.expires_at ?? json?.expiresAt)
|
|
370
|
+
|| (typeof json?.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0);
|
|
371
|
+
const scopes = _oauthParseScopeField(json?.scope);
|
|
372
|
+
const credPath = _oauthCredentialsWritePath();
|
|
373
|
+
let raw = {};
|
|
374
|
+
if (existsSync(credPath)) {
|
|
375
|
+
raw = JSON.parse(readFileSync(credPath, 'utf-8'));
|
|
376
|
+
}
|
|
377
|
+
const existingOauth = raw.claudeAiOauth || {};
|
|
378
|
+
raw.claudeAiOauth = {
|
|
379
|
+
...existingOauth,
|
|
380
|
+
accessToken,
|
|
381
|
+
refreshToken,
|
|
382
|
+
expiresAt,
|
|
383
|
+
scopes,
|
|
384
|
+
subscriptionType: existingOauth.subscriptionType ?? null,
|
|
385
|
+
};
|
|
386
|
+
_saveCredentialsFile(credPath, raw);
|
|
387
|
+
return {
|
|
388
|
+
path: credPath,
|
|
389
|
+
accessToken,
|
|
390
|
+
refreshToken,
|
|
391
|
+
expiresAt,
|
|
392
|
+
scopes,
|
|
393
|
+
subscriptionType: raw.claudeAiOauth.subscriptionType,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function beginOAuthLogin() {
|
|
398
|
+
const pkce = _oauthGeneratePKCE();
|
|
399
|
+
const state = randomBytes(32).toString('base64url');
|
|
400
|
+
const buildUrl = (redirectUri) => {
|
|
401
|
+
const url = new URL(CLAUDE_AI_AUTHORIZE_URL);
|
|
402
|
+
url.searchParams.set('code', 'true');
|
|
403
|
+
url.searchParams.set('client_id', CLAUDE_CODE_CLIENT_ID);
|
|
404
|
+
url.searchParams.set('response_type', 'code');
|
|
405
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
406
|
+
url.searchParams.set('scope', OAUTH_LOGIN_SCOPE);
|
|
407
|
+
url.searchParams.set('code_challenge', pkce.challenge);
|
|
408
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
409
|
+
url.searchParams.set('state', state);
|
|
410
|
+
return url;
|
|
411
|
+
};
|
|
412
|
+
const url = buildUrl(OAUTH_REDIRECT_URI);
|
|
413
|
+
const manualUrl = buildUrl(OAUTH_MANUAL_REDIRECT_URI);
|
|
414
|
+
|
|
415
|
+
let server = null;
|
|
416
|
+
let timeout = null;
|
|
417
|
+
let finish = null;
|
|
418
|
+
const waitForCallback = new Promise((resolve, reject) => {
|
|
419
|
+
let settled = false;
|
|
420
|
+
finish = (value, error = null) => {
|
|
421
|
+
if (settled) return;
|
|
422
|
+
settled = true;
|
|
423
|
+
if (timeout) clearTimeout(timeout);
|
|
424
|
+
try { server?.close(); } catch { /* already closed */ }
|
|
425
|
+
if (error) reject(error);
|
|
426
|
+
else resolve(value);
|
|
427
|
+
};
|
|
428
|
+
server = createServer(async (req, res) => {
|
|
429
|
+
const u = new URL(req.url || '/', `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}`);
|
|
430
|
+
if (u.pathname !== OAUTH_CALLBACK_PATH) {
|
|
431
|
+
res.writeHead(404);
|
|
432
|
+
res.end();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const code = u.searchParams.get('code');
|
|
436
|
+
if (!code || u.searchParams.get('state') !== state) {
|
|
437
|
+
res.writeHead(400);
|
|
438
|
+
res.end('Invalid');
|
|
439
|
+
finish(null);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
const tokens = await exchangeAuthorizationCode({ pkce, code, state, redirectUri: OAUTH_REDIRECT_URI });
|
|
444
|
+
res.writeHead(302, { Location: OAUTH_SUCCESS_REDIRECT_URL });
|
|
445
|
+
res.end();
|
|
446
|
+
finish(tokens);
|
|
447
|
+
} catch (err) {
|
|
448
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
449
|
+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
450
|
+
res.end(`Claude login failed: ${error.message}`);
|
|
451
|
+
finish(null, error);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
timeout = setTimeout(() => finish(null), OAUTH_LOGIN_TIMEOUT_MS);
|
|
455
|
+
server.listen(OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_HOST, async () => {
|
|
456
|
+
process.stderr.write(`\n[anthropic-oauth] Open this URL to log in with Claude:\n${url.toString()}\n\nIf the localhost callback cannot complete, open this manual URL and paste the shown code#state:\n${manualUrl.toString()}\n\n`);
|
|
457
|
+
try {
|
|
458
|
+
const { openInBrowser } = await import('../../../shared/open-url.mjs');
|
|
459
|
+
openInBrowser(url.toString());
|
|
460
|
+
} catch (err) {
|
|
461
|
+
process.stderr.write(`[anthropic-oauth] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
server.on('error', (err) => finish(null, new Error(`[anthropic-oauth] callback server failed on ${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}: ${err?.message || err}`)));
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
provider: 'anthropic-oauth',
|
|
469
|
+
url: url.toString(),
|
|
470
|
+
manualUrl: manualUrl.toString(),
|
|
471
|
+
waitForCallback,
|
|
472
|
+
completeCode: async (input) => {
|
|
473
|
+
const parsed = _parseOAuthCodeInput(input);
|
|
474
|
+
if (parsed.state && parsed.state !== state) throw new Error('[anthropic-oauth] OAuth state mismatch');
|
|
475
|
+
const redirectUri = parsed.redirectUri || (parsed.state ? OAUTH_MANUAL_REDIRECT_URI : OAUTH_REDIRECT_URI);
|
|
476
|
+
const tokens = await exchangeAuthorizationCode({ pkce, code: parsed.code, state, redirectUri });
|
|
477
|
+
finish?.(tokens);
|
|
478
|
+
return tokens;
|
|
479
|
+
},
|
|
480
|
+
cancel: () => {
|
|
481
|
+
finish?.(null);
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export async function loginOAuth() {
|
|
487
|
+
const login = await beginOAuthLogin();
|
|
488
|
+
return await login.waitForCallback;
|
|
489
|
+
}
|