mixdog 1.4.1 → 1.4.2

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 (326) hide show
  1. package/package.json +3 -2
  2. package/scripts/build-shipped-history.mjs +115 -33
  3. package/scripts/gate-local.mjs +172 -61
  4. package/scripts/lib/gate-scheduler.mjs +138 -32
  5. package/scripts/lib/gate-timing-reporter.mjs +16 -0
  6. package/scripts/lib/run-node-tests.mjs +54 -29
  7. package/scripts/lib/scoped-temp.mjs +1 -0
  8. package/scripts/lib/test-failure-records.mjs +14 -1
  9. package/scripts/lib/test-failure-summary.mjs +16 -4
  10. package/scripts/lib/test-spawn-ledger.mjs +12 -3
  11. package/scripts/pptx-kit-index.mjs +5 -6
  12. package/scripts/suite-health.mjs +110 -28
  13. package/scripts/test.mjs +9 -14
  14. package/src/agents/advisor/AGENT.md +4 -4
  15. package/src/agents/advisor/agent.json +1 -1
  16. package/src/agents/explore/AGENT.md +22 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/cli.mjs +7 -0
  19. package/src/defaults/agents.json +7 -0
  20. package/src/defaults/shipped-history.json +18 -2
  21. package/src/defaults/skills/browser-use/SKILL.md +4 -1
  22. package/src/defaults/skills/code-tidy/SKILL.md +71 -102
  23. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
  24. package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
  25. package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
  26. package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
  27. package/src/defaults/skills/computer-use/SKILL.md +12 -2
  28. package/src/defaults/skills/goal-management/SKILL.md +1 -1
  29. package/src/defaults/skills/setup/references/actions.md +12 -6
  30. package/src/defaults/skills/setup/references/surfaces.md +2 -2
  31. package/src/defaults/skills/setup-guide/SKILL.md +5 -2
  32. package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
  33. package/src/defaults/skills/skill-creator/SKILL.md +85 -190
  34. package/src/defaults/skills/skill-creator/references/design.md +70 -162
  35. package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
  36. package/src/defaults/skills/test-suite/SKILL.md +194 -0
  37. package/src/defaults/skills/test-suite/references/case-management.md +51 -0
  38. package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
  39. package/src/defaults/skills/test-suite/references/reliability.md +49 -0
  40. package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
  41. package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
  42. package/src/defaults/skills/visualize/SKILL.md +6 -2
  43. package/src/lib/rules-builder.cjs +6 -30
  44. package/src/rules/lead/LEAD.md +3 -3
  45. package/src/rules/shared/20-research.md +3 -6
  46. package/src/rules/shared/40-editing.md +1 -0
  47. package/src/rules/shared/60-verification.md +10 -9
  48. package/src/rules/shared/70-delivery.md +4 -3
  49. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
  50. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +130 -21
  52. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
  54. package/src/runtime/agent/orchestrator/config.mjs +12 -7
  55. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +7 -7
  56. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
  57. package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
  58. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
  59. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
  60. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
  61. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
  63. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +83 -52
  64. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
  65. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +43 -8
  66. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +30 -28
  67. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
  69. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
  70. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
  71. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
  74. package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
  75. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
  76. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
  77. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
  78. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
  79. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
  80. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +48 -31
  81. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
  82. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
  83. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
  85. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +26 -28
  88. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +3 -1
  89. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +1 -0
  90. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
  92. package/src/runtime/agent/orchestrator/providers/registry.mjs +33 -2
  93. package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
  94. package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
  95. package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
  96. package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
  97. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  98. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +2 -1
  99. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  100. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -0
  101. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
  102. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
  103. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
  104. package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
  105. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +94 -10
  106. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +3 -3
  107. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +10 -5
  108. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +5 -5
  109. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +11 -1
  110. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
  111. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
  112. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +5 -3
  113. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +21 -2
  114. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
  115. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
  116. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
  117. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
  118. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
  119. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +3 -4
  120. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +2 -0
  121. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
  122. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
  123. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -14
  124. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +17 -14
  125. package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
  126. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
  127. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -3
  128. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +47 -29
  129. package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
  130. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
  131. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +1 -1
  132. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +243 -38
  133. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
  134. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
  135. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
  136. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
  137. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +36 -26
  138. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +3 -5
  139. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
  143. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  144. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
  145. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
  146. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
  147. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
  148. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +2 -0
  152. package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
  154. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  155. package/src/runtime/channels/lib/whisper-port.mjs +19 -10
  156. package/src/runtime/channels/lib/whisper-server.mjs +10 -1
  157. package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
  158. package/src/runtime/connectors/gmail-app-password.mjs +3 -3
  159. package/src/runtime/explore/explorer.mjs +93 -0
  160. package/src/runtime/explore/tool-defs.mjs +35 -0
  161. package/src/runtime/explore/tool.mjs +123 -0
  162. package/src/runtime/memory/index.mjs +4 -0
  163. package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
  164. package/src/runtime/memory/lib/embedding-worker.mjs +4 -0
  165. package/src/runtime/office/quality/assurance-structure-docx.mjs +1 -1
  166. package/src/runtime/shared/agent-route-config.mjs +17 -2
  167. package/src/runtime/shared/atomic-file.mjs +103 -2
  168. package/src/runtime/shared/background-tasks.mjs +15 -6
  169. package/src/runtime/shared/connector-accounts.mjs +5 -5
  170. package/src/runtime/shared/connector-catalog.d.mts +11 -4
  171. package/src/runtime/shared/connector-catalog.mjs +60 -52
  172. package/src/runtime/shared/custom-provider-keys.mjs +20 -0
  173. package/src/runtime/shared/file-has-data.mjs +26 -2
  174. package/src/runtime/shared/file-lock.mjs +13 -15
  175. package/src/runtime/shared/git-launch.d.mts +9 -0
  176. package/src/runtime/shared/git-launch.mjs +213 -0
  177. package/src/runtime/shared/git-runner-worker.mjs +2 -1
  178. package/src/runtime/shared/llm/cost.mjs +6 -1
  179. package/src/runtime/shared/llm/http-agent.mjs +313 -29
  180. package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
  181. package/src/runtime/shared/llm/model-catalog.mjs +46 -6
  182. package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
  183. package/src/runtime/shared/llm/system-proxy.mjs +452 -0
  184. package/src/runtime/shared/provider-accounts.mjs +52 -2
  185. package/src/runtime/shared/staged-install-worker.mjs +5 -0
  186. package/src/runtime/shared/staged-update.mjs +47 -2
  187. package/src/runtime/shared/tool-surface.mjs +110 -36
  188. package/src/runtime/shared/tool-work-units.mjs +4 -0
  189. package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
  190. package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
  191. package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
  192. package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
  193. package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
  194. package/src/runtime/shared/worktrees/git.mjs +57 -0
  195. package/src/runtime/shared/worktrees/manager.mjs +205 -137
  196. package/src/runtime/tidy/install.mjs +95 -15
  197. package/src/runtime/tidy/languages.mjs +17 -15
  198. package/src/runtime/tidy/process.mjs +7 -4
  199. package/src/runtime/tidy/resolve.mjs +19 -16
  200. package/src/runtime/tidy/runners/biome.mjs +3 -3
  201. package/src/runtime/tidy/tool.mjs +1 -1
  202. package/src/runtime/tidy/worktree.mjs +10 -6
  203. package/src/runtime/visualize-bridge/client.mjs +4 -1
  204. package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
  205. package/src/runtime/web-search/index.mjs +1 -1
  206. package/src/session-runtime/agent-task-sessions.mjs +410 -353
  207. package/src/session-runtime/boot/tools.mjs +2 -0
  208. package/src/session-runtime/bot-status.mjs +103 -103
  209. package/src/session-runtime/connector-merge-migration.mjs +18 -15
  210. package/src/session-runtime/conversation-import/discovery.mjs +159 -40
  211. package/src/session-runtime/conversation-import/service.mjs +9 -3
  212. package/src/session-runtime/handoff-attachments.mjs +1 -1
  213. package/src/session-runtime/held-requests.mjs +3 -1
  214. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
  215. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
  216. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +143 -116
  217. package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
  218. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  219. package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
  220. package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
  221. package/src/session-runtime/mixy-heartbeat.mjs +1 -1
  222. package/src/session-runtime/provider-auth/login.mjs +2 -2
  223. package/src/session-runtime/resource-mcp-api.mjs +98 -35
  224. package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
  225. package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
  226. package/src/session-runtime/resource-mcp-servers.mjs +5 -4
  227. package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
  228. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
  229. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  230. package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
  231. package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
  232. package/src/session-runtime/services/daemon-candidate.mjs +4 -1
  233. package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
  234. package/src/session-runtime/services/plugin-admin.mjs +10 -6
  235. package/src/session-runtime/services/provider-admin.mjs +281 -10
  236. package/src/session-runtime/session-turn-api.mjs +2 -0
  237. package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
  238. package/src/session-runtime/settings-system-api.mjs +1 -1
  239. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
  240. package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
  241. package/src/session-runtime/setup-tool/executor.mjs +6 -2
  242. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
  243. package/src/session-runtime/turn/agent-ops.mjs +23 -0
  244. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
  245. package/src/session-runtime/workflow-agents-api/agent-route.mjs +10 -1
  246. package/src/session-runtime/workspace/activity-guards.mjs +176 -0
  247. package/src/session-runtime/workspace/session-view.mjs +149 -0
  248. package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
  249. package/src/session-runtime/workspace/transition-runner.mjs +188 -0
  250. package/src/session-runtime/workspace/transitions.mjs +108 -1066
  251. package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
  252. package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
  253. package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
  254. package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
  255. package/src/session-runtime/workspace/worktree-tool.mjs +54 -54
  256. package/src/standalone/daemon-desktop-runtime.mjs +2 -0
  257. package/src/standalone/daemon-session-control.mjs +151 -136
  258. package/src/standalone/daemon.mjs +7 -0
  259. package/src/standalone/desktop-service-registry.mjs +9 -0
  260. package/src/standalone/local-session-runtime.mjs +5 -1
  261. package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
  262. package/src/standalone/session-client.mjs +5 -1
  263. package/src/standalone/session-frame-stream.mjs +83 -11
  264. package/src/standalone/session-protocol.mjs +4 -0
  265. package/src/standalone/session-service/agent-tree.mjs +45 -9
  266. package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
  267. package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
  268. package/src/standalone/session-service/stored-reader.mjs +127 -40
  269. package/src/tui/App.jsx +154 -164
  270. package/src/tui/app/app-view/floating-panel.jsx +3 -0
  271. package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
  272. package/src/tui/app/clipboard.mjs +21 -10
  273. package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
  274. package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
  275. package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
  276. package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
  277. package/src/tui/app/provider-setup/oauth-actions.mjs +25 -6
  278. package/src/tui/app/provider-setup-picker.mjs +20 -13
  279. package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
  280. package/src/tui/app/slash-commands.mjs +1 -0
  281. package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
  282. package/src/tui/app/slash-dispatch.mjs +1 -1
  283. package/src/tui/app/use-btw-panel.mjs +29 -0
  284. package/src/tui/app/use-context-panel-refresh.mjs +31 -0
  285. package/src/tui/app/use-model-catalog-cache.mjs +13 -10
  286. package/src/tui/app/use-mouse-input.mjs +11 -0
  287. package/src/tui/app/use-onboarding-start.mjs +8 -3
  288. package/src/tui/app/use-panel-transition.mjs +10 -1
  289. package/src/tui/app/use-pasted-buffers.mjs +28 -20
  290. package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
  291. package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
  292. package/src/tui/app/use-prompt-queue-history.mjs +3 -2
  293. package/src/tui/app/use-selection-focus.mjs +11 -2
  294. package/src/tui/app/use-selection-geometry.mjs +18 -9
  295. package/src/tui/app/use-selection-paint.mjs +4 -4
  296. package/src/tui/app/use-selection-stitch.mjs +3 -3
  297. package/src/tui/app/use-slash-palette.mjs +62 -0
  298. package/src/tui/app/use-terminal-chrome.mjs +2 -2
  299. package/src/tui/app/use-transcript-activity.mjs +47 -32
  300. package/src/tui/app/use-transcript-follow.mjs +12 -4
  301. package/src/tui/app/use-transcript-scroll.mjs +46 -17
  302. package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
  303. package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
  304. package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
  305. package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
  306. package/src/tui/app/use-transcript-window.mjs +21 -11
  307. package/src/tui/app/use-tui-ready.mjs +18 -0
  308. package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
  309. package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
  310. package/src/tui/components/Picker.jsx +27 -3
  311. package/src/tui/components/SignInUrl.jsx +68 -0
  312. package/src/tui/components/TextEntryPanel.jsx +76 -46
  313. package/src/tui/components/sign-in-url-layout.mjs +24 -0
  314. package/src/tui/dist/index.mjs +1896 -1341
  315. package/src/tui/session/goal-turn-state.mjs +6 -2
  316. package/src/tui/session/session-api/controls.mjs +4 -0
  317. package/src/tui/session/session-api/extensions.mjs +8 -0
  318. package/src/tui/session/session-api/intake/abort.mjs +2 -1
  319. package/src/tui/session/session-api/intake.mjs +2 -0
  320. package/src/tui/session/session-api/integrations.mjs +2 -2
  321. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  322. package/src/tui/session/session-api/settings.mjs +2 -2
  323. package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
  324. package/src/tui/session/transcript-spill.mjs +92 -4
  325. package/src/tui/session/transcript-store.mjs +2 -0
  326. package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
