flowjet 2.0.4__tar.gz → 2.0.5__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 (170) hide show
  1. {flowjet-2.0.4 → flowjet-2.0.5}/CHANGELOG.md +29 -3
  2. {flowjet-2.0.4 → flowjet-2.0.5}/PKG-INFO +3 -3
  3. {flowjet-2.0.4 → flowjet-2.0.5}/README.md +1 -1
  4. {flowjet-2.0.4 → flowjet-2.0.5}/docs/impl/IG-002-isolated-thread-pool-runtime.md +2 -2
  5. {flowjet-2.0.4 → flowjet-2.0.5}/docs/server.md +2 -2
  6. {flowjet-2.0.4 → flowjet-2.0.5}/examples/README.md +2 -1
  7. {flowjet-2.0.4 → flowjet-2.0.5}/pyproject.toml +2 -2
  8. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/cli.py +2 -33
  9. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/completion/engine.py +2 -1
  10. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/doctor_cmd.py +1 -1
  11. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/setup_cmd.py +76 -17
  12. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/bootstrap.py +14 -5
  13. flowjet-2.0.5/src/flowjet/home.py +130 -0
  14. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/__main__.py +5 -7
  15. flowjet-2.0.4/src/flowjet/core/home.py +0 -127
  16. {flowjet-2.0.4 → flowjet-2.0.5}/.gitignore +0 -0
  17. {flowjet-2.0.4 → flowjet-2.0.5}/LICENSE +0 -0
  18. {flowjet-2.0.4 → flowjet-2.0.5}/config/nano.yml +0 -0
  19. {flowjet-2.0.4 → flowjet-2.0.5}/deploy/README.md +0 -0
  20. {flowjet-2.0.4 → flowjet-2.0.5}/deploy/nano.yml +0 -0
  21. {flowjet-2.0.4 → flowjet-2.0.5}/docs/completion-design.md +0 -0
  22. {flowjet-2.0.4 → flowjet-2.0.5}/docs/drafts/README.md +0 -0
  23. {flowjet-2.0.4 → flowjet-2.0.5}/docs/drafts/api-spec.md +0 -0
  24. {flowjet-2.0.4 → flowjet-2.0.5}/docs/impl/IG-001-openai-compatible-server.md +0 -0
  25. {flowjet-2.0.4 → flowjet-2.0.5}/docs/impl/IG-003-production-isolation-hardening.md +0 -0
  26. {flowjet-2.0.4 → flowjet-2.0.5}/docs/impl/README.md +0 -0
  27. {flowjet-2.0.4 → flowjet-2.0.5}/docs/impl/TEST-COVERAGE-MATRIX.md +0 -0
  28. {flowjet-2.0.4 → flowjet-2.0.5}/docs/naming.md +0 -0
  29. {flowjet-2.0.4 → flowjet-2.0.5}/docs/specs/RFC-001-openai-compatible-api.md +0 -0
  30. {flowjet-2.0.4 → flowjet-2.0.5}/docs/specs/RFC-002-isolated-thread-pool-runtime.md +0 -0
  31. {flowjet-2.0.4 → flowjet-2.0.5}/docs/specs/RFC-003-production-isolation-hardening.md +0 -0
  32. {flowjet-2.0.4 → flowjet-2.0.5}/docs/specs/RFC-004-unified-package.md +0 -0
  33. {flowjet-2.0.4 → flowjet-2.0.5}/docs/specs/rfc-history.md +0 -0
  34. {flowjet-2.0.4 → flowjet-2.0.5}/docs/specs/rfc-index.md +0 -0
  35. {flowjet-2.0.4 → flowjet-2.0.5}/docs/specs/rfc-namings.md +0 -0
  36. {flowjet-2.0.4 → flowjet-2.0.5}/docs/specs/templates/architecture-design.md +0 -0
  37. {flowjet-2.0.4 → flowjet-2.0.5}/docs/specs/templates/conceptual-design.md +0 -0
  38. {flowjet-2.0.4 → flowjet-2.0.5}/docs/specs/templates/impl-interface-design.md +0 -0
  39. {flowjet-2.0.4 → flowjet-2.0.5}/docs/specs/templates/rfc-standard.md +0 -0
  40. {flowjet-2.0.4 → flowjet-2.0.5}/docs/specs/templates/rfc-template.md +0 -0
  41. {flowjet-2.0.4 → flowjet-2.0.5}/docs/upgrade.md +0 -0
  42. {flowjet-2.0.4 → flowjet-2.0.5}/nano.yml +0 -0
  43. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/__init__.py +0 -0
  44. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/__main__.py +0 -0
  45. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/README.md +0 -0
  46. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/gws-drive/SKILL.md +0 -0
  47. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/gws-drive-upload/SKILL.md +0 -0
  48. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/logo-generator-skill/.env.example +0 -0
  49. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/logo-generator-skill/README.md +0 -0
  50. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/logo-generator-skill/SKILL.md +0 -0
  51. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/logo-generator-skill/assets/background_library.html +0 -0
  52. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/logo-generator-skill/assets/showcase_template.html +0 -0
  53. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/logo-generator-skill/references/background_styles.md +0 -0
  54. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/logo-generator-skill/references/design_patterns.md +0 -0
  55. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/logo-generator-skill/references/webgl_backgrounds.md +0 -0
  56. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/logo-generator-skill/requirements.txt +0 -0
  57. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/logo-generator-skill/scripts/generate_showcase.py +0 -0
  58. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/logo-generator-skill/scripts/svg_to_png.py +0 -0
  59. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/SKILL.md +0 -0
  60. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/brainstorming/spec-document-reviewer-prompt.md +0 -0
  61. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/coding-plan-template.md +0 -0
  62. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/impl-guide-template.md +0 -0
  63. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/review/pr-review-template.md +0 -0
  64. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/review/review-checklist.md +0 -0
  65. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/specs/rfc-template.md +0 -0
  66. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-architecture-design.md +0 -0
  67. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-conceptual-design.md +0 -0
  68. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-impl-interface-design.md +0 -0
  69. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-drafts-readme.md +0 -0
  70. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-impl-readme.md +0 -0
  71. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-platonic.yml +0 -0
  72. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-history.md +0 -0
  73. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-index.md +0 -0
  74. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-namings.md +0 -0
  75. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-standard.md +0 -0
  76. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/BRAINSTORM/brainstorm.md +0 -0
  77. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-code.md +0 -0
  78. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-create-guide.md +0 -0
  79. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-full.md +0 -0
  80. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-update-guide.md +0 -0
  81. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-validate-guide.md +0 -0
  82. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-plan-modular-specs.md +0 -0
  83. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-architecture.md +0 -0
  84. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-conceptual.md +0 -0
  85. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-impl-interface.md +0 -0
  86. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scaffold.md +0 -0
  87. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scan.md +0 -0
  88. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/REFERENCE.md +0 -0
  89. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/REVIEW/review-spec-compliance.md +0 -0
  90. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-compliance.md +0 -0
  91. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-taxonomy.md +0 -0
  92. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-history.md +0 -0
  93. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-index.md +0 -0
  94. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-namings.md +0 -0
  95. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-refine.md +0 -0
  96. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-validate-consistency.md +0 -0
  97. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-overview.md +0 -0
  98. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-1.md +0 -0
  99. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-2.md +0 -0
  100. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-3.md +0 -0
  101. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/SKILL.md +0 -0
  102. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-deep.json +0 -0
  103. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-first.json +0 -0
  104. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/patterns/idea-first.json +0 -0
  105. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/patterns/loop.json +0 -0
  106. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/patterns/paper-pipeline.json +0 -0
  107. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/patterns/rapid.json +0 -0
  108. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/patterns/rigor-first.json +0 -0
  109. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/patterns/stance-first.json +0 -0
  110. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/patterns/systematic-review.json +0 -0
  111. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/patterns/verify-loop.json +0 -0
  112. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/references/backends.md +0 -0
  113. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-ars-omr.md +0 -0
  114. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-omr-ars.md +0 -0
  115. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/references/routing.md +0 -0
  116. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/scripts/csljson_to_omr.py +0 -0
  117. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/builtin_skills/research-bootstrap/scripts/papers_index_to_csljson.py +0 -0
  118. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/__init__.py +0 -0
  119. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/__main__.py +0 -0
  120. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/agent.py +0 -0
  121. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/completion/__init__.py +0 -0
  122. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/completion/context.py +0 -0
  123. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/doctor_fix_cmd.py +0 -0
  124. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/progress.py +0 -0
  125. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/serve_cmd.py +0 -0
  126. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/shell/__init__.py +0 -0
  127. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/shell/_fj.zsh +0 -0
  128. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/shell/fj.bash +0 -0
  129. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/stream.py +0 -0
  130. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/threads.py +0 -0
  131. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/cli/tool_stream.py +0 -0
  132. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/__init__.py +0 -0
  133. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/backend.py +0 -0
  134. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/backends/__init__.py +0 -0
  135. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/backends/direct.py +0 -0
  136. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/backends/fake.py +0 -0
  137. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/backends/isolation/__init__.py +0 -0
  138. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/backends/isolation/adapter.py +0 -0
  139. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/backends/isolation/admission.py +0 -0
  140. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/backends/isolation/backend.py +0 -0
  141. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/backends/isolation/errors.py +0 -0
  142. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/backends/isolation/mode_gate.py +0 -0
  143. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/backends/isolation/pool.py +0 -0
  144. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/backends/isolation/request.py +0 -0
  145. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/backends/isolation/workspace.py +0 -0
  146. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/bridges/nano/__init__.py +0 -0
  147. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/bridges/nano/adapter.py +0 -0
  148. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/bridges/nano/backend.py +0 -0
  149. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/bridges/nano/mapping.py +0 -0
  150. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/events.py +0 -0
  151. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/identity.py +0 -0
  152. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/modes.py +0 -0
  153. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/protocol.py +0 -0
  154. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/tool_results.py +0 -0
  155. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/core/tool_stream.py +0 -0
  156. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/__init__.py +0 -0
  157. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/acp/__init__.py +0 -0
  158. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/acp/agent.py +0 -0
  159. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/acp/routes.py +0 -0
  160. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/config.py +0 -0
  161. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/http/__init__.py +0 -0
  162. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/http/app.py +0 -0
  163. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/http/auth.py +0 -0
  164. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/openai_compat/__init__.py +0 -0
  165. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/openai_compat/errors.py +0 -0
  166. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/openai_compat/projection.py +0 -0
  167. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/openai_compat/routes.py +0 -0
  168. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/openai_compat/schemas.py +0 -0
  169. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/openai_compat/service.py +0 -0
  170. {flowjet-2.0.4 → flowjet-2.0.5}/src/flowjet/server/openai_compat/store.py +0 -0
