mixdog 0.7.1
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/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.gitattributes +34 -0
- package/.mcp.json +14 -0
- package/ARCHITECTURE.md +77 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +45 -0
- package/DATA-FLOW.md +79 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/SECURITY.md +138 -0
- package/UNINSTALL.md +112 -0
- package/agents/maintenance.md +5 -0
- package/agents/memory-classification.md +30 -0
- package/agents/scheduler-task.md +18 -0
- package/agents/webhook-handler.md +27 -0
- package/agents/worker.md +24 -0
- package/bin/bridge +133 -0
- package/bin/statusline-launcher.mjs +78 -0
- package/bin/statusline-lib.mjs +550 -0
- package/bin/statusline.mjs +607 -0
- package/bun.lock +802 -0
- package/commands/config.md +16 -0
- package/commands/doctor.md +13 -0
- package/commands/setup.md +17 -0
- package/defaults/cycle3-review-prompt.md +90 -0
- package/defaults/hidden-roles.json +65 -0
- package/defaults/memory-chunk-prompt.md +63 -0
- package/defaults/memory-promote-prompt.md +135 -0
- package/defaults/mixdog-config.template.json +27 -0
- package/defaults/user-workflow.json +8 -0
- package/defaults/user-workflow.md +12 -0
- package/hooks/hooks.json +73 -0
- package/hooks/lib/active-instance.cjs +77 -0
- package/hooks/lib/permission-evaluator.cjs +411 -0
- package/hooks/lib/permission-route.cjs +63 -0
- package/hooks/lib/permission-rules.cjs +170 -0
- package/hooks/lib/settings-loader.cjs +116 -0
- package/hooks/post-tool-use.cjs +84 -0
- package/hooks/pre-mcp-sandbox.cjs +158 -0
- package/hooks/pre-tool-subagent.cjs +253 -0
- package/hooks/session-start.cjs +1372 -0
- package/hooks/turn-timer.cjs +82 -0
- package/lib/claude-md-writer.cjs +386 -0
- package/lib/config-cjs.cjs +61 -0
- package/lib/hook-pipe-path.cjs +10 -0
- package/lib/keychain-cjs.cjs +263 -0
- package/lib/plugin-paths.cjs +61 -0
- package/lib/rules-builder.cjs +241 -0
- package/lib/text-utils.cjs +61 -0
- package/native/README.md +117 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +107 -0
- package/prompts/code-review.txt +16 -0
- package/prompts/security-audit.txt +17 -0
- package/rules/bridge/00-common.md +39 -0
- package/rules/bridge/20-skip-protocol.md +18 -0
- package/rules/bridge/30-explorer.md +33 -0
- package/rules/bridge/40-cycle1-agent.md +52 -0
- package/rules/bridge/41-cycle2-agent.md +62 -0
- package/rules/bridge/42-cycle3-agent.md +44 -0
- package/rules/lead/00-tool-lead.md +61 -0
- package/rules/lead/01-general.md +23 -0
- package/rules/lead/02-channels.md +49 -0
- package/rules/lead/03-team.md +27 -0
- package/rules/lead/04-workflow.md +20 -0
- package/rules/shared/00-language.md +14 -0
- package/rules/shared/01-tool.md +138 -0
- package/scripts/bootstrap.mjs +184 -0
- package/scripts/bridge-unify-smoke.mjs +308 -0
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/builtin-utils-smoke.mjs +392 -0
- package/scripts/check-json.mjs +45 -0
- package/scripts/check-syntax-changed.mjs +102 -0
- package/scripts/check-syntax.mjs +58 -0
- package/scripts/code-graph-batch.test.mjs +33 -0
- package/scripts/config-preserve-smoke.mjs +180 -0
- package/scripts/doctor.mjs +484 -0
- package/scripts/edit-normalize-fuzz.mjs +130 -0
- package/scripts/edit-normalize-smoke.mjs +401 -0
- package/scripts/edit-operation-smoke.mjs +369 -0
- package/scripts/edit2-smoke.mjs +63 -0
- package/scripts/fuzzy-e2e.mjs +28 -0
- package/scripts/fuzzy-smoke.mjs +26 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/guard-smoke.mjs +66 -0
- package/scripts/hidden-role-schema-smoke.mjs +162 -0
- package/scripts/hook-routing-smoke.mjs +29 -0
- package/scripts/inject-input.ps1 +204 -0
- package/scripts/io-complex-smoke.mjs +667 -0
- package/scripts/io-explore-bench.mjs +424 -0
- package/scripts/io-guardrails-smoke.mjs +205 -0
- package/scripts/io-mini-bench-baseline.json +11 -0
- package/scripts/io-mini-bench.mjs +216 -0
- package/scripts/io-route-harness.mjs +933 -0
- package/scripts/io-telemetry-report.mjs +691 -0
- package/scripts/mutation-bench.mjs +564 -0
- package/scripts/mutation-io-smoke.mjs +1081 -0
- package/scripts/native-patch-bridge-smoke.mjs +288 -0
- package/scripts/native-patch-smoke.mjs +304 -0
- package/scripts/patch-interior-context-smoke.mjs +49 -0
- package/scripts/patch-newline-utf8-smoke.mjs +157 -0
- package/scripts/perf-hook-smoke.mjs +71 -0
- package/scripts/permission-eval-smoke.mjs +426 -0
- package/scripts/prep-patch.mjs +53 -0
- package/scripts/prep-shim.mjs +96 -0
- package/scripts/provider-cache-smoke.mjs +687 -0
- package/scripts/report-runtime-health.mjs +132 -0
- package/scripts/run-mcp.mjs +1547 -0
- package/scripts/salvage-v4a-shatter.test.mjs +58 -0
- package/scripts/scoped-cache-io-smoke.mjs +103 -0
- package/scripts/shell-policy-round3-smoke.mjs +46 -0
- package/scripts/smoke-runtime-negative.ps1 +100 -0
- package/scripts/smoke-runtime-negative.sh +95 -0
- package/scripts/stall-policy-smoke.mjs +50 -0
- package/scripts/start-memory-worker.mjs +23 -0
- package/scripts/statusline-launcher-smoke.mjs +82 -0
- package/scripts/stress-atomic-write.mjs +1028 -0
- package/scripts/test-config-rmw-restore.mjs +122 -0
- package/scripts/test-fault-inject.mjs +164 -0
- package/scripts/test-large-file.mjs +174 -0
- package/scripts/tool-edge-smoke.mjs +209 -0
- package/scripts/uninstall.mjs +201 -0
- package/scripts/webhook-selfheal-smoke.mjs +29 -0
- package/scripts/write-overwrite-guard-smoke.mjs +56 -0
- package/server-main.mjs +3055 -0
- package/server.mjs +468 -0
- package/setup/config-merge.mjs +254 -0
- package/setup/install.mjs +120 -0
- package/setup/launch-core.mjs +507 -0
- package/setup/launch.mjs +101 -0
- package/setup/setup-server.mjs +3206 -0
- package/setup/setup.html +3693 -0
- package/skills/retro-skill-proposer/SKILL.md +92 -0
- package/skills/schedule-add/SKILL.md +77 -0
- package/skills/setup/SKILL.md +346 -0
- package/skills/webhook-add/SKILL.md +81 -0
- package/src/agent/bridge-stall-watchdog.mjs +337 -0
- package/src/agent/index.mjs +2138 -0
- package/src/agent/orchestrator/activity-bus.mjs +38 -0
- package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
- package/src/agent/orchestrator/bridge-retry.mjs +220 -0
- package/src/agent/orchestrator/bridge-trace.mjs +583 -0
- package/src/agent/orchestrator/cache-mtime.mjs +58 -0
- package/src/agent/orchestrator/config.mjs +358 -0
- package/src/agent/orchestrator/context/collect.mjs +651 -0
- package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
- package/src/agent/orchestrator/drain-registry.mjs +50 -0
- package/src/agent/orchestrator/explore-validator.mjs +8 -0
- package/src/agent/orchestrator/internal-roles.mjs +118 -0
- package/src/agent/orchestrator/internal-tools.mjs +88 -0
- package/src/agent/orchestrator/jobs.mjs +116 -0
- package/src/agent/orchestrator/mcp/client.mjs +364 -0
- package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
- package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
- package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
- package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
- package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
- package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
- package/src/agent/orchestrator/providers/registry.mjs +192 -0
- package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
- package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
- package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
- package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
- package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
- package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
- package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
- package/src/agent/orchestrator/session/cache/util.mjs +49 -0
- package/src/agent/orchestrator/session/loop.mjs +1478 -0
- package/src/agent/orchestrator/session/manager.mjs +1975 -0
- package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
- package/src/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/agent/orchestrator/session/store.mjs +624 -0
- package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
- package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
- package/src/agent/orchestrator/session/trim.mjs +491 -0
- package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
- package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
- package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
- package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
- package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
- package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
- package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
- package/src/agent/orchestrator/stall-policy.mjs +195 -0
- package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
- package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
- package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
- package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
- package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
- package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
- package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
- package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
- package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
- package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
- package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
- package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
- package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
- package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
- package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
- package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
- package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
- package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
- package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
- package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
- package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
- package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
- package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
- package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
- package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
- package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
- package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
- package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
- package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
- package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
- package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
- package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
- package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
- package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
- package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
- package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
- package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
- package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
- package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
- package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
- package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
- package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
- package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
- package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
- package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
- package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
- package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
- package/src/agent/orchestrator/tools/builtin.mjs +500 -0
- package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
- package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
- package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
- package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
- package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
- package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
- package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
- package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
- package/src/agent/orchestrator/tools/host-input.mjs +204 -0
- package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
- package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
- package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
- package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
- package/src/agent/orchestrator/tools/patch.mjs +2754 -0
- package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
- package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
- package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
- package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
- package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
- package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
- package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
- package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
- package/src/agent/orchestrator/workflow-store.mjs +93 -0
- package/src/agent/tool-defs.mjs +103 -0
- package/src/channels/backends/discord.mjs +784 -0
- package/src/channels/data/voice-runtime-manifest.json +138 -0
- package/src/channels/index.mjs +3229 -0
- package/src/channels/lib/cli-worker-host.mjs +12 -0
- package/src/channels/lib/config-lock.mjs +13 -0
- package/src/channels/lib/config.mjs +292 -0
- package/src/channels/lib/drop-trace.mjs +71 -0
- package/src/channels/lib/event-pipeline.mjs +81 -0
- package/src/channels/lib/event-queue.mjs +345 -0
- package/src/channels/lib/executor.mjs +168 -0
- package/src/channels/lib/format.mjs +188 -0
- package/src/channels/lib/holidays.mjs +138 -0
- package/src/channels/lib/hook-pipe-server.mjs +802 -0
- package/src/channels/lib/interaction-workflows.mjs +184 -0
- package/src/channels/lib/memory-client.mjs +149 -0
- package/src/channels/lib/output-forwarder.mjs +765 -0
- package/src/channels/lib/runtime-paths.mjs +479 -0
- package/src/channels/lib/scheduler.mjs +723 -0
- package/src/channels/lib/session-control.mjs +36 -0
- package/src/channels/lib/session-discovery.mjs +103 -0
- package/src/channels/lib/settings.mjs +11 -0
- package/src/channels/lib/state-file.mjs +68 -0
- package/src/channels/lib/status-snapshot.mjs +219 -0
- package/src/channels/lib/tool-format.mjs +140 -0
- package/src/channels/lib/transcript-discovery.mjs +195 -0
- package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
- package/src/channels/lib/webhook.mjs +1179 -0
- package/src/channels/lib/whisper-server.mjs +477 -0
- package/src/channels/tool-defs.mjs +170 -0
- package/src/daemon/host.mjs +118 -0
- package/src/daemon/mcp-transport.mjs +47 -0
- package/src/daemon/session.mjs +100 -0
- package/src/daemon/thin-client.mjs +71 -0
- package/src/daemon/transport.mjs +163 -0
- package/src/memory/data/runtime-manifest.json +40 -0
- package/src/memory/index.mjs +3305 -0
- package/src/memory/lib/agent-ipc.mjs +93 -0
- package/src/memory/lib/bridge-trace-queries.mjs +120 -0
- package/src/memory/lib/core-memory-store.mjs +330 -0
- package/src/memory/lib/embedding-provider.mjs +269 -0
- package/src/memory/lib/embedding-worker.mjs +323 -0
- package/src/memory/lib/llm-worker-host.mjs +17 -0
- package/src/memory/lib/memory-cycle.mjs +11 -0
- package/src/memory/lib/memory-cycle1.mjs +641 -0
- package/src/memory/lib/memory-cycle2.mjs +1284 -0
- package/src/memory/lib/memory-cycle3.mjs +540 -0
- package/src/memory/lib/memory-embed.mjs +299 -0
- package/src/memory/lib/memory-extraction.mjs +5 -0
- package/src/memory/lib/memory-maintenance-store.mjs +32 -0
- package/src/memory/lib/memory-ops-policy.mjs +190 -0
- package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
- package/src/memory/lib/memory-recall-read-query.mjs +7 -0
- package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
- package/src/memory/lib/memory-recall-store.mjs +621 -0
- package/src/memory/lib/memory-retrievers.mjs +112 -0
- package/src/memory/lib/memory-score.mjs +71 -0
- package/src/memory/lib/memory-text-utils.mjs +58 -0
- package/src/memory/lib/memory.mjs +412 -0
- package/src/memory/lib/model-profile.mjs +85 -0
- package/src/memory/lib/pg/adapter.mjs +308 -0
- package/src/memory/lib/pg/process.mjs +360 -0
- package/src/memory/lib/pg/supervisor.mjs +396 -0
- package/src/memory/lib/project-id-resolver.mjs +86 -0
- package/src/memory/lib/runtime-fetcher.mjs +442 -0
- package/src/memory/lib/trace-store.mjs +728 -0
- package/src/memory/tool-defs.mjs +79 -0
- package/src/search/index.mjs +1173 -0
- package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
- package/src/search/lib/backends/exa.mjs +50 -0
- package/src/search/lib/backends/firecrawl.mjs +61 -0
- package/src/search/lib/backends/gemini-api.mjs +83 -0
- package/src/search/lib/backends/grok-oauth.mjs +86 -0
- package/src/search/lib/backends/index.mjs +150 -0
- package/src/search/lib/backends/openai-api.mjs +144 -0
- package/src/search/lib/backends/openai-oauth.mjs +98 -0
- package/src/search/lib/backends/openai-web-search.mjs +76 -0
- package/src/search/lib/backends/tavily.mjs +55 -0
- package/src/search/lib/backends/xai-api.mjs +113 -0
- package/src/search/lib/cache.mjs +131 -0
- package/src/search/lib/config.mjs +192 -0
- package/src/search/lib/formatter.mjs +115 -0
- package/src/search/lib/provider-usage.mjs +67 -0
- package/src/search/lib/providers.mjs +47 -0
- package/src/search/lib/search-intent.mjs +109 -0
- package/src/search/lib/setup-handler.mjs +261 -0
- package/src/search/lib/state.mjs +201 -0
- package/src/search/lib/web-tools.mjs +1207 -0
- package/src/search/tool-defs.mjs +83 -0
- package/src/setup/defender-exclusion.mjs +183 -0
- package/src/shared/abort-controller.mjs +15 -0
- package/src/shared/atomic-file.mjs +420 -0
- package/src/shared/config.mjs +350 -0
- package/src/shared/daemon-recycle.mjs +108 -0
- package/src/shared/disable-claude-builtins.mjs +88 -0
- package/src/shared/err-text.mjs +12 -0
- package/src/shared/llm/cost.mjs +66 -0
- package/src/shared/llm/http-agent.mjs +123 -0
- package/src/shared/llm/index.mjs +41 -0
- package/src/shared/llm/pid-cleanup.mjs +27 -0
- package/src/shared/llm/usage-log.mjs +47 -0
- package/src/shared/plugin-paths.mjs +58 -0
- package/src/shared/schedules-store.mjs +70 -0
- package/src/shared/seed.mjs +119 -0
- package/src/shared/user-cwd.mjs +213 -0
- package/src/shared/user-data-guard.mjs +238 -0
- package/src/status/aggregator.mjs +584 -0
- package/src/status/server.mjs +413 -0
- package/tools.json +1653 -0
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok CLI OAuth provider ("Grok Build").
|
|
3
|
+
*
|
|
4
|
+
* Authenticates against xAI's shared OAuth client via PKCE (discovery at
|
|
5
|
+
* https://auth.x.ai/.well-known/openid-configuration). Credentials come from
|
|
6
|
+
* either mixdog's own token store (grok-oauth.json) OR the Grok CLI's
|
|
7
|
+
* ~/.grok/auth.json — the same dual-source pattern openai-oauth uses with
|
|
8
|
+
* ~/.codex/auth.json — so an existing `grok` CLI login is picked up without a
|
|
9
|
+
* second sign-in.
|
|
10
|
+
*
|
|
11
|
+
* Inference + catalog merge two sources, routed per model:
|
|
12
|
+
* - api.x.ai/v1 (default): grok-4.x chat models and the web_search backend.
|
|
13
|
+
* - cli-chat-proxy.grok.com/v1 (the grok-build proxy): proxy-only models
|
|
14
|
+
* grok-build and grok-composer-2.5-fast, which api.x.ai does not publish.
|
|
15
|
+
* The proxy version-gates /responses (HTTP 426); we clear it with the Grok
|
|
16
|
+
* CLI client headers (proxyHeaders, real local version from version.json).
|
|
17
|
+
*
|
|
18
|
+
* Inference is delegated to an inner OpenAICompatProvider('xai') — the only
|
|
19
|
+
* preset wired for the Responses API — with the base URL + (for proxy models)
|
|
20
|
+
* the CLI headers injected via config.extraHeaders, bearer swapped for the
|
|
21
|
+
* OAuth access token. The model catalog is the union of both endpoints.
|
|
22
|
+
*/
|
|
23
|
+
import { createServer } from 'http';
|
|
24
|
+
import { randomBytes, createHash } from 'crypto';
|
|
25
|
+
import { readFileSync, existsSync, mkdirSync, statSync } from 'fs';
|
|
26
|
+
import { join } from 'path';
|
|
27
|
+
import { homedir } from 'os';
|
|
28
|
+
import { getPluginData } from '../config.mjs';
|
|
29
|
+
import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
30
|
+
import { enrichModels } from './model-catalog.mjs';
|
|
31
|
+
import { OpenAICompatProvider } from './openai-compat.mjs';
|
|
32
|
+
import { createTimeoutSignal } from '../stall-policy.mjs';
|
|
33
|
+
import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
34
|
+
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
35
|
+
|
|
36
|
+
// --- Constants ---
|
|
37
|
+
// xAI's shared OAuth client. The consent screen renders this as "Grok Build".
|
|
38
|
+
const CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
|
|
39
|
+
const ISSUER = 'https://auth.x.ai';
|
|
40
|
+
const DISCOVERY_URL = `${ISSUER}/.well-known/openid-configuration`;
|
|
41
|
+
const SCOPE = 'openid profile email offline_access grok-cli:access api:access';
|
|
42
|
+
const CALLBACK_HOST = '127.0.0.1';
|
|
43
|
+
const CALLBACK_PORT = 56121;
|
|
44
|
+
const CALLBACK_PATH = '/callback';
|
|
45
|
+
const REDIRECT_URI = `http://${CALLBACK_HOST}:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
46
|
+
// Primary inference + search target. The OAuth token's `api:access` scope works
|
|
47
|
+
// against the STANDARD xAI API (GET /models 200, POST /responses 200, web_search
|
|
48
|
+
// 200 with citations). grok-4.x and web search live here. Proxy-only models
|
|
49
|
+
// (grok-build, grok-composer-2.5-fast) are NOT published on api.x.ai — they route
|
|
50
|
+
// to PROXY_BASE_URL below, which is version-gated and needs the Grok CLI client
|
|
51
|
+
// headers (see proxyHeaders).
|
|
52
|
+
const INFERENCE_BASE_URL = 'https://api.x.ai/v1';
|
|
53
|
+
const DEFAULT_MODEL = 'grok-4.3';
|
|
54
|
+
const TOKEN_REFRESH_SKEW_MS = 5 * 60_000;
|
|
55
|
+
|
|
56
|
+
// --- grok-build CLI proxy (Composer 2.5, grok-build) ---
|
|
57
|
+
// These models live ONLY on the grok-build proxy, not api.x.ai. /models is
|
|
58
|
+
// readable with the bare OAuth bearer; /responses is version-gated (HTTP 426)
|
|
59
|
+
// and requires the Grok CLI's client headers. We route only proxy-only models
|
|
60
|
+
// here and keep grok-4.x + search on api.x.ai.
|
|
61
|
+
const PROXY_BASE_URL = 'https://cli-chat-proxy.grok.com/v1';
|
|
62
|
+
const GROK_CLIENT_IDENTIFIER = 'grok-shell';
|
|
63
|
+
const GROK_CLI_VERSION_FALLBACK = '0.2.16';
|
|
64
|
+
|
|
65
|
+
// Route to the proxy: any grok-composer* model, plus the bare `grok-build`
|
|
66
|
+
// coding agent. NOT grok-build-0.1 — that is a real api.x.ai model and must stay
|
|
67
|
+
// on api.x.ai, so we match grok-build exactly rather than by prefix.
|
|
68
|
+
const PROXY_EXACT_MODELS = new Set(['grok-build']);
|
|
69
|
+
function isProxyOnlyModel(model) {
|
|
70
|
+
const m = String(model || '');
|
|
71
|
+
return /^grok-composer/i.test(m) || PROXY_EXACT_MODELS.has(m);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Use the REAL installed Grok CLI version for the proxy version gate
|
|
75
|
+
// (x-grok-client-version), read from ~/.grok/version.json (or the version
|
|
76
|
+
// stamped into models_cache.json). Cached; only falls back to a known-good
|
|
77
|
+
// constant when neither local file is readable.
|
|
78
|
+
let _grokCliVersionCache = null;
|
|
79
|
+
function grokCliVersion() {
|
|
80
|
+
if (_grokCliVersionCache) return _grokCliVersionCache;
|
|
81
|
+
const grokDir = join(homedir(), '.grok');
|
|
82
|
+
for (const [file, field] of [['version.json', 'version'], ['models_cache.json', 'grok_version']]) {
|
|
83
|
+
try {
|
|
84
|
+
const raw = JSON.parse(readFileSync(join(grokDir, file), 'utf-8'));
|
|
85
|
+
const v = String(raw?.[field] || raw?.stable_version || '').trim();
|
|
86
|
+
if (v) { _grokCliVersionCache = v; return v; }
|
|
87
|
+
} catch { /* try next source */ }
|
|
88
|
+
}
|
|
89
|
+
_grokCliVersionCache = GROK_CLI_VERSION_FALLBACK;
|
|
90
|
+
return _grokCliVersionCache;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Headers the Grok CLI sends to clear the proxy version gate — extracted from
|
|
94
|
+
// the grok binary: x-grok-client-version (the actual 426 gate),
|
|
95
|
+
// x-grok-client-identifier, and a matching User-Agent.
|
|
96
|
+
function proxyHeaders() {
|
|
97
|
+
const v = grokCliVersion();
|
|
98
|
+
return {
|
|
99
|
+
'x-grok-client-version': v,
|
|
100
|
+
'x-grok-client-identifier': GROK_CLIENT_IDENTIFIER,
|
|
101
|
+
'User-Agent': `xai-grok-build/${v}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Retired model aliases xAI no longer exposes by their old ids. The live
|
|
106
|
+
// catalog surfaces the coding model as grok-build-0.1; map the legacy ids to
|
|
107
|
+
// it so a stale config selection doesn't hit a model-not-found. Exact table,
|
|
108
|
+
// not a heuristic. Mirrors openclaw extensions/xai/model-definitions.ts.
|
|
109
|
+
const RETIRED_MODEL_ALIASES = Object.freeze({
|
|
110
|
+
'grok-code-fast-1': 'grok-build-0.1',
|
|
111
|
+
'grok-code-fast': 'grok-build-0.1',
|
|
112
|
+
'grok-code-fast-1-0825': 'grok-build-0.1',
|
|
113
|
+
});
|
|
114
|
+
export function normalizeGrokModelId(id) {
|
|
115
|
+
return (id && RETIRED_MODEL_ALIASES[id]) || id;
|
|
116
|
+
}
|
|
117
|
+
const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
118
|
+
const DISCOVERY_TIMEOUT_MS = 15_000;
|
|
119
|
+
const TOKEN_TIMEOUT_MS = 30_000;
|
|
120
|
+
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
121
|
+
|
|
122
|
+
// Grok CLI credential file. Composite top-level key is "<issuer>::<client_id>".
|
|
123
|
+
function grokCliAuthPath() {
|
|
124
|
+
return join(homedir(), '.grok', 'auth.json');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// SSRF guard for any endpoint pulled from the discovery document or saved
|
|
128
|
+
// tokens. xAI OAuth endpoints must be https on x.ai / *.x.ai — reject
|
|
129
|
+
// anything else outright so a hostile discovery response can't redirect the
|
|
130
|
+
// token / refresh request. Mirrors openclaw's isTrustedXaiOAuthEndpoint.
|
|
131
|
+
function assertTrustedXaiEndpoint(endpoint, label) {
|
|
132
|
+
let url;
|
|
133
|
+
try {
|
|
134
|
+
url = new URL(String(endpoint));
|
|
135
|
+
} catch {
|
|
136
|
+
throw new Error(`[grok-oauth] invalid ${label}: ${endpoint}`);
|
|
137
|
+
}
|
|
138
|
+
const host = url.hostname.toLowerCase();
|
|
139
|
+
if (url.protocol !== 'https:' || (host !== 'x.ai' && !host.endsWith('.x.ai'))) {
|
|
140
|
+
throw new Error(`[grok-oauth] untrusted ${label}: ${endpoint}`);
|
|
141
|
+
}
|
|
142
|
+
return url.toString();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let _discoveryCache = null;
|
|
146
|
+
async function fetchDiscovery() {
|
|
147
|
+
if (_discoveryCache) return _discoveryCache;
|
|
148
|
+
const timeout = createTimeoutSignal(null, DISCOVERY_TIMEOUT_MS, 'grok-oauth discovery');
|
|
149
|
+
try {
|
|
150
|
+
const res = await fetch(DISCOVERY_URL, {
|
|
151
|
+
headers: { Accept: 'application/json' },
|
|
152
|
+
// No redirect-following: the discovery doc is a fixed well-known URL.
|
|
153
|
+
// A 3xx could bounce the request to an untrusted host before the
|
|
154
|
+
// endpoint trust checks below ever run.
|
|
155
|
+
redirect: 'error',
|
|
156
|
+
signal: timeout.signal,
|
|
157
|
+
dispatcher: getLlmDispatcher(),
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok) throw new Error(`discovery ${res.status}`);
|
|
160
|
+
const j = await res.json();
|
|
161
|
+
const discovery = {
|
|
162
|
+
authorization_endpoint: assertTrustedXaiEndpoint(j?.authorization_endpoint, 'authorization endpoint'),
|
|
163
|
+
token_endpoint: assertTrustedXaiEndpoint(j?.token_endpoint, 'token endpoint'),
|
|
164
|
+
};
|
|
165
|
+
_discoveryCache = discovery;
|
|
166
|
+
return discovery;
|
|
167
|
+
} finally {
|
|
168
|
+
timeout.cleanup();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- Token store ---
|
|
173
|
+
function getOwnTokenPath() {
|
|
174
|
+
const dir = getPluginData();
|
|
175
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
176
|
+
return join(dir, 'grok-oauth.json');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// expires_at may arrive as a unix number (own store) or an ISO-8601 string
|
|
180
|
+
// (Grok CLI auth.json). Normalize both to epoch milliseconds; 0 means unknown.
|
|
181
|
+
function _normalizeExpiresAt(value) {
|
|
182
|
+
if (typeof value === 'string') {
|
|
183
|
+
const ms = Date.parse(value);
|
|
184
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
185
|
+
}
|
|
186
|
+
const n = Number(value || 0);
|
|
187
|
+
if (!Number.isFinite(n) || n <= 0) return 0;
|
|
188
|
+
return n < 1e12 ? n * 1000 : n;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Fallback expiry from the access_token's JWT `exp` claim (epoch ms) when the
|
|
192
|
+
// store carries no explicit expires_at — without it expires_at stays 0, which
|
|
193
|
+
// ensureAuth reads as "never expires", disabling proactive refresh. Returns 0
|
|
194
|
+
// for opaque (non-JWT) tokens. JWT `exp` is epoch SECONDS (RFC 7519).
|
|
195
|
+
function _expiryFromAccessToken(token) {
|
|
196
|
+
try {
|
|
197
|
+
const parts = String(token || '').split('.');
|
|
198
|
+
if (parts.length !== 3) return 0;
|
|
199
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));
|
|
200
|
+
const exp = Number(payload?.exp);
|
|
201
|
+
return Number.isFinite(exp) && exp > 0 ? exp * 1000 : 0;
|
|
202
|
+
} catch { return 0; }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function _mtimeMs(path) {
|
|
206
|
+
try { return statSync(path).mtimeMs; } catch { return 0; }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// mixdog's own login store (grok-oauth.json). Single writer, accurate
|
|
210
|
+
// numeric expires_at from refresh.
|
|
211
|
+
function _loadOwnTokens() {
|
|
212
|
+
const path = getOwnTokenPath();
|
|
213
|
+
if (!existsSync(path)) return null;
|
|
214
|
+
try {
|
|
215
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
216
|
+
if (!raw?.access_token || !raw?.refresh_token) return null;
|
|
217
|
+
return {
|
|
218
|
+
access_token: raw.access_token,
|
|
219
|
+
refresh_token: raw.refresh_token,
|
|
220
|
+
expires_at: _normalizeExpiresAt(raw.expires_at ?? raw.expiresAt) || _expiryFromAccessToken(raw.access_token),
|
|
221
|
+
token_endpoint: raw.token_endpoint || null,
|
|
222
|
+
source: 'own',
|
|
223
|
+
mtimeMs: _mtimeMs(path),
|
|
224
|
+
};
|
|
225
|
+
} catch { return null; }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Grok CLI store (~/.grok/auth.json). Read-only seed: the access token lives
|
|
229
|
+
// under `key`, keyed by "<issuer>::<client_id>". We never write back here —
|
|
230
|
+
// after the first refresh mixdog manages its own copy, mirroring how
|
|
231
|
+
// openai-oauth treats ~/.codex/auth.json.
|
|
232
|
+
function _loadGrokCliTokens() {
|
|
233
|
+
const path = grokCliAuthPath();
|
|
234
|
+
if (!existsSync(path)) return null;
|
|
235
|
+
try {
|
|
236
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
237
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
238
|
+
// The Grok CLI keys every entry by "<issuer>::<client_id>" — look up
|
|
239
|
+
// exactly that. No scan-for-matching-client_id fallback: a different
|
|
240
|
+
// issuer under the same client_id is a different account/endpoint and
|
|
241
|
+
// must not be silently selected.
|
|
242
|
+
const entry = raw[`${ISSUER}::${CLIENT_ID}`];
|
|
243
|
+
if (!entry?.key || !entry?.refresh_token) return null;
|
|
244
|
+
return {
|
|
245
|
+
access_token: entry.key,
|
|
246
|
+
refresh_token: entry.refresh_token,
|
|
247
|
+
expires_at: _normalizeExpiresAt(entry.expires_at) || _expiryFromAccessToken(entry.key),
|
|
248
|
+
token_endpoint: null,
|
|
249
|
+
source: 'grok-cli',
|
|
250
|
+
mtimeMs: _mtimeMs(path),
|
|
251
|
+
};
|
|
252
|
+
} catch { return null; }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Own store first (accurate expires_at). Fall back to the Grok CLI login.
|
|
256
|
+
function loadTokens() {
|
|
257
|
+
return _loadOwnTokens() || _loadGrokCliTokens();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function saveTokens(tokens) {
|
|
261
|
+
writeJsonAtomicSync(getOwnTokenPath(), {
|
|
262
|
+
access_token: tokens.access_token,
|
|
263
|
+
refresh_token: tokens.refresh_token,
|
|
264
|
+
expires_at: tokens.expires_at || 0,
|
|
265
|
+
token_endpoint: tokens.token_endpoint || null,
|
|
266
|
+
}, { lock: true, fsyncDir: true });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function _scrubTokens(text) {
|
|
270
|
+
return String(text || '')
|
|
271
|
+
.replace(/Bearer [A-Za-z0-9._\-]+/g, 'Bearer [REDACTED]')
|
|
272
|
+
.replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token":"[REDACTED]"')
|
|
273
|
+
.replace(/"refresh_token"\s*:\s*"[^"]+"/g, '"refresh_token":"[REDACTED]"')
|
|
274
|
+
.replace(/"key"\s*:\s*"[^"]+"/g, '"key":"[REDACTED]"');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Public predicate used by config.buildDefaultConfig — enabled when either
|
|
278
|
+
// token source carries credentials. Single truth: same loader the runtime uses.
|
|
279
|
+
export function hasGrokOAuthCredentials() {
|
|
280
|
+
try {
|
|
281
|
+
const tokens = loadTokens();
|
|
282
|
+
return !!(tokens?.access_token && tokens?.refresh_token);
|
|
283
|
+
} catch { return false; }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Write rotated tokens back to the Grok CLI store (~/.grok/auth.json) so the
|
|
287
|
+
// CLI — and any other reader of this single-use refresh-token lineage — picks
|
|
288
|
+
// up the rotation instead of replaying a now-consumed token. Mirrors
|
|
289
|
+
// anthropic-oauth's write-back to ~/.claude/.credentials.json. Best-effort:
|
|
290
|
+
// the own store is the authority, so a failed write-back never breaks a
|
|
291
|
+
// successful refresh. Host-owned file: no secret/mode so we don't re-permission it.
|
|
292
|
+
function _writeBackGrokCliTokens(refreshed) {
|
|
293
|
+
const path = grokCliAuthPath();
|
|
294
|
+
if (!existsSync(path)) return;
|
|
295
|
+
try {
|
|
296
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
297
|
+
const entry = raw?.[`${ISSUER}::${CLIENT_ID}`];
|
|
298
|
+
if (!entry || typeof entry !== 'object') return;
|
|
299
|
+
entry.key = refreshed.access_token;
|
|
300
|
+
entry.refresh_token = refreshed.refresh_token;
|
|
301
|
+
entry.expires_at = new Date(refreshed.expires_at || Date.now()).toISOString();
|
|
302
|
+
// Preserve the host file's existing POSIX mode — writeJsonAtomicSync
|
|
303
|
+
// otherwise defaults the replacement to 0o600, re-permissioning a file
|
|
304
|
+
// the Grok CLI owns.
|
|
305
|
+
let mode;
|
|
306
|
+
try { mode = statSync(path).mode & 0o777; } catch { /* keep helper default */ }
|
|
307
|
+
writeJsonAtomicSync(path, raw, { lock: true, fsyncDir: true, mode });
|
|
308
|
+
} catch (err) {
|
|
309
|
+
process.stderr.write(`[grok-oauth] CLI store write-back failed: ${_scrubTokens(err?.message || String(err)).slice(0, 200)}\n`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let _refreshInFlight = null;
|
|
314
|
+
async function refreshTokens(tokens) {
|
|
315
|
+
if (!tokens?.refresh_token) {
|
|
316
|
+
throw new Error('[grok-oauth] refresh token not available — run the Grok CLI login or the Setup login again');
|
|
317
|
+
}
|
|
318
|
+
const tokenEndpoint = tokens.token_endpoint
|
|
319
|
+
? assertTrustedXaiEndpoint(tokens.token_endpoint, 'token endpoint')
|
|
320
|
+
: (await fetchDiscovery()).token_endpoint;
|
|
321
|
+
const timeout = createTimeoutSignal(null, TOKEN_TIMEOUT_MS, 'grok-oauth refresh');
|
|
322
|
+
try {
|
|
323
|
+
const res = await fetch(tokenEndpoint, {
|
|
324
|
+
method: 'POST',
|
|
325
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
326
|
+
body: new URLSearchParams({
|
|
327
|
+
grant_type: 'refresh_token',
|
|
328
|
+
client_id: CLIENT_ID,
|
|
329
|
+
refresh_token: tokens.refresh_token,
|
|
330
|
+
}),
|
|
331
|
+
// Never follow a redirect on a secret-bearing request: a trusted
|
|
332
|
+
// token endpoint that 307/308-redirects would replay the
|
|
333
|
+
// refresh_token to the redirect target. Fail loud instead.
|
|
334
|
+
redirect: 'error',
|
|
335
|
+
signal: timeout.signal,
|
|
336
|
+
dispatcher: getLlmDispatcher(),
|
|
337
|
+
});
|
|
338
|
+
const text = await res.text();
|
|
339
|
+
let json = null;
|
|
340
|
+
try { json = text ? JSON.parse(text) : null; } catch { /* handled below */ }
|
|
341
|
+
if (!res.ok) {
|
|
342
|
+
// 400/401 (or an explicit invalid_grant/revoked/reused body) means
|
|
343
|
+
// this refresh_token was already consumed by the CLI's single-use
|
|
344
|
+
// lineage. Tag it so refreshTokensWithFallback can adopt the CLI's
|
|
345
|
+
// newer token and retry instead of dead-ending.
|
|
346
|
+
const isInvalidGrant = res.status === 400 || res.status === 401
|
|
347
|
+
|| /invalid_grant|revoked|reused/i.test(text);
|
|
348
|
+
throw Object.assign(
|
|
349
|
+
new Error(`[grok-oauth] token refresh ${res.status}: ${_scrubTokens(text).slice(0, 200)}`),
|
|
350
|
+
{ isInvalidGrant },
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
const accessToken = json?.access_token;
|
|
354
|
+
if (!accessToken) throw new Error('[grok-oauth] token refresh returned no access token');
|
|
355
|
+
const refreshed = {
|
|
356
|
+
access_token: accessToken,
|
|
357
|
+
// xAI rotates refresh tokens; reuse the prior one only when the
|
|
358
|
+
// response omits it (RFC 6749 permits reuse).
|
|
359
|
+
refresh_token: json?.refresh_token || tokens.refresh_token,
|
|
360
|
+
expires_at: typeof json?.expires_in === 'number'
|
|
361
|
+
? Date.now() + json.expires_in * 1000
|
|
362
|
+
: _normalizeExpiresAt(json?.expires_at),
|
|
363
|
+
token_endpoint: tokenEndpoint,
|
|
364
|
+
};
|
|
365
|
+
// Write the CLI store first, own store last: the own store then carries
|
|
366
|
+
// the newest mtime, so ensureAuth's freshest-wins resync treats our own
|
|
367
|
+
// refresh as authoritative and doesn't needlessly flip back to the CLI.
|
|
368
|
+
_writeBackGrokCliTokens(refreshed);
|
|
369
|
+
saveTokens(refreshed);
|
|
370
|
+
return { ...refreshed, source: 'own', mtimeMs: _mtimeMs(getOwnTokenPath()) };
|
|
371
|
+
} finally {
|
|
372
|
+
timeout.cleanup();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// invalid_grant means our refresh_token was already consumed/rotated elsewhere
|
|
377
|
+
// (the Grok CLI shares this single-use lineage). Re-read BOTH on-disk stores
|
|
378
|
+
// and retry once with whichever carries a different (newer) refresh_token —
|
|
379
|
+
// own-store still holds the dead one, so the CLI store is the likely source of
|
|
380
|
+
// the rotation. Mirrors anthropic-oauth's refreshOAuthCredentialsWithFallback.
|
|
381
|
+
async function refreshTokensWithFallback(tokens) {
|
|
382
|
+
try {
|
|
383
|
+
return await refreshTokens(tokens);
|
|
384
|
+
} catch (firstErr) {
|
|
385
|
+
if (!firstErr?.isInvalidGrant) throw firstErr;
|
|
386
|
+
process.stderr.write('[grok-oauth] invalid_grant — re-reading disk, retrying refresh\n');
|
|
387
|
+
// Prefer the freshest store first so we adopt the most recent CLI
|
|
388
|
+
// rotation and never replay an even older stale lineage before it.
|
|
389
|
+
const candidates = [_loadOwnTokens(), _loadGrokCliTokens()].filter(Boolean)
|
|
390
|
+
.sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0));
|
|
391
|
+
const fresh = candidates.find(c => c.refresh_token && c.refresh_token !== tokens.refresh_token);
|
|
392
|
+
if (!fresh) {
|
|
393
|
+
throw new Error('[grok-oauth] refresh token revoked and no newer token on disk — run the Grok CLI login or the Setup login again');
|
|
394
|
+
}
|
|
395
|
+
return await refreshTokens(fresh);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// --- Model catalog cache (24h disk TTL) ---
|
|
400
|
+
function _modelCachePath() {
|
|
401
|
+
return join(getPluginData(), 'grok-oauth-models.json');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function _loadModelCache() {
|
|
405
|
+
const path = _modelCachePath();
|
|
406
|
+
if (!existsSync(path)) return null;
|
|
407
|
+
try {
|
|
408
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
409
|
+
if (!raw?.fetchedAt || !Array.isArray(raw.models)) return null;
|
|
410
|
+
if (Date.now() - raw.fetchedAt > MODEL_CACHE_TTL_MS) return null;
|
|
411
|
+
return raw.models;
|
|
412
|
+
} catch { return null; }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function _saveModelCache(models) {
|
|
416
|
+
try {
|
|
417
|
+
writeJsonAtomicSync(_modelCachePath(), { fetchedAt: Date.now(), models }, { lock: true, fsyncDir: true });
|
|
418
|
+
} catch { /* best-effort */ }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function _normalizeGrokModel(m) {
|
|
422
|
+
const id = m?.id;
|
|
423
|
+
if (!id) return null;
|
|
424
|
+
return {
|
|
425
|
+
id,
|
|
426
|
+
name: id,
|
|
427
|
+
display: id,
|
|
428
|
+
provider: 'grok-oauth',
|
|
429
|
+
family: 'grok',
|
|
430
|
+
tier: 'version',
|
|
431
|
+
latest: false,
|
|
432
|
+
contextWindow: m?.context_window || 0,
|
|
433
|
+
created: typeof m?.created === 'number' ? m.created : null,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Image/video generation ids — excluded from "latest chat model" resolution.
|
|
438
|
+
const NON_CHAT_MODEL_RE = /imagine|image|video/i;
|
|
439
|
+
|
|
440
|
+
function _markLatestGrok(models) {
|
|
441
|
+
let best = null;
|
|
442
|
+
for (const m of models) {
|
|
443
|
+
if (!m?.id || NON_CHAT_MODEL_RE.test(m.id) || isProxyOnlyModel(m.id)) continue;
|
|
444
|
+
if (!best || (Number(m.created) || 0) > (Number(best.created) || 0)) best = m;
|
|
445
|
+
}
|
|
446
|
+
if (best) best.latest = true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Newest chat model by RELEASE DATE (the catalog's `created`), read from the
|
|
450
|
+
// on-disk catalog cache. Deterministic — no version-string guessing (xAI's
|
|
451
|
+
// grok-4.20 actually predates grok-4.3 despite the higher-looking number).
|
|
452
|
+
// Returns null until the catalog is cached; use ensureLatestGrokModel when null.
|
|
453
|
+
export function resolveLatestGrokModel() {
|
|
454
|
+
const cached = _loadModelCache();
|
|
455
|
+
if (!Array.isArray(cached)) return null;
|
|
456
|
+
let best = null;
|
|
457
|
+
for (const m of cached) {
|
|
458
|
+
if (!m?.id || NON_CHAT_MODEL_RE.test(m.id) || isProxyOnlyModel(m.id) || !(Number(m.created) > 0)) continue;
|
|
459
|
+
if (!best || Number(m.created) > Number(best.created)) best = m;
|
|
460
|
+
}
|
|
461
|
+
return best?.id || null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export async function ensureLatestGrokModel(provider) {
|
|
465
|
+
let m = resolveLatestGrokModel();
|
|
466
|
+
if (m) return m;
|
|
467
|
+
await provider._refreshModelCache();
|
|
468
|
+
m = resolveLatestGrokModel();
|
|
469
|
+
if (m) return m;
|
|
470
|
+
throw new Error('[grok-oauth] model catalog unavailable after warmup — cannot resolve default model');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let _modelRefreshInFlight = null;
|
|
474
|
+
|
|
475
|
+
export class GrokOAuthProvider {
|
|
476
|
+
// OpenAI-compatible usage: prompt_tokens includes cached. See registry.mjs.
|
|
477
|
+
static inputExcludesCache = false;
|
|
478
|
+
name = 'grok-oauth';
|
|
479
|
+
config;
|
|
480
|
+
tokens = null;
|
|
481
|
+
_inner = null;
|
|
482
|
+
_innerKey = null;
|
|
483
|
+
|
|
484
|
+
constructor(config) {
|
|
485
|
+
this.config = config || {};
|
|
486
|
+
this.tokens = loadTokens();
|
|
487
|
+
// Warm a kept-alive socket to the xAI inference API so the first
|
|
488
|
+
// request skips the cold TLS handshake. Best-effort; never throws.
|
|
489
|
+
preconnect(INFERENCE_BASE_URL);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async ensureAuth({ forceRefresh = false } = {}) {
|
|
493
|
+
if (!this.tokens) this.tokens = loadTokens();
|
|
494
|
+
if (!this.tokens) {
|
|
495
|
+
throw new Error('[grok-oauth] credentials not found — run the Grok CLI login or the Setup login first');
|
|
496
|
+
}
|
|
497
|
+
// Freshest-wins resync across BOTH stores. The single-use refresh-token
|
|
498
|
+
// lineage is shared with the Grok CLI, so an independent CLI refresh must
|
|
499
|
+
// be adopted proactively — not only reactively on invalid_grant. A disk
|
|
500
|
+
// scan watermark guarantees termination: if the newest file isn't
|
|
501
|
+
// loadable (e.g. a logged-out host file beside a valid own store) we
|
|
502
|
+
// still record the scanned mtime so the same check can't re-fire forever.
|
|
503
|
+
const ownM = _mtimeMs(getOwnTokenPath());
|
|
504
|
+
const cliM = _mtimeMs(grokCliAuthPath());
|
|
505
|
+
const maxM = Math.max(ownM, cliM);
|
|
506
|
+
if (maxM > (this._lastDiskScan || 0) && maxM > (this.tokens.mtimeMs || 0)) {
|
|
507
|
+
const disk = (ownM >= cliM ? _loadOwnTokens() : _loadGrokCliTokens()) || loadTokens();
|
|
508
|
+
if (disk?.access_token) this.tokens = disk;
|
|
509
|
+
this._lastDiskScan = maxM;
|
|
510
|
+
}
|
|
511
|
+
const expiring = this.tokens.expires_at
|
|
512
|
+
&& this.tokens.expires_at < Date.now() + TOKEN_REFRESH_SKEW_MS;
|
|
513
|
+
if (forceRefresh || expiring) {
|
|
514
|
+
if (_refreshInFlight) {
|
|
515
|
+
this.tokens = await _refreshInFlight;
|
|
516
|
+
} else {
|
|
517
|
+
_refreshInFlight = refreshTokensWithFallback(this.tokens)
|
|
518
|
+
.finally(() => { _refreshInFlight = null; });
|
|
519
|
+
this.tokens = await _refreshInFlight;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return this.tokens;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Build (or rebuild on token change) the inner OpenAI-compatible provider
|
|
526
|
+
// that owns request shaping. name 'xai' selects the Responses API path the
|
|
527
|
+
// Grok CLI proxy speaks; baseURL + bearer are overridden for grok-build.
|
|
528
|
+
_ensureInner(token, model) {
|
|
529
|
+
// Proxy-only models (grok-composer*, grok-build) live on the grok-build
|
|
530
|
+
// CLI proxy: different baseURL + the Grok CLI client headers. Everything
|
|
531
|
+
// else stays on api.x.ai. The cache key includes the route so an
|
|
532
|
+
// interleaved sequence of api/proxy calls each get the right inner.
|
|
533
|
+
const proxy = isProxyOnlyModel(model);
|
|
534
|
+
const key = `${proxy ? 'proxy' : 'api'}:${token}`;
|
|
535
|
+
if (this._inner && this._innerKey === key) return this._inner;
|
|
536
|
+
this._inner = new OpenAICompatProvider('xai', {
|
|
537
|
+
...this.config,
|
|
538
|
+
apiKey: token,
|
|
539
|
+
baseURL: proxy ? PROXY_BASE_URL : INFERENCE_BASE_URL,
|
|
540
|
+
// Force the HTTP Responses transport. The default WebSocket path
|
|
541
|
+
// (openai-oauth-ws) is built for the Codex bearer and is unverified
|
|
542
|
+
// with an xAI OAuth token; the HTTP path is proven to work with this
|
|
543
|
+
// token and honors this.client's baseURL.
|
|
544
|
+
responsesTransport: 'http',
|
|
545
|
+
// Proxy-only models additionally need the Grok CLI client headers to
|
|
546
|
+
// clear the proxy version gate (HTTP 426 otherwise).
|
|
547
|
+
...(proxy ? { extraHeaders: proxyHeaders() } : {}),
|
|
548
|
+
});
|
|
549
|
+
this._innerKey = key;
|
|
550
|
+
return this._inner;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async send(messages, model, tools, sendOpts) {
|
|
554
|
+
const useModel = normalizeGrokModelId(
|
|
555
|
+
model || await ensureLatestGrokModel(this),
|
|
556
|
+
);
|
|
557
|
+
const tokens = await this.ensureAuth();
|
|
558
|
+
const inner = this._ensureInner(tokens.access_token, useModel);
|
|
559
|
+
try {
|
|
560
|
+
// Call _doSend directly, bypassing OpenAICompatProvider.send()'s
|
|
561
|
+
// own 401 handler — that one reloads a static apiKey from config,
|
|
562
|
+
// which is wrong for OAuth. We own the refresh-and-retry below.
|
|
563
|
+
// Caller's model passes through (or catalog-top default), with
|
|
564
|
+
// retired aliases normalized. api.x.ai exposes the full grok-4.x
|
|
565
|
+
// catalog to this token — no single-model lock.
|
|
566
|
+
return await inner._doSend(messages, useModel, tools, sendOpts);
|
|
567
|
+
} catch (err) {
|
|
568
|
+
// Refresh-and-retry only on 401 (stale/expired access token).
|
|
569
|
+
// Resolve the status from the structured field (falling back to the
|
|
570
|
+
// shared classifier that derives it from the error text) rather than
|
|
571
|
+
// ad-hoc string matching. A 403 is an entitlement signal (the
|
|
572
|
+
// account's tier lacks the model) — refreshing the same grant can't
|
|
573
|
+
// fix it, so it must surface unretried.
|
|
574
|
+
populateHttpStatusFromMessage(err);
|
|
575
|
+
if (Number(err?.httpStatus || err?.status) === 401) {
|
|
576
|
+
process.stderr.write('[grok-oauth] 401, force-refreshing token...\n');
|
|
577
|
+
const fresh = await this.ensureAuth({ forceRefresh: true });
|
|
578
|
+
const retryInner = this._ensureInner(fresh.access_token, useModel);
|
|
579
|
+
return await retryInner._doSend(messages, useModel, tools, sendOpts);
|
|
580
|
+
}
|
|
581
|
+
throw err;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async _fetchModelItems() {
|
|
586
|
+
const tokens = await this.ensureAuth();
|
|
587
|
+
const timeout = createTimeoutSignal(null, 10_000, 'grok-oauth model list');
|
|
588
|
+
try {
|
|
589
|
+
const res = await fetch(`${INFERENCE_BASE_URL}/models`, {
|
|
590
|
+
method: 'GET',
|
|
591
|
+
headers: { Authorization: `Bearer ${tokens.access_token}` },
|
|
592
|
+
// Bearer-bearing request — refuse redirects so the access token
|
|
593
|
+
// is never replayed to a redirect target.
|
|
594
|
+
redirect: 'error',
|
|
595
|
+
signal: timeout.signal,
|
|
596
|
+
dispatcher: getLlmDispatcher(),
|
|
597
|
+
});
|
|
598
|
+
if (!res.ok) throw new Error(`models ${res.status}`);
|
|
599
|
+
const data = await res.json();
|
|
600
|
+
if (!Array.isArray(data?.data)) {
|
|
601
|
+
throw new Error('[grok-oauth] unexpected /models response shape (no data[])');
|
|
602
|
+
}
|
|
603
|
+
return data.data;
|
|
604
|
+
} finally {
|
|
605
|
+
timeout.cleanup();
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// The grok-build proxy catalog (grok-build, grok-composer-2.5-fast). /models
|
|
610
|
+
// is readable with the bare bearer + the Grok CLI client headers. Best-effort:
|
|
611
|
+
// a proxy hiccup must NOT break the api.x.ai catalog, so failures return [].
|
|
612
|
+
async _fetchProxyModelItems() {
|
|
613
|
+
let tokens;
|
|
614
|
+
try { tokens = await this.ensureAuth(); } catch { return []; }
|
|
615
|
+
const timeout = createTimeoutSignal(null, 10_000, 'grok-oauth proxy model list');
|
|
616
|
+
try {
|
|
617
|
+
const res = await fetch(`${PROXY_BASE_URL}/models`, {
|
|
618
|
+
method: 'GET',
|
|
619
|
+
headers: { Authorization: `Bearer ${tokens.access_token}`, ...proxyHeaders() },
|
|
620
|
+
redirect: 'error',
|
|
621
|
+
signal: timeout.signal,
|
|
622
|
+
dispatcher: getLlmDispatcher(),
|
|
623
|
+
});
|
|
624
|
+
if (!res.ok) return [];
|
|
625
|
+
const data = await res.json();
|
|
626
|
+
return Array.isArray(data?.data) ? data.data : [];
|
|
627
|
+
} catch {
|
|
628
|
+
return [];
|
|
629
|
+
} finally {
|
|
630
|
+
timeout.cleanup();
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Merge api.x.ai ∪ grok-build proxy catalogs, deduped by id (api wins on
|
|
635
|
+
// overlap). The proxy contributes grok-build and grok-composer-2.5-fast,
|
|
636
|
+
// which api.x.ai does not publish. api.x.ai failures still propagate (the
|
|
637
|
+
// primary catalog); proxy failures are swallowed best-effort above.
|
|
638
|
+
async _fetchAllModelItems() {
|
|
639
|
+
const [apiItems, proxyItems] = await Promise.all([
|
|
640
|
+
this._fetchModelItems(),
|
|
641
|
+
this._fetchProxyModelItems(),
|
|
642
|
+
]);
|
|
643
|
+
const byId = new Map();
|
|
644
|
+
for (const m of proxyItems) if (m?.id) byId.set(m.id, m);
|
|
645
|
+
for (const m of apiItems) if (m?.id) byId.set(m.id, m);
|
|
646
|
+
return [...byId.values()];
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async listModels() {
|
|
650
|
+
const cached = _loadModelCache();
|
|
651
|
+
if (cached) return cached;
|
|
652
|
+
// No swallow-to-[] fallback. Catalog/auth failures propagate to the
|
|
653
|
+
// caller (registry warmup + setup model listing), both of which already
|
|
654
|
+
// wrap this in their own catch.
|
|
655
|
+
const items = await this._fetchAllModelItems();
|
|
656
|
+
const normalized = items.map(_normalizeGrokModel).filter(Boolean);
|
|
657
|
+
_markLatestGrok(normalized);
|
|
658
|
+
const enriched = await enrichModels(normalized);
|
|
659
|
+
_saveModelCache(enriched);
|
|
660
|
+
return enriched;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async _refreshModelCache() {
|
|
664
|
+
if (_modelRefreshInFlight) return _modelRefreshInFlight;
|
|
665
|
+
_modelRefreshInFlight = (async () => {
|
|
666
|
+
try {
|
|
667
|
+
const items = await this._fetchAllModelItems();
|
|
668
|
+
const normalized = items.map(_normalizeGrokModel).filter(Boolean);
|
|
669
|
+
_markLatestGrok(normalized);
|
|
670
|
+
const enriched = await enrichModels(normalized);
|
|
671
|
+
_saveModelCache(enriched);
|
|
672
|
+
process.stderr.write(`[grok-oauth] catalog refreshed (${enriched.length} models)\n`);
|
|
673
|
+
return enriched;
|
|
674
|
+
} catch (err) {
|
|
675
|
+
process.stderr.write(`[grok-oauth] catalog refresh failed (${err.message})\n`);
|
|
676
|
+
return null;
|
|
677
|
+
} finally {
|
|
678
|
+
_modelRefreshInFlight = null;
|
|
679
|
+
}
|
|
680
|
+
})();
|
|
681
|
+
return _modelRefreshInFlight;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async isAvailable() {
|
|
685
|
+
return this.tokens !== null || loadTokens() !== null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// --- Login flow (PKCE, export for CLI / setup use) ---
|
|
690
|
+
function generatePKCE() {
|
|
691
|
+
const verifier = randomBytes(32).toString('base64url');
|
|
692
|
+
const challenge = createHash('sha256').update(verifier).digest('base64url');
|
|
693
|
+
return { verifier, challenge };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export async function loginOAuth() {
|
|
697
|
+
const discovery = await fetchDiscovery();
|
|
698
|
+
const pkce = generatePKCE();
|
|
699
|
+
const state = randomBytes(16).toString('hex');
|
|
700
|
+
const nonce = randomBytes(16).toString('hex');
|
|
701
|
+
const url = new URL(discovery.authorization_endpoint);
|
|
702
|
+
url.searchParams.set('response_type', 'code');
|
|
703
|
+
url.searchParams.set('client_id', CLIENT_ID);
|
|
704
|
+
url.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
705
|
+
url.searchParams.set('scope', SCOPE);
|
|
706
|
+
url.searchParams.set('state', state);
|
|
707
|
+
url.searchParams.set('nonce', nonce);
|
|
708
|
+
url.searchParams.set('code_challenge', pkce.challenge);
|
|
709
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
710
|
+
url.searchParams.set('plan', 'generic');
|
|
711
|
+
url.searchParams.set('referrer', 'mixdog');
|
|
712
|
+
process.stderr.write(`\n[grok-oauth] Open this URL to log in (consent shows as "Grok Build"):\n${url.toString()}\n\n`);
|
|
713
|
+
try {
|
|
714
|
+
const { exec } = await import('child_process');
|
|
715
|
+
const opener = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
716
|
+
exec(`${opener} "${url.toString()}"`, { windowsHide: true });
|
|
717
|
+
} catch { /* user opens manually */ }
|
|
718
|
+
|
|
719
|
+
return new Promise((resolve) => {
|
|
720
|
+
const timeout = setTimeout(() => { server.close(); resolve(null); }, LOGIN_TIMEOUT_MS);
|
|
721
|
+
const server = createServer(async (req, res) => {
|
|
722
|
+
const u = new URL(req.url || '/', `http://${CALLBACK_HOST}:${CALLBACK_PORT}`);
|
|
723
|
+
if (u.pathname !== CALLBACK_PATH) {
|
|
724
|
+
res.writeHead(404);
|
|
725
|
+
res.end();
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const code = u.searchParams.get('code');
|
|
729
|
+
if (!code || u.searchParams.get('state') !== state) {
|
|
730
|
+
res.writeHead(400);
|
|
731
|
+
res.end('Invalid');
|
|
732
|
+
clearTimeout(timeout);
|
|
733
|
+
server.close();
|
|
734
|
+
resolve(null);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
738
|
+
res.end('<html><body><h2>Grok login successful! You can close this tab.</h2></body></html>');
|
|
739
|
+
clearTimeout(timeout);
|
|
740
|
+
server.close();
|
|
741
|
+
try {
|
|
742
|
+
const tokenRes = await fetch(discovery.token_endpoint, {
|
|
743
|
+
method: 'POST',
|
|
744
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
745
|
+
body: new URLSearchParams({
|
|
746
|
+
grant_type: 'authorization_code',
|
|
747
|
+
client_id: CLIENT_ID,
|
|
748
|
+
code,
|
|
749
|
+
code_verifier: pkce.verifier,
|
|
750
|
+
redirect_uri: REDIRECT_URI,
|
|
751
|
+
// xAI re-validates the PKCE challenge at token exchange
|
|
752
|
+
// (not just the verifier), so echo it back. Omitting
|
|
753
|
+
// these makes the exchange fail. Matches the Grok CLI.
|
|
754
|
+
code_challenge: pkce.challenge,
|
|
755
|
+
code_challenge_method: 'S256',
|
|
756
|
+
}),
|
|
757
|
+
// Secret-bearing (authorization code + verifier): refuse
|
|
758
|
+
// redirects so they can't be replayed to an untrusted host.
|
|
759
|
+
redirect: 'error',
|
|
760
|
+
signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
|
|
761
|
+
});
|
|
762
|
+
if (!tokenRes.ok) { resolve(null); return; }
|
|
763
|
+
const json = await tokenRes.json();
|
|
764
|
+
if (!json.access_token || !json.refresh_token) { resolve(null); return; }
|
|
765
|
+
const tokens = {
|
|
766
|
+
access_token: json.access_token,
|
|
767
|
+
refresh_token: json.refresh_token,
|
|
768
|
+
expires_at: typeof json.expires_in === 'number'
|
|
769
|
+
? Date.now() + json.expires_in * 1000
|
|
770
|
+
: _normalizeExpiresAt(json.expires_at),
|
|
771
|
+
token_endpoint: discovery.token_endpoint,
|
|
772
|
+
};
|
|
773
|
+
saveTokens(tokens);
|
|
774
|
+
resolve(tokens);
|
|
775
|
+
} catch {
|
|
776
|
+
resolve(null);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
server.listen(CALLBACK_PORT, CALLBACK_HOST);
|
|
780
|
+
server.on('error', () => { clearTimeout(timeout); resolve(null); });
|
|
781
|
+
});
|
|
782
|
+
}
|