mixdog 1.2.4 → 1.2.6
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/package.json +1 -1
- package/scripts/lib/gate-scheduler.mjs +2 -2
- package/scripts/lib/run-node-tests.mjs +5 -14
- package/scripts/mobile-release.mjs +5 -4
- package/scripts/provider-pingpong-live.mjs +9 -9
- package/src/agents/advisor/AGENT.md +4 -4
- package/src/agents/advisor/agent.json +1 -1
- package/src/agents/reviewer/AGENT.md +2 -3
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +11 -0
- package/src/defaults/skills/browser-use/SKILL.md +2 -1
- package/src/defaults/skills/computer-use/SKILL.md +3 -1
- package/src/defaults/skills/visualize/SKILL.md +3 -2
- package/src/lib/rules-builder.cjs +1 -1
- package/src/output-styles/common.md +22 -5
- package/src/output-styles/detailed.md +7 -5
- package/src/output-styles/minimal.md +7 -3
- package/src/output-styles/simple.md +2 -3
- package/src/rules/agent/AGENT.md +3 -1
- package/src/rules/lead/LEAD.md +10 -7
- package/src/rules/shared/10-tool-workflow.md +2 -1
- package/src/rules/shared/60-verification.md +4 -5
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
- package/src/runtime/agent/orchestrator/config.mjs +6 -0
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +53 -60
- package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +40 -19
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
- package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
- package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
- package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
- package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +2 -3
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +3 -8
- package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
- package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +9 -19
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
- package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
- package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +255 -32
- package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +306 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +70 -21
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
- package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
- package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
- package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +119 -32
- package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
- package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
- package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +40 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +35 -4
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
- package/src/runtime/attachments/office-extract.mjs +11 -9
- package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
- package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
- package/src/runtime/memory/lib/tool-call-handler.mjs +7 -5
- package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
- package/src/runtime/office/core/tabular.mjs +1 -1
- package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
- package/src/runtime/office/portable/docx-formatting.mjs +1 -1
- package/src/runtime/office/portable/docx-runs.mjs +1 -1
- package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
- package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
- package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
- package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
- package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
- package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
- package/src/runtime/office/portable/portable-opc.mjs +27 -13
- package/src/runtime/office/portable/portable-pivot.mjs +3 -3
- package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
- package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
- package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
- package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
- package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
- package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
- package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
- package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
- package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
- package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
- package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
- package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
- package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
- package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
- package/src/runtime/office/portable/portable-xml.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +22 -13
- package/src/runtime/shared/background-tasks.mjs +60 -20
- package/src/runtime/shared/child-spawn-gate.mjs +26 -16
- package/src/runtime/shared/config.mjs +0 -10
- package/src/runtime/shared/file-lock.mjs +7 -1
- package/src/runtime/shared/file-permissions-worker.mjs +38 -0
- package/src/runtime/shared/file-permissions.d.mts +19 -4
- package/src/runtime/shared/file-permissions.mjs +310 -4
- package/src/runtime/shared/git-runner-worker.mjs +6 -78
- package/src/runtime/shared/llm/http-agent.mjs +7 -3
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
- package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
- package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
- package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
- package/src/runtime/shared/memory-snapshot.mjs +5 -1
- package/src/runtime/shared/notify-trace.mjs +8 -1
- package/src/runtime/shared/pristine-execution-contract.json +5 -1
- package/src/runtime/shared/process-runner-worker.mjs +7 -0
- package/src/runtime/shared/process-runner.mjs +38 -0
- package/src/runtime/shared/provider-accounts.mjs +2 -1
- package/src/runtime/shared/provider-api-key.mjs +0 -1
- package/src/runtime/shared/read-row-numbers.mjs +1 -1
- package/src/runtime/shared/resource-admission.mjs +60 -18
- package/src/runtime/shared/run-child.mjs +84 -0
- package/src/runtime/shared/session-activity-signal.mjs +23 -0
- package/src/runtime/shared/shell-output-summary.mjs +23 -0
- package/src/runtime/shared/singleton-owner.mjs +126 -42
- package/src/runtime/shared/tool-output-limit.mjs +4 -2
- package/src/runtime/shared/tool-output-marker.mjs +58 -0
- package/src/runtime/shared/user-data-guard.mjs +28 -9
- package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
- package/src/runtime/shared/worktrees/git-head.mjs +20 -2
- package/src/runtime/shared/worktrees/lock.mjs +3 -2
- package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
- package/src/runtime/tidy/runners/biome.mjs +3 -1
- package/src/runtime/tidy/shadow.mjs +2 -1
- package/src/runtime/web-search/index.mjs +5 -7
- package/src/runtime/web-search/lib/formatter.mjs +42 -6
- package/src/runtime/web-search/tool-defs.mjs +6 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -4
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
- package/src/session-runtime/lifecycle/shared.mjs +19 -2
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
- package/src/session-runtime/lifecycle/teardown.mjs +5 -2
- package/src/session-runtime/lifecycle-api.mjs +3 -1
- package/src/session-runtime/provider-auth/login.mjs +10 -6
- package/src/session-runtime/provider-auth/setup.mjs +19 -2
- package/src/session-runtime/provider-catalog-cache.mjs +101 -25
- package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
- package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
- package/src/session-runtime/provider-models.mjs +76 -10
- package/src/session-runtime/provider-readiness.mjs +4 -0
- package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
- package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
- package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
- package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
- package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
- package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
- package/src/session-runtime/services/agent-tool.mjs +20 -0
- package/src/session-runtime/services/provider-admin.mjs +92 -20
- package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
- package/src/session-runtime/services/usage-dashboard.mjs +27 -69
- package/src/session-runtime/turn/session-ops.mjs +2 -2
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
- package/src/session-runtime/usage-stats-api.mjs +6 -1
- package/src/standalone/channel-transport/http-routes.mjs +10 -9
- package/src/standalone/daemon-desktop-runtime.mjs +20 -7
- package/src/standalone/daemon-owner.mjs +117 -25
- package/src/standalone/daemon.mjs +43 -13
- package/src/standalone/desktop-service-registry.mjs +32 -5
- package/src/standalone/session-call-cache.mjs +6 -0
- package/src/standalone/session-protocol.mjs +1 -1
- package/src/standalone/session-service/retention/busy.mjs +7 -2
- package/src/standalone/session-transport/routes.mjs +7 -6
- package/src/tui/app/model-options.mjs +39 -2
- package/src/tui/app/model-picker.mjs +16 -2
- package/src/tui/app/provider-setup-picker.mjs +1 -2
- package/src/tui/dist/index.mjs +436 -123
- package/src/tui/session/notice-text.mjs +0 -1
- package/src/tui/session/session-api/intake/submission.mjs +7 -13
- package/src/tui/session/session-api/integrations.mjs +1 -5
- package/src/tui/session/session-flow/drain.mjs +8 -0
- package/src/tui/session/session-flow/steering.mjs +46 -0
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/turn-stream.mjs +33 -0
- package/src/tui/session/turn.mjs +30 -3
- package/src/ui/statusline.mjs +1 -6
- package/src/ui/streaming-markdown-heal.mjs +94 -25
- package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
- package/src/workflows/default/WORKFLOW.md +9 -13
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { reportStreamItemStart, responsesItemStart } from '../lib/send-callbacks.mjs';
|
|
2
3
|
|
|
3
4
|
// The per-event fold: text/reasoning/tool deltas and item lifecycle feed the
|
|
4
5
|
// response state, the text relay and the semantic watchdog; terminal frames
|
|
5
6
|
// route to the terminal-frame handlers.
|
|
6
|
-
export function createWsEventSwitch({
|
|
7
|
+
export function createWsEventSwitch({
|
|
8
|
+
response,
|
|
9
|
+
textRelay,
|
|
10
|
+
watchdogs,
|
|
11
|
+
midState,
|
|
12
|
+
progress,
|
|
13
|
+
terminal,
|
|
14
|
+
streamingStart,
|
|
15
|
+
onStreamItemStart = null,
|
|
16
|
+
}) {
|
|
7
17
|
let firstDeltaEmitted = false;
|
|
8
18
|
|
|
9
19
|
function onResponseCreated(event) {
|
|
@@ -73,6 +83,7 @@ export function createWsEventSwitch({ response, textRelay, watchdogs, midState,
|
|
|
73
83
|
break;
|
|
74
84
|
case 'response.output_item.added':
|
|
75
85
|
response.noteItemAdded(event.item);
|
|
86
|
+
reportStreamItemStart(onStreamItemStart, responsesItemStart(event.item));
|
|
76
87
|
// Item lifecycle is genuine progress: reset the semantic-idle timer so
|
|
77
88
|
// long server-side tool latency after item-added (before any arg
|
|
78
89
|
// delta) is not mistaken for a silent stall.
|
|
@@ -102,6 +113,8 @@ export function createWsEventSwitch({ response, textRelay, watchdogs, midState,
|
|
|
102
113
|
break;
|
|
103
114
|
case 'response.output_item.done': {
|
|
104
115
|
const kind = response.completeOutputItem(event.item);
|
|
116
|
+
// A stream that names the phase only on the finished message.
|
|
117
|
+
if (event.item?.type === 'message') reportStreamItemStart(onStreamItemStart, responsesItemStart(event.item));
|
|
105
118
|
// Item-done is genuine lifecycle progress — reset semantic idle so
|
|
106
119
|
// latency before the next item/args does not stall.
|
|
107
120
|
watchdogs.resetSemantic();
|
|
@@ -96,6 +96,7 @@ export async function _streamResponse({
|
|
|
96
96
|
onStreamDelta,
|
|
97
97
|
onToolCall,
|
|
98
98
|
onTextDelta,
|
|
99
|
+
onStreamItemStart = null,
|
|
99
100
|
state,
|
|
100
101
|
logSuppressedReasoningDeltas = true,
|
|
101
102
|
traceProvider = 'openai-oauth',
|
|
@@ -236,6 +237,7 @@ export async function _streamResponse({
|
|
|
236
237
|
progress,
|
|
237
238
|
terminal,
|
|
238
239
|
streamingStart,
|
|
240
|
+
onStreamItemStart,
|
|
239
241
|
});
|
|
240
242
|
handlers = createSocketHandlers({
|
|
241
243
|
socket,
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// OpenCode Go login: OAuth device authorization against the OpenCode Console
|
|
2
|
+
// (JSON bodies, client `opencode-cli`), then the account and the organization
|
|
3
|
+
// the token acts for.
|
|
4
|
+
import { openLoginUrl } from './lib/open-login-url.mjs';
|
|
5
|
+
import { scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
|
|
6
|
+
import {
|
|
7
|
+
CLIENT_ID,
|
|
8
|
+
DEVICE_CODE_URL,
|
|
9
|
+
DEVICE_TOKEN_URL,
|
|
10
|
+
ORGS_URL,
|
|
11
|
+
PROVIDER_ID,
|
|
12
|
+
SERVER,
|
|
13
|
+
USER_URL,
|
|
14
|
+
consoleGetJson,
|
|
15
|
+
consoleRequest,
|
|
16
|
+
openCodeGoError,
|
|
17
|
+
saveTokens,
|
|
18
|
+
tokenFieldsFromGrant,
|
|
19
|
+
} from './opencode-go-oauth-tokens.mjs';
|
|
20
|
+
|
|
21
|
+
const DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
22
|
+
const DEFAULT_INTERVAL_S = 5;
|
|
23
|
+
const SLOW_DOWN_STEP_S = 5;
|
|
24
|
+
const MAX_TRANSIENT_FAILURES = 3;
|
|
25
|
+
|
|
26
|
+
const cancelled = () => openCodeGoError(`[${PROVIDER_ID}] login cancelled`, { kind: 'cancelled', cancelled: true });
|
|
27
|
+
|
|
28
|
+
export const defaultSleep = (ms, signal) =>
|
|
29
|
+
new Promise((resolve, reject) => {
|
|
30
|
+
if (signal?.aborted) return reject(cancelled());
|
|
31
|
+
const onAbort = () => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
reject(cancelled());
|
|
34
|
+
};
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
signal?.removeEventListener('abort', onAbort);
|
|
37
|
+
resolve();
|
|
38
|
+
}, ms);
|
|
39
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** The device code request; the verification URL is resolved against the Console. */
|
|
43
|
+
export async function requestDeviceCode({ fetchFn = fetch, signal } = {}) {
|
|
44
|
+
const { res, json, text } = await consoleRequest(DEVICE_CODE_URL, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: { client_id: CLIENT_ID },
|
|
47
|
+
fetchFn,
|
|
48
|
+
signal,
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
throw openCodeGoError(
|
|
52
|
+
`[${PROVIDER_ID}] device authorization ${res.status}: ${scrubOAuthSecrets(text).slice(0, 300)}`,
|
|
53
|
+
{
|
|
54
|
+
kind: 'device-auth',
|
|
55
|
+
status: res.status,
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const deviceCode = typeof json?.device_code === 'string' ? json.device_code : '';
|
|
60
|
+
const userCode = typeof json?.user_code === 'string' ? json.user_code : '';
|
|
61
|
+
const verification = typeof json?.verification_uri_complete === 'string' ? json.verification_uri_complete : '';
|
|
62
|
+
if (!deviceCode || !userCode || !verification) {
|
|
63
|
+
throw openCodeGoError(`[${PROVIDER_ID}] device authorization response is missing required fields`, {
|
|
64
|
+
kind: 'validation',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const url = new URL(verification, `${SERVER}/`);
|
|
68
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
69
|
+
throw openCodeGoError(`[${PROVIDER_ID}] device verification URL is not HTTP(S)`, { kind: 'validation' });
|
|
70
|
+
}
|
|
71
|
+
const interval = Number(json.interval);
|
|
72
|
+
const expiresIn = Number(json.expires_in);
|
|
73
|
+
return {
|
|
74
|
+
deviceCode,
|
|
75
|
+
userCode,
|
|
76
|
+
verificationUrl: url.href,
|
|
77
|
+
intervalS: Number.isFinite(interval) && interval > 0 ? interval : DEFAULT_INTERVAL_S,
|
|
78
|
+
expiresInS: Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Poll until approval, denial or expiry; resolves the token fields of the grant. */
|
|
83
|
+
export async function pollDeviceToken(device, { fetchFn = fetch, signal, sleep = defaultSleep, now = Date.now } = {}) {
|
|
84
|
+
const expired = () =>
|
|
85
|
+
openCodeGoError(`[${PROVIDER_ID}] device code expired — start the login again`, {
|
|
86
|
+
kind: 'device-token',
|
|
87
|
+
code: 'expired_token',
|
|
88
|
+
});
|
|
89
|
+
const deadline = device.expiresInS ? now() + device.expiresInS * 1000 : Number.POSITIVE_INFINITY;
|
|
90
|
+
let intervalMs = device.intervalS * 1000;
|
|
91
|
+
let failures = 0;
|
|
92
|
+
while (now() < deadline) {
|
|
93
|
+
if (signal?.aborted) throw cancelled();
|
|
94
|
+
let reply;
|
|
95
|
+
try {
|
|
96
|
+
reply = await consoleRequest(DEVICE_TOKEN_URL, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
body: { grant_type: DEVICE_GRANT_TYPE, device_code: device.deviceCode, client_id: CLIENT_ID },
|
|
99
|
+
fetchFn,
|
|
100
|
+
signal,
|
|
101
|
+
});
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (signal?.aborted) throw cancelled();
|
|
104
|
+
if (++failures > MAX_TRANSIENT_FAILURES) throw err;
|
|
105
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadline - now())), signal);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const { res, json, text } = reply;
|
|
109
|
+
const error = typeof json?.error === 'string' ? json.error : '';
|
|
110
|
+
if (res.ok && !error && json?.access_token) return tokenFieldsFromGrant(json, now());
|
|
111
|
+
if (error === 'authorization_pending') {
|
|
112
|
+
failures = 0;
|
|
113
|
+
} else if (error === 'slow_down') {
|
|
114
|
+
failures = 0;
|
|
115
|
+
intervalMs += SLOW_DOWN_STEP_S * 1000;
|
|
116
|
+
} else if (error === 'expired_token') {
|
|
117
|
+
throw expired();
|
|
118
|
+
} else if (error === 'access_denied') {
|
|
119
|
+
throw openCodeGoError(`[${PROVIDER_ID}] sign-in was denied`, { kind: 'device-token', code: 'access_denied' });
|
|
120
|
+
} else if (!error && res.status >= 500 && ++failures <= MAX_TRANSIENT_FAILURES) {
|
|
121
|
+
/* transient gateway failure: poll again */
|
|
122
|
+
} else {
|
|
123
|
+
const detail = error || scrubOAuthSecrets(text, [device.deviceCode]).slice(0, 300);
|
|
124
|
+
throw openCodeGoError(`[${PROVIDER_ID}] device token ${res.status}: ${detail}`, {
|
|
125
|
+
kind: 'device-token',
|
|
126
|
+
status: res.status,
|
|
127
|
+
...(error ? { code: error } : {}),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const left = deadline - now();
|
|
131
|
+
if (left <= 0) break;
|
|
132
|
+
await sleep(Math.min(intervalMs, left), signal);
|
|
133
|
+
}
|
|
134
|
+
throw expired();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The organization the login acts for: the grant's own, else the first by name (as opencode picks). */
|
|
138
|
+
export function pickOrganization(orgs, orgId = '') {
|
|
139
|
+
const list = (Array.isArray(orgs) ? orgs : []).filter((org) => typeof org?.id === 'string' && org.id);
|
|
140
|
+
if (orgId) {
|
|
141
|
+
const match = list.find((org) => org.id === orgId);
|
|
142
|
+
if (!match)
|
|
143
|
+
throw openCodeGoError(`[${PROVIDER_ID}] OpenCode organization not found: ${orgId}`, { kind: 'validation' });
|
|
144
|
+
return match;
|
|
145
|
+
}
|
|
146
|
+
return (
|
|
147
|
+
[...list].sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')) || a.id.localeCompare(b.id))[0] ||
|
|
148
|
+
null
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Read the account and organization for a fresh grant and persist the credential. */
|
|
153
|
+
export async function completeLogin(granted, { fetchFn = fetch, signal } = {}) {
|
|
154
|
+
const [user, orgs] = await Promise.all([
|
|
155
|
+
consoleGetJson(USER_URL, granted, { fetchFn, signal }),
|
|
156
|
+
consoleGetJson(ORGS_URL, granted, { fetchFn, signal }),
|
|
157
|
+
]);
|
|
158
|
+
const org = pickOrganization(orgs, granted.org_id);
|
|
159
|
+
if (!org) throw openCodeGoError(`[${PROVIDER_ID}] the OpenCode account has no organization`, { kind: 'validation' });
|
|
160
|
+
return saveTokens({
|
|
161
|
+
...granted,
|
|
162
|
+
account_id: typeof user?.id === 'string' ? user.id : '',
|
|
163
|
+
email: typeof user?.email === 'string' ? user.email.trim().toLowerCase() : '',
|
|
164
|
+
org_id: org.id,
|
|
165
|
+
org_name: typeof org.name === 'string' ? org.name : '',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function beginOAuthLogin({ openBrowser = true, fetchFn = fetch, sleep, now } = {}) {
|
|
170
|
+
const controller = new AbortController();
|
|
171
|
+
const device = await requestDeviceCode({ fetchFn, signal: controller.signal });
|
|
172
|
+
process.stderr.write(
|
|
173
|
+
`\n[${PROVIDER_ID}] Enter code: ${device.userCode}\n[${PROVIDER_ID}] Open: ${device.verificationUrl}\n\n`
|
|
174
|
+
);
|
|
175
|
+
if (openBrowser) await openLoginUrl(PROVIDER_ID, device.verificationUrl);
|
|
176
|
+
const waitForCallback = pollDeviceToken(device, { fetchFn, signal: controller.signal, sleep, now }).then((granted) =>
|
|
177
|
+
completeLogin(granted, { fetchFn, signal: controller.signal })
|
|
178
|
+
);
|
|
179
|
+
// A cancelled flow rejects before some hosts attach a listener.
|
|
180
|
+
waitForCallback.catch(() => {});
|
|
181
|
+
return {
|
|
182
|
+
provider: PROVIDER_ID,
|
|
183
|
+
url: device.verificationUrl,
|
|
184
|
+
manualUrl: null,
|
|
185
|
+
userCode: device.userCode,
|
|
186
|
+
waitForCallback,
|
|
187
|
+
cancel: () => controller.abort(new Error('OpenCode Go login cancelled')),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function loginOAuth() {
|
|
192
|
+
const login = await beginOAuthLogin();
|
|
193
|
+
return await login.waitForCallback;
|
|
194
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// OpenCode Go OAuth credentials: an OpenCode Console device-flow login
|
|
2
|
+
// (client `opencode-cli`) yields a refreshable access token bound to one
|
|
3
|
+
// Console organization. The token reads the organization's remote config
|
|
4
|
+
// (`/api/v2/config`), whose `opencode-go` provider names the inference base
|
|
5
|
+
// URL, the request headers and the model catalog, and the Go usage meters
|
|
6
|
+
// (`/api/go/status`). Stored in Mixdog's own opencode-go-oauth.json, one file
|
|
7
|
+
// per connected account.
|
|
8
|
+
import { readFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
9
|
+
import { join, resolve } from 'node:path';
|
|
10
|
+
import { getPluginData } from '../config.mjs';
|
|
11
|
+
import {
|
|
12
|
+
updateJsonAtomicSync,
|
|
13
|
+
withFileLock,
|
|
14
|
+
withFileLockSync,
|
|
15
|
+
writeJsonAtomicSync,
|
|
16
|
+
} from '../../../shared/atomic-file.mjs';
|
|
17
|
+
import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
|
|
18
|
+
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
19
|
+
import { modelProxyClientMode, serverModelCredentialsDisabledError } from '../../../shared/model-proxy-mode.mjs';
|
|
20
|
+
import {
|
|
21
|
+
accountIdentityFields,
|
|
22
|
+
fileMtimeMs,
|
|
23
|
+
oauthCredentialStatus,
|
|
24
|
+
scrubOAuthSecrets,
|
|
25
|
+
} from './lib/oauth-token-utils.mjs';
|
|
26
|
+
|
|
27
|
+
export const PROVIDER_ID = 'opencode-go-oauth';
|
|
28
|
+
export const SERVER = 'https://opencode.ai/console';
|
|
29
|
+
export const CLIENT_ID = 'opencode-cli';
|
|
30
|
+
export const DEVICE_CODE_URL = `${SERVER}/auth/device/code`;
|
|
31
|
+
export const DEVICE_TOKEN_URL = `${SERVER}/auth/device/token`;
|
|
32
|
+
export const USER_URL = `${SERVER}/api/user`;
|
|
33
|
+
export const ORGS_URL = `${SERVER}/api/orgs`;
|
|
34
|
+
export const CONFIG_URL = `${SERVER}/api/v2/config`;
|
|
35
|
+
export const STATUS_URL = `${SERVER}/api/go/status`;
|
|
36
|
+
// The remote config's provider entry this login serves (`opencode` is Zen).
|
|
37
|
+
export const CONFIG_PROVIDER_KEY = 'opencode-go';
|
|
38
|
+
// Used only when the remote config names no base URL.
|
|
39
|
+
export const FALLBACK_BASE_URL = 'https://opencode.ai/inference/go/openai/v1';
|
|
40
|
+
export const TOKEN_REFRESH_SKEW_MS = 5 * 60_000;
|
|
41
|
+
export const REQUEST_TIMEOUT_MS = 20_000;
|
|
42
|
+
const USER_AGENT = 'mixdog';
|
|
43
|
+
|
|
44
|
+
export function getOwnTokenPath() {
|
|
45
|
+
const bound = boundProviderAuthPath(PROVIDER_ID);
|
|
46
|
+
if (bound) return resolve(bound);
|
|
47
|
+
const explicit = process.env.OPENCODE_GO_OAUTH_CREDENTIALS_PATH;
|
|
48
|
+
if (explicit) return resolve(explicit);
|
|
49
|
+
const dir = getPluginData();
|
|
50
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
51
|
+
return join(dir, 'opencode-go-oauth.json');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const text = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
55
|
+
|
|
56
|
+
export const isValidOpenCodeGoCredentialDocument = (doc) =>
|
|
57
|
+
Boolean(text(doc?.access_token) && text(doc?.refresh_token));
|
|
58
|
+
|
|
59
|
+
export function openCodeGoError(message, meta = {}) {
|
|
60
|
+
return Object.assign(new Error(message), meta);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function openCodeGoReauthError(detail, status = 401) {
|
|
64
|
+
return openCodeGoError(
|
|
65
|
+
`[${PROVIDER_ID}] invalid_grant: OpenCode sign-in expired${detail ? ` (${detail})` : ''} — open /providers in mixdog to sign in again`,
|
|
66
|
+
{ status, reauthRequired: true, kind: 'reauth' }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function loadTokens() {
|
|
71
|
+
if (modelProxyClientMode()) return null;
|
|
72
|
+
const path = getOwnTokenPath();
|
|
73
|
+
if (!existsSync(path)) return null;
|
|
74
|
+
try {
|
|
75
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
76
|
+
if (!isValidOpenCodeGoCredentialDocument(raw)) return null;
|
|
77
|
+
return {
|
|
78
|
+
access_token: raw.access_token,
|
|
79
|
+
refresh_token: raw.refresh_token,
|
|
80
|
+
expires_at: Number(raw.expires_at) || 0,
|
|
81
|
+
account_id: text(raw.account_id),
|
|
82
|
+
email: text(raw.email),
|
|
83
|
+
org_id: text(raw.org_id),
|
|
84
|
+
org_name: text(raw.org_name),
|
|
85
|
+
mtimeMs: fileMtimeMs(path),
|
|
86
|
+
};
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tokenDocument = (tokens) => ({
|
|
93
|
+
access_token: tokens.access_token,
|
|
94
|
+
refresh_token: tokens.refresh_token,
|
|
95
|
+
expires_at: Number(tokens.expires_at) || 0,
|
|
96
|
+
...(text(tokens.account_id) ? { account_id: text(tokens.account_id) } : {}),
|
|
97
|
+
...(text(tokens.email) ? { email: text(tokens.email) } : {}),
|
|
98
|
+
...(text(tokens.org_id) ? { org_id: text(tokens.org_id) } : {}),
|
|
99
|
+
...(text(tokens.org_name) ? { org_name: text(tokens.org_name) } : {}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export function saveTokens(tokens) {
|
|
103
|
+
const doc = tokenDocument(tokens);
|
|
104
|
+
writeJsonAtomicSync(getOwnTokenPath(), doc, { lock: true, fsyncDir: true, mode: 0o600, secret: true });
|
|
105
|
+
return { ...doc, mtimeMs: fileMtimeMs(getOwnTokenPath()) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Stores `tokens` only over the very credential `previous` was read from,
|
|
109
|
+
// compared under the store's write lock: a sign-in that replaced it, or a
|
|
110
|
+
// sign-out that removed it, meanwhile is kept and the stored state returned.
|
|
111
|
+
function replaceTokens(previous, tokens) {
|
|
112
|
+
const path = getOwnTokenPath();
|
|
113
|
+
const doc = tokenDocument(tokens);
|
|
114
|
+
let replaced = false;
|
|
115
|
+
updateJsonAtomicSync(
|
|
116
|
+
path,
|
|
117
|
+
(current) => {
|
|
118
|
+
if (current?.access_token !== previous.access_token || current?.refresh_token !== previous.refresh_token) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
replaced = true;
|
|
122
|
+
return doc;
|
|
123
|
+
},
|
|
124
|
+
{ fsyncDir: true, mode: 0o600, secret: true }
|
|
125
|
+
);
|
|
126
|
+
if (replaced) return { ...doc, mtimeMs: fileMtimeMs(path) };
|
|
127
|
+
const current = loadTokens();
|
|
128
|
+
if (!current) throw openCodeGoReauthError('credentials removed');
|
|
129
|
+
return current;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function hasOpenCodeGoOAuthCredentials() {
|
|
133
|
+
try {
|
|
134
|
+
return Boolean(loadTokens());
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function describeOpenCodeGoOAuthCredentials() {
|
|
141
|
+
try {
|
|
142
|
+
const tokens = loadTokens();
|
|
143
|
+
if (!tokens) {
|
|
144
|
+
return {
|
|
145
|
+
authenticated: false,
|
|
146
|
+
usable: false,
|
|
147
|
+
refreshable: false,
|
|
148
|
+
reauthRequired: false,
|
|
149
|
+
status: 'Not Set',
|
|
150
|
+
detail: 'Mixdog token store',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
...oauthCredentialStatus({
|
|
155
|
+
hasRefresh: true,
|
|
156
|
+
expiresAt: tokens.expires_at,
|
|
157
|
+
detail: tokens.org_name ? `OpenCode Console · ${tokens.org_name}` : 'OpenCode Console',
|
|
158
|
+
refreshSkewMs: TOKEN_REFRESH_SKEW_MS,
|
|
159
|
+
}),
|
|
160
|
+
...accountIdentityFields({ id: tokens.account_id, email: tokens.email }),
|
|
161
|
+
...(tokens.org_id ? { orgId: tokens.org_id } : {}),
|
|
162
|
+
...(tokens.org_name ? { orgName: tokens.org_name } : {}),
|
|
163
|
+
};
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return {
|
|
166
|
+
authenticated: false,
|
|
167
|
+
usable: false,
|
|
168
|
+
refreshable: false,
|
|
169
|
+
reauthRequired: false,
|
|
170
|
+
status: 'Error',
|
|
171
|
+
detail: String(err?.message || err).slice(0, 200),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function forgetOpenCodeGoOAuthCredentials() {
|
|
177
|
+
const path = getOwnTokenPath();
|
|
178
|
+
// Under the store's write lock, so a refresh finishing now cannot restore it.
|
|
179
|
+
return withFileLockSync(
|
|
180
|
+
`${path}.lock`,
|
|
181
|
+
() => {
|
|
182
|
+
if (!existsSync(path)) return { removed: false };
|
|
183
|
+
unlinkSync(path);
|
|
184
|
+
return { removed: true };
|
|
185
|
+
},
|
|
186
|
+
{ secret: true }
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Console bearer headers: the access token plus the organization it acts for. */
|
|
191
|
+
export function consoleHeaders(tokens) {
|
|
192
|
+
return {
|
|
193
|
+
Authorization: `Bearer ${tokens.access_token}`,
|
|
194
|
+
...(tokens.org_id ? { 'x-org-id': tokens.org_id } : {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* One JSON request to the Console. Never follows a redirect (the request
|
|
200
|
+
* carries a token); resolves `{ res, json, text }` whatever the status.
|
|
201
|
+
*/
|
|
202
|
+
export async function consoleRequest(url, { method = 'GET', headers = {}, body, fetchFn = fetch, signal } = {}) {
|
|
203
|
+
const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
|
|
204
|
+
const res = await fetchFn(url, {
|
|
205
|
+
method,
|
|
206
|
+
headers: {
|
|
207
|
+
Accept: 'application/json',
|
|
208
|
+
'User-Agent': USER_AGENT,
|
|
209
|
+
...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
210
|
+
...headers,
|
|
211
|
+
},
|
|
212
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
213
|
+
redirect: 'error',
|
|
214
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
215
|
+
dispatcher: getLlmDispatcher(),
|
|
216
|
+
});
|
|
217
|
+
const textBody = await res.text();
|
|
218
|
+
let json = null;
|
|
219
|
+
try {
|
|
220
|
+
json = textBody ? JSON.parse(textBody) : null;
|
|
221
|
+
} catch {
|
|
222
|
+
/* non-JSON body: callers report the status */
|
|
223
|
+
}
|
|
224
|
+
return { res, json, text: textBody };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Authenticated Console GET; a 401 is a reauth error, any other failure
|
|
229
|
+
* carries its status. `refresh` (a stored credential's read) first refreshes
|
|
230
|
+
* once and retries (withConsoleTokenRetry); a login's fresh grant, not yet
|
|
231
|
+
* stored, has nothing to refresh.
|
|
232
|
+
*/
|
|
233
|
+
export async function consoleGetJson(url, tokens, { fetchFn, signal, refresh = false } = {}) {
|
|
234
|
+
if (refresh) {
|
|
235
|
+
return await withConsoleTokenRetry(tokens, (current) => consoleGetJson(url, current, { fetchFn, signal }), {
|
|
236
|
+
fetchFn,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const { res, json, text: body } = await consoleRequest(url, { headers: consoleHeaders(tokens), fetchFn, signal });
|
|
240
|
+
if (res.status === 401) throw openCodeGoReauthError(`${new URL(url).pathname} 401`);
|
|
241
|
+
if (!res.ok || json === null) {
|
|
242
|
+
const detail = scrubOAuthSecrets(body, [tokens.access_token, tokens.refresh_token]).trim().slice(0, 300);
|
|
243
|
+
throw openCodeGoError(`[${PROVIDER_ID}] ${new URL(url).pathname} ${res.status}${detail ? `: ${detail}` : ''}`, {
|
|
244
|
+
status: res.status,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return json;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Token fields from a device/refresh grant response (`expires_in` seconds). */
|
|
251
|
+
export function tokenFieldsFromGrant(json, now = Date.now()) {
|
|
252
|
+
const access = text(json?.access_token);
|
|
253
|
+
const refresh = text(json?.refresh_token);
|
|
254
|
+
if (!access || !refresh) {
|
|
255
|
+
throw openCodeGoError(`[${PROVIDER_ID}] token response is missing access_token or refresh_token`, {
|
|
256
|
+
kind: 'validation',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const expiresIn = Number(json?.expires_in);
|
|
260
|
+
return {
|
|
261
|
+
access_token: access,
|
|
262
|
+
refresh_token: refresh,
|
|
263
|
+
expires_at: Number.isFinite(expiresIn) && expiresIn > 0 ? now + expiresIn * 1000 : 0,
|
|
264
|
+
...(text(json?.org_id) ? { org_id: text(json.org_id) } : {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function postRefresh(tokens, { fetchFn } = {}) {
|
|
269
|
+
const {
|
|
270
|
+
res,
|
|
271
|
+
json,
|
|
272
|
+
text: body,
|
|
273
|
+
} = await consoleRequest(DEVICE_TOKEN_URL, {
|
|
274
|
+
method: 'POST',
|
|
275
|
+
body: { grant_type: 'refresh_token', refresh_token: tokens.refresh_token, client_id: CLIENT_ID },
|
|
276
|
+
fetchFn,
|
|
277
|
+
});
|
|
278
|
+
if (!res.ok) {
|
|
279
|
+
const oauthError = text(json?.error).toLowerCase();
|
|
280
|
+
if (oauthError === 'invalid_grant' || res.status === 401)
|
|
281
|
+
throw openCodeGoReauthError('refresh rejected', res.status);
|
|
282
|
+
const detail = scrubOAuthSecrets(body, [tokens.access_token, tokens.refresh_token]).trim().slice(0, 200);
|
|
283
|
+
throw openCodeGoError(`[${PROVIDER_ID}] token refresh ${res.status}${detail ? `: ${detail}` : ''}`, {
|
|
284
|
+
status: res.status,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const granted = tokenFieldsFromGrant(json);
|
|
288
|
+
// A grant naming another organization moves the account to it; its name is
|
|
289
|
+
// then unknown until the next sign-in.
|
|
290
|
+
const orgChanged = granted.org_id && granted.org_id !== tokens.org_id;
|
|
291
|
+
return replaceTokens(tokens, {
|
|
292
|
+
...tokens,
|
|
293
|
+
...granted,
|
|
294
|
+
org_id: granted.org_id || tokens.org_id,
|
|
295
|
+
org_name: orgChanged ? granted.org_id : tokens.org_name,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Refresh under a cross-process lease: a waiter that finds a newer generation
|
|
301
|
+
* on disk adopts it instead of spending the refresh token again.
|
|
302
|
+
*/
|
|
303
|
+
export async function refreshTokens(tokens, { force = false, fetchFn } = {}) {
|
|
304
|
+
if (modelProxyClientMode()) throw serverModelCredentialsDisabledError('OpenCode Go OAuth credentials');
|
|
305
|
+
return withFileLock(
|
|
306
|
+
`${getOwnTokenPath()}.refresh.lock`,
|
|
307
|
+
async () => {
|
|
308
|
+
const disk = loadTokens();
|
|
309
|
+
if (!disk) throw openCodeGoReauthError('credentials removed');
|
|
310
|
+
const validAfter = Date.now() + (force ? 0 : TOKEN_REFRESH_SKEW_MS);
|
|
311
|
+
const rotated = disk.access_token !== tokens.access_token || disk.refresh_token !== tokens.refresh_token;
|
|
312
|
+
if (rotated && (!disk.expires_at || disk.expires_at >= validAfter)) return disk;
|
|
313
|
+
return await postRefresh(disk, { fetchFn });
|
|
314
|
+
},
|
|
315
|
+
{ timeoutMs: 60_000, staleMs: 60_000, secret: true }
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* An authenticated Console read of the stored credential that outlives one
|
|
321
|
+
* stale access token: a 401 refreshes once (a newer stored generation is
|
|
322
|
+
* adopted instead of spending the refresh token again) and reads again. Only
|
|
323
|
+
* a second 401, or a refresh the server rejects, asks for a new sign-in.
|
|
324
|
+
*/
|
|
325
|
+
export async function withConsoleTokenRetry(tokens, read, { fetchFn } = {}) {
|
|
326
|
+
try {
|
|
327
|
+
return await read(tokens);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
if (Number(err?.status) !== 401) throw err;
|
|
330
|
+
}
|
|
331
|
+
const next = await refreshTokens(tokens, { force: true, fetchFn });
|
|
332
|
+
if (next.account_id !== tokens.account_id || next.org_id !== tokens.org_id) {
|
|
333
|
+
throw openCodeGoError(`[${PROVIDER_ID}] the OpenCode sign-in changed during the request`, { kind: 'superseded' });
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
return await read(next);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
if (Number(err?.status) === 401 && !err.reauthRequired) throw openCodeGoReauthError('console 401');
|
|
339
|
+
throw err;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** The stored credential, refreshed when it is about to expire (or `force`). */
|
|
344
|
+
export async function ensureFreshTokens({ force = false, fetchFn } = {}) {
|
|
345
|
+
const tokens = loadTokens();
|
|
346
|
+
if (!tokens) {
|
|
347
|
+
throw openCodeGoError(`[${PROVIDER_ID}] credentials not found — open /providers in mixdog to sign in first`, {
|
|
348
|
+
status: 401,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
const expiring = tokens.expires_at > 0 && tokens.expires_at < Date.now() + TOKEN_REFRESH_SKEW_MS;
|
|
352
|
+
return force || expiring ? await refreshTokens(tokens, { force, fetchFn }) : tokens;
|
|
353
|
+
}
|