coderouter-cli 2.11.1__tar.gz → 2.11.2__tar.gz

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 (295) hide show
  1. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/.gitignore +5 -0
  2. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/CHANGELOG.md +89 -0
  3. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/PKG-INFO +1 -1
  4. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/vscode_init.py +172 -16
  5. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/vscode.md +26 -0
  6. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/pyproject.toml +1 -1
  7. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_gui_devices.py +4 -2
  8. coderouter_cli-2.11.2/tests/test_launcher_gui_events.py +498 -0
  9. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_gui_readiness.py +17 -6
  10. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_variant_gui.py +4 -2
  11. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_vscode_init.py +180 -0
  12. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/LICENSE +0 -0
  13. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/README.en.md +0 -0
  14. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/README.md +0 -0
  15. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/__init__.py +0 -0
  16. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/__main__.py +0 -0
  17. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/adapters/__init__.py +0 -0
  18. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/adapters/anthropic_native.py +0 -0
  19. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/adapters/base.py +0 -0
  20. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/adapters/openai_compat.py +0 -0
  21. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/adapters/registry.py +0 -0
  22. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/cli.py +0 -0
  23. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/cli_stats.py +0 -0
  24. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/config/__init__.py +0 -0
  25. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/config/capability_registry.py +0 -0
  26. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/config/env_file.py +0 -0
  27. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/config/loader.py +0 -0
  28. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/config/schemas.py +0 -0
  29. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/cost.py +0 -0
  30. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/data/__init__.py +0 -0
  31. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/data/model-capabilities.yaml +0 -0
  32. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/doctor.py +0 -0
  33. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/doctor_apply.py +0 -0
  34. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/env_security.py +0 -0
  35. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/errors.py +0 -0
  36. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/gguf_introspect.py +0 -0
  37. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/guards/__init__.py +0 -0
  38. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/guards/_fingerprint.py +0 -0
  39. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/guards/backend_health.py +0 -0
  40. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/guards/context_budget.py +0 -0
  41. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/guards/continuous_probe.py +0 -0
  42. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/guards/drift_actions.py +0 -0
  43. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/guards/drift_detection.py +0 -0
  44. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/guards/memory_budget.py +0 -0
  45. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/guards/memory_pressure.py +0 -0
  46. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/guards/self_healing.py +0 -0
  47. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/guards/tool_loop.py +0 -0
  48. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/hardware.py +0 -0
  49. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/ingress/__init__.py +0 -0
  50. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/ingress/anthropic_routes.py +0 -0
  51. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/ingress/app.py +0 -0
  52. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/ingress/dashboard_routes.py +0 -0
  53. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/ingress/launcher_routes.py +0 -0
  54. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/ingress/metrics_routes.py +0 -0
  55. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/ingress/openai_routes.py +0 -0
  56. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/language_tax.py +0 -0
  57. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/launcher_devices.py +0 -0
  58. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/launcher_speculative.py +0 -0
  59. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/launcher_swap.py +0 -0
  60. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/logging.py +0 -0
  61. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/metrics/__init__.py +0 -0
  62. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/metrics/collector.py +0 -0
  63. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/metrics/prometheus.py +0 -0
  64. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/output_filters.py +0 -0
  65. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/plugins/__init__.py +0 -0
  66. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/plugins/base.py +0 -0
  67. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/plugins/loader.py +0 -0
  68. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/plugins/registry.py +0 -0
  69. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/routing/__init__.py +0 -0
  70. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/routing/adaptive.py +0 -0
  71. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/routing/auto_router.py +0 -0
  72. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/routing/budget.py +0 -0
  73. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/routing/capability.py +0 -0
  74. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/routing/fallback.py +0 -0
  75. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/state/__init__.py +0 -0
  76. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/state/audit_log.py +0 -0
  77. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/state/replay.py +0 -0
  78. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/state/request_log.py +0 -0
  79. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/state/store.py +0 -0
  80. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/state/suggest_rules.py +0 -0
  81. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/token_estimation.py +0 -0
  82. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/token_estimation_accurate.py +0 -0
  83. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/translation/__init__.py +0 -0
  84. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/translation/anthropic.py +0 -0
  85. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/translation/convert.py +0 -0
  86. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/coderouter/translation/tool_repair.py +0 -0
  87. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/README.en.md +0 -0
  88. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/README.md +0 -0
  89. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/assets/dashboard-demo.png +0 -0
  90. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
  91. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
  92. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/external-agents.en.md +0 -0
  93. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/external-agents.md +0 -0
  94. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/gguf_dl.en.md +0 -0
  95. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/gguf_dl.md +0 -0
  96. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/hf-ollama-models.en.md +0 -0
  97. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/hf-ollama-models.md +0 -0
  98. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/install-backends.en.md +0 -0
  99. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/install-backends.md +0 -0
  100. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/launcher-quickstart.en.md +0 -0
  101. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/launcher-quickstart.md +0 -0
  102. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/launcher.en.md +0 -0
  103. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/launcher.md +0 -0
  104. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/llamacpp-direct.en.md +0 -0
  105. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/llamacpp-direct.md +0 -0
  106. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/lmstudio-direct.en.md +0 -0
  107. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/lmstudio-direct.md +0 -0
  108. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/backends/verify-ollama-0.23.1.md +0 -0
  109. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/concepts/architecture.en.md +0 -0
  110. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/concepts/architecture.md +0 -0
  111. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/concepts/context-budget.en.md +0 -0
  112. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/concepts/context-budget.md +0 -0
  113. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/concepts/continuous-probing.en.md +0 -0
  114. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/concepts/continuous-probing.md +0 -0
  115. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/concepts/drift-detection.en.md +0 -0
  116. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/concepts/drift-detection.md +0 -0
  117. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/concepts/partial-stitch.en.md +0 -0
  118. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/concepts/partial-stitch.md +0 -0
  119. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/designs/agent-cli-plugin-extraction.md +0 -0
  120. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/designs/external-agents-adapter.md +0 -0
  121. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/designs/launcher-model-swap.md +0 -0
  122. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/designs/launcher-multi-build.md +0 -0
  123. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/designs/orchestration-companion.md +0 -0
  124. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  125. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/designs/v1.6-auto-router-verification.md +0 -0
  126. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/designs/v1.6-auto-router.md +0 -0
  127. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/free-tier-guide.en.md +0 -0
  128. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/free-tier-guide.md +0 -0
  129. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/language-tax.en.md +0 -0
  130. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/language-tax.md +0 -0
  131. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/remote-access.en.md +0 -0
  132. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/remote-access.md +0 -0
  133. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/security.en.md +0 -0
  134. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/security.md +0 -0
  135. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/subagent-routing.en.md +0 -0
  136. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/subagent-routing.md +0 -0
  137. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/troubleshooting.en.md +0 -0
  138. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/troubleshooting.md +0 -0
  139. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/usage-guide.en.md +0 -0
  140. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/guides/usage-guide.md +0 -0
  141. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/low-memory-integration.en.md +0 -0
  142. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/low-memory-integration.md +0 -0
  143. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/openrouter-roster/CHANGES.md +0 -0
  144. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/openrouter-roster/README.md +0 -0
  145. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/openrouter-roster/latest.json +0 -0
  146. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/retrospectives/v0.4.md +0 -0
  147. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/retrospectives/v0.5-verify.md +0 -0
  148. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/retrospectives/v0.5.md +0 -0
  149. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/retrospectives/v0.6.md +0 -0
  150. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/retrospectives/v0.7.md +0 -0
  151. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/retrospectives/v1.0-verify.md +0 -0
  152. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/retrospectives/v1.0.md +0 -0
  153. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/start/quickstart.en.md +0 -0
  154. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/start/quickstart.md +0 -0
  155. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/start/when-do-i-need-coderouter.en.md +0 -0
  156. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/docs/start/when-do-i-need-coderouter.md +0 -0
  157. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/examples/.env.example +0 -0
  158. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/examples/README.md +0 -0
  159. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/examples/providers-agent-cli.yaml +0 -0
  160. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/examples/providers-multiagent.yaml +0 -0
  161. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/examples/providers.context-budget-test.yaml +0 -0
  162. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/examples/providers.llamacpp-vllm.yaml +0 -0
  163. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/examples/providers.nvidia-nim.yaml +0 -0
  164. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/examples/providers.ollama-auto-custom.yaml +0 -0
  165. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/examples/providers.ollama-auto.yaml +0 -0
  166. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/examples/providers.ollama-free-chain.yaml +0 -0
  167. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/examples/providers.raspberrypi.yaml +0 -0
  168. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/examples/providers.swap.yaml +0 -0
  169. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/examples/providers.yaml +0 -0
  170. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/scripts/demo_traffic.sh +0 -0
  171. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/scripts/openrouter_roster_diff.py +0 -0
  172. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/scripts/smoke_v2_2.sh +0 -0
  173. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/scripts/verify-providers.yaml +0 -0
  174. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/scripts/verify_ollama_0_23.py +0 -0
  175. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/scripts/verify_v0_5.sh +0 -0
  176. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/scripts/verify_v1_0.sh +0 -0
  177. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/__init__.py +0 -0
  178. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/conftest.py +0 -0
  179. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_adapter_anthropic.py +0 -0
  180. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_agent_cli.py +0 -0
  181. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_audit_log.py +0 -0
  182. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_auto_router.py +0 -0
  183. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_auto_router_cjk.py +0 -0
  184. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_backend_health.py +0 -0
  185. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_budget.py +0 -0
  186. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_capability.py +0 -0
  187. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_capability_degraded_payload.py +0 -0
  188. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_capability_registry.py +0 -0
  189. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_capability_registry_cache_control.py +0 -0
  190. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_capability_tool_choice.py +0 -0
  191. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_claude_code_suitability.py +0 -0
  192. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_cli.py +0 -0
  193. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_cli_bind_warning.py +0 -0
  194. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_cli_stats.py +0 -0
  195. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_config.py +0 -0
  196. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_config_cwd_warning.py +0 -0
  197. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_config_shim_actions.py +0 -0
  198. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_context_budget.py +0 -0
  199. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_continuous_probe.py +0 -0
  200. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_dashboard_endpoint.py +0 -0
  201. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_doctor.py +0 -0
  202. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_doctor_apply.py +0 -0
  203. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_doctor_cache_probe.py +0 -0
  204. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_drift_actions.py +0 -0
  205. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_drift_detection.py +0 -0
  206. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_drift_detection_integration.py +0 -0
  207. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_env_file.py +0 -0
  208. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_env_security.py +0 -0
  209. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_errors.py +0 -0
  210. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_examples_yaml.py +0 -0
  211. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_examples_yaml_roundtrip.py +0 -0
  212. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fallback.py +0 -0
  213. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fallback_anthropic.py +0 -0
  214. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fallback_cache_control.py +0 -0
  215. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fallback_cache_observed.py +0 -0
  216. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fallback_cache_strip.py +0 -0
  217. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fallback_empty_response.py +0 -0
  218. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fallback_misconfig_warn.py +0 -0
  219. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fallback_paid_gate.py +0 -0
  220. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fallback_thinking.py +0 -0
  221. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fallback_tool_choice_emulate.py +0 -0
  222. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fix_h1_prometheus_drift.py +0 -0
  223. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fix_h2_h6_translation.py +0 -0
  224. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fix_h3_client_reuse.py +0 -0
  225. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fix_h4_h5_guards.py +0 -0
  226. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fix_h7_probe_restore.py +0 -0
  227. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fix_h8_launcher_auth.py +0 -0
  228. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fix_mA_routing.py +0 -0
  229. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fix_mB_adapters.py +0 -0
  230. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fix_mC_convert.py +0 -0
  231. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fix_mD_logging_metrics.py +0 -0
  232. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fix_mE_config_validation.py +0 -0
  233. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_fix_mF_ingress.py +0 -0
  234. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_gguf_introspect.py +0 -0
  235. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_guards_tool_loop.py +0 -0
  236. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_hardware.py +0 -0
  237. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_import_hygiene.py +0 -0
  238. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_ingress_anthropic.py +0 -0
  239. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_ingress_count_tokens.py +0 -0
  240. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_ingress_profile.py +0 -0
  241. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_language_tax.py +0 -0
  242. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_language_tax_integration.py +0 -0
  243. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_backend_variants.py +0 -0
  244. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_config_bench.py +0 -0
  245. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_devices.py +0 -0
  246. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_devices_detect.py +0 -0
  247. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_devices_routes.py +0 -0
  248. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_mtp.py +0 -0
  249. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_mtp_fallback.py +0 -0
  250. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_provider_sync.py +0 -0
  251. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_readiness_restart.py +0 -0
  252. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_swap.py +0 -0
  253. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_swap_review.py +0 -0
  254. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_variant_config.py +0 -0
  255. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_variant_routes.py +0 -0
  256. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_launcher_variant_sweep.py +0 -0
  257. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_memory_budget.py +0 -0
  258. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_memory_pressure.py +0 -0
  259. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_metrics_cache.py +0 -0
  260. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_metrics_collector.py +0 -0
  261. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_metrics_cost.py +0 -0
  262. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_metrics_endpoint.py +0 -0
  263. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_metrics_jsonl.py +0 -0
  264. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_metrics_prometheus.py +0 -0
  265. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_metrics_prometheus_cache.py +0 -0
  266. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_models_passthrough.py +0 -0
  267. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_openai_compat.py +0 -0
  268. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_openrouter_roster_diff.py +0 -0
  269. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_output_filters.py +0 -0
  270. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_output_filters_adapters.py +0 -0
  271. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_partial_stitch.py +0 -0
  272. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_plugin_adapter.py +0 -0
  273. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_plugins_integration.py +0 -0
  274. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_plugins_loader.py +0 -0
  275. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_plugins_registry.py +0 -0
  276. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_reasoning_strip.py +0 -0
  277. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_repair_byte_fallback.py +0 -0
  278. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_request_log.py +0 -0
  279. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_role_normalization.py +0 -0
  280. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_routing_adaptive.py +0 -0
  281. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_self_healing.py +0 -0
  282. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_setup_sh.py +0 -0
  283. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_state_store.py +0 -0
  284. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_stop_reason_forward_compat.py +0 -0
  285. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_token_estimation.py +0 -0
  286. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_token_estimation_accurate.py +0 -0
  287. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_tokens_saved_metric.py +0 -0
  288. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_tool_repair.py +0 -0
  289. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_tool_repair_differential.py +0 -0
  290. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_tool_repair_lenient.py +0 -0
  291. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_tool_repair_r4.py +0 -0
  292. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_tool_repair_scanners.py +0 -0
  293. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_toolrepair_bench.py +0 -0
  294. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_translation_anthropic.py +0 -0
  295. {coderouter_cli-2.11.1 → coderouter_cli-2.11.2}/tests/test_translation_reverse.py +0 -0
