mixdog 0.8.0 → 0.9.0

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.
Files changed (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +344 -0
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1223 -95
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -3,10 +3,7 @@
3
3
  *
4
4
  * Authenticates against xAI's shared OAuth client via PKCE (discovery at
5
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.
6
+ * Mixdog's own token store (grok-oauth.json).
10
7
  *
11
8
  * Inference + catalog merge two sources, routed per model:
12
9
  * - api.x.ai/v1 (default): grok-4.x chat models and the web_search backend.
@@ -24,7 +21,6 @@ import { createServer } from 'http';
24
21
  import { randomBytes, createHash } from 'crypto';
25
22
  import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs';
26
23
  import { join } from 'path';
27
- import { homedir } from 'os';
28
24
  import { getPluginData } from '../config.mjs';
29
25
  import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
30
26
  import { enrichModels, getModelMetadataSync } from './model-catalog.mjs';
@@ -71,22 +67,11 @@ function isProxyOnlyModel(model) {
71
67
  return /^grok-composer/i.test(m) || PROXY_EXACT_MODELS.has(m);
72
68
  }
73
69
 
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.
70
+ // Use a Mixdog-controlled client version for the proxy version gate.
78
71
  let _grokCliVersionCache = null;
79
72
  function grokCliVersion() {
80
73
  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;
74
+ _grokCliVersionCache = String(process.env.MIXDOG_GROK_CLIENT_VERSION || '').trim() || GROK_CLI_VERSION_FALLBACK;
90
75
  return _grokCliVersionCache;
91
76
  }
92
77
 
@@ -132,11 +117,6 @@ const DISCOVERY_TIMEOUT_MS = 15_000;
132
117
  const TOKEN_TIMEOUT_MS = 30_000;
133
118
  const LOGIN_TIMEOUT_MS = 5 * 60_000;
134
119
 
135
- // Grok CLI credential file. Composite top-level key is "<issuer>::<client_id>".
136
- function grokCliAuthPath() {
137
- return join(homedir(), '.grok', 'auth.json');
138
- }
139
-
140
120
  // SSRF guard for any endpoint pulled from the discovery document or saved
141
121
  // tokens. xAI OAuth endpoints must be https on x.ai / *.x.ai — reject
142
122
  // anything else outright so a hostile discovery response can't redirect the
@@ -189,8 +169,8 @@ function getOwnTokenPath() {
189
169
  return join(dir, 'grok-oauth.json');
190
170
  }
191
171
 
192
- // expires_at may arrive as a unix number (own store) or an ISO-8601 string
193
- // (Grok CLI auth.json). Normalize both to epoch milliseconds; 0 means unknown.
172
+ // expires_at may arrive as a unix number or an ISO-8601 string. Normalize both
173
+ // to epoch milliseconds; 0 means unknown.
194
174
  function _normalizeExpiresAt(value) {
195
175
  if (typeof value === 'string') {
196
176
  const ms = Date.parse(value);
@@ -215,6 +195,21 @@ function _expiryFromAccessToken(token) {
215
195
  } catch { return 0; }
216
196
  }
217
197
 
198
+ function _identityFromAccessToken(token) {
199
+ try {
200
+ const parts = String(token || '').split('.');
201
+ if (parts.length !== 3) return {};
202
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));
203
+ const userId = payload?.user_id
204
+ || payload?.userId
205
+ || payload?.principal_id
206
+ || payload?.principalId
207
+ || payload?.sub
208
+ || '';
209
+ return userId ? { user_id: String(userId) } : {};
210
+ } catch { return {}; }
211
+ }
212
+
218
213
  function _mtimeMs(path) {
219
214
  try { return statSync(path).mtimeMs; } catch { return 0; }
220
215
  }
@@ -232,42 +227,16 @@ function _loadOwnTokens() {
232
227
  refresh_token: raw.refresh_token,
233
228
  expires_at: _normalizeExpiresAt(raw.expires_at ?? raw.expiresAt) || _expiryFromAccessToken(raw.access_token),
234
229
  token_endpoint: raw.token_endpoint || null,
230
+ user_id: raw.user_id || raw.userId || _identityFromAccessToken(raw.access_token).user_id || '',
235
231
  source: 'own',
236
232
  mtimeMs: _mtimeMs(path),
237
233
  };
238
234
  } catch { return null; }
239
235
  }
240
236
 