@@ -10,12 +10,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
- ## [2.0.4] — 2026-09-19
13
+ ## [2.0.5] — 2026-09-20
14
14
 
15
15
  ### Changed
16
16
 
17
- - Bumped `soothe` lower bound to `1.0.13` (was `1.0.12`). soothe-nano stays
18
- pinned at `>=1.2.28,<1.3.0`.
17
+ - **Config moved off `~/.soothe` — `FLOWJET_HOME` is now the authority.**
18
+ The config file resolves to `$FLOWJET_HOME/config/nano.yml` (default
19
+ `~/.flowjet/config/nano.yml`) instead of `$SOOTHE_HOME/config/nano.yml`, so an
20
+ exported `SOOTHE_HOME` can no longer drag flowjet back onto `~/.soothe`.
21
+ `SOOTHE_HOME` itself is left untouched: CLI data (thread pins, locks,
22
+ completion history), logs, agents, plugins and the sqlite databases still live
23
+ there. Upgrades keep working — an existing `~/.soothe/config/nano.yml` (plus a
24
+ sibling `soothe.yml`) is copied into `$FLOWJET_HOME/config/` **once**, on first
25
+ run, and the originals are left in place, so the move is reversible.
26
+ - **`fj setup` no longer aborts when the LLM endpoint is unreachable.** A failed
27
+ or empty `/models` probe (connection refused, HTTP error, unresolved
28
+ `${ENV_VAR}`) now prints a `warning:` and falls through to manual model entry
29
+ — offering the models already saved for the selected provider, or accepting a
30
+ typed model id. The provider and router profile are still written, so a config
31
+ can be initialised before the model server is running; the run ends with a note
32
+ to re-check via `fj doctor`.
33
+
34
+ ## [2.0.4] — 2026-09-19
35
+
36
+ ### Fixed
37
+
38
+ - **`fj -V` is instant (1.78s → 0.06s).** It paid the full `soothe_nano` +
39
+ `flowjet.core.bootstrap` import cost on every invocation just to print a
40
+ version string. The stdlib-only logging helpers moved out of `agent.py` into
41
+ `cli.py`, `main()` parses args before setting up logging so `-V`/`-h` exit
42
+ first, and the `-c/--config` help text now resolves lazily.
43
+ (`agent.py` keeps thin delegation wrappers, so existing imports and
44
+ monkeypatching are unaffected.)
19
45
 