@@ -9,6 +9,11 @@ secrets.env
9
9
  ~/.coderouter/
10
10
  CLAUDE.md
11
11
 
12
+ # *.bak — pre-overwrite backups of the files above (e.g. `vscode-init --force`
13
+ # saves `.envrc.bak` before clobbering `.envrc`; `doctor --apply` saves
14
+ # `providers.yaml.bak`). Same secret-risk as the originals, so same rule.
15
+ *.bak
16
+
12
17
  # ============================================================
13
18
  # Python
14
19
  # ============================================================
@@ -9,6 +9,95 @@ are kept verbatim where the Japanese text itself is the subject).
9
9
 
10
10
  ---
11
11
 
12
+ ## [v2.11.2] — 2026-08-04 (file-write safety and launcher event hygiene)
13
+
14
+ Two fixes from the same review round as v2.11.1, split out because they
15
+ rewrite `_atomic_write` and the launcher GUI's event plumbing and deserved
16
+ their own rollback unit. **No breaking changes**: no config-schema field
17
+ changed, no CLI flag changed meaning, `--force` still replaces `.envrc`.
18
+ Existing `.vscode/settings.json` merge semantics are unchanged.
19
+
20
+ ### Fixed
21
+
22
+ - **`vscode-init --force` destroyed `.envrc` with no way back.** The
23
+ command replaced the file outright — no merge, no backup — so a
24
+ hand-added `source_env_if_exists .envrc.local`, the very pattern
25
+ `docs/guides/vscode.md` recommends, vanished silently. direnv then
26
+ looked healthy while exporting nothing, which is a hard failure to
27
+ trace back to its cause. The conflict message said only "Re-run with
28
+ `--force` to overwrite" and never mentioned that the whole file goes.
29
+ The previous contents are now copied to `.envrc.bak` before the write,
30
+ the path is reported in the command's output, and the conflict message
31
+ spells out that this is a whole-file replacement rather than a merge.
32
+ `--dry-run` makes no backup. Only one generation is kept, so a second
33
+ `--force` overwrites the first `.bak` (`vscode_init.py`).
34
+ - **The launcher GUI could lose track of a running backend because of
35
+ that backend's own log output.** `_log_queue` carried control events
36
+ and raw child stdout in the same channel, with the drain loop
37
+ dispatching on `line.startswith("_ERR_:")` and friends. A child line
38
+ beginning `_ERR_:` ran `del self.processes[proc_id]`, so a live
39
+ llama-server dropped out of the process table — unstoppable from the
40
+ UI, missed by the shutdown handler, holding its port and VRAM until
41
+ killed by hand. A line beginning `_SPAWNED_:` with too few colons
42
+ raised `ValueError` inside the drain loop and discarded that tick's
43
+ log processing entirely. This is reachable in ordinary use: llama.cpp
44
+ echoes GGUF metadata such as the chat template verbatim at startup,
45
+ and `extra_args` wrappers put arbitrary text on the same stream.
46
+ Events are now `(kind, proc_id, payload)` with `kind` in
47
+ `log / spawned / ready / error / devices`; raw child output is
48
+ enqueued unconditionally as `log` and never inspected for markers.
49
+ `_cr_log_queue` got the same treatment, the `"_DEVICES_"` proc_id
50
+ sentinel is gone, and a malformed control payload now skips its own
51
+ event instead of killing the loop. While here, `_SPAWNED_` parsing
52
+ moved from `split(":", 2)` to `rpartition`, so model names containing
53
+ a colon no longer mangle the port (`launcher_gui.py`).
54
+
55
+ ### Security
56
+
57
+ - **`vscode-init` widened the permissions of the files it rewrote.**
58
+ `_atomic_write` created its temp file under the process umask and
59
+ `os.replace`d it over the target, so an `.envrc` hardened to `0600` —
60
+ a file that holds `ANTHROPIC_AUTH_TOKEN` — came back `0644`. The temp
61
+ path was also fully predictable (`.envrc.tmp`, `settings.json.tmp`),
62
+ so a pre-planted symlink at that path was followed and its target
63
+ overwritten (CWE-377), and two concurrent runs in one workspace raced
64
+ on the same name. Writes now go through `tempfile.mkstemp` with
65
+ `O_EXCL`, are `fsync`ed, restore the original mode before the rename,
66
+ and clean up the temp file on failure. A newly created `.envrc` is
67
+ written `0600`, matching the standard `coderouter doctor --check-env`
68
+ already enforces for credential-bearing files; an existing file keeps
69
+ whatever mode the operator set. Mode handling is POSIX-only —
70
+ `os.chmod` on Windows only toggles the read-only bit and is skipped.
71
+ `.vscode/settings.json` is backed up before any rewrite too, not just
72
+ under `--force`, since the merge path also edits it in place
73
+ (`vscode_init.py`).
74
+
75
+ ### Changed
76
+
77
+ - `.gitignore` now excludes `*.bak`, so the backups above and
78
+ `providers.yaml.bak` from `doctor --apply` stay out of commits. They
79
+ carry the same secrets as the files they shadow.
80
+ - Three GUI test modules documented their `pytest.importorskip("tkinter")`
81
+ as a CI workaround, stating that the uv-managed Python on CI has no
82
+ tkinter and that these tests are therefore skipped there. Measured on
83
+ 2026-08-04: `uv python install` ships Tk 8.6 on both 3.12.11 and
84
+ 3.13.7, and all 93 cases in those files run and pass on CI. The
85
+ docstrings now say so, and record that the guards stay for local
86
+ system pythons built without Tk — which do exist.
87
+
88
+ ### Tests
89
+
90
+ - 1 new file, 38 new cases (`test_launcher_gui_events.py`, plus
91
+ additions to `test_vscode_init.py`). Suite: 2345 passed, 1 skipped,
92
+ ruff clean.
93
+ - Each fix has a regression test confirmed to fail against the pre-fix
94
+ tree — 10 for the write path, 7 for the event path. The headline one
95
+ asserts a live process survives its own `_ERR_:` stdout line; on the
96
+ old code `app.processes` comes back empty. Another plants a symlink at
97
+ the predictable temp path and asserts its target is untouched.
98
+
99
+ ---
100
+
12
101
  ## [v2.11.1] — 2026-08-04 (second-round review: correctness & hardening)