241
- // Grok CLI store (~/.grok/auth.json). Read-only seed: the access token lives
242
- // under `key`, keyed by "<issuer>::<client_id>". We never write back here —
243
- // after the first refresh mixdog manages its own copy, mirroring how
244
- // openai-oauth treats ~/.codex/auth.json.
245
- function _loadGrokCliTokens() {
246
- const path = grokCliAuthPath();
247
- if (!existsSync(path)) return null;
248
- try {
249
- const raw = JSON.parse(readFileSync(path, 'utf-8'));
250
- if (!raw || typeof raw !== 'object') return null;
251
- // The Grok CLI keys every entry by "<issuer>::<client_id>" — look up
252
- // exactly that. No scan-for-matching-client_id fallback: a different
253
- // issuer under the same client_id is a different account/endpoint and
254
- // must not be silently selected.
255
- const entry = raw[`${ISSUER}::${CLIENT_ID}`];
256
- if (!entry?.key || !entry?.refresh_token) return null;
257
- return {
258
- access_token: entry.key,
259
- refresh_token: entry.refresh_token,
260
- expires_at: _normalizeExpiresAt(entry.expires_at) || _expiryFromAccessToken(entry.key),
261
- token_endpoint: null,
262
- source: 'grok-cli',
263
- mtimeMs: _mtimeMs(path),
264
- };
265
- } catch { return null; }
266
- }
267
-
268
- // Own store first (accurate expires_at). Fall back to the Grok CLI login.
237
+ // Mixdog-owned token store only.
269
238
  function loadTokens() {
270
- return _loadOwnTokens() || _loadGrokCliTokens();
239
+ return _loadOwnTokens();
271
240
  }
272
241
 
273
242
  function saveTokens(tokens) {
@@ -276,6 +245,7 @@ function saveTokens(tokens) {
276
245
  refresh_token: tokens.refresh_token,
277
246
  expires_at: tokens.expires_at || 0,
278
247
  token_endpoint: tokens.token_endpoint || null,
248
+ user_id: tokens.user_id || tokens.userId || _identityFromAccessToken(tokens.access_token).user_id || undefined,
279
249
  }, { lock: true, fsyncDir: true });
280
250
  }
281
251
 
@@ -287,8 +257,8 @@ function _scrubTokens(text) {
287
257
  .replace(/"key"\s*:\s*"[^"]+"/g, '"key":"[REDACTED]"');
288
258
  }
289
259
 