20
46
  ## [2.0.3] — 2026-09-18
21
47
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flowjet
3
- Version: 2.0.4
3
+ Version: 2.0.5
4
4
  Summary: FlowJet — agent CLI (fj) and server (ACP + OpenAI-compatible HTTP) powered by soothe-nano
5
5
  Project-URL: Homepage, https://github.com/caesar0301/flowjet
6
6
  Project-URL: Repository, https://github.com/caesar0301/flowjet
@@ -23,7 +23,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
24
  Requires-Python: <3.15,>=3.11
25
25
  Requires-Dist: soothe-nano<1.3.0,>=1.2.28
26
- Requires-Dist: soothe<2.0.0,>=1.0.13
26
+ Requires-Dist: soothe<2.0.0,>=1.0.12
27
27
  Provides-Extra: dev
28
28
  Requires-Dist: httpx>=0.27.0; extra == 'dev'
29
29
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
@@ -75,7 +75,7 @@ export OPENAI_API_KEY=sk-...
75
75
  fj summarize README.md
76
76
  ```
77
77
 
78
- `fj setup` writes `~/.soothe/config/nano.yml`. Without it, FlowJet falls back to `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`.
78
+ `fj setup` writes `$FLOWJET_HOME/config/nano.yml` (default `~/.flowjet/config/nano.yml`). Without it, FlowJet falls back to `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`.
79
79
 
80
80
  Not sure your machine is ready? `fj doctor` (add `--deep`, `--live-llm`).
81
81
 
@@ -31,7 +31,7 @@ export OPENAI_API_KEY=sk-...
31
31
  fj summarize README.md
32
32
  ```