13
102
 
14
103
  Seven fixes from a full-source review of the 36k-line package, plus two
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.11.1
3
+ Version: 2.11.2
4
4
  Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
5
5
  Project-URL: Homepage, https://github.com/zephel01/CodeRouter
6
6
  Project-URL: Repository, https://github.com/zephel01/CodeRouter
@@ -16,11 +16,21 @@ The design contract:
16
16
  without ``--force`` and print a unified diff so the operator decides.
17
17
  * ``--dry-run`` is byte-identical to the write path minus the actual
18
18
  ``os.replace`` at the end.
19
- * All writes are atomic (tmp file + ``os.replace``) so a partial write
20
- cannot corrupt an existing ``settings.json``. This matters more here
21
- than in most CodeRouter modules because the file we're editing is
22
- workspace-critical: a broken JSON blob would silently break every
23
- VSCode terminal env until a human debugged it.
19
+ * All writes are atomic (``tempfile.mkstemp`` + ``fsync`` +
20
+ ``os.replace``) so a partial write cannot corrupt an existing
21
+ ``settings.json``. This matters more here than in most CodeRouter
22
+ modules because the file we're editing is workspace-critical: a
23
+ broken JSON blob would silently break every VSCode terminal env
24
+ until a human debugged it.
25
+ * Nothing an operator wrote is destroyed without a copy: before any
26
+ existing file is rewritten, its previous bytes (and mode) land in
27
+ ``<name>.bak``. ``--force`` on ``.envrc`` is a whole-file replace,
28
+ not a merge, so this is the only thing standing between the user and
29
+ a lost ``source_env_if_exists .envrc.local`` line.
30
+ * A freshly generated ``.envrc`` is created 0600 — it carries
31
+ ``ANTHROPIC_AUTH_TOKEN`` and sits next to the operator's real
32
+ secrets. An existing file's mode is preserved across rewrites
33
+ instead of being reset to the umask default by ``os.replace``.
24
34
 
