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
@@ -184,25 +184,27 @@ export function gmailAppPasswordServerInput(name, email, password) {
184
184
  }
185
185
 
186
186
  /** Whether several accounts of catalog connector `entry` can be connected at
187
- * once: Gmail (also by app password), Calendar and Drive with the Google
188
- * sign-in. Other remote servers keep one (their tokens and URL are shared). */
187
+ * once: every catalog connector holds its accounts in one server. */
189
188
  export function connectorMultiAccount(entry) {
190
- return entry?.backend?.kind === 'oauth' && entry.backend.oauthClient === 'google';
189
+ return Boolean(entry?.id) && connectorById(entry.id) === entry;
191
190
  }
192
191
 
193
- // ── One server per multi-account connector ──────────────────────────────────
194
- // A multi-account connector is ONE MCP server (`gmail`, `google-calendar`,
195
- // `google-drive`) holding its accounts:
192
+ // ── One server per connector ────────────────────────────────────────────────
193
+ // A catalog connector is ONE MCP server (`gmail`, `notion`, `postgres`…)
194
+ // holding its accounts:
196
195
  // { type: 'http', url: connectorAccountsUrl(id), connector: id,
197
- // accounts: [{ name, url, headers?, oauthClientId? }, …] }
198
- // Each account keeps the transport it had as a server of its own (the Google
199
- // sign-in URL its tokens are stored under, or the app-password address and
200
- // account header), so its keychain entries are untouched. Account names share
201
- // the server-name space (`gmail`, `gmail-2`…): a name-based call (remove, sign
202
- // in, sign out) that names an account acts on that account. The server is
203
- // answered in-process (connectors/connector-accounts-mcp.mjs): one tool set
204
- // with an `account` argument.
205
- const ACCOUNT_FIELDS = ['url', 'headers', 'oauthClientId'];
196
+ // accounts: [{ name, type?, url?, headers?, oauthClientId?, oauthKey?,
197
+ // command?, args?, env? }, …] }
198
+ // Each account keeps the transport it had as a server of its own (its remote
199
+ // URL and sign-in, its key header, its app-password address, or its local
200
+ // command and environment), so its keychain entries are untouched. A remote
201
+ // sign-in is stored under the account's URL, or under its own `oauthKey` when
202
+ // another account of the connector already signs in at that URL. Account names
203
+ // share the server-name space (`notion`, `notion-2`…): a name-based call
204
+ // (remove, sign in, sign out) that names an account acts on that account. The
205
+ // server is answered in-process (connectors/connector-accounts-mcp.mjs): one
206
+ // tool set with an `account` argument.
207
+ const ACCOUNT_FIELDS = ['type', 'url', 'headers', 'oauthClientId', 'oauthKey', 'command', 'args', 'env'];
206
208
 
207
209
  /** The reserved (never resolvable) address of connector `id`'s accounts server. */