33
33
 
34
- `fj setup` writes `~/.soothe/config/nano.yml`. Without it, FlowJet falls back to `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`.
34
+ `fj setup` writes `$FLOWJET_HOME/config/nano.yml` (default `~/.flowjet/config/nano.yml`). Without it, FlowJet falls back to `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`.
35
35
 
36
36
  Not sure your machine is ready? `fj doctor` (add `--deep`, `--live-llm`).
37
37
 
@@ -193,8 +193,8 @@ graph; abnormal turns recycle it before the worker is made available.
193
193
 
194
194
  | Env | Settings field | Default |
195
195
  |-----|----------------|---------|
196
- | `FLOWJET_HOME` | `home` | `~/.flowjet` |
197
- | `FLOWJET_NANO_CONFIG` | `nano_config` | unset → `$SOOTHE_HOME/config/nano.yml` |
196
+ | `FLOWJET_HOME` | `home` | `~/.flowjet` (config file + workspaces) |
197
+ | `FLOWJET_NANO_CONFIG` | `nano_config` | unset → `$FLOWJET_HOME/config/nano.yml` |
198
198
  | `FLOWJET_THREAD_POOL_MIN` | `thread_pool_min` | `2` |
199
199
  | `FLOWJET_THREAD_POOL_MAX` | `thread_pool_max` | `8` |
200
200
  | `FLOWJET_THREAD_POOL_IDLE_TIMEOUT` | `thread_pool_idle_timeout` | `300` |