25
35
  Files touched (opt-in per flag):
26
36
 
@@ -40,8 +50,12 @@ directly without argparse in the way.
40
50
 
41
51
  from __future__ import annotations
42
52
 
53
+ import contextlib
43
54
  import json
44
55
  import os
56
+ import shutil
57
+ import stat
58
+ import tempfile
45
59
  from dataclasses import dataclass
46
60
  from difflib import unified_diff
47
61
  from pathlib import Path
@@ -62,6 +76,15 @@ DEFAULT_PORT = 8088
62
76
  # then flag the unused directive.)
63
77
  DEFAULT_TOKEN = "dummy"
64
78
 
79
+ # v2.11 (H-11): a generated .envrc carries ANTHROPIC_AUTH_TOKEN, and
80
+ # users routinely add their real key next to it (or point
81
+ # ``source_env_if_exists .envrc.local`` at one). ``env_security``
82
+ # already WARNs on any ``.env`` whose mode has 0o077 bits set and tells
83
+ # the operator to ``chmod 0600``; a file we generate ourselves should
84
+ # not be born failing that check. Applies to newly created files only —
85
+ # an existing file keeps whatever mode its owner chose.
86
+ _ENVRC_MODE = 0o600
87
+
65
88
  # VSCode reads terminal env from a separate key per host OS. Writing all
