flowjet 2.1.2__tar.gz → 2.1.3__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.
- {flowjet-2.1.2 → flowjet-2.1.3}/CHANGELOG.md +43 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/PKG-INFO +1 -1
- {flowjet-2.1.2 → flowjet-2.1.3}/pyproject.toml +1 -1
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/cli.py +196 -23
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/stream.py +252 -108
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/nano/mapping.py +9 -1
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/events.py +4 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/pool.py +21 -3
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/sandbox/settings.py +7 -2
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/acp/agent.py +11 -1
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/service.py +12 -2
- {flowjet-2.1.2 → flowjet-2.1.3}/.gitignore +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/LICENSE +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/README.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/config/nano-local.yml +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/config/nano-sandbox-soothe.yml +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/config/nano.yml +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/deploy/README.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/docs/completion-design.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/docs/impl/IG-001-openai-compatible-server.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/docs/impl/IG-002-isolated-thread-pool-runtime.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/docs/impl/IG-003-production-isolation-hardening.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/RFC-001-openai-compatible-api.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/RFC-002-isolated-thread-pool-runtime.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/RFC-003-production-isolation-hardening.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/RFC-004-unified-package.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/RFC-005-arcbox-tool-sandbox.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/rfc-history.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/rfc-index.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/docs/specs/rfc-namings.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/examples/README.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/__main__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/README.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/gws-drive/SKILL.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/gws-drive-upload/SKILL.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/.env.example +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/README.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/SKILL.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/assets/background_library.html +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/assets/showcase_template.html +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/references/background_styles.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/references/design_patterns.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/references/webgl_backgrounds.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/requirements.txt +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/scripts/generate_showcase.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/logo-generator-skill/scripts/svg_to_png.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/SKILL.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/brainstorming/spec-document-reviewer-prompt.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/coding-plan-template.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/implementation/impl-guide-template.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/review/pr-review-template.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/review/review-checklist.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/specs/rfc-template.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-architecture-design.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-conceptual-design.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/specs/template-impl-interface-design.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-drafts-readme.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-impl-readme.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-platonic.yml +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-history.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-index.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-namings.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/assets/templates/template-rfc-standard.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/BRAINSTORM/brainstorm.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-code.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-create-guide.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-full.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-update-guide.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/IMPL/impl-validate-guide.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-plan-modular-specs.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-architecture.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-conceptual.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-recover-impl-interface.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scaffold.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/INIT/init-scan.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/REFERENCE.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/REVIEW/review-spec-compliance.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-compliance.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-check-taxonomy.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-history.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-index.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-generate-namings.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-refine.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/SPECS/specs-validate-consistency.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-overview.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-1.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-2.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/platonic-coding/references/WORKFLOW/workflow-phase-3.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/SKILL.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-deep.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/evidence-first.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/idea-first.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/loop.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/paper-pipeline.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/rapid.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/rigor-first.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/stance-first.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/systematic-review.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/patterns/verify-loop.json +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/references/backends.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-ars-omr.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/references/handoff-omr-ars.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/references/routing.md +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/scripts/csljson_to_omr.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/builtin_skills/research-bootstrap/scripts/papers_index_to_csljson.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/__main__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/agent.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/completion/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/completion/context.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/completion/engine.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/doctor_cmd.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/doctor_fix_cmd.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/progress.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/serve_cmd.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/setup_cmd.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/shell/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/shell/_fj.zsh +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/shell/fj.bash +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/threads.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/cli/tool_stream.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/_warnings.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/agent/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/agent/config.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/agent/factory.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/agent/skills.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/agent/wiring.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/agent/workspace.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/backend_config.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/base.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/nano/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/nano/adapter.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/nano/backend.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/soothe/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/soothe/adapter.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/bridges/soothe/backend.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/identity.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/modes.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/protocol.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/direct.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/fake.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/adapter.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/admission.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/backend.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/errors.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/mode_gate.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/request.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/runtime/isolation/workspace.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/sandbox/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/sandbox/arbox.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/sandbox/resolver.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/streaming/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/streaming/presentation.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/streaming/tool_results.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/core/streaming/tool_stream.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/home.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/__main__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/acp/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/acp/routes.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/config.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/http/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/http/app.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/http/auth.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/__init__.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/errors.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/projection.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/routes.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/schemas.py +0 -0
- {flowjet-2.1.2 → flowjet-2.1.3}/src/flowjet/server/openai_compat/store.py +0 -0
|
@@ -10,6 +10,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [2.1.3] — 2026-10-23
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **CLI: every failure path now surfaces a message — no more silent exits.**
|
|
18
|
+
The CLI could exit with no output (SIGTERM/SIGHUP, plugin `sys.exit`, broken
|
|
19
|
+
pipes, empty answers) and destroyed the original error stack even with `-v`.
|
|
20
|
+
This closes every identified gap in the error-to-output flow:
|
|
21
|
+
|
|
22
|
+
- `RunFailed` now carries `exc_type` + traceback; `stream_query` raises
|
|
23
|
+
`AgentRunError` so the CLI reports the real exception type and, with `-v`,
|
|
24
|
+
the original in-run stack instead of a re-raise site.
|
|
25
|
+
- SIGTERM/SIGHUP cancel the run gracefully: "interrupted by SIGTERM" with
|
|
26
|
+
conventional `128+signal` exit codes (was: instant silent death).
|
|
27
|
+
- Empty-answer runs warn on stderr ("agent produced no output · N tool calls
|
|
28
|
+
failed · last tool error") and exit 3.
|
|
29
|
+
- Partial narration is flushed to stderr before any failure/timeout/interrupt
|
|
30
|
+
message — a provider drop no longer loses the whole answer.
|
|
31
|
+
- `--timeout SECONDS` cancels a stuck run, reporting the last activity.
|
|
32
|
+
- `FJ_LOG_FILE=<path>` keeps WARNING+ with full tracebacks in a rotating log
|
|
33
|
+
for post-mortem diagnosis (incl. `logger.exception` from plugins).
|
|
34
|
+
- Multi-line errors truncated to one line now hint "run with -v".
|
|
35
|
+
- `BrokenPipeError`: exit 141 with a message; shutdown flush noise silenced
|
|
36
|
+
via `devnull` dup2 (`fj q | head`).
|
|
37
|
+
- Teardown suppresses `CancelledError` so it cannot mask the real result.
|
|
38
|
+
- Server parity: pool `worker_error`, OpenAI-compatible and ACP failures
|
|
39
|
+
populate/render `exc_type` + traceback (ACP shows "Error: <Type>: <msg>").
|
|
40
|
+
- Minor: thread-snapshot check failures are logged; config-load errors
|
|
41
|
+
honor `-v` with a traceback; plugin `sys.exit(0)` still fails loudly.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **ArcBox tool sandbox: guest network is now off by default**
|
|
46
|
+
(`sandbox.network` defaults to `false`, was `true`). With `true` the guest is
|
|
47
|
+
NAT'ed through the ArcBox host and the daemon only knows ENABLED|NONE, so
|
|
48
|
+
everything the host can route to — internal network and corporate VPN
|
|
49
|
+
included — was reachable from agent shell commands. With `false` the guest
|
|
50
|
+
boots without a network device (no route, no DNS): shell/code execution can
|
|
51
|
+
reach neither the public internet nor internal services. Web search/crawl is
|
|
52
|
+
unaffected — `web_search`/`fetch_url` are host-placement tools and never run
|
|
53
|
+
in the guest. Opt back in per deployment (`sandbox.network: true` in the
|
|
54
|
+
nano.yml) when the guest really needs egress.
|
|
55
|
+
|
|
13
56
|
## [2.1.2] — 2026-10-21
|
|
14
57
|
|
|
15
58
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flowjet
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.3
|
|
4
4
|
Summary: FlowJet — agent CLI (fj) and server (ACP + OpenAI-compatible HTTP) powered by Soothe
|
|
5
5
|
Project-URL: Homepage, https://github.com/caesar0301/flowjet
|
|
6
6
|
Project-URL: Repository, https://github.com/caesar0301/flowjet
|
|
@@ -10,7 +10,9 @@ import asyncio
|
|
|
10
10
|
import contextlib
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
|
+
import signal
|
|
13
14
|
import sys
|
|
15
|
+
import traceback
|
|
14
16
|
from collections.abc import Coroutine
|
|
15
17
|
from logging.handlers import RotatingFileHandler
|
|
16
18
|
from pathlib import Path
|
|
@@ -21,6 +23,8 @@ from flowjet.cli import __version__
|
|
|
21
23
|
FORMAL_CLI = "flowjet"
|
|
22
24
|
CLI_ALIASES = frozenset({"fj", "fjf", FORMAL_CLI})
|
|
23
25
|
|
|
26
|
+
_logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
24
28
|
|
|
25
29
|
# ---------------------------------------------------------------------------
|
|
26
30
|
# CLI logging — stdlib only, kept here so ``fj -V`` / ``fj -h`` never pay the
|
|
@@ -29,18 +33,30 @@ CLI_ALIASES = frozenset({"fj", "fjf", FORMAL_CLI})
|
|
|
29
33
|
|
|
30
34
|
_BROWSER_USE_SETUP_LOGGING = "BROWSER_USE_SETUP_LOGGING"
|
|
31
35
|
_FJ_CONSOLE_HANDLER = "fj-console"
|
|
36
|
+
_FJ_FILE_HANDLER = "fj-file"
|
|
37
|
+
_LOG_FILE_ENV = "FJ_LOG_FILE"
|
|
32
38
|
|
|
33
39
|
|
|
34
40
|
class _CompactConsoleFormatter(logging.Formatter):
|
|
35
|
-
"""One-line console records
|
|
41
|
+
"""One-line console records; exceptions summarized on the same line.
|
|
42
|
+
|
|
43
|
+
Full tracebacks would smash the ephemeral progress line, but dropping the
|
|
44
|
+
exception entirely (the old behavior) hid the root cause even with ``-v``.
|
|
45
|
+
The full stack for run failures is available via ``AgentRunError``.
|
|
46
|
+
"""
|
|
36
47
|
|
|
37
48
|
def formatException(self, ei: object) -> str: # noqa: N802 - logging API
|
|
38
49
|
return ""
|
|
39
50
|
|
|
40
51
|
def format(self, record: logging.LogRecord) -> str:
|
|
41
|
-
# logger.exception sets exc_info;
|
|
42
|
-
|
|
43
|
-
record.
|
|
52
|
+
# logger.exception sets exc_info; fold "type: message" into the record
|
|
53
|
+
# message so format() stays one line without losing the cause.
|
|
54
|
+
if record.exc_info:
|
|
55
|
+
exc_val = record.exc_info[1]
|
|
56
|
+
if exc_val is not None:
|
|
57
|
+
record.msg = f"{record.getMessage()}: {type(exc_val).__name__}: {exc_val}"
|
|
58
|
+
record.exc_info = None
|
|
59
|
+
record.exc_text = None
|
|
44
60
|
return super().format(record)
|
|
45
61
|
|
|
46
62
|
|
|
@@ -52,6 +68,8 @@ def configure_cli_logging(*, verbose: bool = False) -> None:
|
|
|
52
68
|
lines do not interleave with the agent answer.
|
|
53
69
|
- Prevent ``lastResort`` traceback dumps from soothe tool failures.
|
|
54
70
|
- When ``verbose``, show WARNING+ as single-line messages on stderr.
|
|
71
|
+
- When ``FJ_LOG_FILE`` is set, mirror WARNING+ (with full tracebacks) to
|
|
72
|
+
that rotating file for post-mortem diagnosis of silent failures.
|
|
55
73
|
"""
|
|
56
74
|
os.environ.setdefault(_BROWSER_USE_SETUP_LOGGING, "false")
|
|
57
75
|
_remove_root_console_handlers()
|
|
@@ -68,6 +86,30 @@ def configure_cli_logging(*, verbose: bool = False) -> None:
|
|
|
68
86
|
root.addHandler(null)
|
|
69
87
|
if root.level < logging.WARNING:
|
|
70
88
|
root.setLevel(logging.WARNING)
|
|
89
|
+
_attach_file_log_handler(root)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _attach_file_log_handler(root: logging.Logger) -> None:
|
|
93
|
+
"""Attach the ``FJ_LOG_FILE`` rotating handler once, if requested.
|
|
94
|
+
|
|
95
|
+
Unlike the compact console formatter, the file keeps full tracebacks, so
|
|
96
|
+
``logger.exception`` records from soothe/plugins stay diagnosable after a
|
|
97
|
+
crash — including for deaths the CLI itself cannot report (SIGKILL).
|
|
98
|
+
"""
|
|
99
|
+
if any(getattr(h, "name", None) == _FJ_FILE_HANDLER for h in root.handlers):
|
|
100
|
+
return
|
|
101
|
+
path = os.environ.get(_LOG_FILE_ENV, "").strip()
|
|
102
|
+
if not path:
|
|
103
|
+
return
|
|
104
|
+
try:
|
|
105
|
+
handler = RotatingFileHandler(path, maxBytes=2_000_000, backupCount=2, encoding="utf-8")
|
|
106
|
+
except OSError:
|
|
107
|
+
_logger.warning("FJ_LOG_FILE is not writable: %s", path)
|
|
108
|
+
return
|
|
109
|
+
handler.set_name(_FJ_FILE_HANDLER)
|
|
110
|
+
handler.setLevel(logging.WARNING)
|
|
111
|
+
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)-8s [%(name)s] %(message)s"))
|
|
112
|
+
root.addHandler(handler)
|
|
71
113
|
|
|
72
114
|
|
|
73
115
|
def silence_after_plugins(*, verbose: bool = False) -> None:
|
|
@@ -155,13 +197,18 @@ notes:
|
|
|
155
197
|
• Bypass mode (skip all security enforcement) is the CLI default — use -a for guardrails
|
|
156
198
|
• One query per thread at a time; different threads may run concurrently
|
|
157
199
|
• With -v, prints thread <id> and the project dir on stderr before the run
|
|
200
|
+
• --timeout N cancels a run after N seconds (0 = no limit)
|
|
201
|
+
• Exit codes: 0 ok · 1 error · 2 usage · 3 no output · 130/143 interrupted
|
|
202
|
+
• Set FJ_LOG_FILE=<path> to keep warnings & errors (with tracebacks) on file
|
|
158
203
|
"""
|
|
159
204
|
|
|
160
205
|
|
|
161
206
|
# Boolean short flags that may appear clustered (``-lv`` → ``-l -v``).
|
|
162
207
|
_BOOL_SHORTS = frozenset({"h", "V", "l", "v", "f", "a"})
|
|
163
208
|
# Short flags that consume the next argv token as a value.
|
|
164
|
-
_VALUE_FLAGS = frozenset(
|
|
209
|
+
_VALUE_FLAGS = frozenset(
|
|
210
|
+
{"-c", "--config", "-t", "--thread", "-w", "--workspace", "-n", "--timeout"}
|
|
211
|
+
)
|
|
165
212
|
_FLAG_ONLY = frozenset(
|
|
166
213
|
{
|
|
167
214
|
"-h",
|
|
@@ -179,7 +226,7 @@ _FLAG_ONLY = frozenset(
|
|
|
179
226
|
"--ask",
|
|
180
227
|
}
|
|
181
228
|
)
|
|
182
|
-
_EQUALS_PREFIXES = ("--config=", "--thread=", "--workspace=")
|
|
229
|
+
_EQUALS_PREFIXES = ("--config=", "--thread=", "--workspace=", "--timeout=")
|
|
183
230
|
|
|
184
231
|
|
|
185
232
|
def _expand_short_cluster(tok: str) -> list[str] | None:
|
|
@@ -244,6 +291,7 @@ def validate_arg_composition(args: Any) -> str | None:
|
|
|
244
291
|
- ``-n`` requires ``-l``/``--list``
|
|
245
292
|
- ``-l`` is exclusive with a query, ``-f``, ``-t``, ``-w``, ``--no-stream``, ``--ask``
|
|
246
293
|
- ``-t``/``--thread`` overrides ``-f``/``--follow`` when both are given
|
|
294
|
+
- ``--timeout`` must be >= 0 (0 = no limit)
|
|
247
295
|
"""
|
|
248
296
|
listing = bool(getattr(args, "list", False))
|
|
249
297
|
list_limit = getattr(args, "list_limit", None)
|
|
@@ -257,6 +305,10 @@ def validate_arg_composition(args: Any) -> str | None:
|
|
|
257
305
|
if list_limit is not None and not listing:
|
|
258
306
|
return "-n requires -l/--list"
|
|
259
307
|
|
|
308
|
+
timeout = getattr(args, "timeout", None)
|
|
309
|
+
if timeout is not None and timeout < 0:
|
|
310
|
+
return "--timeout must be >= 0 (0 = no limit)"
|
|
311
|
+
|
|
260
312
|
if listing:
|
|
261
313
|
if query:
|
|
262
314
|
return "-l/--list does not take a query (got leftover text after options)"
|
|
@@ -376,6 +428,14 @@ def _build_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
|
|
376
428
|
help="Ask mode: answer the query with no tools (pure Q&A)",
|
|
377
429
|
)
|
|
378
430
|
|
|
431
|
+
run = parser.add_argument_group("run")
|
|
432
|
+
run.add_argument(
|
|
433
|
+
"--timeout",
|
|
434
|
+
metavar="SECONDS",
|
|
435
|
+
type=float,
|
|
436
|
+
help="Cancel the run after N seconds (0 = no limit)",
|
|
437
|
+
)
|
|
438
|
+
|
|
379
439
|
paths = parser.add_argument_group("paths")
|
|
380
440
|
paths.add_argument(
|
|
381
441
|
"-c",
|
|
@@ -458,6 +518,8 @@ def _load_config_or_exit(args: argparse.Namespace) -> Any | int:
|
|
|
458
518
|
return 1
|
|
459
519
|
except Exception as exc:
|
|
460
520
|
sys.stderr.write(f"error: failed to load config: {exc}\n")
|
|
521
|
+
if getattr(args, "verbose", False):
|
|
522
|
+
sys.stderr.write("".join(traceback.format_exception(type(exc), exc, exc.__traceback__)))
|
|
461
523
|
return 1
|
|
462
524
|
|
|
463
525
|
|
|
@@ -517,7 +579,10 @@ async def _detect_pending_interrupt(agent: Any, thread_id: str) -> Any:
|
|
|
517
579
|
return None
|
|
518
580
|
try:
|
|
519
581
|
snapshot = await get_state({"configurable": {"thread_id": thread_id}})
|
|
520
|
-
except Exception:
|
|
582
|
+
except Exception as exc:
|
|
583
|
+
# Not fatal — but explain why a pending question may be missed and a
|
|
584
|
+
# new turn started instead of resuming (visible with -v / FJ_LOG_FILE).
|
|
585
|
+
_logger.warning("thread snapshot check failed; starting a new turn: %s", exc)
|
|
521
586
|
return None
|
|
522
587
|
if snapshot is None:
|
|
523
588
|
return None
|
|
@@ -567,6 +632,9 @@ async def run_async(args: argparse.Namespace) -> int:
|
|
|
567
632
|
return config
|
|
568
633
|
|
|
569
634
|
workspace = Path(args.workspace).expanduser().resolve() if args.workspace else None
|
|
635
|
+
# ``--timeout 0`` means no limit (same as omitting the flag).
|
|
636
|
+
timeout_raw = getattr(args, "timeout", None)
|
|
637
|
+
timeout_s = float(timeout_raw) if timeout_raw else None
|
|
570
638
|
|
|
571
639
|
try:
|
|
572
640
|
async with open_sqlite_checkpointer(config) as checkpointer:
|
|
@@ -607,19 +675,21 @@ async def run_async(args: argparse.Namespace) -> int:
|
|
|
607
675
|
sys.stdout.flush()
|
|
608
676
|
resume_value = {"answers": [args.query_text]}
|
|
609
677
|
if args.no_stream:
|
|
610
|
-
await invoke_query(
|
|
678
|
+
result = await invoke_query(
|
|
611
679
|
agent,
|
|
612
680
|
args.query_text,
|
|
613
681
|
thread_id=thread_id,
|
|
614
682
|
resume_value=resume_value,
|
|
683
|
+
timeout_s=timeout_s,
|
|
615
684
|
)
|
|
616
685
|
else:
|
|
617
|
-
await stream_query(
|
|
686
|
+
result = await stream_query(
|
|
618
687
|
agent,
|
|
619
688
|
args.query_text,
|
|
620
689
|
thread_id=thread_id,
|
|
621
690
|
show_tool_calls=args.verbose,
|
|
622
691
|
resume_value=resume_value,
|
|
692
|
+
timeout_s=timeout_s,
|
|
623
693
|
)
|
|
624
694
|
except ConcurrentSessionError as exc:
|
|
625
695
|
sys.stderr.write(f"error: {exc}\n")
|
|
@@ -627,6 +697,26 @@ async def run_async(args: argparse.Namespace) -> int:
|
|
|
627
697
|
except KeyboardInterrupt:
|
|
628
698
|
sys.stderr.write("\ninterrupted\n")
|
|
629
699
|
return 130
|
|
700
|
+
except asyncio.CancelledError:
|
|
701
|
+
sig = _interrupt_signal
|
|
702
|
+
if sig is not None:
|
|
703
|
+
name = signal.Signals(sig).name
|
|
704
|
+
sys.stderr.write(f"\ninterrupted by {name} (signal {sig})\n")
|
|
705
|
+
return 128 + sig
|
|
706
|
+
sys.stderr.write("error: run cancelled\n")
|
|
707
|
+
return 130
|
|
708
|
+
except BrokenPipeError:
|
|
709
|
+
# Our stdout consumer (e.g. ``fj q | head``) closed early. The final
|
|
710
|
+
# flush guard in ``main`` silences the shutdown-time noise.
|
|
711
|
+
sys.stderr.write("error: output pipe closed\n")
|
|
712
|
+
return 141
|
|
713
|
+
except SystemExit as exc:
|
|
714
|
+
# A plugin/tool called sys.exit() mid-run; without this the process
|
|
715
|
+
# would exit with no message at all.
|
|
716
|
+
sys.stderr.write(
|
|
717
|
+
f"error: unexpected exit({exc.code}) raised during the run (plugin/tool called sys.exit)\n"
|
|
718
|
+
)
|
|
719
|
+
return 1
|
|
630
720
|
except Exception as exc:
|
|
631
721
|
from flowjet.cli.stream import write_cli_error
|
|
632
722
|
|
|
@@ -639,6 +729,10 @@ async def run_async(args: argparse.Namespace) -> int:
|
|
|
639
729
|
append_history(args.query_text)
|
|
640
730
|
except Exception:
|
|
641
731
|
pass
|
|
732
|
+
if not result:
|
|
733
|
+
# Completed but produced no answer text (a warning is already on
|
|
734
|
+
# stderr) — scripts need a distinct, detectable exit code.
|
|
735
|
+
return 3
|
|
642
736
|
return 0
|
|
643
737
|
|
|
644
738
|
|
|
@@ -646,6 +740,55 @@ async def run_async(args: argparse.Namespace) -> int:
|
|
|
646
740
|
# closed anyway.
|
|
647
741
|
_TEARDOWN_GRACE_S = 3.0
|
|
648
742
|
|
|
743
|
+
# Signal (SIGTERM/SIGHUP) that cancelled the current run, if any. Module-level
|
|
744
|
+
# because the loop-installed callback and `run_async` share the main thread.
|
|
745
|
+
_interrupt_signal: int | None = None
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _on_interrupt_signal(sig: int, task: asyncio.Task[int]) -> None:
|
|
749
|
+
# Convert SIGTERM/SIGHUP into a graceful task cancel (recorded for the exit code).
|
|
750
|
+
global _interrupt_signal
|
|
751
|
+
if _interrupt_signal is None:
|
|
752
|
+
_interrupt_signal = int(sig)
|
|
753
|
+
if not task.done():
|
|
754
|
+
task.cancel()
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _install_signal_cancellation(
|
|
758
|
+
loop: asyncio.AbstractEventLoop,
|
|
759
|
+
task: asyncio.Task[int],
|
|
760
|
+
) -> list[tuple[int, Any]]:
|
|
761
|
+
# Route SIGTERM/SIGHUP through the loop so the run unwinds cleanly.
|
|
762
|
+
#
|
|
763
|
+
# Without this the default disposition kills the process instantly — no
|
|
764
|
+
# cleanup, no message (the "no message and exit" symptom). Platforms
|
|
765
|
+
# without add_signal_handler (Windows) or non-main-thread callers keep
|
|
766
|
+
# the default behaviour.
|
|
767
|
+
installed: list[tuple[int, Any]] = []
|
|
768
|
+
for name in ("SIGTERM", "SIGHUP"):
|
|
769
|
+
sig = getattr(signal, name, None)
|
|
770
|
+
if sig is None:
|
|
771
|
+
continue
|
|
772
|
+
previous = signal.getsignal(sig)
|
|
773
|
+
try:
|
|
774
|
+
loop.add_signal_handler(sig, _on_interrupt_signal, sig, task)
|
|
775
|
+
except (NotImplementedError, RuntimeError, ValueError, OSError):
|
|
776
|
+
continue
|
|
777
|
+
installed.append((int(sig), previous))
|
|
778
|
+
return installed
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _remove_signal_cancellation(
|
|
782
|
+
loop: asyncio.AbstractEventLoop,
|
|
783
|
+
installed: list[tuple[int, Any]],
|
|
784
|
+
) -> None:
|
|
785
|
+
for sig, previous in installed:
|
|
786
|
+
with contextlib.suppress(Exception, asyncio.CancelledError):
|
|
787
|
+
loop.remove_signal_handler(signal.Signals(sig))
|
|
788
|
+
if previous not in (signal.SIG_DFL, None):
|
|
789
|
+
with contextlib.suppress(Exception, asyncio.CancelledError):
|
|
790
|
+
signal.signal(signal.Signals(sig), previous) # type: ignore[arg-type]
|
|
791
|
+
|
|
649
792
|
|
|
650
793
|
async def _cancel_stray_tasks(loop: asyncio.AbstractEventLoop, grace: float) -> None:
|
|
651
794
|
"""Cancel every task but this one, and close async generators, within ``grace``.
|
|
@@ -670,13 +813,24 @@ def run_one_shot(coro: Coroutine[Any, Any, int], *, grace: float = _TEARDOWN_GRA
|
|
|
670
813
|
one task that swallows ``CancelledError`` pins the process after the answer
|
|
671
814
|
has already been printed. ``loop.close()`` is also where libraries install
|
|
672
815
|
their own cleanup hooks, so it runs whether or not the strays went quietly.
|
|
816
|
+
|
|
817
|
+
SIGTERM/SIGHUP cancel the main task instead of killing the process, so the
|
|
818
|
+
run unwinds with a visible message and a conventional 128+signal exit code.
|
|
673
819
|
"""
|
|
820
|
+
global _interrupt_signal
|
|
821
|
+
_interrupt_signal = None
|
|
674
822
|
loop = asyncio.new_event_loop()
|
|
823
|
+
installed: list[tuple[int, Any]] = []
|
|
675
824
|
try:
|
|
676
825
|
asyncio.set_event_loop(loop)
|
|
677
|
-
|
|
826
|
+
task = loop.create_task(coro, name="fj-run")
|
|
827
|
+
installed = _install_signal_cancellation(loop, task)
|
|
828
|
+
return loop.run_until_complete(task)
|
|
678
829
|
finally:
|
|
679
|
-
|
|
830
|
+
_remove_signal_cancellation(loop, installed)
|
|
831
|
+
# CancelledError is a BaseException — a stray re-cancel during teardown
|
|
832
|
+
# must not mask the real result/exception.
|
|
833
|
+
with contextlib.suppress(Exception, asyncio.CancelledError):
|
|
680
834
|
loop.run_until_complete(_cancel_stray_tasks(loop, grace))
|
|
681
835
|
asyncio.set_event_loop(None)
|
|
682
836
|
loop.close()
|
|
@@ -702,6 +856,15 @@ def main_follow(argv: list[str] | None = None) -> int:
|
|
|
702
856
|
return main(_inject_follow(raw))
|
|
703
857
|
|
|
704
858
|
|
|
859
|
+
def _handle_broken_pipe() -> int:
|
|
860
|
+
"""Silence the shutdown flush noise after our stdout consumer closed early."""
|
|
861
|
+
try:
|
|
862
|
+
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
|
|
863
|
+
except (OSError, ValueError, AttributeError): # pragma: no cover - platform
|
|
864
|
+
pass
|
|
865
|
+
return 141 # 128 + SIGPIPE
|
|
866
|
+
|
|
867
|
+
|
|
705
868
|
def main(argv: list[str] | None = None) -> int:
|
|
706
869
|
# Silence noisy third-party warnings (requests version-mismatch,
|
|
707
870
|
# LangChain beta-API) before any heavy import pulls them in.
|
|
@@ -721,33 +884,43 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
721
884
|
if args.command == "setup":
|
|
722
885
|
from flowjet.cli.setup_cmd import run_setup
|
|
723
886
|
|
|
724
|
-
|
|
725
|
-
|
|
887
|
+
result = run_setup(getattr(args, "config", None))
|
|
888
|
+
elif args.command == "doctor":
|
|
726
889
|
from flowjet.cli.doctor_cmd import run_doctor
|
|
727
890
|
|
|
728
|
-
|
|
729
|
-
|
|
891
|
+
result = run_doctor(getattr(args, "doctor_argv", []))
|
|
892
|
+
elif args.command == "doctor-fix":
|
|
730
893
|
from flowjet.cli.doctor_fix_cmd import run_doctor_fix
|
|
731
894
|
|
|
732
|
-
|
|
733
|
-
|
|
895
|
+
result = run_doctor_fix(getattr(args, "doctor_fix_argv", []))
|
|
896
|
+
elif args.command == "__complete":
|
|
734
897
|
from flowjet.cli.completion import run_complete
|
|
735
898
|
|
|
736
|
-
|
|
737
|
-
|
|
899
|
+
result = run_complete(getattr(args, "complete_argv", []))
|
|
900
|
+
elif args.command == "serve":
|
|
738
901
|
# Lazy: the server extra (fastapi/uvicorn/acp) must never be an
|
|
739
902
|
# import-time dependency of the CLI.
|
|
740
903
|
from flowjet.cli.serve_cmd import run_serve
|
|
741
904
|
|
|
742
|
-
|
|
743
|
-
|
|
905
|
+
result = run_serve(getattr(args, "serve_argv", []))
|
|
906
|
+
elif args.command == "completion":
|
|
744
907
|
from flowjet.cli.completion import run_completion_script
|
|
745
908
|
|
|
746
|
-
|
|
747
|
-
|
|
909
|
+
result = run_completion_script(getattr(args, "completion_argv", []))
|
|
910
|
+
else:
|
|
911
|
+
result = run_one_shot(run_async(args))
|
|
748
912
|
except KeyboardInterrupt:
|
|
749
913
|
sys.stderr.write("\ninterrupted\n")
|
|
750
914
|
return 130
|
|
915
|
+
except BrokenPipeError:
|
|
916
|
+
return _handle_broken_pipe()
|
|
917
|
+
try:
|
|
918
|
+
# Flush before returning: a closed consumer would otherwise trigger
|
|
919
|
+
# "Exception ignored" noise during interpreter shutdown.
|
|
920
|
+
sys.stdout.flush()
|
|
921
|
+
except BrokenPipeError:
|
|
922
|
+
return _handle_broken_pipe()
|
|
923
|
+
return result
|
|
751
924
|
|
|
752
925
|
|
|
753
926
|
if __name__ == "__main__": # pragma: no cover
|