290
- // Public predicate used by config.buildDefaultConfig — enabled when either
291
- // token source carries credentials. Single truth: same loader the runtime uses.
260
+ // Public predicate used by config.buildDefaultConfig — enabled when Mixdog's
261
+ // token store carries credentials. Single truth: same loader the runtime uses.
292
262
  export function hasGrokOAuthCredentials() {
293
263
  try {
294
264
  const tokens = loadTokens();
@@ -300,17 +270,13 @@ export function describeGrokOAuthCredentials() {
300
270
  try {
301
271
  const tokens = loadTokens();
302
272
  if (!tokens?.access_token) {
303
- return { authenticated: false, status: 'Not Set', detail: '~/.grok/auth.json or mixdog token store' };
273
+ return { authenticated: false, status: 'Not Set', detail: 'Mixdog token store' };
304
274
  }
305
275
  const hasRefresh = Boolean(tokens.refresh_token);
306
276
  const expiresAt = _normalizeExpiresAt(tokens.expires_at);
307
277
  const expiring = expiresAt > 0 && expiresAt < Date.now() + TOKEN_REFRESH_SKEW_MS;
308
278
  const expired = expiresAt > 0 && expiresAt <= Date.now();
309
- const detail = tokens.source === 'own'
310
- ? 'Mixdog token store'
311
- : tokens.source === 'grok-cli'
312
- ? '~/.grok/auth.json'
313
- : (tokens.source || 'oauth');
279
+ const detail = tokens.source === 'own' ? 'Mixdog token store' : (tokens.source || 'oauth');
314
280
  if (!hasRefresh) {
315
281
  return {
316
282
  authenticated: expiresAt === 0 || !expired,
@@ -334,54 +300,13 @@ export function forgetGrokOAuthCredentials() {
334
300
  unlinkSync(ownPath);
335
301
  removed = true;
336
302
  }
337
- const cliPath = grokCliAuthPath();
338
- if (existsSync(cliPath)) {
339
- try {
340
- const raw = JSON.parse(readFileSync(cliPath, 'utf-8'));
341
- const key = `${ISSUER}::${CLIENT_ID}`;
342
- if (raw?.[key]) {
343
- delete raw[key];
344
- writeJsonAtomicSync(cliPath, raw, { lock: true, fsyncDir: true });
345
- removed = true;
346
- }
347
- } catch (err) {
348
- throw new Error(`Grok OAuth reset failed for ${cliPath}: ${err?.message || err}`);
349
- }
350
- }
351
303
  return { removed };
352
304
  }
353
305
 
354
- // Write rotated tokens back to the Grok CLI store (~/.grok/auth.json) so the
355
- // CLI — and any other reader of this single-use refresh-token lineage — picks
356
- // up the rotation instead of replaying a now-consumed token. Mirrors
357
- // anthropic-oauth's credential write-back. Best-effort:
358
- // the own store is the authority, so a failed write-back never breaks a
359
- // successful refresh. Host-owned file: no secret/mode so we don't re-permission it.
360
- function _writeBackGrokCliTokens(refreshed) {
361
- const path = grokCliAuthPath();
362
- if (!existsSync(path)) return;
363
- try {
364
- const raw = JSON.parse(readFileSync(path, 'utf-8'));
365
- const entry = raw?.[`${ISSUER}::${CLIENT_ID}`];
366
- if (!entry || typeof entry !== 'object') return;
367
- entry.key = refreshed.access_token;
368
- entry.refresh_token = refreshed.refresh_token;
369
- entry.expires_at = new Date(refreshed.expires_at || Date.now()).toISOString();
370
- // Preserve the host file's existing POSIX mode — writeJsonAtomicSync
371
- // otherwise defaults the replacement to 0o600, re-permissioning a file
372
- // the Grok CLI owns.
373
- let mode;
374
- try { mode = statSync(path).mode & 0o777; } catch { /* keep helper default */ }
375
- writeJsonAtomicSync(path, raw, { lock: true, fsyncDir: true, mode });
376
- } catch (err) {
377
- process.stderr.write(`[grok-oauth] CLI store write-back failed: ${_scrubTokens(err?.message || String(err)).slice(0, 200)}\n`);
378
- }
379
- }
380
-
381
306
  let _refreshInFlight = null;
382
307
  async function refreshTokens(tokens) {
383
308
  if (!tokens?.refresh_token) {
384
- throw new Error('[grok-oauth] refresh token not available — run the Grok CLI login or the Setup login again');
309
+ throw new Error('[grok-oauth] refresh token not available — open /providers in mixdog to sign in again');
385
310
  }
386
311
  const tokenEndpoint = tokens.token_endpoint
387
312
  ? assertTrustedXaiEndpoint(tokens.token_endpoint, 'token endpoint')
@@ -408,9 +333,7 @@ async function refreshTokens(tokens) {
408
333
  try { json = text ? JSON.parse(text) : null; } catch { /* handled below */ }
409
334
  if (!res.ok) {
410
335
  // 400/401 (or an explicit invalid_grant/revoked/reused body) means
411
- // this refresh_token was already consumed by the CLI's single-use
412
- // lineage. Tag it so refreshTokensWithFallback can adopt the CLI's
413
- // newer token and retry instead of dead-ending.
336
+ // this refresh_token was revoked or already consumed.
414
337
  const isInvalidGrant = res.status === 400 || res.status === 401
415
338
  || /invalid_grant|revoked|reused/i.test(text);
416
339
  throw Object.assign(
@@ -429,11 +352,8 @@ async function refreshTokens(tokens) {
429
352
  ? Date.now() + json.expires_in * 1000
430
353
  : _normalizeExpiresAt(json?.expires_at),
431
354
  token_endpoint: tokenEndpoint,
355
+ user_id: tokens.user_id || tokens.userId || _identityFromAccessToken(accessToken).user_id || '',
432
356
  };
433
- // Write the CLI store first, own store last: the own store then carries
434
- // the newest mtime, so ensureAuth's freshest-wins resync treats our own
435
- // refresh as authoritative and doesn't needlessly flip back to the CLI.
436
- _writeBackGrokCliTokens(refreshed);
437
357
  saveTokens(refreshed);
438
358
  return { ...refreshed, source: 'own', mtimeMs: _mtimeMs(getOwnTokenPath()) };
439
359
  } finally {
@@ -441,29 +361,6 @@ async function refreshTokens(tokens) {
441
361
  }
442
362
  }
443
363
 
444
- // invalid_grant means our refresh_token was already consumed/rotated elsewhere
445
- // (the Grok CLI shares this single-use lineage). Re-read BOTH on-disk stores
446
- // and retry once with whichever carries a different (newer) refresh_token —
447
- // own-store still holds the dead one, so the CLI store is the likely source of
448
- // the rotation. Mirrors anthropic-oauth's refreshOAuthCredentialsWithFallback.
449
- async function refreshTokensWithFallback(tokens) {
450
- try {
451
- return await refreshTokens(tokens);
452
- } catch (firstErr) {
453
- if (!firstErr?.isInvalidGrant) throw firstErr;
454
- process.stderr.write('[grok-oauth] invalid_grant — re-reading disk, retrying refresh\n');
455
- // Prefer the freshest store first so we adopt the most recent CLI
456
- // rotation and never replay an even older stale lineage before it.
457
- const candidates = [_loadOwnTokens(), _loadGrokCliTokens()].filter(Boolean)
458
- .sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0));
459
- const fresh = candidates.find(c => c.refresh_token && c.refresh_token !== tokens.refresh_token);
460
- if (!fresh) {
461
- throw new Error('[grok-oauth] refresh token revoked and no newer token on disk — run the Grok CLI login or the Setup login again');
462
- }
463
- return await refreshTokens(fresh);
464
- }
465
- }
466
-
467
364
  // --- Model catalog cache (24h disk TTL) ---
468
365
  const _modelCache = makeModelCache({ fileName: 'grok-oauth-models.json', ttlMs: MODEL_CACHE_TTL_MS });
469
366
  const PROXY_MODEL_METADATA = {
@@ -624,21 +521,16 @@ export class GrokOAuthProvider {
624
521
  async ensureAuth({ forceRefresh = false } = {}) {
625
522
  if (!this.tokens) this.tokens = loadTokens();
626
523
  if (!this.tokens) {
627
- throw new Error('[grok-oauth] credentials not found — run the Grok CLI login or the Setup login first');
524
+ throw new Error('[grok-oauth] credentials not found — open /providers in mixdog to sign in first');
628
525
  }
629
- // Freshest-wins resync across BOTH stores. The single-use refresh-token
630
- // lineage is shared with the Grok CLI, so an independent CLI refresh must
631
- // be adopted proactively — not only reactively on invalid_grant. A disk
632
- // scan watermark guarantees termination: if the newest file isn't
633
- // loadable (e.g. a logged-out host file beside a valid own store) we
634
- // still record the scanned mtime so the same check can't re-fire forever.
526
+ // Pick up Mixdog-owned token updates without touching external CLI
527
+ // stores. The scan watermark guarantees the same unreadable write does
528
+ // not re-fire forever.
635
529
  const ownM = _mtimeMs(getOwnTokenPath());
636
- const cliM = _mtimeMs(grokCliAuthPath());
637
- const maxM = Math.max(ownM, cliM);
638
- if (maxM > (this._lastDiskScan || 0) && maxM > (this.tokens.mtimeMs || 0)) {
639
- const disk = (ownM >= cliM ? _loadOwnTokens() : _loadGrokCliTokens()) || loadTokens();
530
+ if (ownM > (this._lastDiskScan || 0) && ownM > (this.tokens.mtimeMs || 0)) {
531
+ const disk = _loadOwnTokens();
640
532
  if (disk?.access_token) this.tokens = disk;
641
- this._lastDiskScan = maxM;
533
+ this._lastDiskScan = ownM;
642
534
  }
643
535
  const expiring = this.tokens.expires_at
644
536
  && this.tokens.expires_at < Date.now() + TOKEN_REFRESH_SKEW_MS;
@@ -646,7 +538,7 @@ export class GrokOAuthProvider {
646
538
  if (_refreshInFlight) {
647
539
  this.tokens = await _refreshInFlight;
648
540
  } else {
649
- _refreshInFlight = refreshTokensWithFallback(this.tokens)
541
+ _refreshInFlight = refreshTokens(this.tokens)
650
542
  .finally(() => { _refreshInFlight = null; });
651
543
  this.tokens = await _refreshInFlight;
652
544
  }
@@ -683,6 +575,9 @@ export class GrokOAuthProvider {
683
575
  }
684
576
 
685
577
  async send(messages, model, tools, sendOpts) {
578
+ // Re-warm a kept-alive socket before the turn (TTL-gated no-op while
579
+ // hot) so a post-idle request skips the cold TLS handshake.
580
+ preconnect(INFERENCE_BASE_URL);
686
581
  const useModel = normalizeGrokModelId(
687
582
  model || await ensureLatestGrokModel(this),
688
583
  );
@@ -867,11 +762,32 @@ async function exchangeAuthorizationCode({ discovery, pkce, code }) {
867
762
  ? Date.now() + json.expires_in * 1000
868
763
  : _normalizeExpiresAt(json.expires_at),
869
764
  token_endpoint: discovery.token_endpoint,
765
+ user_id: _identityFromAccessToken(json.access_token).user_id || '',
870
766
  };
871
767
  saveTokens(tokens);
872
768
  return tokens;
873
769
  }
874
770
 
771
+ function parseOAuthCodeInput(input) {
772
+ const value = String(input || '').trim();
773
+ if (!value) return { code: '', state: '' };
774
+ try {
775
+ const url = new URL(value);
776
+ const code = url.searchParams.get('code') || '';
777
+ const state = url.searchParams.get('state') || '';
778
+ if (code || state) return { code, state };
779
+ } catch { /* not a URL */ }
780
+ if (value.includes('#')) {
781
+ const [code, state] = value.split('#', 2);
782
+ return { code: String(code || '').trim(), state: String(state || '').trim() };
783
+ }
784
+ if (value.includes('code=')) {
785
+ const params = new URLSearchParams(value.startsWith('?') ? value.slice(1) : value);
786
+ return { code: params.get('code') || '', state: params.get('state') || '' };
787
+ }
788
+ return { code: value, state: '' };
789
+ }
790
+
875
791
  export async function beginOAuthLogin() {
876
792
  const discovery = await fetchDiscovery();
877
793
  const pkce = generatePKCE();
@@ -942,8 +858,10 @@ export async function beginOAuthLogin() {
942
858
  provider: 'grok-oauth',
943
859
  url: url.toString(),
944
860
  waitForCallback,
945
- completeCode: async (code) => {
946
- const tokens = await exchangeAuthorizationCode({ discovery, pkce, code });
861
+ completeCode: async (input) => {
862
+ const parsed = parseOAuthCodeInput(input);
863
+ if (parsed.state && parsed.state !== state) throw new Error('[grok-oauth] OAuth state mismatch');
864
+ const tokens = await exchangeAuthorizationCode({ discovery, pkce, code: parsed.code });
947
865
  finish?.(tokens);
948
866
  return tokens;
949
867
  },
@@ -17,6 +17,14 @@ function imageInfo(block) {
17
17
  return null;
18
18
  }
19
19
 
20
+ function geminiInlineInfo(block) {
21
+ if (!block || typeof block !== 'object') return null;
22
+ const inline = block.inlineData || block.inline_data;
23
+ const data = inline?.data;
24
+ if (typeof data !== 'string' || !data) return null;
25
+ return { data, mimeType: cleanMimeType(inline.mimeType || inline.mime_type || inline.mediaType || inline.media_type) };
26
+ }
27
+
20
28
  function imageUrlFromPart(block) {
21
29
  if (!block || typeof block !== 'object') return null;
22
30
  if (block.type === 'image_url') {
@@ -29,16 +37,48 @@ function imageUrlFromPart(block) {
29
37
  if (typeof value === 'string') return value;
30
38
  if (value && typeof value.url === 'string') return value.url;
31
39
  }
40
+ if (block.type === 'image' && block.source?.type === 'url' && typeof block.source.url === 'string') {
41
+ return block.source.url;
42
+ }
32
43
  const info = imageInfo(block);
33
44
  return info ? `data:${info.mimeType};base64,${info.data}` : null;
34
45
  }
35
46
 
47
+ function imageFileUriFromPart(block) {
48
+ if (!block || typeof block !== 'object') return null;
49
+ const fileData = block.fileData || block.file_data;
50
+ const fileUri = fileData?.fileUri || fileData?.file_uri;
51
+ if (typeof fileUri === 'string' && fileUri) {
52
+ return { fileUri, mimeType: cleanMimeType(fileData.mimeType || fileData.mime_type || fileData.mediaType || fileData.media_type) };
53
+ }
54
+ if (block.type === 'image' && typeof block.uri === 'string' && block.uri) {
55
+ return { fileUri: block.uri, mimeType: cleanMimeType(block.mime_type || block.mimeType || block.media_type || block.mediaType) };
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function imageFileIdFromPart(block) {
61
+ if (!block || typeof block !== 'object') return null;
62
+ if (block.type === 'input_image' && typeof block.file_id === 'string' && block.file_id) {
63
+ return block.file_id;
64
+ }
65
+ if (block.type === 'image' && block.source?.type === 'file' && typeof block.source.file_id === 'string' && block.source.file_id) {
66
+ return block.source.file_id;
67
+ }
68
+ return null;
69
+ }
70
+
36
71
  function imageInfoFromDataUrl(url) {
37
72
  const m = String(url || '').match(/^data:(image\/[a-z0-9.+_-]+);base64,(.+)$/is);
38
73
  if (!m) return null;
39
74
  return { mimeType: cleanMimeType(m[1]), data: m[2] };
40
75
  }
41
76
 
77
+ function imageMimeFromDataUrl(url) {
78
+ const m = String(url || '').match(/^data:(image\/[a-z0-9.+_-]+);base64,/i);
79
+ return m ? cleanMimeType(m[1]) : null;
80
+ }
81
+
42
82
  function textFromPart(block) {
43
83
  if (typeof block === 'string') return block;
44
84
  if (!block || typeof block !== 'object') return '';
@@ -63,14 +103,14 @@ function jsonFallbackFromPart(block) {
63
103
  const text = textFromPart(block);
64
104
  if (text) return text;
65
105
  if (!block || typeof block !== 'object') return block == null ? '' : String(block);
66
- if (imageUrlFromPart(block)) return '';
106
+ if (imageUrlFromPart(block) || imageFileIdFromPart(block) || imageFileUriFromPart(block) || geminiInlineInfo(block)) return '';
67
107
  return stringifyFallback(block);
68
108
  }
69
109
 
70
110
  export function contentHasImage(content) {
71
111
  const parts = contentParts(content);
72
112
  if (!parts) return false;
73
- return parts.some((part) => !!imageUrlFromPart(part));
113
+ return parts.some((part) => !!imageUrlFromPart(part) || !!imageFileIdFromPart(part) || !!imageFileUriFromPart(part) || !!geminiInlineInfo(part));
74
114
  }
75
115
 
76
116
  export function contentToText(content, fallback = '') {
@@ -81,6 +121,41 @@ export function contentToText(content, fallback = '') {
81
121
  return text || fallback;
82
122
  }
83
123
 
124
+ function storedHistoryImagePlaceholder(part) {
125
+ const info = imageInfo(part) || geminiInlineInfo(part);
126
+ const url = imageUrlFromPart(part);
127
+ const fileUri = imageFileUriFromPart(part);
128
+ const mimeType = info?.mimeType || imageMimeFromDataUrl(url) || fileUri?.mimeType || (part?.type === 'image' ? DEFAULT_IMAGE_MIME : '');
129
+ return `[Image omitted from stored history${mimeType ? `: ${mimeType}` : ''}]`;
130
+ }
131
+
132
+ function sanitizePartForStoredHistory(part) {
133
+ if (typeof part === 'string') return part;
134
+ if (!part || typeof part !== 'object') return part;
135
+ if (part.type === 'image' || part.type === 'image_url' || part.type === 'input_image' || imageUrlFromPart(part) || imageFileIdFromPart(part) || imageFileUriFromPart(part) || geminiInlineInfo(part)) {
136
+ return { type: 'text', text: storedHistoryImagePlaceholder(part) };
137
+ }
138
+ if (Array.isArray(part.content)) {
139
+ const nextContent = sanitizeContentForStoredHistory(part.content);
140
+ if (nextContent !== part.content) return { ...part, content: nextContent };
141
+ }
142
+ return part;
143
+ }
144
+
145
+ export function sanitizeContentForStoredHistory(content) {
146
+ if (typeof content === 'string') return content;
147
+ const parts = contentParts(content);
148
+ if (!parts) return content;
149
+ let changed = false;
150
+ const out = parts.map((part) => {
151
+ const next = sanitizePartForStoredHistory(part);
152
+ if (next !== part) changed = true;
153
+ return next;
154
+ });
155
+ if (!changed) return content;
156
+ return Array.isArray(content) ? out : { ...content, content: out };
157
+ }
158
+
84
159
  export function normalizeContentForAnthropic(content) {
85
160
  const parts = contentParts(content);
86
161
  if (!parts) return content;
@@ -98,8 +173,28 @@ export function normalizeContentForAnthropic(content) {
98
173
  if (part.cache_control) out.cache_control = part.cache_control;
99
174
  return out;
100
175
  }
176
+ const fileId = imageFileIdFromPart(part);
177
+ if (fileId) {
178
+ return { type: 'image', source: { type: 'file', file_id: fileId } };
179
+ }
180
+ const url = imageUrlFromPart(part);
181
+ const dataUrlInfo = imageInfoFromDataUrl(url);
182
+ if (dataUrlInfo) {
183
+ return {
184
+ type: 'image',
185
+ source: {
186
+ type: 'base64',
187
+ media_type: dataUrlInfo.mimeType,
188
+ data: dataUrlInfo.data,
189
+ },
190
+ };
191
+ }
192
+ if (url) {
193
+ return { type: 'image', source: { type: 'url', url } };
194
+ }
101
195
  if (part?.type === 'image') {
102
196
  if (part.source?.type === 'url' && typeof part.source.url === 'string') return part;
197
+ if (part.source?.type === 'file' && typeof part.source.file_id === 'string') return part;
103
198
  return { type: 'text', text: `[unsupported image content: ${stringifyFallback(part)}]` };
104
199
  }
105
200
  if (part?.type === 'tool_result') {
@@ -124,6 +219,16 @@ export function normalizeContentForOpenAIChat(content, { role = 'user' } = {}) {
124
219
  if (!parts) return content;
125
220
  const out = [];
126
221
  for (const part of parts) {
222
+ const fileId = imageFileIdFromPart(part);
223
+ if (fileId) {
224
+ out.push({ type: 'text', text: `[unsupported image file_id for OpenAI Chat-compatible request: ${fileId}]` });
225
+ continue;
226
+ }
227
+ const fileUri = imageFileUriFromPart(part);
228
+ if (fileUri) {
229
+ out.push({ type: 'image_url', image_url: { url: fileUri.fileUri } });
230
+ continue;
231
+ }
127
232
  const url = imageUrlFromPart(part);
128
233
  if (url) {
129
234
  out.push({ type: 'image_url', image_url: { url } });
@@ -146,6 +251,16 @@ export function normalizeContentForOpenAIResponses(content, { role = 'user' } =
146
251
  }
147
252
  const out = [];
148
253
  for (const part of parts) {
254
+ const fileId = imageFileIdFromPart(part);
255
+ if (fileId) {
256
+ out.push({ type: 'input_image', file_id: fileId });
257
+ continue;
258
+ }
259
+ const fileUri = imageFileUriFromPart(part);
260
+ if (fileUri) {
261
+ out.push({ type: 'input_image', image_url: fileUri.fileUri });
262
+ continue;
263
+ }
149
264
  const url = imageUrlFromPart(part);
150
265
  if (url) {
151
266
  out.push({ type: 'input_image', image_url: url });
@@ -166,6 +281,21 @@ export function normalizeContentForGeminiParts(content) {
166
281
  }
167
282
  const out = [];
168
283
  for (const part of parts) {
284
+ const inlineInfo = geminiInlineInfo(part);
285
+ if (inlineInfo) {
286
+ out.push({ inlineData: { mimeType: inlineInfo.mimeType, data: inlineInfo.data } });
287
+ continue;
288
+ }
289
+ const fileUri = imageFileUriFromPart(part);
290
+ if (fileUri) {
291
+ out.push({ fileData: { mimeType: fileUri.mimeType, fileUri: fileUri.fileUri } });
292
+ continue;
293
+ }
294
+ const fileId = imageFileIdFromPart(part);
295
+ if (fileId) {
296
+ out.push({ text: `[unsupported image file_id for Gemini request: ${fileId}]` });
297
+ continue;
298
+ }
169
299
  const info = imageInfo(part);
170
300
  if (info) {
171
301
  out.push({ inlineData: { mimeType: info.mimeType, data: info.data } });
@@ -304,7 +304,7 @@ function _modelsDevMetadataSync(id, provider) {
304
304
  * 2. disk cache one-shot read if memory is cold (first call after boot),
305
305
  * 3. null if neither is available (async loadCatalog will fill later).
306
306
  *
307
- * Used by hot-path loggers (bridge-trace usage row) that must not await.
307
+ * Used by hot-path loggers (agent-trace usage row) that must not await.
308
308
  * The disk fallback is a single ~5ms blocking read on cold start; all
309
309
  * subsequent calls hit memory. TTL is intentionally ignored here — stale
310
310
  * catalog beats no catalog, and the async path refreshes on schedule.
@@ -358,6 +358,7 @@ function _normalize(entry) {
358
358
  cacheWriteCostPerM: entry.cache_creation_input_token_cost != null ? entry.cache_creation_input_token_cost * 1_000_000 : null,
359
359
  supportsVision: entry.supports_vision === true,
360
360
  supportsFunctionCalling: entry.supports_function_calling === true,
361
+ supportsWebSearch: entry.supports_web_search === true || entry.supports_websearch === true,
361
362
  supportsPromptCaching: entry.supports_prompt_caching === true,
362
363
  supportsReasoning: entry.supports_reasoning === true,
363
364
  reasoningOptions: Array.isArray(entry.reasoning_options) ? entry.reasoning_options : [],
@@ -375,6 +376,7 @@ function mergeModelMetadata(base, overlay) {
375
376
  outputTokens: overlay.outputTokens || base.outputTokens || null,
376
377
  supportsVision: base.supportsVision || overlay.supportsVision,
377
378
  supportsFunctionCalling: base.supportsFunctionCalling || overlay.supportsFunctionCalling,
379
+ supportsWebSearch: base.supportsWebSearch || overlay.supportsWebSearch,
378
380
  supportsPromptCaching: base.supportsPromptCaching || overlay.supportsPromptCaching,
379
381
  supportsReasoning: base.supportsReasoning || overlay.supportsReasoning,
380
382
  reasoningOptions: overlay.reasoningOptions?.length ? overlay.reasoningOptions : (base.reasoningOptions || []),
@@ -430,6 +432,7 @@ export async function enrichModels(models) {
430
432
  cacheWriteCostPerM: meta.cacheWriteCostPerM,
431
433
  supportsVision: meta.supportsVision,
432
434
  supportsFunctionCalling: meta.supportsFunctionCalling,
435
+ supportsWebSearch: meta.supportsWebSearch || m.supportsWebSearch === true,
433
436
  supportsPromptCaching: meta.supportsPromptCaching,
434
437
  supportsReasoning: meta.supportsReasoning,
435
438
  reasoningOptions: meta.reasoningOptions || m.reasoningOptions || [],
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
4
+
5
+ const ANTHROPIC_DEFAULT_CREDENTIALS_PATH = join(resolvePluginData(), 'anthropic-oauth-credentials.json');
6
+
7
+ function readJsonIfExists(path) {
8
+ if (!path || !existsSync(path)) return null;
9
+ try { return JSON.parse(readFileSync(path, 'utf-8')); }
10
+ catch { return null; }
11
+ }
12
+
13
+ function pushUnique(list, value) {
14
+ if (!value || typeof value !== 'string') return;
15
+ if (!list.includes(value)) list.push(value);
16
+ }
17
+
18
+ export function hasAnthropicOAuthCredentials() {
19
+ const paths = [];
20
+ pushUnique(paths, process.env.ANTHROPIC_OAUTH_CREDENTIALS_PATH);
21
+ pushUnique(paths, ANTHROPIC_DEFAULT_CREDENTIALS_PATH);
22
+ for (const path of paths) {
23
+ const raw = readJsonIfExists(path);
24
+ const oauth = raw?.claudeAiOauth;
25
+ if (oauth?.accessToken && Array.isArray(oauth.scopes) && oauth.scopes.includes('user:inference')) {
26
+ return true;
27
+ }
28
+ }
29
+ return false;
30
+ }
31
+
32
+ export function hasOpenAIOAuthCredentials() {
33
+ const paths = [join(resolvePluginData(), 'openai-oauth.json')];
34
+ for (const path of paths) {
35
+ const raw = readJsonIfExists(path);
36
+ const tokens = raw?.tokens || raw;
37
+ if (tokens?.access_token && tokens?.refresh_token) return true;
38
+ }
39
+ return false;
40
+ }
41
+
42
+ export function hasGrokOAuthCredentials() {
43
+ const own = readJsonIfExists(join(resolvePluginData(), 'grok-oauth.json'));
44
+ return !!(own?.access_token && own?.refresh_token);
45
+ }