66
89
  # three means the same ``.vscode/`` folder works whether the developer
67
90
  # opens it on macOS, Linux, or WSL/Windows without a second scaffolder
@@ -261,7 +284,9 @@ def _apply_settings_json(
261
284
  diff=diff,
262
285
  reason=(
263
286
  "existing settings.json has different value(s) for "
264
- f"{', '.join(sorted(conflicts))}. Re-run with --force to overwrite."
287
+ f"{', '.join(sorted(conflicts))}. Re-run with --force to "
288
+ "overwrite those keys (unrelated keys are preserved; the "
289
+ f"previous file is saved to {_backup_path(path).name})."
265
290
  ),
266
291
  )
267
292
 
@@ -270,7 +295,24 @@ def _apply_settings_json(
270
295
  return FileOutcome(path=path, action="unchanged")
271
296
 
272
297
  diff = _text_diff(old_text, new_text, path)
298
+ reason = ""
273
299
  if not dry_run:
300
+ # H-11: same deal as .envrc. The merge preserves unrelated keys
301
+ # by design, but a bug in _merge_terminal_env would rewrite a
302
+ # hand-tuned settings.json with no way back — so keep the old
303
+ # bytes. Unlike .envrc this is not gated on --force: an additive
304
+ # merge rewrites the file without --force too, and that is
305
+ # exactly the path a merge bug would take.
306
+ if old_text:
307
+ backup = _make_backup(path)
308
+ reason = (
309
+ f"previous contents backed up to {backup}"
310
+ if backup is not None
311
+ else (
312
+ f"could not write {_backup_path(path).name} — "
313
+ "previous contents were NOT preserved"
314
+ )
315
+ )
274
316
  try:
275
317
  path.parent.mkdir(parents=True, exist_ok=True)
276
318
  _atomic_write(path, new_text)
@@ -281,11 +323,14 @@ def _apply_settings_json(
281
323
  reason=f"write failed: {exc}",
282
324
  diff=diff,
283
325
  )
326
+ elif old_text:
327
+ reason = f"would back up previous contents to {_backup_path(path)}"
284
328
 
285
329
  return FileOutcome(
286
330
  path=path,
287
331
  action="updated" if old_text else "created",
288
332
  diff=diff,
333
+ reason=reason,
289
334
  )
290
335
 
291
336
 
@@ -374,15 +419,32 @@ def _apply_envrc(
374
419
  diff=diff,
375
420
  reason=(
376
421
  ".envrc already exists and its contents differ. "
377
- "Re-run with --force to overwrite. "
422
+ "Re-run with --force to replace the WHOLE file with the "
423
+ "generated contents (this is not a merge — custom lines "
424
+ "such as `source_env_if_exists .envrc.local` are dropped); "
425
+ f"the previous contents are saved to {_backup_path(path).name}. "
378
426
  "(Tip: keep secrets in a separate .envrc.local and "
379
427
  "``source_env_if_exists`` from .envrc.)"
380
428
  ),
381
429
  )
382
430
 
431
+ reason = ""
383
432
  if not dry_run:
433
+ # H-11: --force replaces the whole file, so stash the old bytes
434
+ # (mode included) before they are gone. Inside the dry_run guard
435
+ # on purpose — --dry-run must not touch the filesystem at all.
436
+ if old_text:
437
+ backup = _make_backup(path)
438
+ reason = (
439
+ f"previous contents backed up to {backup}"
440
+ if backup is not None
441
+ else (
442
+ f"could not write {_backup_path(path).name} — "
443
+ "previous contents were NOT preserved"
444
+ )
445
+ )
384
446
  try:
385
- _atomic_write(path, new_text)
447
+ _atomic_write(path, new_text, mode=_ENVRC_MODE)
386
448
  except OSError as exc:
387
449
  return FileOutcome(
388
450
  path=path,
@@ -390,11 +452,14 @@ def _apply_envrc(
390
452
  reason=f"write failed: {exc}",
391
453
  diff=diff,
392
454
  )
455
+ elif old_text:
456
+ reason = f"would back up previous contents to {_backup_path(path)}"
393
457
 
394
458
  return FileOutcome(
395
459
  path=path,
396
460
  action="updated" if old_text else "created",
397
461
  diff=diff,
462
+ reason=reason,
398
463
  )
399
464
 
400
465
 
@@ -538,21 +603,112 @@ def _dump_json(obj: object) -> str:
538
603
  return json.dumps(obj, indent=2, ensure_ascii=False) + "\n"
539
604
 
540
605
 
541
- def _atomic_write(path: Path, text: str) -> None:
542
- """Write ``text`` to ``path`` atomically (tmp + os.replace).
606
+ def _default_file_mode() -> int:
607
+ """Return the mode a plain ``open(..., "w")`` would produce.
608
+
609
+ ``tempfile.mkstemp`` deliberately creates its file 0600. That is the
610
+ right default for a secret-bearing ``.envrc``, but it would silently
611
+ tighten ``settings.json`` (previously umask-derived, typically
612
+ 0644). Reading the umask back keeps the pre-existing behaviour for
613
+ files that have no explicit mode requirement.
614
+ """
615
+ current = os.umask(0)
616
+ os.umask(current)
617
+ return 0o666 & ~current
618
+
619
+
620
+ def _atomic_write(path: Path, text: str, *, mode: int | None = None) -> None:
621
+ """Write ``text`` to ``path`` atomically (tmp + ``os.replace``).
543
622
 
544
623
  A partial write cannot corrupt the target file: the tmp file lives
545
624
  on the same filesystem (same parent directory) so ``os.replace`` is
546
625
  an atomic inode swap on POSIX. On Windows ``os.replace`` is also
547
626
  atomic since Python 3.3.
548
627
 
549
- The tmp file uses the parent + ``<name>.tmp`` pattern rather than
550
- ``Path.with_suffix`` because ``.envrc`` has no stem/suffix in the
551
- ``Path`` sense and ``with_suffix`` would silently misbehave.
628
+ v2.11 hardening (H-11). The tmp file used to be the predictable
629
+ ``<name>.tmp`` next to the target, written with ``Path.write_text``:
630
+
631
+ * a pre-planted symlink at that exact path was *followed*, so the
632
+ write landed on the link's target (CWE-377);
633
+ * two concurrent ``vscode-init`` runs on the same workspace
634
+ clobbered each other's tmp file;
635
+ * ``os.replace`` swaps inodes, so the target's mode was reset to the
636
+ umask default — a 0600 ``.envrc`` holding ``ANTHROPIC_AUTH_TOKEN``
637
+ silently became 0644;
638
+ * without ``fsync`` a crash right after the rename could leave a
639
+ zero-length file.
640
+
641
+ So: ``tempfile.mkstemp`` (random name, ``O_EXCL``, no symlink
642
+ following), ``fsync`` before the rename, mode restored from the
643
+ file being replaced (or from ``mode`` for a fresh file), and the tmp
644
+ file unlinked in ``finally`` so a failed write leaves no debris.
645
+
646
+ Parameters
647
+ ----------
648
+ mode:
649
+ POSIX mode for a *newly created* file. Ignored when ``path``
650
+ already exists — an existing file's own mode wins, so we never
651
+ loosen (or tighten) what the operator chose. ``None`` means
652
+ "whatever the umask would have given us".
653
+ """
654
+ existing_mode: int | None = None
655
+ try:
656
+ existing_mode = stat.S_IMODE(path.stat().st_mode)
657
+ except OSError:
658
+ existing_mode = None
659
+
660
+ target_mode = existing_mode if existing_mode is not None else mode
661
+ if target_mode is None:
662
+ target_mode = _default_file_mode()
663
+
664
+ fd, tmp_name = tempfile.mkstemp(
665
+ dir=str(path.parent),
666
+ prefix=f".{path.name}.",
667
+ suffix=".tmp",
668
+ )
669
+ tmp = Path(tmp_name)
670
+ try:
671
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
672
+ handle.write(text)
673
+ handle.flush()
674
+ os.fsync(handle.fileno())
675
+ # Windows has no POSIX mode bits worth honouring; os.chmod there
676
+ # only toggles the read-only flag, which would be a surprising
677
+ # side effect of "preserve the mode".
678
+ if os.name != "nt":
679
+ os.chmod(tmp, target_mode)
680
+ os.replace(tmp, path)
681
+ finally:
682
+ # Best effort: a leftover tmp file is noise, not corruption, and
683
+ # cleaning it up must never mask the original exception.
684
+ with contextlib.suppress(OSError):
685
+ tmp.unlink(missing_ok=True)
686
+
687
+
688
+ def _backup_path(path: Path) -> Path:
689
+ """Return the ``.bak`` sibling used to preserve ``path``'s old bytes."""
690
+ return path.with_name(path.name + ".bak")
691
+
692
+
693
+ def _make_backup(path: Path) -> Path | None:
694
+ """Copy ``path`` to ``<name>.bak``, preserving mode and mtime.
695
+
696
+ Returns the backup path, or ``None`` when the copy failed (the
697
+ caller then proceeds without a backup rather than refusing to write
698
+ — but the failure is not silent, see the ``reason`` plumbing).
699
+
700
+ The destination is unlinked first: ``shutil.copy2`` opens the
701
+ destination for writing and would happily follow a symlink planted
702
+ at ``<name>.bak``. ``Path.unlink`` removes the link itself, so the
703
+ subsequent copy always creates a fresh regular file.
552
704
  """
553
- tmp = path.parent / (path.name + ".tmp")
554
- tmp.write_text(text, encoding="utf-8")
555
- os.replace(tmp, path)
705
+ backup = _backup_path(path)
706
+ try:
707
+ backup.unlink(missing_ok=True)
708
+ shutil.copy2(path, backup)
709
+ except OSError:
710
+ return None
711
+ return backup
556
712
 
557
713
 
558
714
  def _text_diff(old: str, new: str, path: Path) -> str:
@@ -64,6 +64,25 @@ coderouter vscode-init [--target PATH]
64
64
  - `--dry-run`: `.vscode/settings.json` に何を書くかの unified diff だけ表示
65
65
  - `--force`: 既存の `ANTHROPIC_BASE_URL` などが違う値だったとき上書き(既定はコンフリクト報告のみで書かない)
66
66
 
67
+ #### `--force` の挙動(v2.11 以降)
68
+
69
+ `--force` の効き方は 2 つのファイルで異なります。
70
+
71
+ - `.vscode/settings.json`: **マージ**です。CodeRouter が管理する 3 キー(`ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN` / `CODEROUTER_MODE`)だけを上書きし、`editor.fontSize` などの無関係なキーはそのまま残ります
72
+ - `.envrc`: **ファイル全体の置き換え**です。マージはしません。手で足した行(例: `source_env_if_exists .envrc.local`)は生成内容には含まれないため消えます
73
+
74
+ そのため、既存ファイルを書き換える前に**必ず退避コピーを作ります**。
75
+
76
+ | 元のファイル | 退避先 |
77
+ |---|---|
78
+ | `.envrc` | `.envrc.bak` |
79
+ | `.vscode/settings.json` | `.vscode/settings.json.bak` |
80
+
81
+ - 退避先のパスは実行結果の `→` 行に表示されます
82
+ - `--dry-run` では退避も含めて**一切ファイルを作りません**
83
+ - `.bak` は毎回上書きされます。2 世代前は残らないので、重要な `.envrc` は `--force` の前に自分でも控えを取るか、素直に `.envrc.local` へ分離してください
84
+ - `*.bak` はプロジェクトの `.gitignore` に入れておくことを推奨します
85
+
67
86
  ### 再実行しても壊れない
68
87
 
69
88
  `vscode-init` は冪等です。同じ引数で再実行すれば `unchanged` を報告して終了。異なる値と衝突した場合は `conflict` を出して**ファイルに触りません**(`--force` 必要)。オンボーディングスクリプトに含めて安全です。
@@ -76,6 +95,13 @@ coderouter vscode-init --with-envrc
76
95
 
77
96
  これで `.envrc` も生成されます。生成後に **`direnv allow`** を 1 回実行してください。シークレットは `.envrc.local` に分けて `source_env_if_exists .envrc.local` の形が安全です(`.envrc` はプロジェクトに commit することが多いので)。
78
97
 
98
+ パーミッションについて(v2.11 以降):
99
+
100
+ - **新規生成した `.envrc` は `0600`(所有者のみ読み書き)**で作られます。`ANTHROPIC_AUTH_TOKEN` を含むファイルなので、`coderouter --check-env` が `.env` に課しているのと同じ基準に揃えてあります
101
+ - **既存ファイルを上書きするときは、そのファイルの現在の mode をそのまま維持**します(以前は `os.replace` で umask 既定=多くの環境で `0644` に戻っていました)
102
+ - Windows では POSIX の mode ビットに意味がないため、mode の設定・維持は行いません
103
+ - なお `--force` で `source_env_if_exists .envrc.local` の行は消えます。消える前の内容は `.envrc.bak` に残るので、必要な行はそこから戻してください
104
+
79
105
  ### 注意点
80
106
 
81
107
  - `.vscode/` は CodeRouter リポジトリの `.gitignore` に既に入っているため、`.vscode/settings.json` は既定で **git に含まれません**。あなたのプロジェクトの `.gitignore` は個別に確認してください
@@ -11,7 +11,7 @@
11
11
  # in plan.md §11.B; once granted, this name will become an alias and
12
12
  # `coderouter` will become the canonical distribution name.
13
13
  name = "coderouter-cli"
14
- version = "2.11.1"
14
+ version = "2.11.2"
15
15
  description = "Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible."
16
16
  readme = "README.md"
17
17
  requires-python = ">=3.12"
@@ -5,8 +5,10 @@ bench-sweep pure logic was deliberately factored into Tk-independent
5
5
  module-level functions / dataclasses (mirroring the readiness/auto-restart
6
6
  split covered by tests/test_launcher_gui_readiness.py) so every case below
7
7
  exercises them without ever instantiating ``tk.Tk()`` / ``LauncherApp`` /
8
- ``SweepWindow`` — no display server, matching the desktop CI constraint
9
- (uv-managed CPython has no tkinter, hence the importorskip below).
8
+ ``SweepWindow`` — no display server needed. The importorskip below is not
9
+ for CI: uv-managed CPython does ship tkinter, so these tests really do run
10
+ there. It is for local system pythons built without Tk, which would
11
+ otherwise fail at collection time.
10
12
 
11
13
  Subprocess spawning is fully mocked: no real llama-server / llmbench ever
12
14
  runs. The shared, frozen core logic lives in coderouter.launcher_devices and