@@ -12,13 +12,21 @@ export const CUSTOM_REASONING_LEVELS = CUSTOM_EFFORT_LEVELS;
12
12
  export const CUSTOM_REASONING_WIRE_FORMATS = Object.freeze(['standard', 'deepseek', 'together', 'vllm']);
13
13
  export const CUSTOM_PROVIDER_AUTH_MODES = Object.freeze(['key', 'none']);
14
14
  export const CUSTOM_MODEL_OVERRIDE_LIMIT = 500;
15
+ export const CUSTOM_HEADER_LIMIT = 64;
15
16
 
16
17
  const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
17
18
  const MAX_MODEL_ID = 256;
18
19
  const MAX_NAME = 200;
19
20
  const MAX_WIRE_VALUE = 64;
21
+ const MAX_HEADER_TEXT = 4096;
20
22
  // biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting control characters is the point.
21
23
  const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
24
+ // RFC 9110 field-name token, and a field value of visible ASCII, space, tab and
25
+ // obs-text (what a fetch Headers value can carry).
26
+ const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
27
+ const HEADER_VALUE = /^[\t\x20-\x7e\x80-\xff]*$/;
28
+ // The API key field owns authentication; a header may not carry a credential.
29
+ const AUTH_HEADERS = new Set(['authorization', 'x-api-key', 'api-key']);
22
30
 