208
210
  export function connectorAccountsUrl(id) {
@@ -211,14 +213,16 @@ export function connectorAccountsUrl(id) {
211
213
 
212
214
  const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
213
215
 
214
- /** The stored accounts of a multi-account connector server, [] for any other
215
- * config (a legacy one-account server included). */
216
+ /** The stored accounts of a connector server, [] for any other config (a
217
+ * legacy one-account server included). */
216
218
  export function connectorAccountsOf(cfg) {
217
- if (!isRecord(cfg) || !Array.isArray(cfg.accounts)) return [];
218
- if (!connectorMultiAccount(connectorById(connectorOfServer(cfg)))) return [];
219
+ if (!isRecord(cfg) || !Array.isArray(cfg.accounts) || !connectorOfServer(cfg)) return [];
219
220
  return cfg.accounts.filter(
220
221
  (account) =>
221
- isRecord(account) && typeof account.name === 'string' && account.name && typeof account.url === 'string'
222
+ isRecord(account) &&
223
+ typeof account.name === 'string' &&
224
+ account.name &&
225
+ (typeof account.url === 'string' || typeof account.command === 'string')
222
226
  );
223
227
  }
224
228
 
@@ -231,6 +235,12 @@ export function connectorAccountServer(cfg, account) {
231
235
  return out;
232
236
  }
233
237
 
238
+ /** `cfg` without what is an account's own connection: the settings of the
239
+ * server holding its accounts (timeouts, enabled…). */
240
+ export function connectorServerSettings(cfg) {
241
+ return Object.fromEntries(Object.entries(cfg).filter(([key]) => key !== 'accounts' && !ACCOUNT_FIELDS.includes(key)));
242
+ }
243
+
234
244
  /** Every account of `cfg` as its one-account server config. */
235
245
  export function connectorAccountServers(cfg) {
236
246
  return connectorAccountsOf(cfg).map((account) => connectorAccountServer(cfg, account));
@@ -264,33 +274,39 @@ export function withConnectorAccountEnabled(cfg, name, enabled) {
264
274
  return connectorAccountsOf(next).some(connectorAccountEnabled) ? next : { ...next, enabled: false };
265
275
  }
266
276
 
267
- /** The stored account entry of one-account config `cfg` under `name`. */
277
+ /** The stored account entry of one-account config `cfg` under `name` (an
278
+ * http transport is the default and is not stored). */
268
279
  export function connectorAccountEntry(name, cfg) {
269
280
  const out = { name };
270
281
  for (const field of ACCOUNT_FIELDS) if (cfg?.[field] !== undefined) out[field] = cfg[field];
282
+ if (out.type === 'http') delete out.type;
271
283
  return out;
272
284
  }
273
285
 
274
286
  const naturalOrder = (a, b) => a.localeCompare(b, 'en', { numeric: true });
275
287
 
288
+ /** What makes an account the same connection as another: its transport
289
+ * (URL, sign-in key, headers, command and environment), whatever its name. */
290
+ const connectionOf = (cfg, account) => JSON.stringify({ ...connectorAccountServer(cfg, account), name: '' });
291
+
276
292
  /**
277
- * Merges every multi-account connector's servers into one server holding the
278
- * accounts: legacy one-account servers (`gmail`, `gmail-2`…, Calendar and
279
- * Drive alike) and any second accounts server. The server keeps the name of
280
- * an existing accounts server, else the connector id when a server has it,
293
+ * Merges every catalog connector's servers into one server holding the
294
+ * accounts: legacy one-account servers (`notion`, `notion-2`…, Gmail, Calendar
295
+ * and Drive alike) and any second accounts server. The server keeps the name
296
+ * of an existing accounts server, else the connector id when a server has it,
281
297
  * else the first name; its other fields come from that server. Accounts keep
282
298
  * their names, transports and order (that server's first); a second entry
283
- * for the same name or app-password address is dropped. The merged server is
284
- * off only when every merged one was; otherwise the accounts of a merged
285
- * server that was off stay off (`enabled: false` on the account), and an
286
- * account's own `enabled: false` is kept. A server left with no switched-on
287
- * account is off. Idempotent: `merged` is empty when
288
- * nothing changed. Returns `{ servers, merged: [{ connector, name, from }] }`.
299
+ * for the same name, app-password address or connection is dropped. The
300
+ * merged server is off only when every merged one was; otherwise the accounts
301
+ * of a merged server that was off stay off (`enabled: false` on the account),
302
+ * and an account's own `enabled: false` is kept. A server left with no
303
+ * switched-on account is off. Idempotent: `merged` is empty when nothing
304
+ * changed. Returns `{ servers, merged: [{ connector, name, from }] }`.
289
305
  */
290
306
  export function mergeConnectorAccountServers(input) {
291
307
  const servers = isRecord(input) ? { ...input } : {};
292
308
  const merged = [];
293
- for (const entry of CONNECTOR_CATALOG.filter(connectorMultiAccount)) {
309
+ for (const entry of CONNECTOR_CATALOG) {
294
310
  const names = Object.keys(servers)
295
311
  .filter((name) => isRecord(servers[name]) && connectorOfServer(servers[name]) === entry.id)
296
312
  .sort(naturalOrder);
@@ -303,6 +319,7 @@ export function mergeConnectorAccountServers(input) {
303
319
  const accounts = [];
304
320
  const seenNames = new Set();
305
321
  const seenAddresses = new Set();
322
+ const seenConnections = new Set();
306
323
  for (const name of order) {
307
324
  const cfg = servers[name];
308
325
  const own = Array.isArray(cfg.accounts) ? connectorAccountsOf(cfg) : [connectorAccountEntry(name, cfg)];
@@ -311,21 +328,17 @@ export function mergeConnectorAccountServers(input) {
311
328
  const serverOff = !off && cfg.enabled === false;
312
329
  for (const account of own) {
313
330
  const address = gmailAppPasswordAccount(connectorAccountServer(cfg, account));
314
- if (seenNames.has(account.name) || (address && seenAddresses.has(address))) continue;
331
+ const connection = connectionOf(cfg, account);
332
+ if (seenNames.has(account.name) || (address && seenAddresses.has(address)) || seenConnections.has(connection)) {
333
+ continue;
334
+ }
315
335
  seenNames.add(account.name);
316
336
  if (address) seenAddresses.add(address);
337
+ seenConnections.add(connection);
317
338
  accounts.push(serverOff ? { ...account, enabled: false } : account);
318
339
  }
319
340
  }
320
- const {
321
- type: _type,
322
- url: _url,
323
- headers: _headers,
324
- oauthClientId: _client,
325
- accounts: _accounts,
326
- enabled: _enabled,
327
- ...rest
328
- } = servers[target];
341
+ const { enabled: _enabled, ...rest } = connectorServerSettings(servers[target]);
329
342
  for (const name of order) delete servers[name];
330
343
  servers[target] = {
331
344
  ...rest,
@@ -352,17 +365,12 @@ export function connectorUsesGoogleClient(entry) {
352
365
  }
353
366
 
354
367
  /** The MCP server input that connects catalog entry `entry` under `name`
355
- * (oauth / apiKey); `key` fills an apiKey backend; `extraAccount`
356
- * makes it one more account of a multi-account connector. */
357
- export function connectorServerInput(entry, name, key = '', { extraAccount = false } = {}) {
368
+ * (oauth / apiKey); `key` fills an apiKey backend. Every account keeps the
369
+ * service's real URL; another account at that URL gets its own sign-in key
370
+ * (resource-mcp-connector-accounts.mjs). */
371
+ export function connectorServerInput(entry, name, key = '') {
358
372
  const { backend } = entry;
359
- if (backend.kind === 'oauth') {
360
- // Tokens are stored per exact server URL, so another account of the same
361
- // connector needs its own URL; the in-process Google servers ignore the query.
362
- const url =
363
- extraAccount && connectorMultiAccount(entry) ? `${backend.url}?account=${encodeURIComponent(name)}` : backend.url;
364
- return { name, type: 'http', url, connector: entry.id };
365
- }
373
+ if (backend.kind === 'oauth') return { name, type: 'http', url: backend.url, connector: entry.id };
366
374
  if (backend.kind !== 'apiKey') throw new Error(`${entry.name} is not an MCP connector`);
367
375
  const value = String(key || '').trim();
368
376
  if (!value) throw new Error(`${entry.name} needs a key`);
@@ -0,0 +1,20 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const { getSecret } = require('../../lib/keychain-cjs.cjs');
5
+
6
+ // A custom provider's additional API keys are keychain secrets beside its
7
+ // primary key (`agent.<id>.apiKey`), one per key id of its roster
8
+ // (provider-accounts.mjs `setCustomProviderKeyIds`). Never config values.
9
+ export function customProviderKeyAccount(providerId, keyId) {
10
+ return `agent.${providerId}.apiKey.${keyId}`;
11
+ }
12
+
13
+ /** The stored value of additional key `keyId`, or '' when it is not readable. */
14
+ export function readCustomProviderKey(providerId, keyId) {
15
+ try {
16
+ return getSecret(customProviderKeyAccount(providerId, keyId)) || '';
17
+ } catch {
18
+ return '';
19
+ }
20
+ }
@@ -1,14 +1,22 @@
1
1
  import { closeSync, openSync, readSync } from 'node:fs';
2
+ import { open } from 'node:fs/promises';
2
3
  import { holdsNoData } from './atomic-file.mjs';
3
4
 
4
5
  const PROBE_BYTES = 4096;
5
6
 
7
+ // Only these errno codes mean the path names no file. Every other failure
8
+ // leaves the file present (a scanner's or the running process's lock, EIO, ...):
9
+ // this is an install check on settings and status paths, so it never throws.
10
+ const ABSENT_CODES = new Set(['ENOENT', 'ENOTDIR']);
11
+ const presentDespite = (err) => !ABSENT_CODES.has(err?.code);
12
+
6
13
  /**
7
14
  * Install check for a downloaded binary or model: false when `path` is absent
8
15
  * or its first bytes hold no data (empty, or NUL-filled by a crash that
9
16
  * committed the size but not the bytes). Cheaper than hashing a multi-GB file
10
17
  * on every use. A file that exists but cannot be opened right now (locked by a
11
- * scanner or by the process running it) still counts as present.
18
+ * scanner or by the process running it) still counts as present, as does any
19
+ * other failure.
12
20
  */
13
21
  export function fileHasData(path) {
14
22
  let fd;
@@ -17,7 +25,7 @@ export function fileHasData(path) {
17
25
  const head = Buffer.alloc(PROBE_BYTES);
18
26
  return !holdsNoData(head.subarray(0, readSync(fd, head, 0, PROBE_BYTES, 0)));
19
27
  } catch (err) {
20
- return err?.code !== 'ENOENT' && err?.code !== 'ENOTDIR';
28
+ return presentDespite(err);
21
29
  } finally {
22
30
  if (fd !== undefined) {
23
31
  try {
@@ -26,3 +34,19 @@ export function fileHasData(path) {
26
34
  }
27
35
  }
28
36
  }
37
+
38
+ /** Same verdict as `fileHasData`, without blocking the event loop on the open
39
+ * (an antivirus scan of a binary can stall a synchronous open for seconds). */
40
+ export async function fileHasDataAsync(path) {
41
+ let handle;
42
+ try {
43
+ handle = await open(path, 'r');
44
+ const head = Buffer.alloc(PROBE_BYTES);
45
+ const { bytesRead } = await handle.read(head, 0, PROBE_BYTES, 0);
46
+ return !holdsNoData(head.subarray(0, bytesRead));
47
+ } catch (err) {
48
+ return presentDespite(err);
49
+ } finally {
50
+ await handle?.close().catch(() => {});
51
+ }
52
+ }
@@ -1,16 +1,8 @@
1
- import {
2
- linkSync,
3
- mkdirSync,
4
- readdirSync,
5
- readFileSync,
6
- renameSync,
7
- statSync,
8
- unlinkSync,
9
- writeFileSync,
10
- } from 'node:fs';
1
+ import { linkSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
11
2
  import {
12
3
  link as linkAsync,
13
4
  mkdir as mkdirAsync,
5
+ readdir as readdirAsync,
14
6
  readFile as readFileAsync,
15
7
  stat as statAsync,
16
8
  unlink as unlinkAsync,
@@ -384,10 +376,16 @@ function lockStaleMs(opts) {
384
376
  // A staged owner record lives for one link call; a process killed between its
385
377
  // create and unlink leaves `<lock>.<pid>.<hex>.tmp` behind. Each lock path is
386
378
  // swept once per process for staged files older than this whose pid is dead.
379
+ // The sweep is cleanup only — no lock decision depends on it — so it runs
380
+ // beside the acquisition instead of in front of it: listing a crowded
381
+ // directory synchronously blocked every lock taker for seconds. A file it
382
+ // removes belongs to a dead pid and is past any link call, so it cannot race
383
+ // a live acquisition.
387
384
  const STAGED_LOCK_FILE_STALE_MS = 60_000;
388
385
  const sweptLockPaths = new Set();
389
386
 
390
- export function sweepStagedLockFiles(lockPath, now = Date.now()) {
387
+ /** Resolves with the number of staged files removed; 0 once the path was swept in this process. */
388
+ export async function sweepStagedLockFiles(lockPath, now = Date.now()) {
391
389
  if (sweptLockPaths.has(lockPath)) return 0;
392
390
  sweptLockPaths.add(lockPath);
393
391
  const dir = dirname(lockPath);
@@ -395,7 +393,7 @@ export function sweepStagedLockFiles(lockPath, now = Date.now()) {
395
393
  let removed = 0;
396
394
  let names;
397
395
  try {
398
- names = readdirSync(dir);
396
+ names = await readdirAsync(dir);
399
397
  } catch {
400
398
  return 0;
401
399
  }
@@ -405,9 +403,9 @@ export function sweepStagedLockFiles(lockPath, now = Date.now()) {
405
403
  if (!match) continue;
406
404
  const path = join(dir, name);
407
405
  try {
408
- if (now - statSync(path).mtimeMs < STAGED_LOCK_FILE_STALE_MS) continue;
406
+ if (now - (await statAsync(path)).mtimeMs < STAGED_LOCK_FILE_STALE_MS) continue;
409
407
  if (ownerIsLive({ pid: Number(match[1]), token: null })) continue;
410
- unlinkSync(path);
408
+ await unlinkAsync(path);
411
409
  removed += 1;
412
410
  } catch {
413
411
  // Another sweeper or a late unlink got there first.
@@ -421,7 +419,7 @@ function* acquireLock(lockPath, opts, mode, markHeld, timing = null) {
421
419
  const staleMs = lockStaleMs(opts);
422
420
  const deadline = Date.now() + timeoutMs;
423
421
  yield ['mkdirp', dirname(lockPath)];
424
- sweepStagedLockFiles(lockPath);
422
+ void sweepStagedLockFiles(lockPath);
425
423
  const waitStartedAt = Date.now();
426
424
  let attempt = 0;
427
425
  let lastError = null;
@@ -0,0 +1,9 @@
1
+ export function gitCommand(env?: NodeJS.ProcessEnv | null): string;
2
+ export function gitChildEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
3
+ export function gitChildEnv(env?: null): NodeJS.ProcessEnv | null;
4
+ export function forgetMissingGit(command: string, error: unknown): boolean;
5
+ export function startGit<T>(
6
+ env: NodeJS.ProcessEnv,
7
+ start: (command: string, env: NodeJS.ProcessEnv) => Promise<T>,
8
+ spawnError?: (outcome: T) => unknown
9
+ ): Promise<T>;
@@ -0,0 +1,213 @@
1
+ // The git executable product code starts, and the environment it starts with.
2
+ //
3
+ // On Windows `git` on PATH is usually Git for Windows' launcher,
4
+ // <root>\cmd\git.exe (or the identical <root>\bin\git.exe). It only prepares
5
+ // the environment and starts <root>\<msystem>\bin\git.exe as a second process,
6
+ // so every git child costs two process creations. When PATH's git is that
7
+ // launcher, children start the real binary directly with the environment the
8
+ // launcher would have built. The steps below follow git-wrapper.c
9
+ // (git-for-windows/MINGW-packages, mingw-w64-git/git-wrapper.c): main()'s
10
+ // `git.exe` branch and setup_environment(top_level_path, full_path=1) plus
11
+ // maybe_read_config(). Anything else — another platform, a scoop shim, MinGit,
12
+ // a git.exe outside such a layout, an ambiguous or incomplete install — keeps
13
+ // plain `git` and the caller's environment, exactly as before.
14
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
15
+ import { basename, delimiter, dirname, isAbsolute, join } from 'node:path';
16
+
17
+ // The wrapper's compile-time MSYSTEM, lower-cased, names its binary directory.
18
+ const MSYSTEM_DIRECTORIES = ['mingw64', 'clangarm64', 'mingw32'];
19
+
20
+ function isFile(path) {
21
+ try {
22
+ return statSync(path).isFile();
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function isDirectory(path) {
29
+ try {
30
+ return statSync(path).isDirectory();
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ // Windows names are case-insensitive. Of several spellings in one object
37
+ // Node passes the first in sorted order to the child, so that one is read and
38
+ // written.
39
+ function envKey(env, name) {
40
+ const upper = name.toUpperCase();
41
+ return (
42
+ Object.keys(env)
43
+ .filter((key) => key.toUpperCase() === upper)
44
+ .sort()[0] ?? name
45
+ );
46
+ }
47
+
48
+ function envValue(env, name) {
49
+ const value = env[envKey(env, name)];
50
+ return value === undefined || value === null ? undefined : String(value);
51
+ }
52
+
53
+ // The wrapper's my_path_append: one backslash between the parts.
54
+ function appendPath(list, part) {
55
+ return list.endsWith('\\') ? `${list}${part}` : `${list}\\${part}`;
56
+ }
57
+
58
+ // libuv's search for a bare `git` (no directory, no extension): each PATH
59
+ // entry in order, `git.com` before `git.exe`.
60
+ function launcherOnPath(pathValue) {
61
+ for (const raw of String(pathValue || '').split(delimiter)) {
62
+ const entry = raw.trim().replace(/^"(.*)"$/, '$1');
63
+ if (!entry) continue;
64
+ // A relative entry resolves against each child's cwd; not decidable once.
65
+ if (!isAbsolute(entry)) return null;
66
+ if (isFile(join(entry, 'git.com'))) return null;
67
+ const candidate = join(entry, 'git.exe');
68
+ if (isFile(candidate)) return candidate;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ // etc\git-bash.config `MSYS=` lines, each prepended to MSYS in file order.
74
+ function readMsysConfig(root) {
75
+ let text = '';
76
+ try {
77
+ text = readFileSync(join(root, 'etc', 'git-bash.config'), 'utf8');
78
+ } catch {
79
+ return [];
80
+ }
81
+ return text
82
+ .split('\n')
83
+ .map((line) => line.replace(/\r$/, ''))
84
+ .filter((line) => line.startsWith('MSYS='))
85
+ .map((line) => line.slice(5));
86
+ }
87
+
88
+ /**
89
+ * The Git for Windows layout behind PATH's `git`, or null when there is none
90
+ * to bypass. `{ command, root, msystem, msysConfig }`.
91
+ */
92
+ export function resolveGitLaunch({ platform = process.platform, pathValue = process.env.PATH } = {}) {
93
+ if (platform !== 'win32') return null;
94
+ const found = launcherOnPath(pathValue);
95
+ if (!found) return null;
96
+ let launcher;
97
+ try {
98
+ // The wrapper locates its install from its own final path.
99
+ launcher = realpathSync.native(found);
100
+ } catch {
101
+ return null;
102
+ }
103
+ if (basename(launcher).toLowerCase() !== 'git.exe') return null;
104
+ const launcherDirectory = dirname(launcher);
105
+ if (!['cmd', 'bin'].includes(basename(launcherDirectory).toLowerCase())) return null;
106
+ const root = dirname(launcherDirectory);
107
+ // The installer and PortableGit ship git-bash.exe; MinGit and other layouts do not.
108
+ if (!isFile(join(root, 'git-bash.exe')) || !isDirectory(join(root, 'usr', 'bin'))) return null;
109
+ const installed = MSYSTEM_DIRECTORIES.filter((name) => isFile(join(root, name, 'bin', 'git.exe')));
110
+ if (installed.length !== 1) return null;
111
+ const [msystemDirectory] = installed;
112
+ return {
113
+ command: join(root, msystemDirectory, 'bin', 'git.exe'),
114
+ root,
115
+ msystem: msystemDirectory.toUpperCase(),
116
+ msysConfig: readMsysConfig(root),
117
+ };
118
+ }
119
+
120
+ function isSystemDirectory(path) {
121
+ const system = join(process.env.SystemRoot || process.env.windir || 'C:\\Windows', 'System32');
122
+ return path.toLowerCase() === system.toLowerCase();
123
+ }
124
+
125
+ /** `base` as the launcher would hand it to the real git (setup_environment). */
126
+ export function gitLaunchEnvironment(launch, base) {
127
+ const env = { ...base };
128
+ const set = (name, value) => {
129
+ env[envKey(env, name)] = value;
130
+ };
131
+ set('MSYSTEM', launch.msystem);
132
+ if (envValue(env, 'PLINK_PROTOCOL') === undefined) set('PLINK_PROTOCOL', 'ssh');
133
+ if (envValue(env, 'HOME') === undefined) {
134
+ const homePath = envValue(env, 'HOMEPATH');
135
+ const drive = `${envValue(env, 'HOMEDRIVE') ?? ''}${homePath ?? ''}`;
136
+ if (homePath !== undefined && !isSystemDirectory(drive) && isDirectory(drive)) set('HOME', drive);
137
+ else if (envValue(env, 'USERPROFILE') !== undefined) set('HOME', envValue(env, 'USERPROFILE'));
138
+ }
139
+ // <root>\<msystem>\bin;<root>\usr\bin;<HOME>\bin; ahead of the inherited PATH.
140
+ const prefix =
141
+ `${appendPath(launch.root, `${launch.msystem.toLowerCase()}\\bin`)};` +
142
+ appendPath(launch.root, 'usr\\bin;') +
143
+ appendPath(envValue(env, 'HOME') ?? '', 'bin;');
144
+ set('PATH', `${prefix}${envValue(env, 'PATH') ?? ''}`);
145
+ for (const msys of launch.msysConfig) {
146
+ const current = envValue(env, 'MSYS');
147
+ set('MSYS', current === undefined ? msys : `${msys} ${current}`);
148
+ }
149
+ return env;
150
+ }
151
+
152
+ // PATH value -> its launch (or null), resolved once per PATH: a child is
153
+ // started with the git its own environment's PATH names, as bare `git` is.
154
+ const resolved = new Map();
155
+ function gitLaunch(env) {
156
+ const pathValue = envValue(env ?? process.env, 'PATH') ?? '';
157
+ if (!resolved.has(pathValue)) resolved.set(pathValue, resolveGitLaunch({ pathValue }));
158
+ return resolved.get(pathValue);
159
+ }
160
+
161
+ /** The program to spawn for git with `env` (null: this process's environment). */
162
+ export function gitCommand(env = null) {
163
+ return gitLaunch(env)?.command ?? 'git';
164
+ }
165
+
166
+ /**
167
+ * The environment for a git child: `env` (null: this process's environment)
168
+ * with the launcher's additions when {@link gitCommand} bypasses it. Without a
169
+ * bypass `env` is returned unchanged, null included.
170
+ */
171
+ export function gitChildEnv(env = null) {
172
+ const launch = gitLaunch(env);
173
+ return launch ? gitLaunchEnvironment(launch, env ?? process.env) : env;
174
+ }
175
+
176
+ /**
177
+ * Whether a spawn of `command` failed because that resolved binary is gone (an
178
+ * uninstalled or moved Git). Its resolutions are dropped then, so the next
179
+ * {@link gitCommand} resolves PATH afresh. ENOENT alone is not enough: a
180
+ * missing cwd reports it too, with the binary still in place.
181
+ */
182
+ export function forgetMissingGit(command, error) {
183
+ if (error?.code !== 'ENOENT' || command === 'git' || isFile(command)) return false;
184
+ for (const [pathValue, launch] of resolved) if (launch?.command === command) resolved.delete(pathValue);
185
+ return true;
186
+ }
187
+
188
+ /**
189
+ * Starts git as `start(command, childEnv)` and, when that spawn failed only
190
+ * because the resolved binary is gone, once more with a fresh resolution —
191
+ * where bare `git` would have found whatever PATH holds now. `start` rejects
192
+ * with the spawn error, or settles an outcome `spawnError` reads it from.
193
+ */
194
+ export async function startGit(env, start, spawnError = () => null) {
195
+ const command = gitCommand(env);
196
+ let outcome;
197
+ try {
198
+ outcome = await start(command, gitChildEnv(env));
199
+ } catch (error) {
200
+ if (!forgetMissingGit(command, error)) throw error;
201
+ return start(gitCommand(env), gitChildEnv(env));
202
+ }
203
+ if (!forgetMissingGit(command, spawnError(outcome))) return outcome;
204
+ return start(gitCommand(env), gitChildEnv(env));
205
+ }
206
+
207
+ /** {@link startGit} for a synchronous `start` (spawnSync). */
208
+ export function startGitSync(env, start, spawnError = (result) => result?.error) {
209
+ const command = gitCommand(env);
210
+ const outcome = start(command, gitChildEnv(env));
211
+ if (!forgetMissingGit(command, spawnError(outcome))) return outcome;
212
+ return start(gitCommand(env), gitChildEnv(env));
213
+ }
@@ -2,7 +2,8 @@
2
2
  // thread, never on the host event loop (run-child.mjs). Each request resolves
3
3
  // to { code, stdout, stderr } or an error with the same codes the in-process
4
4
  // runner used (ETIMEDOUT, EMAXBUFFER, spawn errors).
5
+ import { startGit } from './git-launch.mjs';
5
6
  import { runChild } from './run-child.mjs';
6
7
  import { serveWorkerRequests } from './worker-requests.mjs';
7
8
 
8
- serveWorkerRequests((request) => runChild({ ...request, command: 'git' }));
9
+ serveWorkerRequests((request) => startGit(request.env, (command, env) => runChild({ ...request, command, env })));
@@ -133,7 +133,12 @@ export function priceUsage(args) {
133
133
  // premium request needs the model's published Fast/Priority rates.
134
134
  const anthropicFast = premium && sentTier === 'fast' && supportsAnthropicFastMode(identity.pricingModel);
135
135
  const premiumPricing = sentTier === 'ultrafast' ? meta.ultrafastPricing : meta.fastPricing;
136
- const rateMeta = premium && !anthropicFast ? premiumPricing || {} : meta;
136
+ // A request the server reports as served on Flex (Vertex Flex PayGo can be
137
+ // served by Provisioned Throughput first) bills the catalog's flex columns.
138
+ const flex = args.serviceTier === 'flex';
139
+ let rateMeta = meta;
140
+ if (flex) rateMeta = meta.flexPricing || {};
141
+ else if (premium && !anthropicFast) rateMeta = premiumPricing || {};
137
142
  if (args.historicalAggregate && rateMeta.pricingTiers?.some((tier) => promptTokens > tier.aboveInputTokens)) {
138
143
  // A daily sum cannot establish which individual requests crossed a
139
144
  // context boundary. Do not price the whole day as one huge prompt.