@@ -72,8 +72,8 @@ with client.responses.stream(
72
72
  | `FLOWJET_MODELS` | `default` | Comma-separated logical model ids |
73
73
  | `FLOWJET_HOST` | `::` | Bind host (dual-stack IPv4/IPv6) |
74
74
  | `FLOWJET_PORT` | `8618` | Bind port |
75
- | `FLOWJET_HOME` | `~/.flowjet` | Root for per-session workspaces |
76
- | `FLOWJET_NANO_CONFIG` | unset | Optional `nano.yml` (else `$SOOTHE_HOME/config/nano.yml`) |
75
+ | `FLOWJET_HOME` | `~/.flowjet` | Root for the config file (`$FLOWJET_HOME/config/nano.yml`) and per-session workspaces |
76
+ | `FLOWJET_NANO_CONFIG` | unset | Optional `nano.yml` (else `$FLOWJET_HOME/config/nano.yml`) |
77
77
  | `FLOWJET_THREAD_POOL_MIN` | `2` | Min isolation worker threads |
78
78
  | `FLOWJET_THREAD_POOL_MAX` | `8` | Max isolation worker threads |
79
79
  | `FLOWJET_THREAD_POOL_IDLE_TIMEOUT` | `300` | Scaled-worker idle exit seconds (`0` = never) |
@@ -35,7 +35,8 @@ make examples-e2e
35
35
  ```
36
36
 
37
37
  The server uses soothe-nano with thread-pool isolation. Nano loads
38
- `~/.soothe/config/nano.yml` (or `FLOWJET_NANO_CONFIG`) and its active router profile.
38
+ `$FLOWJET_HOME/config/nano.yml` (default `~/.flowjet/config/nano.yml`, or
39
+ `FLOWJET_NANO_CONFIG`) and its active router profile.
39
40
 
40
41
  ## Environment
41
42
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "flowjet"
7
- version = "2.0.4"
7
+ version = "2.0.5"
8
8
  description = "FlowJet — agent CLI (fj) and server (ACP + OpenAI-compatible HTTP) powered by soothe-nano"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -31,7 +31,7 @@ classifiers = [
31
31
  dependencies = [
32
32
  # Default CLI runtime (FLOWJET_BACKEND=soothe). soothe is a host layer over
33
33
  # soothe-nano, so nano stays pinned explicitly: soothe only caps it at <2.
34
- "soothe>=1.0.13,<2.0.0",
34
+ "soothe>=1.0.12,<2.0.0",
35
35
  "soothe-nano>=1.2.28,<1.3.0",
36
36
  ]
37
37
 
@@ -44,20 +44,14 @@ class _CompactConsoleFormatter(logging.Formatter):
44
44
  return super().format(record)
45
45
 
46
46
 
47
- _FILE_LOGGING_INSTALLED = False
48
-
49
-
50
47
  def configure_cli_logging(*, verbose: bool = False) -> None:
51
- """Quiet the console for one-shot CLI use and enable file logging by default.
48
+ """Quiet the console for one-shot CLI use.
52
49
 
53
50
  - Opt out of ``browser_use`` import-time root logger setup when unset.
54
51
  - Remove existing root stream handlers (stderr/stdout) so init INFO
55
52
  lines do not interleave with the agent answer.
56
53
  - Prevent ``lastResort`` traceback dumps from soothe tool failures.
57
54
  - When ``verbose``, show WARNING+ as single-line messages on stderr.
58
- - Attach a rotating file handler at ``<FLOWJET_HOME>/logs/cli.log`` so
59
- every CLI run is recorded by default (idempotent across re-invocations
60
- within the same process, e.g. tests).
61
55
  """
62
56
  os.environ.setdefault(_BROWSER_USE_SETUP_LOGGING, "false")
63
57
  _remove_root_console_handlers()
@@ -75,25 +69,6 @@ def configure_cli_logging(*, verbose: bool = False) -> None:
75
69
  if root.level < logging.WARNING:
76
70
  root.setLevel(logging.WARNING)
77
71
 
78
- _ensure_cli_file_logging()
79
-
80
-
81
- def _ensure_cli_file_logging() -> None:
82
- """Attach the rotating file handler for CLI runs (idempotent)."""
83
- global _FILE_LOGGING_INSTALLED
84
- if _FILE_LOGGING_INSTALLED:
85
- return
86
- try:
87
- from flowjet.core.home import cli_log_path, ensure_flowjet_home
88
-
89
- ensure_flowjet_home()
90
- from soothe_nano.logging.setup import setup_logging
91
-
92
- setup_logging(log_file=str(cli_log_path()), extra_logger_names=("flowjet",))
93
- _FILE_LOGGING_INSTALLED = True
94
- except Exception: # pragma: no cover - logging must never break the CLI
95
- pass
96
-
97
72
 
98
73
  def silence_after_plugins(*, verbose: bool = False) -> None:
99
74
  """Re-apply quieting after plugin imports (belt-and-suspenders)."""
@@ -321,7 +296,7 @@ class _HelpFormatter(argparse.RawDescriptionHelpFormatter):
321
296
 
322
297
 
323
298
  def _default_config_help() -> str:
324
- from flowjet.cli.agent import default_config_path
299
+ from flowjet.home import default_config_path
325
300
 
326
301
  return f"Alternate nano.yml (default: {default_config_path()})"
327
302
 
@@ -687,12 +662,6 @@ def main_follow(argv: list[str] | None = None) -> int:
687
662
 
688
663
  def main(argv: list[str] | None = None) -> int:
689
664
  try:
690
- # Align SOOTHE_HOME with FLOWJET_HOME before any soothe import so
691
- # config, data, and logs all land under ~/.flowjet. Must run first:
692
- # soothe_sdk.paths binds SOOTHE_HOME at import time.
693
- from flowjet.core.home import ensure_flowjet_home
694
-
695
- ensure_flowjet_home()
696
665
  # Parse first so ``-V`` / ``-h`` / ``--help`` exit before we touch the
697
666
  # logging stack (and the heavy ``flowjet.cli.agent`` import it used to
698
667
  # drag in). Version/help must stay instant.
@@ -419,13 +419,14 @@ def _split_complete_argv(raw: list[str]) -> tuple[list[str], list[str]]:
419
419
  def run_complete(argv: list[str] | None = None) -> int:
420
420
  """Hidden shell protocol: print one candidate per stdout line."""
421
421
  from flowjet.cli.cli import resolve_cli_prog
422
+ from flowjet.home import default_config_path
422
423
 
423
424
  parser = argparse.ArgumentParser(prog=f"{resolve_cli_prog()} __complete", add_help=True)
424
425
  parser.add_argument(
425
426
  "-c",
426
427
  "--config",
427
428
  metavar="PATH",
428
- help="nano.yml path (default: ~/.soothe/config/nano.yml)",
429
+ help=f"nano.yml path (default: {default_config_path()})",
429
430
  )
430
431
  parser.add_argument(
431
432
  "--no-llm",
@@ -46,8 +46,8 @@ REFERENCE_SKILLS: tuple[dict[str, Any], ...] = (
46
46
 
47
47
 
48
48
  def _build_parser() -> argparse.ArgumentParser:
49
- from flowjet.cli.agent import default_config_path
50
49
  from flowjet.cli.cli import resolve_cli_prog
50
+ from flowjet.home import default_config_path
51
51
 
52
52
  parser = argparse.ArgumentParser(
53
53
  prog=f"{resolve_cli_prog()} doctor",
@@ -21,7 +21,7 @@ except ImportError: # pragma: no cover - Windows
21
21
  import yaml
22
22
  from soothe_nano.config import _ENV_VAR_RE, _resolve_env
23
23
 
24
- from flowjet.cli.agent import default_config_path
24
+ from flowjet.home import default_config_path, ensure_flowjet_config
25
25
 
26
26
  DEFAULT_PROVIDER_NAME = "local"
27
27
  DEFAULT_NEW_PROVIDER_NAME = "flowjet-default"
@@ -65,6 +65,10 @@ def run_setup(config_path: str | None = None) -> int:
65
65
 
66
66
  def _run_setup(config_path: str | None = None) -> int:
67
67
  path = Path(config_path).expanduser() if config_path else default_config_path()
68
+ if config_path is None:
69
+ # ``fj setup`` bypasses ``load_config``, so it pulls in a ``~/.soothe/config``
70
+ # copy itself before reading the provider list.
71
+ ensure_flowjet_config()
68
72
  existing = _load_existing_config(path)
69
73
  providers = _list_providers(existing)
70
74
  _, seed_model = _seed_from_config(existing)
@@ -100,27 +104,39 @@ def _run_setup(config_path: str | None = None) -> int:
100
104
  api_key = _prompt_secret_with_default("API key", api_key_default)
101
105
 
102
106
  sys.stdout.write("Fetching available models...\n")
107
+ models: list[str] = []
108
+ fetch_issue: str | None = None
103
109
  try:
104
110
  resolved_endpoint = resolve_config_value(endpoint, field="API endpoint")
105
111
  resolved_api_key = resolve_config_value(api_key, field="API key")
106
- models = fetch_models(resolved_endpoint, resolved_api_key)
107
112
  except RuntimeError as exc:
108
- sys.stderr.write(f"error: {exc}\n")
109
- return 1
110
-
111
- if not models:
112
- sys.stderr.write("error: no models returned by endpoint\n")
113
- return 1
114
-
115
- preferred = seed_model
116
- if selected:
117
- selected_models = selected.get("models")
118
- if isinstance(selected_models, list) and selected_models:
119
- preferred = str(selected_models[0])
120
- if preferred and preferred in models:
121
- models = [preferred] + [m for m in models if m != preferred]
113
+ fetch_issue = str(exc)
114
+ else:
115
+ try:
116
+ models = fetch_models(resolved_endpoint, resolved_api_key)
117
+ except RuntimeError as exc:
118
+ fetch_issue = str(exc)
119
+ if not models and fetch_issue is None:
120
+ fetch_issue = f"{resolved_endpoint} returned no models"
121
+
122
+ if fetch_issue:
123
+ # An unreachable endpoint is not fatal: save the provider anyway so the
124
+ # user can start the model server later and pick a model by hand now.
125
+ sys.stderr.write(f"warning: could not list models ({fetch_issue})\n")
126
+ sys.stderr.write("warning: continuing without a model list\n")
127
+
128
+ if models:
129
+ preferred = seed_model
130
+ if selected:
131
+ selected_models = selected.get("models")
132
+ if isinstance(selected_models, list) and selected_models:
133
+ preferred = str(selected_models[0])
134
+ if preferred and preferred in models:
135
+ models = [preferred] + [m for m in models if m != preferred]
136
+ model = choose_model_interactive(models)
137
+ else:
138
+ model = _prompt_model_with_fallback(_fallback_model_candidates(selected, seed_model))
122
139
 
123
- model = choose_model_interactive(models)
124
140
  if not model:
125
141
  sys.stderr.write("setup cancelled\n")
126
142
  return 1
@@ -139,6 +155,10 @@ def _run_setup(config_path: str | None = None) -> int:
139
155
  sys.stdout.write(f"Saved config to: {path}\n")
140
156
  sys.stdout.write(f"Active profile: {profile}\n")
141
157
  sys.stdout.write(f"Configured model: {provider_name}:{model}\n")
158
+ if fetch_issue:
159
+ sys.stdout.write(
160
+ "Note: model list was not verified; start the endpoint and run 'fj doctor' to confirm.\n"
161
+ )
142
162
  return 0
143
163
 
144
164
 
@@ -464,6 +484,45 @@ def _parse_model_filter(choice: str) -> str:
464
484
  return raw
465
485
 
466
486
 
487
+ def _fallback_model_candidates(selected: dict[str, Any], seed_model: str | None) -> list[str]:
488
+ """Models already known from config, used when the endpoint cannot be queried."""
489
+ candidates: list[str] = []
490
+ if selected:
491
+ models = selected.get("models")
492
+ if isinstance(models, list):
493
+ candidates.extend(str(item) for item in models if str(item).strip())
494
+ if seed_model and seed_model not in candidates:
495
+ candidates.insert(0, seed_model)
496
+ return candidates
497
+
498
+
499
+ def _prompt_model_with_fallback(candidates: list[str]) -> str | None:
500
+ """Ask for a model id directly when the endpoint has no model list to pick from."""
501
+ default = candidates[0] if candidates else DEFAULT_MODEL
502
+ if candidates:
503
+ sys.stdout.write("Models saved for this provider:\n")
504
+ for idx, name in enumerate(candidates, start=1):
505
+ sys.stdout.write(f" {idx}. {name}\n")
506
+ label = f"Model name or number 1-{len(candidates)}"
507
+ else:
508
+ label = "Model name"
509
+
510
+ while True:
511
+ try:
512
+ value = input(f"{label} [{default}]: ").strip()
513
+ except EOFError:
514
+ return default
515
+ if not value:
516
+ return default
517
+ if value.isdigit() and candidates:
518
+ idx = int(value)
519
+ if 1 <= idx <= len(candidates):
520
+ return candidates[idx - 1]
521
+ sys.stdout.write(f"Invalid number; enter 1-{len(candidates)} or a model name.\n")
522
+ continue
523
+ return value
524
+
525
+
467
526
  def _load_existing_config(path: Path) -> dict[str, Any]:
468
527
  if not path.is_file():
469
528
  return {}
@@ -18,7 +18,7 @@ from contextlib import asynccontextmanager
18
18
  from pathlib import Path
19
19
  from typing import Any
20
20
 
21
- from soothe_nano.config import SOOTHE_HOME, SootheConfig
21
+ from soothe_nano.config import SootheConfig
22
22
  from soothe_nano.resolve import resolve_checkpointer
23
23
 
24
24
  from flowjet.core.backend import Backend, resolve_backend
@@ -29,6 +29,8 @@ from flowjet.core.modes import (
29
29
  SurfaceProfile,
30
30
  require_mode,
31
31
  )
32
+ from flowjet.home import default_config_path as _flowjet_config_path
33
+ from flowjet.home import ensure_flowjet_config
32
34
 
33
35
  logger = logging.getLogger(__name__)
34
36
 
@@ -75,8 +77,8 @@ def register_flowjet_builtin_skills() -> None:
75
77
 
76
78
 
77
79
  def default_config_path() -> Path:
78
- """Return ``~/.soothe/config/nano.yml`` (respects ``SOOTHE_HOME``)."""
79
- return SOOTHE_HOME / "config" / "nano.yml"
80
+ """Return ``$FLOWJET_HOME/config/nano.yml`` (default ``~/.flowjet/config/nano.yml``)."""
81
+ return _flowjet_config_path()
80
82
 
81
83
 
82
84
  def load_config(
@@ -93,14 +95,21 @@ def load_config(
93
95
 
94
96
  Resolution order:
95
97
  1. Explicit ``config_path``
96
- 2. ``SOOTHE_HOME / config / nano.yml`` (default ``~/.soothe/config/nano.yml``)
98
+ 2. ``$FLOWJET_HOME / config / nano.yml`` (default ``~/.flowjet/config/nano.yml``)
97
99
  3. Zero-config ``SootheConfig()`` from ``OPENAI_API_KEY`` / ``ANTHROPIC_API_KEY``
98
100
 
99
101
  On the ``soothe`` backend, a sibling ``soothe.yml`` (host-owned keys) is
100
102
  composed over ``nano.yml`` when present.
103
+
104
+ Without an explicit ``config_path``, a config found in ``~/.soothe/config`` is
105
+ copied into the flowjet home first — see :func:`flowjet.home.ensure_flowjet_config`.
101
106
  """
102
107
  backend = backend or resolve_backend(Surface.CLI)
103
- path = Path(config_path).expanduser() if config_path else default_config_path()
108
+ if config_path is None:
109
+ ensure_flowjet_config()
110
+ path = default_config_path()
111
+ else:
112
+ path = Path(config_path).expanduser()
104
113
  if path.is_file():
105
114
  return _load_config_file(path, backend)
106
115
  if config_path is not None:
@@ -0,0 +1,130 @@
1
+ """``FLOWJET_HOME`` resolution for the config file.
2
+
3
+ FlowJet reads its config from ``$FLOWJET_HOME/config/nano.yml`` (default
4
+ ``~/.flowjet/config/nano.yml``). ``FLOWJET_HOME`` is the only authority for that
5
+ path; ``SOOTHE_HOME`` does not affect it, and soothe-owned state — CLI data,
6
+ logs, agents, plugins, sqlite databases — stays where the soothe runtime puts it.
7
+
8
+ This module is deliberately a stdlib-only leaf: it is imported from argparse
9
+ help builders (``fj -c``), so it must stay cheap, and it must not do file I/O
10
+ during import or inside :func:`default_config_path`.
11
+
12
+ :func:`ensure_flowjet_config` copies an existing ``~/.soothe/config`` into the
13
+ flowjet home once, leaving the originals in place.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import os
20
+ import shutil
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ DEFAULT_FLOWJET_HOME = "~/.flowjet"
27
+
28
+ # Config filenames that make up a runnable flowjet setup. ``nano.yml`` is the
29
+ # provider/router file the CLI and server read; ``soothe.yml`` is the optional
30
+ # host-owned sibling composed over it on the ``soothe`` backend.
31
+ _CONFIG_FILENAMES = ("nano.yml", "soothe.yml")
32
+
33
+
34
+ def flowjet_home() -> Path:
35
+ """Return ``$FLOWJET_HOME`` (default ``~/.flowjet``), expanded.
36
+
37
+ Read at call time, so callers can set the env var late.
38
+ """
39
+ raw = os.environ.get("FLOWJET_HOME") or DEFAULT_FLOWJET_HOME
40
+ return Path(raw).expanduser()
41
+
42
+
43
+ def legacy_soothe_home() -> Path:
44
+ """Return ``~/.soothe``, the home :func:`ensure_flowjet_config` copies from.
45
+
46
+ Not env-driven: the source of a copy is a fixed location, not a redirect.
47
+ """
48
+ return Path.home() / ".soothe"
49
+
50
+
51
+ def default_config_path() -> Path:
52
+ """Return ``$FLOWJET_HOME/config/nano.yml``.
53
+
54
+ Pure — no directory creation, no existence checks — because argparse help
55
+ builders call it while rendering ``-h``.
56
+ """
57
+ return flowjet_home() / "config" / "nano.yml"
58
+
59
+
60
+ def migrate_legacy_config(
61
+ *,
62
+ legacy_home: Path | None = None,
63
+ target_home: Path | None = None,
64
+ ) -> list[str]:
65
+ """Copy ``~/.soothe/config`` into the flowjet home, once.
66
+
67
+ No-op unless the target ``nano.yml`` is missing and the source has one, so an
68
+ existing flowjet config is never overwritten and this is safe to call on
69
+ every start. Originals are left in place. Returns the copied filenames.
70
+ """
71
+ legacy = Path(legacy_home) if legacy_home is not None else legacy_soothe_home()
72
+ target = Path(target_home) if target_home is not None else flowjet_home()
73
+
74
+ try:
75
+ if legacy.resolve() == target.resolve():
76
+ return []
77
+ legacy_config = legacy / "config"
78
+ target_config = target / "config"
79
+ if not (legacy_config / "nano.yml").is_file():
80
+ return []
81
+ if (target_config / "nano.yml").is_file():
82
+ return []
83
+
84
+ copied: list[str] = []
85
+ for name in _CONFIG_FILENAMES:
86
+ source = legacy_config / name
87
+ if not source.is_file():
88
+ continue
89
+ if _copy_atomic(source, target_config / name):
90
+ copied.append(name)
91
+ except OSError as exc:
92
+ logger.warning("could not migrate legacy config from %s: %s", legacy, exc)
93
+ return []
94
+ return copied
95
+
96
+
97
+ def ensure_flowjet_config(
98
+ *,
99
+ legacy_home: Path | None = None,
100
+ target_home: Path | None = None,
101
+ ) -> list[str]:
102
+ """Copy a soothe-home config into the flowjet home, reporting it on stderr.
103
+
104
+ Idempotent: returns the copied filenames, empty when there was nothing to copy.
105
+ """
106
+ copied = migrate_legacy_config(legacy_home=legacy_home, target_home=target_home)
107
+ if copied:
108
+ source = Path(legacy_home) if legacy_home is not None else legacy_soothe_home()
109
+ target = Path(target_home) if target_home is not None else flowjet_home()
110
+ names = ", ".join(copied)
111
+ sys.stderr.write(f"Copied {names} from {source / 'config'} to {target / 'config'}\n")
112
+ sys.stderr.write("The original soothe config was left in place.\n")
113
+ return copied
114
+
115
+
116
+ def _copy_atomic(source: Path, dest: Path) -> bool:
117
+ """Copy ``source`` to ``dest`` via a temp file + ``os.replace``.
118
+
119
+ Concurrent callers — server pool workers reaching this through
120
+ ``load_config`` — must never observe a half-written destination.
121
+ """
122
+ try:
123
+ dest.parent.mkdir(parents=True, exist_ok=True)
124
+ tmp = dest.parent / f".{dest.name}.tmp-{os.getpid()}"
125
+ shutil.copyfile(source, tmp)
126
+ os.replace(tmp, dest)
127
+ except OSError as exc:
128
+ logger.warning("could not copy %s to %s: %s", source, dest, exc)
129
+ return False
130
+ return True
@@ -35,14 +35,12 @@ def _create_dual_stack_socket(host: str, port: int) -> socket.socket:
35
35
 
36
36
 
37
37
  def main() -> None:
38
- # Align SOOTHE_HOME with FLOWJET_HOME before any soothe import so config,
39
- # data, and logs all land under ~/.flowjet. Must run before Settings() or
40
- # any soothe_nano import: soothe_sdk.paths binds SOOTHE_HOME at import time.
41
- from flowjet.core.home import ensure_flowjet_home, setup_file_logging
38
+ # Pull in a ~/.soothe/config copy before the first agent build. Kept out of
39
+ # create_app(): the ASGI app is built at import time, and importing a module
40
+ # must not write files.
41
+ from flowjet.home import ensure_flowjet_config
42
42
 
43
- ensure_flowjet_home()
44
- # Record server runs to <FLOWJET_HOME>/logs/flowjet.log by default.
45
- setup_file_logging(extra_logger_names=("flowjet",))
43
+ ensure_flowjet_config()
46
44
 
47
45
  settings = Settings()
48
46
  # When the host is a wildcard (``::`` or ``0.0.0.0``), use a dual-stack