23
31
  function fail(message) {
24
32
  throw new Error(`[custom-provider] ${message}`);
@@ -84,6 +92,7 @@ const V2_TOP_LEVEL_KEYS = new Set([
84
92
  'modelOverrides',
85
93
  'wires',
86
94
  'modelWires',
95
+ 'headers',
87
96
  'id',
88
97
  'apiKey',
89
98
  'apiKeyEnv',
@@ -295,6 +304,34 @@ function normalizeModelWires(raw, protocol, wires) {
295
304
  return out;
296
305
  }
297
306
 
307
+ // `headers`: extra HTTP headers sent with every request to the endpoint
308
+ // (inference, model discovery, connection test). Names are case-insensitive
309
+ // tokens; the credential headers belong to the API key field.
310
+ function normalizeHeaders(raw) {
311
+ if (raw === undefined) return {};
312
+ if (!isPlainObject(raw)) fail('headers must be an object of header names to string values');
313
+ const names = Object.keys(raw);
314
+ if (names.length > CUSTOM_HEADER_LIMIT) fail(`at most ${CUSTOM_HEADER_LIMIT} headers are allowed`);
315
+ const seen = new Set();
316
+ const entries = [];
317
+ for (const name of names) {
318
+ if (name.length > MAX_HEADER_TEXT || !HEADER_NAME.test(name))
319
+ fail(`headers has an invalid header name: ${JSON.stringify(name.slice(0, 64))}`);
320
+ const lower = name.toLowerCase();
321
+ if (AUTH_HEADERS.has(lower)) fail(`headers must not set ${name}: authentication belongs to the API key field`);
322
+ if (seen.has(lower)) fail(`headers has duplicate header: ${name}`);
323
+ seen.add(lower);
324
+ if (typeof raw[name] !== 'string') fail(`headers.${name} must be a string`);
325
+ // Validate before trimming, and trim only HTTP whitespace (space, tab), so
326
+ // CR/LF is refused rather than stripped and other characters are kept.
327
+ if (!HEADER_VALUE.test(raw[name])) fail(`headers.${name} has an invalid value`);
328
+ const value = raw[name].replace(/^[ \t]+|[ \t]+$/g, '');
329
+ if (value.length > MAX_HEADER_TEXT) fail(`headers.${name} has an invalid value`);
330
+ entries.push([name, value]);
331
+ }
332
+ return Object.fromEntries(entries);
333
+ }
334
+
298
335
  // Legacy (pre-v2) `models: [{ id, name?, contextWindow?, maxOutputTokens? }]`
299
336
  // become manual, visible overrides; their presence hides newly discovered ids.
300
337
  function migrateLegacy(input) {
@@ -336,6 +373,7 @@ export function normalizeCustomProviderConfig(input) {
336
373
  fail('modelOverrides and discovery require schemaVersion 2');
337
374
  if (input.wires !== undefined || input.modelWires !== undefined)
338
375
  fail('wires and modelWires require schemaVersion 2');
376
+ if (input.headers !== undefined) fail('headers require schemaVersion 2');
339
377
  source = migrateLegacy(input);
340
378
  } else if (Number.isInteger(version) && version > CUSTOM_PROVIDER_SCHEMA_VERSION)
341
379
  fail(`unsupported schemaVersion ${version}; update mixdog to use this provider`);
@@ -368,6 +406,7 @@ export function normalizeCustomProviderConfig(input) {
368
406
  const newModelsHidden = optionalBoolean(discovery.newModelsHidden, 'discovery.newModelsHidden') ?? false;
369
407
  const wires = normalizeWires(source.wires, protocol);
370
408
  const modelWires = normalizeModelWires(source.modelWires, protocol, wires);
409
+ const headers = normalizeHeaders(source.headers);
371
410
  return {
372
411
  type: 'custom',
373
412
  schemaVersion: CUSTOM_PROVIDER_SCHEMA_VERSION,
@@ -377,6 +416,7 @@ export function normalizeCustomProviderConfig(input) {
377
416
  enabled: source.enabled !== false,
378
417
  auth,
379
418
  ...(apiKeyEnv && auth === 'key' ? { apiKeyEnv } : {}),
419
+ ...(Object.keys(headers).length ? { headers } : {}),
380
420
  discovery: { newModelsHidden },
381
421
  modelOverrides: normalizeOverrides(source.modelOverrides, protocol, legacy),
382
422
  ...(wires ? { wires } : {}),
@@ -0,0 +1,34 @@
1
+ // The engine a custom provider sends with when one of its additional API keys
2
+ // serves (account-pool.mjs `createCustomKeyPoolProvider`). Every key reaches
3
+ // the same endpoint, so the engine is a view of the primary instance: the
4
+ // catalog, discovered models, wire routes and settings stay the primary's,
5
+ // and only the credential and the client built from it are the key's own.
6
+
7
+ import { readCustomProviderKey } from '../../../shared/custom-provider-keys.mjs';
8
+
9
+ function keyView(engine, providerId, keyId, key) {
10
+ const view = Object.create(engine);
11
+ const rekey = (value) => {
12
+ // The key is the only source: an environment variable names the primary key alone.
13
+ view.config = { ...engine.config, apiKey: value, apiKeyEnv: undefined };
14
+ view.apiKey = value;
15
+ view.client = view._createClient(value);
16
+ };
17
+ rekey(key);
18
+ // An auth retry re-reads this key, never the primary's keychain entry.
19
+ view.reloadApiKey = () => {
20
+ const fresh = readCustomProviderKey(providerId, keyId);
21
+ if (fresh) rekey(fresh);
22
+ };
23
+ return view;
24
+ }
25
+
26
+ /** `primary` sending with additional key `keyId` of custom provider `providerId`. */
27
+ export function customKeyEngine(primary, providerId, keyId) {
28
+ const key = readCustomProviderKey(providerId, keyId);
29
+ if (!key) throw new Error(`An additional API key of ${providerId} is not readable; enter it again in its settings.`);
30
+ const engine = keyView(primary, providerId, keyId, key);
31
+ // A Messages-wire route sends through the bound Anthropic engine.
32
+ if (primary.messagesEngine) engine.messagesEngine = keyView(primary.messagesEngine, providerId, keyId, key);
33
+ return engine;
34
+ }
@@ -120,6 +120,7 @@ async function discoverAnthropicModels(provider, signal, timeoutMs = DISCOVERY_T
120
120
  headers: {
121
121
  ...(provider._customKeyless ? {} : { 'x-api-key': provider._customKey() }),
122
122
  'anthropic-version': ANTHROPIC_VERSION,
123
+ ...(provider.config.extraHeaders || {}),
123
124
  },
124
125
  });
125
126
  if (!res.ok) throw Object.assign(new Error(`${provider.name} models ${res.status}`), { status: res.status });
@@ -153,6 +154,8 @@ export async function createCustomProvider(id, config) {
153
154
  apiKey: apiKey || undefined,
154
155
  ...(cfg.apiKeyEnv ? { apiKeyEnv: cfg.apiKeyEnv } : {}),
155
156
  ...(keyless ? { keyless: true } : {}),
157
+ // User-defined headers ride the engines' existing `extraHeaders` path.
158
+ ...(cfg.headers ? { extraHeaders: cfg.headers } : {}),
156
159
  ...(config.preconnect === false ? { preconnect: false } : {}),
157
160
  };
158
161
  let provider;
@@ -14,6 +14,10 @@ import {
14
14
  decodeJwtPayload,
15
15
  expiryFromAccessToken,
16
16
  normalizeAccountIdentity,
17
+ OAUTH_NETWORK_HINT,
18
+ oauthFailureReason,
19
+ oauthNetworkCause,
20
+ oauthPublicError,
17
21
  } from './lib/oauth-token-utils.mjs';
18
22
 
19
23
  const AUTHORIZE_URL = 'https://app.devin.ai/auth/cli/continue';
@@ -46,9 +50,11 @@ export const DEVIN_OAUTH_FAILURE_CODES = Object.freeze([
46
50
  'DEVIN_OAUTH_TOKEN_EXCHANGE_FAILED',
47
51
  ]);
48
52
 
53
+ // Built from fixed text, statuses and error codes only, so a failure page may
54
+ // show it; response bodies are never quoted (they may carry the login token).
49
55
  function loginFailure(code, text, props = {}) {
50
56
  const failureCode = `DEVIN_OAUTH_${code}`;
51
- return Object.assign(new Error(`${failureCode}: ${text}`), { failureCode }, props);
57
+ return oauthPublicError(Object.assign(new Error(`${failureCode}: ${text}`), { failureCode }, props));
52
58
  }
53
59
 
54
60
  function credentialsPath() {
@@ -175,12 +181,13 @@ export async function exchangeDevinCode(code, verifier, { fetchFn = fetch, signa
175
181
  });
176
182
  } catch (error) {
177
183
  // A cancelled login keeps its own abort; any other failure to get an
178
- // answer (network, timeout) is a coded exchange failure. The cause is not
179
- // quoted: only its kind is.
184
+ // answer (network, timeout) is a coded exchange failure. Its message is
185
+ // not quoted: only its error code.
180
186
  if (signal?.aborted) throw error;
187
+ const cause = oauthNetworkCause(error) || 'network error';
181
188
  throw loginFailure(
182
189
  'TOKEN_EXCHANGE_FAILED',
183
- error?.name === 'TimeoutError' ? 'Devin token exchange timed out' : 'Devin token exchange request failed'
190
+ `Devin token exchange request failed (${cause}). ${OAUTH_NETWORK_HINT}`
184
191
  );
185
192
  }
186
193
  // Response bodies may carry the login token itself, so none is ever quoted
@@ -341,9 +348,9 @@ async function startDevinOAuthLogin({ openBrowser = true, timeoutMs = LOGIN_TIME
341
348
  try {
342
349
  await acceptCode(code);
343
350
  reply(res, 200, oauthSuccessHtml('Devin'));
344
- } catch {
345
- // The shared exchange reports its error through waitForCallback.
346
- reply(res, 500, oauthErrorHtml('Devin could not complete the token exchange.'));
351
+ } catch (error) {
352
+ // The shared exchange also reports its error through waitForCallback.
353
+ reply(res, 500, oauthErrorHtml(oauthFailureReason('Devin could not complete the token exchange.', error)));
347
354
  }
348
355
  };
349
356
 
@@ -190,10 +190,11 @@ function connectError(
190
190
  const finalCode = tooLarge ? 'request_too_large' : mixdogCode;
191
191
  const head = `Devin ${operation} ${code || 'error'}${status ? ` (HTTP ${status})` : ''}`;
192
192
  const text = [head, detail].filter(Boolean).join(': ') + (detailsSummary ? ` [details: ${detailsSummary}]` : '');
193
+ // A reworded throttle reports its throttle status, not the carrier's.
194
+ const carrierStatus = reworded ? mappedStatus : status || mappedStatus;
193
195
  const error = new DevinWireError(text, {
194
196
  code: finalCode,
195
- // A reworded throttle reports its throttle status, not the carrier's.
196
- status: tooLarge ? 413 : reworded ? mappedStatus : status || mappedStatus,
197
+ status: tooLarge ? 413 : carrierStatus,
197
198
  retryable: tooLarge ? false : retryable,
198
199
  connectCode: code,
199
200
  detail: [detail, detailsSummary].filter(Boolean).join(' '),
@@ -115,7 +115,7 @@ export function geminiFailureUsage(opts, cachedContent, useModel) {
115
115
  };
116
116
  }
117
117
 
118
- export function resolveGeminiUsage(response, opts, cachedContent, useModel) {
118
+ export function resolveGeminiUsage(response, opts, cachedContent, useModel, provider = 'gemini') {
119
119
  const um = response.usageMetadata || null;
120
120
  if (!um) return null;
121
121
  const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
@@ -143,7 +143,7 @@ export function resolveGeminiUsage(response, opts, cachedContent, useModel) {
143
143
  model: useModel,
144
144
  modelDisplay: useModel,
145
145
  rawUsage: um,
146
- provider: 'gemini',
146
+ provider,
147
147
  });
148
148
  return resolvedUsage;
149
149
  }
@@ -589,6 +589,9 @@ export async function consumeGeminiRestStreamResponse(
589
589
  onChunk = null,
590
590
  failureUsage = null,
591
591
  firstByteTimeoutMs = GEMINI_FIRST_BYTE_TIMEOUT_MS,
592
+ // A first-byte window longer than the idle window must govern alone until
593
+ // the first payload, or the idle timer would cut the wait short.
594
+ idleAfterFirstChunk = false,
592
595
  }
593
596
  ) {
594
597
  const unwrap =
@@ -649,7 +652,7 @@ export async function consumeGeminiRestStreamResponse(
649
652
  }
650
653
 
651
654
  try {
652
- watchdogs.resetIdle();
655
+ if (!idleAfterFirstChunk) watchdogs.resetIdle();
653
656
  while (true) {
654
657
  let chunk;
655
658
  try {
@@ -6,7 +6,13 @@ import { randomBytes } from 'node:crypto';
6
6
  import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
7
7
  import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
8
8
  import { OAUTH_PAGE_CONTENT_TYPE, oauthErrorHtml, oauthSuccessHtml } from './lib/oauth-page.mjs';
9
- import { emailFromJwts } from './lib/oauth-token-utils.mjs';
9
+ import {
10
+ emailFromJwts,
11
+ oauthFailureReason,
12
+ oauthHttpError,
13
+ oauthRequestError,
14
+ oauthResponseJson,
15
+ } from './lib/oauth-token-utils.mjs';
10
16
  import {
11
17
  CLIENT_ID,
12
18
  SCOPE,
@@ -65,34 +71,43 @@ export async function exchangeAuthorizationCode({ discovery, pkce, code, signal
65
71
  signal?.throwIfAborted();
66
72
  const cleanCode = String(code || '').trim();
67
73
  if (!cleanCode) throw new Error('[grok-oauth] authorization code is required');
68
- const tokenRes = await fetch(discovery.token_endpoint, {
69
- method: 'POST',
70
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
71
- body: new URLSearchParams({
72
- grant_type: 'authorization_code',
73
- client_id: CLIENT_ID,
74
- code: cleanCode,
75
- code_verifier: pkce.verifier,
76
- redirect_uri: REDIRECT_URI,
77
- // xAI re-validates the PKCE challenge at token exchange
78
- // (not just the verifier), so echo it back. Omitting
79
- // these makes the exchange fail. Matches the Grok CLI.
80
- code_challenge: pkce.challenge,
81
- code_challenge_method: 'S256',
82
- }),
83
- // Secret-bearing (authorization code + verifier): refuse
84
- // redirects so they can't be replayed to an untrusted host.
85
- redirect: 'error',
86
- signal: signal
87
- ? AbortSignal.any([signal, AbortSignal.timeout(TOKEN_TIMEOUT_MS)])
88
- : AbortSignal.timeout(TOKEN_TIMEOUT_MS),
89
- dispatcher: getLlmDispatcher(),
90
- });
74
+ let tokenRes;
75
+ try {
76
+ tokenRes = await fetch(discovery.token_endpoint, {
77
+ method: 'POST',
78
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
79
+ body: new URLSearchParams({
80
+ grant_type: 'authorization_code',
81
+ client_id: CLIENT_ID,
82
+ code: cleanCode,
83
+ code_verifier: pkce.verifier,
84
+ redirect_uri: REDIRECT_URI,
85
+ // xAI re-validates the PKCE challenge at token exchange
86
+ // (not just the verifier), so echo it back. Omitting
87
+ // these makes the exchange fail. Matches the Grok CLI.
88
+ code_challenge: pkce.challenge,
89
+ code_challenge_method: 'S256',
90
+ }),
91
+ // Secret-bearing (authorization code + verifier): refuse
92
+ // redirects so they can't be replayed to an untrusted host.
93
+ redirect: 'error',
94
+ signal: signal
95
+ ? AbortSignal.any([signal, AbortSignal.timeout(TOKEN_TIMEOUT_MS)])
96
+ : AbortSignal.timeout(TOKEN_TIMEOUT_MS),
97
+ dispatcher: getLlmDispatcher(),
98
+ });
99
+ } catch (err) {
100
+ throw oauthRequestError('[grok-oauth] token exchange', err);
101
+ }
91
102
  if (!tokenRes.ok) {
92
103
  const text = await tokenRes.text().catch(() => '');
93
- throw new Error(`[grok-oauth] token exchange ${tokenRes.status}: ${_scrubTokens(text).slice(0, 500)}`);
104
+ throw oauthHttpError('[grok-oauth] token exchange', tokenRes.status, text, [
105
+ cleanCode,
106
+ pkce.verifier,
107
+ pkce.challenge,
108
+ ]);
94
109
  }
95
- const tokens = tokensFromGrant(await tokenRes.json(), discovery);
110
+ const tokens = tokensFromGrant(await oauthResponseJson(tokenRes, '[grok-oauth] token exchange'), discovery);
96
111
  // A cancelled login never stores the account it was exchanging.
97
112
  signal?.throwIfAborted();
98
113
  saveTokens(tokens);
@@ -338,14 +353,16 @@ export async function beginOAuthLogin({ openBrowser = true, sleep = defaultSleep
338
353
  res.end(oauthErrorHtml('The sign-in callback was invalid or did not match this login.'));
339
354
  return;
340
355
  }
356
+ // The login settles (closing its server) before this answer is sent, so
357
+ // the connection must not stay open to the closed login's handler.
341
358
  try {
342
359
  await acceptCode(code);
343
- res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
360
+ res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
344
361
  res.end(oauthSuccessHtml('Grok'));
345
- } catch {
346
- // The shared exchange reports its error through waitForCallback.
347
- res.writeHead(500, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
348
- res.end(oauthErrorHtml('Grok could not complete the token exchange.'));
362
+ } catch (err) {
363
+ // The shared exchange also reports its error through waitForCallback.
364
+ res.writeHead(500, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
365
+ res.end(oauthErrorHtml(oauthFailureReason('Grok could not complete the token exchange.', err)));
349
366
  }
350
367
  });
351
368
  timeout = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
@@ -40,7 +40,6 @@ const COPY = {
40
40
  failureTitle: '로그인 실패',
41
41
  failureBody: 'Mixdog로 돌아가 다시 시도해 주세요.',
42
42
  reason: '사유',
43
- close: '닫기',
44
43
  },
45
44
  en: {
46
45
  successTitle: 'Sign-in complete',
@@ -48,7 +47,6 @@ const COPY = {
48
47
  failureTitle: 'Sign-in failed',
49
48
  failureBody: 'Return to Mixdog and try again.',
50
49
  reason: 'Reason',
51
- close: 'Close',
52
50
  },
53
51
  };
54
52
 
@@ -69,7 +67,6 @@ function renderPage({ kind, providerName, reason, lang }) {
69
67
  const reasonBlock = reason
70
68
  ? `<div class="reason"><span>${langs.map((code) => COPY[code].reason).join(' / ')}</span>${escapeHtml(reason)}</div>`
71
69
  : '';
72
- const close = langs.map((code) => COPY[code].close).join(' · ');
73
70
  const documentTitle = escapeHtml(
74
71
  `${langs.map((code) => COPY[code][kind === 'success' ? 'successTitle' : 'failureTitle']).join(' / ')} · Mixdog`
75
72
  );
@@ -130,20 +127,6 @@ function renderPage({ kind, providerName, reason, lang }) {
130
127
  word-break: break-word;
131
128
  }
132
129
  .reason span { display: block; margin-bottom: 4px; color: var(--muted); font-family: inherit; font-size: 12px; }
133
- button {
134
- margin-top: 24px;
135
- min-width: 120px;
136
- height: 40px;
137
- padding: 0 20px;
138
- border: 1px solid var(--line);
139
- border-radius: 10px;
140
- background: transparent;
141
- color: var(--fg);
142
- font: inherit;
143
- font-size: 14px;
144
- cursor: pointer;
145
- }
146
- button:hover { border-color: var(--muted); }
147
130
  </style>
148
131
  </head>
149
132
  <body>
@@ -151,7 +134,6 @@ function renderPage({ kind, providerName, reason, lang }) {
151
134
  <div class="logo">${LOGO_SVG}</div>
152
135
  ${blocks}
153
136
  ${reasonBlock}
154
- <button type="button" onclick="window.close()">${escapeHtml(close)}</button>
155
137
  </main>
156
138
  </body>
157
139
  </html>`;
@@ -140,14 +140,28 @@ export function isDefinitiveOAuthFailure(text, status = 0) {
140
140
  return s === 401;
141
141
  }
142
142
 
143
- export function scrubOAuthSecrets(text, secretValues = []) {
144
- let scrubbed = String(text || '')
143
+ // Fields whose string values are secrets, snake_case or camelCase. `code` is
144
+ // not one: it is also every API's error/status code. The authorization code
145
+ // is redacted by value — exchange paths pass it in `secretValues`.
146
+ const SECRET_FIELD =
147
+ '(?:access_?token|refresh_?token|id_?token|client_?secret|code_?verifier|code_?challenge|device_?code|api_?key|token|key|secret|password|assertion)';
148
+ const SECRET_KEY_RE = new RegExp(`^${SECRET_FIELD}$`, 'i');
149
+ // Fallback for text that is not JSON. `"field": value` — a string (possibly
150
+ // cut off) or a bare non-numeric value in malformed JSON.
151
+ const JSON_SECRET_RE = new RegExp(
152
+ `"(${SECRET_FIELD})"\\s*:\\s*(?:"(?:[^"\\\\]|\\\\.)*"?|(?!-?\\d|true\\b|false\\b|null\\b)[^,}\\]\\s]+)`,
153
+ 'gi'
154
+ );
155
+ // Form-encoded or query `field=value`.
156
+ const FORM_SECRET_RE = new RegExp(`(^|[^A-Za-z0-9_])(${SECRET_FIELD})=[^&\\s"']*`, 'gi');
157
+ const JWT_RE = /\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}(?:\.[A-Za-z0-9_-]*)?/g;
158
+
159
+ // Bearer/JWT/Anthropic tokens and the given secret values, anywhere in `text`.
160
+ function scrubTokenText(text, secretValues) {
161
+ let scrubbed = text
145
162
  .replace(/Bearer [A-Za-z0-9._-]+/gi, 'Bearer [REDACTED]')
146
163
  .replace(/sk-ant-[A-Za-z0-9._-]+/g, '[REDACTED]')
147
- .replace(
148
- /"(accessToken|refreshToken|access_token|refresh_token|code|key)"\s*:\s*"[^"]+"/g,
149
- (_match, key) => `"${key}":"[REDACTED]"`
150
- );
164
+ .replace(JWT_RE, '[REDACTED]');
151
165
  for (const secret of secretValues) {
152
166
  if (typeof secret === 'string' && secret) {
153
167
  scrubbed = scrubbed.split(secret).join('[REDACTED]');
@@ -155,3 +169,121 @@ export function scrubOAuthSecrets(text, secretValues = []) {
155
169
  }
156
170
  return scrubbed;
157
171
  }
172
+
173
+ // A JSON object or array is redacted on its parsed form, so escaped key names
174
+ // (`client\u005fsecret`) cannot slip past a pattern. Numbers keep their exact
175
+ // source text; an untouched body is returned as it came. Null when `text` is
176
+ // not a JSON object or array.
177
+ function scrubJsonSecrets(text, secretValues) {
178
+ let parsed;
179
+ try {
180
+ parsed = JSON.parse(text, (_key, value, context) =>
181
+ typeof value === 'number' ? JSON.rawJSON(context.source) : value
182
+ );
183
+ } catch {
184
+ return null;
185
+ }
186
+ if (!parsed || typeof parsed !== 'object' || JSON.isRawJSON(parsed)) return null;
187
+ let changed = false;
188
+ // Each decoded string (keys too) gets the full redaction: it may hold
189
+ // form-encoded fields, a query string or JSON of its own.
190
+ const scrubString = (value) => {
191
+ const scrubbed = scrubOAuthSecrets(value, secretValues);
192
+ if (scrubbed !== value) changed = true;
193
+ return scrubbed;
194
+ };
195
+ const walk = (value) => {
196
+ if (typeof value === 'string') return scrubString(value);
197
+ if (!value || typeof value !== 'object' || JSON.isRawJSON(value)) return value;
198
+ if (Array.isArray(value)) return value.map(walk);
199
+ return Object.fromEntries(
200
+ Object.entries(value).map(([key, inner]) => {
201
+ if (SECRET_KEY_RE.test(key) && typeof inner === 'string') {
202
+ changed = true;
203
+ return [key, '[REDACTED]'];
204
+ }
205
+ return [scrubString(key), walk(inner)];
206
+ })
207
+ );
208
+ };
209
+ const scrubbed = walk(parsed);
210
+ return changed ? JSON.stringify(scrubbed) : text;
211
+ }
212
+
213
+ /** Redact OAuth secrets from a body or message: secret fields (decoded JSON
214
+ * keys, or JSON-like and form-encoded text), bearer/JWT/Anthropic tokens,
215
+ * and the given values. Numeric and status codes are kept. */
216
+ export function scrubOAuthSecrets(text, secretValues = []) {
217
+ const raw = String(text || '');
218
+ const json = scrubJsonSecrets(raw, secretValues);
219
+ if (json !== null) return json;
220
+ return scrubTokenText(
221
+ raw
222
+ .replace(JSON_SECRET_RE, (_match, key) => `"${key}":"[REDACTED]"`)
223
+ .replace(FORM_SECRET_RE, (_match, lead, key) => `${lead}${key}=[REDACTED]`),
224
+ secretValues
225
+ );
226
+ }
227
+
228
+ // --- Token-exchange failures a sign-in page may show ---
229
+
230
+ export const OAUTH_NETWORK_HINT = 'Check your proxy/VPN: Mixdog uses the system proxy, or HTTPS_PROXY.';
231
+ const NETWORK_CODE = /^(?:E[A-Z0-9]+|UND_ERR_[A-Z0-9_]+|ERR_[A-Z0-9_]+)$/;
232
+
233
+ /**
234
+ * The transport cause of a fetch that got no answer: its error code
235
+ * (ECONNRESET, UND_ERR_CONNECT_TIMEOUT, ENOTFOUND…) or "timed out". Only codes,
236
+ * never messages, which may quote anything. Null for a cancel or a non-network
237
+ * error.
238
+ */
239
+ export function oauthNetworkCause(error) {
240
+ if (!error || typeof error !== 'object' || error.name === 'AbortError') return null;
241
+ if (error.name === 'TimeoutError') return 'timed out';
242
+ for (let e = error, depth = 0; e && typeof e === 'object' && depth < 5; e = e.cause, depth += 1) {
243
+ if (typeof e.code === 'string' && NETWORK_CODE.test(e.code)) return e.code;
244
+ }
245
+ return error.name === 'TypeError' ? 'network error' : null;
246
+ }
247
+
248
+ // Errors whose message was built only from codes, statuses and redacted
249
+ // bodies: the only ones a failure page quotes.
250
+ const publicErrors = new WeakSet();
251
+
252
+ /** Mark `error` as safe to show on the sign-in failure page. */
253
+ export function oauthPublicError(error) {
254
+ publicErrors.add(error);
255
+ return error;
256
+ }
257
+
258
+ /** A fetch failure as `<operation> request failed (<cause>). <hint>`; other errors unchanged. */
259
+ export function oauthRequestError(operation, error) {
260
+ const cause = oauthNetworkCause(error);
261
+ return cause ? oauthPublicError(new Error(`${operation} request failed (${cause}). ${OAUTH_NETWORK_HINT}`)) : error;
262
+ }
263
+
264
+ /** A token endpoint's body for an error message: secrets redacted, whitespace folded, shortened. */
265
+ export function oauthBodySummary(text, secretValues = []) {
266
+ return scrubOAuthSecrets(text, secretValues).replace(/\s+/g, ' ').trim().slice(0, 500);
267
+ }
268
+
269
+ /** A token endpoint's HTTP failure as `<operation> <status>: <redacted body>`. */
270
+ export function oauthHttpError(operation, status, text, secretValues = []) {
271
+ const summary = oauthBodySummary(text, secretValues);
272
+ return oauthPublicError(new Error(`${operation} ${status}${summary ? `: ${summary}` : ''}`));
273
+ }
274
+
275
+ /** The token response's JSON. A body that does not parse is named by its
276
+ * status only: the parser's message quotes the body, tokens included. */
277
+ export async function oauthResponseJson(response, operation) {
278
+ try {
279
+ return await response.json();
280
+ } catch (error) {
281
+ if (error?.name !== 'SyntaxError') throw oauthRequestError(operation, error);
282
+ throw oauthPublicError(new Error(`${operation} returned a malformed response (HTTP ${response.status})`));
283
+ }
284
+ }
285
+
286
+ /** The failure page reason: the summary, plus the error's message only when it is page-safe. */
287
+ export function oauthFailureReason(summary, error) {
288
+ return error && publicErrors.has(error) ? `${summary}\n${error.message}` : summary;
289
+ }
@@ -195,7 +195,7 @@ export class MimoOAuthProvider {
195
195
  // publishes nothing. `_listRows` revalidates `seq` and the key before
196
196
  // touching shared state, since enrichment runs after this returns.
197
197
  let published = true;
198
- if (items.length) published = publish(items.map(rosterRow));
198
+ if (items.length) published = await publish(items.map(rosterRow));
199
199
  else items = seedModelItems();
200
200
  return { items, published, seq, apiKey };
201
201
  }